1#!/usr/bin/python2 2 3# Copyright (c) 2014 The Chromium OS Authors. All rights reserved. 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7""" 8This script generates a csv file containing the mapping of 9(device_hostname, rpm_hostname, outlet, hydra_hostname) for each 10host in our lab. The csv file is in the following format. 11 12chromeos-rack2-host1,chromeos-rack2-rpm1,.A1,chromeos-197-hydra1.mtv 13chromeos-rack2-host2,chromeos-rack2-rpm1,.A2,chromeos-197-hydra1.mtv 14... 15 16The generated csv file can be used as input to add_host_powerunit_info.py 17 18Workflow: 19 <Generate the csv file> 20 python generate_rpm_mapping.py --csv mapping_file.csv --server cautotest 21 22 <Upload mapping information in csv file to AFE> 23 python add_host_powerunit_info.py --csv mapping_file.csv 24 25""" 26import argparse 27import collections 28import logging 29import re 30import sys 31 32import common 33 34from autotest_lib.client.common_lib import autotest_enum 35from autotest_lib.server.cros.dynamic_suite import frontend_wrappers 36 37CHROMEOS_LABS = autotest_enum.AutotestEnum('OysterBay', 'Atlantis', 38 'Chaos', 'Destiny', start_value=1) 39HOST_REGX = 'chromeos(\d+)(-row(\d+))*-rack(\d+)-host(\d+)' 40DeviceHostname = collections.namedtuple( 41 'DeviceHostname', ['lab', 'row', 'rack', 'host']) 42 43 44class BaseLabConfig(object): 45 """Base class for a lab configuration.""" 46 RPM_OUTLET_MAP = {} 47 LAB_NUMBER = -1 48 49 @classmethod 50 def get_rpm_hostname(cls, device_hostname): 51 """Get rpm hostname given a device. 52 53 @param device_hostname: A DeviceHostname named tuple. 54 55 @returns: the rpm hostname, default to empty string. 56 57 """ 58 return '' 59 60 61 @classmethod 62 def get_rpm_outlet(cls, device_hostname): 63 """Get rpm outlet given a device. 64 65 @param device_hostname: A DeviceHostname named tuple. 66 67 @returns: the rpm outlet, default to empty string. 68 69 """ 70 return '' 71 72 73 @classmethod 74 def get_hydra_hostname(cls, device_hostname): 75 """Get hydra hostname given a device. 76 77 @param device_hostname: A DeviceHostname named tuple. 78 79 @returns: the hydra hostname, default to empty string. 80 81 """ 82 return '' 83 84 85 @classmethod 86 def is_device_in_the_lab(cls, device_hostname): 87 """Check whether a dut belongs to the lab. 88 89 @param device_hostname: A DeviceHostname named tuple. 90 91 @returns: True if the dut belongs to the lab, 92 False otherwise. 93 94 """ 95 return device_hostname.lab == cls.LAB_NUMBER 96 97 98class OysterBayConfig(BaseLabConfig): 99 """Configuration for OysterBay""" 100 101 LAB_NUMBER = CHROMEOS_LABS.OYSTERBAY 102 103 104 @classmethod 105 def get_rpm_hostname(cls, device_hostname): 106 """Get rpm hostname. 107 108 @param device_hostname: A DeviceHostname named tuple. 109 110 @returns: hostname of the rpm that has the device. 111 112 """ 113 if not device_hostname.row: 114 return '' 115 return 'chromeos%d-row%d-rack%d-rpm1' % ( 116 device_hostname.lab, device_hostname.row, 117 device_hostname.rack) 118 119 120 @classmethod 121 def get_rpm_outlet(cls, device_hostname): 122 """Get rpm outlet. 123 124 @param device_hostname: A DeviceHostname named tuple. 125 126 @returns: rpm outlet, e.g. '.A1' 127 128 """ 129 if not device_hostname.row: 130 return '' 131 return '.A%d' % device_hostname.host 132 133 134class AtlantisConfig(BaseLabConfig): 135 """Configuration for Atlantis lab.""" 136 137 LAB_NUMBER = CHROMEOS_LABS.ATLANTIS 138 # chromeos2, hostX -> outlet 139 RPM_OUTLET_MAP = { 140 1: 1, 141 7: 2, 142 2: 4, 143 8: 5, 144 3: 7, 145 9: 8, 146 4: 9, 147 10: 10, 148 5: 12, 149 11: 13, 150 6: 15, 151 12: 16} 152 153 @classmethod 154 def get_rpm_hostname(cls, device_hostname): 155 """Get rpm hostname. 156 157 @param device_hostname: A DeviceHostname named tuple. 158 159 @returns: hostname of the rpm that has the device. 160 161 """ 162 return 'chromeos%d-row%d-rack%d-rpm1' % ( 163 device_hostname.lab, device_hostname.row, 164 device_hostname.rack) 165 166 167 @classmethod 168 def get_rpm_outlet(cls, device_hostname): 169 """Get rpm outlet. 170 171 @param device_hostname: A DeviceHostname named tuple. 172 173 @returns: rpm outlet, e.g. '.A1' 174 175 """ 176 return '.A%d' % cls.RPM_OUTLET_MAP[device_hostname.host] 177 178 179 @classmethod 180 def get_hydra_hostname(cls, device_hostname): 181 """Get hydra hostname. 182 183 @param device_hostname: A DeviceHostname named tuple. 184 185 @returns: hydra hostname 186 187 """ 188 row = device_hostname.row 189 rack = device_hostname.rack 190 if row >= 1 and row <= 5 and rack >= 1 and rack <= 7: 191 return 'chromeos-197-hydra1.cros' 192 elif row >= 1 and row <= 5 and rack >= 8 and rack <= 11: 193 return 'chromeos-197-hydra2.cros' 194 else: 195 logging.error('Could not determine hydra for %s', 196 device_hostname) 197 return '' 198 199 200class ChaosConfig(BaseLabConfig): 201 """Configuration for Chaos lab.""" 202 203 LAB_NUMBER = CHROMEOS_LABS.CHAOS 204 205 206 @classmethod 207 def get_rpm_hostname(cls, device_hostname): 208 """Get rpm hostname. 209 210 @param device_hostname: A DeviceHostname named tuple. 211 212 @returns: hostname of the rpm that has the device. 213 214 """ 215 return 'chromeos%d-row%d-rack%d-rpm1' % ( 216 device_hostname.lab, device_hostname.row, 217 device_hostname.rack) 218 219 220 @classmethod 221 def get_rpm_outlet(cls, device_hostname): 222 """Get rpm outlet. 223 224 @param device_hostname: A DeviceHostname named tuple. 225 226 @returns: rpm outlet, e.g. '.A1' 227 228 """ 229 return '.A%d' % device_hostname.host 230 231 232class DestinyConfig(BaseLabConfig): 233 """Configuration for Desitny lab.""" 234 235 LAB_NUMBER = CHROMEOS_LABS.DESTINY 236 # None-densified rack: one host per shelf 237 # (rowX % 2, hostY) -> outlet 238 RPM_OUTLET_MAP = { 239 (1, 1): 1, 240 (0, 1): 2, 241 (1, 2): 4, 242 (0, 2): 5, 243 (1, 3): 7, 244 (0, 3): 8, 245 (1, 4): 9, 246 (0, 4): 10, 247 (1, 5): 12, 248 (0, 5): 13, 249 (1, 6): 15, 250 (0, 6): 16, 251 } 252 253 # Densified rack: one shelf can have two chromeboxes or one notebook. 254 # (rowX % 2, hostY) -> outlet 255 DENSIFIED_RPM_OUTLET_MAP = { 256 (1, 2): 1, (1, 1): 1, 257 (0, 1): 2, (0, 2): 2, 258 (1, 4): 3, (1, 3): 3, 259 (0, 3): 4, (0, 4): 4, 260 (1, 6): 5, (1, 5): 5, 261 (0, 5): 6, (0, 6): 6, 262 (1, 8): 7, (1, 7): 7, 263 (0, 7): 8, (0, 8): 8, 264 # outlet 9, 10 are not used 265 (1, 10): 11, (1, 9): 11, 266 (0, 9): 12, (0, 10): 12, 267 (1, 12): 13, (1, 11): 13, 268 (0, 11): 14, (0, 12): 14, 269 (1, 14): 15, (1, 13): 15, 270 (0, 13): 16, (0, 14): 16, 271 (1, 16): 17, (1, 15): 17, 272 (0, 15): 18, (0, 16): 18, 273 (1, 18): 19, (1, 17): 19, 274 (0, 17): 20, (0, 18): 20, 275 (1, 20): 21, (1, 19): 21, 276 (0, 19): 22, (0, 20): 22, 277 (1, 22): 23, (1, 21): 23, 278 (0, 21): 24, (0, 22): 24, 279 } 280 281 282 @classmethod 283 def is_densified(cls, device_hostname): 284 """Whether the host is on a densified rack. 285 286 @param device_hostname: A DeviceHostname named tuple. 287 288 @returns: True if on a densified rack, False otherwise. 289 """ 290 return device_hostname.rack in (0, 12, 13) 291 292 293 @classmethod 294 def get_rpm_hostname(cls, device_hostname): 295 """Get rpm hostname. 296 297 @param device_hostname: A DeviceHostname named tuple. 298 299 @returns: hostname of the rpm that has the device. 300 301 """ 302 row = device_hostname.row 303 if row == 13: 304 logging.warn('Rule not implemented for row 13 in chromeos4') 305 return '' 306 307 # rpm row is like chromeos4-row1_2-rackX-rpmY 308 rpm_row = ('%d_%d' % (row - 1, row) if row % 2 == 0 else 309 '%d_%d' % (row, row + 1)) 310 311 if cls.is_densified(device_hostname): 312 # Densified rack has two rpms, decide which one the host belongs to 313 # Rule: 314 # odd row number, even host number -> rpm1 315 # odd row number, odd host number -> rpm2 316 # even row number, odd host number -> rpm1 317 # even row number, even host number -> rpm2 318 rpm_number = 1 if (row + device_hostname.host) % 2 == 1 else 2 319 else: 320 # Non-densified rack only has one rpm 321 rpm_number = 1 322 return 'chromeos%d-row%s-rack%d-rpm%d' % ( 323 device_hostname.lab, 324 rpm_row, device_hostname.rack, rpm_number) 325 326 327 @classmethod 328 def get_rpm_outlet(cls, device_hostname): 329 """Get rpm outlet. 330 331 @param device_hostname: A DeviceHostname named tuple. 332 333 @returns: rpm outlet, e.g. '.A1' 334 335 """ 336 try: 337 outlet_map = (cls.DENSIFIED_RPM_OUTLET_MAP 338 if cls.is_densified(device_hostname) else 339 cls.RPM_OUTLET_MAP) 340 outlet_number = outlet_map[(device_hostname.row % 2, 341 device_hostname.host)] 342 return '.A%d' % outlet_number 343 except KeyError: 344 logging.error('Could not determine outlet for device %s', 345 device_hostname) 346 return '' 347 348 349 @classmethod 350 def get_hydra_hostname(cls, device_hostname): 351 """Get hydra hostname. 352 353 @param device_hostname: A DeviceHostname named tuple. 354 355 @returns: hydra hostname 356 357 """ 358 row = device_hostname.row 359 rack = device_hostname.rack 360 if row >= 1 and row <= 6 and rack >=1 and rack <= 11: 361 return 'chromeos-destiny-hydra1.cros' 362 elif row >= 7 and row <= 12 and rack >=1 and rack <= 11: 363 return 'chromeos-destiny-hydra2.cros' 364 elif row >= 1 and row <= 10 and rack >=12 and rack <= 13: 365 return 'chromeos-destiny-hydra3.cros' 366 elif row in [3, 4, 5, 6, 9, 10] and rack == 0: 367 return 'chromeos-destiny-hydra3.cros' 368 elif row == 13 and rack >= 0 and rack <= 11: 369 return 'chromeos-destiny-hydra3.cros' 370 else: 371 logging.error('Could not determine hydra hostname for %s', 372 device_hostname) 373 return '' 374 375 376def parse_device_hostname(device_hostname): 377 """Parse device_hostname to DeviceHostname object. 378 379 @param device_hostname: A string, e.g. 'chromeos2-row2-rack4-host3' 380 381 @returns: A DeviceHostname named tuple or None if the 382 the hostname doesn't follow the pattern 383 defined in HOST_REGX. 384 385 """ 386 m = re.match(HOST_REGX, device_hostname.strip()) 387 if m: 388 return DeviceHostname( 389 lab=int(m.group(1)), 390 row=int(m.group(3)) if m.group(3) else None, 391 rack=int(m.group(4)), 392 host=int(m.group(5))) 393 else: 394 logging.error('Could not parse %s', device_hostname) 395 return None 396 397 398def generate_mapping(hosts, lab_configs): 399 """Generate device_hostname-rpm-outlet-hydra mapping. 400 401 @param hosts: hosts objects get from AFE. 402 @param lab_configs: A list of configuration classes, 403 each one for a lab. 404 405 @returns: A dictionary that maps device_hostname to 406 (rpm_hostname, outlet, hydra_hostname) 407 408 """ 409 # device hostname -> (rpm_hostname, outlet, hydra_hostname) 410 rpm_mapping = {} 411 for host in hosts: 412 device_hostname = parse_device_hostname(host.hostname) 413 if not device_hostname: 414 continue 415 for lab in lab_configs: 416 if lab.is_device_in_the_lab(device_hostname): 417 rpm_hostname = lab.get_rpm_hostname(device_hostname) 418 rpm_outlet = lab.get_rpm_outlet(device_hostname) 419 hydra_hostname = lab.get_hydra_hostname(device_hostname) 420 if not rpm_hostname or not rpm_outlet: 421 logging.error( 422 'Skipping device %s: could not determine ' 423 'rpm hostname or outlet.', host.hostname) 424 break 425 rpm_mapping[host.hostname] = ( 426 rpm_hostname, rpm_outlet, hydra_hostname) 427 break 428 else: 429 logging.info( 430 '%s is not in a know lab ' 431 '(oyster bay, atlantis, chaos, destiny)', 432 host.hostname) 433 return rpm_mapping 434 435 436def output_csv(rpm_mapping, csv_file): 437 """Dump the rpm mapping dictionary to csv file. 438 439 @param rpm_mapping: A dictionary that maps device_hostname to 440 (rpm_hostname, outlet, hydra_hostname) 441 @param csv_file: The name of the file to write to. 442 443 """ 444 with open(csv_file, 'w') as f: 445 for hostname, rpm_info in rpm_mapping.iteritems(): 446 line = ','.join(rpm_info) 447 line = ','.join([hostname, line]) 448 f.write(line + '\n') 449 450 451if __name__ == '__main__': 452 logging.basicConfig(level=logging.DEBUG) 453 parser = argparse.ArgumentParser( 454 description='Generate device_hostname-rpm-outlet-hydra mapping ' 455 'file needed by add_host_powerunit_info.py') 456 parser.add_argument('--csv', type=str, dest='csv_file', required=True, 457 help='The path to the csv file where we are going to ' 458 'write the mapping information to.') 459 parser.add_argument('--server', type=str, dest='server', default=None, 460 help='AFE server that the script will be talking to. ' 461 'If not specified, will default to using the ' 462 'server in global_config.ini') 463 options = parser.parse_args() 464 465 AFE = frontend_wrappers.RetryingAFE(timeout_min=5, delay_sec=10, 466 server=options.server) 467 logging.info('Connected to %s', AFE.server) 468 rpm_mapping = generate_mapping( 469 AFE.get_hosts(), 470 [OysterBayConfig, AtlantisConfig, ChaosConfig, DestinyConfig]) 471 output_csv(rpm_mapping, options.csv_file) 472