• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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