1*9c5db199SXin Li# pylint: disable=missing-docstring 2*9c5db199SXin Li 3*9c5db199SXin Liimport contextlib 4*9c5db199SXin Lifrom datetime import datetime 5*9c5db199SXin Lifrom datetime import timedelta 6*9c5db199SXin Liimport logging 7*9c5db199SXin Liimport os 8*9c5db199SXin Li 9*9c5db199SXin Liimport django.core 10*9c5db199SXin Liimport six 11*9c5db199SXin Litry: 12*9c5db199SXin Li from django.db import models as dbmodels, connection 13*9c5db199SXin Liexcept django.core.exceptions.ImproperlyConfigured: 14*9c5db199SXin Li raise ImportError('Django database not yet configured. Import either ' 15*9c5db199SXin Li 'setup_django_environment or ' 16*9c5db199SXin Li 'setup_django_lite_environment from ' 17*9c5db199SXin Li 'autotest_lib.frontend before any imports that ' 18*9c5db199SXin Li 'depend on django models.') 19*9c5db199SXin Lifrom xml.sax import saxutils 20*9c5db199SXin Liimport common 21*9c5db199SXin Lifrom autotest_lib.frontend.afe import model_logic, model_attributes 22*9c5db199SXin Lifrom autotest_lib.frontend.afe import rdb_model_extensions 23*9c5db199SXin Lifrom autotest_lib.frontend import settings, thread_local 24*9c5db199SXin Lifrom autotest_lib.client.common_lib import autotest_enum, error, host_protections 25*9c5db199SXin Lifrom autotest_lib.client.common_lib import global_config 26*9c5db199SXin Lifrom autotest_lib.client.common_lib import host_queue_entry_states 27*9c5db199SXin Lifrom autotest_lib.client.common_lib import control_data, priorities, decorators 28*9c5db199SXin Lifrom autotest_lib.server import utils as server_utils 29*9c5db199SXin Li 30*9c5db199SXin Li# job options and user preferences 31*9c5db199SXin LiDEFAULT_REBOOT_BEFORE = model_attributes.RebootBefore.IF_DIRTY 32*9c5db199SXin LiDEFAULT_REBOOT_AFTER = model_attributes.RebootBefore.NEVER 33*9c5db199SXin Li 34*9c5db199SXin LiRESPECT_STATIC_LABELS = global_config.global_config.get_config_value( 35*9c5db199SXin Li 'SKYLAB', 'respect_static_labels', type=bool, default=False) 36*9c5db199SXin Li 37*9c5db199SXin LiRESPECT_STATIC_ATTRIBUTES = global_config.global_config.get_config_value( 38*9c5db199SXin Li 'SKYLAB', 'respect_static_attributes', type=bool, default=False) 39*9c5db199SXin Li 40*9c5db199SXin Li 41*9c5db199SXin Liclass AclAccessViolation(Exception): 42*9c5db199SXin Li """\ 43*9c5db199SXin Li Raised when an operation is attempted with proper permissions as 44*9c5db199SXin Li dictated by ACLs. 45*9c5db199SXin Li """ 46*9c5db199SXin Li 47*9c5db199SXin Li 48*9c5db199SXin Liclass AtomicGroup(model_logic.ModelWithInvalid, dbmodels.Model): 49*9c5db199SXin Li """\ 50*9c5db199SXin Li An atomic group defines a collection of hosts which must only be scheduled 51*9c5db199SXin Li all at once. Any host with a label having an atomic group will only be 52*9c5db199SXin Li scheduled for a job at the same time as other hosts sharing that label. 53*9c5db199SXin Li 54*9c5db199SXin Li Required: 55*9c5db199SXin Li name: A name for this atomic group, e.g. 'rack23' or 'funky_net'. 56*9c5db199SXin Li max_number_of_machines: The maximum number of machines that will be 57*9c5db199SXin Li scheduled at once when scheduling jobs to this atomic group. 58*9c5db199SXin Li The job.synch_count is considered the minimum. 59*9c5db199SXin Li 60*9c5db199SXin Li Optional: 61*9c5db199SXin Li description: Arbitrary text description of this group's purpose. 62*9c5db199SXin Li """ 63*9c5db199SXin Li name = dbmodels.CharField(max_length=255, unique=True) 64*9c5db199SXin Li description = dbmodels.TextField(blank=True) 65*9c5db199SXin Li # This magic value is the default to simplify the scheduler logic. 66*9c5db199SXin Li # It must be "large". The common use of atomic groups is to want all 67*9c5db199SXin Li # machines in the group to be used, limits on which subset used are 68*9c5db199SXin Li # often chosen via dependency labels. 69*9c5db199SXin Li # TODO(dennisjeffrey): Revisit this so we don't have to assume that 70*9c5db199SXin Li # "infinity" is around 3.3 million. 71*9c5db199SXin Li INFINITE_MACHINES = 333333333 72*9c5db199SXin Li max_number_of_machines = dbmodels.IntegerField(default=INFINITE_MACHINES) 73*9c5db199SXin Li invalid = dbmodels.BooleanField(default=False, 74*9c5db199SXin Li editable=settings.FULL_ADMIN) 75*9c5db199SXin Li 76*9c5db199SXin Li name_field = 'name' 77*9c5db199SXin Li objects = model_logic.ModelWithInvalidManager() 78*9c5db199SXin Li valid_objects = model_logic.ValidObjectsManager() 79*9c5db199SXin Li 80*9c5db199SXin Li 81*9c5db199SXin Li def enqueue_job(self, job, is_template=False): 82*9c5db199SXin Li """Enqueue a job on an associated atomic group of hosts. 83*9c5db199SXin Li 84*9c5db199SXin Li @param job: A job to enqueue. 85*9c5db199SXin Li @param is_template: Whether the status should be "Template". 86*9c5db199SXin Li """ 87*9c5db199SXin Li queue_entry = HostQueueEntry.create(atomic_group=self, job=job, 88*9c5db199SXin Li is_template=is_template) 89*9c5db199SXin Li queue_entry.save() 90*9c5db199SXin Li 91*9c5db199SXin Li 92*9c5db199SXin Li def clean_object(self): 93*9c5db199SXin Li self.label_set.clear() 94*9c5db199SXin Li 95*9c5db199SXin Li 96*9c5db199SXin Li class Meta: 97*9c5db199SXin Li """Metadata for class AtomicGroup.""" 98*9c5db199SXin Li db_table = 'afe_atomic_groups' 99*9c5db199SXin Li 100*9c5db199SXin Li 101*9c5db199SXin Li def __unicode__(self): 102*9c5db199SXin Li return unicode(self.name) 103*9c5db199SXin Li 104*9c5db199SXin Li 105*9c5db199SXin Liclass Label(model_logic.ModelWithInvalid, dbmodels.Model): 106*9c5db199SXin Li """\ 107*9c5db199SXin Li Required: 108*9c5db199SXin Li name: label name 109*9c5db199SXin Li 110*9c5db199SXin Li Optional: 111*9c5db199SXin Li kernel_config: URL/path to kernel config for jobs run on this label. 112*9c5db199SXin Li platform: If True, this is a platform label (defaults to False). 113*9c5db199SXin Li only_if_needed: If True, a Host with this label can only be used if that 114*9c5db199SXin Li label is requested by the job/test (either as the meta_host or 115*9c5db199SXin Li in the job_dependencies). 116*9c5db199SXin Li atomic_group: The atomic group associated with this label. 117*9c5db199SXin Li """ 118*9c5db199SXin Li name = dbmodels.CharField(max_length=255, unique=True) 119*9c5db199SXin Li kernel_config = dbmodels.CharField(max_length=255, blank=True) 120*9c5db199SXin Li platform = dbmodels.BooleanField(default=False) 121*9c5db199SXin Li invalid = dbmodels.BooleanField(default=False, 122*9c5db199SXin Li editable=settings.FULL_ADMIN) 123*9c5db199SXin Li only_if_needed = dbmodels.BooleanField(default=False) 124*9c5db199SXin Li 125*9c5db199SXin Li name_field = 'name' 126*9c5db199SXin Li objects = model_logic.ModelWithInvalidManager() 127*9c5db199SXin Li valid_objects = model_logic.ValidObjectsManager() 128*9c5db199SXin Li atomic_group = dbmodels.ForeignKey(AtomicGroup, null=True, blank=True) 129*9c5db199SXin Li 130*9c5db199SXin Li 131*9c5db199SXin Li def clean_object(self): 132*9c5db199SXin Li self.host_set.clear() 133*9c5db199SXin Li self.test_set.clear() 134*9c5db199SXin Li 135*9c5db199SXin Li 136*9c5db199SXin Li def enqueue_job(self, job, is_template=False): 137*9c5db199SXin Li """Enqueue a job on any host of this label. 138*9c5db199SXin Li 139*9c5db199SXin Li @param job: A job to enqueue. 140*9c5db199SXin Li @param is_template: Whether the status should be "Template". 141*9c5db199SXin Li """ 142*9c5db199SXin Li queue_entry = HostQueueEntry.create(meta_host=self, job=job, 143*9c5db199SXin Li is_template=is_template) 144*9c5db199SXin Li queue_entry.save() 145*9c5db199SXin Li 146*9c5db199SXin Li 147*9c5db199SXin Li 148*9c5db199SXin Li class Meta: 149*9c5db199SXin Li """Metadata for class Label.""" 150*9c5db199SXin Li db_table = 'afe_labels' 151*9c5db199SXin Li 152*9c5db199SXin Li 153*9c5db199SXin Li def __unicode__(self): 154*9c5db199SXin Li return unicode(self.name) 155*9c5db199SXin Li 156*9c5db199SXin Li 157*9c5db199SXin Li def is_replaced_by_static(self): 158*9c5db199SXin Li """Detect whether a label is replaced by a static label. 159*9c5db199SXin Li 160*9c5db199SXin Li 'Static' means it can only be modified by skylab inventory tools. 161*9c5db199SXin Li """ 162*9c5db199SXin Li if RESPECT_STATIC_LABELS: 163*9c5db199SXin Li replaced = ReplacedLabel.objects.filter(label__id=self.id) 164*9c5db199SXin Li if len(replaced) > 0: 165*9c5db199SXin Li return True 166*9c5db199SXin Li 167*9c5db199SXin Li return False 168*9c5db199SXin Li 169*9c5db199SXin Li 170*9c5db199SXin Liclass StaticLabel(model_logic.ModelWithInvalid, dbmodels.Model): 171*9c5db199SXin Li """\ 172*9c5db199SXin Li Required: 173*9c5db199SXin Li name: label name 174*9c5db199SXin Li 175*9c5db199SXin Li Optional: 176*9c5db199SXin Li kernel_config: URL/path to kernel config for jobs run on this label. 177*9c5db199SXin Li platform: If True, this is a platform label (defaults to False). 178*9c5db199SXin Li only_if_needed: Deprecated. This is always False. 179*9c5db199SXin Li atomic_group: Deprecated. This is always NULL. 180*9c5db199SXin Li """ 181*9c5db199SXin Li name = dbmodels.CharField(max_length=255, unique=True) 182*9c5db199SXin Li kernel_config = dbmodels.CharField(max_length=255, blank=True) 183*9c5db199SXin Li platform = dbmodels.BooleanField(default=False) 184*9c5db199SXin Li invalid = dbmodels.BooleanField(default=False, 185*9c5db199SXin Li editable=settings.FULL_ADMIN) 186*9c5db199SXin Li only_if_needed = dbmodels.BooleanField(default=False) 187*9c5db199SXin Li 188*9c5db199SXin Li name_field = 'name' 189*9c5db199SXin Li objects = model_logic.ModelWithInvalidManager() 190*9c5db199SXin Li valid_objects = model_logic.ValidObjectsManager() 191*9c5db199SXin Li atomic_group = dbmodels.ForeignKey(AtomicGroup, null=True, blank=True) 192*9c5db199SXin Li 193*9c5db199SXin Li def clean_object(self): 194*9c5db199SXin Li self.host_set.clear() 195*9c5db199SXin Li self.test_set.clear() 196*9c5db199SXin Li 197*9c5db199SXin Li 198*9c5db199SXin Li class Meta: 199*9c5db199SXin Li """Metadata for class StaticLabel.""" 200*9c5db199SXin Li db_table = 'afe_static_labels' 201*9c5db199SXin Li 202*9c5db199SXin Li 203*9c5db199SXin Li def __unicode__(self): 204*9c5db199SXin Li return unicode(self.name) 205*9c5db199SXin Li 206*9c5db199SXin Li 207*9c5db199SXin Liclass ReplacedLabel(dbmodels.Model, model_logic.ModelExtensions): 208*9c5db199SXin Li """The tag to indicate Whether to replace labels with static labels.""" 209*9c5db199SXin Li label = dbmodels.ForeignKey(Label) 210*9c5db199SXin Li objects = model_logic.ExtendedManager() 211*9c5db199SXin Li 212*9c5db199SXin Li 213*9c5db199SXin Li class Meta: 214*9c5db199SXin Li """Metadata for class ReplacedLabel.""" 215*9c5db199SXin Li db_table = 'afe_replaced_labels' 216*9c5db199SXin Li 217*9c5db199SXin Li 218*9c5db199SXin Li def __unicode__(self): 219*9c5db199SXin Li return unicode(self.label) 220*9c5db199SXin Li 221*9c5db199SXin Li 222*9c5db199SXin Liclass Shard(dbmodels.Model, model_logic.ModelExtensions): 223*9c5db199SXin Li 224*9c5db199SXin Li hostname = dbmodels.CharField(max_length=255, unique=True) 225*9c5db199SXin Li 226*9c5db199SXin Li name_field = 'hostname' 227*9c5db199SXin Li 228*9c5db199SXin Li labels = dbmodels.ManyToManyField(Label, blank=True, 229*9c5db199SXin Li db_table='afe_shards_labels') 230*9c5db199SXin Li 231*9c5db199SXin Li class Meta: 232*9c5db199SXin Li """Metadata for class ParameterizedJob.""" 233*9c5db199SXin Li db_table = 'afe_shards' 234*9c5db199SXin Li 235*9c5db199SXin Li 236*9c5db199SXin Liclass Drone(dbmodels.Model, model_logic.ModelExtensions): 237*9c5db199SXin Li """ 238*9c5db199SXin Li A scheduler drone 239*9c5db199SXin Li 240*9c5db199SXin Li hostname: the drone's hostname 241*9c5db199SXin Li """ 242*9c5db199SXin Li hostname = dbmodels.CharField(max_length=255, unique=True) 243*9c5db199SXin Li 244*9c5db199SXin Li name_field = 'hostname' 245*9c5db199SXin Li objects = model_logic.ExtendedManager() 246*9c5db199SXin Li 247*9c5db199SXin Li 248*9c5db199SXin Li def save(self, *args, **kwargs): 249*9c5db199SXin Li if not User.current_user().is_superuser(): 250*9c5db199SXin Li raise Exception('Only superusers may edit drones') 251*9c5db199SXin Li super(Drone, self).save(*args, **kwargs) 252*9c5db199SXin Li 253*9c5db199SXin Li 254*9c5db199SXin Li def delete(self): 255*9c5db199SXin Li if not User.current_user().is_superuser(): 256*9c5db199SXin Li raise Exception('Only superusers may delete drones') 257*9c5db199SXin Li super(Drone, self).delete() 258*9c5db199SXin Li 259*9c5db199SXin Li 260*9c5db199SXin Li class Meta: 261*9c5db199SXin Li """Metadata for class Drone.""" 262*9c5db199SXin Li db_table = 'afe_drones' 263*9c5db199SXin Li 264*9c5db199SXin Li def __unicode__(self): 265*9c5db199SXin Li return unicode(self.hostname) 266*9c5db199SXin Li 267*9c5db199SXin Li 268*9c5db199SXin Liclass DroneSet(dbmodels.Model, model_logic.ModelExtensions): 269*9c5db199SXin Li """ 270*9c5db199SXin Li A set of scheduler drones 271*9c5db199SXin Li 272*9c5db199SXin Li These will be used by the scheduler to decide what drones a job is allowed 273*9c5db199SXin Li to run on. 274*9c5db199SXin Li 275*9c5db199SXin Li name: the drone set's name 276*9c5db199SXin Li drones: the drones that are part of the set 277*9c5db199SXin Li """ 278*9c5db199SXin Li DRONE_SETS_ENABLED = global_config.global_config.get_config_value( 279*9c5db199SXin Li 'SCHEDULER', 'drone_sets_enabled', type=bool, default=False) 280*9c5db199SXin Li DEFAULT_DRONE_SET_NAME = global_config.global_config.get_config_value( 281*9c5db199SXin Li 'SCHEDULER', 'default_drone_set_name', default=None) 282*9c5db199SXin Li 283*9c5db199SXin Li name = dbmodels.CharField(max_length=255, unique=True) 284*9c5db199SXin Li drones = dbmodels.ManyToManyField(Drone, db_table='afe_drone_sets_drones') 285*9c5db199SXin Li 286*9c5db199SXin Li name_field = 'name' 287*9c5db199SXin Li objects = model_logic.ExtendedManager() 288*9c5db199SXin Li 289*9c5db199SXin Li 290*9c5db199SXin Li def save(self, *args, **kwargs): 291*9c5db199SXin Li if not User.current_user().is_superuser(): 292*9c5db199SXin Li raise Exception('Only superusers may edit drone sets') 293*9c5db199SXin Li super(DroneSet, self).save(*args, **kwargs) 294*9c5db199SXin Li 295*9c5db199SXin Li 296*9c5db199SXin Li def delete(self): 297*9c5db199SXin Li if not User.current_user().is_superuser(): 298*9c5db199SXin Li raise Exception('Only superusers may delete drone sets') 299*9c5db199SXin Li super(DroneSet, self).delete() 300*9c5db199SXin Li 301*9c5db199SXin Li 302*9c5db199SXin Li @classmethod 303*9c5db199SXin Li def drone_sets_enabled(cls): 304*9c5db199SXin Li """Returns whether drone sets are enabled. 305*9c5db199SXin Li 306*9c5db199SXin Li @param cls: Implicit class object. 307*9c5db199SXin Li """ 308*9c5db199SXin Li return cls.DRONE_SETS_ENABLED 309*9c5db199SXin Li 310*9c5db199SXin Li 311*9c5db199SXin Li @classmethod 312*9c5db199SXin Li def default_drone_set_name(cls): 313*9c5db199SXin Li """Returns the default drone set name. 314*9c5db199SXin Li 315*9c5db199SXin Li @param cls: Implicit class object. 316*9c5db199SXin Li """ 317*9c5db199SXin Li return cls.DEFAULT_DRONE_SET_NAME 318*9c5db199SXin Li 319*9c5db199SXin Li 320*9c5db199SXin Li @classmethod 321*9c5db199SXin Li def get_default(cls): 322*9c5db199SXin Li """Gets the default drone set name, compatible with Job.add_object. 323*9c5db199SXin Li 324*9c5db199SXin Li @param cls: Implicit class object. 325*9c5db199SXin Li """ 326*9c5db199SXin Li return cls.smart_get(cls.DEFAULT_DRONE_SET_NAME) 327*9c5db199SXin Li 328*9c5db199SXin Li 329*9c5db199SXin Li @classmethod 330*9c5db199SXin Li def resolve_name(cls, drone_set_name): 331*9c5db199SXin Li """ 332*9c5db199SXin Li Returns the name of one of these, if not None, in order of preference: 333*9c5db199SXin Li 1) the drone set given, 334*9c5db199SXin Li 2) the current user's default drone set, or 335*9c5db199SXin Li 3) the global default drone set 336*9c5db199SXin Li 337*9c5db199SXin Li or returns None if drone sets are disabled 338*9c5db199SXin Li 339*9c5db199SXin Li @param cls: Implicit class object. 340*9c5db199SXin Li @param drone_set_name: A drone set name. 341*9c5db199SXin Li """ 342*9c5db199SXin Li if not cls.drone_sets_enabled(): 343*9c5db199SXin Li return None 344*9c5db199SXin Li 345*9c5db199SXin Li user = User.current_user() 346*9c5db199SXin Li user_drone_set_name = user.drone_set and user.drone_set.name 347*9c5db199SXin Li 348*9c5db199SXin Li return drone_set_name or user_drone_set_name or cls.get_default().name 349*9c5db199SXin Li 350*9c5db199SXin Li 351*9c5db199SXin Li def get_drone_hostnames(self): 352*9c5db199SXin Li """ 353*9c5db199SXin Li Gets the hostnames of all drones in this drone set 354*9c5db199SXin Li """ 355*9c5db199SXin Li return set(self.drones.all().values_list('hostname', flat=True)) 356*9c5db199SXin Li 357*9c5db199SXin Li 358*9c5db199SXin Li class Meta: 359*9c5db199SXin Li """Metadata for class DroneSet.""" 360*9c5db199SXin Li db_table = 'afe_drone_sets' 361*9c5db199SXin Li 362*9c5db199SXin Li def __unicode__(self): 363*9c5db199SXin Li return unicode(self.name) 364*9c5db199SXin Li 365*9c5db199SXin Li 366*9c5db199SXin Liclass User(dbmodels.Model, model_logic.ModelExtensions): 367*9c5db199SXin Li """\ 368*9c5db199SXin Li Required: 369*9c5db199SXin Li login :user login name 370*9c5db199SXin Li 371*9c5db199SXin Li Optional: 372*9c5db199SXin Li access_level: 0=User (default), 1=Admin, 100=Root 373*9c5db199SXin Li """ 374*9c5db199SXin Li ACCESS_ROOT = 100 375*9c5db199SXin Li ACCESS_ADMIN = 1 376*9c5db199SXin Li ACCESS_USER = 0 377*9c5db199SXin Li 378*9c5db199SXin Li AUTOTEST_SYSTEM = 'autotest_system' 379*9c5db199SXin Li 380*9c5db199SXin Li login = dbmodels.CharField(max_length=255, unique=True) 381*9c5db199SXin Li access_level = dbmodels.IntegerField(default=ACCESS_USER, blank=True) 382*9c5db199SXin Li 383*9c5db199SXin Li # user preferences 384*9c5db199SXin Li reboot_before = dbmodels.SmallIntegerField( 385*9c5db199SXin Li choices=model_attributes.RebootBefore.choices(), blank=True, 386*9c5db199SXin Li default=DEFAULT_REBOOT_BEFORE) 387*9c5db199SXin Li reboot_after = dbmodels.SmallIntegerField( 388*9c5db199SXin Li choices=model_attributes.RebootAfter.choices(), blank=True, 389*9c5db199SXin Li default=DEFAULT_REBOOT_AFTER) 390*9c5db199SXin Li drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True) 391*9c5db199SXin Li show_experimental = dbmodels.BooleanField(default=False) 392*9c5db199SXin Li 393*9c5db199SXin Li name_field = 'login' 394*9c5db199SXin Li objects = model_logic.ExtendedManager() 395*9c5db199SXin Li 396*9c5db199SXin Li 397*9c5db199SXin Li def save(self, *args, **kwargs): 398*9c5db199SXin Li # is this a new object being saved for the first time? 399*9c5db199SXin Li first_time = (self.id is None) 400*9c5db199SXin Li user = thread_local.get_user() 401*9c5db199SXin Li if user and not user.is_superuser() and user.login != self.login: 402*9c5db199SXin Li raise AclAccessViolation("You cannot modify user " + self.login) 403*9c5db199SXin Li super(User, self).save(*args, **kwargs) 404*9c5db199SXin Li if first_time: 405*9c5db199SXin Li everyone = AclGroup.objects.get(name='Everyone') 406*9c5db199SXin Li everyone.users.add(self) 407*9c5db199SXin Li 408*9c5db199SXin Li 409*9c5db199SXin Li def is_superuser(self): 410*9c5db199SXin Li """Returns whether the user has superuser access.""" 411*9c5db199SXin Li return self.access_level >= self.ACCESS_ROOT 412*9c5db199SXin Li 413*9c5db199SXin Li 414*9c5db199SXin Li @classmethod 415*9c5db199SXin Li def current_user(cls): 416*9c5db199SXin Li """Returns the current user. 417*9c5db199SXin Li 418*9c5db199SXin Li @param cls: Implicit class object. 419*9c5db199SXin Li """ 420*9c5db199SXin Li user = thread_local.get_user() 421*9c5db199SXin Li if user is None: 422*9c5db199SXin Li user, _ = cls.objects.get_or_create(login=cls.AUTOTEST_SYSTEM) 423*9c5db199SXin Li user.access_level = cls.ACCESS_ROOT 424*9c5db199SXin Li user.save() 425*9c5db199SXin Li return user 426*9c5db199SXin Li 427*9c5db199SXin Li 428*9c5db199SXin Li @classmethod 429*9c5db199SXin Li def get_record(cls, data): 430*9c5db199SXin Li """Check the database for an identical record. 431*9c5db199SXin Li 432*9c5db199SXin Li Check for a record with matching id and login. If one exists, 433*9c5db199SXin Li return it. If one does not exist there is a possibility that 434*9c5db199SXin Li the following cases have happened: 435*9c5db199SXin Li 1. Same id, different login 436*9c5db199SXin Li We received: "1 chromeos-test" 437*9c5db199SXin Li And we have: "1 debug-user" 438*9c5db199SXin Li In this case we need to delete "1 debug_user" and insert 439*9c5db199SXin Li "1 chromeos-test". 440*9c5db199SXin Li 441*9c5db199SXin Li 2. Same login, different id: 442*9c5db199SXin Li We received: "1 chromeos-test" 443*9c5db199SXin Li And we have: "2 chromeos-test" 444*9c5db199SXin Li In this case we need to delete "2 chromeos-test" and insert 445*9c5db199SXin Li "1 chromeos-test". 446*9c5db199SXin Li 447*9c5db199SXin Li As long as this method deletes bad records and raises the 448*9c5db199SXin Li DoesNotExist exception the caller will handle creating the 449*9c5db199SXin Li new record. 450*9c5db199SXin Li 451*9c5db199SXin Li @raises: DoesNotExist, if a record with the matching login and id 452*9c5db199SXin Li does not exist. 453*9c5db199SXin Li """ 454*9c5db199SXin Li 455*9c5db199SXin Li # Both the id and login should be uniqe but there are cases when 456*9c5db199SXin Li # we might already have a user with the same login/id because 457*9c5db199SXin Li # current_user will proactively create a user record if it doesn't 458*9c5db199SXin Li # exist. Since we want to avoid conflict between the main and 459*9c5db199SXin Li # shard, just delete any existing user records that don't match 460*9c5db199SXin Li # what we're about to deserialize from the main. 461*9c5db199SXin Li try: 462*9c5db199SXin Li return cls.objects.get(login=data['login'], id=data['id']) 463*9c5db199SXin Li except cls.DoesNotExist: 464*9c5db199SXin Li cls.delete_matching_record(login=data['login']) 465*9c5db199SXin Li cls.delete_matching_record(id=data['id']) 466*9c5db199SXin Li raise 467*9c5db199SXin Li 468*9c5db199SXin Li 469*9c5db199SXin Li class Meta: 470*9c5db199SXin Li """Metadata for class User.""" 471*9c5db199SXin Li db_table = 'afe_users' 472*9c5db199SXin Li 473*9c5db199SXin Li def __unicode__(self): 474*9c5db199SXin Li return unicode(self.login) 475*9c5db199SXin Li 476*9c5db199SXin Li 477*9c5db199SXin Liclass Host(model_logic.ModelWithInvalid, rdb_model_extensions.AbstractHostModel, 478*9c5db199SXin Li model_logic.ModelWithAttributes): 479*9c5db199SXin Li """\ 480*9c5db199SXin Li Required: 481*9c5db199SXin Li hostname 482*9c5db199SXin Li 483*9c5db199SXin Li optional: 484*9c5db199SXin Li locked: if true, host is locked and will not be queued 485*9c5db199SXin Li 486*9c5db199SXin Li Internal: 487*9c5db199SXin Li From AbstractHostModel: 488*9c5db199SXin Li status: string describing status of host 489*9c5db199SXin Li invalid: true if the host has been deleted 490*9c5db199SXin Li protection: indicates what can be done to this host during repair 491*9c5db199SXin Li lock_time: DateTime at which the host was locked 492*9c5db199SXin Li dirty: true if the host has been used without being rebooted 493*9c5db199SXin Li Local: 494*9c5db199SXin Li locked_by: user that locked the host, or null if the host is unlocked 495*9c5db199SXin Li """ 496*9c5db199SXin Li 497*9c5db199SXin Li SERIALIZATION_LINKS_TO_FOLLOW = set(['aclgroup_set', 498*9c5db199SXin Li 'hostattribute_set', 499*9c5db199SXin Li 'labels', 500*9c5db199SXin Li 'shard']) 501*9c5db199SXin Li SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['invalid']) 502*9c5db199SXin Li 503*9c5db199SXin Li 504*9c5db199SXin Li def custom_deserialize_relation(self, link, data): 505*9c5db199SXin Li assert link == 'shard', 'Link %s should not be deserialized' % link 506*9c5db199SXin Li self.shard = Shard.deserialize(data) 507*9c5db199SXin Li 508*9c5db199SXin Li 509*9c5db199SXin Li # Note: Only specify foreign keys here, specify host columns in 510*9c5db199SXin Li # rdb_model_extensions instead. 511*9c5db199SXin Li Protection = host_protections.Protection 512*9c5db199SXin Li labels = dbmodels.ManyToManyField(Label, blank=True, 513*9c5db199SXin Li db_table='afe_hosts_labels') 514*9c5db199SXin Li static_labels = dbmodels.ManyToManyField( 515*9c5db199SXin Li StaticLabel, blank=True, db_table='afe_static_hosts_labels') 516*9c5db199SXin Li locked_by = dbmodels.ForeignKey(User, null=True, blank=True, editable=False) 517*9c5db199SXin Li name_field = 'hostname' 518*9c5db199SXin Li objects = model_logic.ModelWithInvalidManager() 519*9c5db199SXin Li valid_objects = model_logic.ValidObjectsManager() 520*9c5db199SXin Li leased_objects = model_logic.LeasedHostManager() 521*9c5db199SXin Li 522*9c5db199SXin Li shard = dbmodels.ForeignKey(Shard, blank=True, null=True) 523*9c5db199SXin Li 524*9c5db199SXin Li def __init__(self, *args, **kwargs): 525*9c5db199SXin Li super(Host, self).__init__(*args, **kwargs) 526*9c5db199SXin Li self._record_attributes(['status']) 527*9c5db199SXin Li 528*9c5db199SXin Li 529*9c5db199SXin Li @classmethod 530*9c5db199SXin Li def classify_labels(cls, label_names): 531*9c5db199SXin Li """Split labels to static & non-static. 532*9c5db199SXin Li 533*9c5db199SXin Li @label_names: a list of labels (string). 534*9c5db199SXin Li 535*9c5db199SXin Li @returns: a list of StaticLabel objects & a list of 536*9c5db199SXin Li (non-static) Label objects. 537*9c5db199SXin Li """ 538*9c5db199SXin Li if not label_names: 539*9c5db199SXin Li return [], [] 540*9c5db199SXin Li 541*9c5db199SXin Li labels = Label.objects.filter(name__in=label_names) 542*9c5db199SXin Li 543*9c5db199SXin Li if not RESPECT_STATIC_LABELS: 544*9c5db199SXin Li return [], labels 545*9c5db199SXin Li 546*9c5db199SXin Li return cls.classify_label_objects(labels) 547*9c5db199SXin Li 548*9c5db199SXin Li 549*9c5db199SXin Li @classmethod 550*9c5db199SXin Li def classify_label_objects(cls, label_objects): 551*9c5db199SXin Li if not RESPECT_STATIC_LABELS: 552*9c5db199SXin Li return [], label_objects 553*9c5db199SXin Li 554*9c5db199SXin Li replaced_labels = ReplacedLabel.objects.filter(label__in=label_objects) 555*9c5db199SXin Li replaced_ids = [l.label.id for l in replaced_labels] 556*9c5db199SXin Li non_static_labels = [ 557*9c5db199SXin Li l for l in label_objects if not l.id in replaced_ids] 558*9c5db199SXin Li static_label_names = [ 559*9c5db199SXin Li l.name for l in label_objects if l.id in replaced_ids] 560*9c5db199SXin Li static_labels = StaticLabel.objects.filter(name__in=static_label_names) 561*9c5db199SXin Li return static_labels, non_static_labels 562*9c5db199SXin Li 563*9c5db199SXin Li 564*9c5db199SXin Li @classmethod 565*9c5db199SXin Li def get_hosts_with_labels(cls, label_names, initial_query): 566*9c5db199SXin Li """Get hosts by label filters. 567*9c5db199SXin Li 568*9c5db199SXin Li @param label_names: label (string) lists for fetching hosts. 569*9c5db199SXin Li @param initial_query: a model_logic.QuerySet of Host object, e.g. 570*9c5db199SXin Li 571*9c5db199SXin Li Host.objects.all(), Host.valid_objects.all(). 572*9c5db199SXin Li 573*9c5db199SXin Li This initial_query cannot be a sliced QuerySet, e.g. 574*9c5db199SXin Li 575*9c5db199SXin Li Host.objects.all().filter(query_limit=10) 576*9c5db199SXin Li """ 577*9c5db199SXin Li if not label_names: 578*9c5db199SXin Li return initial_query 579*9c5db199SXin Li 580*9c5db199SXin Li static_labels, non_static_labels = cls.classify_labels(label_names) 581*9c5db199SXin Li if len(static_labels) + len(non_static_labels) != len(label_names): 582*9c5db199SXin Li # Some labels don't exist in afe db, which means no hosts 583*9c5db199SXin Li # should be matched. 584*9c5db199SXin Li return set() 585*9c5db199SXin Li 586*9c5db199SXin Li for l in static_labels: 587*9c5db199SXin Li initial_query = initial_query.filter(static_labels=l) 588*9c5db199SXin Li 589*9c5db199SXin Li for l in non_static_labels: 590*9c5db199SXin Li initial_query = initial_query.filter(labels=l) 591*9c5db199SXin Li 592*9c5db199SXin Li return initial_query 593*9c5db199SXin Li 594*9c5db199SXin Li 595*9c5db199SXin Li @classmethod 596*9c5db199SXin Li def get_hosts_with_label_ids(cls, label_ids, initial_query): 597*9c5db199SXin Li """Get hosts by label_id filters. 598*9c5db199SXin Li 599*9c5db199SXin Li @param label_ids: label id (int) lists for fetching hosts. 600*9c5db199SXin Li @param initial_query: a list of Host object, e.g. 601*9c5db199SXin Li [<Host: 100.107.151.253>, <Host: 100.107.151.251>, ...] 602*9c5db199SXin Li """ 603*9c5db199SXin Li labels = Label.objects.filter(id__in=label_ids) 604*9c5db199SXin Li label_names = [l.name for l in labels] 605*9c5db199SXin Li return cls.get_hosts_with_labels(label_names, initial_query) 606*9c5db199SXin Li 607*9c5db199SXin Li 608*9c5db199SXin Li @staticmethod 609*9c5db199SXin Li def create_one_time_host(hostname): 610*9c5db199SXin Li """Creates a one-time host. 611*9c5db199SXin Li 612*9c5db199SXin Li @param hostname: The name for the host. 613*9c5db199SXin Li """ 614*9c5db199SXin Li query = Host.objects.filter(hostname=hostname) 615*9c5db199SXin Li if query.count() == 0: 616*9c5db199SXin Li host = Host(hostname=hostname, invalid=True) 617*9c5db199SXin Li host.do_validate() 618*9c5db199SXin Li else: 619*9c5db199SXin Li host = query[0] 620*9c5db199SXin Li if not host.invalid: 621*9c5db199SXin Li raise model_logic.ValidationError({ 622*9c5db199SXin Li 'hostname' : '%s already exists in the autotest DB. ' 623*9c5db199SXin Li 'Select it rather than entering it as a one time ' 624*9c5db199SXin Li 'host.' % hostname 625*9c5db199SXin Li }) 626*9c5db199SXin Li host.protection = host_protections.Protection.DO_NOT_REPAIR 627*9c5db199SXin Li host.locked = False 628*9c5db199SXin Li host.save() 629*9c5db199SXin Li host.clean_object() 630*9c5db199SXin Li return host 631*9c5db199SXin Li 632*9c5db199SXin Li 633*9c5db199SXin Li @classmethod 634*9c5db199SXin Li def _assign_to_shard_nothing_helper(cls): 635*9c5db199SXin Li """Does nothing. 636*9c5db199SXin Li 637*9c5db199SXin Li This method is called in the middle of assign_to_shard, and does 638*9c5db199SXin Li nothing. It exists to allow integration tests to simulate a race 639*9c5db199SXin Li condition.""" 640*9c5db199SXin Li 641*9c5db199SXin Li 642*9c5db199SXin Li @classmethod 643*9c5db199SXin Li def assign_to_shard(cls, shard, known_ids): 644*9c5db199SXin Li """Assigns hosts to a shard. 645*9c5db199SXin Li 646*9c5db199SXin Li For all labels that have been assigned to a shard, all hosts that 647*9c5db199SXin Li have at least one of the shard's labels are assigned to the shard. 648*9c5db199SXin Li Hosts that are assigned to the shard but aren't already present on the 649*9c5db199SXin Li shard are returned. 650*9c5db199SXin Li 651*9c5db199SXin Li Any boards that are in |known_ids| but that do not belong to the shard 652*9c5db199SXin Li are incorrect ids, which are also returned so that the shard can remove 653*9c5db199SXin Li them locally. 654*9c5db199SXin Li 655*9c5db199SXin Li Board to shard mapping is many-to-one. Many different boards can be 656*9c5db199SXin Li hosted in a shard. However, DUTs of a single board cannot be distributed 657*9c5db199SXin Li into more than one shard. 658*9c5db199SXin Li 659*9c5db199SXin Li @param shard: The shard object to assign labels/hosts for. 660*9c5db199SXin Li @param known_ids: List of all host-ids the shard already knows. 661*9c5db199SXin Li This is used to figure out which hosts should be sent 662*9c5db199SXin Li to the shard. If shard_ids were used instead, hosts 663*9c5db199SXin Li would only be transferred once, even if the client 664*9c5db199SXin Li failed persisting them. 665*9c5db199SXin Li The number of hosts usually lies in O(100), so the 666*9c5db199SXin Li overhead is acceptable. 667*9c5db199SXin Li 668*9c5db199SXin Li @returns a tuple of (hosts objects that should be sent to the shard, 669*9c5db199SXin Li incorrect host ids that should not belong to] 670*9c5db199SXin Li shard) 671*9c5db199SXin Li """ 672*9c5db199SXin Li # Disclaimer: concurrent heartbeats should theoretically not occur in 673*9c5db199SXin Li # the current setup. As they may be introduced in the near future, 674*9c5db199SXin Li # this comment will be left here. 675*9c5db199SXin Li 676*9c5db199SXin Li # Sending stuff twice is acceptable, but forgetting something isn't. 677*9c5db199SXin Li # Detecting duplicates on the client is easy, but here it's harder. The 678*9c5db199SXin Li # following options were considered: 679*9c5db199SXin Li # - SELECT ... WHERE and then UPDATE ... WHERE: Update might update more 680*9c5db199SXin Li # than select returned, as concurrently more hosts might have been 681*9c5db199SXin Li # inserted 682*9c5db199SXin Li # - UPDATE and then SELECT WHERE shard=shard: select always returns all 683*9c5db199SXin Li # hosts for the shard, this is overhead 684*9c5db199SXin Li # - SELECT and then UPDATE only selected without requerying afterwards: 685*9c5db199SXin Li # returns the old state of the records. 686*9c5db199SXin Li new_hosts = [] 687*9c5db199SXin Li 688*9c5db199SXin Li possible_new_host_ids = set(Host.objects.filter( 689*9c5db199SXin Li labels__in=shard.labels.all(), 690*9c5db199SXin Li leased=False 691*9c5db199SXin Li ).exclude( 692*9c5db199SXin Li id__in=known_ids, 693*9c5db199SXin Li ).values_list('pk', flat=True)) 694*9c5db199SXin Li 695*9c5db199SXin Li # No-op in production, used to simulate race condition in tests. 696*9c5db199SXin Li cls._assign_to_shard_nothing_helper() 697*9c5db199SXin Li 698*9c5db199SXin Li if possible_new_host_ids: 699*9c5db199SXin Li Host.objects.filter( 700*9c5db199SXin Li pk__in=possible_new_host_ids, 701*9c5db199SXin Li labels__in=shard.labels.all(), 702*9c5db199SXin Li leased=False 703*9c5db199SXin Li ).update(shard=shard) 704*9c5db199SXin Li new_hosts = list(Host.objects.filter( 705*9c5db199SXin Li pk__in=possible_new_host_ids, 706*9c5db199SXin Li shard=shard 707*9c5db199SXin Li ).all()) 708*9c5db199SXin Li 709*9c5db199SXin Li invalid_host_ids = list(Host.objects.filter( 710*9c5db199SXin Li id__in=known_ids 711*9c5db199SXin Li ).exclude( 712*9c5db199SXin Li shard=shard 713*9c5db199SXin Li ).values_list('pk', flat=True)) 714*9c5db199SXin Li 715*9c5db199SXin Li return new_hosts, invalid_host_ids 716*9c5db199SXin Li 717*9c5db199SXin Li def resurrect_object(self, old_object): 718*9c5db199SXin Li super(Host, self).resurrect_object(old_object) 719*9c5db199SXin Li # invalid hosts can be in use by the scheduler (as one-time hosts), so 720*9c5db199SXin Li # don't change the status 721*9c5db199SXin Li self.status = old_object.status 722*9c5db199SXin Li 723*9c5db199SXin Li 724*9c5db199SXin Li def clean_object(self): 725*9c5db199SXin Li self.aclgroup_set.clear() 726*9c5db199SXin Li self.labels.clear() 727*9c5db199SXin Li self.static_labels.clear() 728*9c5db199SXin Li 729*9c5db199SXin Li 730*9c5db199SXin Li def save(self, *args, **kwargs): 731*9c5db199SXin Li # extra spaces in the hostname can be a sneaky source of errors 732*9c5db199SXin Li self.hostname = self.hostname.strip() 733*9c5db199SXin Li # is this a new object being saved for the first time? 734*9c5db199SXin Li first_time = (self.id is None) 735*9c5db199SXin Li if not first_time: 736*9c5db199SXin Li AclGroup.check_for_acl_violation_hosts([self]) 737*9c5db199SXin Li # If locked is changed, send its status and user made the change to 738*9c5db199SXin Li # metaDB. Locks are important in host history because if a device is 739*9c5db199SXin Li # locked then we don't really care what state it is in. 740*9c5db199SXin Li if self.locked and not self.locked_by: 741*9c5db199SXin Li self.locked_by = User.current_user() 742*9c5db199SXin Li if not self.lock_time: 743*9c5db199SXin Li self.lock_time = datetime.now() 744*9c5db199SXin Li self.dirty = True 745*9c5db199SXin Li elif not self.locked and self.locked_by: 746*9c5db199SXin Li self.locked_by = None 747*9c5db199SXin Li self.lock_time = None 748*9c5db199SXin Li super(Host, self).save(*args, **kwargs) 749*9c5db199SXin Li if first_time: 750*9c5db199SXin Li everyone = AclGroup.objects.get(name='Everyone') 751*9c5db199SXin Li everyone.hosts.add(self) 752*9c5db199SXin Li # remove attributes that may have lingered from an old host and 753*9c5db199SXin Li # should not be associated with a new host 754*9c5db199SXin Li for host_attribute in self.hostattribute_set.all(): 755*9c5db199SXin Li self.delete_attribute(host_attribute.attribute) 756*9c5db199SXin Li self._check_for_updated_attributes() 757*9c5db199SXin Li 758*9c5db199SXin Li 759*9c5db199SXin Li def delete(self): 760*9c5db199SXin Li AclGroup.check_for_acl_violation_hosts([self]) 761*9c5db199SXin Li logging.info('Preconditions for deleting host %s...', self.hostname) 762*9c5db199SXin Li for queue_entry in self.hostqueueentry_set.all(): 763*9c5db199SXin Li logging.info(' Deleting and aborting hqe %s...', queue_entry) 764*9c5db199SXin Li queue_entry.deleted = True 765*9c5db199SXin Li queue_entry.abort() 766*9c5db199SXin Li logging.info(' ... done with hqe %s.', queue_entry) 767*9c5db199SXin Li for host_attribute in self.hostattribute_set.all(): 768*9c5db199SXin Li logging.info(' Deleting attribute %s...', host_attribute) 769*9c5db199SXin Li self.delete_attribute(host_attribute.attribute) 770*9c5db199SXin Li logging.info(' ... done with attribute %s.', host_attribute) 771*9c5db199SXin Li logging.info('... preconditions done for host %s.', self.hostname) 772*9c5db199SXin Li logging.info('Deleting host %s...', self.hostname) 773*9c5db199SXin Li super(Host, self).delete() 774*9c5db199SXin Li logging.info('... done.') 775*9c5db199SXin Li 776*9c5db199SXin Li 777*9c5db199SXin Li def on_attribute_changed(self, attribute, old_value): 778*9c5db199SXin Li assert attribute == 'status' 779*9c5db199SXin Li logging.info('%s -> %s', self.hostname, self.status) 780*9c5db199SXin Li 781*9c5db199SXin Li 782*9c5db199SXin Li def enqueue_job(self, job, is_template=False): 783*9c5db199SXin Li """Enqueue a job on this host. 784*9c5db199SXin Li 785*9c5db199SXin Li @param job: A job to enqueue. 786*9c5db199SXin Li @param is_template: Whther the status should be "Template". 787*9c5db199SXin Li """ 788*9c5db199SXin Li queue_entry = HostQueueEntry.create(host=self, job=job, 789*9c5db199SXin Li is_template=is_template) 790*9c5db199SXin Li # allow recovery of dead hosts from the frontend 791*9c5db199SXin Li if not self.active_queue_entry() and self.is_dead(): 792*9c5db199SXin Li self.status = Host.Status.READY 793*9c5db199SXin Li self.save() 794*9c5db199SXin Li queue_entry.save() 795*9c5db199SXin Li 796*9c5db199SXin Li block = IneligibleHostQueue(job=job, host=self) 797*9c5db199SXin Li block.save() 798*9c5db199SXin Li 799*9c5db199SXin Li 800*9c5db199SXin Li def platform(self): 801*9c5db199SXin Li """The platform of the host.""" 802*9c5db199SXin Li # TODO(showard): slighly hacky? 803*9c5db199SXin Li platforms = self.labels.filter(platform=True) 804*9c5db199SXin Li if len(platforms) == 0: 805*9c5db199SXin Li return None 806*9c5db199SXin Li return platforms[0] 807*9c5db199SXin Li platform.short_description = 'Platform' 808*9c5db199SXin Li 809*9c5db199SXin Li 810*9c5db199SXin Li @classmethod 811*9c5db199SXin Li def check_no_platform(cls, hosts): 812*9c5db199SXin Li """Verify the specified hosts have no associated platforms. 813*9c5db199SXin Li 814*9c5db199SXin Li @param cls: Implicit class object. 815*9c5db199SXin Li @param hosts: The hosts to verify. 816*9c5db199SXin Li @raises model_logic.ValidationError if any hosts already have a 817*9c5db199SXin Li platform. 818*9c5db199SXin Li """ 819*9c5db199SXin Li Host.objects.populate_relationships(hosts, Label, 'label_list') 820*9c5db199SXin Li Host.objects.populate_relationships(hosts, StaticLabel, 821*9c5db199SXin Li 'staticlabel_list') 822*9c5db199SXin Li errors = [] 823*9c5db199SXin Li for host in hosts: 824*9c5db199SXin Li platforms = [label.name for label in host.label_list 825*9c5db199SXin Li if label.platform] 826*9c5db199SXin Li if RESPECT_STATIC_LABELS: 827*9c5db199SXin Li platforms += [label.name for label in host.staticlabel_list 828*9c5db199SXin Li if label.platform] 829*9c5db199SXin Li 830*9c5db199SXin Li if platforms: 831*9c5db199SXin Li # do a join, just in case this host has multiple platforms, 832*9c5db199SXin Li # we'll be able to see it 833*9c5db199SXin Li errors.append('Host %s already has a platform: %s' % ( 834*9c5db199SXin Li host.hostname, ', '.join(platforms))) 835*9c5db199SXin Li if errors: 836*9c5db199SXin Li raise model_logic.ValidationError({'labels': '; '.join(errors)}) 837*9c5db199SXin Li 838*9c5db199SXin Li 839*9c5db199SXin Li @classmethod 840*9c5db199SXin Li def check_board_labels_allowed(cls, hosts, new_labels=[]): 841*9c5db199SXin Li """Verify the specified hosts have valid board labels and the given 842*9c5db199SXin Li new board labels can be added. 843*9c5db199SXin Li 844*9c5db199SXin Li @param cls: Implicit class object. 845*9c5db199SXin Li @param hosts: The hosts to verify. 846*9c5db199SXin Li @param new_labels: A list of labels to be added to the hosts. 847*9c5db199SXin Li 848*9c5db199SXin Li @raises model_logic.ValidationError if any host has invalid board labels 849*9c5db199SXin Li or the given board labels cannot be added to the hsots. 850*9c5db199SXin Li """ 851*9c5db199SXin Li Host.objects.populate_relationships(hosts, Label, 'label_list') 852*9c5db199SXin Li Host.objects.populate_relationships(hosts, StaticLabel, 853*9c5db199SXin Li 'staticlabel_list') 854*9c5db199SXin Li errors = [] 855*9c5db199SXin Li for host in hosts: 856*9c5db199SXin Li boards = [label.name for label in host.label_list 857*9c5db199SXin Li if label.name.startswith('board:')] 858*9c5db199SXin Li if RESPECT_STATIC_LABELS: 859*9c5db199SXin Li boards += [label.name for label in host.staticlabel_list 860*9c5db199SXin Li if label.name.startswith('board:')] 861*9c5db199SXin Li 862*9c5db199SXin Li new_boards = [name for name in new_labels 863*9c5db199SXin Li if name.startswith('board:')] 864*9c5db199SXin Li if len(boards) + len(new_boards) > 1: 865*9c5db199SXin Li # do a join, just in case this host has multiple boards, 866*9c5db199SXin Li # we'll be able to see it 867*9c5db199SXin Li errors.append('Host %s already has board labels: %s' % ( 868*9c5db199SXin Li host.hostname, ', '.join(boards))) 869*9c5db199SXin Li if errors: 870*9c5db199SXin Li raise model_logic.ValidationError({'labels': '; '.join(errors)}) 871*9c5db199SXin Li 872*9c5db199SXin Li 873*9c5db199SXin Li def is_dead(self): 874*9c5db199SXin Li """Returns whether the host is dead (has status repair failed).""" 875*9c5db199SXin Li return self.status == Host.Status.REPAIR_FAILED 876*9c5db199SXin Li 877*9c5db199SXin Li 878*9c5db199SXin Li def active_queue_entry(self): 879*9c5db199SXin Li """Returns the active queue entry for this host, or None if none.""" 880*9c5db199SXin Li active = list(self.hostqueueentry_set.filter(active=True)) 881*9c5db199SXin Li if not active: 882*9c5db199SXin Li return None 883*9c5db199SXin Li assert len(active) == 1, ('More than one active entry for ' 884*9c5db199SXin Li 'host ' + self.hostname) 885*9c5db199SXin Li return active[0] 886*9c5db199SXin Li 887*9c5db199SXin Li 888*9c5db199SXin Li def _get_attribute_model_and_args(self, attribute): 889*9c5db199SXin Li return HostAttribute, dict(host=self, attribute=attribute) 890*9c5db199SXin Li 891*9c5db199SXin Li 892*9c5db199SXin Li def _get_static_attribute_model_and_args(self, attribute): 893*9c5db199SXin Li return StaticHostAttribute, dict(host=self, attribute=attribute) 894*9c5db199SXin Li 895*9c5db199SXin Li 896*9c5db199SXin Li def _is_replaced_by_static_attribute(self, attribute): 897*9c5db199SXin Li if RESPECT_STATIC_ATTRIBUTES: 898*9c5db199SXin Li model, args = self._get_static_attribute_model_and_args(attribute) 899*9c5db199SXin Li try: 900*9c5db199SXin Li static_attr = model.objects.get(**args) 901*9c5db199SXin Li return True 902*9c5db199SXin Li except StaticHostAttribute.DoesNotExist: 903*9c5db199SXin Li return False 904*9c5db199SXin Li 905*9c5db199SXin Li return False 906*9c5db199SXin Li 907*9c5db199SXin Li 908*9c5db199SXin Li @classmethod 909*9c5db199SXin Li def get_attribute_model(cls): 910*9c5db199SXin Li """Return the attribute model. 911*9c5db199SXin Li 912*9c5db199SXin Li Override method in parent class. See ModelExtensions for details. 913*9c5db199SXin Li @returns: The attribute model of Host. 914*9c5db199SXin Li """ 915*9c5db199SXin Li return HostAttribute 916*9c5db199SXin Li 917*9c5db199SXin Li 918*9c5db199SXin Li class Meta: 919*9c5db199SXin Li """Metadata for the Host class.""" 920*9c5db199SXin Li db_table = 'afe_hosts' 921*9c5db199SXin Li 922*9c5db199SXin Li 923*9c5db199SXin Li def __unicode__(self): 924*9c5db199SXin Li return unicode(self.hostname) 925*9c5db199SXin Li 926*9c5db199SXin Li 927*9c5db199SXin Liclass HostAttribute(dbmodels.Model, model_logic.ModelExtensions): 928*9c5db199SXin Li """Arbitrary keyvals associated with hosts.""" 929*9c5db199SXin Li 930*9c5db199SXin Li SERIALIZATION_LINKS_TO_KEEP = set(['host']) 931*9c5db199SXin Li SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['value']) 932*9c5db199SXin Li host = dbmodels.ForeignKey(Host) 933*9c5db199SXin Li attribute = dbmodels.CharField(max_length=90) 934*9c5db199SXin Li value = dbmodels.CharField(max_length=300) 935*9c5db199SXin Li 936*9c5db199SXin Li objects = model_logic.ExtendedManager() 937*9c5db199SXin Li 938*9c5db199SXin Li class Meta: 939*9c5db199SXin Li """Metadata for the HostAttribute class.""" 940*9c5db199SXin Li db_table = 'afe_host_attributes' 941*9c5db199SXin Li 942*9c5db199SXin Li 943*9c5db199SXin Li @classmethod 944*9c5db199SXin Li def get_record(cls, data): 945*9c5db199SXin Li """Check the database for an identical record. 946*9c5db199SXin Li 947*9c5db199SXin Li Use host_id and attribute to search for a existing record. 948*9c5db199SXin Li 949*9c5db199SXin Li @raises: DoesNotExist, if no record found 950*9c5db199SXin Li @raises: MultipleObjectsReturned if multiple records found. 951*9c5db199SXin Li """ 952*9c5db199SXin Li # TODO(fdeng): We should use host_id and attribute together as 953*9c5db199SXin Li # a primary key in the db. 954*9c5db199SXin Li return cls.objects.get(host_id=data['host_id'], 955*9c5db199SXin Li attribute=data['attribute']) 956*9c5db199SXin Li 957*9c5db199SXin Li 958*9c5db199SXin Li @classmethod 959*9c5db199SXin Li def deserialize(cls, data): 960*9c5db199SXin Li """Override deserialize in parent class. 961*9c5db199SXin Li 962*9c5db199SXin Li Do not deserialize id as id is not kept consistent on main and shards. 963*9c5db199SXin Li 964*9c5db199SXin Li @param data: A dictionary of data to deserialize. 965*9c5db199SXin Li 966*9c5db199SXin Li @returns: A HostAttribute object. 967*9c5db199SXin Li """ 968*9c5db199SXin Li if data: 969*9c5db199SXin Li data.pop('id') 970*9c5db199SXin Li return super(HostAttribute, cls).deserialize(data) 971*9c5db199SXin Li 972*9c5db199SXin Li 973*9c5db199SXin Liclass StaticHostAttribute(dbmodels.Model, model_logic.ModelExtensions): 974*9c5db199SXin Li """Static arbitrary keyvals associated with hosts.""" 975*9c5db199SXin Li 976*9c5db199SXin Li SERIALIZATION_LINKS_TO_KEEP = set(['host']) 977*9c5db199SXin Li SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['value']) 978*9c5db199SXin Li host = dbmodels.ForeignKey(Host) 979*9c5db199SXin Li attribute = dbmodels.CharField(max_length=90) 980*9c5db199SXin Li value = dbmodels.CharField(max_length=300) 981*9c5db199SXin Li 982*9c5db199SXin Li objects = model_logic.ExtendedManager() 983*9c5db199SXin Li 984*9c5db199SXin Li class Meta: 985*9c5db199SXin Li """Metadata for the StaticHostAttribute class.""" 986*9c5db199SXin Li db_table = 'afe_static_host_attributes' 987*9c5db199SXin Li 988*9c5db199SXin Li 989*9c5db199SXin Li @classmethod 990*9c5db199SXin Li def get_record(cls, data): 991*9c5db199SXin Li """Check the database for an identical record. 992*9c5db199SXin Li 993*9c5db199SXin Li Use host_id and attribute to search for a existing record. 994*9c5db199SXin Li 995*9c5db199SXin Li @raises: DoesNotExist, if no record found 996*9c5db199SXin Li @raises: MultipleObjectsReturned if multiple records found. 997*9c5db199SXin Li """ 998*9c5db199SXin Li return cls.objects.get(host_id=data['host_id'], 999*9c5db199SXin Li attribute=data['attribute']) 1000*9c5db199SXin Li 1001*9c5db199SXin Li 1002*9c5db199SXin Li @classmethod 1003*9c5db199SXin Li def deserialize(cls, data): 1004*9c5db199SXin Li """Override deserialize in parent class. 1005*9c5db199SXin Li 1006*9c5db199SXin Li Do not deserialize id as id is not kept consistent on main and shards. 1007*9c5db199SXin Li 1008*9c5db199SXin Li @param data: A dictionary of data to deserialize. 1009*9c5db199SXin Li 1010*9c5db199SXin Li @returns: A StaticHostAttribute object. 1011*9c5db199SXin Li """ 1012*9c5db199SXin Li if data: 1013*9c5db199SXin Li data.pop('id') 1014*9c5db199SXin Li return super(StaticHostAttribute, cls).deserialize(data) 1015*9c5db199SXin Li 1016*9c5db199SXin Li 1017*9c5db199SXin Liclass Test(dbmodels.Model, model_logic.ModelExtensions): 1018*9c5db199SXin Li """\ 1019*9c5db199SXin Li Required: 1020*9c5db199SXin Li author: author name 1021*9c5db199SXin Li description: description of the test 1022*9c5db199SXin Li name: test name 1023*9c5db199SXin Li time: short, medium, long 1024*9c5db199SXin Li test_class: This describes the class for your the test belongs in. 1025*9c5db199SXin Li test_category: This describes the category for your tests 1026*9c5db199SXin Li test_type: Client or Server 1027*9c5db199SXin Li path: path to pass to run_test() 1028*9c5db199SXin Li sync_count: is a number >=1 (1 being the default). If it's 1, then it's an 1029*9c5db199SXin Li async job. If it's >1 it's sync job for that number of machines 1030*9c5db199SXin Li i.e. if sync_count = 2 it is a sync job that requires two 1031*9c5db199SXin Li machines. 1032*9c5db199SXin Li Optional: 1033*9c5db199SXin Li dependencies: What the test requires to run. Comma deliminated list 1034*9c5db199SXin Li dependency_labels: many-to-many relationship with labels corresponding to 1035*9c5db199SXin Li test dependencies. 1036*9c5db199SXin Li experimental: If this is set to True production servers will ignore the test 1037*9c5db199SXin Li run_verify: Whether or not the scheduler should run the verify stage 1038*9c5db199SXin Li run_reset: Whether or not the scheduler should run the reset stage 1039*9c5db199SXin Li test_retry: Number of times to retry test if the test did not complete 1040*9c5db199SXin Li successfully. (optional, default: 0) 1041*9c5db199SXin Li """ 1042*9c5db199SXin Li TestTime = autotest_enum.AutotestEnum('SHORT', 'MEDIUM', 'LONG', 1043*9c5db199SXin Li start_value=1) 1044*9c5db199SXin Li 1045*9c5db199SXin Li name = dbmodels.CharField(max_length=255, unique=True) 1046*9c5db199SXin Li author = dbmodels.CharField(max_length=255) 1047*9c5db199SXin Li test_class = dbmodels.CharField(max_length=255) 1048*9c5db199SXin Li test_category = dbmodels.CharField(max_length=255) 1049*9c5db199SXin Li dependencies = dbmodels.CharField(max_length=255, blank=True) 1050*9c5db199SXin Li description = dbmodels.TextField(blank=True) 1051*9c5db199SXin Li experimental = dbmodels.BooleanField(default=True) 1052*9c5db199SXin Li run_verify = dbmodels.BooleanField(default=False) 1053*9c5db199SXin Li test_time = dbmodels.SmallIntegerField(choices=TestTime.choices(), 1054*9c5db199SXin Li default=TestTime.MEDIUM) 1055*9c5db199SXin Li test_type = dbmodels.SmallIntegerField( 1056*9c5db199SXin Li choices=control_data.CONTROL_TYPE.choices()) 1057*9c5db199SXin Li sync_count = dbmodels.IntegerField(default=1) 1058*9c5db199SXin Li path = dbmodels.CharField(max_length=255, unique=True) 1059*9c5db199SXin Li test_retry = dbmodels.IntegerField(blank=True, default=0) 1060*9c5db199SXin Li run_reset = dbmodels.BooleanField(default=True) 1061*9c5db199SXin Li 1062*9c5db199SXin Li dependency_labels = ( 1063*9c5db199SXin Li dbmodels.ManyToManyField(Label, blank=True, 1064*9c5db199SXin Li db_table='afe_autotests_dependency_labels')) 1065*9c5db199SXin Li name_field = 'name' 1066*9c5db199SXin Li objects = model_logic.ExtendedManager() 1067*9c5db199SXin Li 1068*9c5db199SXin Li 1069*9c5db199SXin Li def admin_description(self): 1070*9c5db199SXin Li """Returns a string representing the admin description.""" 1071*9c5db199SXin Li escaped_description = saxutils.escape(self.description) 1072*9c5db199SXin Li return '<span style="white-space:pre">%s</span>' % escaped_description 1073*9c5db199SXin Li admin_description.allow_tags = True 1074*9c5db199SXin Li admin_description.short_description = 'Description' 1075*9c5db199SXin Li 1076*9c5db199SXin Li 1077*9c5db199SXin Li class Meta: 1078*9c5db199SXin Li """Metadata for class Test.""" 1079*9c5db199SXin Li db_table = 'afe_autotests' 1080*9c5db199SXin Li 1081*9c5db199SXin Li def __unicode__(self): 1082*9c5db199SXin Li return unicode(self.name) 1083*9c5db199SXin Li 1084*9c5db199SXin Li 1085*9c5db199SXin Liclass TestParameter(dbmodels.Model): 1086*9c5db199SXin Li """ 1087*9c5db199SXin Li A declared parameter of a test 1088*9c5db199SXin Li """ 1089*9c5db199SXin Li test = dbmodels.ForeignKey(Test) 1090*9c5db199SXin Li name = dbmodels.CharField(max_length=255) 1091*9c5db199SXin Li 1092*9c5db199SXin Li class Meta: 1093*9c5db199SXin Li """Metadata for class TestParameter.""" 1094*9c5db199SXin Li db_table = 'afe_test_parameters' 1095*9c5db199SXin Li unique_together = ('test', 'name') 1096*9c5db199SXin Li 1097*9c5db199SXin Li def __unicode__(self): 1098*9c5db199SXin Li return u'%s (%s)' % (self.name, self.test.name) 1099*9c5db199SXin Li 1100*9c5db199SXin Li 1101*9c5db199SXin Liclass Profiler(dbmodels.Model, model_logic.ModelExtensions): 1102*9c5db199SXin Li """\ 1103*9c5db199SXin Li Required: 1104*9c5db199SXin Li name: profiler name 1105*9c5db199SXin Li test_type: Client or Server 1106*9c5db199SXin Li 1107*9c5db199SXin Li Optional: 1108*9c5db199SXin Li description: arbirary text description 1109*9c5db199SXin Li """ 1110*9c5db199SXin Li name = dbmodels.CharField(max_length=255, unique=True) 1111*9c5db199SXin Li description = dbmodels.TextField(blank=True) 1112*9c5db199SXin Li 1113*9c5db199SXin Li name_field = 'name' 1114*9c5db199SXin Li objects = model_logic.ExtendedManager() 1115*9c5db199SXin Li 1116*9c5db199SXin Li 1117*9c5db199SXin Li class Meta: 1118*9c5db199SXin Li """Metadata for class Profiler.""" 1119*9c5db199SXin Li db_table = 'afe_profilers' 1120*9c5db199SXin Li 1121*9c5db199SXin Li def __unicode__(self): 1122*9c5db199SXin Li return unicode(self.name) 1123*9c5db199SXin Li 1124*9c5db199SXin Li 1125*9c5db199SXin Liclass AclGroup(dbmodels.Model, model_logic.ModelExtensions): 1126*9c5db199SXin Li """\ 1127*9c5db199SXin Li Required: 1128*9c5db199SXin Li name: name of ACL group 1129*9c5db199SXin Li 1130*9c5db199SXin Li Optional: 1131*9c5db199SXin Li description: arbitrary description of group 1132*9c5db199SXin Li """ 1133*9c5db199SXin Li 1134*9c5db199SXin Li SERIALIZATION_LINKS_TO_FOLLOW = set(['users']) 1135*9c5db199SXin Li 1136*9c5db199SXin Li name = dbmodels.CharField(max_length=255, unique=True) 1137*9c5db199SXin Li description = dbmodels.CharField(max_length=255, blank=True) 1138*9c5db199SXin Li users = dbmodels.ManyToManyField(User, blank=False, 1139*9c5db199SXin Li db_table='afe_acl_groups_users') 1140*9c5db199SXin Li hosts = dbmodels.ManyToManyField(Host, blank=True, 1141*9c5db199SXin Li db_table='afe_acl_groups_hosts') 1142*9c5db199SXin Li 1143*9c5db199SXin Li name_field = 'name' 1144*9c5db199SXin Li objects = model_logic.ExtendedManager() 1145*9c5db199SXin Li 1146*9c5db199SXin Li @staticmethod 1147*9c5db199SXin Li def check_for_acl_violation_hosts(hosts): 1148*9c5db199SXin Li """Verify the current user has access to the specified hosts. 1149*9c5db199SXin Li 1150*9c5db199SXin Li @param hosts: The hosts to verify against. 1151*9c5db199SXin Li @raises AclAccessViolation if the current user doesn't have access 1152*9c5db199SXin Li to a host. 1153*9c5db199SXin Li """ 1154*9c5db199SXin Li user = User.current_user() 1155*9c5db199SXin Li if user.is_superuser(): 1156*9c5db199SXin Li return 1157*9c5db199SXin Li accessible_host_ids = set( 1158*9c5db199SXin Li host.id for host in Host.objects.filter(aclgroup__users=user)) 1159*9c5db199SXin Li for host in hosts: 1160*9c5db199SXin Li # Check if the user has access to this host, 1161*9c5db199SXin Li # but only if it is not a metahost or a one-time-host. 1162*9c5db199SXin Li no_access = (isinstance(host, Host) 1163*9c5db199SXin Li and not host.invalid 1164*9c5db199SXin Li and int(host.id) not in accessible_host_ids) 1165*9c5db199SXin Li if no_access: 1166*9c5db199SXin Li raise AclAccessViolation("%s does not have access to %s" % 1167*9c5db199SXin Li (str(user), str(host))) 1168*9c5db199SXin Li 1169*9c5db199SXin Li 1170*9c5db199SXin Li @staticmethod 1171*9c5db199SXin Li def check_abort_permissions(queue_entries): 1172*9c5db199SXin Li """Look for queue entries that aren't abortable by the current user. 1173*9c5db199SXin Li 1174*9c5db199SXin Li An entry is not abortable if: 1175*9c5db199SXin Li * the job isn't owned by this user, and 1176*9c5db199SXin Li * the machine isn't ACL-accessible, or 1177*9c5db199SXin Li * the machine is in the "Everyone" ACL 1178*9c5db199SXin Li 1179*9c5db199SXin Li @param queue_entries: The queue entries to check. 1180*9c5db199SXin Li @raises AclAccessViolation if a queue entry is not abortable by the 1181*9c5db199SXin Li current user. 1182*9c5db199SXin Li """ 1183*9c5db199SXin Li user = User.current_user() 1184*9c5db199SXin Li if user.is_superuser(): 1185*9c5db199SXin Li return 1186*9c5db199SXin Li not_owned = queue_entries.exclude(job__owner=user.login) 1187*9c5db199SXin Li # I do this using ID sets instead of just Django filters because 1188*9c5db199SXin Li # filtering on M2M dbmodels is broken in Django 0.96. It's better in 1189*9c5db199SXin Li # 1.0. 1190*9c5db199SXin Li # TODO: Use Django filters, now that we're using 1.0. 1191*9c5db199SXin Li accessible_ids = set( 1192*9c5db199SXin Li entry.id for entry 1193*9c5db199SXin Li in not_owned.filter(host__aclgroup__users__login=user.login)) 1194*9c5db199SXin Li public_ids = set(entry.id for entry 1195*9c5db199SXin Li in not_owned.filter(host__aclgroup__name='Everyone')) 1196*9c5db199SXin Li cannot_abort = [entry for entry in not_owned.select_related() 1197*9c5db199SXin Li if entry.id not in accessible_ids 1198*9c5db199SXin Li or entry.id in public_ids] 1199*9c5db199SXin Li if len(cannot_abort) == 0: 1200*9c5db199SXin Li return 1201*9c5db199SXin Li entry_names = ', '.join('%s-%s/%s' % (entry.job.id, entry.job.owner, 1202*9c5db199SXin Li entry.host_or_metahost_name()) 1203*9c5db199SXin Li for entry in cannot_abort) 1204*9c5db199SXin Li raise AclAccessViolation('You cannot abort the following job entries: ' 1205*9c5db199SXin Li + entry_names) 1206*9c5db199SXin Li 1207*9c5db199SXin Li 1208*9c5db199SXin Li def check_for_acl_violation_acl_group(self): 1209*9c5db199SXin Li """Verifies the current user has acces to this ACL group. 1210*9c5db199SXin Li 1211*9c5db199SXin Li @raises AclAccessViolation if the current user doesn't have access to 1212*9c5db199SXin Li this ACL group. 1213*9c5db199SXin Li """ 1214*9c5db199SXin Li user = User.current_user() 1215*9c5db199SXin Li if user.is_superuser(): 1216*9c5db199SXin Li return 1217*9c5db199SXin Li if self.name == 'Everyone': 1218*9c5db199SXin Li raise AclAccessViolation("You cannot modify 'Everyone'!") 1219*9c5db199SXin Li if not user in self.users.all(): 1220*9c5db199SXin Li raise AclAccessViolation("You do not have access to %s" 1221*9c5db199SXin Li % self.name) 1222*9c5db199SXin Li 1223*9c5db199SXin Li @staticmethod 1224*9c5db199SXin Li def on_host_membership_change(): 1225*9c5db199SXin Li """Invoked when host membership changes.""" 1226*9c5db199SXin Li everyone = AclGroup.objects.get(name='Everyone') 1227*9c5db199SXin Li 1228*9c5db199SXin Li # find hosts that aren't in any ACL group and add them to Everyone 1229*9c5db199SXin Li # TODO(showard): this is a bit of a hack, since the fact that this query 1230*9c5db199SXin Li # works is kind of a coincidence of Django internals. This trick 1231*9c5db199SXin Li # doesn't work in general (on all foreign key relationships). I'll 1232*9c5db199SXin Li # replace it with a better technique when the need arises. 1233*9c5db199SXin Li orphaned_hosts = Host.valid_objects.filter(aclgroup__id__isnull=True) 1234*9c5db199SXin Li everyone.hosts.add(*orphaned_hosts.distinct()) 1235*9c5db199SXin Li 1236*9c5db199SXin Li # find hosts in both Everyone and another ACL group, and remove them 1237*9c5db199SXin Li # from Everyone 1238*9c5db199SXin Li hosts_in_everyone = Host.valid_objects.filter(aclgroup__name='Everyone') 1239*9c5db199SXin Li acled_hosts = set() 1240*9c5db199SXin Li for host in hosts_in_everyone: 1241*9c5db199SXin Li # Has an ACL group other than Everyone 1242*9c5db199SXin Li if host.aclgroup_set.count() > 1: 1243*9c5db199SXin Li acled_hosts.add(host) 1244*9c5db199SXin Li everyone.hosts.remove(*acled_hosts) 1245*9c5db199SXin Li 1246*9c5db199SXin Li 1247*9c5db199SXin Li def delete(self): 1248*9c5db199SXin Li if (self.name == 'Everyone'): 1249*9c5db199SXin Li raise AclAccessViolation("You cannot delete 'Everyone'!") 1250*9c5db199SXin Li self.check_for_acl_violation_acl_group() 1251*9c5db199SXin Li super(AclGroup, self).delete() 1252*9c5db199SXin Li self.on_host_membership_change() 1253*9c5db199SXin Li 1254*9c5db199SXin Li 1255*9c5db199SXin Li def add_current_user_if_empty(self): 1256*9c5db199SXin Li """Adds the current user if the set of users is empty.""" 1257*9c5db199SXin Li if not self.users.count(): 1258*9c5db199SXin Li self.users.add(User.current_user()) 1259*9c5db199SXin Li 1260*9c5db199SXin Li 1261*9c5db199SXin Li def perform_after_save(self, change): 1262*9c5db199SXin Li """Called after a save. 1263*9c5db199SXin Li 1264*9c5db199SXin Li @param change: Whether there was a change. 1265*9c5db199SXin Li """ 1266*9c5db199SXin Li if not change: 1267*9c5db199SXin Li self.users.add(User.current_user()) 1268*9c5db199SXin Li self.add_current_user_if_empty() 1269*9c5db199SXin Li self.on_host_membership_change() 1270*9c5db199SXin Li 1271*9c5db199SXin Li 1272*9c5db199SXin Li def save(self, *args, **kwargs): 1273*9c5db199SXin Li change = bool(self.id) 1274*9c5db199SXin Li if change: 1275*9c5db199SXin Li # Check the original object for an ACL violation 1276*9c5db199SXin Li AclGroup.objects.get(id=self.id).check_for_acl_violation_acl_group() 1277*9c5db199SXin Li super(AclGroup, self).save(*args, **kwargs) 1278*9c5db199SXin Li self.perform_after_save(change) 1279*9c5db199SXin Li 1280*9c5db199SXin Li 1281*9c5db199SXin Li class Meta: 1282*9c5db199SXin Li """Metadata for class AclGroup.""" 1283*9c5db199SXin Li db_table = 'afe_acl_groups' 1284*9c5db199SXin Li 1285*9c5db199SXin Li def __unicode__(self): 1286*9c5db199SXin Li return unicode(self.name) 1287*9c5db199SXin Li 1288*9c5db199SXin Li 1289*9c5db199SXin Liclass ParameterizedJob(dbmodels.Model): 1290*9c5db199SXin Li """ 1291*9c5db199SXin Li Auxiliary configuration for a parameterized job. 1292*9c5db199SXin Li 1293*9c5db199SXin Li This class is obsolete, and ought to be dead. Due to a series of 1294*9c5db199SXin Li unfortunate events, it can't be deleted: 1295*9c5db199SXin Li * In `class Job` we're required to keep a reference to this class 1296*9c5db199SXin Li for the sake of the scheduler unit tests. 1297*9c5db199SXin Li * The existence of the reference in `Job` means that certain 1298*9c5db199SXin Li methods here will get called from the `get_jobs` RPC. 1299*9c5db199SXin Li So, the definitions below seem to be the minimum stub we can support 1300*9c5db199SXin Li unless/until we change the database schema. 1301*9c5db199SXin Li """ 1302*9c5db199SXin Li 1303*9c5db199SXin Li @classmethod 1304*9c5db199SXin Li def smart_get(cls, id_or_name, *args, **kwargs): 1305*9c5db199SXin Li """For compatibility with Job.add_object. 1306*9c5db199SXin Li 1307*9c5db199SXin Li @param cls: Implicit class object. 1308*9c5db199SXin Li @param id_or_name: The ID or name to get. 1309*9c5db199SXin Li @param args: Non-keyword arguments. 1310*9c5db199SXin Li @param kwargs: Keyword arguments. 1311*9c5db199SXin Li """ 1312*9c5db199SXin Li return cls.objects.get(pk=id_or_name) 1313*9c5db199SXin Li 1314*9c5db199SXin Li 1315*9c5db199SXin Li def job(self): 1316*9c5db199SXin Li """Returns the job if it exists, or else None.""" 1317*9c5db199SXin Li jobs = self.job_set.all() 1318*9c5db199SXin Li assert jobs.count() <= 1 1319*9c5db199SXin Li return jobs and jobs[0] or None 1320*9c5db199SXin Li 1321*9c5db199SXin Li 1322*9c5db199SXin Li class Meta: 1323*9c5db199SXin Li """Metadata for class ParameterizedJob.""" 1324*9c5db199SXin Li db_table = 'afe_parameterized_jobs' 1325*9c5db199SXin Li 1326*9c5db199SXin Li def __unicode__(self): 1327*9c5db199SXin Li return u'%s (parameterized) - %s' % (self.test.name, self.job()) 1328*9c5db199SXin Li 1329*9c5db199SXin Li 1330*9c5db199SXin Liclass JobManager(model_logic.ExtendedManager): 1331*9c5db199SXin Li 'Custom manager to provide efficient status counts querying.' 1332*9c5db199SXin Li def get_status_counts(self, job_ids): 1333*9c5db199SXin Li """Returns a dict mapping the given job IDs to their status count dicts. 1334*9c5db199SXin Li 1335*9c5db199SXin Li @param job_ids: A list of job IDs. 1336*9c5db199SXin Li """ 1337*9c5db199SXin Li if not job_ids: 1338*9c5db199SXin Li return {} 1339*9c5db199SXin Li id_list = '(%s)' % ','.join(str(job_id) for job_id in job_ids) 1340*9c5db199SXin Li cursor = connection.cursor() 1341*9c5db199SXin Li cursor.execute(""" 1342*9c5db199SXin Li SELECT job_id, status, aborted, complete, COUNT(*) 1343*9c5db199SXin Li FROM afe_host_queue_entries 1344*9c5db199SXin Li WHERE job_id IN %s 1345*9c5db199SXin Li GROUP BY job_id, status, aborted, complete 1346*9c5db199SXin Li """ % id_list) 1347*9c5db199SXin Li all_job_counts = dict((job_id, {}) for job_id in job_ids) 1348*9c5db199SXin Li for job_id, status, aborted, complete, count in cursor.fetchall(): 1349*9c5db199SXin Li job_dict = all_job_counts[job_id] 1350*9c5db199SXin Li full_status = HostQueueEntry.compute_full_status(status, aborted, 1351*9c5db199SXin Li complete) 1352*9c5db199SXin Li job_dict.setdefault(full_status, 0) 1353*9c5db199SXin Li job_dict[full_status] += count 1354*9c5db199SXin Li return all_job_counts 1355*9c5db199SXin Li 1356*9c5db199SXin Li 1357*9c5db199SXin Liclass Job(dbmodels.Model, model_logic.ModelExtensions): 1358*9c5db199SXin Li """\ 1359*9c5db199SXin Li owner: username of job owner 1360*9c5db199SXin Li name: job name (does not have to be unique) 1361*9c5db199SXin Li priority: Integer priority value. Higher is more important. 1362*9c5db199SXin Li control_file: contents of control file 1363*9c5db199SXin Li control_type: Client or Server 1364*9c5db199SXin Li created_on: date of job creation 1365*9c5db199SXin Li submitted_on: date of job submission 1366*9c5db199SXin Li synch_count: how many hosts should be used per autoserv execution 1367*9c5db199SXin Li run_verify: Whether or not to run the verify phase 1368*9c5db199SXin Li run_reset: Whether or not to run the reset phase 1369*9c5db199SXin Li timeout: DEPRECATED - hours from queuing time until job times out 1370*9c5db199SXin Li timeout_mins: minutes from job queuing time until the job times out 1371*9c5db199SXin Li max_runtime_hrs: DEPRECATED - hours from job starting time until job 1372*9c5db199SXin Li times out 1373*9c5db199SXin Li max_runtime_mins: minutes from job starting time until job times out 1374*9c5db199SXin Li email_list: list of people to email on completion delimited by any of: 1375*9c5db199SXin Li white space, ',', ':', ';' 1376*9c5db199SXin Li dependency_labels: many-to-many relationship with labels corresponding to 1377*9c5db199SXin Li job dependencies 1378*9c5db199SXin Li reboot_before: Never, If dirty, or Always 1379*9c5db199SXin Li reboot_after: Never, If all tests passed, or Always 1380*9c5db199SXin Li parse_failed_repair: if True, a failed repair launched by this job will have 1381*9c5db199SXin Li its results parsed as part of the job. 1382*9c5db199SXin Li drone_set: The set of drones to run this job on 1383*9c5db199SXin Li parent_job: Parent job (optional) 1384*9c5db199SXin Li test_retry: Number of times to retry test if the test did not complete 1385*9c5db199SXin Li successfully. (optional, default: 0) 1386*9c5db199SXin Li require_ssp: Require server-side packaging unless require_ssp is set to 1387*9c5db199SXin Li False. (optional, default: None) 1388*9c5db199SXin Li """ 1389*9c5db199SXin Li 1390*9c5db199SXin Li # TODO: Investigate, if jobkeyval_set is really needed. 1391*9c5db199SXin Li # dynamic_suite will write them into an attached file for the drone, but 1392*9c5db199SXin Li # it doesn't seem like they are actually used. If they aren't used, remove 1393*9c5db199SXin Li # jobkeyval_set here. 1394*9c5db199SXin Li SERIALIZATION_LINKS_TO_FOLLOW = set(['dependency_labels', 1395*9c5db199SXin Li 'hostqueueentry_set', 1396*9c5db199SXin Li 'jobkeyval_set', 1397*9c5db199SXin Li 'shard']) 1398*9c5db199SXin Li 1399*9c5db199SXin Li EXCLUDE_KNOWN_JOBS_CLAUSE = ''' 1400*9c5db199SXin Li AND NOT (afe_host_queue_entries.aborted = 0 1401*9c5db199SXin Li AND afe_jobs.id IN (%(known_ids)s)) 1402*9c5db199SXin Li ''' 1403*9c5db199SXin Li 1404*9c5db199SXin Li EXCLUDE_OLD_JOBS_CLAUSE = 'AND (afe_jobs.created_on > "%(cutoff)s")' 1405*9c5db199SXin Li 1406*9c5db199SXin Li SQL_SHARD_JOBS = ''' 1407*9c5db199SXin Li SELECT DISTINCT(afe_jobs.id) FROM afe_jobs 1408*9c5db199SXin Li INNER JOIN afe_host_queue_entries 1409*9c5db199SXin Li ON (afe_jobs.id = afe_host_queue_entries.job_id) 1410*9c5db199SXin Li LEFT OUTER JOIN afe_jobs_dependency_labels 1411*9c5db199SXin Li ON (afe_jobs.id = afe_jobs_dependency_labels.job_id) 1412*9c5db199SXin Li JOIN afe_shards_labels 1413*9c5db199SXin Li ON (afe_shards_labels.label_id = afe_jobs_dependency_labels.label_id 1414*9c5db199SXin Li OR afe_shards_labels.label_id = afe_host_queue_entries.meta_host) 1415*9c5db199SXin Li WHERE (afe_shards_labels.shard_id = %(shard_id)s 1416*9c5db199SXin Li AND afe_host_queue_entries.complete != 1 1417*9c5db199SXin Li AND afe_host_queue_entries.active != 1 1418*9c5db199SXin Li %(exclude_known_jobs)s 1419*9c5db199SXin Li %(exclude_old_jobs)s) 1420*9c5db199SXin Li ''' 1421*9c5db199SXin Li 1422*9c5db199SXin Li # Jobs can be created with assigned hosts and have no dependency 1423*9c5db199SXin Li # labels nor meta_host. 1424*9c5db199SXin Li # We are looking for: 1425*9c5db199SXin Li # - a job whose hqe's meta_host is null 1426*9c5db199SXin Li # - a job whose hqe has a host 1427*9c5db199SXin Li # - one of the host's labels matches the shard's label. 1428*9c5db199SXin Li # Non-aborted known jobs, completed jobs, active jobs, jobs 1429*9c5db199SXin Li # without hqe are exluded as we do with SQL_SHARD_JOBS. 1430*9c5db199SXin Li SQL_SHARD_JOBS_WITH_HOSTS = ''' 1431*9c5db199SXin Li SELECT DISTINCT(afe_jobs.id) FROM afe_jobs 1432*9c5db199SXin Li INNER JOIN afe_host_queue_entries 1433*9c5db199SXin Li ON (afe_jobs.id = afe_host_queue_entries.job_id) 1434*9c5db199SXin Li LEFT OUTER JOIN %(host_label_table)s 1435*9c5db199SXin Li ON (afe_host_queue_entries.host_id = %(host_label_table)s.host_id) 1436*9c5db199SXin Li WHERE (%(host_label_table)s.%(host_label_column)s IN %(label_ids)s 1437*9c5db199SXin Li AND afe_host_queue_entries.complete != 1 1438*9c5db199SXin Li AND afe_host_queue_entries.active != 1 1439*9c5db199SXin Li AND afe_host_queue_entries.meta_host IS NULL 1440*9c5db199SXin Li AND afe_host_queue_entries.host_id IS NOT NULL 1441*9c5db199SXin Li %(exclude_known_jobs)s 1442*9c5db199SXin Li %(exclude_old_jobs)s) 1443*9c5db199SXin Li ''' 1444*9c5db199SXin Li 1445*9c5db199SXin Li # Even if we had filters about complete, active and aborted 1446*9c5db199SXin Li # bits in the above two SQLs, there is a chance that 1447*9c5db199SXin Li # the result may still contain a job with an hqe with 'complete=1' 1448*9c5db199SXin Li # or 'active=1'.' 1449*9c5db199SXin Li # This happens when a job has two (or more) hqes and at least 1450*9c5db199SXin Li # one hqe has different bits than others. 1451*9c5db199SXin Li # We use a second sql to ensure we exclude all un-desired jobs. 1452*9c5db199SXin Li SQL_JOBS_TO_EXCLUDE = ''' 1453*9c5db199SXin Li SELECT afe_jobs.id FROM afe_jobs 1454*9c5db199SXin Li INNER JOIN afe_host_queue_entries 1455*9c5db199SXin Li ON (afe_jobs.id = afe_host_queue_entries.job_id) 1456*9c5db199SXin Li WHERE (afe_jobs.id in (%(candidates)s) 1457*9c5db199SXin Li AND (afe_host_queue_entries.complete=1 1458*9c5db199SXin Li OR afe_host_queue_entries.active=1)) 1459*9c5db199SXin Li ''' 1460*9c5db199SXin Li 1461*9c5db199SXin Li def _deserialize_relation(self, link, data): 1462*9c5db199SXin Li if link in ['hostqueueentry_set', 'jobkeyval_set']: 1463*9c5db199SXin Li for obj in data: 1464*9c5db199SXin Li obj['job_id'] = self.id 1465*9c5db199SXin Li 1466*9c5db199SXin Li super(Job, self)._deserialize_relation(link, data) 1467*9c5db199SXin Li 1468*9c5db199SXin Li 1469*9c5db199SXin Li def custom_deserialize_relation(self, link, data): 1470*9c5db199SXin Li assert link == 'shard', 'Link %s should not be deserialized' % link 1471*9c5db199SXin Li self.shard = Shard.deserialize(data) 1472*9c5db199SXin Li 1473*9c5db199SXin Li 1474*9c5db199SXin Li def _check_update_from_shard(self, shard, updated_serialized): 1475*9c5db199SXin Li # If the job got aborted on the main after the client fetched it 1476*9c5db199SXin Li # no shard_id will be set. The shard might still push updates though, 1477*9c5db199SXin Li # as the job might complete before the abort bit syncs to the shard. 1478*9c5db199SXin Li # Alternative considered: The main scheduler could be changed to not 1479*9c5db199SXin Li # set aborted jobs to completed that are sharded out. But that would 1480*9c5db199SXin Li # require database queries and seemed more complicated to implement. 1481*9c5db199SXin Li # This seems safe to do, as there won't be updates pushed from the wrong 1482*9c5db199SXin Li # shards should be powered off and wiped hen they are removed from the 1483*9c5db199SXin Li # main. 1484*9c5db199SXin Li if self.shard_id and self.shard_id != shard.id: 1485*9c5db199SXin Li raise error.IgnorableUnallowedRecordsSentToMain( 1486*9c5db199SXin Li 'Job id=%s is assigned to shard (%s). Cannot update it with %s ' 1487*9c5db199SXin Li 'from shard %s.' % (self.id, self.shard_id, updated_serialized, 1488*9c5db199SXin Li shard.id)) 1489*9c5db199SXin Li 1490*9c5db199SXin Li 1491*9c5db199SXin Li RebootBefore = model_attributes.RebootBefore 1492*9c5db199SXin Li RebootAfter = model_attributes.RebootAfter 1493*9c5db199SXin Li # TIMEOUT is deprecated. 1494*9c5db199SXin Li DEFAULT_TIMEOUT = global_config.global_config.get_config_value( 1495*9c5db199SXin Li 'AUTOTEST_WEB', 'job_timeout_default', default=24) 1496*9c5db199SXin Li DEFAULT_TIMEOUT_MINS = global_config.global_config.get_config_value( 1497*9c5db199SXin Li 'AUTOTEST_WEB', 'job_timeout_mins_default', default=24*60) 1498*9c5db199SXin Li # MAX_RUNTIME_HRS is deprecated. Will be removed after switch to mins is 1499*9c5db199SXin Li # completed. 1500*9c5db199SXin Li DEFAULT_MAX_RUNTIME_HRS = global_config.global_config.get_config_value( 1501*9c5db199SXin Li 'AUTOTEST_WEB', 'job_max_runtime_hrs_default', default=72) 1502*9c5db199SXin Li DEFAULT_MAX_RUNTIME_MINS = global_config.global_config.get_config_value( 1503*9c5db199SXin Li 'AUTOTEST_WEB', 'job_max_runtime_mins_default', default=72*60) 1504*9c5db199SXin Li DEFAULT_PARSE_FAILED_REPAIR = global_config.global_config.get_config_value( 1505*9c5db199SXin Li 'AUTOTEST_WEB', 'parse_failed_repair_default', type=bool, default=False) 1506*9c5db199SXin Li FETCH_READONLY_JOBS = global_config.global_config.get_config_value( 1507*9c5db199SXin Li 'AUTOTEST_WEB','readonly_heartbeat', type=bool, default=False) 1508*9c5db199SXin Li # TODO(ayatane): Deprecated, not removed due to difficulty untangling imports 1509*9c5db199SXin Li SKIP_JOBS_CREATED_BEFORE = 0 1510*9c5db199SXin Li 1511*9c5db199SXin Li 1512*9c5db199SXin Li 1513*9c5db199SXin Li owner = dbmodels.CharField(max_length=255) 1514*9c5db199SXin Li name = dbmodels.CharField(max_length=255) 1515*9c5db199SXin Li priority = dbmodels.SmallIntegerField(default=priorities.Priority.DEFAULT) 1516*9c5db199SXin Li control_file = dbmodels.TextField(null=True, blank=True) 1517*9c5db199SXin Li control_type = dbmodels.SmallIntegerField( 1518*9c5db199SXin Li choices=control_data.CONTROL_TYPE.choices(), 1519*9c5db199SXin Li blank=True, # to allow 0 1520*9c5db199SXin Li default=control_data.CONTROL_TYPE.CLIENT) 1521*9c5db199SXin Li created_on = dbmodels.DateTimeField() 1522*9c5db199SXin Li synch_count = dbmodels.IntegerField(blank=True, default=0) 1523*9c5db199SXin Li timeout = dbmodels.IntegerField(default=DEFAULT_TIMEOUT) 1524*9c5db199SXin Li run_verify = dbmodels.BooleanField(default=False) 1525*9c5db199SXin Li email_list = dbmodels.CharField(max_length=250, blank=True) 1526*9c5db199SXin Li dependency_labels = ( 1527*9c5db199SXin Li dbmodels.ManyToManyField(Label, blank=True, 1528*9c5db199SXin Li db_table='afe_jobs_dependency_labels')) 1529*9c5db199SXin Li reboot_before = dbmodels.SmallIntegerField( 1530*9c5db199SXin Li choices=model_attributes.RebootBefore.choices(), blank=True, 1531*9c5db199SXin Li default=DEFAULT_REBOOT_BEFORE) 1532*9c5db199SXin Li reboot_after = dbmodels.SmallIntegerField( 1533*9c5db199SXin Li choices=model_attributes.RebootAfter.choices(), blank=True, 1534*9c5db199SXin Li default=DEFAULT_REBOOT_AFTER) 1535*9c5db199SXin Li parse_failed_repair = dbmodels.BooleanField( 1536*9c5db199SXin Li default=DEFAULT_PARSE_FAILED_REPAIR) 1537*9c5db199SXin Li # max_runtime_hrs is deprecated. Will be removed after switch to mins is 1538*9c5db199SXin Li # completed. 1539*9c5db199SXin Li max_runtime_hrs = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_HRS) 1540*9c5db199SXin Li max_runtime_mins = dbmodels.IntegerField(default=DEFAULT_MAX_RUNTIME_MINS) 1541*9c5db199SXin Li drone_set = dbmodels.ForeignKey(DroneSet, null=True, blank=True) 1542*9c5db199SXin Li 1543*9c5db199SXin Li # TODO(jrbarnette) We have to keep `parameterized_job` around or it 1544*9c5db199SXin Li # breaks the scheduler_models unit tests (and fixing the unit tests 1545*9c5db199SXin Li # will break the scheduler, so don't do that). 1546*9c5db199SXin Li # 1547*9c5db199SXin Li # The ultimate fix is to delete the column from the database table 1548*9c5db199SXin Li # at which point, you _must_ delete this. Until you're ready to do 1549*9c5db199SXin Li # that, DON'T MUCK WITH IT. 1550*9c5db199SXin Li parameterized_job = dbmodels.ForeignKey(ParameterizedJob, null=True, 1551*9c5db199SXin Li blank=True) 1552*9c5db199SXin Li 1553*9c5db199SXin Li parent_job = dbmodels.ForeignKey('self', blank=True, null=True) 1554*9c5db199SXin Li 1555*9c5db199SXin Li test_retry = dbmodels.IntegerField(blank=True, default=0) 1556*9c5db199SXin Li 1557*9c5db199SXin Li run_reset = dbmodels.BooleanField(default=True) 1558*9c5db199SXin Li 1559*9c5db199SXin Li timeout_mins = dbmodels.IntegerField(default=DEFAULT_TIMEOUT_MINS) 1560*9c5db199SXin Li 1561*9c5db199SXin Li # If this is None on the main, a shard should be found. 1562*9c5db199SXin Li # If this is None on a shard, it should be synced back to the main 1563*9c5db199SXin Li shard = dbmodels.ForeignKey(Shard, blank=True, null=True) 1564*9c5db199SXin Li 1565*9c5db199SXin Li # If this is None, server-side packaging will be used for server side test. 1566*9c5db199SXin Li require_ssp = dbmodels.NullBooleanField(default=None, blank=True, null=True) 1567*9c5db199SXin Li 1568*9c5db199SXin Li # custom manager 1569*9c5db199SXin Li objects = JobManager() 1570*9c5db199SXin Li 1571*9c5db199SXin Li 1572*9c5db199SXin Li @decorators.cached_property 1573*9c5db199SXin Li def labels(self): 1574*9c5db199SXin Li """All the labels of this job""" 1575*9c5db199SXin Li # We need to convert dependency_labels to a list, because all() gives us 1576*9c5db199SXin Li # back an iterator, and storing/caching an iterator means we'd only be 1577*9c5db199SXin Li # able to read from it once. 1578*9c5db199SXin Li return list(self.dependency_labels.all()) 1579*9c5db199SXin Li 1580*9c5db199SXin Li 1581*9c5db199SXin Li def is_server_job(self): 1582*9c5db199SXin Li """Returns whether this job is of type server.""" 1583*9c5db199SXin Li return self.control_type == control_data.CONTROL_TYPE.SERVER 1584*9c5db199SXin Li 1585*9c5db199SXin Li 1586*9c5db199SXin Li @classmethod 1587*9c5db199SXin Li def create(cls, owner, options, hosts): 1588*9c5db199SXin Li """Creates a job. 1589*9c5db199SXin Li 1590*9c5db199SXin Li The job is created by taking some information (the listed args) and 1591*9c5db199SXin Li filling in the rest of the necessary information. 1592*9c5db199SXin Li 1593*9c5db199SXin Li @param cls: Implicit class object. 1594*9c5db199SXin Li @param owner: The owner for the job. 1595*9c5db199SXin Li @param options: An options object. 1596*9c5db199SXin Li @param hosts: The hosts to use. 1597*9c5db199SXin Li """ 1598*9c5db199SXin Li AclGroup.check_for_acl_violation_hosts(hosts) 1599*9c5db199SXin Li 1600*9c5db199SXin Li control_file = options.get('control_file') 1601*9c5db199SXin Li 1602*9c5db199SXin Li user = User.current_user() 1603*9c5db199SXin Li if options.get('reboot_before') is None: 1604*9c5db199SXin Li options['reboot_before'] = user.get_reboot_before_display() 1605*9c5db199SXin Li if options.get('reboot_after') is None: 1606*9c5db199SXin Li options['reboot_after'] = user.get_reboot_after_display() 1607*9c5db199SXin Li 1608*9c5db199SXin Li drone_set = DroneSet.resolve_name(options.get('drone_set')) 1609*9c5db199SXin Li 1610*9c5db199SXin Li if options.get('timeout_mins') is None and options.get('timeout'): 1611*9c5db199SXin Li options['timeout_mins'] = options['timeout'] * 60 1612*9c5db199SXin Li 1613*9c5db199SXin Li job = cls.add_object( 1614*9c5db199SXin Li owner=owner, 1615*9c5db199SXin Li name=options['name'], 1616*9c5db199SXin Li priority=options['priority'], 1617*9c5db199SXin Li control_file=control_file, 1618*9c5db199SXin Li control_type=options['control_type'], 1619*9c5db199SXin Li synch_count=options.get('synch_count'), 1620*9c5db199SXin Li # timeout needs to be deleted in the future. 1621*9c5db199SXin Li timeout=options.get('timeout'), 1622*9c5db199SXin Li timeout_mins=options.get('timeout_mins'), 1623*9c5db199SXin Li max_runtime_mins=options.get('max_runtime_mins'), 1624*9c5db199SXin Li run_verify=options.get('run_verify'), 1625*9c5db199SXin Li email_list=options.get('email_list'), 1626*9c5db199SXin Li reboot_before=options.get('reboot_before'), 1627*9c5db199SXin Li reboot_after=options.get('reboot_after'), 1628*9c5db199SXin Li parse_failed_repair=options.get('parse_failed_repair'), 1629*9c5db199SXin Li created_on=datetime.now(), 1630*9c5db199SXin Li drone_set=drone_set, 1631*9c5db199SXin Li parent_job=options.get('parent_job_id'), 1632*9c5db199SXin Li test_retry=options.get('test_retry'), 1633*9c5db199SXin Li run_reset=options.get('run_reset'), 1634*9c5db199SXin Li require_ssp=options.get('require_ssp')) 1635*9c5db199SXin Li 1636*9c5db199SXin Li job.dependency_labels = options['dependencies'] 1637*9c5db199SXin Li 1638*9c5db199SXin Li if options.get('keyvals'): 1639*9c5db199SXin Li for key, value in six.iteritems(options['keyvals']): 1640*9c5db199SXin Li # None (or NULL) is not acceptable by DB, so change it to an 1641*9c5db199SXin Li # empty string in case. 1642*9c5db199SXin Li JobKeyval.objects.create(job=job, key=key, 1643*9c5db199SXin Li value='' if value is None else value) 1644*9c5db199SXin Li 1645*9c5db199SXin Li return job 1646*9c5db199SXin Li 1647*9c5db199SXin Li 1648*9c5db199SXin Li @classmethod 1649*9c5db199SXin Li def assign_to_shard(cls, shard, known_ids): 1650*9c5db199SXin Li """Assigns unassigned jobs to a shard. 1651*9c5db199SXin Li 1652*9c5db199SXin Li For all labels that have been assigned to this shard, all jobs that 1653*9c5db199SXin Li have this label are assigned to this shard. 1654*9c5db199SXin Li 1655*9c5db199SXin Li @param shard: The shard to assign jobs to. 1656*9c5db199SXin Li @param known_ids: List of all ids of incomplete jobs the shard already 1657*9c5db199SXin Li knows about. 1658*9c5db199SXin Li 1659*9c5db199SXin Li @returns The job objects that should be sent to the shard. 1660*9c5db199SXin Li """ 1661*9c5db199SXin Li with cls._readonly_job_query_context(): 1662*9c5db199SXin Li job_ids = cls._get_new_jobs_for_shard(shard, known_ids) 1663*9c5db199SXin Li if not job_ids: 1664*9c5db199SXin Li return [] 1665*9c5db199SXin Li cls._assign_jobs_to_shard(job_ids, shard) 1666*9c5db199SXin Li return cls._jobs_with_ids(job_ids) 1667*9c5db199SXin Li 1668*9c5db199SXin Li 1669*9c5db199SXin Li @classmethod 1670*9c5db199SXin Li @contextlib.contextmanager 1671*9c5db199SXin Li def _readonly_job_query_context(cls): 1672*9c5db199SXin Li #TODO: Get rid of this kludge if/when we update Django to >=1.7 1673*9c5db199SXin Li #correct usage would be .raw(..., using='readonly') 1674*9c5db199SXin Li old_db = Job.objects._db 1675*9c5db199SXin Li try: 1676*9c5db199SXin Li if cls.FETCH_READONLY_JOBS: 1677*9c5db199SXin Li Job.objects._db = 'readonly' 1678*9c5db199SXin Li yield 1679*9c5db199SXin Li finally: 1680*9c5db199SXin Li Job.objects._db = old_db 1681*9c5db199SXin Li 1682*9c5db199SXin Li 1683*9c5db199SXin Li @classmethod 1684*9c5db199SXin Li def _assign_jobs_to_shard(cls, job_ids, shard): 1685*9c5db199SXin Li Job.objects.filter(pk__in=job_ids).update(shard=shard) 1686*9c5db199SXin Li 1687*9c5db199SXin Li 1688*9c5db199SXin Li @classmethod 1689*9c5db199SXin Li def _jobs_with_ids(cls, job_ids): 1690*9c5db199SXin Li return list(Job.objects.filter(pk__in=job_ids).all()) 1691*9c5db199SXin Li 1692*9c5db199SXin Li 1693*9c5db199SXin Li @classmethod 1694*9c5db199SXin Li def _get_new_jobs_for_shard(cls, shard, known_ids): 1695*9c5db199SXin Li job_ids = cls._get_jobs_without_hosts(shard, known_ids) 1696*9c5db199SXin Li job_ids |= cls._get_jobs_with_hosts(shard, known_ids) 1697*9c5db199SXin Li if job_ids: 1698*9c5db199SXin Li job_ids -= cls._filter_finished_jobs(job_ids) 1699*9c5db199SXin Li return job_ids 1700*9c5db199SXin Li 1701*9c5db199SXin Li 1702*9c5db199SXin Li @classmethod 1703*9c5db199SXin Li def _filter_finished_jobs(cls, job_ids): 1704*9c5db199SXin Li query = Job.objects.raw( 1705*9c5db199SXin Li cls.SQL_JOBS_TO_EXCLUDE % 1706*9c5db199SXin Li {'candidates': ','.join([str(i) for i in job_ids])}) 1707*9c5db199SXin Li return set([j.id for j in query]) 1708*9c5db199SXin Li 1709*9c5db199SXin Li 1710*9c5db199SXin Li @classmethod 1711*9c5db199SXin Li def _get_jobs_without_hosts(cls, shard, known_ids): 1712*9c5db199SXin Li raw_sql = cls.SQL_SHARD_JOBS % { 1713*9c5db199SXin Li 'exclude_known_jobs': cls._exclude_known_jobs_clause(known_ids), 1714*9c5db199SXin Li 'exclude_old_jobs': cls._exclude_old_jobs_clause(), 1715*9c5db199SXin Li 'shard_id': shard.id 1716*9c5db199SXin Li } 1717*9c5db199SXin Li return set([j.id for j in Job.objects.raw(raw_sql)]) 1718*9c5db199SXin Li 1719*9c5db199SXin Li 1720*9c5db199SXin Li @classmethod 1721*9c5db199SXin Li def _get_jobs_with_hosts(cls, shard, known_ids): 1722*9c5db199SXin Li job_ids = set([]) 1723*9c5db199SXin Li static_labels, non_static_labels = Host.classify_label_objects( 1724*9c5db199SXin Li shard.labels.all()) 1725*9c5db199SXin Li if static_labels: 1726*9c5db199SXin Li label_ids = [str(l.id) for l in static_labels] 1727*9c5db199SXin Li query = Job.objects.raw(cls.SQL_SHARD_JOBS_WITH_HOSTS % { 1728*9c5db199SXin Li 'exclude_known_jobs': cls._exclude_known_jobs_clause(known_ids), 1729*9c5db199SXin Li 'exclude_old_jobs': cls._exclude_old_jobs_clause(), 1730*9c5db199SXin Li 'host_label_table': 'afe_static_hosts_labels', 1731*9c5db199SXin Li 'host_label_column': 'staticlabel_id', 1732*9c5db199SXin Li 'label_ids': '(%s)' % ','.join(label_ids)}) 1733*9c5db199SXin Li job_ids |= set([j.id for j in query]) 1734*9c5db199SXin Li if non_static_labels: 1735*9c5db199SXin Li label_ids = [str(l.id) for l in non_static_labels] 1736*9c5db199SXin Li query = Job.objects.raw(cls.SQL_SHARD_JOBS_WITH_HOSTS % { 1737*9c5db199SXin Li 'exclude_known_jobs': cls._exclude_known_jobs_clause(known_ids), 1738*9c5db199SXin Li 'exclude_old_jobs': cls._exclude_old_jobs_clause(), 1739*9c5db199SXin Li 'host_label_table': 'afe_hosts_labels', 1740*9c5db199SXin Li 'host_label_column': 'label_id', 1741*9c5db199SXin Li 'label_ids': '(%s)' % ','.join(label_ids)}) 1742*9c5db199SXin Li job_ids |= set([j.id for j in query]) 1743*9c5db199SXin Li return job_ids 1744*9c5db199SXin Li 1745*9c5db199SXin Li 1746*9c5db199SXin Li @classmethod 1747*9c5db199SXin Li def _exclude_known_jobs_clause(cls, known_ids): 1748*9c5db199SXin Li if not known_ids: 1749*9c5db199SXin Li return '' 1750*9c5db199SXin Li return (cls.EXCLUDE_KNOWN_JOBS_CLAUSE % 1751*9c5db199SXin Li {'known_ids': ','.join([str(i) for i in known_ids])}) 1752*9c5db199SXin Li 1753*9c5db199SXin Li 1754*9c5db199SXin Li @classmethod 1755*9c5db199SXin Li def _exclude_old_jobs_clause(cls): 1756*9c5db199SXin Li """Filter queried jobs to be created within a few hours in the past. 1757*9c5db199SXin Li 1758*9c5db199SXin Li With this clause, any jobs older than a configurable number of hours are 1759*9c5db199SXin Li skipped in the jobs query. 1760*9c5db199SXin Li The job creation window affects the overall query performance. Longer 1761*9c5db199SXin Li creation windows require a range query over more Job table rows using 1762*9c5db199SXin Li the created_on column index. c.f. http://crbug.com/966872#c35 1763*9c5db199SXin Li """ 1764*9c5db199SXin Li if cls.SKIP_JOBS_CREATED_BEFORE <= 0: 1765*9c5db199SXin Li return '' 1766*9c5db199SXin Li cutoff = datetime.now()- timedelta(hours=cls.SKIP_JOBS_CREATED_BEFORE) 1767*9c5db199SXin Li return (cls.EXCLUDE_OLD_JOBS_CLAUSE % 1768*9c5db199SXin Li {'cutoff': cutoff.strftime('%Y-%m-%d %H:%M:%S')}) 1769*9c5db199SXin Li 1770*9c5db199SXin Li 1771*9c5db199SXin Li def queue(self, hosts, is_template=False): 1772*9c5db199SXin Li """Enqueue a job on the given hosts. 1773*9c5db199SXin Li 1774*9c5db199SXin Li @param hosts: The hosts to use. 1775*9c5db199SXin Li @param is_template: Whether the status should be "Template". 1776*9c5db199SXin Li """ 1777*9c5db199SXin Li if not hosts: 1778*9c5db199SXin Li # hostless job 1779*9c5db199SXin Li entry = HostQueueEntry.create(job=self, is_template=is_template) 1780*9c5db199SXin Li entry.save() 1781*9c5db199SXin Li return 1782*9c5db199SXin Li 1783*9c5db199SXin Li for host in hosts: 1784*9c5db199SXin Li host.enqueue_job(self, is_template=is_template) 1785*9c5db199SXin Li 1786*9c5db199SXin Li 1787*9c5db199SXin Li def user(self): 1788*9c5db199SXin Li """Gets the user of this job, or None if it doesn't exist.""" 1789*9c5db199SXin Li try: 1790*9c5db199SXin Li return User.objects.get(login=self.owner) 1791*9c5db199SXin Li except self.DoesNotExist: 1792*9c5db199SXin Li return None 1793*9c5db199SXin Li 1794*9c5db199SXin Li 1795*9c5db199SXin Li def abort(self): 1796*9c5db199SXin Li """Aborts this job.""" 1797*9c5db199SXin Li for queue_entry in self.hostqueueentry_set.all(): 1798*9c5db199SXin Li queue_entry.abort() 1799*9c5db199SXin Li 1800*9c5db199SXin Li 1801*9c5db199SXin Li def tag(self): 1802*9c5db199SXin Li """Returns a string tag for this job.""" 1803*9c5db199SXin Li return server_utils.get_job_tag(self.id, self.owner) 1804*9c5db199SXin Li 1805*9c5db199SXin Li 1806*9c5db199SXin Li def keyval_dict(self): 1807*9c5db199SXin Li """Returns all keyvals for this job as a dictionary.""" 1808*9c5db199SXin Li return dict((keyval.key, keyval.value) 1809*9c5db199SXin Li for keyval in self.jobkeyval_set.all()) 1810*9c5db199SXin Li 1811*9c5db199SXin Li 1812*9c5db199SXin Li @classmethod 1813*9c5db199SXin Li def get_attribute_model(cls): 1814*9c5db199SXin Li """Return the attribute model. 1815*9c5db199SXin Li 1816*9c5db199SXin Li Override method in parent class. This class is called when 1817*9c5db199SXin Li deserializing the one-to-many relationship betwen Job and JobKeyval. 1818*9c5db199SXin Li On deserialization, we will try to clear any existing job keyvals 1819*9c5db199SXin Li associated with a job to avoid any inconsistency. 1820*9c5db199SXin Li Though Job doesn't implement ModelWithAttribute, we still treat 1821*9c5db199SXin Li it as an attribute model for this purpose. 1822*9c5db199SXin Li 1823*9c5db199SXin Li @returns: The attribute model of Job. 1824*9c5db199SXin Li """ 1825*9c5db199SXin Li return JobKeyval 1826*9c5db199SXin Li 1827*9c5db199SXin Li 1828*9c5db199SXin Li class Meta: 1829*9c5db199SXin Li """Metadata for class Job.""" 1830*9c5db199SXin Li db_table = 'afe_jobs' 1831*9c5db199SXin Li 1832*9c5db199SXin Li def __unicode__(self): 1833*9c5db199SXin Li return u'%s (%s-%s)' % (self.name, self.id, self.owner) 1834*9c5db199SXin Li 1835*9c5db199SXin Li 1836*9c5db199SXin Liclass JobHandoff(dbmodels.Model, model_logic.ModelExtensions): 1837*9c5db199SXin Li """Jobs that have been handed off to lucifer.""" 1838*9c5db199SXin Li 1839*9c5db199SXin Li job = dbmodels.OneToOneField(Job, on_delete=dbmodels.CASCADE, 1840*9c5db199SXin Li primary_key=True) 1841*9c5db199SXin Li created = dbmodels.DateTimeField(auto_now_add=True) 1842*9c5db199SXin Li completed = dbmodels.BooleanField(default=False) 1843*9c5db199SXin Li drone = dbmodels.CharField( 1844*9c5db199SXin Li max_length=128, null=True, 1845*9c5db199SXin Li help_text=''' 1846*9c5db199SXin LiThe hostname of the drone the job is running on and whose job_aborter 1847*9c5db199SXin Lishould be responsible for aborting the job if the job process dies. 1848*9c5db199SXin LiNULL means any drone's job_aborter has free reign to abort the job. 1849*9c5db199SXin Li''') 1850*9c5db199SXin Li 1851*9c5db199SXin Li class Meta: 1852*9c5db199SXin Li """Metadata for class Job.""" 1853*9c5db199SXin Li db_table = 'afe_job_handoffs' 1854*9c5db199SXin Li 1855*9c5db199SXin Li 1856*9c5db199SXin Liclass JobKeyval(dbmodels.Model, model_logic.ModelExtensions): 1857*9c5db199SXin Li """Keyvals associated with jobs""" 1858*9c5db199SXin Li 1859*9c5db199SXin Li SERIALIZATION_LINKS_TO_KEEP = set(['job']) 1860*9c5db199SXin Li SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['value']) 1861*9c5db199SXin Li 1862*9c5db199SXin Li job = dbmodels.ForeignKey(Job) 1863*9c5db199SXin Li key = dbmodels.CharField(max_length=90) 1864*9c5db199SXin Li value = dbmodels.CharField(max_length=300) 1865*9c5db199SXin Li 1866*9c5db199SXin Li objects = model_logic.ExtendedManager() 1867*9c5db199SXin Li 1868*9c5db199SXin Li 1869*9c5db199SXin Li @classmethod 1870*9c5db199SXin Li def get_record(cls, data): 1871*9c5db199SXin Li """Check the database for an identical record. 1872*9c5db199SXin Li 1873*9c5db199SXin Li Use job_id and key to search for a existing record. 1874*9c5db199SXin Li 1875*9c5db199SXin Li @raises: DoesNotExist, if no record found 1876*9c5db199SXin Li @raises: MultipleObjectsReturned if multiple records found. 1877*9c5db199SXin Li """ 1878*9c5db199SXin Li # TODO(fdeng): We should use job_id and key together as 1879*9c5db199SXin Li # a primary key in the db. 1880*9c5db199SXin Li return cls.objects.get(job_id=data['job_id'], key=data['key']) 1881*9c5db199SXin Li 1882*9c5db199SXin Li 1883*9c5db199SXin Li @classmethod 1884*9c5db199SXin Li def deserialize(cls, data): 1885*9c5db199SXin Li """Override deserialize in parent class. 1886*9c5db199SXin Li 1887*9c5db199SXin Li Do not deserialize id as id is not kept consistent on main and shards. 1888*9c5db199SXin Li 1889*9c5db199SXin Li @param data: A dictionary of data to deserialize. 1890*9c5db199SXin Li 1891*9c5db199SXin Li @returns: A JobKeyval object. 1892*9c5db199SXin Li """ 1893*9c5db199SXin Li if data: 1894*9c5db199SXin Li data.pop('id') 1895*9c5db199SXin Li return super(JobKeyval, cls).deserialize(data) 1896*9c5db199SXin Li 1897*9c5db199SXin Li 1898*9c5db199SXin Li class Meta: 1899*9c5db199SXin Li """Metadata for class JobKeyval.""" 1900*9c5db199SXin Li db_table = 'afe_job_keyvals' 1901*9c5db199SXin Li 1902*9c5db199SXin Li 1903*9c5db199SXin Liclass IneligibleHostQueue(dbmodels.Model, model_logic.ModelExtensions): 1904*9c5db199SXin Li """Represents an ineligible host queue.""" 1905*9c5db199SXin Li job = dbmodels.ForeignKey(Job) 1906*9c5db199SXin Li host = dbmodels.ForeignKey(Host) 1907*9c5db199SXin Li 1908*9c5db199SXin Li objects = model_logic.ExtendedManager() 1909*9c5db199SXin Li 1910*9c5db199SXin Li class Meta: 1911*9c5db199SXin Li """Metadata for class IneligibleHostQueue.""" 1912*9c5db199SXin Li db_table = 'afe_ineligible_host_queues' 1913*9c5db199SXin Li 1914*9c5db199SXin Li 1915*9c5db199SXin Liclass HostQueueEntry(dbmodels.Model, model_logic.ModelExtensions): 1916*9c5db199SXin Li """Represents a host queue entry.""" 1917*9c5db199SXin Li 1918*9c5db199SXin Li SERIALIZATION_LINKS_TO_FOLLOW = set(['meta_host']) 1919*9c5db199SXin Li SERIALIZATION_LINKS_TO_KEEP = set(['host']) 1920*9c5db199SXin Li SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set(['aborted']) 1921*9c5db199SXin Li 1922*9c5db199SXin Li 1923*9c5db199SXin Li def custom_deserialize_relation(self, link, data): 1924*9c5db199SXin Li assert link == 'meta_host' 1925*9c5db199SXin Li self.meta_host = Label.deserialize(data) 1926*9c5db199SXin Li 1927*9c5db199SXin Li 1928*9c5db199SXin Li def _check_update_from_shard(self, shard, updated_serialized, 1929*9c5db199SXin Li job_ids_sent): 1930*9c5db199SXin Li if self.job_id not in job_ids_sent: 1931*9c5db199SXin Li raise error.IgnorableUnallowedRecordsSentToMain( 1932*9c5db199SXin Li 'Sent HostQueueEntry without corresponding ' 1933*9c5db199SXin Li 'job entry: %s' % updated_serialized) 1934*9c5db199SXin Li 1935*9c5db199SXin Li 1936*9c5db199SXin Li Status = host_queue_entry_states.Status 1937*9c5db199SXin Li ACTIVE_STATUSES = host_queue_entry_states.ACTIVE_STATUSES 1938*9c5db199SXin Li COMPLETE_STATUSES = host_queue_entry_states.COMPLETE_STATUSES 1939*9c5db199SXin Li PRE_JOB_STATUSES = host_queue_entry_states.PRE_JOB_STATUSES 1940*9c5db199SXin Li IDLE_PRE_JOB_STATUSES = host_queue_entry_states.IDLE_PRE_JOB_STATUSES 1941*9c5db199SXin Li 1942*9c5db199SXin Li job = dbmodels.ForeignKey(Job) 1943*9c5db199SXin Li host = dbmodels.ForeignKey(Host, blank=True, null=True) 1944*9c5db199SXin Li status = dbmodels.CharField(max_length=255) 1945*9c5db199SXin Li meta_host = dbmodels.ForeignKey(Label, blank=True, null=True, 1946*9c5db199SXin Li db_column='meta_host') 1947*9c5db199SXin Li active = dbmodels.BooleanField(default=False) 1948*9c5db199SXin Li complete = dbmodels.BooleanField(default=False) 1949*9c5db199SXin Li deleted = dbmodels.BooleanField(default=False) 1950*9c5db199SXin Li execution_subdir = dbmodels.CharField(max_length=255, blank=True, 1951*9c5db199SXin Li default='') 1952*9c5db199SXin Li # If atomic_group is set, this is a virtual HostQueueEntry that will 1953*9c5db199SXin Li # be expanded into many actual hosts within the group at schedule time. 1954*9c5db199SXin Li atomic_group = dbmodels.ForeignKey(AtomicGroup, blank=True, null=True) 1955*9c5db199SXin Li aborted = dbmodels.BooleanField(default=False) 1956*9c5db199SXin Li started_on = dbmodels.DateTimeField(null=True, blank=True) 1957*9c5db199SXin Li finished_on = dbmodels.DateTimeField(null=True, blank=True) 1958*9c5db199SXin Li 1959*9c5db199SXin Li objects = model_logic.ExtendedManager() 1960*9c5db199SXin Li 1961*9c5db199SXin Li 1962*9c5db199SXin Li def __init__(self, *args, **kwargs): 1963*9c5db199SXin Li super(HostQueueEntry, self).__init__(*args, **kwargs) 1964*9c5db199SXin Li self._record_attributes(['status']) 1965*9c5db199SXin Li 1966*9c5db199SXin Li 1967*9c5db199SXin Li @classmethod 1968*9c5db199SXin Li def create(cls, job, host=None, meta_host=None, 1969*9c5db199SXin Li is_template=False): 1970*9c5db199SXin Li """Creates a new host queue entry. 1971*9c5db199SXin Li 1972*9c5db199SXin Li @param cls: Implicit class object. 1973*9c5db199SXin Li @param job: The associated job. 1974*9c5db199SXin Li @param host: The associated host. 1975*9c5db199SXin Li @param meta_host: The associated meta host. 1976*9c5db199SXin Li @param is_template: Whether the status should be "Template". 1977*9c5db199SXin Li """ 1978*9c5db199SXin Li if is_template: 1979*9c5db199SXin Li status = cls.Status.TEMPLATE 1980*9c5db199SXin Li else: 1981*9c5db199SXin Li status = cls.Status.QUEUED 1982*9c5db199SXin Li 1983*9c5db199SXin Li return cls(job=job, host=host, meta_host=meta_host, status=status) 1984*9c5db199SXin Li 1985*9c5db199SXin Li 1986*9c5db199SXin Li def save(self, *args, **kwargs): 1987*9c5db199SXin Li self._set_active_and_complete() 1988*9c5db199SXin Li super(HostQueueEntry, self).save(*args, **kwargs) 1989*9c5db199SXin Li self._check_for_updated_attributes() 1990*9c5db199SXin Li 1991*9c5db199SXin Li 1992*9c5db199SXin Li def execution_path(self): 1993*9c5db199SXin Li """ 1994*9c5db199SXin Li Path to this entry's results (relative to the base results directory). 1995*9c5db199SXin Li """ 1996*9c5db199SXin Li return server_utils.get_hqe_exec_path(self.job.tag(), 1997*9c5db199SXin Li self.execution_subdir) 1998*9c5db199SXin Li 1999*9c5db199SXin Li 2000*9c5db199SXin Li def host_or_metahost_name(self): 2001*9c5db199SXin Li """Returns the first non-None name found in priority order. 2002*9c5db199SXin Li 2003*9c5db199SXin Li The priority order checked is: (1) host name; (2) meta host name 2004*9c5db199SXin Li """ 2005*9c5db199SXin Li if self.host: 2006*9c5db199SXin Li return self.host.hostname 2007*9c5db199SXin Li else: 2008*9c5db199SXin Li assert self.meta_host 2009*9c5db199SXin Li return self.meta_host.name 2010*9c5db199SXin Li 2011*9c5db199SXin Li 2012*9c5db199SXin Li def _set_active_and_complete(self): 2013*9c5db199SXin Li if self.status in self.ACTIVE_STATUSES: 2014*9c5db199SXin Li self.active, self.complete = True, False 2015*9c5db199SXin Li elif self.status in self.COMPLETE_STATUSES: 2016*9c5db199SXin Li self.active, self.complete = False, True 2017*9c5db199SXin Li else: 2018*9c5db199SXin Li self.active, self.complete = False, False 2019*9c5db199SXin Li 2020*9c5db199SXin Li 2021*9c5db199SXin Li def on_attribute_changed(self, attribute, old_value): 2022*9c5db199SXin Li assert attribute == 'status' 2023*9c5db199SXin Li logging.info('%s/%d (%d) -> %s', self.host, self.job.id, self.id, 2024*9c5db199SXin Li self.status) 2025*9c5db199SXin Li 2026*9c5db199SXin Li 2027*9c5db199SXin Li def is_meta_host_entry(self): 2028*9c5db199SXin Li 'True if this is a entry has a meta_host instead of a host.' 2029*9c5db199SXin Li return self.host is None and self.meta_host is not None 2030*9c5db199SXin Li 2031*9c5db199SXin Li 2032*9c5db199SXin Li # This code is shared between rpc_interface and models.HostQueueEntry. 2033*9c5db199SXin Li # Sadly due to circular imports between the 2 (crbug.com/230100) making it 2034*9c5db199SXin Li # a class method was the best way to refactor it. Attempting to put it in 2035*9c5db199SXin Li # rpc_utils or a new utils module failed as that would require us to import 2036*9c5db199SXin Li # models.py but to call it from here we would have to import the utils.py 2037*9c5db199SXin Li # thus creating a cycle. 2038*9c5db199SXin Li @classmethod 2039*9c5db199SXin Li def abort_host_queue_entries(cls, host_queue_entries): 2040*9c5db199SXin Li """Aborts a collection of host_queue_entries. 2041*9c5db199SXin Li 2042*9c5db199SXin Li Abort these host queue entry and all host queue entries of jobs created 2043*9c5db199SXin Li by them. 2044*9c5db199SXin Li 2045*9c5db199SXin Li @param host_queue_entries: List of host queue entries we want to abort. 2046*9c5db199SXin Li """ 2047*9c5db199SXin Li # This isn't completely immune to race conditions since it's not atomic, 2048*9c5db199SXin Li # but it should be safe given the scheduler's behavior. 2049*9c5db199SXin Li 2050*9c5db199SXin Li # TODO(milleral): crbug.com/230100 2051*9c5db199SXin Li # The |abort_host_queue_entries| rpc does nearly exactly this, 2052*9c5db199SXin Li # however, trying to re-use the code generates some horrible 2053*9c5db199SXin Li # circular import error. I'd be nice to refactor things around 2054*9c5db199SXin Li # sometime so the code could be reused. 2055*9c5db199SXin Li 2056*9c5db199SXin Li # Fixpoint algorithm to find the whole tree of HQEs to abort to 2057*9c5db199SXin Li # minimize the total number of database queries: 2058*9c5db199SXin Li children = set() 2059*9c5db199SXin Li new_children = set(host_queue_entries) 2060*9c5db199SXin Li while new_children: 2061*9c5db199SXin Li children.update(new_children) 2062*9c5db199SXin Li new_child_ids = [hqe.job_id for hqe in new_children] 2063*9c5db199SXin Li new_children = HostQueueEntry.objects.filter( 2064*9c5db199SXin Li job__parent_job__in=new_child_ids, 2065*9c5db199SXin Li complete=False, aborted=False).all() 2066*9c5db199SXin Li # To handle circular parental relationships 2067*9c5db199SXin Li new_children = set(new_children) - children 2068*9c5db199SXin Li 2069*9c5db199SXin Li # Associate a user with the host queue entries that we're about 2070*9c5db199SXin Li # to abort so that we can look up who to blame for the aborts. 2071*9c5db199SXin Li child_ids = [hqe.id for hqe in children] 2072*9c5db199SXin Li # Get a list of hqe ids that already exists, so we can exclude them when 2073*9c5db199SXin Li # we do bulk_create later to avoid IntegrityError. 2074*9c5db199SXin Li existing_hqe_ids = set(AbortedHostQueueEntry.objects. 2075*9c5db199SXin Li filter(queue_entry_id__in=child_ids). 2076*9c5db199SXin Li values_list('queue_entry_id', flat=True)) 2077*9c5db199SXin Li now = datetime.now() 2078*9c5db199SXin Li user = User.current_user() 2079*9c5db199SXin Li aborted_hqes = [AbortedHostQueueEntry(queue_entry=hqe, 2080*9c5db199SXin Li aborted_by=user, aborted_on=now) for hqe in children 2081*9c5db199SXin Li if hqe.id not in existing_hqe_ids] 2082*9c5db199SXin Li AbortedHostQueueEntry.objects.bulk_create(aborted_hqes) 2083*9c5db199SXin Li # Bulk update all of the HQEs to set the abort bit. 2084*9c5db199SXin Li HostQueueEntry.objects.filter(id__in=child_ids).update(aborted=True) 2085*9c5db199SXin Li 2086*9c5db199SXin Li 2087*9c5db199SXin Li def abort(self): 2088*9c5db199SXin Li """ Aborts this host queue entry. 2089*9c5db199SXin Li 2090*9c5db199SXin Li Abort this host queue entry and all host queue entries of jobs created by 2091*9c5db199SXin Li this one. 2092*9c5db199SXin Li 2093*9c5db199SXin Li """ 2094*9c5db199SXin Li if not self.complete and not self.aborted: 2095*9c5db199SXin Li HostQueueEntry.abort_host_queue_entries([self]) 2096*9c5db199SXin Li 2097*9c5db199SXin Li 2098*9c5db199SXin Li @classmethod 2099*9c5db199SXin Li def compute_full_status(cls, status, aborted, complete): 2100*9c5db199SXin Li """Returns a modified status msg if the host queue entry was aborted. 2101*9c5db199SXin Li 2102*9c5db199SXin Li @param cls: Implicit class object. 2103*9c5db199SXin Li @param status: The original status message. 2104*9c5db199SXin Li @param aborted: Whether the host queue entry was aborted. 2105*9c5db199SXin Li @param complete: Whether the host queue entry was completed. 2106*9c5db199SXin Li """ 2107*9c5db199SXin Li if aborted and not complete: 2108*9c5db199SXin Li return 'Aborted (%s)' % status 2109*9c5db199SXin Li return status 2110*9c5db199SXin Li 2111*9c5db199SXin Li 2112*9c5db199SXin Li def full_status(self): 2113*9c5db199SXin Li """Returns the full status of this host queue entry, as a string.""" 2114*9c5db199SXin Li return self.compute_full_status(self.status, self.aborted, 2115*9c5db199SXin Li self.complete) 2116*9c5db199SXin Li 2117*9c5db199SXin Li 2118*9c5db199SXin Li def _postprocess_object_dict(self, object_dict): 2119*9c5db199SXin Li object_dict['full_status'] = self.full_status() 2120*9c5db199SXin Li 2121*9c5db199SXin Li 2122*9c5db199SXin Li class Meta: 2123*9c5db199SXin Li """Metadata for class HostQueueEntry.""" 2124*9c5db199SXin Li db_table = 'afe_host_queue_entries' 2125*9c5db199SXin Li 2126*9c5db199SXin Li 2127*9c5db199SXin Li 2128*9c5db199SXin Li def __unicode__(self): 2129*9c5db199SXin Li hostname = None 2130*9c5db199SXin Li if self.host: 2131*9c5db199SXin Li hostname = self.host.hostname 2132*9c5db199SXin Li return u"%s/%d (%d)" % (hostname, self.job.id, self.id) 2133*9c5db199SXin Li 2134*9c5db199SXin Li 2135*9c5db199SXin Liclass HostQueueEntryStartTimes(dbmodels.Model): 2136*9c5db199SXin Li """An auxilary table to HostQueueEntry to index by start time.""" 2137*9c5db199SXin Li insert_time = dbmodels.DateTimeField() 2138*9c5db199SXin Li highest_hqe_id = dbmodels.IntegerField() 2139*9c5db199SXin Li 2140*9c5db199SXin Li class Meta: 2141*9c5db199SXin Li """Metadata for class HostQueueEntryStartTimes.""" 2142*9c5db199SXin Li db_table = 'afe_host_queue_entry_start_times' 2143*9c5db199SXin Li 2144*9c5db199SXin Li 2145*9c5db199SXin Liclass AbortedHostQueueEntry(dbmodels.Model, model_logic.ModelExtensions): 2146*9c5db199SXin Li """Represents an aborted host queue entry.""" 2147*9c5db199SXin Li queue_entry = dbmodels.OneToOneField(HostQueueEntry, primary_key=True) 2148*9c5db199SXin Li aborted_by = dbmodels.ForeignKey(User) 2149*9c5db199SXin Li aborted_on = dbmodels.DateTimeField() 2150*9c5db199SXin Li 2151*9c5db199SXin Li objects = model_logic.ExtendedManager() 2152*9c5db199SXin Li 2153*9c5db199SXin Li 2154*9c5db199SXin Li def save(self, *args, **kwargs): 2155*9c5db199SXin Li self.aborted_on = datetime.now() 2156*9c5db199SXin Li super(AbortedHostQueueEntry, self).save(*args, **kwargs) 2157*9c5db199SXin Li 2158*9c5db199SXin Li class Meta: 2159*9c5db199SXin Li """Metadata for class AbortedHostQueueEntry.""" 2160*9c5db199SXin Li db_table = 'afe_aborted_host_queue_entries' 2161*9c5db199SXin Li 2162*9c5db199SXin Li 2163*9c5db199SXin Liclass SpecialTask(dbmodels.Model, model_logic.ModelExtensions): 2164*9c5db199SXin Li """\ 2165*9c5db199SXin Li Tasks to run on hosts at the next time they are in the Ready state. Use this 2166*9c5db199SXin Li for high-priority tasks, such as forced repair or forced reinstall. 2167*9c5db199SXin Li 2168*9c5db199SXin Li host: host to run this task on 2169*9c5db199SXin Li task: special task to run 2170*9c5db199SXin Li time_requested: date and time the request for this task was made 2171*9c5db199SXin Li is_active: task is currently running 2172*9c5db199SXin Li is_complete: task has finished running 2173*9c5db199SXin Li is_aborted: task was aborted 2174*9c5db199SXin Li time_started: date and time the task started 2175*9c5db199SXin Li time_finished: date and time the task finished 2176*9c5db199SXin Li queue_entry: Host queue entry waiting on this task (or None, if task was not 2177*9c5db199SXin Li started in preparation of a job) 2178*9c5db199SXin Li """ 2179*9c5db199SXin Li Task = autotest_enum.AutotestEnum('Verify', 'Cleanup', 'Repair', 'Reset', 2180*9c5db199SXin Li 'Provision', string_values=True) 2181*9c5db199SXin Li 2182*9c5db199SXin Li host = dbmodels.ForeignKey(Host, blank=False, null=False) 2183*9c5db199SXin Li task = dbmodels.CharField(max_length=64, choices=Task.choices(), 2184*9c5db199SXin Li blank=False, null=False) 2185*9c5db199SXin Li requested_by = dbmodels.ForeignKey(User) 2186*9c5db199SXin Li time_requested = dbmodels.DateTimeField(auto_now_add=True, blank=False, 2187*9c5db199SXin Li null=False) 2188*9c5db199SXin Li is_active = dbmodels.BooleanField(default=False, blank=False, null=False) 2189*9c5db199SXin Li is_complete = dbmodels.BooleanField(default=False, blank=False, null=False) 2190*9c5db199SXin Li is_aborted = dbmodels.BooleanField(default=False, blank=False, null=False) 2191*9c5db199SXin Li time_started = dbmodels.DateTimeField(null=True, blank=True) 2192*9c5db199SXin Li queue_entry = dbmodels.ForeignKey(HostQueueEntry, blank=True, null=True) 2193*9c5db199SXin Li success = dbmodels.BooleanField(default=False, blank=False, null=False) 2194*9c5db199SXin Li time_finished = dbmodels.DateTimeField(null=True, blank=True) 2195*9c5db199SXin Li 2196*9c5db199SXin Li objects = model_logic.ExtendedManager() 2197*9c5db199SXin Li 2198*9c5db199SXin Li 2199*9c5db199SXin Li def save(self, **kwargs): 2200*9c5db199SXin Li if self.queue_entry: 2201*9c5db199SXin Li self.requested_by = User.objects.get( 2202*9c5db199SXin Li login=self.queue_entry.job.owner) 2203*9c5db199SXin Li super(SpecialTask, self).save(**kwargs) 2204*9c5db199SXin Li 2205*9c5db199SXin Li 2206*9c5db199SXin Li def execution_path(self): 2207*9c5db199SXin Li """Returns the execution path for a special task.""" 2208*9c5db199SXin Li return server_utils.get_special_task_exec_path( 2209*9c5db199SXin Li self.host.hostname, self.id, self.task, self.time_requested) 2210*9c5db199SXin Li 2211*9c5db199SXin Li 2212*9c5db199SXin Li # property to emulate HostQueueEntry.status 2213*9c5db199SXin Li @property 2214*9c5db199SXin Li def status(self): 2215*9c5db199SXin Li """Returns a host queue entry status appropriate for a speical task.""" 2216*9c5db199SXin Li return server_utils.get_special_task_status( 2217*9c5db199SXin Li self.is_complete, self.success, self.is_active) 2218*9c5db199SXin Li 2219*9c5db199SXin Li 2220*9c5db199SXin Li # property to emulate HostQueueEntry.started_on 2221*9c5db199SXin Li @property 2222*9c5db199SXin Li def started_on(self): 2223*9c5db199SXin Li """Returns the time at which this special task started.""" 2224*9c5db199SXin Li return self.time_started 2225*9c5db199SXin Li 2226*9c5db199SXin Li 2227*9c5db199SXin Li @classmethod 2228*9c5db199SXin Li def schedule_special_task(cls, host, task): 2229*9c5db199SXin Li """Schedules a special task on a host if not already scheduled. 2230*9c5db199SXin Li 2231*9c5db199SXin Li @param cls: Implicit class object. 2232*9c5db199SXin Li @param host: The host to use. 2233*9c5db199SXin Li @param task: The task to schedule. 2234*9c5db199SXin Li """ 2235*9c5db199SXin Li existing_tasks = SpecialTask.objects.filter(host__id=host.id, task=task, 2236*9c5db199SXin Li is_active=False, 2237*9c5db199SXin Li is_complete=False) 2238*9c5db199SXin Li if existing_tasks: 2239*9c5db199SXin Li return existing_tasks[0] 2240*9c5db199SXin Li 2241*9c5db199SXin Li special_task = SpecialTask(host=host, task=task, 2242*9c5db199SXin Li requested_by=User.current_user()) 2243*9c5db199SXin Li special_task.save() 2244*9c5db199SXin Li return special_task 2245*9c5db199SXin Li 2246*9c5db199SXin Li 2247*9c5db199SXin Li def abort(self): 2248*9c5db199SXin Li """ Abort this special task.""" 2249*9c5db199SXin Li self.is_aborted = True 2250*9c5db199SXin Li self.save() 2251*9c5db199SXin Li 2252*9c5db199SXin Li 2253*9c5db199SXin Li def activate(self): 2254*9c5db199SXin Li """ 2255*9c5db199SXin Li Sets a task as active and sets the time started to the current time. 2256*9c5db199SXin Li """ 2257*9c5db199SXin Li logging.info('Starting: %s', self) 2258*9c5db199SXin Li self.is_active = True 2259*9c5db199SXin Li self.time_started = datetime.now() 2260*9c5db199SXin Li self.save() 2261*9c5db199SXin Li 2262*9c5db199SXin Li 2263*9c5db199SXin Li def finish(self, success): 2264*9c5db199SXin Li """Sets a task as completed. 2265*9c5db199SXin Li 2266*9c5db199SXin Li @param success: Whether or not the task was successful. 2267*9c5db199SXin Li """ 2268*9c5db199SXin Li logging.info('Finished: %s', self) 2269*9c5db199SXin Li self.is_active = False 2270*9c5db199SXin Li self.is_complete = True 2271*9c5db199SXin Li self.success = success 2272*9c5db199SXin Li if self.time_started: 2273*9c5db199SXin Li self.time_finished = datetime.now() 2274*9c5db199SXin Li self.save() 2275*9c5db199SXin Li 2276*9c5db199SXin Li 2277*9c5db199SXin Li class Meta: 2278*9c5db199SXin Li """Metadata for class SpecialTask.""" 2279*9c5db199SXin Li db_table = 'afe_special_tasks' 2280*9c5db199SXin Li 2281*9c5db199SXin Li 2282*9c5db199SXin Li def __unicode__(self): 2283*9c5db199SXin Li result = u'Special Task %s (host %s, task %s, time %s)' % ( 2284*9c5db199SXin Li self.id, self.host, self.task, self.time_requested) 2285*9c5db199SXin Li if self.is_complete: 2286*9c5db199SXin Li result += u' (completed)' 2287*9c5db199SXin Li elif self.is_active: 2288*9c5db199SXin Li result += u' (active)' 2289*9c5db199SXin Li 2290*9c5db199SXin Li return result 2291*9c5db199SXin Li 2292*9c5db199SXin Li 2293*9c5db199SXin Liclass StableVersion(dbmodels.Model, model_logic.ModelExtensions): 2294*9c5db199SXin Li 2295*9c5db199SXin Li board = dbmodels.CharField(max_length=255, unique=True) 2296*9c5db199SXin Li version = dbmodels.CharField(max_length=255) 2297*9c5db199SXin Li 2298*9c5db199SXin Li class Meta: 2299*9c5db199SXin Li """Metadata for class StableVersion.""" 2300*9c5db199SXin Li db_table = 'afe_stable_versions' 2301*9c5db199SXin Li 2302*9c5db199SXin Li def save(self, *args, **kwargs): 2303*9c5db199SXin Li if os.getenv("OVERRIDE_STABLE_VERSION_BAN"): 2304*9c5db199SXin Li super(StableVersion, self).save(*args, **kwargs) 2305*9c5db199SXin Li else: 2306*9c5db199SXin Li raise RuntimeError("the ability to save StableVersions has been intentionally removed") 2307*9c5db199SXin Li 2308*9c5db199SXin Li # pylint:disable=undefined-variable 2309*9c5db199SXin Li def delete(self): 2310*9c5db199SXin Li if os.getenv("OVERRIDE_STABLE_VERSION_BAN"): 2311*9c5db199SXin Li super(StableVersion, self).delete(*args, **kwargs) 2312*9c5db199SXin Li else: 2313*9c5db199SXin Li raise RuntimeError("the ability to delete StableVersions has been intentionally removed") 2314