[autotest] Add POE support to RPM server

Add POE support to RPM server. This allows us to hard reset
beaglebones utilizing the RPM server.

The core implementation resides in POEController in rpm_controller.py
To find out to which switch a servo is connected to,
we store the mapping information(servo->switch) in a csv-formatted file.
The file name is specified in rpm_config.ini.

rpm_dispatcher is modified to route poe requests to POEController instances.
Once rpm_dispatcher is initialized, it loads the mapping file to a dictionary
and uses it to determine the switch hostname and interface for a servo.

Simple integration tests are added in rpm_controller.py
Unit tests are added in rpm_controller_unittest.py
utils_unittest.py is added to test util functions in utils.py

servo-interface-mapping.csv should be updated if configuration
in switches changes.

BUG=chromium:232614
TEST=ran rpm_controller_unittest.py, rpm_dispacher_unittest.py,
frontend_server_unittest.py, utils_unittest.py, integration tests
in rpm_controller.py, test_client.py, and ran frontend_server.py,
rpm_dispatcher, rpm_client locally.

Change-Id: I294e34ad7efc6d9cc7543e1c57694466086d1791
Reviewed-on: https://gerrit.chromium.org/gerrit/55838
Reviewed-by: Simran Basi <sbasi@chromium.org>
Commit-Queue: Fang Deng <fdeng@chromium.org>
Tested-by: Fang Deng <fdeng@chromium.org>
diff --git a/site_utils/rpm_control_system/frontend_server.py b/site_utils/rpm_control_system/frontend_server.py
index 77f596f..e7af41b 100755
--- a/site_utils/rpm_control_system/frontend_server.py
+++ b/site_utils/rpm_control_system/frontend_server.py
@@ -6,16 +6,18 @@
 import errno
 import heapq
 import logging
+import os
 import re
 import sys
 import socket
 import threading
 import xmlrpclib
 
+import rpm_logging_config
+import utils
 from config import rpm_config
 from MultiThreadedXMLRPCServer import MultiThreadedXMLRPCServer
 from rpm_infrastructure_exception import RPMInfrastructureException
-import rpm_logging_config
 
 DEFAULT_RPM_COUNT = 0
 TERMINATED = -1
@@ -33,6 +35,9 @@
 # RPM Hostname regex.
 RPM_REGEX = re.compile('host[^.]*')
 
+# Servo-interface mapping file
+MAPPING_FILE = rpm_config.get('CiscoPOE', 'servo_interface_mapping_file')
+
 
 class RPMFrontendServer(object):
     """
@@ -49,13 +54,16 @@
     @var _dispatcher_minheap: Min heap that returns a list of format-
                               [ num_rpm's, dispatcher_uri ]
                               Used to choose the least loaded dispatcher.
-    @var _entry_dict: maps dispatcher URI to an entry (list) inside the min
+    @var _entry_dict: Maps dispatcher URI to an entry (list) inside the min
                      heap. If a dispatcher server shuts down this allows us to
                      invalidate the entry in the minheap.
     @var _lock: Used to protect data from multiple running threads all
                 manipulating the same data.
     @var _rpm_dict: Maps rpm hostname's to an already assigned dispatcher
                     server.
+    @var _mapping_last_modified: Last-modified time of the servo-interface
+                                 mapping file.
+    @var _servo_interface: Maps servo hostname to (switch_hostname, interface).
     """
 
 
@@ -69,6 +77,8 @@
         self._entry_dict = {}
         self._lock = threading.Lock()
         self._rpm_dict = {}
+        self._mapping_last_modified = os.path.getmtime(MAPPING_FILE)
+        self._servo_interface = utils.load_servo_interface_mapping()
 
 
     def queue_request(self, dut_hostname, new_state):
@@ -164,7 +174,23 @@
         @return: URI of dispatcher server responsible for this DUT's rpm. None
                  if no dispatcher servers are available.
         """
-        rpm_hostname = RPM_REGEX.sub(DEFAULT_RPM_ID, dut_hostname, count=1)
+        if dut_hostname.endswith('servo'):
+            # Servos are managed by Cisco POE switches.
+            reload_info = utils.reload_servo_interface_mapping_if_necessary(
+                    self._mapping_last_modified)
+            if reload_info:
+                self._mapping_last_modified, self._servo_interface = reload_info
+            switch_if_tuple = self._servo_interface.get(dut_hostname)
+            if not switch_if_tuple:
+                logging.error('Could not determine POE hostname for %s. '
+                              'Please check the servo-interface mapping file.',
+                              dut_hostname)
+                return None
+            else:
+                rpm_hostname = switch_if_tuple[0]
+        else:
+            # Regular DUTs are managed by RPMs.
+            rpm_hostname = RPM_REGEX.sub(DEFAULT_RPM_ID, dut_hostname, count=1)
         with self._lock:
             if self._rpm_dict.get(rpm_hostname):
                 return self._rpm_dict[rpm_hostname]