434 lines
16 KiB
Python
434 lines
16 KiB
Python
# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
"""RDB Host objects.
|
|
|
|
RDBHost: Basic host object, capable of retrieving fields of a host that
|
|
correspond to columns of the host table.
|
|
|
|
RDBServerHostWrapper: Server side host adapters that help in making a raw
|
|
database host object more ameanable to the classes and functions in the rdb
|
|
and/or rdb clients.
|
|
|
|
RDBClientHostWrapper: Scheduler host proxy that converts host information
|
|
returned by the rdb into a client host object capable of proxying updates
|
|
back to the rdb.
|
|
"""
|
|
|
|
import logging
|
|
import time
|
|
|
|
from django.core import exceptions as django_exceptions
|
|
|
|
import common
|
|
from autotest_lib.client.common_lib import utils
|
|
from autotest_lib.frontend.afe import rdb_model_extensions as rdb_models
|
|
from autotest_lib.frontend.afe import models as afe_models
|
|
from autotest_lib.scheduler import rdb_requests
|
|
from autotest_lib.scheduler import rdb_utils
|
|
from autotest_lib.site_utils import lab_inventory
|
|
from autotest_lib.site_utils import metadata_reporter
|
|
from autotest_lib.site_utils.suite_scheduler import constants
|
|
|
|
try:
|
|
from chromite.lib import metrics
|
|
except ImportError:
|
|
metrics = utils.metrics_mock
|
|
|
|
|
|
class RDBHost(object):
|
|
"""A python host object representing a django model for the host."""
|
|
|
|
required_fields = set(
|
|
rdb_models.AbstractHostModel.get_basic_field_names() + ['id'])
|
|
|
|
|
|
def _update_attributes(self, new_attributes):
|
|
"""Updates attributes based on an input dictionary.
|
|
|
|
Since reads are not proxied to the rdb this method caches updates to
|
|
the host tables as class attributes.
|
|
|
|
@param new_attributes: A dictionary of attributes to update.
|
|
"""
|
|
for name, value in new_attributes.iteritems():
|
|
setattr(self, name, value)
|
|
|
|
|
|
def __init__(self, **kwargs):
|
|
if self.required_fields - set(kwargs.keys()):
|
|
raise rdb_utils.RDBException('Creating %s requires %s, got %s '
|
|
% (self.__class__, self.required_fields, kwargs.keys()))
|
|
self._update_attributes(kwargs)
|
|
|
|
|
|
@classmethod
|
|
def get_required_fields_from_host(cls, host):
|
|
"""Returns all required attributes of the host parsed into a dict.
|
|
|
|
Required attributes are defined as the attributes required to
|
|
create an RDBHost, and mirror the columns of the host table.
|
|
|
|
@param host: A host object containing all required fields as attributes.
|
|
"""
|
|
required_fields_map = {}
|
|
try:
|
|
for field in cls.required_fields:
|
|
required_fields_map[field] = getattr(host, field)
|
|
except AttributeError as e:
|
|
raise rdb_utils.RDBException('Required %s' % e)
|
|
required_fields_map['id'] = host.id
|
|
return required_fields_map
|
|
|
|
|
|
def wire_format(self):
|
|
"""Returns information about this host object.
|
|
|
|
@return: A dictionary of fields representing the host.
|
|
"""
|
|
return RDBHost.get_required_fields_from_host(self)
|
|
|
|
|
|
class RDBServerHostWrapper(RDBHost):
|
|
"""A host wrapper for the base host object.
|
|
|
|
This object contains all the attributes of the raw database columns,
|
|
and a few more that make the task of host assignment easier. It handles
|
|
the following duties:
|
|
1. Serialization of the host object and foreign keys
|
|
2. Conversion of label ids to label names, and retrieval of platform
|
|
3. Checking the leased bit/status of a host before leasing it out.
|
|
"""
|
|
|
|
def __init__(self, host):
|
|
"""Create an RDBServerHostWrapper.
|
|
|
|
@param host: An instance of the Host model class.
|
|
"""
|
|
host_fields = RDBHost.get_required_fields_from_host(host)
|
|
super(RDBServerHostWrapper, self).__init__(**host_fields)
|
|
self.labels = rdb_utils.LabelIterator(host.labels.all())
|
|
self.acls = [aclgroup.id for aclgroup in host.aclgroup_set.all()]
|
|
self.protection = host.protection
|
|
platform = host.platform()
|
|
# Platform needs to be a method, not an attribute, for
|
|
# backwards compatibility with the rest of the host model.
|
|
self.platform_name = platform.name if platform else None
|
|
self.shard_id = host.shard_id
|
|
|
|
|
|
def refresh(self, fields=None):
|
|
"""Refresh the attributes on this instance.
|
|
|
|
@param fields: A list of fieldnames to refresh. If None
|
|
all the required fields of the host are refreshed.
|
|
|
|
@raises RDBException: If refreshing a field fails.
|
|
"""
|
|
# TODO: This is mainly required for cache correctness. If it turns
|
|
# into a bottleneck, cache host_ids instead of rdbhosts and rebuild
|
|
# the hosts once before leasing them out. The important part is to not
|
|
# trust the leased bit on a cached host.
|
|
fields = self.required_fields if not fields else fields
|
|
try:
|
|
refreshed_fields = afe_models.Host.objects.filter(
|
|
id=self.id).values(*fields)[0]
|
|
except django_exceptions.FieldError as e:
|
|
raise rdb_utils.RDBException('Couldn\'t refresh fields %s: %s' %
|
|
fields, e)
|
|
self._update_attributes(refreshed_fields)
|
|
|
|
|
|
def lease(self):
|
|
"""Set the leased bit on the host object, and in the database.
|
|
|
|
@raises RDBException: If the host is already leased.
|
|
"""
|
|
self.refresh(fields=['leased'])
|
|
if self.leased:
|
|
raise rdb_utils.RDBException('Host %s is already leased' %
|
|
self.hostname)
|
|
self.leased = True
|
|
# TODO: Avoid leaking django out of rdb.QueryManagers. This is still
|
|
# preferable to calling save() on the host object because we're only
|
|
# updating/refreshing a single indexed attribute, the leased bit.
|
|
afe_models.Host.objects.filter(id=self.id).update(leased=self.leased)
|
|
|
|
|
|
def wire_format(self, unwrap_foreign_keys=True):
|
|
"""Returns all information needed to scheduler jobs on the host.
|
|
|
|
@param unwrap_foreign_keys: If true this method will retrieve and
|
|
serialize foreign keys of the original host, which are stored
|
|
in the RDBServerHostWrapper as iterators.
|
|
|
|
@return: A dictionary of host information.
|
|
"""
|
|
host_info = super(RDBServerHostWrapper, self).wire_format()
|
|
|
|
if unwrap_foreign_keys:
|
|
host_info['labels'] = self.labels.get_label_names()
|
|
host_info['acls'] = self.acls
|
|
host_info['platform_name'] = self.platform_name
|
|
host_info['protection'] = self.protection
|
|
return host_info
|
|
|
|
|
|
class RDBClientHostWrapper(RDBHost):
|
|
"""A client host wrapper for the base host object.
|
|
|
|
This wrapper is used whenever the queue entry needs direct access
|
|
to the host.
|
|
"""
|
|
# Shows more detailed status of what a DUT is doing.
|
|
_HOST_WORKING_METRIC = 'chromeos/autotest/dut_working'
|
|
# Shows which hosts are working.
|
|
_HOST_STATUS_METRIC = 'chromeos/autotest/dut_status'
|
|
# Maps duts to pools.
|
|
_HOST_POOL_METRIC = 'chromeos/autotest/dut_pool'
|
|
# Shows which scheduler machines are using a DUT.
|
|
_BOARD_SHARD_METRIC = 'chromeos/autotest/shard/board_presence'
|
|
|
|
|
|
def __init__(self, **kwargs):
|
|
|
|
# This class is designed to only check for the bare minimum
|
|
# attributes on a host, so if a client tries accessing an
|
|
# unpopulated foreign key it will result in an exception. Doing
|
|
# so makes it easier to add fields to the rdb host without
|
|
# updating all the clients.
|
|
super(RDBClientHostWrapper, self).__init__(**kwargs)
|
|
|
|
# TODO(beeps): Remove this once we transition to urls
|
|
from autotest_lib.scheduler import rdb
|
|
self.update_request_manager = rdb_requests.RDBRequestManager(
|
|
rdb_requests.UpdateHostRequest, rdb.update_hosts)
|
|
self.dbg_str = ''
|
|
self.metadata = {}
|
|
|
|
|
|
def _update(self, payload):
|
|
"""Send an update to rdb, save the attributes of the payload locally.
|
|
|
|
@param: A dictionary representing 'key':value of the update required.
|
|
|
|
@raises RDBException: If the update fails.
|
|
"""
|
|
logging.info('Host %s in %s updating %s through rdb on behalf of: %s ',
|
|
self.hostname, self.status, payload, self.dbg_str)
|
|
self.update_request_manager.add_request(host_id=self.id,
|
|
payload=payload)
|
|
for response in self.update_request_manager.response():
|
|
if response:
|
|
raise rdb_utils.RDBException('Host %s unable to perform update '
|
|
'%s through rdb on behalf of %s: %s', self.hostname,
|
|
payload, self.dbg_str, response)
|
|
super(RDBClientHostWrapper, self)._update_attributes(payload)
|
|
|
|
|
|
def record_state(self, type_str, state, value):
|
|
"""Record metadata in elasticsearch.
|
|
|
|
@param type_str: sets the _type field in elasticsearch db.
|
|
@param state: string representing what state we are recording,
|
|
e.g. 'status'
|
|
@param value: value of the state, e.g. 'running'
|
|
"""
|
|
metadata = {
|
|
state: value,
|
|
'hostname': self.hostname,
|
|
'board': self.board,
|
|
'pools': self.pools,
|
|
'dbg_str': self.dbg_str,
|
|
'_type': type_str,
|
|
'time_recorded': time.time(),
|
|
}
|
|
metadata.update(self.metadata)
|
|
metadata_reporter.queue(metadata)
|
|
|
|
|
|
def get_metric_fields(self):
|
|
"""Generate default set of fields to include for Monarch.
|
|
|
|
@return: Dictionary of default fields.
|
|
"""
|
|
fields = {
|
|
'dut_host_name': self.hostname,
|
|
'board': self.board or '',
|
|
}
|
|
|
|
return fields
|
|
|
|
|
|
def record_pool(self, fields):
|
|
"""Report to Monarch current pool of dut.
|
|
|
|
@param fields Dictionary of fields to include.
|
|
"""
|
|
pool = ''
|
|
if len(self.pools) == 1:
|
|
pool = self.pools[0]
|
|
if pool in lab_inventory.MANAGED_POOLS:
|
|
pool = 'managed:' + pool
|
|
|
|
metrics.String(self._HOST_POOL_METRIC,
|
|
reset_after=True).set(pool, fields=fields)
|
|
|
|
|
|
def set_status(self, status):
|
|
"""Proxy for setting the status of a host via the rdb.
|
|
|
|
@param status: The new status.
|
|
"""
|
|
# Update elasticsearch db.
|
|
self._update({'status': status})
|
|
self.record_state('host_history', 'status', status)
|
|
|
|
# Update Monarch.
|
|
fields = self.get_metric_fields()
|
|
self.record_pool(fields)
|
|
# As each device switches state, indicate that it is not in any
|
|
# other state. This allows Monarch queries to avoid double counting
|
|
# when additional points are added by the Window Align operation.
|
|
host_status_metric = metrics.Boolean(
|
|
self._HOST_STATUS_METRIC, reset_after=True)
|
|
for s in rdb_models.AbstractHostModel.Status.names:
|
|
fields['status'] = s
|
|
host_status_metric.set(s == status, fields=fields)
|
|
|
|
|
|
def record_working_state(self, working, timestamp):
|
|
"""Report to Monarch whether we are working or broken.
|
|
|
|
@param working Host repair status. `True` means that the DUT
|
|
is up and expected to pass tests. `False`
|
|
means the DUT has failed repair and requires
|
|
manual intervention.
|
|
@param timestamp Time that the status was recorded.
|
|
"""
|
|
fields = self.get_metric_fields()
|
|
metrics.Boolean(
|
|
self._HOST_WORKING_METRIC, reset_after=True).set(
|
|
working, fields=fields)
|
|
metrics.Boolean(
|
|
self._BOARD_SHARD_METRIC, reset_after=True).set(
|
|
True, fields={'board': self.board or ''})
|
|
self.record_pool(fields)
|
|
|
|
|
|
def update_field(self, fieldname, value):
|
|
"""Proxy for updating a field on the host.
|
|
|
|
@param fieldname: The fieldname as a string.
|
|
@param value: The value to assign to the field.
|
|
"""
|
|
self._update({fieldname: value})
|
|
|
|
|
|
def platform_and_labels(self):
|
|
"""Get the platform and labels on this host.
|
|
|
|
@return: A tuple containing a list of label names and the platform name.
|
|
"""
|
|
platform = self.platform_name
|
|
labels = [label for label in self.labels if label != platform]
|
|
return platform, labels
|
|
|
|
|
|
def platform(self):
|
|
"""Get the name of the platform of this host.
|
|
|
|
@return: A string representing the name of the platform.
|
|
"""
|
|
return self.platform_name
|
|
|
|
|
|
def find_labels_start_with(self, search_string):
|
|
"""Find all labels started with given string.
|
|
|
|
@param search_string: A string to match the beginning of the label.
|
|
@return: A list of all matched labels.
|
|
"""
|
|
try:
|
|
return [l for l in self.labels if l.startswith(search_string)]
|
|
except AttributeError:
|
|
return []
|
|
|
|
|
|
@property
|
|
def board(self):
|
|
"""Get the names of the board of this host.
|
|
|
|
@return: A string of the name of the board, e.g., lumpy.
|
|
"""
|
|
boards = self.find_labels_start_with(constants.Labels.BOARD_PREFIX)
|
|
return (boards[0][len(constants.Labels.BOARD_PREFIX):] if boards
|
|
else None)
|
|
|
|
|
|
@property
|
|
def pools(self):
|
|
"""Get the names of the pools of this host.
|
|
|
|
@return: A list of pool names that the host is assigned to.
|
|
"""
|
|
return [label[len(constants.Labels.POOL_PREFIX):] for label in
|
|
self.find_labels_start_with(constants.Labels.POOL_PREFIX)]
|
|
|
|
|
|
def get_object_dict(self, **kwargs):
|
|
"""Serialize the attributes of this object into a dict.
|
|
|
|
This method is called through frontend code to get a serialized
|
|
version of this object.
|
|
|
|
@param kwargs:
|
|
extra_fields: Extra fields, outside the columns of a host table.
|
|
|
|
@return: A dictionary representing the fields of this host object.
|
|
"""
|
|
# TODO(beeps): Implement support for extra fields. Currently nothing
|
|
# requires them.
|
|
return self.wire_format()
|
|
|
|
|
|
def save(self):
|
|
"""Save any local data a client of this host object might have saved.
|
|
|
|
Setting attributes on a model before calling its save() method is a
|
|
common django pattern. Most, if not all updates to the host happen
|
|
either through set status or update_field. Though we keep the internal
|
|
state of the RDBClientHostWrapper consistent through these updates
|
|
we need a bulk save method such as this one to save any attributes of
|
|
this host another model might have set on it before calling its own
|
|
save method. Eg:
|
|
task = ST.objects.get(id=12)
|
|
task.host.status = 'Running'
|
|
task.save() -> this should result in the hosts status changing to
|
|
Running.
|
|
|
|
Functions like add_host_to_labels will have to update this host object
|
|
differently, as that is another level of foreign key indirection.
|
|
"""
|
|
self._update(self.get_required_fields_from_host(self))
|
|
|
|
|
|
def return_rdb_host(func):
|
|
"""Decorator for functions that return a list of Host objects.
|
|
|
|
@param func: The decorated function.
|
|
@return: A functions capable of converting each host_object to a
|
|
rdb_hosts.RDBServerHostWrapper.
|
|
"""
|
|
def get_rdb_host(*args, **kwargs):
|
|
"""Takes a list of hosts and returns a list of host_infos.
|
|
|
|
@param hosts: A list of hosts. Each host is assumed to contain
|
|
all the fields in a host_info defined above.
|
|
@return: A list of rdb_hosts.RDBServerHostWrappers, one per host, or an
|
|
empty list is no hosts were found..
|
|
"""
|
|
hosts = func(*args, **kwargs)
|
|
return [RDBServerHostWrapper(host) for host in hosts]
|
|
return get_rdb_host
|