Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 1 | # Copyright (c) 2014 The Chromium OS Authors. All rights reserved. |
| 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
| 4 | |
| 5 | """Model extensions common to both the server and client rdb modules. |
| 6 | """ |
| 7 | |
| 8 | |
| 9 | from django.core import exceptions as django_exceptions |
| 10 | from django.db import models as dbmodels |
| 11 | |
| 12 | |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 13 | from autotest_lib.client.common_lib import host_protections |
Fang Deng | 042c147 | 2014-10-23 13:56:41 -0700 | [diff] [blame] | 14 | from autotest_lib.client.common_lib import host_states |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 15 | from autotest_lib.frontend import settings |
| 16 | |
| 17 | |
| 18 | class ModelValidators(object): |
| 19 | """Convenience functions for model validation. |
| 20 | |
| 21 | This model is duplicated both on the client and server rdb. Any method |
| 22 | added to this class must only be capable of class level validation of model |
| 23 | fields, since anything else is meaningless on the client side. |
| 24 | """ |
| 25 | # TODO: at least some of these functions really belong in a custom |
| 26 | # Manager class. |
| 27 | |
| 28 | field_dict = None |
| 29 | # subclasses should override if they want to support smart_get() by name |
| 30 | name_field = None |
| 31 | |
| 32 | @classmethod |
| 33 | def get_field_dict(cls): |
| 34 | if cls.field_dict is None: |
| 35 | cls.field_dict = {} |
| 36 | for field in cls._meta.fields: |
| 37 | cls.field_dict[field.name] = field |
| 38 | return cls.field_dict |
| 39 | |
| 40 | |
| 41 | @classmethod |
| 42 | def clean_foreign_keys(cls, data): |
| 43 | """\ |
| 44 | -Convert foreign key fields in data from <field>_id to just |
| 45 | <field>. |
| 46 | -replace foreign key objects with their IDs |
| 47 | This method modifies data in-place. |
| 48 | """ |
| 49 | for field in cls._meta.fields: |
| 50 | if not field.rel: |
| 51 | continue |
| 52 | if (field.attname != field.name and |
| 53 | field.attname in data): |
| 54 | data[field.name] = data[field.attname] |
| 55 | del data[field.attname] |
| 56 | if field.name not in data: |
| 57 | continue |
| 58 | value = data[field.name] |
| 59 | if isinstance(value, dbmodels.Model): |
| 60 | data[field.name] = value._get_pk_val() |
| 61 | |
| 62 | |
| 63 | @classmethod |
| 64 | def _convert_booleans(cls, data): |
| 65 | """ |
| 66 | Ensure BooleanFields actually get bool values. The Django MySQL |
| 67 | backend returns ints for BooleanFields, which is almost always not |
| 68 | a problem, but it can be annoying in certain situations. |
| 69 | """ |
| 70 | for field in cls._meta.fields: |
| 71 | if type(field) == dbmodels.BooleanField and field.name in data: |
| 72 | data[field.name] = bool(data[field.name]) |
| 73 | |
| 74 | |
| 75 | # TODO(showard) - is there a way to not have to do this? |
| 76 | @classmethod |
| 77 | def provide_default_values(cls, data): |
| 78 | """\ |
| 79 | Provide default values for fields with default values which have |
| 80 | nothing passed in. |
| 81 | |
| 82 | For CharField and TextField fields with "blank=True", if nothing |
| 83 | is passed, we fill in an empty string value, even if there's no |
| 84 | :retab default set. |
| 85 | """ |
| 86 | new_data = dict(data) |
| 87 | field_dict = cls.get_field_dict() |
| 88 | for name, obj in field_dict.iteritems(): |
| 89 | if data.get(name) is not None: |
| 90 | continue |
| 91 | if obj.default is not dbmodels.fields.NOT_PROVIDED: |
| 92 | new_data[name] = obj.default |
| 93 | elif (isinstance(obj, dbmodels.CharField) or |
| 94 | isinstance(obj, dbmodels.TextField)): |
| 95 | new_data[name] = '' |
| 96 | return new_data |
| 97 | |
| 98 | |
| 99 | @classmethod |
| 100 | def validate_field_names(cls, data): |
| 101 | 'Checks for extraneous fields in data.' |
| 102 | errors = {} |
| 103 | field_dict = cls.get_field_dict() |
| 104 | for field_name in data: |
| 105 | if field_name not in field_dict: |
| 106 | errors[field_name] = 'No field of this name' |
| 107 | return errors |
| 108 | |
| 109 | |
| 110 | @classmethod |
| 111 | def prepare_data_args(cls, data): |
| 112 | 'Common preparation for add_object and update_object' |
| 113 | # must check for extraneous field names here, while we have the |
| 114 | # data in a dict |
| 115 | errors = cls.validate_field_names(data) |
| 116 | if errors: |
| 117 | raise django_exceptions.ValidationError(errors) |
| 118 | return data |
| 119 | |
| 120 | |
| 121 | @classmethod |
| 122 | def _get_required_field_names(cls): |
| 123 | """Get the fields without which we cannot create a host. |
| 124 | |
| 125 | @return: A list of field names that cannot be blank on host creation. |
| 126 | """ |
| 127 | return [field.name for field in cls._meta.fields if not field.blank] |
| 128 | |
| 129 | |
| 130 | @classmethod |
| 131 | def get_basic_field_names(cls): |
| 132 | """Get all basic fields of the Model. |
| 133 | |
| 134 | This method returns the names of all fields that the client can provide |
| 135 | a value for during host creation. The fields not included in this list |
Allen Li | e4c0827 | 2017-02-01 16:40:53 -0800 | [diff] [blame] | 136 | are those that we can leave blank. Specifying non-null values for such |
| 137 | fields only makes sense as an update to the host. |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 138 | |
| 139 | @return A list of basic fields. |
Allen Li | e4c0827 | 2017-02-01 16:40:53 -0800 | [diff] [blame] | 140 | Eg: set([hostname, locked, leased, status, invalid, |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 141 | protection, lock_time, dirty]) |
| 142 | """ |
| 143 | return [field.name for field in cls._meta.fields |
| 144 | if field.has_default()] + cls._get_required_field_names() |
| 145 | |
| 146 | |
| 147 | @classmethod |
| 148 | def validate_model_fields(cls, data): |
| 149 | """Validate parameters needed to create a host. |
| 150 | |
| 151 | Check that all required fields are specified, that specified fields |
| 152 | are actual model values, and provide defaults for the unspecified |
| 153 | but unrequired fields. |
| 154 | |
| 155 | @param dict: A dictionary with the args to create the model. |
| 156 | |
| 157 | @raises dajngo_exceptions.ValidationError: If either an invalid field |
| 158 | is specified or a required field is missing. |
| 159 | """ |
| 160 | missing_fields = set(cls._get_required_field_names()) - set(data.keys()) |
| 161 | if missing_fields: |
| 162 | raise django_exceptions.ValidationError('%s required to create %s, ' |
| 163 | 'supplied %s ' % (missing_fields, cls.__name__, data)) |
| 164 | data = cls.prepare_data_args(data) |
| 165 | data = cls.provide_default_values(data) |
| 166 | return data |
| 167 | |
| 168 | |
| 169 | class AbstractHostModel(dbmodels.Model, ModelValidators): |
| 170 | """Abstract model specifying all fields one can use to create a host. |
| 171 | |
| 172 | This model enforces consistency between the host models of the rdb and |
| 173 | their representation on the client side. |
| 174 | |
| 175 | Internal fields: |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 176 | status: string describing status of host |
| 177 | invalid: true if the host has been deleted |
| 178 | protection: indicates what can be done to this host during repair |
| 179 | lock_time: DateTime at which the host was locked |
| 180 | dirty: true if the host has been used without being rebooted |
MK Ryu | 53cc141 | 2015-10-07 16:48:38 -0700 | [diff] [blame] | 181 | lock_reason: The reason for locking the host. |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 182 | """ |
Fang Deng | 042c147 | 2014-10-23 13:56:41 -0700 | [diff] [blame] | 183 | Status = host_states.Status |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 184 | hostname = dbmodels.CharField(max_length=255, unique=True) |
| 185 | locked = dbmodels.BooleanField(default=False) |
| 186 | leased = dbmodels.BooleanField(default=True) |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 187 | status = dbmodels.CharField(max_length=255, default=Status.READY, |
| 188 | choices=Status.choices(), |
| 189 | editable=settings.FULL_ADMIN) |
| 190 | invalid = dbmodels.BooleanField(default=False, |
| 191 | editable=settings.FULL_ADMIN) |
| 192 | protection = dbmodels.SmallIntegerField(null=False, blank=True, |
| 193 | choices=host_protections.choices, |
| 194 | default=host_protections.default) |
| 195 | lock_time = dbmodels.DateTimeField(null=True, blank=True, editable=False) |
| 196 | dirty = dbmodels.BooleanField(default=True, editable=settings.FULL_ADMIN) |
MK Ryu | 53cc141 | 2015-10-07 16:48:38 -0700 | [diff] [blame] | 197 | lock_reason = dbmodels.CharField(null=True, max_length=255, blank=True, |
| 198 | default='') |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 199 | |
| 200 | |
| 201 | class Meta: |
| 202 | abstract = True |