508 lines
19 KiB
Python
Executable file
508 lines
19 KiB
Python
Executable file
#!/usr/bin/python
|
|
#pylint: disable-msg=C0111
|
|
|
|
# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
import abc
|
|
import os
|
|
|
|
import common
|
|
|
|
from autotest_lib.database import database_connection
|
|
from autotest_lib.frontend import setup_django_environment
|
|
from autotest_lib.frontend.afe import frontend_test_utils
|
|
from autotest_lib.frontend.afe import models
|
|
from autotest_lib.frontend.afe import rdb_model_extensions as rdb_models
|
|
from autotest_lib.scheduler import monitor_db
|
|
from autotest_lib.scheduler import query_managers
|
|
from autotest_lib.scheduler import scheduler_lib
|
|
from autotest_lib.scheduler import scheduler_models
|
|
from autotest_lib.scheduler import rdb_hosts
|
|
from autotest_lib.scheduler import rdb_requests
|
|
from autotest_lib.server.cros import provision
|
|
|
|
|
|
# Set for verbose table creation output.
|
|
_DEBUG = False
|
|
DEFAULT_ACLS = ['Everyone', 'my_acl']
|
|
DEFAULT_DEPS = ['a', 'b']
|
|
DEFAULT_USER = 'system'
|
|
|
|
|
|
def get_default_job_params():
|
|
return {'deps': DEFAULT_DEPS, 'user': DEFAULT_USER, 'acls': DEFAULT_ACLS,
|
|
'priority': 0, 'parent_job_id': 0}
|
|
|
|
|
|
def get_default_host_params():
|
|
return {'deps': DEFAULT_DEPS, 'acls': DEFAULT_ACLS}
|
|
|
|
|
|
class FakeHost(rdb_hosts.RDBHost):
|
|
"""Fake host to use in unittests."""
|
|
|
|
def __init__(self, hostname, host_id, **kwargs):
|
|
kwargs.update({'hostname': hostname, 'id': host_id})
|
|
kwargs = rdb_models.AbstractHostModel.provide_default_values(
|
|
kwargs)
|
|
super(FakeHost, self).__init__(**kwargs)
|
|
|
|
|
|
def wire_format_response_map(response_map):
|
|
wire_formatted_map = {}
|
|
for request, response in response_map.iteritems():
|
|
wire_formatted_map[request] = [reply.wire_format()
|
|
for reply in response]
|
|
return wire_formatted_map
|
|
|
|
|
|
class DBHelper(object):
|
|
"""Utility class for updating the database."""
|
|
|
|
def __init__(self):
|
|
"""Initialized django so it uses an in memory SQLite database."""
|
|
self.database = (
|
|
database_connection.TranslatingDatabase.get_test_database(
|
|
translators=scheduler_lib._DB_TRANSLATORS))
|
|
self.database.connect(db_type='django')
|
|
self.database.debug = _DEBUG
|
|
|
|
|
|
@classmethod
|
|
def get_labels(cls, **kwargs):
|
|
"""Get a label queryset based on the kwargs."""
|
|
return models.Label.objects.filter(**kwargs)
|
|
|
|
|
|
@classmethod
|
|
def get_acls(cls, **kwargs):
|
|
"""Get an aclgroup queryset based on the kwargs."""
|
|
return models.AclGroup.objects.filter(**kwargs)
|
|
|
|
|
|
@classmethod
|
|
def get_host(cls, **kwargs):
|
|
"""Get a host queryset based on the kwargs."""
|
|
return models.Host.objects.filter(**kwargs)
|
|
|
|
|
|
@classmethod
|
|
def get_hqes(cls, **kwargs):
|
|
return models.HostQueueEntry.objects.filter(**kwargs)
|
|
|
|
|
|
@classmethod
|
|
def get_tasks(cls, **kwargs):
|
|
return models.SpecialTask.objects.filter(**kwargs)
|
|
|
|
|
|
@classmethod
|
|
def get_shard(cls, **kwargs):
|
|
return models.Shard.objects.filter(**kwargs)
|
|
|
|
|
|
@classmethod
|
|
def create_label(cls, name, **kwargs):
|
|
label = cls.get_labels(name=name, **kwargs)
|
|
return (models.Label.add_object(name=name, **kwargs)
|
|
if not label else label[0])
|
|
|
|
|
|
@classmethod
|
|
def create_user(cls, name):
|
|
user = models.User.objects.filter(login=name)
|
|
return models.User.add_object(login=name) if not user else user[0]
|
|
|
|
|
|
@classmethod
|
|
def create_special_task(cls, job_id=None, host_id=None,
|
|
task=models.SpecialTask.Task.VERIFY,
|
|
user='autotest-system'):
|
|
if job_id:
|
|
queue_entry = cls.get_hqes(job_id=job_id)[0]
|
|
host_id = queue_entry.host.id
|
|
else:
|
|
queue_entry = None
|
|
host = models.Host.objects.get(id=host_id)
|
|
owner = cls.create_user(user)
|
|
if not host:
|
|
raise ValueError('Require a host to create special tasks.')
|
|
return models.SpecialTask.objects.create(
|
|
host=host, queue_entry=queue_entry, task=task,
|
|
requested_by_id=owner.id)
|
|
|
|
|
|
@classmethod
|
|
def create_shard(cls, shard_hostname):
|
|
"""Create a shard with the given hostname if one doesn't already exist.
|
|
|
|
@param shard_hostname: The hostname of the shard.
|
|
"""
|
|
shard = cls.get_shard(hostname=shard_hostname)
|
|
return (models.Shard.objects.create(hostname=shard_hostname)
|
|
if not shard else shard[0])
|
|
|
|
|
|
@classmethod
|
|
def add_labels_to_host(cls, host, label_names=set([])):
|
|
label_objects = set([])
|
|
for label in label_names:
|
|
label_objects.add(cls.create_label(label))
|
|
host.labels.add(*label_objects)
|
|
|
|
|
|
@classmethod
|
|
def create_acl_group(cls, name):
|
|
aclgroup = cls.get_acls(name=name)
|
|
return (models.AclGroup.add_object(name=name)
|
|
if not aclgroup else aclgroup[0])
|
|
|
|
|
|
@classmethod
|
|
def add_deps_to_job(cls, job, dep_names=set([])):
|
|
label_objects = set([])
|
|
for label in dep_names:
|
|
label_objects.add(cls.create_label(label))
|
|
job.dependency_labels.add(*label_objects)
|
|
|
|
|
|
@classmethod
|
|
def assign_job_to_shard(cls, job_id, shard_hostname):
|
|
"""Assign a job to a shard.
|
|
|
|
@param job: A job object without a shard.
|
|
@param shard_hostname: The hostname of a shard to assign the job.
|
|
|
|
@raises ValueError: If the job already has a shard.
|
|
"""
|
|
job_filter = models.Job.objects.filter(id=job_id, shard__isnull=True)
|
|
if len(job_filter) != 1:
|
|
raise ValueError('Failed to assign job %s to shard %s' %
|
|
job_filter, shard_hostname)
|
|
job_filter.update(shard=cls.create_shard(shard_hostname))
|
|
|
|
|
|
@classmethod
|
|
def add_host_to_aclgroup(cls, host, aclgroup_names=set([])):
|
|
for group_name in aclgroup_names:
|
|
aclgroup = cls.create_acl_group(group_name)
|
|
aclgroup.hosts.add(host)
|
|
|
|
|
|
@classmethod
|
|
def add_user_to_aclgroups(cls, username, aclgroup_names=set([])):
|
|
user = cls.create_user(username)
|
|
for group_name in aclgroup_names:
|
|
aclgroup = cls.create_acl_group(group_name)
|
|
aclgroup.users.add(user)
|
|
|
|
|
|
@classmethod
|
|
def create_host(cls, name, deps=set([]), acls=set([]), status='Ready',
|
|
locked=0, lock_reason='', leased=0, protection=0, dirty=0):
|
|
"""Create a host.
|
|
|
|
Also adds the appropriate labels to the host, and adds the host to the
|
|
required acl groups.
|
|
|
|
@param name: The hostname.
|
|
@param kwargs:
|
|
deps: The labels on the host that match job deps.
|
|
acls: The aclgroups this host must be a part of.
|
|
status: The status of the host.
|
|
locked: 1 if the host is locked.
|
|
lock_reason: non-empty string if the host is locked.
|
|
leased: 1 if the host is leased.
|
|
protection: Any protection level, such as Do Not Verify.
|
|
dirty: 1 if the host requires cleanup.
|
|
|
|
@return: The host object for the new host.
|
|
"""
|
|
# TODO: Modify this to use the create host request once
|
|
# crbug.com/350995 is fixed.
|
|
host = models.Host.add_object(
|
|
hostname=name, status=status, locked=locked,
|
|
lock_reason=lock_reason, leased=leased,
|
|
protection=protection)
|
|
cls.add_labels_to_host(host, label_names=deps)
|
|
cls.add_host_to_aclgroup(host, aclgroup_names=acls)
|
|
|
|
# Though we can return the host object above, this proves that the host
|
|
# actually got saved in the database. For example, this will return none
|
|
# if save() wasn't called on the model.Host instance.
|
|
return cls.get_host(hostname=name)[0]
|
|
|
|
|
|
@classmethod
|
|
def update_hqe(cls, hqe_id, **kwargs):
|
|
"""Update the hqe with the given kwargs.
|
|
|
|
@param hqe_id: The id of the hqe to update.
|
|
"""
|
|
models.HostQueueEntry.objects.filter(id=hqe_id).update(**kwargs)
|
|
|
|
|
|
@classmethod
|
|
def update_special_task(cls, task_id, **kwargs):
|
|
"""Update special tasks with the given kwargs.
|
|
|
|
@param task_id: The if of the task to update.
|
|
"""
|
|
models.SpecialTask.objects.filter(id=task_id).update(**kwargs)
|
|
|
|
|
|
@classmethod
|
|
def add_host_to_job(cls, host, job_id, activate=0):
|
|
"""Add a host to the hqe of a job.
|
|
|
|
@param host: An instance of the host model.
|
|
@param job_id: The job to which we need to add the host.
|
|
@param activate: If true, flip the active bit on the hqe.
|
|
|
|
@raises ValueError: If the hqe for the job already has a host,
|
|
or if the host argument isn't a Host instance.
|
|
"""
|
|
hqe = models.HostQueueEntry.objects.get(job_id=job_id)
|
|
if hqe.host:
|
|
raise ValueError('HQE for job %s already has a host' % job_id)
|
|
hqe.host = host
|
|
hqe.save()
|
|
if activate:
|
|
cls.update_hqe(hqe.id, active=True)
|
|
|
|
|
|
@classmethod
|
|
def increment_priority(cls, job_id):
|
|
job = models.Job.objects.get(id=job_id)
|
|
job.priority = job.priority + 1
|
|
job.save()
|
|
|
|
|
|
class FileDatabaseHelper(object):
|
|
"""A helper class to setup a SQLite database backed by a file.
|
|
|
|
Note that initializing a file database takes significantly longer than an
|
|
in-memory database and should only be used for functional tests.
|
|
"""
|
|
|
|
DB_FILE = os.path.join(common.autotest_dir, 'host_scheduler_db')
|
|
|
|
def initialize_database_for_testing(self, db_file_path=None):
|
|
"""Initialize a SQLite database for testing.
|
|
|
|
To force monitor_db and the host_scheduler to use the same SQLite file
|
|
database, call this method before initializing the database through
|
|
frontend_test_utils. The host_scheduler is setup to look for the
|
|
host_scheduler_db when invoked with --testing.
|
|
|
|
@param db_file_path: The name of the file to use to create
|
|
a SQLite database. Since this database is shared across different
|
|
processes using a file is closer to the real world.
|
|
"""
|
|
if not db_file_path:
|
|
db_file_path = self.DB_FILE
|
|
# TODO: Move the translating database elsewhere. Monitor_db circular
|
|
# imports host_scheduler.
|
|
from autotest_lib.frontend import setup_test_environment
|
|
from django.conf import settings
|
|
self.old_django_db_name = settings.DATABASES['default']['NAME']
|
|
settings.DATABASES['default']['NAME'] = db_file_path
|
|
self.db_file_path = db_file_path
|
|
_db_manager = scheduler_lib.ConnectionManager(autocommit=False)
|
|
_db_manager.db_connection = (
|
|
database_connection.TranslatingDatabase.get_test_database(
|
|
translators=scheduler_lib._DB_TRANSLATORS))
|
|
|
|
|
|
def teardown_file_database(self):
|
|
"""Teardown django database settings."""
|
|
# TODO: Move the translating database elsewhere. Monitor_db circular
|
|
# imports host_scheduler.
|
|
from django.conf import settings
|
|
settings.DATABASES['default']['NAME'] = self.old_django_db_name
|
|
try:
|
|
os.remove(self.db_file_path)
|
|
except (OSError, AttributeError):
|
|
pass
|
|
|
|
|
|
class AbstractBaseRDBTester(frontend_test_utils.FrontendTestMixin):
|
|
|
|
__meta__ = abc.ABCMeta
|
|
_config_section = 'AUTOTEST_WEB'
|
|
|
|
|
|
@staticmethod
|
|
def get_request(dep_names, acl_names, priority=0, parent_job_id=0):
|
|
deps = [dep.id for dep in DBHelper.get_labels(name__in=dep_names)]
|
|
acls = [acl.id for acl in DBHelper.get_acls(name__in=acl_names)]
|
|
return rdb_requests.AcquireHostRequest(
|
|
deps=deps, acls=acls, host_id=None, priority=priority,
|
|
parent_job_id=parent_job_id)._request
|
|
|
|
|
|
def _release_unused_hosts(self):
|
|
"""Release all hosts unused by an active hqe. """
|
|
self.host_scheduler.tick()
|
|
|
|
|
|
def setUp(self, inline_host_acquisition=True, setup_tables=True):
|
|
"""Common setup module for tests that need a jobs/host database.
|
|
|
|
@param inline_host_acquisition: If True, the dispatcher tries to acquire
|
|
hosts inline with the rest of the tick.
|
|
"""
|
|
self.db_helper = DBHelper()
|
|
self._database = self.db_helper.database
|
|
# Runs syncdb setting up initial database conditions
|
|
self._frontend_common_setup(setup_tables=setup_tables)
|
|
connection_manager = scheduler_lib.ConnectionManager(autocommit=False)
|
|
self.god.stub_with(connection_manager, 'db_connection', self._database)
|
|
self.god.stub_with(monitor_db, '_db_manager', connection_manager)
|
|
self.god.stub_with(scheduler_models, '_db', self._database)
|
|
self.god.stub_with(monitor_db, '_inline_host_acquisition',
|
|
inline_host_acquisition)
|
|
self._dispatcher = monitor_db.Dispatcher()
|
|
self.host_scheduler = self._dispatcher._host_scheduler
|
|
self.host_query_manager = query_managers.AFEHostQueryManager()
|
|
self.job_query_manager = self._dispatcher._job_query_manager
|
|
self._release_unused_hosts()
|
|
|
|
|
|
def tearDown(self):
|
|
self.god.unstub_all()
|
|
self._database.disconnect()
|
|
self._frontend_common_teardown()
|
|
|
|
|
|
def create_job(self, user='autotest_system',
|
|
deps=set([]), acls=set([]), hostless_job=False,
|
|
priority=0, parent_job_id=None, shard_hostname=None):
|
|
"""Create a job owned by user, with the deps and acls specified.
|
|
|
|
This method is a wrapper around frontend_test_utils.create_job, that
|
|
also takes care of creating the appropriate deps for a job, and the
|
|
appropriate acls for the given user.
|
|
|
|
@raises ValueError: If no deps are specified for a job, since all jobs
|
|
need at least the metahost.
|
|
@raises AssertionError: If no hqe was created for the job.
|
|
|
|
@return: An instance of the job model associated with the new job.
|
|
"""
|
|
# This is a slight hack around the implementation of
|
|
# scheduler_models.is_hostless_job, even though a metahost is just
|
|
# another label to the rdb.
|
|
if not deps:
|
|
raise ValueError('Need at least one dep for metahost')
|
|
|
|
# TODO: This is a hack around the fact that frontend_test_utils still
|
|
# need a metahost, but metahost is treated like any other label.
|
|
metahost = self.db_helper.create_label(list(deps)[0])
|
|
job = self._create_job(metahosts=[metahost.id], priority=priority,
|
|
owner=user, parent_job_id=parent_job_id)
|
|
self.assert_(len(job.hostqueueentry_set.all()) == 1)
|
|
|
|
self.db_helper.add_deps_to_job(job, dep_names=list(deps)[1:])
|
|
self.db_helper.add_user_to_aclgroups(user, aclgroup_names=acls)
|
|
if shard_hostname:
|
|
self.db_helper.assign_job_to_shard(job.id, shard_hostname)
|
|
return models.Job.objects.filter(id=job.id)[0]
|
|
|
|
|
|
def assert_host_db_status(self, host_id):
|
|
"""Assert host state right after acquisition.
|
|
|
|
Call this method to check the status of any host leased by the
|
|
rdb before it has been assigned to an hqe. It must be leased and
|
|
ready at this point in time.
|
|
|
|
@param host_id: Id of the host to check.
|
|
|
|
@raises AssertionError: If the host is either not leased or Ready.
|
|
"""
|
|
host = models.Host.objects.get(id=host_id)
|
|
self.assert_(host.leased)
|
|
self.assert_(host.status == 'Ready')
|
|
|
|
|
|
def check_hosts(self, host_iter):
|
|
"""Sanity check all hosts in the host_gen.
|
|
|
|
@param host_iter: A generator/iterator of RDBClientHostWrappers.
|
|
eg: The generator returned by rdb_lib.acquire_hosts. If a request
|
|
was not satisfied this iterator can contain None.
|
|
|
|
@raises AssertionError: If any of the sanity checks fail.
|
|
"""
|
|
for host in host_iter:
|
|
if host:
|
|
self.assert_host_db_status(host.id)
|
|
self.assert_(host.leased == 1)
|
|
|
|
|
|
def create_suite(self, user='autotest_system', num=2, priority=0,
|
|
board='z', build='x', acls=set()):
|
|
"""Create num jobs with the same parent_job_id, board, build, priority.
|
|
|
|
@return: A dictionary with the parent job object keyed as 'parent_job'
|
|
and all other jobs keyed at an index from 0-num.
|
|
"""
|
|
jobs = {}
|
|
# Create a hostless parent job without an hqe or deps. Since the
|
|
# hostless job does nothing, we need to hand craft cros-version.
|
|
parent_job = self._create_job(owner=user, priority=priority)
|
|
jobs['parent_job'] = parent_job
|
|
build = '%s:%s' % (provision.CROS_VERSION_PREFIX, build)
|
|
for job_index in range(0, num):
|
|
jobs[job_index] = self.create_job(user=user, priority=priority,
|
|
deps=set([board, build]),
|
|
acls=acls,
|
|
parent_job_id=parent_job.id)
|
|
return jobs
|
|
|
|
|
|
def check_host_assignment(self, job_id, host_id):
|
|
"""Check is a job<->host assignment is valid.
|
|
|
|
Uses the deps of a job and the aclgroups the owner of the job is
|
|
in to see if the given host can be used to run the given job. Also
|
|
checks that the host-job assignment has Not been made, but that the
|
|
host is no longer in the available hosts pool.
|
|
|
|
Use this method to check host assignements made by the rdb, Before
|
|
they're handed off to the scheduler, since the scheduler.
|
|
|
|
@param job_id: The id of the job to use in the compatibility check.
|
|
@param host_id: The id of the host to check for compatibility.
|
|
|
|
@raises AssertionError: If the job and the host are incompatible.
|
|
"""
|
|
job = models.Job.objects.get(id=job_id)
|
|
host = models.Host.objects.get(id=host_id)
|
|
hqe = job.hostqueueentry_set.all()[0]
|
|
|
|
# Confirm that the host has not been assigned, either to another hqe
|
|
# or the this one.
|
|
all_hqes = models.HostQueueEntry.objects.filter(
|
|
host_id=host_id, complete=0)
|
|
self.assert_(len(all_hqes) <= 1)
|
|
self.assert_(hqe.host_id == None)
|
|
self.assert_host_db_status(host_id)
|
|
|
|
# Assert that all deps of the job are satisfied.
|
|
job_deps = set([d.name for d in job.dependency_labels.all()])
|
|
host_labels = set([l.name for l in host.labels.all()])
|
|
self.assert_(job_deps.intersection(host_labels) == job_deps)
|
|
|
|
# Assert that the owner of the job is in at least one of the
|
|
# groups that owns the host.
|
|
job_owner_aclgroups = set([job_acl.name for job_acl
|
|
in job.user().aclgroup_set.all()])
|
|
host_aclgroups = set([host_acl.name for host_acl
|
|
in host.aclgroup_set.all()])
|
|
self.assert_(job_owner_aclgroups.intersection(host_aclgroups))
|
|
|
|
|