普通文本  |  330行  |  9.44 KB

# 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))