1# Copyright (c) 2014 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5"""RDB Host objects. 6 7RDBHost: Basic host object, capable of retrieving fields of a host that 8correspond to columns of the host table. 9 10RDBServerHostWrapper: Server side host adapters that help in making a raw 11database host object more ameanable to the classes and functions in the rdb 12and/or rdb clients. 13 14RDBClientHostWrapper: Scheduler host proxy that converts host information 15returned by the rdb into a client host object capable of proxying updates 16back to the rdb. 17""" 18 19import logging 20import time 21 22from django.core import exceptions as django_exceptions 23 24import common 25from autotest_lib.client.common_lib import utils 26from autotest_lib.frontend.afe import rdb_model_extensions as rdb_models 27from autotest_lib.frontend.afe import models as afe_models 28from autotest_lib.scheduler import rdb_requests 29from autotest_lib.scheduler import rdb_utils 30from autotest_lib.site_utils import lab_inventory 31from autotest_lib.site_utils import metadata_reporter 32from autotest_lib.site_utils.suite_scheduler import constants 33 34try: 35 from chromite.lib import metrics 36except ImportError: 37 metrics = utils.metrics_mock 38 39 40class RDBHost(object): 41 """A python host object representing a django model for the host.""" 42 43 required_fields = set( 44 rdb_models.AbstractHostModel.get_basic_field_names() + ['id']) 45 46 47 def _update_attributes(self, new_attributes): 48 """Updates attributes based on an input dictionary. 49 50 Since reads are not proxied to the rdb this method caches updates to 51 the host tables as class attributes. 52 53 @param new_attributes: A dictionary of attributes to update. 54 """ 55 for name, value in new_attributes.iteritems(): 56 setattr(self, name, value) 57 58 59 def __init__(self, **kwargs): 60 if self.required_fields - set(kwargs.keys()): 61 raise rdb_utils.RDBException('Creating %s requires %s, got %s ' 62 % (self.__class__, self.required_fields, kwargs.keys())) 63 self._update_attributes(kwargs) 64 65 66 @classmethod 67 def get_required_fields_from_host(cls, host): 68 """Returns all required attributes of the host parsed into a dict. 69 70 Required attributes are defined as the attributes required to 71 create an RDBHost, and mirror the columns of the host table. 72 73 @param host: A host object containing all required fields as attributes. 74 """ 75 required_fields_map = {} 76 try: 77 for field in cls.required_fields: 78 required_fields_map[field] = getattr(host, field) 79 except AttributeError as e: 80 raise rdb_utils.RDBException('Required %s' % e) 81 required_fields_map['id'] = host.id 82 return required_fields_map 83 84 85 def wire_format(self): 86 """Returns information about this host object. 87 88 @return: A dictionary of fields representing the host. 89 """ 90 return RDBHost.get_required_fields_from_host(self) 91 92 93class RDBServerHostWrapper(RDBHost): 94 """A host wrapper for the base host object. 95 96 This object contains all the attributes of the raw database columns, 97 and a few more that make the task of host assignment easier. It handles 98 the following duties: 99 1. Serialization of the host object and foreign keys 100 2. Conversion of label ids to label names, and retrieval of platform 101 3. Checking the leased bit/status of a host before leasing it out. 102 """ 103 104 def __init__(self, host): 105 """Create an RDBServerHostWrapper. 106 107 @param host: An instance of the Host model class. 108 """ 109 host_fields = RDBHost.get_required_fields_from_host(host) 110 super(RDBServerHostWrapper, self).__init__(**host_fields) 111 self.labels = rdb_utils.LabelIterator(host.labels.all()) 112 self.acls = [aclgroup.id for aclgroup in host.aclgroup_set.all()] 113 self.protection = host.protection 114 platform = host.platform() 115 # Platform needs to be a method, not an attribute, for 116 # backwards compatibility with the rest of the host model. 117 self.platform_name = platform.name if platform else None 118 self.shard_id = host.shard_id 119 120 121 def refresh(self, fields=None): 122 """Refresh the attributes on this instance. 123 124 @param fields: A list of fieldnames to refresh. If None 125 all the required fields of the host are refreshed. 126 127 @raises RDBException: If refreshing a field fails. 128 """ 129 # TODO: This is mainly required for cache correctness. If it turns 130 # into a bottleneck, cache host_ids instead of rdbhosts and rebuild 131 # the hosts once before leasing them out. The important part is to not 132 # trust the leased bit on a cached host. 133 fields = self.required_fields if not fields else fields 134 try: 135 refreshed_fields = afe_models.Host.objects.filter( 136 id=self.id).values(*fields)[0] 137 except django_exceptions.FieldError as e: 138 raise rdb_utils.RDBException('Couldn\'t refresh fields %s: %s' % 139 fields, e) 140 self._update_attributes(refreshed_fields) 141 142 143 def lease(self): 144 """Set the leased bit on the host object, and in the database. 145 146 @raises RDBException: If the host is already leased. 147 """ 148 self.refresh(fields=['leased']) 149 if self.leased: 150 raise rdb_utils.RDBException('Host %s is already leased' % 151 self.hostname) 152 self.leased = True 153 # TODO: Avoid leaking django out of rdb.QueryManagers. This is still 154 # preferable to calling save() on the host object because we're only 155 # updating/refreshing a single indexed attribute, the leased bit. 156 afe_models.Host.objects.filter(id=self.id).update(leased=self.leased) 157 158 159 def wire_format(self, unwrap_foreign_keys=True): 160 """Returns all information needed to scheduler jobs on the host. 161 162 @param unwrap_foreign_keys: If true this method will retrieve and 163 serialize foreign keys of the original host, which are stored 164 in the RDBServerHostWrapper as iterators. 165 166 @return: A dictionary of host information. 167 """ 168 host_info = super(RDBServerHostWrapper, self).wire_format() 169 170 if unwrap_foreign_keys: 171 host_info['labels'] = self.labels.get_label_names() 172 host_info['acls'] = self.acls 173 host_info['platform_name'] = self.platform_name 174 host_info['protection'] = self.protection 175 return host_info 176 177 178class RDBClientHostWrapper(RDBHost): 179 """A client host wrapper for the base host object. 180 181 This wrapper is used whenever the queue entry needs direct access 182 to the host. 183 """ 184 185 _HOST_WORKING_METRIC = 'chromeos/autotest/dut_working' 186 _HOST_STATUS_METRIC = 'chromeos/autotest/dut_status' 187 _HOST_POOL_METRIC = 'chromeos/autotest/dut_pool' 188 189 190 def __init__(self, **kwargs): 191 192 # This class is designed to only check for the bare minimum 193 # attributes on a host, so if a client tries accessing an 194 # unpopulated foreign key it will result in an exception. Doing 195 # so makes it easier to add fields to the rdb host without 196 # updating all the clients. 197 super(RDBClientHostWrapper, self).__init__(**kwargs) 198 199 # TODO(beeps): Remove this once we transition to urls 200 from autotest_lib.scheduler import rdb 201 self.update_request_manager = rdb_requests.RDBRequestManager( 202 rdb_requests.UpdateHostRequest, rdb.update_hosts) 203 self.dbg_str = '' 204 self.metadata = {} 205 206 207 def _update(self, payload): 208 """Send an update to rdb, save the attributes of the payload locally. 209 210 @param: A dictionary representing 'key':value of the update required. 211 212 @raises RDBException: If the update fails. 213 """ 214 logging.info('Host %s in %s updating %s through rdb on behalf of: %s ', 215 self.hostname, self.status, payload, self.dbg_str) 216 self.update_request_manager.add_request(host_id=self.id, 217 payload=payload) 218 for response in self.update_request_manager.response(): 219 if response: 220 raise rdb_utils.RDBException('Host %s unable to perform update ' 221 '%s through rdb on behalf of %s: %s', self.hostname, 222 payload, self.dbg_str, response) 223 super(RDBClientHostWrapper, self)._update_attributes(payload) 224 225 226 def record_state(self, type_str, state, value): 227 """Record metadata in elasticsearch. 228 229 @param type_str: sets the _type field in elasticsearch db. 230 @param state: string representing what state we are recording, 231 e.g. 'status' 232 @param value: value of the state, e.g. 'running' 233 """ 234 metadata = { 235 state: value, 236 'hostname': self.hostname, 237 'board': self.board, 238 'pools': self.pools, 239 'dbg_str': self.dbg_str, 240 '_type': type_str, 241 'time_recorded': time.time(), 242 } 243 metadata.update(self.metadata) 244 metadata_reporter.queue(metadata) 245 246 247 def get_metric_fields(self): 248 """Generate default set of fields to include for Monarch. 249 250 @return: Dictionary of default fields. 251 """ 252 fields = { 253 'dut_host_name': self.hostname, 254 'board': self.board or '', 255 } 256 257 return fields 258 259 260 def record_pool(self, fields): 261 """Report to Monarch current pool of dut. 262 263 @param fields Dictionary of fields to include. 264 """ 265 pool = '' 266 if len(self.pools) == 1: 267 pool = self.pools[0] 268 if pool in lab_inventory.MANAGED_POOLS: 269 pool = 'managed:' + pool 270 271 metrics.String(self._HOST_POOL_METRIC, 272 reset_after=True).set(pool, fields=fields) 273 274 275 def set_status(self, status): 276 """Proxy for setting the status of a host via the rdb. 277 278 @param status: The new status. 279 """ 280 # Update elasticsearch db. 281 self._update({'status': status}) 282 self.record_state('host_history', 'status', status) 283 284 # Update Monarch. 285 fields = self.get_metric_fields() 286 self.record_pool(fields) 287 # As each device switches state, indicate that it is not in any 288 # other state. This allows Monarch queries to avoid double counting 289 # when additional points are added by the Window Align operation. 290 host_status_metric = metrics.Boolean( 291 self._HOST_STATUS_METRIC, reset_after=True) 292 for s in rdb_models.AbstractHostModel.Status.names: 293 fields['status'] = s 294 host_status_metric.set(s == status, fields=fields) 295 296 297 def record_working_state(self, working, timestamp): 298 """Report to Monarch whether we are working or broken. 299 300 @param working Host repair status. `True` means that the DUT 301 is up and expected to pass tests. `False` 302 means the DUT has failed repair and requires 303 manual intervention. 304 @param timestamp Time that the status was recorded. 305 """ 306 fields = self.get_metric_fields() 307 metrics.Boolean( 308 self._HOST_WORKING_METRIC, reset_after=True).set( 309 working, fields=fields) 310 self.record_pool(fields) 311 312 313 def update_field(self, fieldname, value): 314 """Proxy for updating a field on the host. 315 316 @param fieldname: The fieldname as a string. 317 @param value: The value to assign to the field. 318 """ 319 self._update({fieldname: value}) 320 321 322 def platform_and_labels(self): 323 """Get the platform and labels on this host. 324 325 @return: A tuple containing a list of label names and the platform name. 326 """ 327 platform = self.platform_name 328 labels = [label for label in self.labels if label != platform] 329 return platform, labels 330 331 332 def platform(self): 333 """Get the name of the platform of this host. 334 335 @return: A string representing the name of the platform. 336 """ 337 return self.platform_name 338 339 340 def find_labels_start_with(self, search_string): 341 """Find all labels started with given string. 342 343 @param search_string: A string to match the beginning of the label. 344 @return: A list of all matched labels. 345 """ 346 try: 347 return [l for l in self.labels if l.startswith(search_string)] 348 except AttributeError: 349 return [] 350 351 352 @property 353 def board(self): 354 """Get the names of the board of this host. 355 356 @return: A string of the name of the board, e.g., lumpy. 357 """ 358 boards = self.find_labels_start_with(constants.Labels.BOARD_PREFIX) 359 return (boards[0][len(constants.Labels.BOARD_PREFIX):] if boards 360 else None) 361 362 363 @property 364 def pools(self): 365 """Get the names of the pools of this host. 366 367 @return: A list of pool names that the host is assigned to. 368 """ 369 return [label[len(constants.Labels.POOL_PREFIX):] for label in 370 self.find_labels_start_with(constants.Labels.POOL_PREFIX)] 371 372 373 def get_object_dict(self, **kwargs): 374 """Serialize the attributes of this object into a dict. 375 376 This method is called through frontend code to get a serialized 377 version of this object. 378 379 @param kwargs: 380 extra_fields: Extra fields, outside the columns of a host table. 381 382 @return: A dictionary representing the fields of this host object. 383 """ 384 # TODO(beeps): Implement support for extra fields. Currently nothing 385 # requires them. 386 return self.wire_format() 387 388 389 def save(self): 390 """Save any local data a client of this host object might have saved. 391 392 Setting attributes on a model before calling its save() method is a 393 common django pattern. Most, if not all updates to the host happen 394 either through set status or update_field. Though we keep the internal 395 state of the RDBClientHostWrapper consistent through these updates 396 we need a bulk save method such as this one to save any attributes of 397 this host another model might have set on it before calling its own 398 save method. Eg: 399 task = ST.objects.get(id=12) 400 task.host.status = 'Running' 401 task.save() -> this should result in the hosts status changing to 402 Running. 403 404 Functions like add_host_to_labels will have to update this host object 405 differently, as that is another level of foreign key indirection. 406 """ 407 self._update(self.get_required_fields_from_host(self)) 408 409 410def return_rdb_host(func): 411 """Decorator for functions that return a list of Host objects. 412 413 @param func: The decorated function. 414 @return: A functions capable of converting each host_object to a 415 rdb_hosts.RDBServerHostWrapper. 416 """ 417 def get_rdb_host(*args, **kwargs): 418 """Takes a list of hosts and returns a list of host_infos. 419 420 @param hosts: A list of hosts. Each host is assumed to contain 421 all the fields in a host_info defined above. 422 @return: A list of rdb_hosts.RDBServerHostWrappers, one per host, or an 423 empty list is no hosts were found.. 424 """ 425 hosts = func(*args, **kwargs) 426 return [RDBServerHostWrapper(host) for host in hosts] 427 return get_rdb_host 428