diff --git a/fuelclient/commands/node.py b/fuelclient/commands/node.py index 6844455..e9cf835 100644 --- a/fuelclient/commands/node.py +++ b/fuelclient/commands/node.py @@ -263,3 +263,44 @@ class NodeLabelDelete(NodeMixIn, base.BaseCommand): msg = "Labels have been deleted on nodes: {0} \n".format( ','.join(data)) self.app.stdout.write(msg) + + +class NodeAttributesDownload(NodeMixIn, base.BaseCommand): + """Download node attributes.""" + + def get_parser(self, prog_name): + parser = super(NodeAttributesDownload, self).get_parser(prog_name) + + parser.add_argument( + 'id', type=int, help='Node ID') + parser.add_argument( + '--dir', type=str, help='Directory to save attributes') + + return parser + + def take_action(self, parsed_args): + file_path = self.client.download_attributes( + parsed_args.id, parsed_args.dir) + self.app.stdout.write( + "Attributes for node {0} were written to {1}" + .format(parsed_args.id, file_path)) + + +class NodeAttributesUpload(NodeMixIn, base.BaseCommand): + """Upload node attributes.""" + + def get_parser(self, prog_name): + parser = super(NodeAttributesUpload, self).get_parser(prog_name) + + parser.add_argument( + 'id', type=int, help='Node ID') + parser.add_argument( + '--dir', type=str, help='Directory to read attributes from') + + return parser + + def take_action(self, parsed_args): + self.client.upload_attributes(parsed_args.id, parsed_args.dir) + self.app.stdout.write( + "Attributes for node {0} were uploaded." + .format(parsed_args.id)) diff --git a/fuelclient/tests/unit/v2/cli/test_node.py b/fuelclient/tests/unit/v2/cli/test_node.py index 1fb1b16..f8015cf 100644 --- a/fuelclient/tests/unit/v2/cli/test_node.py +++ b/fuelclient/tests/unit/v2/cli/test_node.py @@ -270,3 +270,19 @@ class TestNodeCommand(test_engine.BaseCLITest): self.m_get_client.assert_called_once_with('node', mock.ANY) self.m_client.delete_labels_for_nodes.assert_called_once_with( labels=labels_expected, node_ids=node_ids) + + def test_node_attributes_download(self): + args = 'node attributes-download 42' + + self.exec_command(args) + + self.m_get_client.assert_called_once_with('node', mock.ANY) + self.m_client.download_attributes.assert_called_once_with(42, None) + + def test_node_attributes_upload(self): + args = 'node attributes-upload 42' + + self.exec_command(args) + + self.m_get_client.assert_called_once_with('node', mock.ANY) + self.m_client.upload_attributes.assert_called_once_with(42, None) diff --git a/fuelclient/tests/unit/v2/lib/test_node.py b/fuelclient/tests/unit/v2/lib/test_node.py index 76c5c5b..6541f47 100644 --- a/fuelclient/tests/unit/v2/lib/test_node.py +++ b/fuelclient/tests/unit/v2/lib/test_node.py @@ -15,9 +15,11 @@ # under the License. import mock +import yaml import fuelclient from fuelclient.cli import error +from fuelclient.cli import serializers from fuelclient.objects import base as base_object from fuelclient.tests.unit.v2.lib import test_api from fuelclient.tests import utils @@ -334,3 +336,49 @@ class TestNodeFacade(test_api.BaseLibTest): node_id = 42 self.assertRaises(error.BadDataException, self.client.update, node_id, status=42) + + @mock.patch('fuelclient.objects.node.os.mkdir', mock.Mock()) + def test_node_attributes_download(self): + node_id = 42 + expected_uri = self.get_object_uri( + self.res_uri, node_id, '/attributes/') + fake_attributes = { + 'attribute_name': 'attribute_value' + } + + m_get = self.m_request.get(expected_uri, json=fake_attributes) + + m_open = mock.mock_open() + with mock.patch('fuelclient.cli.serializers.open', + m_open, create=True): + self.client.download_attributes(node_id, directory='/fake/dir') + + self.assertTrue(m_get.called) + m_open.assert_called_once_with( + '/fake/dir/node_{0}/attributes.yaml'.format(node_id), mock.ANY) + serializer = serializers.Serializer() + m_open().write.assert_called_once_with( + serializer.serialize(fake_attributes)) + + @mock.patch('fuelclient.objects.node.os.path.exists', + mock.Mock(return_value=True)) + def test_node_attribute_upload(self): + node_id = 42 + expected_uri = self.get_object_uri( + self.res_uri, node_id, '/attributes/') + fake_attributes = { + 'attribute_name': 'attribute_value' + } + + m_put = self.m_request.put(expected_uri, json=fake_attributes) + + m_open = mock.mock_open(read_data=yaml.safe_dump(fake_attributes)) + with mock.patch('fuelclient.cli.serializers.open', + m_open, create=True): + self.client.upload_attributes(node_id, directory='/fake/dir') + + self.assertTrue(m_put.called) + m_open.assert_called_once_with( + '/fake/dir/node_{0}/attributes.yaml'.format(node_id), mock.ANY) + self.assertEqual(m_put.last_request.json(), fake_attributes) + m_open().read.assert_called_once_with() diff --git a/fuelclient/v1/node.py b/fuelclient/v1/node.py index b6a7e4e..3784d28 100644 --- a/fuelclient/v1/node.py +++ b/fuelclient/v1/node.py @@ -162,6 +162,17 @@ class NodeClient(base_v1.BaseV1Client): return data_to_return + def download_attributes(self, node_id, directory=None): + node = self._entity_wrapper(node_id) + attributes = node.get_node_attributes() + return node.write_attribute( + 'attributes', attributes, directory=directory) + + def upload_attributes(self, node_id, directory=None): + node = self._entity_wrapper(node_id) + attributes = node.read_attribute('attributes', directory=directory) + node.update_node_attributes(attributes) + def _check_label(self, labels, item): checking_list = [] diff --git a/setup.cfg b/setup.cfg index f6b10aa..8c0ca7e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,6 +47,8 @@ fuelclient = network-template_delete=fuelclient.commands.network_template:NetworkTemplateDelete network-template_download=fuelclient.commands.network_template:NetworkTemplateDownload network-template_upload=fuelclient.commands.network_template:NetworkTemplateUpload + node_attributes-download=fuelclient.commands.node:NodeAttributesDownload + node_attributes-upload=fuelclient.commands.node:NodeAttributesUpload node_create-vms-conf=fuelclient.commands.node:NodeCreateVMsConf node_label_delete=fuelclient.commands.node:NodeLabelDelete node_label_list=fuelclient.commands.node:NodeLabelList