diff --git a/requirements.txt b/requirements.txt index c0a25f569..cf25ff199 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ pbr>=0.5.21,<1.0 +jsonpatch os-client-config>=0.7.0 six diff --git a/shade/__init__.py b/shade/__init__.py index 7cceea2e6..070c5cad3 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -24,6 +24,7 @@ import glanceclient import glanceclient.exc from ironicclient import client as ironic_client from ironicclient import exceptions as ironic_exceptions +import jsonpatch from keystoneclient import auth as ksc_auth from keystoneclient import session as ksc_session from keystoneclient import client as keystone_client @@ -2294,6 +2295,100 @@ class OperatorCloud(OpenStackCloud): "Error updating machine via patch operation. node: %s. " "%s" % (name_or_id, e)) + def update_machine(self, name_or_id, chassis_uuid=None, driver=None, + driver_info=None, name=None, instance_info=None, + instance_uuid=None, properties=None): + """Update a machine with new configuration information + + A user-friendly method to perform updates of a machine, in whole or + part. + + :param string name_or_id: A machine name or UUID to be updated. + :param string chassis_uuid: Assign a chassis UUID to the machine. + NOTE: As of the Kilo release, this value + cannot be changed once set. If a user + attempts to change this value, then the + Ironic API, as of Kilo, will reject the + request. + :param string driver: The driver name for controlling the machine. + :param dict driver_info: The dictonary defining the configuration + that the driver will utilize to control + the machine. Permutations of this are + dependent upon the specific driver utilized. + :param string name: A human relatable name to represent the machine. + :param dict instance_info: A dictonary of configuration information + that conveys to the driver how the host + is to be configured when deployed. + be deployed to the machine. + :param string instance_uuid: A UUID value representing the instance + that the deployed machine represents. + :param dict properties: A dictonary defining the properties of a + machine. + + :raises: OpenStackCloudException on operation error. + + :returns: Dictonary containing a machine sub-dictonary consisting + of the updated data returned from the API update operation, + and a list named changes which contains all of the API paths + that received updates. + """ + try: + machine = self.get_machine(name_or_id) + + machine_config = {} + new_config = {} + + if chassis_uuid: + machine_config['chassis_uuid'] = machine['chassis_uuid'] + new_config['chassis_uuid'] = chassis_uuid + + if driver: + machine_config['driver'] = machine['driver'] + new_config['driver'] = driver + + if driver_info: + machine_config['driver_info'] = machine['driver_info'] + new_config['driver_info'] = driver_info + + if name: + machine_config['name'] = machine['name'] + new_config['name'] = name + + if instance_info: + machine_config['instance_info'] = machine['instance_info'] + new_config['instance_info'] = instance_info + + if instance_uuid: + machine_config['instance_uuid'] = machine['instance_uuid'] + new_config['instance_uuid'] = instance_uuid + + if properties: + machine_config['properties'] = machine['properties'] + new_config['properties'] = properties + + patch = jsonpatch.JsonPatch.from_diff(machine_config, new_config) + + if not patch: + return dict( + node=machine, + changes=None + ) + else: + machine = self.patch_machine(machine['uuid'], list(patch)) + change_list = [] + for change in list(patch): + change_list.append(change['path']) + return dict( + node=machine, + changes=change_list + ) + except Exception as e: + self.log.debug( + "Machine update failed", exc_info=True) + raise OpenStackCloudException( + "Error updating machine node %s. " + "%s" % (name_or_id, e)) + def validate_node(self, uuid): try: ifaces = self.ironic_client.node.validate(uuid) diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index 98f49b520..4d436e617 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -211,6 +211,250 @@ class TestShadeOperator(base.TestCase): self.cloud.patch_machine(node_id, patch) self.assertTrue(mock_client.node.update.called) + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_no_action(self, mock_patch, mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + name = 'node01' + + expected_machine = dict( + uuid='00000000-0000-0000-0000-000000000000', + name='node01' + ) + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine('node01') + self.assertIsNone(update_dict['changes']) + self.assertFalse(mock_patch.called) + self.assertDictEqual(expected_machine, update_dict['node']) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_no_action_name(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + name = 'node01' + + expected_machine = dict( + uuid='00000000-0000-0000-0000-000000000000', + name='node01' + ) + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine('node01', name='node01') + self.assertIsNone(update_dict['changes']) + self.assertFalse(mock_patch.called) + self.assertDictEqual(expected_machine, update_dict['node']) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_action_name(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + name = 'evil' + + expected_patch = [dict(op='replace', path='/name', value='good')] + + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine('evil', name='good') + self.assertIsNotNone(update_dict['changes']) + self.assertEqual('/name', update_dict['changes'][0]) + self.assertTrue(mock_patch.called) + mock_patch.assert_called_with( + '00000000-0000-0000-0000-000000000000', + expected_patch) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_update_name(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + name = 'evil' + + expected_patch = [dict(op='replace', path='/name', value='good')] + + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine('evil', name='good') + self.assertIsNotNone(update_dict['changes']) + self.assertEqual('/name', update_dict['changes'][0]) + self.assertTrue(mock_patch.called) + mock_patch.assert_called_with( + '00000000-0000-0000-0000-000000000000', + expected_patch) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_update_chassis_uuid(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + chassis_uuid = None + + expected_patch = [ + dict( + op='replace', + path='/chassis_uuid', + value='00000000-0000-0000-0000-000000000001' + )] + + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine( + '00000000-0000-0000-0000-000000000000', + chassis_uuid='00000000-0000-0000-0000-000000000001') + self.assertIsNotNone(update_dict['changes']) + self.assertEqual('/chassis_uuid', update_dict['changes'][0]) + self.assertTrue(mock_patch.called) + mock_patch.assert_called_with( + '00000000-0000-0000-0000-000000000000', + expected_patch) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_update_driver(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + driver = None + + expected_patch = [ + dict( + op='replace', + path='/driver', + value='fake' + )] + + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine( + '00000000-0000-0000-0000-000000000000', + driver='fake' + ) + self.assertIsNotNone(update_dict['changes']) + self.assertEqual('/driver', update_dict['changes'][0]) + self.assertTrue(mock_patch.called) + mock_patch.assert_called_with( + '00000000-0000-0000-0000-000000000000', + expected_patch) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_update_driver_info(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + driver_info = None + + expected_patch = [ + dict( + op='replace', + path='/driver_info', + value=dict(var='fake') + )] + + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine( + '00000000-0000-0000-0000-000000000000', + driver_info=dict(var="fake") + ) + self.assertIsNotNone(update_dict['changes']) + self.assertEqual('/driver_info', update_dict['changes'][0]) + self.assertTrue(mock_patch.called) + mock_patch.assert_called_with( + '00000000-0000-0000-0000-000000000000', + expected_patch) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_update_instance_info(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + instance_info = None + + expected_patch = [ + dict( + op='replace', + path='/instance_info', + value=dict(var='fake') + )] + + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine( + '00000000-0000-0000-0000-000000000000', + instance_info=dict(var="fake") + ) + self.assertIsNotNone(update_dict['changes']) + self.assertEqual('/instance_info', update_dict['changes'][0]) + self.assertTrue(mock_patch.called) + mock_patch.assert_called_with( + '00000000-0000-0000-0000-000000000000', + expected_patch) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_update_instance_uuid(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + instance_uuid = None + + expected_patch = [ + dict( + op='replace', + path='/instance_uuid', + value='00000000-0000-0000-0000-000000000002' + )] + + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine( + '00000000-0000-0000-0000-000000000000', + instance_uuid='00000000-0000-0000-0000-000000000002' + ) + self.assertIsNotNone(update_dict['changes']) + self.assertEqual('/instance_uuid', update_dict['changes'][0]) + self.assertTrue(mock_patch.called) + mock_patch.assert_called_with( + '00000000-0000-0000-0000-000000000000', + expected_patch) + + @mock.patch.object(shade.OperatorCloud, 'ironic_client') + @mock.patch.object(shade.OperatorCloud, 'patch_machine') + def test_update_machine_patch_update_properties(self, mock_patch, + mock_client): + class client_return_value: + uuid = '00000000-0000-0000-0000-000000000000' + properties = None + + expected_patch = [ + dict( + op='replace', + path='/properties', + value=dict(var='fake') + )] + + mock_client.node.get.return_value = client_return_value + + update_dict = self.cloud.update_machine( + '00000000-0000-0000-0000-000000000000', + properties=dict(var="fake") + ) + self.assertIsNotNone(update_dict['changes']) + self.assertEqual('/properties', update_dict['changes'][0]) + self.assertTrue(mock_patch.called) + mock_patch.assert_called_with( + '00000000-0000-0000-0000-000000000000', + expected_patch) + @mock.patch.object(shade.OperatorCloud, 'ironic_client') def test_register_machine(self, mock_client): class fake_node: