[autotest] Add support for scheduler to honor require_ssp attribute in control file
This CL adds changes to pipe require_ssp attribute in control file to autoserv
command. Following are the work flow:
1. The control file parser stores require_ssp attribute value in afe_jobs table.
2. QueueTask compiles command line list, --require-ssp option will be added to
the command line list if following conditions are met:
a. AUTOSERV/enable_ssp_container in global config is True
b. The test is a server-side test
c. require_ssp for the job entry is None or True.
3. When agent_task tries to call run method to run the command, it will check if
there is any drone supporting server-side packaging first. If no drone is found,
the agent task will will run the command in a drone without using server-side
packaging. A warning will be posted in the autoserv log.
4. If a drone without SSP supported is assigned to a test requires SSP, the test
will be run without ssp.
BUG=chromium:453624
TEST=unittest, local test:
set AUTOSERV/enable_ssp_container to True in shadow config;
Create a job for dummy_PassServer in AFE, check require SSP, confirm the job
succeeds but with a warning in the autoserv log.
Create a job for dummy_PassServer_nossp in AFE, uncheck require SSP, confirm
the job passes without warning in the autoserv log.
set AUTOSERV/enable_ssp_container to False in shadow config, restart scheduler.
Create a job for dummy_PassServer in AFE, check require SSP, confirm the job
succeeds without warning in the autoserv log.
also run test_that in local chroot to make sure test_that is not affected.
DEPLOY=apache,scheduler, db migrate must be done before push this CL to prod.
Change-Id: I02f3d137186676ae570e8380d975a1bcd9ffbb94
Reviewed-on: https://chromium-review.googlesource.com/249841
Reviewed-by: Dan Shi <[email protected]>
Commit-Queue: Dan Shi <[email protected]>
Trybot-Ready: Dan Shi <[email protected]>
Tested-by: Dan Shi <[email protected]>
diff --git a/cli/job.py b/cli/job.py
index ddf4e68..6277f38 100644
--- a/cli/job.py
+++ b/cli/job.py
@@ -364,7 +364,7 @@
[--one-time-hosts <hosts>] [--email <email>]
[--dependencies <labels this job is dependent on>]
[--atomic_group <atomic group name>] [--parse-failed-repair <option>]
- [--image <http://path/to/image>]
+ [--image <http://path/to/image>] [--require-ssp]
job_name
Creating a job is rather different from the other create operations,
@@ -430,6 +430,9 @@
self.parser.add_option('-i', '--image',
help='OS image to install before running the '
'test.')
+ self.parser.add_option('--require-ssp',
+ help='Require server-side packaging',
+ default=False, action='store_true')
@staticmethod
@@ -520,6 +523,8 @@
else:
self.data['control_type'] = control_data.CONTROL_TYPE_NAMES.CLIENT
+ self.data['require_ssp'] = options.require_ssp
+
return options, leftover
diff --git a/cli/job_unittest.py b/cli/job_unittest.py
index 34ef429..bac0ceb 100755
--- a/cli/job_unittest.py
+++ b/cli/job_unittest.py
@@ -735,7 +735,8 @@
data = {'priority': 'Medium', 'control_file': ctrl_file, 'hosts': ['host0'],
'name': 'test_job0', 'control_type': CLIENT, 'email_list': '',
- 'meta_hosts': [], 'synch_count': 1, 'dependencies': []}
+ 'meta_hosts': [], 'synch_count': 1, 'dependencies': [],
+ 'require_ssp': False}
def test_execute_create_job(self):
diff --git a/client/common_lib/control_data.py b/client/common_lib/control_data.py
index 943d54d4..4eaae61 100644
--- a/client/common_lib/control_data.py
+++ b/client/common_lib/control_data.py
@@ -56,6 +56,11 @@
self.test_class = ''
self.retries = 0
self.job_retries = 0
+ # Default to require server-side package. Unless require_ssp is
+ # explicitly set to False, server-side package will be used for the
+ # job. This can be overridden by global config
+ # AUTOSERV/enable_ssp_container
+ self.require_ssp = None
diff = REQUIRED_VARS - set(vars)
if diff:
@@ -188,6 +193,7 @@
def set_retries(self, val):
self._set_int('retries', val)
+
def set_job_retries(self, val):
self._set_int('job_retries', val)
@@ -197,6 +203,10 @@
setattr(self, 'bug_template', val)
+ def set_require_ssp(self, val):
+ self._set_bool('require_ssp', val)
+
+
def _extract_const(expr):
assert(expr.__class__ == compiler.ast.Const)
assert(expr.value.__class__ in (str, int, float, unicode))
diff --git a/client/common_lib/control_data_unittest.py b/client/common_lib/control_data_unittest.py
index f4fd6d4..50664da 100755
--- a/client/common_lib/control_data_unittest.py
+++ b/client/common_lib/control_data_unittest.py
@@ -24,6 +24,7 @@
TEST_CATEGORY='Stress'
TEST_TYPE='client'
RETRIES = 5
+REQUIRE_SSP = False
"""
@@ -52,6 +53,7 @@
self.assertEquals(cd.test_category, "stress")
self.assertEquals(cd.test_type, "client")
self.assertEquals(cd.retries, 5)
+ self.assertEquals(cd.require_ssp, False)
class ParseControlFileBugTemplate(unittest.TestCase):
diff --git a/frontend/afe/rpc_interface.py b/frontend/afe/rpc_interface.py
index 193389d..232af52 100644
--- a/frontend/afe/rpc_interface.py
+++ b/frontend/afe/rpc_interface.py
@@ -636,7 +636,8 @@
run_verify=False, email_list='', dependencies=(),
reboot_before=None, reboot_after=None,
parse_failed_repair=None, hostless=False,
- keyvals=None, drone_set=None, run_reset=True):
+ keyvals=None, drone_set=None, run_reset=True,
+ require_ssq=None):
"""
Creates and enqueues a parameterized job.
@@ -743,8 +744,8 @@
run_verify=False, email_list='', dependencies=(),
reboot_before=None, reboot_after=None, parse_failed_repair=None,
hostless=False, keyvals=None, drone_set=None, image=None,
- parent_job_id=None, test_retry=0, run_reset=True, args=(),
- **kwargs):
+ parent_job_id=None, test_retry=0, run_reset=True,
+ require_ssp=None, args=(), **kwargs):
"""\
Create and enqueue a job.
@@ -780,6 +781,12 @@
@param test_retry Number of times to retry test if the test did not
complete successfully. (optional, default: 0)
@param run_reset Should the host be reset before running the test?
+ @param require_ssp Set to True to require server-side packaging to run the
+ test. If it's set to None, drone will still try to run
+ the server side with server-side packaging. If the
+ autotest-server package doesn't exist for the build or
+ image is not set, drone will run the test without server-
+ side packaging. Default is None.
@param args A list of args to be injected into control file.
@param kwargs extra keyword args. NOT USED.
diff --git a/frontend/afe/rpc_utils.py b/frontend/afe/rpc_utils.py
index 6804c32..70dd91a 100644
--- a/frontend/afe/rpc_utils.py
+++ b/frontend/afe/rpc_utils.py
@@ -781,7 +781,8 @@
dependencies=(), reboot_before=None, reboot_after=None,
parse_failed_repair=None, hostless=False, keyvals=None,
drone_set=None, parameterized_job=None,
- parent_job_id=None, test_retry=0, run_reset=True):
+ parent_job_id=None, test_retry=0, run_reset=True,
+ require_ssp=None):
#pylint: disable-msg=C0111
"""
Common code between creating "standard" jobs and creating parameterized jobs
@@ -896,7 +897,8 @@
parameterized_job=parameterized_job,
parent_job_id=parent_job_id,
test_retry=test_retry,
- run_reset=run_reset)
+ run_reset=run_reset,
+ require_ssp=require_ssp)
return create_new_job(owner=owner,
options=options,
host_objects=host_objects,
diff --git a/scheduler/drone_manager.py b/scheduler/drone_manager.py
index 065949d..402a232 100644
--- a/scheduler/drone_manager.py
+++ b/scheduler/drone_manager.py
@@ -234,7 +234,7 @@
def _add_drone(self, hostname):
- logging.info('Adding drone %s' % hostname)
+ logging.info('Adding drone %s', hostname)
drone = drones.get_drone(hostname)
if drone:
self._drones[drone.hostname] = drone
@@ -277,7 +277,6 @@
disabled = config.get_config_value(
section, '%s_disabled' % hostname, default='')
drone.enabled = not bool(disabled)
-
drone.max_processes = config.get_config_value(
section, '%s_max_processes' % hostname, type=int,
default=scheduler_config.config.max_processes_per_drone)
@@ -610,11 +609,25 @@
def _choose_drone_for_execution(self, num_processes, username,
- drone_hostnames_allowed):
+ drone_hostnames_allowed,
+ require_ssp=False):
+ """Choose a drone to execute command.
+
+ @param num_processes: Number of processes needed for execution.
+ @param username: Name of the user to execute the command.
+ @param drone_hostnames_allowed: A list of names of drone allowed.
+ @param require_ssp: Require server-side packaging to execute the,
+ command, default to False.
+
+ @return: A drone object to be used for execution.
+ """
# cycle through drones is order of increasing used capacity until
# we find one that can handle these processes
checked_drones = []
usable_drones = []
+ # Drones do not support server-side packaging, used as backup if no
+ # drone is found to run command requires server-side packaging.
+ no_ssp_drones = []
drone_to_use = None
while self._drone_queue:
drone = heapq.heappop(self._drone_queue).drone
@@ -628,6 +641,11 @@
if not drone_allowed:
logging.debug('Drone %s not allowed: ', drone.hostname)
continue
+ if require_ssp and not drone.support_ssp:
+ logging.debug('Drone %s does not support server-side '
+ 'packaging.', drone.hostname)
+ no_ssp_drones.append(drone)
+ continue
usable_drones.append(drone)
@@ -639,6 +657,7 @@
drone.max_processes)
if not drone_to_use and usable_drones:
+ # Drones are all over loaded, pick the one with least load.
drone_summary = ','.join('%s %s/%s' % (drone.hostname,
drone.active_processes,
drone.max_processes)
@@ -646,6 +665,9 @@
logging.error('No drone has capacity to handle %d processes (%s) '
'for user %s', num_processes, drone_summary, username)
drone_to_use = self._least_loaded_drone(usable_drones)
+ elif not drone_to_use and require_ssp and no_ssp_drones:
+ # No drone supports server-side packaging, choose the least loaded.
+ drone_to_use = self._least_loaded_drone(no_ssp_drones)
# refill _drone_queue
for drone in checked_drones:
@@ -696,15 +718,21 @@
if paired_with_pidfile:
drone = self._get_drone_for_pidfile_id(paired_with_pidfile)
else:
- drone = self._choose_drone_for_execution(num_processes, username,
- drone_hostnames_allowed)
+ require_ssp = '--require-ssp' in command
+ drone = self._choose_drone_for_execution(
+ num_processes, username, drone_hostnames_allowed,
+ require_ssp=require_ssp)
+ # Enable --warn-no-ssp option for autoserv to log a warning and run
+ # the command without using server-side packaging.
+ if require_ssp and not drone.support_ssp:
+ command.append('--warn-no-ssp')
if not drone:
raise DroneManagerError('command failed; no drones available: %s'
% command)
- logging.info("command = %s" % command)
- logging.info('log file = %s:%s' % (drone.hostname, log_file))
+ logging.info("command = %s", command)
+ logging.info('log file = %s:%s', drone.hostname, log_file)
self._write_attached_files(working_directory, drone)
drone.queue_call('execute_command', command, abs_working_directory,
log_file, pidfile_name)
diff --git a/scheduler/drone_manager_unittest.py b/scheduler/drone_manager_unittest.py
index 5a30600..e18f696 100755
--- a/scheduler/drone_manager_unittest.py
+++ b/scheduler/drone_manager_unittest.py
@@ -17,13 +17,15 @@
class MockDrone(drones._AbstractDrone):
def __init__(self, name, active_processes=0, max_processes=10,
- allowed_users=None):
+ allowed_users=None, support_ssp=False):
super(MockDrone, self).__init__()
self.name = name
self.hostname = name
self.active_processes = active_processes
self.max_processes = max_processes
self.allowed_users = allowed_users
+ self._host = 'mock_drone'
+ self._support_ssp = support_ssp
# maps method names list of tuples containing method arguments
self._recorded_calls = {'queue_call': [],
'send_file_to': []}
@@ -102,14 +104,20 @@
def _test_choose_drone_for_execution_helper(self, processes_info_list,
- requested_processes):
+ requested_processes,
+ require_ssp=False):
for index, process_info in enumerate(processes_info_list):
- active_processes, max_processes = process_info
- self.manager._enqueue_drone(MockDrone(index, active_processes,
- max_processes))
+ if len(process_info) == 2:
+ active_processes, max_processes = process_info
+ support_ssp = False
+ else:
+ active_processes, max_processes, support_ssp = process_info
+ self.manager._enqueue_drone(MockDrone(
+ index, active_processes, max_processes, allowed_users=None,
+ support_ssp=support_ssp))
- return self.manager._choose_drone_for_execution(requested_processes,
- self._USERNAME, None)
+ return self.manager._choose_drone_for_execution(
+ requested_processes, self._USERNAME, None, require_ssp)
def test_choose_drone_for_execution(self):
@@ -136,6 +144,19 @@
self.assertEquals(drone.name, 1)
+ def test_choose_drone_for_execution_no_ssp_support(self):
+ drone = self._test_choose_drone_for_execution_helper(
+ [(0, 1), (1, 3)], 1, True)
+ self.assertEquals(drone.name, 0)
+
+
+ def test_choose_drone_for_execution_with_ssp_support(self):
+ self.mock_drone._support_ssp = True
+ drone = self._test_choose_drone_for_execution_helper(
+ [(0, 1), (1, 3, True)], 1, True)
+ self.assertEquals(drone.name, 1)
+
+
def test_user_restrictions(self):
# this drone is restricted to a different user
self.manager._enqueue_drone(MockDrone(1, max_processes=10,
diff --git a/scheduler/drones.py b/scheduler/drones.py
index d3d3562..e62d3f0 100644
--- a/scheduler/drones.py
+++ b/scheduler/drones.py
@@ -42,6 +42,9 @@
self._autotest_install_dir = AUTOTEST_INSTALL_DIR
self._host = None
self.timestamp_remote_calls = timestamp_remote_calls
+ # If drone supports server-side packaging. The property support_ssp will
+ # init self._support_ssp later.
+ self._support_ssp = None
def shutdown(self):
@@ -138,6 +141,24 @@
pass
+ @property
+ def support_ssp(self):
+ """Check if the drone supports server-side packaging with container.
+
+ @return: True if the drone supports server-side packaging with container
+ """
+ if not self._host:
+ raise ValueError('Can not determine if drone supports server-side '
+ 'packaging before host is set.')
+ if self._support_ssp is None:
+ try:
+ self._host.run('which lxc-start')
+ self._support_ssp = True
+ except error.AutotestHostRunError:
+ self._support_ssp = False
+ return self._support_ssp
+
+
SiteDrone = utils.import_site_class(
__file__, 'autotest_lib.scheduler.site_drones',
'_SiteAbstractDrone', _BaseAbstractDrone)
diff --git a/scheduler/monitor_db.py b/scheduler/monitor_db.py
index 59af937..a29023a 100755
--- a/scheduler/monitor_db.py
+++ b/scheduler/monitor_db.py
@@ -19,6 +19,7 @@
import django.db
+from autotest_lib.client.common_lib import control_data
from autotest_lib.client.common_lib import global_config
from autotest_lib.client.common_lib import utils
from autotest_lib.client.common_lib.cros.graphite import autotest_stats
@@ -70,6 +71,9 @@
scheduler_config.CONFIG_SECTION, 'inline_host_acquisition', type=bool,
default=True)
+_enable_ssp_container = global_config.global_config.get_config_value(
+ 'AUTOSERV', 'enable_ssp_container', type=bool,
+ default=True)
def _site_init_monitor_db_dummy():
return {}
@@ -1291,6 +1295,11 @@
def _command_line(self):
invocation = super(QueueTask, self)._command_line()
+ # Check if server-side packaging is needed.
+ if (_enable_ssp_container and
+ self.job.control_type == control_data.CONTROL_TYPE.SERVER and
+ self.job.require_ssp != False):
+ invocation += ['--require-ssp']
return invocation + ['--verify_job_repo_url']
diff --git a/server/autoserv b/server/autoserv
index 24ec567..63748b0 100755
--- a/server/autoserv
+++ b/server/autoserv
@@ -65,6 +65,14 @@
sys.exit(1)
def run_autoserv(pid_file_manager, results, parser):
+ if parser.options.warn_no_ssp:
+ # Post a warning in the log and force to not use server-side packaging.
+ parser.options.require_ssp = False
+ logging.warn('Autoserv is required to run with server-side packaging. '
+ 'However, no drone is found to support server-side '
+ 'packaging. The test will be executed in a drone without '
+ 'server-side packaging supported.')
+
# send stdin to /dev/null
dev_null = os.open(os.devnull, os.O_RDONLY)
os.dup2(dev_null, sys.stdin.fileno())
@@ -188,7 +196,8 @@
% group_name)
kwargs = {'group_name': group_name, 'tag': execution_tag,
- 'disable_sysinfo': parser.options.disable_sysinfo}
+ 'disable_sysinfo': parser.options.disable_sysinfo,
+ 'require_ssp': parser.options.require_ssp}
if control_filename:
kwargs['control_filename'] = control_filename
job = server_job.server_job(control, parser.args[1:], results, label,
diff --git a/server/autoserv_parser.py b/server/autoserv_parser.py
index 5633bfb..3d706a4 100644
--- a/server/autoserv_parser.py
+++ b/server/autoserv_parser.py
@@ -168,6 +168,16 @@
dest="ssh_options", default='',
help=("A string giving command line flags "
"that will be included in ssh commands"))
+ self.parser.add_option("--require-ssp", action="store_true",
+ dest="require_ssp", default=False,
+ help=("Force the autoserv process to run with "
+ "server-side packaging"))
+ self.parser.add_option("--warn-no-ssp", action="store_true",
+ dest="warn_no_ssp", default=False,
+ help=("Post a warning in autoserv log that the "
+ "process runs in a drone without server-"
+ "side packaging support, even though the "
+ "job requires server-side packaging"))
def parse_args(self):
diff --git a/server/cros/dynamic_suite/fakes.py b/server/cros/dynamic_suite/fakes.py
index ff4e1f6..0a36069 100644
--- a/server/cros/dynamic_suite/fakes.py
+++ b/server/cros/dynamic_suite/fakes.py
@@ -25,6 +25,7 @@
self.sync_count = 1
self.job_retries = job_retries
self.bug_template = {}
+ self.require_ssp = None
class FakeJob(object):
diff --git a/server/cros/dynamic_suite/suite.py b/server/cros/dynamic_suite/suite.py
index 4ae7d60..bfc6d79 100644
--- a/server/cros/dynamic_suite/suite.py
+++ b/server/cros/dynamic_suite/suite.py
@@ -669,7 +669,8 @@
parent_job_id=self._suite_job_id,
test_retry=test.retries,
priority=self._priority,
- synch_count=test.sync_count)
+ synch_count=test.sync_count,
+ require_ssp=test.require_ssp)
setattr(test_obj, 'test_name', test.name)
diff --git a/server/cros/dynamic_suite/suite_unittest.py b/server/cros/dynamic_suite/suite_unittest.py
index a130e35..081cbd6 100755
--- a/server/cros/dynamic_suite/suite_unittest.py
+++ b/server/cros/dynamic_suite/suite_unittest.py
@@ -261,7 +261,8 @@
parent_job_id=None,
test_retry=0,
priority=priorities.Priority.DEFAULT,
- synch_count=test.sync_count
+ synch_count=test.sync_count,
+ require_ssp=test.require_ssp
)
if raises:
job_mock.AndRaise(error.NoEligibleHostException())
diff --git a/server/server_job.py b/server/server_job.py
index c573047..2488587 100644
--- a/server/server_job.py
+++ b/server/server_job.py
@@ -153,7 +153,8 @@
ssh_options=host_factory.DEFAULT_SSH_OPTIONS,
test_retry=0, group_name='',
tag='', disable_sysinfo=False,
- control_filename=SERVER_CONTROL_FILENAME):
+ control_filename=SERVER_CONTROL_FILENAME,
+ require_ssp=False):
"""
Create a server side job object.
@@ -181,6 +182,7 @@
tests for a modest shortening of test time. [optional]
@param control_filename: The filename where the server control file
should be written in the results directory.
+ @param require_ssp: Require to use server-side packaging to run the test.
"""
super(base_server_job, self).__init__(resultdir=resultdir,
test_retry=test_retry)
@@ -216,6 +218,7 @@
self.drop_caches_between_iterations = False
self._control_filename = control_filename
self._disable_sysinfo = disable_sysinfo
+ self._require_ssp = require_ssp
self.logging = logging_manager.get_logging_manager(
manage_stdout_and_stderr=True, redirect_fds=True)
@@ -510,6 +513,12 @@
return wrapper
+ def _run_with_ssp(self):
+ """Run the server job with server-side packaging.
+ """
+ raise NotImplementedError('Server-side packaging is not supported yet.')
+
+
def parallel_simple(self, function, machines, log=True, timeout=None,
return_results=False):
"""
@@ -640,7 +649,10 @@
else:
utils.open_write_close(server_control_file, control)
logging.info("Processing control file")
- self._execute_code(server_control_file, namespace)
+ if self._require_ssp:
+ self._run_with_ssp()
+ else:
+ self._execute_code(server_control_file, namespace)
logging.info("Finished processing control file")
# If no device error occured, no need to collect crashinfo.