Home | History | Annotate | Download | only in afe
      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 
     13 from autotest_lib.client.common_lib import host_protections
     14 from autotest_lib.client.common_lib import host_states
     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
    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.
    138 
    139         @return A list of basic fields.
    140             Eg: set([hostname, locked, leased, 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 
    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:
    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
    181         lock_reason: The reason for locking the host.
    182     """
    183     Status = host_states.Status
    184     hostname = dbmodels.CharField(max_length=255, unique=True)
    185     locked = dbmodels.BooleanField(default=False)
    186     leased = dbmodels.BooleanField(default=True)
    187     # TODO(ayatane): This is needed until synch_id is removed from Host._fields
    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