[autotest] rpm dispatcher/controller get power info from frontend server

This is the 3rd CL for making the rpm infra read rpm/outlet/hydra
info from AFE.

Previously, rpm dispatcher/controler uses regular expression pattern
to determine the hydra hostname. And outlet(ports) must be labeled
with hostname beforehand.

This cl makes rpm dispatcher/controler receive a PowerUnitInfo instance
from frontend server, which includes the rpm(poe)/outlet(port)/hydra
information for the device.

CQ-DEPEND=CL:212346
BUG=chromium:392548
TEST=unittest; Integration tests with other cls in this series, set
up a local rpm server and power cycle devices.

Change-Id: I492531c8dc3f134d32f73d0a0560bf54f8a28b70
Reviewed-on: https://chromium-review.googlesource.com/212357
Tested-by: Fang Deng <fdeng@chromium.org>
Reviewed-by: Simran Basi <sbasi@chromium.org>
Commit-Queue: Fang Deng <fdeng@chromium.org>
diff --git a/site_utils/rpm_control_system/rpm_config.ini b/site_utils/rpm_control_system/rpm_config.ini
index a5aed76..9557a7e 100644
--- a/site_utils/rpm_control_system/rpm_config.ini
+++ b/site_utils/rpm_control_system/rpm_config.ini
@@ -45,15 +45,7 @@
 password: google
 servo_interface_mapping_file: servo_interface_mapping.csv
 
-[hydra1]
-hostname: chromeos-197-hydra1.mtv
-username: autotest
-password: s3rialsw
-admin_username: root
-admin_password: tslinux
-
-[hydra2]
-hostname: chromeos-197-hydra2.mtv
+[HYDRA]
 username: autotest
 password: s3rialsw
 admin_username: root
diff --git a/site_utils/rpm_control_system/rpm_controller.py b/site_utils/rpm_control_system/rpm_controller.py
index e306033..825d874 100644
--- a/site_utils/rpm_control_system/rpm_controller.py
+++ b/site_utils/rpm_control_system/rpm_controller.py
@@ -10,7 +10,6 @@
 import pexpect
 import Queue
 import re
-import subprocess
 import threading
 import time
 
@@ -29,34 +28,9 @@
                                           'call_timeout_mins')
 SET_POWER_STATE_TIMEOUT_SECONDS = rpm_config.getint(
         'RPM_INFRASTRUCTURE', 'set_power_state_timeout_seconds')
-HYDRA_HOST_REGX = 'chromeos2-row\d+-rack(\d+)-rpm1$'
 PROCESS_TIMEOUT_BUFFER = 30
 
 
-def get_hydra_name(hostname):
-    """Return hydra name given rpm hostname.
-
-    @param hostname: the hostname of rpm,
-            e.g. chromeos2-row2-rack1-rpm1, chromeos-rack1-rpm1
-
-    @returns: The corresponding hydra name or None if rpm is not
-              behind a known hydra.
-
-    """
-    # TODO(fdeng): Replace the harded coded mapping with something
-    # dynamic. crbug.com/376538
-    m = re.match(HYDRA_HOST_REGX, hostname)
-    if m:
-        rack = int(m.group(1))
-        # chromeos2-row{..}-rack{1..7} --> Hydra1
-        if rack >= 1 and rack <= 7:
-            return 'hydra1'
-        # chromeos2-row{..}-rack{8..11} --> Hydra2
-        elif rack >= 8 and rack <= 11:
-            return 'hydra2'
-    return None
-
-
 class RPMController(object):
     """
     This abstract class implements RPM request queueing and
@@ -65,7 +39,7 @@
     The actual interaction with the RPM device will be implemented
     by the RPM specific subclasses.
 
-    It assumes that you know the RPM hostname and that the DUT is on
+    It assumes that you know the RPM hostname and that the device is on
     the specified RPM.
 
     This class also allows support for RPM devices that can be accessed
@@ -109,8 +83,8 @@
     DEVICE_PROMPT = '$'
     PASSWORD_PROMPT = 'Password:'
     # The state change command can be any string format but must accept 2 vars:
-    # state followed by DUT/Plug name.
-    STATE_CMD = '%s %s'
+    # state followed by device/Plug name.
+    SET_STATE_CMD = '%s %s'
     SUCCESS_MSG = None # Some RPM's may not return a success msg.
 
     NEW_STATE_ON = 'ON'
@@ -119,7 +93,7 @@
     TYPE = 'Should set TYPE in subclass.'
 
 
-    def __init__(self, rpm_hostname, hydra_name=None):
+    def __init__(self, rpm_hostname, hydra_hostname=None):
         """
         RPMController Constructor.
         To be called by subclasses.
@@ -131,13 +105,10 @@
         self.request_queue = Queue.Queue()
         self._running = False
         self.is_running_lock = threading.Lock()
-        self.behind_hydra = False
         # If a hydra name is provided by the subclass then we know we are
         # talking to an rpm behind a hydra device.
-        if hydra_name:
-            self.behind_hydra = True
-            self.hydra_name = hydra_name
-            self._hydra_hostname = rpm_config.get(hydra_name,'hostname')
+        self.hydra_hostname = hydra_hostname if hydra_hostname else None
+        self.behind_hydra = hydra_hostname is not None
 
 
     def _start_processing_requests(self):
@@ -180,13 +151,14 @@
           threading.Thread(target=rpm_controller.run).start()
 
         Requests are in the format of:
-          [dut_hostname, new_state, condition_var, result]
+          [powerunit_info, new_state, condition_var, result]
         Run will set the result with the correct value.
         """
         while not self.request_queue.empty():
             try:
                 result = multiprocessing.Value(ctypes.c_bool, False)
                 request = self.request_queue.get()
+                device_hostname = request['powerunit_info'].device_hostname
                 if (datetime.datetime.utcnow() > (request['start_time'] +
                         datetime.timedelta(minutes=RPM_CALL_TIMEOUT_MINS))):
                     logging.error('The request was waited for too long to be '
@@ -204,7 +176,7 @@
                              PROCESS_TIMEOUT_BUFFER)
                 if process.is_alive():
                     logging.debug('%s: process (%s) still running, will be '
-                                  'terminated!', request['dut'], process.pid)
+                                  'terminated!', device_hostname, process.pid)
                     process.terminate()
                     is_timeout.value = True
 
@@ -214,10 +186,10 @@
                             'seconds.' % SET_POWER_STATE_TIMEOUT_SECONDS)
                 if not result.value:
                     logging.error('Request to change %s to state %s failed.',
-                                  request['dut'], request['new_state'])
+                                  device_hostname, request['new_state'])
             except Exception as e:
                 logging.error('Request to change %s to state %s failed: '
-                              'Raised exception: %s', request['dut'],
+                              'Raised exception: %s', device_hostname,
                               request['new_state'], e)
                 result.value = False
 
@@ -227,13 +199,13 @@
 
 
     def _process_request(self, request, result, is_timeout):
-        """Process the request to change a DUT's outlet state.
+        """Process the request to change a device's outlet state.
 
         The call of set_power_state is made in a new running process. If it
         takes longer than SET_POWER_STATE_TIMEOUT_SECONDS, the request will be
         timed out.
 
-        @param request: A request to change a DUT's outlet state.
+        @param request: A request to change a device's outlet state.
         @param result: A Value object passed to the new process for the caller
                        thread to retrieve the result.
         @param is_timeout: A Value object passed to the new process for the
@@ -261,7 +233,7 @@
                     log_filename_format=log_filename_format,
                     use_log_server=False)
             logging.info('Failed to set up logging through log server: %s', e)
-        kwargs = {'dut_hostname':request['dut'],
+        kwargs = {'powerunit_info':request['powerunit_info'],
                   'new_state':request['new_state']}
         is_timeout_value, result_value = retry.timeout(
                 self.set_power_state,
@@ -272,20 +244,20 @@
         is_timeout.value = is_timeout_value
 
 
-    def queue_request(self, dut_hostname, new_state):
+    def queue_request(self, powerunit_info, new_state):
         """
-        Queues up a requested state change for a DUT's outlet.
+        Queues up a requested state change for a device's outlet.
 
         Requests are in the format of:
-          [dut_hostname, new_state, condition_var, result]
+          [powerunit_info, new_state, condition_var, result]
         Run will set the result with the correct value.
 
-        @param dut_hostname: hostname of DUT whose outlet we want to change.
+        @param powerunit_info: And PowerUnitInfo instance.
         @param new_state: ON/OFF/CYCLE - state or action we want to perform on
                           the outlet.
         """
         request = {}
-        request['dut'] = dut_hostname
+        request['powerunit_info'] = powerunit_info
         request['new_state'] = new_state
         request['start_time'] = datetime.datetime.utcnow()
         # Reserve a spot for the result to be stored.
@@ -310,7 +282,7 @@
         if not ssh:
             return
         ssh.expect(RPMController.PASSWORD_PROMPT, timeout=60)
-        ssh.sendline(rpm_config.get(self.hydra_name, 'admin_password'))
+        ssh.sendline(rpm_config.get('HYDRA', 'admin_password'))
         ssh.expect(RPMController.HYDRA_PROMPT)
         ssh.sendline(RPMController.CLI_CMD)
         cli_prompt_re = re.compile(RPMController.CLI_PROMPT)
@@ -362,7 +334,7 @@
                 return False
         if response == 0:
             try:
-                ssh.sendline(rpm_config.get(self.hydra_name,'password'))
+                ssh.sendline(rpm_config.get('HYDRA','password'))
                 ssh.sendline('')
                 response = ssh.expect_list(
                         [re.compile(RPMController.USERNAME_PROMPT),
@@ -409,11 +381,11 @@
                  would be if another user is logged into the device.
         """
         if admin_override:
-            username = rpm_config.get(self.hydra_name, 'admin_username')
+            username = rpm_config.get('HYDRA', 'admin_username')
         else:
-            username = '%s:%s' % (rpm_config.get(self.hydra_name,'username'),
+            username = '%s:%s' % (rpm_config.get('HYDRA','username'),
                                   self.hostname)
-        cmd = RPMController.SSH_LOGIN_CMD % (username, self._hydra_hostname)
+        cmd = RPMController.SSH_LOGIN_CMD % (username, self.hydra_hostname)
         num_attempts = 0
         while num_attempts < RPMController.HYDRA_MAX_CONNECT_RETRIES:
             try:
@@ -490,7 +462,7 @@
             time.sleep(5)
 
 
-    def set_power_state(self, dut_hostname, new_state):
+    def set_power_state(self, powerunit_info, new_state):
         """
         Set the state of the dut's outlet on this RPM.
 
@@ -502,7 +474,7 @@
         proper connection and state change code. And the subclass will handle
         accessing the RPM devices.
 
-        @param dut_hostname: hostname of DUT whose outlet we want to change.
+        @param powerunit_info: An instance of PowerUnitInfo.
         @param new_state: ON/OFF/CYCLE - state or action we want to perform on
                           the outlet.
 
@@ -513,16 +485,16 @@
         if not ssh:
             return False
         if new_state == self.NEW_STATE_CYCLE:
-            logging.debug('Beginning Power Cycle for DUT: %s',
-                          dut_hostname)
-            result = self._change_state(dut_hostname, self.NEW_STATE_OFF, ssh)
+            logging.debug('Beginning Power Cycle for device: %s',
+                          powerunit_info.device_hostname)
+            result = self._change_state(powerunit_info, self.NEW_STATE_OFF, ssh)
             if not result:
                 return result
             time.sleep(RPMController.CYCLE_SLEEP_TIME)
-            result = self._change_state(dut_hostname, self.NEW_STATE_ON, ssh)
+            result = self._change_state(powerunit_info, self.NEW_STATE_ON, ssh)
         else:
-            # Try to change the state of the DUT's power outlet.
-            result = self._change_state(dut_hostname, new_state, ssh)
+            # Try to change the state of the device's power outlet.
+            result = self._change_state(powerunit_info, new_state, ssh)
 
         # Terminate hydra connection if necessary.
         self._logout(ssh)
@@ -530,14 +502,14 @@
         return result
 
 
-    def _change_state(self, dut_hostname, new_state, ssh):
+    def _change_state(self, powerunit_info, new_state, ssh):
         """
         Perform the actual state change operation.
 
         Once we have established communication with the RPM this method is
         responsible for changing the state of the RPM outlet.
 
-        @param dut_hostname: hostname of DUT whose outlet we want to change.
+        @param powerunit_info: An instance of PowerUnitInfo.
         @param new_state: ON/OFF - state or action we want to perform on
                           the outlet.
         @param ssh: The ssh connection used to execute the state change commands
@@ -546,17 +518,25 @@
         @return: True if the attempt to change power state was successful,
                  False otherwise.
         """
-        ssh.sendline(self.SET_STATE_CMD % (new_state, dut_hostname))
+        outlet = powerunit_info.outlet
+        device_hostname = powerunit_info.device_hostname
+        if not outlet:
+            logging.error('Request to change outlet for device: %s to new '
+                          'state %s failed: outlet is unknown, please '
+                          'make sure POWERUNIT_OUTLET exist in the host\'s '
+                          'attributes in afe.', device_hostname, new_state)
+        ssh.sendline(self.SET_STATE_CMD % (new_state, outlet))
         if self.SUCCESS_MSG:
             # If this RPM device returns a success message check for it before
             # continuing.
             try:
                 ssh.expect(self.SUCCESS_MSG, timeout=60)
             except pexpect.ExceptionPexpect:
-                logging.error('Request to change outlet for DUT: %s to new '
-                              'state %s failed.', dut_hostname, new_state)
+                logging.error('Request to change outlet for device: %s to new '
+                              'state %s failed.', device_hostname, new_state)
                 return False
-        logging.debug('Outlet for DUT: %s set to %s', dut_hostname, new_state)
+        logging.debug('Outlet for device: %s set to %s', device_hostname,
+                      new_state)
         return True
 
 
@@ -570,37 +550,6 @@
         return self.TYPE
 
 
-    def get_next_rpm_hostname(self):
-        """Return the hostname of the next RPM in the same location if it
-        exists.
-
-        For example chromeos3-rack2-row3 may have rpm1 and rpm2.
-
-        @returns Hostname of the next rpm or None.
-        """
-        if self.behind_hydra:
-            # For now lets not do the hydra-case. It would require us to log
-            # into the admin console, and see if an entry exists for the next
-            # RPM hostname. This would impact the run time of any failure that
-            # occurs with an RPM behind the hydra, as well as constantly
-            # disconnecting the lab admins from the admin console.
-            return None
-        # Determine the RPM location and number.
-        hostname_regex = '(?P<LOCATION>.*)-rpm(?P<RPM_NUM>[\d]+)(.)*'
-        match = re.match(hostname_regex, self.hostname)
-        location = match.group('LOCATION')
-        rpm_number = int(match.group('RPM_NUM'))
-        next_rpm = '%s-rpm%d' % (location, int(rpm_number) + 1)
-        try:
-            # Ping it to see if it exists.
-            subprocess.check_call(
-                    ['ping', '%s.%s' % (next_rpm, self._dns_zone), '-w 3'],
-                    stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-            return next_rpm
-        except subprocess.CalledProcessError:
-            return None
-
-
 class SentryRPMController(RPMController):
     """
     This class implements power control for Sentry Switched CDU
@@ -618,41 +567,16 @@
     DEVICE_PROMPT = 'Switched CDU:'
     SET_STATE_CMD = '%s %s'
     SUCCESS_MSG = 'Command successful'
-    SET_OUTLET_NAME_CMD = 'set outlet name .A%d %s'
     NUM_OF_OUTLETS = 17
     TYPE = 'Sentry'
 
 
-    def __init__(self, hostname, hydra_name=None):
-        hydra_name = hydra_name or get_hydra_name(hostname)
-        super(SentryRPMController, self).__init__(hostname, hydra_name)
+    def __init__(self, hostname, hydra_hostname=None):
+        super(SentryRPMController, self).__init__(hostname, hydra_hostname)
         self._username = rpm_config.get('SENTRY', 'username')
         self._password = rpm_config.get('SENTRY', 'password')
 
 
-    def set_power_state(self, dut_hostname, new_state):
-        """
-        Set the state of the dut's outlet on this RPM.
-
-        Overload set_power_state in RPMController.
-        @param dut_hostname: hostname of DUT whose outlet we want to change.
-        @param new_state: ON/OFF/CYCLE - state or action we want to perform on
-                          the outlet.
-
-        @return: True if the attempt to change power state was successful,
-                 False otherwise.
-        """
-        if 'row' in dut_hostname:
-            # Because the devices with a row and a rack all have long
-            # hostnames, we can't store their full names in the rpm, therefore
-            # for these devices we drop the 'chromeosX' part of their name.
-            # For example: chromeos2-rack2-row1-host1 is just stored as
-            # rack2-row1-host1 inside the RPM.
-            dut_hostname = dut_hostname.split('-', 1)[1]
-        return super(SentryRPMController, self).set_power_state(
-                dut_hostname, new_state)
-
-
     def _setup_test_user(self, ssh):
         """Configure the test user for the RPM
 
@@ -695,6 +619,12 @@
         Configure the RPM by adding the test user and setting up the outlet
         names.
 
+        Note the rpm infrastructure does not rely on the outlet name to map a
+        device to its outlet any more. We keep this method in case there is
+        a need to label outlets for other reasons. We may deprecate
+        this method if it has been proved the outlet names will not be used
+        in any scenario.
+
         @param outlet_naming_map: Dictionary used to map the outlet numbers to
                                   host names. Keys must be ints. And names are
                                   in the format of 'hostX'.
@@ -709,12 +639,12 @@
             self._setup_test_user(ssh)
             # Set up the outlet names.
             # Hosts have the same name format as the RPM hostname except they
-            # end in hostX instead of rpm1.
-            dut_name_format = self.hostname.replace('-rpm1', '')
+            # end in hostX instead of rpmX.
+            dut_name_format = re.sub('-rpm[0-9]*', '', self.hostname)
             if self.behind_hydra:
-                # Remove "chromeos2" from DUTs behind the hydra due to a length
+                # Remove "chromeosX" from DUTs behind the hydra due to a length
                 # constraint on the names we can store inside the RPM.
-                dut_name_format = dut_name_format.replace('chromeos2-', '')
+                dut_name_format = re.sub('chromeos[0-9]*-', '', dut_name_format)
             dut_name_format = dut_name_format + '-%s'
             self._clear_outlet_names(ssh)
             for outlet, name in outlet_naming_map.items():
@@ -759,59 +689,62 @@
             self._rpm = powerswitch
 
 
-    def _get_outlet_value_and_state(self, dut_hostname):
+    def _get_outlet_state(self, outlet):
         """
-        Look up the outlet and state for a given hostname on the RPM.
+        Look up the state for a given outlet on the RPM.
 
-        @param dut_hostname: hostname of DUT whose outlet we want to lookup.
+        @param outlet: the outlet to look up.
 
-        @return [outlet, state]: the outlet number as well as its current state.
+        @return state: the outlet's current state.
         """
         status_list = self._rpm.statuslist()
-        for outlet, hostname, state in status_list:
-            if hostname == dut_hostname:
-                return outlet, state
+        for outlet_name, hostname, state in status_list:
+            if outlet_name == outlet:
+                return state
         return None
 
 
-    def set_power_state(self, dut_hostname, new_state):
+    def set_power_state(self, powerunit_info, new_state):
         """
         Since this does not utilize SSH in any manner, this will overload the
         set_power_state in RPMController and completes all steps of changing
-        the DUT's outlet state.
+        the device's outlet state.
         """
-        outlet_and_state = self._get_outlet_value_and_state(dut_hostname)
-        if not outlet_and_state:
-            logging.error('DUT %s is not on rpm %s',
-                          dut_hostname, self.hostname)
+        device_hostname = powerunit_info.device_hostname
+        outlet = powerunit_info.outlet
+        if not outlet:
+            logging.error('Request to change outlet for device %s to '
+                          'new state %s failed: outlet is unknown. Make sure '
+                          'POWERUNIT_OUTLET exists in the host\'s '
+                          'attributes in afe' , device_hostname, new_state)
             return False
-        outlet, state = outlet_and_state
+        state = self._get_outlet_state(outlet)
         expected_state = new_state
         if new_state == self.NEW_STATE_CYCLE:
-            logging.debug('Beginning Power Cycle for DUT: %s',
-                          dut_hostname)
+            logging.debug('Beginning Power Cycle for device: %s',
+                          device_hostname)
             self._rpm.off(outlet)
-            logging.debug('Outlet for DUT: %s set to OFF', dut_hostname)
+            logging.debug('Outlet for device: %s set to OFF', device_hostname)
             # Pause for 5 seconds before restoring power.
             time.sleep(RPMController.CYCLE_SLEEP_TIME)
             self._rpm.on(outlet)
-            logging.debug('Outlet for DUT: %s set to ON', dut_hostname)
+            logging.debug('Outlet for device: %s set to ON', device_hostname)
             expected_state = self.NEW_STATE_ON
         if new_state == self.NEW_STATE_OFF:
             self._rpm.off(outlet)
-            logging.debug('Outlet for DUT: %s set to OFF', dut_hostname)
+            logging.debug('Outlet for device: %s set to OFF', device_hostname)
         if new_state == self.NEW_STATE_ON:
             self._rpm.on(outlet)
-            logging.debug('Outlet for DUT: %s set to ON', dut_hostname)
+            logging.debug('Outlet for device: %s set to ON', device_hostname)
         # Lookup the final state of the outlet
-        return self._is_plug_state(dut_hostname, expected_state)
+        return self._is_plug_state(powerunit_info, expected_state)
 
 
-    def _is_plug_state(self, dut_hostname, expected_state):
-        outlet, state = self._get_outlet_value_and_state(dut_hostname)
+    def _is_plug_state(self, powerunit_info, expected_state):
+        state = self._get_outlet_state(powerunit_info.outlet)
         if expected_state not in state:
-            logging.error('Outlet for DUT: %s did not change to new state'
-                          ' %s', dut_hostname, expected_state)
+            logging.error('Outlet for device: %s did not change to new state'
+                          ' %s', powerunit_info.device_hostname, expected_state)
             return False
         return True
 
@@ -848,13 +781,11 @@
     TYPE = 'CiscoPOE'
 
 
-    def __init__(self, hostname, servo_interface):
+    def __init__(self, hostname):
         """
         Initialize controller class for a Cisco POE switch.
 
         @param hostname: the Cisco POE switch host name.
-        @param servo_interface: a dictionary that maps servo hostname
-                                to (switch_hostname, interface).
         """
         super(CiscoPOEController, self).__init__(hostname)
         self._username = rpm_config.get('CiscoPOE', 'username')
@@ -865,7 +796,6 @@
         self.poe_prompt = self.POE_PROMPT % short_hostname
         self.config_prompt = self.CONFIG_PROMPT % short_hostname
         self.config_if_prompt = self.CONFIG_IF_PROMPT % short_hostname
-        self._servo_interface = servo_interface
 
 
     def _login(self):
@@ -999,13 +929,13 @@
         ssh.sendline(self.EXIT_CMD)
 
 
-    def _change_state(self, dut_hostname, new_state, ssh):
+    def _change_state(self, powerunit_info, new_state, ssh):
         """
         Perform the actual state change operation.
 
         Overload _change_state in RPMController.
 
-        @param dut_hostname: hostname of servo, whose outlet we want to change.
+        @param powerunit_info: An PowerUnitInfo instance.
         @param new_state: ON/OFF - state or action we want to perform on
                           the outlet.
         @param ssh: The ssh connection used to execute the state change commands
@@ -1014,14 +944,13 @@
         @return: True if the attempt to change power state was successful,
                  False otherwise.
         """
-        switch_if_tuple = self._servo_interface.get(dut_hostname)
-        if not switch_if_tuple:
-            logging.error('Could not find the interface for %s on switch %s. '
-                          'Maybe the mapping file is out of date?',
-                          dut_hostname, self.hostname)
+        interface = powerunit_info.outlet
+        device_hostname = powerunit_info.device_hostname
+        if not interface:
+            logging.error('Could not change state: the interface on %s for %s '
+                          'was not given.', self.hostname, device_hostname)
             return False
-        else:
-            interface = switch_if_tuple[1]
+
         # Enter configuration terminal.
         if not self._enter_configuration_terminal(interface, ssh):
             logging.error('Could not enter configuration terminal for %s',
@@ -1037,28 +966,20 @@
             return False
         # Exit configuraiton terminal.
         if not self._exit_configuration_terminal(ssh):
-            logging.error('Skipping verifying outlet state for DUT: %s, '
+            logging.error('Skipping verifying outlet state for device: %s, '
                           'because could not exit configuration terminal.',
-                          dut_hostname)
+                          device_hostname)
             return False
         # Verify if the state has changed successfully.
         if not self._verify_state(interface, new_state, ssh):
             logging.error('Could not verify state on interface %s', interface)
             return False
 
-        logging.debug('Outlet for DUT: %s set to %s',
-                      dut_hostname, new_state)
+        logging.debug('Outlet for device: %s set to %s',
+                      device_hostname, new_state)
         return True
 
 
-    def get_next_rpm_hostname(self):
-        """Override from RPMController. Not applicable to POE Controller.
-
-        @returns None.
-        """
-        return None
-
-
 def test_in_order_requests():
     """Simple integration testing."""
     rpm = WebPoweredRPMController('chromeos-rack8e-rpm1')
diff --git a/site_utils/rpm_control_system/rpm_controller_unittest.py b/site_utils/rpm_control_system/rpm_controller_unittest.py
index f88f1c1..dd6bb2f 100755
--- a/site_utils/rpm_control_system/rpm_controller_unittest.py
+++ b/site_utils/rpm_control_system/rpm_controller_unittest.py
@@ -11,6 +11,9 @@
 
 import rpm_controller
 
+import common
+from autotest_lib.site_utils.rpm_control_system import utils
+
 
 class TestRPMControllerQueue(mox.MoxTestBase):
     """Test request can be queued and processed in controller.
@@ -19,11 +22,16 @@
     def setUp(self):
         super(TestRPMControllerQueue, self).setUp()
         self.rpm = rpm_controller.SentryRPMController('chromeos-rack1-host8')
+        self.powerunit_info = utils.PowerUnitInfo(
+                device_hostname='chromos-rack1-host8',
+                powerunit_hostname='chromeos-rack1-rpm1',
+                powerunit_type=utils.PowerUnitInfo.POWERUNIT_TYPES.RPM,
+                outlet='.A100',
+                hydra_hostname=None)
 
 
     def testQueueRequest(self):
         """Should create a new process to handle request."""
-        dut_hostname = 'chromos-rack1-host8'
         new_state = 'ON'
         process = self.mox.CreateMockAnything()
         rpm_controller.multiprocessing.Process = self.mox.CreateMockAnything()
@@ -32,7 +40,7 @@
         process.start()
         process.join()
         self.mox.ReplayAll()
-        self.assertFalse(self.rpm.queue_request(dut_hostname, new_state))
+        self.assertFalse(self.rpm.queue_request(self.powerunit_info, new_state))
         self.mox.VerifyAll()
 
 
@@ -46,23 +54,29 @@
         rpm_controller.pexpect.spawn = self.mox.CreateMockAnything()
         rpm_controller.pexpect.spawn(mox.IgnoreArg()).AndReturn(self.ssh)
         self.rpm = rpm_controller.SentryRPMController('chromeos-rack1-host8')
+        self.powerunit_info = utils.PowerUnitInfo(
+                device_hostname='chromos-rack1-host8',
+                powerunit_hostname='chromeos-rack1-rpm1',
+                powerunit_type=utils.PowerUnitInfo.POWERUNIT_TYPES.RPM,
+                outlet='.A100',
+                hydra_hostname=None)
 
 
     def testSuccessfullyChangeOutlet(self):
         """Should return True if change was successful."""
         prompt = 'Switched CDU:'
         password = 'admn'
-        dut_hostname = 'chromos-rack1-host8'
         new_state = 'ON'
         self.ssh.expect('Password:', timeout=60)
         self.ssh.sendline(password)
         self.ssh.expect(prompt, timeout=60)
-        self.ssh.sendline('%s %s' % (new_state, dut_hostname))
+        self.ssh.sendline('%s %s' % (new_state, self.powerunit_info.outlet))
         self.ssh.expect('Command successful', timeout=60)
         self.ssh.sendline('logout')
         self.ssh.close(force=True)
         self.mox.ReplayAll()
-        self.assertTrue(self.rpm.set_power_state(dut_hostname, new_state))
+        self.assertTrue(self.rpm.set_power_state(
+                self.powerunit_info, new_state))
         self.mox.VerifyAll()
 
 
@@ -70,18 +84,17 @@
         """Should return False if change was unsuccessful."""
         prompt = 'Switched CDU:'
         password = 'admn'
-        dut_hostname = 'chromos-rack1-host8'
         new_state = 'ON'
         self.ssh.expect('Password:', timeout=60)
         self.ssh.sendline(password)
         self.ssh.expect(prompt, timeout=60)
-        self.ssh.sendline('%s %s' % (new_state, dut_hostname))
+        self.ssh.sendline('%s %s' % (new_state, self.powerunit_info.outlet))
         self.ssh.expect('Command successful',
                         timeout=60).AndRaise(pexpect.TIMEOUT('Timed Out'))
         self.ssh.sendline('logout')
         self.ssh.close(force=True)
         self.mox.ReplayAll()
-        self.assertFalse(self.rpm.set_power_state(dut_hostname, new_state))
+        self.assertFalse(self.rpm.set_power_state(self.powerunit_info, new_state))
         self.mox.VerifyAll()
 
 
@@ -100,6 +113,12 @@
         # Outlet statuses are in the format "u'ON'"
         initial_state = 'u\'ON\''
         self.test_status_list_initial = [[outlet, dut, initial_state]]
+        self.powerunit_info = utils.PowerUnitInfo(
+                device_hostname=dut,
+                powerunit_hostname=hostname,
+                powerunit_type=utils.PowerUnitInfo.POWERUNIT_TYPES.RPM,
+                outlet=outlet,
+                hydra_hostname=None)
 
 
     def testSuccessfullyChangeOutlet(self):
@@ -109,8 +128,8 @@
         self.dli_ps.off(8)
         self.dli_ps.statuslist().AndReturn(test_status_list_final)
         self.mox.ReplayAll()
-        self.assertTrue(self.web_rpm.set_power_state('chromeos-rack8a-host8',
-                                                   'OFF'))
+        self.assertTrue(self.web_rpm.set_power_state(
+                self.powerunit_info, 'OFF'))
         self.mox.VerifyAll()
 
 
@@ -121,18 +140,16 @@
         self.dli_ps.off(8)
         self.dli_ps.statuslist().AndReturn(test_status_list_final)
         self.mox.ReplayAll()
-        self.assertFalse(self.web_rpm.set_power_state('chromeos-rack8a-host8',
-                                                    'OFF'))
+        self.assertFalse(self.web_rpm.set_power_state(
+                self.powerunit_info, 'OFF'))
         self.mox.VerifyAll()
 
 
-    def testDutNotOnRPM(self):
+    def testNoOutlet(self):
         """Should return False if DUT hostname is not on the RPM device."""
-        self.dli_ps.statuslist().AndReturn(self.test_status_list_initial)
-        self.mox.ReplayAll()
-        self.assertFalse(self.web_rpm.set_power_state('chromeos-rack8a-host1',
-                                                    'OFF'))
-        self.mox.VerifyAll()
+        self.powerunit_info.outlet=None
+        self.assertFalse(self.web_rpm.set_power_state(
+                self.powerunit_info, 'OFF'))
 
 
 class TestCiscoPOEController(mox.MoxTestBase):
@@ -155,6 +172,12 @@
     SERVO = 'chromeos1-rack3-host12-servo'
     SWITCH = 'chromeos2-poe-switch8'
     PORT = 'fa32'
+    POWERUNIT_INFO = utils.PowerUnitInfo(
+            device_hostname=PORT,
+            powerunit_hostname=SERVO,
+            powerunit_type=utils.PowerUnitInfo.POWERUNIT_TYPES.POE,
+            outlet=PORT,
+            hydra_hostname=None)
 
 
     def setUp(self):
@@ -162,9 +185,7 @@
         self.mox.StubOutWithMock(pexpect.spawn, '_spawn')
         self.mox.StubOutWithMock(pexpect.spawn, 'read_nonblocking')
         self.mox.StubOutWithMock(pexpect.spawn, 'sendline')
-        servo_interface = {self.SERVO:(self.SWITCH, self.PORT)}
-        self.poe = rpm_controller.CiscoPOEController(
-                self.SWITCH, servo_interface)
+        self.poe = rpm_controller.CiscoPOEController(self.SWITCH)
         pexpect.spawn._spawn(mox.IgnoreArg(), mox.IgnoreArg())
         pexpect.spawn.read_nonblocking(
                 mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(self.STREAM_WELCOME)
@@ -222,7 +243,7 @@
                 mox.IgnoreArg(), mox.IgnoreArg()).AndReturn(self.STREAM_DEVICE)
         pexpect.spawn.sendline('exit')
         self.mox.ReplayAll()
-        self.assertTrue(self.poe.set_power_state(self.SERVO, 'ON'))
+        self.assertTrue(self.poe.set_power_state(self.POWERUNIT_INFO, 'ON'))
         self.mox.VerifyAll()
 
 
@@ -231,7 +252,7 @@
         self._EnterConfigurationHelper(success=False)
         pexpect.spawn.sendline('exit')
         self.mox.ReplayAll()
-        self.assertFalse(self.poe.set_power_state(self.SERVO, 'ON'))
+        self.assertFalse(self.poe.set_power_state(self.POWERUNIT_INFO, 'ON'))
         self.mox.VerifyAll()
 
 
@@ -251,7 +272,7 @@
         pexpect.spawn.__str__().AndReturn('A pexpect.spawn object.')
         pexpect.spawn.sendline('exit')
         self.mox.ReplayAll()
-        self.assertFalse(self.poe.set_power_state(self.SERVO, 'ON'))
+        self.assertFalse(self.poe.set_power_state(self.POWERUNIT_INFO, 'ON'))
         self.mox.VerifyAll()
 
 
@@ -274,7 +295,7 @@
         pexpect.spawn.__str__().AndReturn('A pexpect.spawn object.')
         pexpect.spawn.sendline('exit')
         self.mox.ReplayAll()
-        self.assertFalse(self.poe.set_power_state(self.SERVO, 'ON'))
+        self.assertFalse(self.poe.set_power_state(self.POWERUNIT_INFO, 'ON'))
         self.mox.VerifyAll()
 
 
diff --git a/site_utils/rpm_control_system/rpm_dispatcher.py b/site_utils/rpm_control_system/rpm_dispatcher.py
index f050a3f..25fca6d 100755
--- a/site_utils/rpm_control_system/rpm_dispatcher.py
+++ b/site_utils/rpm_control_system/rpm_dispatcher.py
@@ -6,7 +6,6 @@
 import atexit
 import errno
 import logging
-import os
 import re
 import sys
 import socket
@@ -25,11 +24,6 @@
 
 LOG_FILENAME_FORMAT = rpm_config.get('GENERAL','dispatcher_logname_format')
 
-# Servo-interface mapping file
-MAPPING_FILE = os.path.join(
-        os.path.dirname(__file__),
-        rpm_config.get('CiscoPOE', 'servo_interface_mapping_file'))
-
 
 class RPMDispatcher(object):
     """
@@ -72,8 +66,6 @@
         self._worker_dict = {}
         self._frontend_server = rpm_config.get('RPM_INFRASTRUCTURE',
                                                'frontend_uri')
-        self._mapping_last_modified = os.path.getmtime(MAPPING_FILE)
-        self._servo_interface = utils.load_servo_interface_mapping()
         logging.info('Registering this rpm dispatcher with the frontend '
                      'server at %s.', self._frontend_server)
         client = xmlrpclib.ServerProxy(self._frontend_server)
@@ -121,67 +113,31 @@
         return True
 
 
-    def queue_request(self, dut_hostname, new_state):
+    def queue_request(self, powerunit_info_dict, new_state):
         """
-        Looks up the appropriate RPMController instance for this DUT and queues
+        Looks up the appropriate RPMController instance for the device and queues
         up the request.
 
-        @param dut_hostname: hostname of the DUT whose outlet we are trying to
-                             change.
+        @param powerunit_info_dict: A dictionary, containing the attribute/values
+                                    of an unmarshalled PowerUnitInfo instance.
         @param new_state: [ON, OFF, CYCLE] state we want to the change the
                           outlet to.
         @return: True if the attempt to change power state was successful,
                  False otherwise.
         """
-        logging.info('Received request to set DUT: %s to state: %s',
-                     dut_hostname, new_state)
-        rpm_hostname = self._get_rpm_hostname_for_dut(dut_hostname)
+        powerunit_info = utils.PowerUnitInfo(**powerunit_info_dict)
+        logging.info('Received request to set device: %s to state: %s',
+                     powerunit_info.device_hostname, new_state)
         result = False
-        while not result and rpm_hostname:
-            rpm_controller = self._get_rpm_controller(rpm_hostname)
-            result = rpm_controller.queue_request(dut_hostname, new_state)
-            if not result:
-                # If the request failed, check to see if there is another RPM
-                # at this location.
-                rpm_hostname = rpm_controller.get_next_rpm_hostname()
+        while not result:
+            rpm_controller = self._get_rpm_controller(
+                    powerunit_info.powerunit_hostname,
+                    powerunit_info.hydra_hostname)
+            result = rpm_controller.queue_request(powerunit_info, new_state)
         return result
 
 
-    def _get_rpm_hostname_for_dut(self, dut_hostname):
-        """
-        Private method that retreives the appropriate RPMController instance
-        for this DUT.
-
-        @param dut_hostname: hostname of the DUT whose RPMController we want.
-
-        @return: RPM Hostname responsible for this DUT.
-                 Return None on failure.
-        """
-        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]
-            logging.info('POE hostname for DUT %s is %s', dut_hostname,
-                         rpm_hostname)
-        else:
-            # Regular DUTs are managed by RPMs.
-            rpm_hostname = re.sub('host[^.]*', 'rpm1', dut_hostname, count=1)
-            logging.info('RPM hostname for DUT %s is %s',  dut_hostname,
-                         rpm_hostname)
-        return rpm_hostname
-
-
-    def _get_rpm_controller(self, rpm_hostname):
+    def _get_rpm_controller(self, rpm_hostname, hydra_hostname=None):
         """
         Private method that retreives the appropriate RPMController instance
         for this RPM Hostname or calls _create_rpm_controller it if it does not
@@ -195,12 +151,13 @@
             return None
         rpm_controller = self._worker_dict_get(rpm_hostname)
         if not rpm_controller:
-            rpm_controller = self._create_rpm_controller(rpm_hostname)
+            rpm_controller = self._create_rpm_controller(
+                    rpm_hostname, hydra_hostname)
             self._worker_dict_put(rpm_hostname, rpm_controller)
         return rpm_controller
 
 
-    def _create_rpm_controller(self, rpm_hostname):
+    def _create_rpm_controller(self, rpm_hostname, hydra_hostname):
         """
         Determines the type of RPMController required and initializes it.
 
@@ -212,8 +169,7 @@
         if hostname_elements[-2] == 'poe':
             # POE switch hostname looks like 'chromeos2-poe-switch1'.
             logging.info('The controller is a Cisco POE switch.')
-            return rpm_controller.CiscoPOEController(
-                    rpm_hostname, self._servo_interface)
+            return rpm_controller.CiscoPOEController(rpm_hostname)
         else:
             # The device is an RPM.
             rack_id = hostname_elements[-2]
@@ -223,7 +179,9 @@
                 return rpm_controller.WebPoweredRPMController(rpm_hostname)
             else:
                 logging.info('RPM is a Sentry CDU device.')
-                return rpm_controller.SentryRPMController(rpm_hostname)
+                return rpm_controller.SentryRPMController(
+                        hostname=rpm_hostname,
+                        hydra_hostname=hydra_hostname)
 
 
     def _get_serveruri(self):
diff --git a/site_utils/rpm_control_system/rpm_dispatcher_unittest.py b/site_utils/rpm_control_system/rpm_dispatcher_unittest.py
index aaa6298..b7e0162 100755
--- a/site_utils/rpm_control_system/rpm_dispatcher_unittest.py
+++ b/site_utils/rpm_control_system/rpm_dispatcher_unittest.py
@@ -11,6 +11,7 @@
 
 DUT_SAME_RPM1 = 'chromeos-rack8e-hostbs1'
 DUT_SAME_RPM2 = 'chromeos-rack8e-hostbs2'
+RPM_HOSTNAME = 'chromeos-rack8e-rpm1'
 DUT_DIFFERENT_RPM = 'chromeos-rack1-hostbs1'
 FAKE_DISPATCHER_URI = 'fake-dispatcher'
 FAKE_DISPATCHER_PORT = 9999
@@ -60,10 +61,8 @@
         belong to the same RPM device create and retrieve the same RPMController
         instance.
         """
-        controller1 = self.dispatcher._get_rpm_controller(
-                self.dispatcher._get_rpm_hostname_for_dut(DUT_SAME_RPM1))
-        controller2 = self.dispatcher._get_rpm_controller(
-                self.dispatcher._get_rpm_hostname_for_dut(DUT_SAME_RPM2))
+        controller1 = self.dispatcher._get_rpm_controller(RPM_HOSTNAME)
+        controller2 = self.dispatcher._get_rpm_controller(RPM_HOSTNAME)
         self.assertEquals(controller1, controller2)