1 #pylint: disable-msg=C0111 2 3 import cPickle 4 import logging 5 import os 6 import time 7 8 import common 9 from autotest_lib.scheduler import drone_utility, email_manager 10 from autotest_lib.client.bin import local_host 11 from autotest_lib.client.common_lib import error, global_config, utils 12 13 CONFIG = global_config.global_config 14 AUTOTEST_INSTALL_DIR = CONFIG.get_config_value('SCHEDULER', 15 'drone_installation_directory') 16 DEFAULT_CONTAINER_PATH = CONFIG.get_config_value('AUTOSERV', 'container_path') 17 18 class DroneUnreachable(Exception): 19 """The drone is non-sshable.""" 20 pass 21 22 23 class _BaseAbstractDrone(object): 24 """ 25 Attributes: 26 * allowed_users: set of usernames allowed to use this drone. if None, 27 any user can use this drone. 28 """ 29 def __init__(self, timestamp_remote_calls=True): 30 """Instantiate an abstract drone. 31 32 @param timestamp_remote_calls: If true, drone_utility is invoked with 33 the --call_time option and the current time. Currently this is only 34 used for testing. 35 """ 36 self._calls = [] 37 self.hostname = None 38 self.enabled = True 39 self.max_processes = 0 40 self.active_processes = 0 41 self.allowed_users = None 42 self._autotest_install_dir = AUTOTEST_INSTALL_DIR 43 self._host = None 44 self.timestamp_remote_calls = timestamp_remote_calls 45 # If drone supports server-side packaging. The property support_ssp will 46 # init self._support_ssp later. 47 self._support_ssp = None 48 49 50 def shutdown(self): 51 pass 52 53 54 @property 55 def _drone_utility_path(self): 56 return os.path.join(self._autotest_install_dir, 57 'scheduler', 'drone_utility.py') 58 59 60 def used_capacity(self): 61 """Gets the capacity used by this drone 62 63 Returns a tuple of (percentage_full, -max_capacity). This is to aid 64 direct comparisons, so that a 0/10 drone is considered less heavily 65 loaded than a 0/2 drone. 66 67 This value should never be used directly. It should only be used in 68 direct comparisons using the basic comparison operators, or using the 69 cmp() function. 70 """ 71 if self.max_processes == 0: 72 return (1.0, 0) 73 return (float(self.active_processes) / self.max_processes, 74 -self.max_processes) 75 76 77 def usable_by(self, user): 78 if self.allowed_users is None: 79 return True 80 return user in self.allowed_users 81 82 83 def _execute_calls_impl(self, calls): 84 if not self._host: 85 raise ValueError('Drone cannot execute calls without a host.') 86 drone_utility_cmd = self._drone_utility_path 87 if self.timestamp_remote_calls: 88 drone_utility_cmd = '%s --call_time %s' % ( 89 drone_utility_cmd, time.time()) 90 logging.info("Running drone_utility on %s", self.hostname) 91 result = self._host.run('python %s' % drone_utility_cmd, 92 stdin=cPickle.dumps(calls), stdout_tee=None, 93 connect_timeout=300) 94 try: 95 return cPickle.loads(result.stdout) 96 except Exception: # cPickle.loads can throw all kinds of exceptions 97 logging.critical('Invalid response:\n---\n%s\n---', result.stdout) 98 raise 99 100 101 def _execute_calls(self, calls): 102 return_message = self._execute_calls_impl(calls) 103 for warning in return_message['warnings']: 104 subject = 'Warning from drone %s' % self.hostname 105 logging.warning(subject + '\n' + warning) 106 email_manager.manager.enqueue_notify_email(subject, warning) 107 return return_message['results'] 108 109 110 def get_calls(self): 111 """Returns the calls queued against this drone. 112 113 @return: A list of calls queued against the drone. 114 """ 115 return self._calls 116 117 118 def call(self, method, *args, **kwargs): 119 return self._execute_calls( 120 [drone_utility.call(method, *args, **kwargs)]) 121 122 123 def queue_call(self, method, *args, **kwargs): 124 self._calls.append(drone_utility.call(method, *args, **kwargs)) 125 126 127 def clear_call_queue(self): 128 self._calls = [] 129 130 131 def execute_queued_calls(self): 132 if not self._calls: 133 return 134 results = self._execute_calls(self._calls) 135 self.clear_call_queue() 136 return results 137 138 139 def set_autotest_install_dir(self, path): 140 pass 141 142 143 @property 144 def support_ssp(self): 145 """Check if the drone supports server-side packaging with container. 146 147 @return: True if the drone supports server-side packaging with container 148 """ 149 if not self._host: 150 raise ValueError('Can not determine if drone supports server-side ' 151 'packaging before host is set.') 152 if self._support_ssp is None: 153 try: 154 # TODO(crbug.com/471316): We need a better way to check if drone 155 # supports container, and install/upgrade base container. The 156 # check of base container folder is not reliable and shall be 157 # obsoleted once that bug is fixed. 158 self._host.run('which lxc-start') 159 # Test if base container is setup. 160 base_container_name = CONFIG.get_config_value( 161 'AUTOSERV', 'container_base_name') 162 base_container = os.path.join(DEFAULT_CONTAINER_PATH, 163 base_container_name) 164 # SSP uses privileged containers, sudo access is required. If 165 # the process can't run sudo command without password, SSP can't 166 # work properly. sudo command option -n will avoid user input. 167 # If password is required, the command will fail and raise 168 # AutoservRunError exception. 169 self._host.run('sudo -n ls "%s"' % base_container) 170 self._support_ssp = True 171 except (error.AutoservRunError, error.AutotestHostRunError): 172 # Local drone raises AutotestHostRunError, while remote drone 173 # raises AutoservRunError. 174 self._support_ssp = False 175 return self._support_ssp 176 177 178 SiteDrone = utils.import_site_class( 179 __file__, 'autotest_lib.scheduler.site_drones', 180 '_SiteAbstractDrone', _BaseAbstractDrone) 181 182 183 class _AbstractDrone(SiteDrone): 184 pass 185 186 187 class _LocalDrone(_AbstractDrone): 188 def __init__(self, timestamp_remote_calls=True): 189 super(_LocalDrone, self).__init__( 190 timestamp_remote_calls=timestamp_remote_calls) 191 self.hostname = 'localhost' 192 self._host = local_host.LocalHost() 193 self._drone_utility = drone_utility.DroneUtility() 194 195 196 def send_file_to(self, drone, source_path, destination_path, 197 can_fail=False): 198 if drone.hostname == self.hostname: 199 self.queue_call('copy_file_or_directory', source_path, 200 destination_path) 201 else: 202 self.queue_call('send_file_to', drone.hostname, source_path, 203 destination_path, can_fail) 204 205 206 class _RemoteDrone(_AbstractDrone): 207 def __init__(self, hostname, timestamp_remote_calls=True): 208 super(_RemoteDrone, self).__init__( 209 timestamp_remote_calls=timestamp_remote_calls) 210 self.hostname = hostname 211 self._host = drone_utility.create_host(hostname) 212 if not self._host.is_up(): 213 logging.error('Drone %s is unpingable, kicking out', hostname) 214 raise DroneUnreachable 215 216 217 def set_autotest_install_dir(self, path): 218 self._autotest_install_dir = path 219 220 221 def shutdown(self): 222 super(_RemoteDrone, self).shutdown() 223 self._host.close() 224 225 226 def send_file_to(self, drone, source_path, destination_path, 227 can_fail=False): 228 if drone.hostname == self.hostname: 229 self.queue_call('copy_file_or_directory', source_path, 230 destination_path) 231 elif isinstance(drone, _LocalDrone): 232 drone.queue_call('get_file_from', self.hostname, source_path, 233 destination_path) 234 else: 235 self.queue_call('send_file_to', drone.hostname, source_path, 236 destination_path, can_fail) 237 238 239 def get_drone(hostname): 240 """ 241 Use this factory method to get drone objects. 242 """ 243 if hostname == 'localhost': 244 return _LocalDrone() 245 try: 246 return _RemoteDrone(hostname) 247 except DroneUnreachable: 248 return None 249