diff --git a/os_faults/api/node_collection.py b/os_faults/api/node_collection.py index 4e6ea1c..1ccb972 100644 --- a/os_faults/api/node_collection.py +++ b/os_faults/api/node_collection.py @@ -165,6 +165,20 @@ class NodeCollection(object): LOG.info('Reset nodes: %s', self) self.power_management.reset(self.get_macs()) + def snapshot(self, snapshot_name, suspend=True): + """Create snapshot for all nodes + + """ + LOG.info('Create snapshot "%s" for nodes: %s', snapshot_name, self) + self.power_management.snapshot(self.get_macs(), snapshot_name, suspend) + + def revert(self, snapshot_name, resume=True): + """Revert snapshot for all nodes + + """ + LOG.info('Revert snapshot "%s" for nodes: %s', snapshot_name, self) + self.power_management.revert(self.get_macs(), snapshot_name, resume) + @public def disconnect(self, network_name): """Disconnect nodes from network diff --git a/os_faults/api/power_management.py b/os_faults/api/power_management.py index 5d0fc4e..c595fdc 100644 --- a/os_faults/api/power_management.py +++ b/os_faults/api/power_management.py @@ -32,3 +32,9 @@ class PowerManagement(base_driver.BaseDriver): @abc.abstractmethod def reset(self, hosts): pass + + def snapshot(self, hosts, snapshot_name, suspend=True): + raise NotImplementedError + + def revert(self, hosts, snapshot_name, resume=True): + raise NotImplementedError diff --git a/os_faults/drivers/ipmi.py b/os_faults/drivers/ipmi.py index 365ec31..ae364df 100644 --- a/os_faults/drivers/ipmi.py +++ b/os_faults/drivers/ipmi.py @@ -100,10 +100,16 @@ class IPMIDriver(power_management.PowerManagement): LOG.info('Node reset: %s', mac_address) def poweroff(self, mac_addresses_list): - utils.run(self._poweroff, mac_addresses_list) + kwargs_list = [dict(mac_address=mac_address) + for mac_address in mac_addresses_list] + utils.run(self._poweroff, kwargs_list) def poweron(self, mac_addresses_list): - utils.run(self._poweron, mac_addresses_list) + kwargs_list = [dict(mac_address=mac_address) + for mac_address in mac_addresses_list] + utils.run(self._poweron, kwargs_list) def reset(self, mac_addresses_list): - utils.run(self._reset, mac_addresses_list) + kwargs_list = [dict(mac_address=mac_address) + for mac_address in mac_addresses_list] + utils.run(self._reset, kwargs_list) diff --git a/os_faults/drivers/libvirt_driver.py b/os_faults/drivers/libvirt_driver.py index bde4f39..4842c45 100644 --- a/os_faults/drivers/libvirt_driver.py +++ b/os_faults/drivers/libvirt_driver.py @@ -28,7 +28,6 @@ class LibvirtDriver(power_management.PowerManagement): '$schema': 'http://json-schema.org/draft-04/schema#', 'properties': { 'connection_uri': {'type': 'string'}, - }, 'required': ['connection_uri'], 'additionalProperties': False, @@ -79,11 +78,56 @@ class LibvirtDriver(power_management.PowerManagement): domain.reset() LOG.info('Domain reset: %s', mac_address) + def _snapshot(self, mac_address, snapshot_name, suspend): + LOG.debug('Create snapshot "%s" for domain with MAC address: %s', + snapshot_name, mac_address) + domain = self._find_domain_by_mac_address(mac_address) + if suspend: + domain.suspend() + domain.snapshotCreateXML( + '{}'.format( + snapshot_name)) + if suspend: + domain.resume() + LOG.debug('Created snapshot "%s" for domain with MAC address: %s', + snapshot_name, mac_address) + + def _revert(self, mac_address, snapshot_name, resume): + LOG.debug('Revert snapshot "%s" for domain with MAC address: %s', + snapshot_name, mac_address) + domain = self._find_domain_by_mac_address(mac_address) + snapshot = domain.snapshotLookupByName(snapshot_name) + domain.revertToSnapshot(snapshot) + if resume: + domain.resume() + LOG.debug('Reverted snapshot "%s" for domain with MAC address: %s', + snapshot_name, mac_address) + def poweroff(self, mac_addresses_list): - utils.run(self._poweroff, mac_addresses_list) + kwargs_list = [dict(mac_address=mac_address) + for mac_address in mac_addresses_list] + utils.run(self._poweroff, kwargs_list) def poweron(self, mac_addresses_list): - utils.run(self._poweron, mac_addresses_list) + kwargs_list = [dict(mac_address=mac_address) + for mac_address in mac_addresses_list] + utils.run(self._poweron, kwargs_list) def reset(self, mac_addresses_list): - utils.run(self._reset, mac_addresses_list) + kwargs_list = [dict(mac_address=mac_address) + for mac_address in mac_addresses_list] + utils.run(self._reset, kwargs_list) + + def snapshot(self, mac_addresses_list, snapshot_name, suspend=True): + kwargs_list = [dict(mac_address=mac_address, + snapshot_name=snapshot_name, + suspend=suspend) + for mac_address in mac_addresses_list] + utils.run(self._snapshot, kwargs_list) + + def revert(self, mac_addresses_list, snapshot_name, resume=True): + kwargs_list = [dict(mac_address=mac_address, + snapshot_name=snapshot_name, + resume=resume) + for mac_address in mac_addresses_list] + utils.run(self._revert, kwargs_list) diff --git a/os_faults/tests/unit/api/test_node_collection.py b/os_faults/tests/unit/api/test_node_collection.py index 830cd5f..1251ffb 100644 --- a/os_faults/tests/unit/api/test_node_collection.py +++ b/os_faults/tests/unit/api/test_node_collection.py @@ -198,3 +198,15 @@ class NodeCollectionTestCase(test.TestCase): self.mock_cloud_management.execute_on_cloud.assert_called_once_with( ['10.0.0.2', '10.0.0.3', '10.0.0.4', '10.0.0.5'], {'command': 'reboot now'}) + + def test_snapshot(self): + self.node_collection.snapshot('foo') + self.mock_power_management.snapshot.assert_called_once_with( + ['09:7b:74:90:63:c1', '09:7b:74:90:63:c2', + '09:7b:74:90:63:c3', '09:7b:74:90:63:c4'], 'foo', True) + + def test_revert(self): + self.node_collection.revert('foo') + self.mock_power_management.revert.assert_called_once_with( + ['09:7b:74:90:63:c1', '09:7b:74:90:63:c2', + '09:7b:74:90:63:c3', '09:7b:74:90:63:c4'], 'foo', True) diff --git a/os_faults/tests/unit/drivers/test_ipmi.py b/os_faults/tests/unit/drivers/test_ipmi.py index 803c0ee..63fd94f 100644 --- a/os_faults/tests/unit/drivers/test_ipmi.py +++ b/os_faults/tests/unit/drivers/test_ipmi.py @@ -89,5 +89,7 @@ class IPMIDriverTestCase(test.TestCase): def test_driver_actions(self, action, mock_run): macs_list = ['00:00:00:00:00:00', '00:00:00:00:00:01'] getattr(self.driver, action)(macs_list) - mock_run.assert_called_once_with(getattr(self.driver, '_%s' % action), - macs_list) + mock_run.assert_called_once_with( + getattr(self.driver, '_%s' % action), + [{'mac_address': '00:00:00:00:00:00'}, + {'mac_address': '00:00:00:00:00:01'}]) diff --git a/os_faults/tests/unit/drivers/test_libvirt_driver.py b/os_faults/tests/unit/drivers/test_libvirt_driver.py index 04e0630..5078a47 100644 --- a/os_faults/tests/unit/drivers/test_libvirt_driver.py +++ b/os_faults/tests/unit/drivers/test_libvirt_driver.py @@ -77,10 +77,74 @@ class LibvirtDriverTestCase(test.TestCase): domain = mock__find_domain_by_mac_address.return_value getattr(domain, actions[1]).assert_called_once_with() + @mock.patch(DRIVER_PATH + '.LibvirtDriver._find_domain_by_mac_address') + def test__snapshot(self, mock__find_domain_by_mac_address): + self.driver._snapshot('52:54:00:f9:b8:f9', 'foo', suspend=False) + domain = mock__find_domain_by_mac_address.return_value + domain.snapshotCreateXML.assert_called_once_with( + 'foo') + + @mock.patch(DRIVER_PATH + '.LibvirtDriver._find_domain_by_mac_address') + def test__snapshot_suspend(self, mock__find_domain_by_mac_address): + self.driver._snapshot('52:54:00:f9:b8:f9', 'foo', suspend=True) + domain = mock__find_domain_by_mac_address.return_value + domain.assert_has_calls(( + mock.call.suspend(), + mock.call.snapshotCreateXML( + 'foo'), + mock.call.resume(), + )) + + @mock.patch(DRIVER_PATH + '.LibvirtDriver._find_domain_by_mac_address') + def test__revert(self, mock__find_domain_by_mac_address): + self.driver._revert('52:54:00:f9:b8:f9', 'foo', resume=False) + domain = mock__find_domain_by_mac_address.return_value + snapshot = domain.snapshotLookupByName.return_value + domain.snapshotLookupByName.assert_called_once_with('foo') + domain.revertToSnapshot.assert_called_once_with(snapshot) + self.assertFalse(domain.resume.called) + + @mock.patch(DRIVER_PATH + '.LibvirtDriver._find_domain_by_mac_address') + def test__revert_resume(self, mock__find_domain_by_mac_address): + self.driver._revert('52:54:00:f9:b8:f9', 'foo', resume=True) + domain = mock__find_domain_by_mac_address.return_value + snapshot = domain.snapshotLookupByName.return_value + domain.snapshotLookupByName.assert_called_once_with('foo') + domain.revertToSnapshot.assert_called_once_with(snapshot) + domain.resume.assert_called_once_with() + @mock.patch('os_faults.utils.run') @ddt.data('poweroff', 'poweron', 'reset') def test_driver_actions(self, action, mock_run): macs_list = ['52:54:00:f9:b8:f9', '52:54:00:ab:64:42'] getattr(self.driver, action)(macs_list) - mock_run.assert_called_once_with(getattr(self.driver, '_%s' % action), - macs_list) + mock_run.assert_called_once_with( + getattr(self.driver, '_%s' % action), + [{'mac_address': '52:54:00:f9:b8:f9'}, + {'mac_address': '52:54:00:ab:64:42'}]) + + @mock.patch('os_faults.utils.run') + def test_driver_snapshot(self, mock_run): + macs_list = ['52:54:00:f9:b8:f9', '52:54:00:ab:64:42'] + self.driver.snapshot(macs_list, 'foo_snap') + mock_run.assert_called_once_with( + self.driver._snapshot, + [{'mac_address': '52:54:00:f9:b8:f9', + 'snapshot_name': 'foo_snap', + 'suspend': True}, + {'mac_address': '52:54:00:ab:64:42', + 'snapshot_name': 'foo_snap', + 'suspend': True}]) + + @mock.patch('os_faults.utils.run') + def test_driver_revert(self, mock_run): + macs_list = ['52:54:00:f9:b8:f9', '52:54:00:ab:64:42'] + self.driver.revert(macs_list, 'foo_snap', resume=False) + mock_run.assert_called_once_with( + self.driver._revert, + [{'mac_address': '52:54:00:f9:b8:f9', + 'snapshot_name': 'foo_snap', + 'resume': False}, + {'mac_address': '52:54:00:ab:64:42', + 'snapshot_name': 'foo_snap', + 'resume': False}]) diff --git a/os_faults/tests/unit/test_utils.py b/os_faults/tests/unit/test_utils.py index cdc8031..16b6ba2 100644 --- a/os_faults/tests/unit/test_utils.py +++ b/os_faults/tests/unit/test_utils.py @@ -27,7 +27,7 @@ class UtilsTestCase(test.TestCase): def test_run(self): target = mock.Mock() - utils.run(target, ['01', '02']) + utils.run(target, [{'mac_address': '01'}, {'mac_address': '02'}]) target.assert_has_calls([mock.call(mac_address='01'), mock.call(mac_address='02')]) @@ -35,7 +35,8 @@ class UtilsTestCase(test.TestCase): target = mock.Mock() target.side_effect = MyException() self.assertRaises(error.PowerManagementError, - utils.run, target, ['01', '02']) + utils.run, target, [{'mac_address': '01'}, + {'mac_address': '02'}]) def test_start_thread(self): target = mock.Mock() diff --git a/os_faults/utils.py b/os_faults/utils.py index 263fb17..0dc9970 100644 --- a/os_faults/utils.py +++ b/os_faults/utils.py @@ -20,10 +20,10 @@ from os_faults.api import error LOG = logging.getLogger(__name__) -def run(target, mac_addresses_list): +def run(target, kwargs_list): tw = ThreadsWrapper(target) - for mac_address in mac_addresses_list: - tw.start_thread(mac_address=mac_address) + for kwargs in kwargs_list: + tw.start_thread(**kwargs) tw.join_threads() if tw.errors: