普通文本  |  471行  |  14.13 KB

#! /usr/bin/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.

"""
This script generates a csv file containing the mapping of
(device_hostname, rpm_hostname, outlet, hydra_hostname) for each
host in our lab. The csv file is in the following format.

chromeos-rack2-host1,chromeos-rack2-rpm1,.A1,chromeos-197-hydra1.mtv
chromeos-rack2-host2,chromeos-rack2-rpm1,.A2,chromeos-197-hydra1.mtv
...

The generated csv file can be used as input to add_host_powerunit_info.py

Workflow:
    <Generate the csv file>
    python generate_rpm_mapping.py --csv mapping_file.csv --server cautotest

    <Upload mapping information in csv file to AFE>
    python add_host_powerunit_info.py --csv mapping_file.csv

"""
import argparse
import collections
import logging
import re
import sys

import common

from autotest_lib.client.common_lib import enum
from autotest_lib.server.cros.dynamic_suite import frontend_wrappers

CHROMEOS_LABS = enum.Enum('OysterBay', 'Atlantis', 'Chaos', 'Destiny', start_value=1)
HOST_REGX = 'chromeos(\d+)(-row(\d+))*-rack(\d+)-host(\d+)'
DeviceHostname = collections.namedtuple(
        'DeviceHostname', ['lab', 'row', 'rack', 'host'])


class BaseLabConfig(object):
    """Base class for a lab configuration."""
    RPM_OUTLET_MAP = {}
    LAB_NUMBER = -1

    @classmethod
    def get_rpm_hostname(cls, device_hostname):
        """Get rpm hostname given a device.

        @param device_hostname: A DeviceHostname named tuple.

        @returns: the rpm hostname, default to empty string.

        """
        return ''


    @classmethod
    def get_rpm_outlet(cls, device_hostname):
        """Get rpm outlet given a device.

        @param device_hostname: A DeviceHostname named tuple.

        @returns: the rpm outlet, default to empty string.

        """
        return ''


    @classmethod
    def get_hydra_hostname(cls, device_hostname):
        """Get hydra hostname given a device.

        @param device_hostname: A DeviceHostname named tuple.

        @returns: the hydra hostname, default to empty string.

        """
        return ''


    @classmethod
    def is_device_in_the_lab(cls, device_hostname):
        """Check whether a dut belongs to the lab.

        @param device_hostname: A DeviceHostname named tuple.

        @returns: True if the dut belongs to the lab,
                  False otherwise.

        """
        return device_hostname.lab == cls.LAB_NUMBER


class OysterBayConfig(BaseLabConfig):
    """Configuration for OysterBay"""

    LAB_NUMBER = CHROMEOS_LABS.OYSTERBAY


    @classmethod
    def get_rpm_hostname(cls, device_hostname):
        """Get rpm hostname.

        @param device_hostname: A DeviceHostname named tuple.

        @returns: hostname of the rpm that has the device.

        """
        if not device_hostname.row:
            return ''
        return 'chromeos%d-row%d-rack%d-rpm1' % (
                device_hostname.lab, device_hostname.row,
                device_hostname.rack)


    @classmethod
    def get_rpm_outlet(cls, device_hostname):
        """Get rpm outlet.

        @param device_hostname: A DeviceHostname named tuple.

        @returns: rpm outlet, e.g. '.A1'

        """
        if not device_hostname.row:
            return ''
        return '.A%d' % device_hostname.host


class AtlantisConfig(BaseLabConfig):
    """Configuration for Atlantis lab."""

    LAB_NUMBER = CHROMEOS_LABS.ATLANTIS
    # chromeos2, hostX -> outlet
    RPM_OUTLET_MAP = {
            1: 1,
            7: 2,
            2: 4,
            8: 5,
            3: 7,
            9: 8,
            4: 9,
            10: 10,
            5: 12,
            11: 13,
            6: 15,
            12: 16}

    @classmethod
    def get_rpm_hostname(cls, device_hostname):
        """Get rpm hostname.

        @param device_hostname: A DeviceHostname named tuple.

        @returns: hostname of the rpm that has the device.

        """
        return 'chromeos%d-row%d-rack%d-rpm1' % (
                device_hostname.lab, device_hostname.row,
                device_hostname.rack)


    @classmethod
    def get_rpm_outlet(cls, device_hostname):
        """Get rpm outlet.

        @param device_hostname: A DeviceHostname named tuple.

        @returns: rpm outlet, e.g. '.A1'

        """
        return '.A%d' % cls.RPM_OUTLET_MAP[device_hostname.host]


    @classmethod
    def get_hydra_hostname(cls, device_hostname):
        """Get hydra hostname.

        @param device_hostname: A DeviceHostname named tuple.

        @returns: hydra hostname

        """
        row = device_hostname.row
        rack = device_hostname.rack
        if row >= 1 and row <= 5 and rack >= 1 and rack <= 7:
            return 'chromeos-197-hydra1.cros'
        elif row >= 1 and row <= 5 and rack >= 8 and rack <= 11:
            return 'chromeos-197-hydra2.cros'
        else:
            logging.error('Could not determine hydra for %s',
                          device_hostname)
            return ''


class ChaosConfig(BaseLabConfig):
    """Configuration for Chaos lab."""

    LAB_NUMBER = CHROMEOS_LABS.CHAOS


    @classmethod
    def get_rpm_hostname(cls, device_hostname):
        """Get rpm hostname.

        @param device_hostname: A DeviceHostname named tuple.

        @returns: hostname of the rpm that has the device.

        """
        return 'chromeos%d-row%d-rack%d-rpm1' % (
                device_hostname.lab, device_hostname.row,
                device_hostname.rack)


    @classmethod
    def get_rpm_outlet(cls, device_hostname):
        """Get rpm outlet.

        @param device_hostname: A DeviceHostname named tuple.

        @returns: rpm outlet, e.g. '.A1'

        """
        return '.A%d' % device_hostname.host


class DestinyConfig(BaseLabConfig):
    """Configuration for Desitny lab."""

    LAB_NUMBER = CHROMEOS_LABS.DESTINY
    # None-densified rack: one host per shelf
    # (rowX % 2, hostY) -> outlet
    RPM_OUTLET_MAP = {
            (1, 1): 1,
            (0, 1): 2,
            (1, 2): 4,
            (0, 2): 5,
            (1, 3): 7,
            (0, 3): 8,
            (1, 4): 9,
            (0, 4): 10,
            (1, 5): 12,
            (0, 5): 13,
            (1, 6): 15,
            (0, 6): 16,
    }

    # Densified rack: one shelf can have two chromeboxes or one notebook.
    # (rowX % 2, hostY) -> outlet
    DENSIFIED_RPM_OUTLET_MAP = {
            (1, 2):  1,  (1, 1): 1,
            (0, 1):  2,  (0, 2): 2,
            (1, 4):  3,  (1, 3): 3,
            (0, 3):  4,  (0, 4): 4,
            (1, 6):  5,  (1, 5): 5,
            (0, 5):  6,  (0, 6): 6,
            (1, 8):  7,  (1, 7): 7,
            (0, 7):  8,  (0, 8): 8,
            # outlet 9, 10 are not used
            (1, 10): 11, (1, 9): 11,
            (0, 9):  12, (0, 10): 12,
            (1, 12): 13, (1, 11): 13,
            (0, 11): 14, (0, 12): 14,
            (1, 14): 15, (1, 13): 15,
            (0, 13): 16, (0, 14): 16,
            (1, 16): 17, (1, 15): 17,
            (0, 15): 18, (0, 16): 18,
            (1, 18): 19, (1, 17): 19,
            (0, 17): 20, (0, 18): 20,
            (1, 20): 21, (1, 19): 21,
            (0, 19): 22, (0, 20): 22,
            (1, 22): 23, (1, 21): 23,
            (0, 21): 24, (0, 22): 24,
    }


    @classmethod
    def is_densified(cls, device_hostname):
        """Whether the host is on a densified rack.

        @param device_hostname: A DeviceHostname named tuple.

        @returns: True if on a densified rack, False otherwise.
        """
        return device_hostname.rack in (0, 12, 13)


    @classmethod
    def get_rpm_hostname(cls, device_hostname):
        """Get rpm hostname.

        @param device_hostname: A DeviceHostname named tuple.

        @returns: hostname of the rpm that has the device.

        """
        row = device_hostname.row
        if row == 13:
            logging.warn('Rule not implemented for row 13 in chromeos4')
            return ''

        # rpm row is like chromeos4-row1_2-rackX-rpmY
        rpm_row = ('%d_%d' % (row - 1, row) if row % 2 == 0 else
                   '%d_%d' % (row, row + 1))

        if cls.is_densified(device_hostname):
            # Densified rack has two rpms, decide which one the host belongs to
            # Rule:
            #     odd row number,  even host number -> rpm1
            #     odd row number,  odd host number  -> rpm2
            #     even row number, odd host number  -> rpm1
            #     even row number, even host number -> rpm2
            rpm_number = 1 if (row + device_hostname.host) % 2 == 1 else 2
        else:
            # Non-densified rack only has one rpm
            rpm_number = 1
        return 'chromeos%d-row%s-rack%d-rpm%d' % (
                device_hostname.lab,
                rpm_row, device_hostname.rack, rpm_number)


    @classmethod
    def get_rpm_outlet(cls, device_hostname):
        """Get rpm outlet.

        @param device_hostname: A DeviceHostname named tuple.

        @returns: rpm outlet, e.g. '.A1'

        """
        try:
            outlet_map = (cls.DENSIFIED_RPM_OUTLET_MAP
                          if cls.is_densified(device_hostname) else
                          cls.RPM_OUTLET_MAP)
            outlet_number = outlet_map[(device_hostname.row % 2,
                                        device_hostname.host)]
            return '.A%d' % outlet_number
        except KeyError:
            logging.error('Could not determine outlet for device %s',
                          device_hostname)
            return ''


    @classmethod
    def get_hydra_hostname(cls, device_hostname):
        """Get hydra hostname.

        @param device_hostname: A DeviceHostname named tuple.

        @returns: hydra hostname

        """
        row = device_hostname.row
        rack = device_hostname.rack
        if row >= 1 and row <= 6 and rack >=1 and rack <= 11:
            return 'chromeos-destiny-hydra1.cros'
        elif row >= 7 and row <= 12 and rack >=1 and rack <= 11:
            return 'chromeos-destiny-hydra2.cros'
        elif row >= 1 and row <= 10 and rack >=12 and rack <= 13:
            return 'chromeos-destiny-hydra3.cros'
        elif row in [3, 4, 5, 6, 9, 10] and rack == 0:
            return 'chromeos-destiny-hydra3.cros'
        elif row == 13 and rack >= 0 and rack <= 11:
            return 'chromeos-destiny-hydra3.cros'
        else:
            logging.error('Could not determine hydra hostname for %s',
                          device_hostname)
            return ''


def parse_device_hostname(device_hostname):
    """Parse device_hostname to DeviceHostname object.

    @param device_hostname: A string, e.g. 'chromeos2-row2-rack4-host3'

    @returns: A DeviceHostname named tuple or None if the
              the hostname doesn't follow the pattern
              defined in HOST_REGX.

    """
    m = re.match(HOST_REGX, device_hostname.strip())
    if m:
        return DeviceHostname(
                lab=int(m.group(1)),
                row=int(m.group(3)) if m.group(3) else None,
                rack=int(m.group(4)),
                host=int(m.group(5)))
    else:
        logging.error('Could not parse %s', device_hostname)
        return None


def generate_mapping(hosts, lab_configs):
    """Generate device_hostname-rpm-outlet-hydra mapping.

    @param hosts: hosts objects get from AFE.
    @param lab_configs: A list of configuration classes,
                        each one for a lab.

    @returns: A dictionary that maps device_hostname to
              (rpm_hostname, outlet, hydra_hostname)

    """
    # device hostname -> (rpm_hostname, outlet, hydra_hostname)
    rpm_mapping = {}
    for host in hosts:
        device_hostname = parse_device_hostname(host.hostname)
        if not device_hostname:
            continue
        for lab in lab_configs:
            if lab.is_device_in_the_lab(device_hostname):
                rpm_hostname = lab.get_rpm_hostname(device_hostname)
                rpm_outlet = lab.get_rpm_outlet(device_hostname)
                hydra_hostname = lab.get_hydra_hostname(device_hostname)
                if not rpm_hostname or not rpm_outlet:
                    logging.error(
                            'Skipping device %s: could not determine '
                            'rpm hostname or outlet.', host.hostname)
                    break
                rpm_mapping[host.hostname] = (
                        rpm_hostname, rpm_outlet, hydra_hostname)
                break
        else:
            logging.info(
                    '%s is not in a know lab '
                    '(oyster bay, atlantis, chaos, destiny)',
                    host.hostname)
    return rpm_mapping


def output_csv(rpm_mapping, csv_file):
    """Dump the rpm mapping dictionary to csv file.

    @param rpm_mapping: A dictionary that maps device_hostname to
                        (rpm_hostname, outlet, hydra_hostname)
    @param csv_file: The name of the file to write to.

    """
    with open(csv_file, 'w') as f:
        for hostname, rpm_info in rpm_mapping.iteritems():
            line = ','.join(rpm_info)
            line = ','.join([hostname, line])
            f.write(line + '\n')


if __name__ == '__main__':
    logging.basicConfig(level=logging.DEBUG)
    parser = argparse.ArgumentParser(
            description='Generate device_hostname-rpm-outlet-hydra mapping '
                        'file needed by add_host_powerunit_info.py')
    parser.add_argument('--csv', type=str, dest='csv_file', required=True,
                        help='The path to the csv file where we are going to '
                             'write the mapping information to.')
    parser.add_argument('--server', type=str, dest='server', default=None,
                        help='AFE server that the script will be talking to. '
                             'If not specified, will default to using the '
                             'server in global_config.ini')
    options = parser.parse_args()

    AFE = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10,
                                        server=options.server)
    logging.info('Connected to %s', AFE.server)
    rpm_mapping = generate_mapping(
            AFE.get_hosts(),
            [OysterBayConfig, AtlantisConfig, ChaosConfig, DestinyConfig])
    output_csv(rpm_mapping, options.csv_file)