普通文本  |  271行  |  8.31 KB

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

import contextlib
import getpass
import subprocess
import os

import common
from autotest_lib.server.hosts import ssh_host
from autotest_lib.client.common_lib import error
from autotest_lib.client.common_lib import global_config
from autotest_lib.client.common_lib import utils
from autotest_lib.server.cros.dynamic_suite import frontend_wrappers


@contextlib.contextmanager
def chdir(dirname=None):
    """A context manager to help change directories.

    Will chdir into the provided dirname for the lifetime of the context and
    return to cwd thereafter.

    @param dirname: The dirname to chdir into.
    """
    curdir = os.getcwd()
    try:
        if dirname is not None:
            os.chdir(dirname)
        yield
    finally:
        os.chdir(curdir)


def local_runner(cmd, stream_output=False):
    """
    Runs a command on the local system as the current user.

    @param cmd: The command to run.
    @param stream_output: If True, streams the stdout of the process.

    @returns: The output of cmd.
    @raises CalledProcessError: If there was a non-0 return code.
    """
    if not stream_output:
        return subprocess.check_output(cmd, shell=True)
    proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
    while proc.poll() is None:
        print proc.stdout.readline().rstrip('\n')


_host_objects = {}

def host_object_runner(host, **kwargs):
    """
    Returns a function that returns the output of running a command via a host
    object.

    @param host: The host to run a command on.
    @returns: A function that can invoke a command remotely.
    """
    try:
        host_object = _host_objects[host]
    except KeyError:
        username = global_config.global_config.get_config_value(
                'CROS', 'infrastructure_user')
        host_object = ssh_host.SSHHost(host, user=username)
        _host_objects[host] = host_object

    def runner(cmd):
        """
        Runs a command via a host object on the enclosed host.  Translates
        host.run errors to the subprocess equivalent to expose a common API.

        @param cmd: The command to run.
        @returns: The output of cmd.
        @raises CalledProcessError: If there was a non-0 return code.
        """
        try:
            return host_object.run(cmd).stdout
        except error.AutotestHostRunError as e:
            exit_status = e.result_obj.exit_status
            command = e.result_obj.command
            raise subprocess.CalledProcessError(exit_status, command)
    return runner


def googlesh_runner(host, **kwargs):
    """
    Returns a function that return the output of running a command via shelling
    out to `googlesh`.

    @param host: The host to run a command on
    @returns: A function that can invoke a command remotely.
    """
    def runner(cmd):
        """
        Runs a command via googlesh on the enclosed host.

        @param cmd: The command to run.
        @returns: The output of cmd.
        @raises CalledProcessError: If there was a non-0 return code.
        """
        out = subprocess.check_output(['googlesh', '-s', '-uchromeos-test',
                                       '-m%s' % host, '%s' % cmd])
        return out
    return runner


def execute_command(host, cmd, **kwargs):
    """
    Executes a command on the host `host`.  This an optimization that if
    we're already chromeos-test, we can just ssh to the machine in question.
    Or if we're local, we don't have to ssh at all.

    @param host: The hostname to execute the command on.
    @param cmd: The command to run.  Special shell syntax (such as pipes)
                is allowed.
    @param kwargs: Key word arguments for the runner functions.
    @returns: The output of the command.
    """
    if utils.is_localhost(host):
        runner = local_runner
    elif getpass.getuser() == 'chromeos-test':
        runner = host_object_runner(host)
    else:
        runner = googlesh_runner(host)

    return runner(cmd, **kwargs)


def _csv_to_list(s):
    """
    Converts a list seperated by commas into a list of strings.

    >>> _csv_to_list('')
    []
    >>> _csv_to_list('one')
    ['one']
    >>> _csv_to_list('one, two,three')
    ['one', 'two', 'three']
    """
    return [x.strip() for x in s.split(',') if x]


# The goal with these functions is to give you a list of hosts that are valid
# arguments to ssh.  Note that this only really works since our instances use
# names that are findable by our default /etc/resolv.conf `search` domains,
# because all of our instances have names under .corp
def sam_servers():
    """
    Generate a list of all scheduler/afe instances of autotest.

    Note that we don't include the mysql database host if the database is split
    from the rest of the system.
    """
    sams_config = global_config.global_config.get_config_value(
            'CROS', 'sam_instances', default='')
    sams = _csv_to_list(sams_config)
    return set(sams)


def extra_servers():
    """
    Servers that have an autotest checkout in /usr/local/autotest, but aren't
    in any other list.

    @returns: A set of hosts.
    """
    servers = global_config.global_config.get_config_value(
                'CROS', 'extra_servers', default='')
    return set(_csv_to_list(servers))


def test_instance():
    """
    A server that is set up to run tests of the autotest infrastructure.

    @returns: A hostname
    """
    server = global_config.global_config.get_config_value(
                'CROS', 'test_instance', default='')
    return server


# The most reliable way to pull information about the state of the lab is to
# look at the global/shadow config on each server.  The best way to do this is
# via the global_config module.  Therefore, we invoke python on the remote end
# to call global_config to get whatever values we want.
_VALUE_FROM_CONFIG = '''
cd /usr/local/autotest
python -c "
import common
from autotest_lib.client.common_lib import global_config
print global_config.global_config.get_config_value(
  '%s', '%s', default='')
"
'''
# There's possibly cheaper ways to do some of this, for example, we could scrape
# instance:13467 for the list of drones, but this way you can get the list of
# drones that is what should/will be running, and not what the scheduler thinks
# is running.  (It could have kicked one out, or we could be bringing a new one
# into rotation.)  So scraping the config on remote servers, while slow, gives
# us consistent logical results.


def _scrape_from_instances(section, key):
    sams = sam_servers()
    all_servers = set()
    for sam in sams:
        servers_csv = execute_command(sam, _VALUE_FROM_CONFIG % (section, key))
        servers = _csv_to_list(servers_csv)
        for server in servers:
            if server == 'localhost':
                all_servers.add(sam)
            else:
                all_servers.add(server)
    return all_servers


def database_servers():
    """
    Generate a list of all database servers running for instances of autotest.

    @returns: An iterable of all hosts.
    """
    return _scrape_from_instances('AUTOTEST_WEB', 'host')


def drone_servers():
    """
    Generate a list of all drones used by all instances of autotest in
    production.

    @returns: An iterable of drone servers.
    """
    return _scrape_from_instances('SCHEDULER', 'drones')


def devserver_servers():
    """
    Generate a list of all devservers.

    @returns: An iterable of all hosts.
    """
    zone = global_config.global_config.get_config_value(
            'CLIENT', 'dns_zone')
    servers = _scrape_from_instances('CROS', 'dev_server_hosts')
    # The default text we get back here isn't something you can ssh into unless
    # you've set up your /etc/resolve.conf to automatically try .cros, so we
    # append the zone to try and make this more in line with everything else.
    return set([server+'.'+zone for server in servers])


def shard_servers():
    """
    Generate a list of all shard servers.

    @returns: An iterable of all shard servers.
    """
    shard_hostnames = set()
    sams = sam_servers()
    for sam in sams:
        afe = frontend_wrappers.RetryingAFE(server=sam)
        shards = afe.run('get_shards')
        for shard in shards:
            shard_hostnames.add(shard['hostname'])

    return list(shard_hostnames)