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 9from django.core import exceptions as django_exceptions 10from django.db import models as dbmodels 11 12 13from autotest_lib.client.common_lib import host_protections 14from autotest_lib.client.common_lib import host_states 15from autotest_lib.frontend import settings 16 17 18class 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 136 are those that we can leave blank, such as synch_id. Specifying non 137 null values for such fields only makes sense as an update to the host. 138 139 @return A list of basic fields. 140 Eg: set([hostname, locked, leased, synch_id, status, invalid, 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 169class 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: 176 synch_id: currently unused 177 status: string describing status of host 178 invalid: true if the host has been deleted 179 protection: indicates what can be done to this host during repair 180 lock_time: DateTime at which the host was locked 181 dirty: true if the host has been used without being rebooted 182 lock_reason: The reason for locking the host. 183 """ 184 Status = host_states.Status 185 hostname = dbmodels.CharField(max_length=255, unique=True) 186 locked = dbmodels.BooleanField(default=False) 187 leased = dbmodels.BooleanField(default=True) 188 synch_id = dbmodels.IntegerField(blank=True, null=True, 189 editable=settings.FULL_ADMIN) 190 status = dbmodels.CharField(max_length=255, default=Status.READY, 191 choices=Status.choices(), 192 editable=settings.FULL_ADMIN) 193 invalid = dbmodels.BooleanField(default=False, 194 editable=settings.FULL_ADMIN) 195 protection = dbmodels.SmallIntegerField(null=False, blank=True, 196 choices=host_protections.choices, 197 default=host_protections.default) 198 lock_time = dbmodels.DateTimeField(null=True, blank=True, editable=False) 199 dirty = dbmodels.BooleanField(default=True, editable=settings.FULL_ADMIN) 200 lock_reason = dbmodels.CharField(null=True, max_length=255, blank=True, 201 default='') 202 203 204 class Meta: 205 abstract = True 206