# 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.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 metadata_reporter
from autotest_lib.site_utils.suite_scheduler import constants
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.
"""
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 set_status(self, status):
"""Proxy for setting the status of a host via the rdb.
@param status: The new status.
"""
self._update({'status': status})
self.record_state('host_history', 'status', status)
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