blob: ebb48bdc9c923f5976fed2468b4eb5bf4bf74c94 [file] [log] [blame]
Prashanth B2d8047e2014-04-27 18:54:47 -07001#!/usr/bin/python
2#pylint: disable-msg=C0111
3
4# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
5# Use of this source code is governed by a BSD-style license that can be
6# found in the LICENSE file.
7
8import abc
Prashanth B4ec98672014-05-15 10:44:54 -07009import os
Prashanth B2d8047e2014-04-27 18:54:47 -070010
11import common
12
13from autotest_lib.database import database_connection
14from autotest_lib.frontend import setup_django_environment
15from autotest_lib.frontend.afe import frontend_test_utils
16from autotest_lib.frontend.afe import models
17from autotest_lib.frontend.afe import rdb_model_extensions as rdb_models
18from autotest_lib.scheduler import monitor_db
Prashanth Bf66d51b2014-05-06 12:42:25 -070019from autotest_lib.scheduler import query_managers
Prashanth B0e960282014-05-13 19:38:28 -070020from autotest_lib.scheduler import scheduler_lib
Prashanth B2d8047e2014-04-27 18:54:47 -070021from autotest_lib.scheduler import scheduler_models
22from autotest_lib.scheduler import rdb_hosts
23from autotest_lib.scheduler import rdb_requests
24from autotest_lib.server.cros import provision
25
26
27# Set for verbose table creation output.
28_DEBUG = False
29DEFAULT_ACLS = ['Everyone', 'my_acl']
30DEFAULT_DEPS = ['a', 'b']
31DEFAULT_USER = 'system'
32
Prashanth Bf66d51b2014-05-06 12:42:25 -070033
Prashanth B2d8047e2014-04-27 18:54:47 -070034def get_default_job_params():
35 return {'deps': DEFAULT_DEPS, 'user': DEFAULT_USER, 'acls': DEFAULT_ACLS,
36 'priority': 0, 'parent_job_id': 0}
37
38
39def get_default_host_params():
40 return {'deps': DEFAULT_DEPS, 'acls': DEFAULT_ACLS}
41
42
43class FakeHost(rdb_hosts.RDBHost):
44 """Fake host to use in unittests."""
45
46 def __init__(self, hostname, host_id, **kwargs):
47 kwargs.update({'hostname': hostname, 'id': host_id})
48 kwargs = rdb_models.AbstractHostModel.provide_default_values(
49 kwargs)
50 super(FakeHost, self).__init__(**kwargs)
51
52
53def wire_format_response_map(response_map):
54 wire_formatted_map = {}
55 for request, response in response_map.iteritems():
56 wire_formatted_map[request] = [reply.wire_format()
57 for reply in response]
58 return wire_formatted_map
59
60
61class DBHelper(object):
62 """Utility class for updating the database."""
63
64 def __init__(self):
Prashanth B4ec98672014-05-15 10:44:54 -070065 """Initialized django so it uses an in memory SQLite database."""
Prashanth B2d8047e2014-04-27 18:54:47 -070066 self.database = (
67 database_connection.TranslatingDatabase.get_test_database(
Prashanth B4ec98672014-05-15 10:44:54 -070068 translators=scheduler_lib._DB_TRANSLATORS))
Prashanth B2d8047e2014-04-27 18:54:47 -070069 self.database.connect(db_type='django')
70 self.database.debug = _DEBUG
71
72
73 @classmethod
74 def get_labels(cls, **kwargs):
75 """Get a label queryset based on the kwargs."""
76 return models.Label.objects.filter(**kwargs)
77
78
79 @classmethod
80 def get_acls(cls, **kwargs):
81 """Get an aclgroup queryset based on the kwargs."""
82 return models.AclGroup.objects.filter(**kwargs)
83
84
85 @classmethod
86 def get_host(cls, **kwargs):
87 """Get a host queryset based on the kwargs."""
88 return models.Host.objects.filter(**kwargs)
89
90
91 @classmethod
Prashanth Bf66d51b2014-05-06 12:42:25 -070092 def get_hqes(cls, **kwargs):
93 return models.HostQueueEntry.objects.filter(**kwargs)
94
95
96 @classmethod
Prashanth B4ec98672014-05-15 10:44:54 -070097 def get_tasks(cls, **kwargs):
98 return models.SpecialTask.objects.filter(**kwargs)
99
100
101 @classmethod
Prashanth B2d8047e2014-04-27 18:54:47 -0700102 def create_label(cls, name, **kwargs):
103 label = cls.get_labels(name=name, **kwargs)
104 return (models.Label.add_object(name=name, **kwargs)
105 if not label else label[0])
106
107
108 @classmethod
109 def create_user(cls, name):
110 user = models.User.objects.filter(login=name)
111 return models.User.add_object(login=name) if not user else user[0]
112
113
114 @classmethod
Prashanth Bf66d51b2014-05-06 12:42:25 -0700115 def create_special_task(cls, job_id=None, host_id=None,
116 task=models.SpecialTask.Task.VERIFY,
117 user='autotest-system'):
118 if job_id:
119 queue_entry = cls.get_hqes(job_id=job_id)[0]
120 host_id = queue_entry.host.id
121 else:
122 queue_entry = None
123 host = models.Host.objects.get(id=host_id)
124 owner = cls.create_user(user)
125 if not host:
126 raise ValueError('Require a host to create special tasks.')
127 return models.SpecialTask.objects.create(
128 host=host, queue_entry=queue_entry, task=task,
129 requested_by_id=owner.id)
130
131
132 @classmethod
Prashanth B2d8047e2014-04-27 18:54:47 -0700133 def add_labels_to_host(cls, host, label_names=set([])):
134 label_objects = set([])
135 for label in label_names:
136 label_objects.add(cls.create_label(label))
137 host.labels.add(*label_objects)
138
139
140 @classmethod
141 def create_acl_group(cls, name):
142 aclgroup = cls.get_acls(name=name)
143 return (models.AclGroup.add_object(name=name)
144 if not aclgroup else aclgroup[0])
145
146
147 @classmethod
148 def add_deps_to_job(cls, job, dep_names=set([])):
149 label_objects = set([])
150 for label in dep_names:
151 label_objects.add(cls.create_label(label))
152 job.dependency_labels.add(*label_objects)
153
154
155 @classmethod
156 def add_host_to_aclgroup(cls, host, aclgroup_names=set([])):
157 for group_name in aclgroup_names:
158 aclgroup = cls.create_acl_group(group_name)
159 aclgroup.hosts.add(host)
160
161
162 @classmethod
163 def add_user_to_aclgroups(cls, username, aclgroup_names=set([])):
164 user = cls.create_user(username)
165 for group_name in aclgroup_names:
166 aclgroup = cls.create_acl_group(group_name)
167 aclgroup.users.add(user)
168
169
170 @classmethod
171 def create_host(cls, name, deps=set([]), acls=set([]), status='Ready',
172 locked=0, leased=0, protection=0, dirty=0):
173 """Create a host.
174
175 Also adds the appropriate labels to the host, and adds the host to the
176 required acl groups.
177
178 @param name: The hostname.
179 @param kwargs:
180 deps: The labels on the host that match job deps.
181 acls: The aclgroups this host must be a part of.
182 status: The status of the host.
183 locked: 1 if the host is locked.
184 leased: 1 if the host is leased.
185 protection: Any protection level, such as Do Not Verify.
186 dirty: 1 if the host requires cleanup.
187
188 @return: The host object for the new host.
189 """
190 # TODO: Modify this to use the create host request once
191 # crbug.com/350995 is fixed.
192 host = models.Host.add_object(
193 hostname=name, status=status, locked=locked,
194 leased=leased, protection=protection)
195 cls.add_labels_to_host(host, label_names=deps)
196 cls.add_host_to_aclgroup(host, aclgroup_names=acls)
197
198 # Though we can return the host object above, this proves that the host
199 # actually got saved in the database. For example, this will return none
200 # if save() wasn't called on the model.Host instance.
201 return cls.get_host(hostname=name)[0]
202
203
204 @classmethod
Prashanth B4ec98672014-05-15 10:44:54 -0700205 def update_hqe(cls, hqe_id, **kwargs):
206 """Update the hqe with the given kwargs.
207
208 @param hqe_id: The id of the hqe to update.
209 """
210 models.HostQueueEntry.objects.filter(id=hqe_id).update(**kwargs)
211
212
213 @classmethod
214 def update_special_task(cls, task_id, **kwargs):
215 """Update special tasks with the given kwargs.
216
217 @param task_id: The if of the task to update.
218 """
219 models.SpecialTask.objects.filter(id=task_id).update(**kwargs)
220
221
222 @classmethod
223 def add_host_to_job(cls, host, job_id, activate=0):
Prashanth B2d8047e2014-04-27 18:54:47 -0700224 """Add a host to the hqe of a job.
225
226 @param host: An instance of the host model.
227 @param job_id: The job to which we need to add the host.
Prashanth B4ec98672014-05-15 10:44:54 -0700228 @param activate: If true, flip the active bit on the hqe.
Prashanth B2d8047e2014-04-27 18:54:47 -0700229
230 @raises ValueError: If the hqe for the job already has a host,
231 or if the host argument isn't a Host instance.
232 """
233 hqe = models.HostQueueEntry.objects.get(job_id=job_id)
234 if hqe.host:
235 raise ValueError('HQE for job %s already has a host' % job_id)
236 hqe.host = host
237 hqe.save()
Prashanth B4ec98672014-05-15 10:44:54 -0700238 if activate:
239 cls.update_hqe(hqe.id, active=True)
Prashanth B2d8047e2014-04-27 18:54:47 -0700240
241
242 @classmethod
Prashanth B2d8047e2014-04-27 18:54:47 -0700243 def increment_priority(cls, job_id):
244 job = models.Job.objects.get(id=job_id)
245 job.priority = job.priority + 1
246 job.save()
247
248
Prashanth B4ec98672014-05-15 10:44:54 -0700249class FileDatabaseHelper(object):
250 """A helper class to setup a SQLite database backed by a file.
251
252 Note that initializing a file database takes significantly longer than an
253 in-memory database and should only be used for functional tests.
254 """
255
256 DB_FILE = os.path.join(common.autotest_dir, 'host_scheduler_db')
257
258 def initialize_database_for_testing(self, db_file_path=None):
259 """Initialize a SQLite database for testing.
260
261 To force monitor_db and the host_scheduler to use the same SQLite file
262 database, call this method before initializing the database through
263 frontend_test_utils. The host_scheduler is setup to look for the
264 host_scheduler_db when invoked with --testing.
265
266 @param db_file_path: The name of the file to use to create
267 a SQLite database. Since this database is shared across different
268 processes using a file is closer to the real world.
269 """
270 if not db_file_path:
271 db_file_path = self.DB_FILE
272 # TODO: Move the translating database elsewhere. Monitor_db circular
273 # imports host_scheduler.
274 from autotest_lib.frontend import setup_test_environment
275 from django.conf import settings
276 self.old_django_db_name = settings.DATABASES['default']['NAME']
277 settings.DATABASES['default']['NAME'] = db_file_path
278 self.db_file_path = db_file_path
279 _db_manager = scheduler_lib.ConnectionManager(autocommit=False)
280 _db_manager.db_connection = (
281 database_connection.TranslatingDatabase.get_test_database(
282 translators=scheduler_lib._DB_TRANSLATORS))
283
284
285 def teardown_file_database(self):
286 """Teardown django database settings."""
287 # TODO: Move the translating database elsewhere. Monitor_db circular
288 # imports host_scheduler.
289 from django.conf import settings
290 settings.DATABASES['default']['NAME'] = self.old_django_db_name
291 try:
292 os.remove(self.db_file_path)
293 except (OSError, AttributeError):
294 pass
295
296
Prashanth B2d8047e2014-04-27 18:54:47 -0700297class AbstractBaseRDBTester(frontend_test_utils.FrontendTestMixin):
298
299 __meta__ = abc.ABCMeta
300 _config_section = 'AUTOTEST_WEB'
301
302
303 @staticmethod
304 def get_request(dep_names, acl_names, priority=0, parent_job_id=0):
305 deps = [dep.id for dep in DBHelper.get_labels(name__in=dep_names)]
306 acls = [acl.id for acl in DBHelper.get_acls(name__in=acl_names)]
307 return rdb_requests.AcquireHostRequest(
308 deps=deps, acls=acls, host_id=None, priority=priority,
309 parent_job_id=parent_job_id)._request
310
311
312 def _release_unused_hosts(self):
313 """Release all hosts unused by an active hqe. """
314 self.host_scheduler.tick()
315
316
Prashanth B4ec98672014-05-15 10:44:54 -0700317 def setUp(self, inline_host_acquisition=True, setup_tables=True):
Prashanth Bf66d51b2014-05-06 12:42:25 -0700318 """Common setup module for tests that need a jobs/host database.
319
320 @param inline_host_acquisition: If True, the dispatcher tries to acquire
321 hosts inline with the rest of the tick.
322 """
Prashanth B2d8047e2014-04-27 18:54:47 -0700323 self.db_helper = DBHelper()
324 self._database = self.db_helper.database
325 # Runs syncdb setting up initial database conditions
Prashanth B4ec98672014-05-15 10:44:54 -0700326 self._frontend_common_setup(setup_tables=setup_tables)
Prashanth B0e960282014-05-13 19:38:28 -0700327 connection_manager = scheduler_lib.ConnectionManager(autocommit=False)
328 self.god.stub_with(connection_manager, 'db_connection', self._database)
329 self.god.stub_with(monitor_db, '_db_manager', connection_manager)
Prashanth B2d8047e2014-04-27 18:54:47 -0700330 self.god.stub_with(scheduler_models, '_db', self._database)
Prashanth Bf66d51b2014-05-06 12:42:25 -0700331 self.god.stub_with(monitor_db, '_inline_host_acquisition',
332 inline_host_acquisition)
Prashanth B2d8047e2014-04-27 18:54:47 -0700333 self._dispatcher = monitor_db.Dispatcher()
334 self.host_scheduler = self._dispatcher._host_scheduler
Prashanth Bf66d51b2014-05-06 12:42:25 -0700335 self.host_query_manager = query_managers.AFEHostQueryManager()
336 self.job_query_manager = self._dispatcher._job_query_manager
Prashanth B2d8047e2014-04-27 18:54:47 -0700337 self._release_unused_hosts()
338
339
340 def tearDown(self):
341 self.god.unstub_all()
342 self._database.disconnect()
343 self._frontend_common_teardown()
344
345
346 def create_job(self, user='autotest_system',
347 deps=set([]), acls=set([]), hostless_job=False,
348 priority=0, parent_job_id=None):
349 """Create a job owned by user, with the deps and acls specified.
350
351 This method is a wrapper around frontend_test_utils.create_job, that
352 also takes care of creating the appropriate deps for a job, and the
353 appropriate acls for the given user.
354
355 @raises ValueError: If no deps are specified for a job, since all jobs
356 need at least the metahost.
357 @raises AssertionError: If no hqe was created for the job.
358
359 @return: An instance of the job model associated with the new job.
360 """
361 # This is a slight hack around the implementation of
362 # scheduler_models.is_hostless_job, even though a metahost is just
363 # another label to the rdb.
364 if not deps:
365 raise ValueError('Need at least one dep for metahost')
366
367 # TODO: This is a hack around the fact that frontend_test_utils still
368 # need a metahost, but metahost is treated like any other label.
369 metahost = self.db_helper.create_label(list(deps)[0])
370 job = self._create_job(metahosts=[metahost.id], priority=priority,
371 owner=user, parent_job_id=parent_job_id)
372 self.assert_(len(job.hostqueueentry_set.all()) == 1)
373
374 self.db_helper.add_deps_to_job(job, dep_names=list(deps)[1:])
375 self.db_helper.add_user_to_aclgroups(user, aclgroup_names=acls)
376 return models.Job.objects.filter(id=job.id)[0]
377
378
379 def assert_host_db_status(self, host_id):
380 """Assert host state right after acquisition.
381
382 Call this method to check the status of any host leased by the
383 rdb before it has been assigned to an hqe. It must be leased and
384 ready at this point in time.
385
386 @param host_id: Id of the host to check.
387
388 @raises AssertionError: If the host is either not leased or Ready.
389 """
390 host = models.Host.objects.get(id=host_id)
391 self.assert_(host.leased)
392 self.assert_(host.status == 'Ready')
393
394
395 def check_hosts(self, host_iter):
396 """Sanity check all hosts in the host_gen.
397
398 @param host_iter: A generator/iterator of RDBClientHostWrappers.
399 eg: The generator returned by rdb_lib.acquire_hosts. If a request
400 was not satisfied this iterator can contain None.
401
402 @raises AssertionError: If any of the sanity checks fail.
403 """
404 for host in host_iter:
405 if host:
406 self.assert_host_db_status(host.id)
407 self.assert_(host.leased == 1)
408
409
410 def create_suite(self, user='autotest_system', num=2, priority=0,
411 board='z', build='x'):
412 """Create num jobs with the same parent_job_id, board, build, priority.
413
414 @return: A dictionary with the parent job object keyed as 'parent_job'
415 and all other jobs keyed at an index from 0-num.
416 """
417 jobs = {}
418 # Create a hostless parent job without an hqe or deps. Since the
419 # hostless job does nothing, we need to hand craft cros-version.
420 parent_job = self._create_job(owner=user, priority=priority)
421 jobs['parent_job'] = parent_job
422 build = '%s:%s' % (provision.CROS_VERSION_PREFIX, build)
423 for job_index in range(0, num):
424 jobs[job_index] = self.create_job(user=user, priority=priority,
425 deps=set([board, build]),
426 parent_job_id=parent_job.id)
427 return jobs
428
429
430 def check_host_assignment(self, job_id, host_id):
431 """Check is a job<->host assignment is valid.
432
433 Uses the deps of a job and the aclgroups the owner of the job is
434 in to see if the given host can be used to run the given job. Also
435 checks that the host-job assignment has Not been made, but that the
436 host is no longer in the available hosts pool.
437
438 Use this method to check host assignements made by the rdb, Before
439 they're handed off to the scheduler, since the scheduler.
440
441 @param job_id: The id of the job to use in the compatibility check.
442 @param host_id: The id of the host to check for compatibility.
443
444 @raises AssertionError: If the job and the host are incompatible.
445 """
446 job = models.Job.objects.get(id=job_id)
447 host = models.Host.objects.get(id=host_id)
448 hqe = job.hostqueueentry_set.all()[0]
449
450 # Confirm that the host has not been assigned, either to another hqe
451 # or the this one.
452 all_hqes = models.HostQueueEntry.objects.filter(
453 host_id=host_id, complete=0)
454 self.assert_(len(all_hqes) <= 1)
455 self.assert_(hqe.host_id == None)
456 self.assert_host_db_status(host_id)
457
458 # Assert that all deps of the job are satisfied.
459 job_deps = set([d.name for d in job.dependency_labels.all()])
460 host_labels = set([l.name for l in host.labels.all()])
461 self.assert_(job_deps.intersection(host_labels) == job_deps)
462
463 # Assert that the owner of the job is in at least one of the
464 # groups that owns the host.
465 job_owner_aclgroups = set([job_acl.name for job_acl
466 in job.user().aclgroup_set.all()])
467 host_aclgroups = set([host_acl.name for host_acl
468 in host.aclgroup_set.all()])
469 self.assert_(job_owner_aclgroups.intersection(host_aclgroups))
470
471