• 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
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    # Shows more detailed status of what a DUT is doing.
185    _HOST_WORKING_METRIC = 'chromeos/autotest/dut_working'
186    # Shows which hosts are working.
187    _HOST_STATUS_METRIC = 'chromeos/autotest/dut_status'
188    # Maps duts to pools.
189    _HOST_POOL_METRIC = 'chromeos/autotest/dut_pool'
190    # Shows which scheduler machines are using a DUT.
191    _BOARD_SHARD_METRIC = 'chromeos/autotest/shard/board_presence'
192
193
194    def __init__(self, **kwargs):
195
196        # This class is designed to only check for the bare minimum
197        # attributes on a host, so if a client tries accessing an
198        # unpopulated foreign key it will result in an exception. Doing
199        # so makes it easier to add fields to the rdb host without
200        # updating all the clients.
201        super(RDBClientHostWrapper, self).__init__(**kwargs)
202
203        # TODO(beeps): Remove this once we transition to urls
204        from autotest_lib.scheduler import rdb
205        self.update_request_manager = rdb_requests.RDBRequestManager(
206                rdb_requests.UpdateHostRequest, rdb.update_hosts)
207        self.dbg_str = ''
208        self.metadata = {}
209
210
211    def _update(self, payload):
212        """Send an update to rdb, save the attributes of the payload locally.
213
214        @param: A dictionary representing 'key':value of the update required.
215
216        @raises RDBException: If the update fails.
217        """
218        logging.info('Host %s in %s updating %s through rdb on behalf of: %s ',
219                     self.hostname, self.status, payload, self.dbg_str)
220        self.update_request_manager.add_request(host_id=self.id,
221                payload=payload)
222        for response in self.update_request_manager.response():
223            if response:
224                raise rdb_utils.RDBException('Host %s unable to perform update '
225                        '%s through rdb on behalf of %s: %s',  self.hostname,
226                        payload, self.dbg_str, response)
227        super(RDBClientHostWrapper, self)._update_attributes(payload)
228
229
230    def record_state(self, type_str, state, value):
231        """Record metadata in elasticsearch.
232
233        @param type_str: sets the _type field in elasticsearch db.
234        @param state: string representing what state we are recording,
235                      e.g. 'status'
236        @param value: value of the state, e.g. 'running'
237        """
238        metadata = {
239            state: value,
240            'hostname': self.hostname,
241            'board': self.board,
242            'pools': self.pools,
243            'dbg_str': self.dbg_str,
244            '_type': type_str,
245            'time_recorded': time.time(),
246        }
247        metadata.update(self.metadata)
248        metadata_reporter.queue(metadata)
249
250
251    def get_metric_fields(self):
252        """Generate default set of fields to include for Monarch.
253
254        @return: Dictionary of default fields.
255        """
256        fields = {
257            'dut_host_name': self.hostname,
258            'board': self.board or '',
259        }
260
261        return fields
262
263
264    def record_pool(self, fields):
265        """Report to Monarch current pool of dut.
266
267        @param fields   Dictionary of fields to include.
268        """
269        pool = ''
270        if len(self.pools) == 1:
271            pool = self.pools[0]
272        if pool in lab_inventory.MANAGED_POOLS:
273            pool = 'managed:' + pool
274
275        metrics.String(self._HOST_POOL_METRIC,
276                       reset_after=True).set(pool, fields=fields)
277
278
279    def set_status(self, status):
280        """Proxy for setting the status of a host via the rdb.
281
282        @param status: The new status.
283        """
284        # Update elasticsearch db.
285        self._update({'status': status})
286        self.record_state('host_history', 'status', status)
287
288        # Update Monarch.
289        fields = self.get_metric_fields()
290        self.record_pool(fields)
291        # As each device switches state, indicate that it is not in any
292        # other state.  This allows Monarch queries to avoid double counting
293        # when additional points are added by the Window Align operation.
294        host_status_metric = metrics.Boolean(
295                self._HOST_STATUS_METRIC, reset_after=True)
296        for s in rdb_models.AbstractHostModel.Status.names:
297            fields['status'] = s
298            host_status_metric.set(s == status, fields=fields)
299
300
301    def record_working_state(self, working, timestamp):
302        """Report to Monarch whether we are working or broken.
303
304        @param working    Host repair status. `True` means that the DUT
305                          is up and expected to pass tests.  `False`
306                          means the DUT has failed repair and requires
307                          manual intervention.
308        @param timestamp  Time that the status was recorded.
309        """
310        fields = self.get_metric_fields()
311        metrics.Boolean(
312                self._HOST_WORKING_METRIC, reset_after=True).set(
313                        working, fields=fields)
314        metrics.Boolean(
315                self._BOARD_SHARD_METRIC, reset_after=True).set(
316            True, fields={'board': self.board or ''})
317        self.record_pool(fields)
318
319
320    def update_field(self, fieldname, value):
321        """Proxy for updating a field on the host.
322
323        @param fieldname: The fieldname as a string.
324        @param value: The value to assign to the field.
325        """
326        self._update({fieldname: value})
327
328
329    def platform_and_labels(self):
330        """Get the platform and labels on this host.
331
332        @return: A tuple containing a list of label names and the platform name.
333        """
334        platform = self.platform_name
335        labels = [label for label in self.labels if label != platform]
336        return platform, labels
337
338
339    def platform(self):
340        """Get the name of the platform of this host.
341
342        @return: A string representing the name of the platform.
343        """
344        return self.platform_name
345
346
347    def find_labels_start_with(self, search_string):
348        """Find all labels started with given string.
349
350        @param search_string: A string to match the beginning of the label.
351        @return: A list of all matched labels.
352        """
353        try:
354            return [l for l in self.labels if l.startswith(search_string)]
355        except AttributeError:
356            return []
357
358
359    @property
360    def board(self):
361        """Get the names of the board of this host.
362
363        @return: A string of the name of the board, e.g., lumpy.
364        """
365        boards = self.find_labels_start_with(constants.Labels.BOARD_PREFIX)
366        return (boards[0][len(constants.Labels.BOARD_PREFIX):] if boards
367                else None)
368
369
370    @property
371    def pools(self):
372        """Get the names of the pools of this host.
373
374        @return: A list of pool names that the host is assigned to.
375        """
376        return [label[len(constants.Labels.POOL_PREFIX):] for label in
377                self.find_labels_start_with(constants.Labels.POOL_PREFIX)]
378
379
380    def get_object_dict(self, **kwargs):
381        """Serialize the attributes of this object into a dict.
382
383        This method is called through frontend code to get a serialized
384        version of this object.
385
386        @param kwargs:
387            extra_fields: Extra fields, outside the columns of a host table.
388
389        @return: A dictionary representing the fields of this host object.
390        """
391        # TODO(beeps): Implement support for extra fields. Currently nothing
392        # requires them.
393        return self.wire_format()
394
395
396    def save(self):
397        """Save any local data a client of this host object might have saved.
398
399        Setting attributes on a model before calling its save() method is a
400        common django pattern. Most, if not all updates to the host happen
401        either through set status or update_field. Though we keep the internal
402        state of the RDBClientHostWrapper consistent through these updates
403        we need a bulk save method such as this one to save any attributes of
404        this host another model might have set on it before calling its own
405        save method. Eg:
406            task = ST.objects.get(id=12)
407            task.host.status = 'Running'
408            task.save() -> this should result in the hosts status changing to
409            Running.
410
411        Functions like add_host_to_labels will have to update this host object
412        differently, as that is another level of foreign key indirection.
413        """
414        self._update(self.get_required_fields_from_host(self))
415
416
417def return_rdb_host(func):
418    """Decorator for functions that return a list of Host objects.
419
420    @param func: The decorated function.
421    @return: A functions capable of converting each host_object to a
422        rdb_hosts.RDBServerHostWrapper.
423    """
424    def get_rdb_host(*args, **kwargs):
425        """Takes a list of hosts and returns a list of host_infos.
426
427        @param hosts: A list of hosts. Each host is assumed to contain
428            all the fields in a host_info defined above.
429        @return: A list of rdb_hosts.RDBServerHostWrappers, one per host, or an
430            empty list is no hosts were found..
431        """
432        hosts = func(*args, **kwargs)
433        return [RDBServerHostWrapper(host) for host in hosts]
434    return get_rdb_host
435