From b990a2f931608e5e491cda9839fd8d5ed4f2c3e5 Mon Sep 17 00:00:00 2001 From: Jonathan Provost Date: Thu, 20 Oct 2016 22:28:12 -0400 Subject: [PATCH] Move the outlet state responsibility to the core Now, the core stores the outlet state and the SNMP PDU adapter only notifies and consult the core about the state of an outlet. In the case where the outlet has never been set, the driver is queried to get the outlet state. In the event that there is no device (libvirt domain) associated to an outlet, the default outlet state from the configuration will be returned. Also, the outlet default state is now in the "global" section of the configuration as there is no outlet defalut state per PDU anymore. [global] outlet_default_state = ON --- virtualpdu/core.py | 41 ++++++++++++++----- virtualpdu/main.py | 21 ++++++---- virtualpdu/pdu/__init__.py | 26 ++++++------ virtualpdu/pdu/apc_rackpdu.py | 4 +- virtualpdu/pdu/baytech_mrp27.py | 4 +- .../tests/integration/pdu/test_apc_rackpdu.py | 8 +++- .../integration/pdu/test_baytech_mrp27.py | 8 +++- virtualpdu/tests/integration/pdu/test_pdu.py | 6 +-- .../integration/test_core_integration.py | 16 ++++++-- .../tests/integration/test_entrypoint.py | 6 ++- .../tests/unit/pdu/base_pdu_test_cases.py | 36 +++++++++++++--- virtualpdu/tests/unit/test_core.py | 38 +++++++++++++---- 12 files changed, 152 insertions(+), 62 deletions(-) diff --git a/virtualpdu/core.py b/virtualpdu/core.py index 2c9d8ce..645853a 100644 --- a/virtualpdu/core.py +++ b/virtualpdu/core.py @@ -19,32 +19,51 @@ REBOOT = 'REBOOT' class Core(object): - def __init__(self, driver, mapping): + def __init__(self, driver, mapping, store, default_state): self.driver = driver self.mapping = mapping + self.store = store + self.default_state = default_state self.logger = logging.getLogger(__name__) - def pdu_outlet_state_changed(self, name, outlet_number, state): + def pdu_outlet_state_changed(self, pdu, outlet, state): + self.store[(pdu, outlet)] = state + self.logger.info("PDU '{}', outlet '{}' has new state: '{}'".format( - name, outlet_number, state) + pdu, outlet, state) ) try: - server_name = self._get_server_name(name, outlet_number) + device = self._get_device(pdu, outlet) self.logger.debug( "Found server '{}' on PDU '{}' outlet '{}'".format( - server_name, name, outlet_number) + device, pdu, outlet) ) except KeyError: return if state == POWER_ON: - self.driver.power_on(server_name) + self.driver.power_on(device) elif state == POWER_OFF: - self.driver.power_off(server_name) + self.driver.power_off(device) elif state == REBOOT: - self.driver.power_off(server_name) - self.driver.power_on(server_name) + self.driver.power_off(device) + self.driver.power_on(device) - def _get_server_name(self, pdu_name, outlet_number): - return self.mapping[(pdu_name, outlet_number)] + def get_pdu_outlet_state(self, pdu, outlet): + try: + return self.store[(pdu, outlet)] + except KeyError: + pass + + try: + device = self._get_device(pdu, outlet) + power_state = self.driver.get_power_state(device) + except KeyError: + power_state = self.default_state + + self.store[(pdu, outlet)] = power_state + return power_state + + def _get_device(self, pdu, outlet): + return self.mapping[(pdu, outlet)] diff --git a/virtualpdu/main.py b/virtualpdu/main.py index 2dbe365..b38a4ac 100644 --- a/virtualpdu/main.py +++ b/virtualpdu/main.py @@ -40,7 +40,9 @@ def main(): config.read(config_file) driver = get_driver_from_config(config) mapping = get_mapping_for_config(config) - core = virtualpdu.core.Core(driver=driver, mapping=mapping) + outlet_default_state = get_default_state_from_config(config) + core = virtualpdu.core.Core(driver=driver, mapping=mapping, store={}, + default_state=outlet_default_state) pdu_threads = [] for pdu in [s for s in config.sections() if s != 'global']: @@ -48,14 +50,7 @@ def main(): port = int(config.get(pdu, 'listen_port')) community = config.get(pdu, 'community') - try: - default_state = config.get(pdu, 'outlet_default_state') - except configparser.NoOptionError: - default_state = 'ON' - - outlet_default_state = parse_default_state_config(default_state) - - apc_pdu = apc_rackpdu.APCRackPDU(pdu, core, outlet_default_state) + apc_pdu = apc_rackpdu.APCRackPDU(pdu, core) pdu_threads.append(pysnmp_handler.SNMPPDUHarness( apc_pdu, @@ -115,5 +110,13 @@ def get_mapping_for_config(conf): return mapping +def get_default_state_from_config(conf): + try: + default_state = conf.get('global', 'outlet_default_state') + except (configparser.NoSectionError, configparser.NoOptionError): + default_state = 'ON' + return parse_default_state_config(default_state) + + class UnableToParseConfig(Exception): pass diff --git a/virtualpdu/pdu/__init__.py b/virtualpdu/pdu/__init__.py index 8e8e627..8a34ff3 100644 --- a/virtualpdu/pdu/__init__.py +++ b/virtualpdu/pdu/__init__.py @@ -45,20 +45,18 @@ class PDUOutletStates(BasePDUOutletStates): class PDUOutlet(object): states = PDUOutletStates() - def __init__(self, outlet_number, pdu, default_state): + def __init__(self, outlet_number, pdu): self.outlet_number = outlet_number self.pdu = pdu - self._state = default_state self.oid = None @property def state(self): - return self._state + return self.pdu.get_outlet_state(self.outlet_number) @state.setter def state(self, state): - self._state = state - self.pdu.outlet_state_changed(self.outlet_number, self._state) + self.pdu.set_outlet_state(self.outlet_number, state) class PDU(object): @@ -66,24 +64,26 @@ class PDU(object): outlet_index_start = 1 outlet_class = PDUOutlet - def __init__(self, name, core, outlet_default_state=core.POWER_ON): + def __init__(self, name, core): self.name = name self.core = core - outlet_native_default_state = \ - self.outlet_class.states.from_core(outlet_default_state) - self.oids = [ self.outlet_class(outlet_number=o + self.outlet_index_start, pdu=self, - default_state=outlet_native_default_state ) for o in range(self.outlet_count) ] self.oid_mapping = {oid.oid: oid for oid in self.oids} - def outlet_state_changed(self, outlet_number, value): + def set_outlet_state(self, outlet_number, value): self.core.pdu_outlet_state_changed( - name=self.name, - outlet_number=outlet_number, + pdu=self.name, + outlet=outlet_number, state=self.outlet_class.states.to_core(value)) + + def get_outlet_state(self, outlet_number): + return self.outlet_class.states.from_core( + self.core.get_pdu_outlet_state( + pdu=self.name, + outlet=outlet_number)) diff --git a/virtualpdu/pdu/apc_rackpdu.py b/virtualpdu/pdu/apc_rackpdu.py index 67db816..51681a1 100644 --- a/virtualpdu/pdu/apc_rackpdu.py +++ b/virtualpdu/pdu/apc_rackpdu.py @@ -42,9 +42,9 @@ class APCRackPDUOutletStates(BasePDUOutletStates): class APCRackPDUOutlet(PDUOutlet): states = APCRackPDUOutletStates() - def __init__(self, outlet_number, pdu, default_state): + def __init__(self, outlet_number, pdu): super(APCRackPDUOutlet, self).__init__( - outlet_number, pdu, default_state) + outlet_number, pdu) self.oid = rPDU_outlet_control_outlet_command + (self.outlet_number, ) diff --git a/virtualpdu/pdu/baytech_mrp27.py b/virtualpdu/pdu/baytech_mrp27.py index f71762f..2ac86ab 100644 --- a/virtualpdu/pdu/baytech_mrp27.py +++ b/virtualpdu/pdu/baytech_mrp27.py @@ -40,9 +40,9 @@ class BaytechMRP27PDUOutletStates(BasePDUOutletStates): class BaytechMRP27PDUOutlet(PDUOutlet): states = BaytechMRP27PDUOutletStates() - def __init__(self, outlet_number, pdu, default_state): + def __init__(self, outlet_number, pdu): super(BaytechMRP27PDUOutlet, self).__init__( - outlet_number, pdu, default_state) + outlet_number, pdu) self.oid = sBTA_modules_RPC_outlet_state + (1, self.outlet_number) diff --git a/virtualpdu/tests/integration/pdu/test_apc_rackpdu.py b/virtualpdu/tests/integration/pdu/test_apc_rackpdu.py index 9b92a52..89b40c4 100644 --- a/virtualpdu/tests/integration/pdu/test_apc_rackpdu.py +++ b/virtualpdu/tests/integration/pdu/test_apc_rackpdu.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +from virtualpdu import core from virtualpdu.pdu.apc_rackpdu import APCRackPDU from virtualpdu.tests.integration.pdu import PDUTestCase @@ -21,6 +21,8 @@ class TestAPCRackPDU(PDUTestCase): pdu_class = APCRackPDU def test_all_ports_are_on_by_default(self): + self.core_mock.get_pdu_outlet_state.return_value = core.POWER_ON + enterprises = (1, 3, 6, 1, 4, 1) rPDUControl = (318, 1, 1, 12, 3, 3, 1, 1, 4) @@ -40,10 +42,14 @@ class TestAPCRackPDU(PDUTestCase): rPDUControl = (318, 1, 1, 12, 3, 3, 1, 1, 4) outlet_1 = enterprises + rPDUControl + (1,) + self.core_mock.get_pdu_outlet_state.return_value = core.POWER_ON self.assertEqual(self.pdu.outlet_class.states.IMMEDIATE_ON, self.snmp_get(outlet_1)) self.snmp_set(outlet_1, self.pdu.outlet_class.states.IMMEDIATE_OFF) + self.core_mock.pdu_outlet_state_changed.assert_called_with( + pdu=self.pdu.name, outlet=1, state=core.POWER_OFF) + self.core_mock.get_pdu_outlet_state.return_value = core.POWER_OFF self.assertEqual(self.pdu.outlet_class.states.IMMEDIATE_OFF, self.snmp_get(outlet_1)) diff --git a/virtualpdu/tests/integration/pdu/test_baytech_mrp27.py b/virtualpdu/tests/integration/pdu/test_baytech_mrp27.py index 5900e24..b2c47a3 100644 --- a/virtualpdu/tests/integration/pdu/test_baytech_mrp27.py +++ b/virtualpdu/tests/integration/pdu/test_baytech_mrp27.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +from virtualpdu import core from virtualpdu.pdu.baytech_mrp27 import BaytechMRP27PDU from virtualpdu.tests.integration.pdu import PDUTestCase @@ -21,6 +21,8 @@ class TestBaytechMRP27PDU(PDUTestCase): pdu_class = BaytechMRP27PDU def test_all_ports_are_on_by_default(self): + self.core_mock.get_pdu_outlet_state.return_value = core.POWER_ON + outlet_state_oid = (1, 3, 6, 1, 4, 1) + (4779, 1, 3, 5, 3, 1, 3) self.assertEqual(1, self.snmp_get(outlet_state_oid + (1, 1))) @@ -40,10 +42,14 @@ class TestBaytechMRP27PDU(PDUTestCase): outlet_state_oid = (1, 3, 6, 1, 4, 1) + (4779, 1, 3, 5, 3, 1, 3) outlet_1 = outlet_state_oid + (1, 1) + self.core_mock.get_pdu_outlet_state.return_value = core.POWER_ON self.assertEqual(self.pdu.outlet_class.states.ON, self.snmp_get(outlet_1)) self.snmp_set(outlet_1, self.pdu.outlet_class.states.OFF) + self.core_mock.pdu_outlet_state_changed.assert_called_with( + pdu=self.pdu.name, outlet=1, state=core.POWER_OFF) + self.core_mock.get_pdu_outlet_state.return_value = core.POWER_OFF self.assertEqual(self.pdu.outlet_class.states.OFF, self.snmp_get(outlet_1)) diff --git a/virtualpdu/tests/integration/pdu/test_pdu.py b/virtualpdu/tests/integration/pdu/test_pdu.py index 3982bb1..3ced144 100644 --- a/virtualpdu/tests/integration/pdu/test_pdu.py +++ b/virtualpdu/tests/integration/pdu/test_pdu.py @@ -15,6 +15,7 @@ from pyasn1.type import univ from pysnmp.proto.rfc1905 import NoSuchInstance +from virtualpdu import core from virtualpdu import pdu from virtualpdu.tests.integration.pdu import PDUTestCase from virtualpdu.tests.snmp_error_indications import RequestTimedOut @@ -34,11 +35,10 @@ class TestPDU(PDUTestCase): self.snmp_set(enterprises + (42,), univ.Integer(7))) def test_get_valid_oid_wrong_community(self): - default_state = self.pdu.outlet_class.states.ON + self.core_mock.get_pdu_outlet_state.return_value = core.POWER_ON self.pdu.oid_mapping[enterprises + (88, 1)] = \ pdu.PDUOutlet(outlet_number=1, - pdu=self.pdu, - default_state=default_state) + pdu=self.pdu) self.assertEqual(self.pdu.outlet_class.states.ON, self.snmp_get(enterprises + (88, 1))) diff --git a/virtualpdu/tests/integration/test_core_integration.py b/virtualpdu/tests/integration/test_core_integration.py index 9297889..1569579 100644 --- a/virtualpdu/tests/integration/test_core_integration.py +++ b/virtualpdu/tests/integration/test_core_integration.py @@ -32,7 +32,9 @@ class TestCoreIntegration(base.TestCase): self.core = core.Core(driver=self.driver, mapping={ ('my_pdu', 5): 'test' - }) + }, + store={}, + default_state=core.POWER_OFF) self.outlet_oid = apc_rackpdu.rPDU_outlet_control_outlet_command + (5,) @@ -73,14 +75,22 @@ class TestCoreIntegration(base.TestCase): self.driver.get_power_state('test')) def test_initial_outlet_power_state_on(self): - pdu = apc_rackpdu.APCRackPDU('my_pdu', self.core, core.POWER_ON) + my_core = core.Core(driver=self.driver, + mapping={}, + store={}, + default_state=core.POWER_ON) + pdu = apc_rackpdu.APCRackPDU('my_pdu', my_core) snmp_client_ = self.get_harness_client(pdu) self.assertEqual(pdu.outlet_class.states.IMMEDIATE_ON, snmp_client_.get_one(self.outlet_oid)) def test_initial_outlet_power_state_off(self): - pdu = apc_rackpdu.APCRackPDU('my_pdu', self.core, core.POWER_OFF) + my_core = core.Core(driver=self.driver, + mapping={}, + store={}, + default_state=core.POWER_OFF) + pdu = apc_rackpdu.APCRackPDU('my_pdu', my_core) snmp_client_ = self.get_harness_client(pdu) self.assertEqual(pdu.outlet_class.states.IMMEDIATE_OFF, diff --git a/virtualpdu/tests/integration/test_entrypoint.py b/virtualpdu/tests/integration/test_entrypoint.py index 2189ece..1fc49c1 100644 --- a/virtualpdu/tests/integration/test_entrypoint.py +++ b/virtualpdu/tests/integration/test_entrypoint.py @@ -128,8 +128,10 @@ class TestEntryPointIntegration(base.TestCase): def test_entrypoint_raise_on_invalid_mode(self): with tempfile.NamedTemporaryFile() as f: - test_config2 = TEST_CONFIG + """outlet_default_state = invalid_mode - """ + test_config2 = """[global] +libvirt_uri=test:///default +outlet_default_state = invalid_mode +""" f.write(bytearray(test_config2, encoding='utf-8')) f.flush() try: diff --git a/virtualpdu/tests/unit/pdu/base_pdu_test_cases.py b/virtualpdu/tests/unit/pdu/base_pdu_test_cases.py index 5c34ecc..0fd4914 100644 --- a/virtualpdu/tests/unit/pdu/base_pdu_test_cases.py +++ b/virtualpdu/tests/unit/pdu/base_pdu_test_cases.py @@ -21,8 +21,8 @@ class BasePDUTests(object): self.pdu.outlet_class.states.from_core(core.POWER_ON) self.core_mock.pdu_outlet_state_changed.assert_called_with( - name='my_pdu', - outlet_number=1, + pdu='my_pdu', + outlet=1, state=core.POWER_ON) def test_reboot_notifies_core(self): @@ -30,8 +30,8 @@ class BasePDUTests(object): self.pdu.outlet_class.states.from_core(core.REBOOT) self.core_mock.pdu_outlet_state_changed.assert_called_with( - name='my_pdu', - outlet_number=1, + pdu='my_pdu', + outlet=1, state=core.REBOOT) def test_power_off_notifies_core(self): @@ -39,6 +39,30 @@ class BasePDUTests(object): self.pdu.outlet_class.states.from_core(core.POWER_OFF) self.core_mock.pdu_outlet_state_changed.assert_called_with( - name='my_pdu', - outlet_number=1, + pdu='my_pdu', + outlet=1, state=core.POWER_OFF) + + def test_read_power_on(self): + self.core_mock.get_pdu_outlet_state.return_value = core.POWER_ON + + self.assertEqual( + self.pdu.outlet_class.states.from_core(core.POWER_ON), + self.pdu.oids[0].state + ) + + self.core_mock.get_pdu_outlet_state.assert_called_with( + pdu='my_pdu', + outlet=1) + + def test_read_power_off(self): + self.core_mock.get_pdu_outlet_state.return_value = core.POWER_OFF + + self.assertEqual( + self.pdu.outlet_class.states.from_core(core.POWER_OFF), + self.pdu.oids[0].state + ) + + self.core_mock.get_pdu_outlet_state.assert_called_with( + pdu='my_pdu', + outlet=1) diff --git a/virtualpdu/tests/unit/test_core.py b/virtualpdu/tests/unit/test_core.py index ec9d6c1..55f68d7 100644 --- a/virtualpdu/tests/unit/test_core.py +++ b/virtualpdu/tests/unit/test_core.py @@ -27,34 +27,54 @@ class TestCore(base.TestCase): ('my_pdu', 1): 'server_one' } - self.core = core.Core(driver=self.driver_mock, mapping=mapping) + self.store = {} + self.core = core.Core(driver=self.driver_mock, mapping=mapping, + store=self.store, default_state=core.POWER_ON) def test_pdu_outlet_state_changed_on_power_off(self): - self.core.pdu_outlet_state_changed(name='my_pdu', - outlet_number=1, + self.core.pdu_outlet_state_changed(pdu='my_pdu', + outlet=1, state=core.POWER_OFF) self.driver_mock.power_off.assert_called_with('server_one') def test_pdu_outlet_state_changed_machine_not_in_mapping_noop(self): - self.core.pdu_outlet_state_changed(name='my_pdu', - outlet_number=2, + self.core.pdu_outlet_state_changed(pdu='my_pdu', + outlet=2, state=core.POWER_OFF) self.assertFalse(self.driver_mock.power_off.called) self.assertFalse(self.driver_mock.power_on.called) def test_pdu_outlet_state_changed_on_power_on(self): - self.core.pdu_outlet_state_changed(name='my_pdu', - outlet_number=1, + self.core.pdu_outlet_state_changed(pdu='my_pdu', + outlet=1, state=core.POWER_ON) self.driver_mock.power_on.assert_called_with('server_one') def test_pdu_outlet_state_changed_on_reboot(self): - self.core.pdu_outlet_state_changed(name='my_pdu', - outlet_number=1, + self.core.pdu_outlet_state_changed(pdu='my_pdu', + outlet=1, state=core.REBOOT) self.driver_mock.assert_has_calls([mock.call.power_off('server_one'), mock.call.power_on('server_one')]) + + def test_pdu_outlet_state_on_cached_state(self): + self.store[('my_pdu', 1)] = core.POWER_OFF + self.assertEqual( + core.POWER_OFF, + self.core.get_pdu_outlet_state(pdu='my_pdu', outlet=1)) + + def test_pdu_outlet_state_on_connected_device(self): + self.driver_mock.get_power_state.return_value = core.POWER_OFF + self.assertEqual( + core.POWER_OFF, + self.core.get_pdu_outlet_state(pdu='my_pdu', outlet=1)) + self.driver_mock.get_power_state.assert_called_with('server_one') + + def test_pdu_outlet_state_on_disconnected_outlet(self): + self.assertEqual( + core.POWER_ON, + self.core.get_pdu_outlet_state(pdu='my_pdu', outlet=2))