blob: dad2de56e09860398dcfdcc4558a4752ed151949 [file] [log] [blame]
Prashanth B340fd1e2014-06-22 12:44:10 -07001#pylint: disable-msg=C0111
2
Prashanth Bcf731e32014-08-10 18:03:57 -07003import cPickle
4import logging
5import os
Prashanth Bcf731e32014-08-10 18:03:57 -07006import time
Dan Shi8f6108d2015-03-27 10:18:51 -07007
showard170873e2009-01-07 00:22:26 +00008import common
9from autotest_lib.scheduler import drone_utility, email_manager
Prashanth B340fd1e2014-06-22 12:44:10 -070010from autotest_lib.client.bin import local_host
Allen Li9bd3eca2017-02-06 15:09:23 -080011from autotest_lib.client.common_lib import error, global_config
showard170873e2009-01-07 00:22:26 +000012
Dan Shi0a7e4d62016-06-21 12:43:03 -070013CONFIG = global_config.global_config
14AUTOTEST_INSTALL_DIR = CONFIG.get_config_value('SCHEDULER',
15 'drone_installation_directory')
16DEFAULT_CONTAINER_PATH = CONFIG.get_config_value('AUTOSERV', 'container_path')
showard170873e2009-01-07 00:22:26 +000017
Keith Haddowb1c3e6e2017-10-13 09:20:59 -070018SSP_REQUIRED = CONFIG.get_config_value('SCHEDULER', 'exit_on_failed_ssp_setup',
19 default=False)
20
Eric Li861b2d52011-02-04 14:50:35 -080021class DroneUnreachable(Exception):
22 """The drone is non-sshable."""
23 pass
24
25
Allen Li9bd3eca2017-02-06 15:09:23 -080026class SiteDrone(object):
showard9bb960b2009-11-19 01:02:11 +000027 """
28 Attributes:
29 * allowed_users: set of usernames allowed to use this drone. if None,
30 any user can use this drone.
31 """
Prashanth Bcf731e32014-08-10 18:03:57 -070032 def __init__(self, timestamp_remote_calls=True):
33 """Instantiate an abstract drone.
34
35 @param timestamp_remote_calls: If true, drone_utility is invoked with
36 the --call_time option and the current time. Currently this is only
37 used for testing.
38 """
showard170873e2009-01-07 00:22:26 +000039 self._calls = []
40 self.hostname = None
showardc5afc462009-01-13 00:09:39 +000041 self.enabled = True
showard324bf812009-01-20 23:23:38 +000042 self.max_processes = 0
43 self.active_processes = 0
showard9bb960b2009-11-19 01:02:11 +000044 self.allowed_users = None
Prashanth B340fd1e2014-06-22 12:44:10 -070045 self._autotest_install_dir = AUTOTEST_INSTALL_DIR
46 self._host = None
Prashanth Bcf731e32014-08-10 18:03:57 -070047 self.timestamp_remote_calls = timestamp_remote_calls
Dan Shiec1d47d2015-02-13 11:38:13 -080048 # If drone supports server-side packaging. The property support_ssp will
49 # init self._support_ssp later.
50 self._support_ssp = None
Allen Li9bd3eca2017-02-06 15:09:23 -080051 self._processes_to_kill = []
showard170873e2009-01-07 00:22:26 +000052
53
54 def shutdown(self):
55 pass
56
57
Prashanth B340fd1e2014-06-22 12:44:10 -070058 @property
59 def _drone_utility_path(self):
60 return os.path.join(self._autotest_install_dir,
61 'scheduler', 'drone_utility.py')
62
63
showard324bf812009-01-20 23:23:38 +000064 def used_capacity(self):
jamesren37b50452010-03-25 20:38:56 +000065 """Gets the capacity used by this drone
66
67 Returns a tuple of (percentage_full, -max_capacity). This is to aid
68 direct comparisons, so that a 0/10 drone is considered less heavily
69 loaded than a 0/2 drone.
70
71 This value should never be used directly. It should only be used in
72 direct comparisons using the basic comparison operators, or using the
73 cmp() function.
74 """
showard324bf812009-01-20 23:23:38 +000075 if self.max_processes == 0:
jamesren37b50452010-03-25 20:38:56 +000076 return (1.0, 0)
77 return (float(self.active_processes) / self.max_processes,
78 -self.max_processes)
showard324bf812009-01-20 23:23:38 +000079
80
showard9bb960b2009-11-19 01:02:11 +000081 def usable_by(self, user):
82 if self.allowed_users is None:
83 return True
84 return user in self.allowed_users
85
86
showard170873e2009-01-07 00:22:26 +000087 def _execute_calls_impl(self, calls):
Prashanth B340fd1e2014-06-22 12:44:10 -070088 if not self._host:
89 raise ValueError('Drone cannot execute calls without a host.')
Prashanth Bcf731e32014-08-10 18:03:57 -070090 drone_utility_cmd = self._drone_utility_path
91 if self.timestamp_remote_calls:
92 drone_utility_cmd = '%s --call_time %s' % (
93 drone_utility_cmd, time.time())
Prashanth B340fd1e2014-06-22 12:44:10 -070094 logging.info("Running drone_utility on %s", self.hostname)
Prashanth Bcf731e32014-08-10 18:03:57 -070095 result = self._host.run('python %s' % drone_utility_cmd,
Prashanth B340fd1e2014-06-22 12:44:10 -070096 stdin=cPickle.dumps(calls), stdout_tee=None,
97 connect_timeout=300)
98 try:
99 return cPickle.loads(result.stdout)
100 except Exception: # cPickle.loads can throw all kinds of exceptions
101 logging.critical('Invalid response:\n---\n%s\n---', result.stdout)
102 raise
showard170873e2009-01-07 00:22:26 +0000103
104
105 def _execute_calls(self, calls):
106 return_message = self._execute_calls_impl(calls)
107 for warning in return_message['warnings']:
108 subject = 'Warning from drone %s' % self.hostname
Ilja H. Friedel04be2bd2014-05-07 21:29:59 -0700109 logging.warning(subject + '\n' + warning)
showard170873e2009-01-07 00:22:26 +0000110 email_manager.manager.enqueue_notify_email(subject, warning)
111 return return_message['results']
112
113
Prashanth B340fd1e2014-06-22 12:44:10 -0700114 def get_calls(self):
115 """Returns the calls queued against this drone.
116
117 @return: A list of calls queued against the drone.
118 """
119 return self._calls
120
121
showard170873e2009-01-07 00:22:26 +0000122 def call(self, method, *args, **kwargs):
123 return self._execute_calls(
124 [drone_utility.call(method, *args, **kwargs)])
125
126
127 def queue_call(self, method, *args, **kwargs):
128 self._calls.append(drone_utility.call(method, *args, **kwargs))
129
Dan Shi55d58992015-05-05 09:10:02 -0700130
showard170873e2009-01-07 00:22:26 +0000131 def clear_call_queue(self):
132 self._calls = []
133
134
135 def execute_queued_calls(self):
Allen Li9bd3eca2017-02-06 15:09:23 -0800136 """Execute queued calls.
137
138 If there are any processes queued to kill, kill them then process the
139 remaining queued up calls.
140 """
141 if self._processes_to_kill:
142 self.queue_call('kill_processes', self._processes_to_kill)
143 self.clear_processes_to_kill()
144
showard170873e2009-01-07 00:22:26 +0000145 if not self._calls:
146 return
Prashanth B340fd1e2014-06-22 12:44:10 -0700147 results = self._execute_calls(self._calls)
showard170873e2009-01-07 00:22:26 +0000148 self.clear_call_queue()
Prashanth B340fd1e2014-06-22 12:44:10 -0700149 return results
showard170873e2009-01-07 00:22:26 +0000150
151
showardac5b0002009-10-19 18:34:00 +0000152 def set_autotest_install_dir(self, path):
153 pass
154
155
Dan Shiec1d47d2015-02-13 11:38:13 -0800156 @property
157 def support_ssp(self):
158 """Check if the drone supports server-side packaging with container.
159
160 @return: True if the drone supports server-side packaging with container
161 """
162 if not self._host:
163 raise ValueError('Can not determine if drone supports server-side '
164 'packaging before host is set.')
165 if self._support_ssp is None:
166 try:
167 self._host.run('which lxc-start')
Dan Shi8f6108d2015-03-27 10:18:51 -0700168 # Test if base container is setup.
Dan Shi0a7e4d62016-06-21 12:43:03 -0700169 base_container_name = CONFIG.get_config_value(
170 'AUTOSERV', 'container_base_name')
171 base_container = os.path.join(DEFAULT_CONTAINER_PATH,
172 base_container_name)
Dan Shica3be482015-05-05 23:23:53 -0700173 # SSP uses privileged containers, sudo access is required. If
174 # the process can't run sudo command without password, SSP can't
175 # work properly. sudo command option -n will avoid user input.
176 # If password is required, the command will fail and raise
177 # AutoservRunError exception.
178 self._host.run('sudo -n ls "%s"' % base_container)
Dan Shiec1d47d2015-02-13 11:38:13 -0800179 self._support_ssp = True
Dan Shi8f6108d2015-03-27 10:18:51 -0700180 except (error.AutoservRunError, error.AutotestHostRunError):
181 # Local drone raises AutotestHostRunError, while remote drone
182 # raises AutoservRunError.
Dan Shid8c63892017-10-11 09:31:36 -0700183 logging.exception('Drone %s does not support server-side '
184 'packaging.', self.hostname)
Dan Shiec1d47d2015-02-13 11:38:13 -0800185 self._support_ssp = False
Keith Haddowb1c3e6e2017-10-13 09:20:59 -0700186 if SSP_REQUIRED:
187 raise
Dan Shiec1d47d2015-02-13 11:38:13 -0800188 return self._support_ssp
189
190
Allen Li9bd3eca2017-02-06 15:09:23 -0800191 def queue_kill_process(self, process):
192 """Queue a process to kill/abort.
193
194 @param process: Process to kill/abort.
195 """
196 self._processes_to_kill.append(process)
197
198
199 def clear_processes_to_kill(self):
200 """Reset the list of processes to kill for this tick."""
201 self._processes_to_kill = []
Simran Basiaf9b8e72012-10-12 15:02:36 -0700202
203
204class _AbstractDrone(SiteDrone):
205 pass
206
207
showard170873e2009-01-07 00:22:26 +0000208class _LocalDrone(_AbstractDrone):
Prashanth Bcf731e32014-08-10 18:03:57 -0700209 def __init__(self, timestamp_remote_calls=True):
210 super(_LocalDrone, self).__init__(
211 timestamp_remote_calls=timestamp_remote_calls)
showard170873e2009-01-07 00:22:26 +0000212 self.hostname = 'localhost'
Prashanth B340fd1e2014-06-22 12:44:10 -0700213 self._host = local_host.LocalHost()
showard170873e2009-01-07 00:22:26 +0000214
215
showard170873e2009-01-07 00:22:26 +0000216 def send_file_to(self, drone, source_path, destination_path,
217 can_fail=False):
218 if drone.hostname == self.hostname:
showardde634ee2009-01-30 01:44:24 +0000219 self.queue_call('copy_file_or_directory', source_path,
220 destination_path)
showard170873e2009-01-07 00:22:26 +0000221 else:
222 self.queue_call('send_file_to', drone.hostname, source_path,
223 destination_path, can_fail)
224
225
226class _RemoteDrone(_AbstractDrone):
Prashanth Bcf731e32014-08-10 18:03:57 -0700227 def __init__(self, hostname, timestamp_remote_calls=True):
228 super(_RemoteDrone, self).__init__(
229 timestamp_remote_calls=timestamp_remote_calls)
showard170873e2009-01-07 00:22:26 +0000230 self.hostname = hostname
231 self._host = drone_utility.create_host(hostname)
Eric Li861b2d52011-02-04 14:50:35 -0800232 if not self._host.is_up():
233 logging.error('Drone %s is unpingable, kicking out', hostname)
234 raise DroneUnreachable
Eric Lid656d562011-04-20 11:48:29 -0700235
236
showard202343e2009-10-14 16:20:24 +0000237 def set_autotest_install_dir(self, path):
238 self._autotest_install_dir = path
239
240
showard170873e2009-01-07 00:22:26 +0000241 def shutdown(self):
242 super(_RemoteDrone, self).shutdown()
243 self._host.close()
244
245
showard170873e2009-01-07 00:22:26 +0000246 def send_file_to(self, drone, source_path, destination_path,
247 can_fail=False):
248 if drone.hostname == self.hostname:
showardde634ee2009-01-30 01:44:24 +0000249 self.queue_call('copy_file_or_directory', source_path,
250 destination_path)
showard170873e2009-01-07 00:22:26 +0000251 elif isinstance(drone, _LocalDrone):
252 drone.queue_call('get_file_from', self.hostname, source_path,
253 destination_path)
254 else:
255 self.queue_call('send_file_to', drone.hostname, source_path,
256 destination_path, can_fail)
257
258
showard170873e2009-01-07 00:22:26 +0000259def get_drone(hostname):
260 """
261 Use this factory method to get drone objects.
262 """
263 if hostname == 'localhost':
264 return _LocalDrone()
Eric Li861b2d52011-02-04 14:50:35 -0800265 try:
266 return _RemoteDrone(hostname)
267 except DroneUnreachable:
268 return None