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