Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 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 | |
| 7 | RDBHost: Basic host object, capable of retrieving fields of a host that |
| 8 | correspond to columns of the host table. |
| 9 | |
| 10 | RDBServerHostWrapper: Server side host adapters that help in making a raw |
| 11 | database host object more ameanable to the classes and functions in the rdb |
| 12 | and/or rdb clients. |
| 13 | |
| 14 | RDBClientHostWrapper: Scheduler host proxy that converts host information |
| 15 | returned by the rdb into a client host object capable of proxying updates |
| 16 | back to the rdb. |
| 17 | """ |
| 18 | |
| 19 | import logging |
Richard Barnette | ffed172 | 2016-05-18 15:57:22 -0700 | [diff] [blame] | 20 | |
Prashanth B | 2d8047e | 2014-04-27 18:54:47 -0700 | [diff] [blame] | 21 | from django.core import exceptions as django_exceptions |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 22 | |
| 23 | import common |
Dan Shi | 5e2efb7 | 2017-02-07 11:40:23 -0800 | [diff] [blame] | 24 | from autotest_lib.client.common_lib import utils |
Prashanth B | 2d8047e | 2014-04-27 18:54:47 -0700 | [diff] [blame] | 25 | from autotest_lib.frontend.afe import rdb_model_extensions as rdb_models |
| 26 | from autotest_lib.frontend.afe import models as afe_models |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 27 | from autotest_lib.scheduler import rdb_requests |
| 28 | from autotest_lib.scheduler import rdb_utils |
Xixuan Wu | 93e646c | 2017-12-07 18:36:10 -0800 | [diff] [blame] | 29 | from autotest_lib.server import constants |
Prathmesh Prabhu | 31d1e26 | 2017-11-21 12:24:34 -0800 | [diff] [blame] | 30 | from autotest_lib.utils import labellib |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 31 | |
Dan Shi | 5e2efb7 | 2017-02-07 11:40:23 -0800 | [diff] [blame] | 32 | try: |
| 33 | from chromite.lib import metrics |
| 34 | except ImportError: |
| 35 | metrics = utils.metrics_mock |
| 36 | |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 37 | |
| 38 | class RDBHost(object): |
| 39 | """A python host object representing a django model for the host.""" |
| 40 | |
| 41 | required_fields = set( |
Prashanth B | 2d8047e | 2014-04-27 18:54:47 -0700 | [diff] [blame] | 42 | rdb_models.AbstractHostModel.get_basic_field_names() + ['id']) |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 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 | |
| 91 | class RDBServerHostWrapper(RDBHost): |
| 92 | """A host wrapper for the base host object. |
| 93 | |
| 94 | This object contains all the attributes of the raw database columns, |
Prashanth B | 2d8047e | 2014-04-27 18:54:47 -0700 | [diff] [blame] | 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. |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 100 | """ |
| 101 | |
| 102 | def __init__(self, host): |
Prashanth B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 103 | """Create an RDBServerHostWrapper. |
| 104 | |
| 105 | @param host: An instance of the Host model class. |
| 106 | """ |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 107 | host_fields = RDBHost.get_required_fields_from_host(host) |
| 108 | super(RDBServerHostWrapper, self).__init__(**host_fields) |
Prashanth B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 109 | self.labels = rdb_utils.LabelIterator(host.labels.all()) |
| 110 | self.acls = [aclgroup.id for aclgroup in host.aclgroup_set.all()] |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 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 |
Prashanth Balasubramanian | 8c98ac1 | 2014-12-23 11:26:44 -0800 | [diff] [blame] | 116 | self.shard_id = host.shard_id |
Prashanth B | 2d8047e | 2014-04-27 18:54:47 -0700 | [diff] [blame] | 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) |
Prashanth B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 139 | |
| 140 | |
| 141 | def lease(self): |
| 142 | """Set the leased bit on the host object, and in the database. |
| 143 | |
Prashanth B | 2d8047e | 2014-04-27 18:54:47 -0700 | [diff] [blame] | 144 | @raises RDBException: If the host is already leased. |
Prashanth B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 145 | """ |
Prashanth B | 2d8047e | 2014-04-27 18:54:47 -0700 | [diff] [blame] | 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) |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 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: |
Prashanth B | b474fdf | 2014-04-03 16:05:38 -0700 | [diff] [blame] | 169 | host_info['labels'] = self.labels.get_label_names() |
| 170 | host_info['acls'] = self.acls |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 171 | host_info['platform_name'] = self.platform_name |
| 172 | host_info['protection'] = self.protection |
| 173 | return host_info |
| 174 | |
| 175 | |
| 176 | class 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 | """ |
Paul Hobbs | 76f2357 | 2016-09-29 14:01:06 -0700 | [diff] [blame] | 182 | # Shows more detailed status of what a DUT is doing. |
Paul Hobbs | eedcb8b | 2016-10-05 16:44:27 -0700 | [diff] [blame] | 183 | _HOST_WORKING_METRIC = 'chromeos/autotest/dut_working' |
Paul Hobbs | 76f2357 | 2016-09-29 14:01:06 -0700 | [diff] [blame] | 184 | # Shows which hosts are working. |
Paul Hobbs | eedcb8b | 2016-10-05 16:44:27 -0700 | [diff] [blame] | 185 | _HOST_STATUS_METRIC = 'chromeos/autotest/dut_status' |
Paul Hobbs | 76f2357 | 2016-09-29 14:01:06 -0700 | [diff] [blame] | 186 | # Maps duts to pools. |
Paul Hobbs | eedcb8b | 2016-10-05 16:44:27 -0700 | [diff] [blame] | 187 | _HOST_POOL_METRIC = 'chromeos/autotest/dut_pool' |
Paul Hobbs | 76f2357 | 2016-09-29 14:01:06 -0700 | [diff] [blame] | 188 | # Shows which scheduler machines are using a DUT. |
| 189 | _BOARD_SHARD_METRIC = 'chromeos/autotest/shard/board_presence' |
Richard Barnette | ffed172 | 2016-05-18 15:57:22 -0700 | [diff] [blame] | 190 | |
| 191 | |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 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 = '' |
Dan Shi | 7cf3d84 | 2014-08-13 11:20:38 -0700 | [diff] [blame] | 206 | self.metadata = {} |
Prathmesh Prabhu | 31d1e26 | 2017-11-21 12:24:34 -0800 | [diff] [blame] | 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 | |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 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 | |
David Riley | a7cfd85 | 2016-09-16 14:27:36 -0700 | [diff] [blame] | 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, |
Prathmesh Prabhu | b916738 | 2017-11-21 12:31:41 -0800 | [diff] [blame] | 240 | 'board': self.board, |
Prathmesh Prabhu | 4872b35 | 2017-11-21 12:34:47 -0800 | [diff] [blame] | 241 | 'model': self._model, |
David Riley | a7cfd85 | 2016-09-16 14:27:36 -0700 | [diff] [blame] | 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] |
Prathmesh Prabhu | 4971c1f | 2017-11-08 17:15:59 -0800 | [diff] [blame] | 255 | if pool in constants.Pools.MANAGED_POOLS: |
David Riley | a7cfd85 | 2016-09-16 14:27:36 -0700 | [diff] [blame] | 256 | pool = 'managed:' + pool |
| 257 | |
Paul Hobbs | eedcb8b | 2016-10-05 16:44:27 -0700 | [diff] [blame] | 258 | metrics.String(self._HOST_POOL_METRIC, |
| 259 | reset_after=True).set(pool, fields=fields) |
David Riley | a7cfd85 | 2016-09-16 14:27:36 -0700 | [diff] [blame] | 260 | |
| 261 | |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 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 | """ |
David Riley | a7cfd85 | 2016-09-16 14:27:36 -0700 | [diff] [blame] | 267 | # Update elasticsearch db. |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 268 | self._update({'status': status}) |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 269 | |
David Riley | a7cfd85 | 2016-09-16 14:27:36 -0700 | [diff] [blame] | 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. |
Paul Hobbs | eedcb8b | 2016-10-05 16:44:27 -0700 | [diff] [blame] | 276 | host_status_metric = metrics.Boolean( |
| 277 | self._HOST_STATUS_METRIC, reset_after=True) |
David Riley | a7cfd85 | 2016-09-16 14:27:36 -0700 | [diff] [blame] | 278 | for s in rdb_models.AbstractHostModel.Status.names: |
| 279 | fields['status'] = s |
Paul Hobbs | eedcb8b | 2016-10-05 16:44:27 -0700 | [diff] [blame] | 280 | host_status_metric.set(s == status, fields=fields) |
David Riley | a7cfd85 | 2016-09-16 14:27:36 -0700 | [diff] [blame] | 281 | |
Dan Shi | 0e96b04 | 2014-09-30 00:17:24 -0700 | [diff] [blame] | 282 | |
Richard Barnette | ffed172 | 2016-05-18 15:57:22 -0700 | [diff] [blame] | 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 | """ |
David Riley | a7cfd85 | 2016-09-16 14:27:36 -0700 | [diff] [blame] | 292 | fields = self.get_metric_fields() |
Paul Hobbs | eedcb8b | 2016-10-05 16:44:27 -0700 | [diff] [blame] | 293 | metrics.Boolean( |
| 294 | self._HOST_WORKING_METRIC, reset_after=True).set( |
| 295 | working, fields=fields) |
Prathmesh Prabhu | 4872b35 | 2017-11-21 12:34:47 -0800 | [diff] [blame] | 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 | ) |
David Riley | a7cfd85 | 2016-09-16 14:27:36 -0700 | [diff] [blame] | 303 | self.record_pool(fields) |
Richard Barnette | ffed172 | 2016-05-18 15:57:22 -0700 | [diff] [blame] | 304 | |
| 305 | |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 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 | |
Dan Shi | 0e96b04 | 2014-09-30 00:17:24 -0700 | [diff] [blame] | 333 | @property |
| 334 | def board(self): |
| 335 | """Get the names of the board of this host. |
| 336 | |
Prathmesh Prabhu | b916738 | 2017-11-21 12:31:41 -0800 | [diff] [blame] | 337 | @return: A string of the name of the board, e.g., lumpy. Returns '' if |
| 338 | no board label is found. |
Dan Shi | 0e96b04 | 2014-09-30 00:17:24 -0700 | [diff] [blame] | 339 | """ |
Prathmesh Prabhu | 31d1e26 | 2017-11-21 12:24:34 -0800 | [diff] [blame] | 340 | labels = labellib.LabelsMapping(self.labels) |
Prathmesh Prabhu | ed31428 | 2018-01-05 18:43:03 -0800 | [diff] [blame] | 341 | return labels.get('board', '') |
Dan Shi | 0e96b04 | 2014-09-30 00:17:24 -0700 | [diff] [blame] | 342 | |
| 343 | |
| 344 | @property |
Prathmesh Prabhu | 4872b35 | 2017-11-21 12:34:47 -0800 | [diff] [blame] | 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 |
Dan Shi | 0e96b04 | 2014-09-30 00:17:24 -0700 | [diff] [blame] | 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 | """ |
Prathmesh Prabhu | 31d1e26 | 2017-11-21 12:24:34 -0800 | [diff] [blame] | 361 | return [l[len(constants.Labels.POOL_PREFIX):] for l in self.labels |
| 362 | if l.startswith(constants.Labels.POOL_PREFIX)] |
Dan Shi | 0e96b04 | 2014-09-30 00:17:24 -0700 | [diff] [blame] | 363 | |
| 364 | |
Prashanth B | 489b91d | 2014-03-15 12:17:16 -0700 | [diff] [blame] | 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 | |
| 402 | def 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 |