# Copyright (c) 2012 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.
#pylint: disable-msg=C0111
def order_by_complexity(host_spec_list):
"""
Returns a new list of HostSpecs, ordered from most to least complex.
Currently, 'complex' means that the spec contains more labels.
We may want to get smarter about this.
@param host_spec_list: a list of HostSpec objects.
@return a new list of HostSpec, ordered from most to least complex.
"""
def extract_label_list_len(host_spec):
return len(host_spec.labels)
return sorted(host_spec_list, key=extract_label_list_len, reverse=True)
def is_simple_list(host_spec_list):
"""
Returns true if this is a 'simple' list of HostSpec objects.
A 'simple' list of HostSpec objects is defined as a list of one HostSpec.
@param host_spec_list: a list of HostSpec objects.
@return True if this is a list of size 1, False otherwise.
"""
return len(host_spec_list) == 1
def simple_get_spec_and_hosts(host_specs, hosts_per_spec):
"""Given a simple list of HostSpec, extract hosts from hosts_per_spec.
Given a simple list of HostSpec objects, pull out the spec and use it to
get the associated hosts out of hosts_per_spec. Return the spec and the
host list as a pair.
@param host_specs: an iterable of HostSpec objects.
@param hosts_per_spec: map of {HostSpec: [list, of, hosts]}
@return (HostSpec, [list, of, hosts]}
"""
spec = host_specs.pop()
return spec, hosts_per_spec[spec]
class HostGroup(object):
"""A high-level specification of a group of hosts.
A HostGroup represents a group of hosts against which a job can be
scheduled. An instance is capable of returning arguments that can specify
this group in a call to AFE.create_job().
"""
def __init__(self):
pass
def as_args(self):
"""Return args suitable for passing to AFE.create_job()."""
raise NotImplementedError()
def size(self):
"""Returns the number of hosts specified by the group."""
raise NotImplementedError()
def mark_host_success(self, hostname):
"""Marks the provided host as successfully reimaged.
@param hostname: the name of the host that was reimaged.
"""
raise NotImplementedError()
def enough_hosts_succeeded(self):
"""Returns True if enough hosts in the group were reimaged for use."""
raise NotImplementedError()
#pylint: disable-msg=C0111
@property
def unsatisfied_specs(self):
return []
#pylint: disable-msg=C0111
@property
def doomed_specs(self):
return []
class ExplicitHostGroup(HostGroup):
"""A group of hosts, specified by name, to be reimaged for use.
@var _hostname_data_dict: {hostname: HostData()}.
"""
class HostData(object):
"""A HostSpec of a given host, and whether it reimaged successfully."""
def __init__(self, spec):
self.spec = spec
self.image_success = False
def __init__(self, hosts_per_spec={}):
"""Constructor.
@param hosts_per_spec: {HostSpec: [list, of, hosts]}.
Each host can appear only once.
"""
self._hostname_data_dict = {}
self._potentially_unsatisfied_specs = []
for spec, host_list in hosts_per_spec.iteritems():
for host in host_list:
self.add_host_for_spec(spec, host)
def _get_host_datas(self):
return self._hostname_data_dict.itervalues()
def as_args(self):
return {'hosts': self._hostname_data_dict.keys()}
def size(self):
return len(self._hostname_data_dict)
def mark_host_success(self, hostname):
self._hostname_data_dict[hostname].image_success = True
def enough_hosts_succeeded(self):
"""If _any_ hosts were reimaged, that's enough."""
return True in [d.image_success for d in self._get_host_datas()]
def add_host_for_spec(self, spec, host):
"""Add a new host for the given HostSpec to the group.
@param spec: HostSpec to associate host with.
@param host: a Host object; each host can appear only once.
If None, this spec will be relegated to the list of
potentially unsatisfied specs.
"""
if not host:
if spec not in [d.spec for d in self._get_host_datas()]:
self._potentially_unsatisfied_specs.append(spec)
return
if self.contains_host(host):
raise ValueError('A Host can appear in an '
'ExplicitHostGroup only once.')
if spec in self._potentially_unsatisfied_specs:
self._potentially_unsatisfied_specs.remove(spec)
self._hostname_data_dict[host.hostname] = self.HostData(spec)
def contains_host(self, host):
"""Whether host is already part of this HostGroup
@param host: a Host object.
@return True if the host is already tracked; False otherwise.
"""
return host.hostname in self._hostname_data_dict
@property
def unsatisfied_specs(self):
unsatisfied = []
for spec in self._potentially_unsatisfied_specs:
# If a spec in _potentially_unsatisfied_specs is a subset of some
# satisfied spec, then it's not unsatisfied.
if filter(lambda d: spec.is_subset(d.spec), self._get_host_datas()):
continue
unsatisfied.append(spec)
return unsatisfied
@property
def doomed_specs(self):
ok = set()
possibly_doomed = set()
for data in self._get_host_datas():
# If imaging succeeded for any host that satisfies a spec,
# it's definitely not doomed.
if data.image_success:
ok.add(data.spec)
else:
possibly_doomed.add(data.spec)
# If a spec is not a subset of any ok spec, it's doomed.
return set([s for s in possibly_doomed if not filter(s.is_subset, ok)])
class MetaHostGroup(HostGroup):
"""A group of hosts, specified by a meta_host and deps, to be reimaged.
@var _meta_hosts: a meta_host, as expected by AFE.create_job()
@var _dependencies: list of dependencies that all hosts to be used
must satisfy
@var _successful_hosts: set of successful hosts.
"""
def __init__(self, labels, num):
"""Constructor.
Given a set of labels specifying what kind of hosts we need,
and the num of hosts we need, build a meta_host and dependency list
that represent this group of hosts.
@param labels: list of labels indicating what kind of hosts need
to be reimaged.
@param num: how many hosts we'd like to reimage.
"""
self._spec = HostSpec(labels)
self._meta_hosts = labels[:1]*num
self._dependencies = labels[1:]
self._successful_hosts = set()
def as_args(self):
return {'meta_hosts': self._meta_hosts,
'dependencies': self._dependencies}
def size(self):
return len(self._meta_hosts)
def mark_host_success(self, hostname):
self._successful_hosts.add(hostname)
def enough_hosts_succeeded(self):
return self._successful_hosts
@property
def doomed_specs(self):
if self._successful_hosts:
return []
return [self._spec]
def _safeunion(iter_a, iter_b):
"""Returns an immutable set that contains the union of two iterables.
This function returns a frozen set containing the all the elements of
two iterables, regardless of whether those iterables are lists, sets,
or whatever.
@param iter_a: The first iterable.
@param iter_b: The second iterable.
@returns: An immutable union of the contents of iter_a and iter_b.
"""
return frozenset({a for a in iter_a} | {b for b in iter_b})
class HostSpec(object):
"""Specifies a kind of host on which dependency-having tests can be run.
Wraps a list of labels, for the purposes of specifying a set of hosts
on which a test with matching dependencies can be run.
"""
def __init__(self, base, extended=[]):
self._labels = _safeunion(base, extended)
# To amortize cost of __hash__()
self._str = 'HostSpec %r' % sorted(self._labels)
self._trivial = extended == []
#pylint: disable-msg=C0111
@property
def labels(self):
# Can I just do this as a set? Inquiring minds want to know.
return sorted(self._labels)
#pylint: disable-msg=C0111
@property
def is_trivial(self):
return self._trivial
#pylint: disable-msg=C0111
def is_subset(self, other):
return self._labels <= other._labels
def __str__(self):
return self._str
def __repr__(self):
return self._str
def __lt__(self, other):
return str(self) < str(other)
def __le__(self, other):
return str(self) <= str(other)
def __eq__(self, other):
return str(self) == str(other)
def __ne__(self, other):
return str(self) != str(other)
def __gt__(self, other):
return str(self) > str(other)
def __ge__(self, other):
return str(self) >= str(other)
def __hash__(self):
"""Allows instances to be correctly deduped when used in a set."""
return hash(str(self))