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