• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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