1# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5
6"""This file provides util functions used by RPM infrastructure."""
7
8
9import collections
10import csv
11import logging
12import os
13import time
14
15import common
16
17import rpm_infrastructure_exception
18from config import rpm_config
19from autotest_lib.client.common_lib import enum
20
21
22MAPPING_FILE = os.path.join(
23        os.path.dirname(__file__),
24        rpm_config.get('CiscoPOE', 'servo_interface_mapping_file'))
25
26
27POWERUNIT_HOSTNAME_KEY = 'powerunit_hostname'
28POWERUNIT_OUTLET_KEY = 'powerunit_outlet'
29HYDRA_HOSTNAME_KEY = 'hydra_hostname'
30DEFAULT_EXPIRATION_SECS = 60 * 30
31
32class PowerUnitInfo(object):
33    """A class that wraps rpm/poe information of a device."""
34
35    POWERUNIT_TYPES = enum.Enum('POE', 'RPM', string_value=True)
36
37    def __init__(self, device_hostname, powerunit_type,
38                 powerunit_hostname, outlet, hydra_hostname=None):
39        self.device_hostname = device_hostname
40        self.powerunit_type = powerunit_type
41        self.powerunit_hostname = powerunit_hostname
42        self.outlet = outlet
43        self.hydra_hostname = hydra_hostname
44
45
46    @staticmethod
47    def get_powerunit_info(afe_host):
48        """Constructe a PowerUnitInfo instance from an afe host.
49
50        @param afe_host: A host object.
51
52        @returns: A PowerUnitInfo object populated with the power management
53                  unit information of the host.
54        """
55        if (not POWERUNIT_HOSTNAME_KEY in afe_host.attributes or
56            not POWERUNIT_OUTLET_KEY in afe_host.attributes):
57            raise rpm_infrastructure_exception.RPMInfrastructureException(
58                    'Can not retrieve complete rpm information'
59                    'from AFE for %s, please make sure %s and %s are'
60                    ' in the host\'s attributes.' % (afe_host.hostname,
61                    POWERUNIT_HOSTNAME_KEY, POWERUNIT_OUTLET_KEY))
62
63        hydra_hostname=(afe_host.attributes[HYDRA_HOSTNAME_KEY]
64                        if HYDRA_HOSTNAME_KEY in afe_host.attributes
65                        else None)
66        return PowerUnitInfo(
67                device_hostname=afe_host.hostname,
68                powerunit_type=PowerUnitInfo.POWERUNIT_TYPES.RPM,
69                powerunit_hostname=afe_host.attributes[POWERUNIT_HOSTNAME_KEY],
70                outlet=afe_host.attributes[POWERUNIT_OUTLET_KEY],
71                hydra_hostname=hydra_hostname)
72
73
74class LRUCache(object):
75    """A simple implementation of LRU Cache."""
76
77
78    def __init__(self, size, expiration_secs=DEFAULT_EXPIRATION_SECS):
79        """Initialize.
80
81        @param size: Size of the cache.
82        @param expiration_secs: The items expire after |expiration_secs|
83                                Set to None so that items never expire.
84                                Default to DEFAULT_EXPIRATION_SECS.
85        """
86        self.size = size
87        self.cache = collections.OrderedDict()
88        self.timestamps = {}
89        self.expiration_secs = expiration_secs
90
91
92    def __getitem__(self, key):
93        """Get an item from the cache"""
94        # pop and insert the element again so that it
95        # is moved to the end.
96        value = self.cache.pop(key)
97        self.cache[key] = value
98        return value
99
100
101    def __setitem__(self, key, value):
102        """Insert an item into the cache."""
103        if key in self.cache:
104            self.cache.pop(key)
105        elif len(self.cache) == self.size:
106            removed_key, _ = self.cache.popitem(last=False)
107            self.timestamps.pop(removed_key)
108        self.cache[key] = value
109        self.timestamps[key] = time.time()
110
111
112    def __contains__(self, key):
113        """Check whether a key is in the cache."""
114        if (self.expiration_secs is not None and
115            key in self.timestamps and
116            time.time() - self.timestamps[key] > self.expiration_secs):
117            self.cache.pop(key)
118            self.timestamps.pop(key)
119        return key in self.cache
120
121
122def load_servo_interface_mapping(mapping_file=MAPPING_FILE):
123    """
124    Load servo-switch-interface mapping from a CSV file.
125
126    In the file, the first column represents servo hostnames,
127    the second column represents switch hostnames, the third column
128    represents interface names. Columns are saparated by comma.
129
130    chromeos1-rack3-host12-servo,chromeos1-poe-switch1,fa31
131    chromeos1-rack4-host2-servo,chromeos1-poe-switch1,fa32
132    ,chromeos1-poe-switch1,fa33
133    ...
134
135    A row without a servo hostname indicates that no servo
136    has been connected to the corresponding interface.
137    This method ignores such rows.
138
139    @param mapping_file: A csv file that stores the mapping.
140                         If None, the setting in rpm_config.ini will be used.
141
142    @return a dictionary that maps servo host name to a
143              tuple of switch hostname and interface.
144              e.g. {
145              'chromeos1-rack3-host12-servo': ('chromeos1-poe-switch1', 'fa31')
146               ...}
147
148    @raises: rpm_infrastructure_exception.RPMInfrastructureException
149             when arg mapping_file is None.
150    """
151    if not mapping_file:
152        raise rpm_infrastructure_exception.RPMInfrastructureException(
153                'mapping_file is None.')
154    servo_interface = {}
155    with open(mapping_file) as csvfile:
156        reader = csv.reader(csvfile, delimiter=',')
157        for row in reader:
158            servo_hostname = row[0].strip()
159            switch_hostname = row[1].strip()
160            interface = row[2].strip()
161            if servo_hostname:
162                servo_interface[servo_hostname] = (switch_hostname, interface)
163    return servo_interface
164
165
166def reload_servo_interface_mapping_if_necessary(
167        check_point, mapping_file=MAPPING_FILE):
168    """Reload the servo-interface mapping file if it is modified.
169
170    This method checks if the last-modified time of |mapping_file| is
171    later than |check_point|, if so, it reloads the file.
172
173    @param check_point: A float number representing a time, used to determine
174                        whether we need to reload the mapping file.
175    @param mapping_file: A csv file that stores the mapping, if none,
176                         the setting in rpm_config.ini will be used.
177
178    @return: If the file is reloaded, returns a tuple
179             (last_modified_time, servo_interface) where
180             the first element is the last_modified_time of the
181             mapping file, the second element is a dictionary that
182             maps servo hostname to (switch hostname, interface).
183             If the file is not reloaded, return None.
184
185    @raises: rpm_infrastructure_exception.RPMInfrastructureException
186             when arg mapping_file is None.
187    """
188    if not mapping_file:
189        raise rpm_infrastructure_exception.RPMInfrastructureException(
190                'mapping_file is None.')
191    last_modified = os.path.getmtime(mapping_file)
192    if check_point < last_modified:
193        servo_interface = load_servo_interface_mapping(mapping_file)
194        logging.info('Servo-interface mapping file %s is reloaded.',
195                     mapping_file)
196        return (last_modified, servo_interface)
197    return None
198