diff --git a/tests/common/plugins/pdu_controller/controller_base.py b/tests/common/plugins/pdu_controller/controller_base.py index d26b669dd1..3880d0f9f8 100644 --- a/tests/common/plugins/pdu_controller/controller_base.py +++ b/tests/common/plugins/pdu_controller/controller_base.py @@ -40,14 +40,15 @@ def turn_off_outlet(self, outlet): """ raise NotImplementedError - def get_outlet_status(self, outlet=None): + def get_outlet_status(self, outlet=None, hostname=None): """ @summary: Get current power status of PDU outlets @param outlet: Optional outlet ID, it could be integer or string digit. If no outlet is specified, power status of all PDU outlets should be returned + @param hostname: Optional hostname used to partial match any label @return: Returns a list of dictionaries. For example: - [{"outlet_id": 0, "outlet_on": True}, {"outlet_id": 1, "outlet_on": True}] + [{"outlet_id": "0.0.1", "outlet_on": True}, {"outlet_id": "0.0.2", "outlet_on": True}] If getting outlet(s) status failed, an empty list should be returned. """ raise NotImplementedError diff --git a/tests/common/plugins/pdu_controller/pdu_manager.py b/tests/common/plugins/pdu_controller/pdu_manager.py index 5ecf706aa3..c919dd3aa8 100644 --- a/tests/common/plugins/pdu_controller/pdu_manager.py +++ b/tests/common/plugins/pdu_controller/pdu_manager.py @@ -97,11 +97,11 @@ def add_controller(self, psu_name, psu_peer, pdu_vars): next_index = len(self.controllers) self.controllers.append(pdu) if not shared_pdu: - controller = get_pdu_controller(pdu_ip, self.dut_hostname, pdu_vars) + controller = get_pdu_controller(pdu_ip, pdu_vars) if not controller: logger.warning('Failed creating pdu controller: {}'.format(psu_peer)) return - outlets = controller.get_outlet_status() + outlets = controller.get_outlet_status(hostname=self.dut_hostname) self._update_outlets(outlets, next_index) pdu['outlets'] = outlets pdu['controller'] = controller @@ -158,14 +158,14 @@ def get_outlet_status(self, outlet=None): if outlet is not None: pdu_index = outlet['pdu_index'] controller = self._get_pdu_controller(pdu_index) - outlets = controller.get_outlet_status(outlet['outlet_id']) + outlets = controller.get_outlet_status(outlet=outlet['outlet_id']) self._update_outlets(outlets, pdu_index) status = status + outlets else: # collect all status for pdu_index, controller in enumerate(self.controllers): if len(controller['outlets']) > 0: - outlets = controller['controller'].get_outlet_status() + outlets = controller['controller'].get_outlet_status(hostname=self.dut_hostname) self._update_outlets(outlets, pdu_index) status = status + outlets diff --git a/tests/common/plugins/pdu_controller/snmp_pdu_controllers.py b/tests/common/plugins/pdu_controller/snmp_pdu_controllers.py index 63f9132b07..14ea5593d7 100644 --- a/tests/common/plugins/pdu_controller/snmp_pdu_controllers.py +++ b/tests/common/plugins/pdu_controller/snmp_pdu_controllers.py @@ -10,6 +10,8 @@ from pysnmp.proto import rfc1902 from pysnmp.entity.rfc3413.oneliner import cmdgen +logger = logging.getLogger(__name__) + class snmpPduController(PduControllerBase): """ PDU Controller class for SNMP conrolled PDUs - 'Sentry Switched CDU' and 'APC Web/SNMP Management Card' @@ -32,7 +34,7 @@ def get_pdu_controller_type(self): errorIndication, errorStatus, errorIndex, varBinds = cmdGen.getCmd( snmp_auth, cmdgen.UdpTransportTarget((self.controller, 161), timeout=5.0), - cmdgen.MibVariable(pSYSDESCR,), + cmdgen.MibVariable(pSYSDESCR) ) if errorIndication: logging.info("Failed to get pdu controller type, exception: " + str(errorIndication)) @@ -59,57 +61,79 @@ def pduCntrlOid(self): Define Oids based on the PDU Type """ # MIB OIDs for 'APC Web/SNMP Management PDU' - APC_PORT_NAME_BASE_OID = "1.3.6.1.4.1.318.1.1.4.4.2.1.4" - APC_PORT_STATUS_BASE_OID = "1.3.6.1.4.1.318.1.1.12.3.5.1.1.4" - APC_PORT_CONTROL_BASE_OID = "1.3.6.1.4.1.318.1.1.12.3.3.1.1.4" + APC_PORT_NAME_BASE_OID = "1.3.6.1.4.1.318.1.1.4.4.2.1" + APC_PORT_STATUS_BASE_OID = "1.3.6.1.4.1.318.1.1.12.3.5.1.1" + APC_PORT_CONTROL_BASE_OID = "1.3.6.1.4.1.318.1.1.12.3.3.1.1" # MIB OID for 'Sentry Switched CDU' - SENTRY_PORT_NAME_BASE_OID = "1.3.6.1.4.1.1718.3.2.3.1.3.1" - SENTRY_PORT_STATUS_BASE_OID = "1.3.6.1.4.1.1718.3.2.3.1.5.1" - SENTRY_PORT_CONTROL_BASE_OID = "1.3.6.1.4.1.1718.3.2.3.1.11.1" + SENTRY_PORT_NAME_BASE_OID = "1.3.6.1.4.1.1718.3.2.3.1.3" + SENTRY_PORT_STATUS_BASE_OID = "1.3.6.1.4.1.1718.3.2.3.1.5" + SENTRY_PORT_CONTROL_BASE_OID = "1.3.6.1.4.1.1718.3.2.3.1.11" # MIB OID for 'Emerson' - EMERSON_PORT_NAME_BASE_OID = "1.3.6.1.4.1.476.1.42.3.8.50.20.1.10.1.1" - EMERSON_PORT_STATUS_BASE_OID = "1.3.6.1.4.1.476.1.42.3.8.50.20.1.100.1.1" - EMERSON_PORT_CONTROL_BASE_OID = "1.3.6.1.4.1.476.1.42.3.8.50.20.1.100.1.1" + EMERSON_PORT_NAME_BASE_OID = "1.3.6.1.4.1.476.1.42.3.8.50.20.1.10.1" + EMERSON_PORT_STATUS_BASE_OID = "1.3.6.1.4.1.476.1.42.3.8.50.20.1.100.1" + EMERSON_PORT_CONTROL_BASE_OID = "1.3.6.1.4.1.476.1.42.3.8.50.20.1.100.1" # MIB OID for 'Sentry Switched PDU' SENTRY4_PORT_NAME_BASE_OID = "1.3.6.1.4.1.1718.4.1.8.2.1.3" SENTRY4_PORT_STATUS_BASE_OID = "1.3.6.1.4.1.1718.4.1.8.3.1.1" SENTRY4_PORT_CONTROL_BASE_OID = "1.3.6.1.4.1.1718.4.1.8.5.1.2" + SENTRY4_PORT_POWER_BASE_OID = "1.3.6.1.4.1.1718.4.1.8.3.1.9" self.STATUS_ON = "1" self.STATUS_OFF = "0" self.CONTROL_ON = "1" self.CONTROL_OFF = "2" + self.has_lanes = True + self.max_lanes = 5 + self.PORT_POWER_BASE_OID = None if self.pduType == "APC": - self.pPORT_NAME_BASE_OID = '.'+APC_PORT_NAME_BASE_OID - self.pPORT_STATUS_BASE_OID = '.'+APC_PORT_STATUS_BASE_OID - self.pPORT_CONTROL_BASE_OID = '.'+APC_PORT_CONTROL_BASE_OID self.PORT_NAME_BASE_OID = APC_PORT_NAME_BASE_OID self.PORT_STATUS_BASE_OID = APC_PORT_STATUS_BASE_OID self.PORT_CONTROL_BASE_OID = APC_PORT_CONTROL_BASE_OID elif self.pduType == "SENTRY": - self.pPORT_NAME_BASE_OID = '.'+SENTRY_PORT_NAME_BASE_OID - self.pPORT_STATUS_BASE_OID = '.'+SENTRY_PORT_STATUS_BASE_OID - self.pPORT_CONTROL_BASE_OID = '.'+SENTRY_PORT_CONTROL_BASE_OID self.PORT_NAME_BASE_OID = SENTRY_PORT_NAME_BASE_OID self.PORT_STATUS_BASE_OID = SENTRY_PORT_STATUS_BASE_OID self.PORT_CONTROL_BASE_OID = SENTRY_PORT_CONTROL_BASE_OID elif self.pduType == "Emerson": - self.pPORT_NAME_BASE_OID = '.'+EMERSON_PORT_NAME_BASE_OID - self.pPORT_STATUS_BASE_OID = '.'+EMERSON_PORT_STATUS_BASE_OID - self.pPORT_CONTROL_BASE_OID = '.'+EMERSON_PORT_CONTROL_BASE_OID self.PORT_NAME_BASE_OID = EMERSON_PORT_NAME_BASE_OID self.PORT_STATUS_BASE_OID = EMERSON_PORT_STATUS_BASE_OID self.PORT_CONTROL_BASE_OID = EMERSON_PORT_CONTROL_BASE_OID elif self.pduType == "SENTRY4": - self.pPORT_NAME_BASE_OID = '.'+SENTRY4_PORT_NAME_BASE_OID - self.pPORT_STATUS_BASE_OID = '.'+SENTRY4_PORT_STATUS_BASE_OID - self.pPORT_CONTROL_BASE_OID = '.'+SENTRY4_PORT_CONTROL_BASE_OID self.PORT_NAME_BASE_OID = SENTRY4_PORT_NAME_BASE_OID self.PORT_STATUS_BASE_OID = SENTRY4_PORT_STATUS_BASE_OID self.PORT_CONTROL_BASE_OID = SENTRY4_PORT_CONTROL_BASE_OID + self.PORT_POWER_BASE_OID = SENTRY4_PORT_POWER_BASE_OID + self.has_lanes = False + self.max_lanes = 1 else: pass + def _build_outlet_maps(self, port_oid, label): + self.port_oid_dict[port_oid] = { 'label' : label } + self.port_label_dict[label] = { 'port_oid' : port_oid } + + + def _probe_lane(self, lane_id, cmdGen, snmp_auth): + pdu_port_base = self.PORT_NAME_BASE_OID + query_oid = '.' + pdu_port_base + if self.has_lanes: + query_oid = query_oid + str(lane_id) + + errorIndication, errorStatus, errorIndex, varTable = cmdGen.nextCmd( + snmp_auth, + cmdgen.UdpTransportTarget((self.controller, 161)), + cmdgen.MibVariable(query_oid) + ) + if errorIndication: + logging.debug("Failed to get ports controlling PSUs of DUT, exception: " + str(errorIndication)) + else: + for varBinds in varTable: + for oid, val in varBinds: + current_oid = oid.prettyPrint() + port_oid = current_oid.replace(pdu_port_base, '') + label = val.prettyPrint().lower() + self._build_outlet_maps(port_oid, label) + + def _get_pdu_ports(self): """ @summary: Helper method for getting PDU ports connected to PSUs of DUT @@ -118,68 +142,31 @@ def _get_pdu_ports(self): This method depends on this configuration to find out the PDU ports connected to PSUs of specific DUT. """ if not self.pduType: - logging.info('PDU type is unknown: pdu_ip {} dut {}'.format(self.controller, self.hostname)) + logging.info('PDU type is unknown: pdu_ip {}'.format(self.controller)) return - max_lane = 5 - host_matched = False cmdGen = cmdgen.CommandGenerator() snmp_auth = cmdgen.CommunityData(self.snmp_rocommunity) - for lane_id in range(1, max_lane + 1): - pdu_port_base = self.PORT_NAME_BASE_OID[0: -1] + str(lane_id) - - errorIndication, errorStatus, errorIndex, varTable = cmdGen.nextCmd( - snmp_auth, - cmdgen.UdpTransportTarget((self.controller, 161)), - cmdgen.MibVariable("." + pdu_port_base,), - ) - if errorIndication: - logging.debug("Failed to get ports controlling PSUs of DUT, exception: " + str(errorIndication)) - else: - for varBinds in varTable: - for oid, val in varBinds: - current_oid = oid.prettyPrint() - current_val = val.prettyPrint() - if self.hostname.lower() in current_val.lower(): - host_matched = True - # Remove the preceding PORT_NAME_BASE_OID, remaining string is the PDU port ID - self.pdu_ports.append(current_oid.replace(pdu_port_base, '')) - if host_matched: - self.map_host_to_lane(lane_id) - break - else: - logging.error("{} device is not attached to any outlet of PDU {}".format(self.hostname.lower(), self.controller)) + for lane_id in range(1, self.max_lanes + 1): + self._probe_lane(lane_id, cmdGen, snmp_auth) - def map_host_to_lane(self, lane_id): - """ - Dynamically update Oids based on the PDU lane ID - """ - if self.pduType == "SENTRY4": - # No need to update lane for SENTRY4 - return - self.pPORT_NAME_BASE_OID = self.pPORT_NAME_BASE_OID[0: -1] + str(lane_id) - self.pPORT_STATUS_BASE_OID = self.pPORT_STATUS_BASE_OID[0: -1] + str(lane_id) - self.pPORT_CONTROL_BASE_OID = self.pPORT_CONTROL_BASE_OID[0: -1] + str(lane_id) - self.PORT_NAME_BASE_OID = self.PORT_NAME_BASE_OID[0: -1] + str(lane_id) - self.PORT_STATUS_BASE_OID = self.PORT_STATUS_BASE_OID[0: -1] + str(lane_id) - self.PORT_CONTROL_BASE_OID = self.PORT_CONTROL_BASE_OID[0: -1] + str(lane_id) - - def __init__(self, hostname, controller, pdu): + def __init__(self, controller, pdu): logging.info("Initializing " + self.__class__.__name__) PduControllerBase.__init__(self) - self.hostname = hostname self.controller = controller self.snmp_rocommunity = pdu['snmp_rocommunity'] self.snmp_rwcommunity = pdu['snmp_rwcommunity'] - self.pdu_ports = [] self.pduType = None + self.port_oid_dict = {} + self.port_label_dict = {} self.get_pdu_controller_type() self.pduCntrlOid() self._get_pdu_ports() logging.info("Initialized " + self.__class__.__name__) + def turn_on_outlet(self, outlet): """ @summary: Use SNMP to turn on power to PDU of DUT specified by outlet @@ -190,23 +177,19 @@ def turn_on_outlet(self, outlet): Because of this, currently we just find out which PDU ports are connected to PSUs of which DUT. We cannot find out the exact mapping between PDU ports and PSUs of DUT. - To overcome this limitation, the trick is to convert the specified outlet to integer, then calculate the mode - upon the number of PSUs on DUT. The calculated mode is used as an index to get PDU ports ID stored in - self.pdu_ports. But still, we cannot gurante that outlet 0 is first PDU of DUT, and so on. - @param outlet: ID of the PDU on SONiC DUT @return: Return true if successfully execute the command for turning on power. Otherwise return False. """ if not self.pduType: - logging.error('Unable to turn on: PDU type is unknown: pdu_ip {} dut {}'.format(self.controller, self.hostname)) + logging.error('Unable to turn on: PDU type is unknown: pdu_ip {}'.format(self.controller)) return False - port_oid = self.pPORT_CONTROL_BASE_OID + self.pdu_ports[rfc1902.Integer(outlet)] + port_oid = '.' + self.PORT_CONTROL_BASE_OID + outlet errorIndication, errorStatus, _, _ = \ cmdgen.CommandGenerator().setCmd( cmdgen.CommunityData(self.snmp_rwcommunity), cmdgen.UdpTransportTarget((self.controller, 161)), - (port_oid, rfc1902.Integer(self.CONTROL_ON)), + (port_oid, rfc1902.Integer(self.CONTROL_ON)) ) if errorIndication or errorStatus != 0: logging.debug("Failed to turn on outlet %s, exception: %s" % (str(outlet), str(errorStatus))) @@ -223,30 +206,71 @@ def turn_off_outlet(self, outlet): Because of this, currently we just find out which PDU outlets are connected to PSUs of which DUT. We cannot find out the exact mapping between PDU outlets and PSUs of DUT. - To overcome this limitation, the trick is to convert the specified outlet to integer, then calculate the mode - upon the number of PSUs on DUT. The calculated mode is used as an index to get PDU ports ID stored in - self.pdu_ports. But still, we cannot guarantee that outlet 0 is first PSU of DUT, and so on. - @param outlet: ID of the outlet on PDU @return: Return true if successfully execute the command for turning off power. Otherwise return False. """ if not self.pduType: - logging.error('Unable to turn off: PDU type is unknown: pdu_ip {} dut {}'.format(self.controller, self.hostname)) + logging.error('Unable to turn off: PDU type is unknown: pdu_ip {}'.format(self.controller)) return False - port_oid = self.pPORT_CONTROL_BASE_OID + self.pdu_ports[rfc1902.Integer(outlet)] + port_oid = '.' + self.PORT_CONTROL_BASE_OID + outlet errorIndication, errorStatus, _, _ = \ cmdgen.CommandGenerator().setCmd( cmdgen.CommunityData(self.snmp_rwcommunity), cmdgen.UdpTransportTarget((self.controller, 161)), - (port_oid, rfc1902.Integer(self.CONTROL_OFF)), + (port_oid, rfc1902.Integer(self.CONTROL_OFF)) ) if errorIndication or errorStatus != 0: logging.debug("Failed to turn on outlet %s, exception: %s" % (str(outlet), str(errorStatus))) return False return True - def get_outlet_status(self, outlet=None): + + def _get_one_outlet_power(self, cmdGen, snmp_auth, port_id, status): + if not self.PORT_POWER_BASE_OID: + return + + query_id = '.' + self.PORT_POWER_BASE_OID + port_id + errorIndication, errorStatus, errorIndex, varBinds = cmdGen.getCmd( + snmp_auth, + cmdgen.UdpTransportTarget((self.controller, 161)), + cmdgen.MibVariable(query_id) + ) + if errorIndication: + logging.debug("Failed to get outlet power level of DUT outlet, exception: " + str(errorIndication)) + + for oid, val in varBinds: + current_oid = oid.prettyPrint() + current_val = val.prettyPrint() + port_oid = current_oid.replace(self.PORT_POWER_BASE_OID, '') + if port_oid == port_id: + status['output_watts'] = current_val + return + + + def _get_one_outlet_status(self, cmdGen, snmp_auth, port_id): + query_id = '.' + self.PORT_STATUS_BASE_OID + port_id + errorIndication, errorStatus, errorIndex, varBinds = cmdGen.getCmd( + snmp_auth, + cmdgen.UdpTransportTarget((self.controller, 161)), + cmdgen.MibVariable(query_id) + ) + if errorIndication: + logging.debug("Failed to outlet status of PDU, exception: " + str(errorIndication)) + + for oid, val in varBinds: + current_oid = oid.prettyPrint() + current_val = val.prettyPrint() + port_oid = current_oid.replace(self.PORT_STATUS_BASE_OID, '') + if port_oid == port_id: + status = {"outlet_id": port_oid, "outlet_on": True if current_val == self.STATUS_ON else False} + self._get_one_outlet_power(cmdGen, snmp_auth, port_id, status) + return status + + return None + + + def get_outlet_status(self, outlet=None, hostname=None): """ @summary: Use SNMP to get status of PDU ports supplying power to PSUs of DUT @@ -256,42 +280,38 @@ def get_outlet_status(self, outlet=None): Because of this, currently we just find out which PDU ports are connected to PSUs of which DUT. We cannot find out the exact mapping between PDU outlets and PSUs of DUT. - To overcome this limitation, the trick is to convert the specified outlet to integer, then calculate the mode - upon the number of PSUs on DUT. The calculated mode is used as an index to get PDU outlet ID stored in - self.pdu_ports. But still, we cannot guarantee that outlet 0 is first PSU of DUT, and so on. - @param outlet: Optional. If specified, only return status of PDU outlet connected to specified PSU of DUT. If omitted, return status of all PDU outlets connected to PSUs of DUT. @return: Return status of PDU outlets connected to PSUs of DUT in a list of dictionary. Example result: - [{"outlet_id": 0, "outlet_on": True}, {"outlet_id": 1, "outlet_on": True}] + [{"outlet_id": "0.0.1", "outlet_on": True}, {"outlet_id": "0.0.2", "outlet_on": True}] The outlet in returned result is integer starts from 0. """ results = [] if not self.pduType: - logging.error('Unable to retrieve status: PDU type is unknown: pdu_ip {} dut {}'.format(self.controller, self.hostname)) + logging.error('Unable to retrieve status: PDU type is unknown: pdu_ip {}'.format(self.controller)) return results + if not outlet and not hostname: + # Return status of all outlets + ports = self.port_oid_dict.keys() + elif outlet: + ports = [ oid for oid in self.port_oid_dict.keys() if oid.endswith(outlet) ] + if not ports: + logging.error("Outlet ID {} doesn't belong to PDU {}".format(outlet, self.controller)) + elif hostname: + hn = hostname.lower() + ports = [ self.port_label_dict[label]['port_oid'] for label in self.port_label_dict.keys() if hn in label ] + if not ports: + logging.error("{} device is not attached to any outlet of PDU {}".format(hn, self.controller)) + cmdGen = cmdgen.CommandGenerator() snmp_auth = cmdgen.CommunityData(self.snmp_rocommunity) - errorIndication, errorStatus, errorIndex, varTable = cmdGen.nextCmd( - snmp_auth, - cmdgen.UdpTransportTarget((self.controller, 161)), - cmdgen.MibVariable(self.pPORT_STATUS_BASE_OID,), - ) - if errorIndication: - logging.debug("Failed to get ports controlling PSUs of DUT, exception: " + str(errorIndication)) - for varBinds in varTable: - for oid, val in varBinds: - current_oid = oid.prettyPrint() - current_val = val.prettyPrint() - for idx, port in enumerate(self.pdu_ports): - port_oid = self.PORT_STATUS_BASE_OID + port - if current_oid == port_oid: - status = {"outlet_id": idx, "outlet_on": True if current_val == self.STATUS_ON else False} - results.append(status) - if outlet is not None: - idx = int(outlet) % len(self.pdu_ports) - results = results[idx:idx+1] + + for port in ports: + status = self._get_one_outlet_status(cmdGen, snmp_auth, port) + if status: + results.append(status) + logging.info("Got outlet status: %s" % str(results)) return results @@ -299,9 +319,9 @@ def close(self): pass -def get_pdu_controller(controller_ip, dut_hostname, pdu): +def get_pdu_controller(controller_ip, pdu): """ @summary: Factory function to create the actual PDU controller object. @return: The actual PDU controller object. Returns None if something went wrong. """ - return snmpPduController(dut_hostname, controller_ip, pdu) + return snmpPduController(controller_ip, pdu)