From 51a6f5f7331c7c0675b4f7c8eaf959c4e99e636c Mon Sep 17 00:00:00 2001 From: Alexander Saprykin Date: Fri, 25 Mar 2016 11:42:04 +0200 Subject: [PATCH] Add multinode support to openstack config CLIv2 * Option --node accepts list of node ids. * Improve client library, make many parameters optional. * Add unit tests for openstack config library code. Related-Bug: #1557462 Change-Id: Ib6e6ab3cbba6fb28cdec5738267d7aab354dce9f (cherry-picked from 78a01f5aa1a5453f909fae6414860d7957e6d95a) --- fuelclient/commands/openstack_config.py | 16 +- fuelclient/objects/openstack_config.py | 5 + .../unit/v2/cli/test_openstack_config.py | 16 +- .../unit/v2/lib/test_openstack_config.py | 137 ++++++++++++++++++ fuelclient/v1/openstack_config.py | 16 +- 5 files changed, 167 insertions(+), 23 deletions(-) create mode 100644 fuelclient/tests/unit/v2/lib/test_openstack_config.py diff --git a/fuelclient/commands/openstack_config.py b/fuelclient/commands/openstack_config.py index 3d948741..fc10ae37 100644 --- a/fuelclient/commands/openstack_config.py +++ b/fuelclient/commands/openstack_config.py @@ -41,10 +41,10 @@ class OpenstackConfigMixin(object): ) @staticmethod - def add_node_id_arg(parser): + def add_node_ids_arg(parser): parser.add_argument( '-n', '--node', - type=int, default=None, help='Node ID.' + type=int, nargs='+', default=None, help='Node IDs.' ) @staticmethod @@ -81,7 +81,7 @@ class OpenstackConfigList(OpenstackConfigMixin, base.BaseCommand): parser = super(OpenstackConfigList, self).get_parser(prog_name) self.add_env_arg(parser) - self.add_node_id_arg(parser) + self.add_node_ids_arg(parser) self.add_node_role_arg(parser) self.add_deleted_arg(parser) @@ -89,7 +89,7 @@ class OpenstackConfigList(OpenstackConfigMixin, base.BaseCommand): def take_action(self, args): data = self.client.get_filtered( - cluster_id=args.env, node_id=args.node, + cluster_id=args.env, node_ids=args.node, node_role=args.role, is_active=(not args.deleted)) data = data_utils.get_display_data_multi(self.columns, data) @@ -124,7 +124,7 @@ class OpenstackConfigUpload(OpenstackConfigMixin, base.BaseCommand): parser = super(OpenstackConfigUpload, self).get_parser(prog_name) self.add_env_arg(parser) - self.add_node_id_arg(parser) + self.add_node_ids_arg(parser) self.add_node_role_arg(parser) self.add_file_arg(parser) @@ -133,7 +133,7 @@ class OpenstackConfigUpload(OpenstackConfigMixin, base.BaseCommand): def take_action(self, args): config = self.client.upload( path=args.file, cluster_id=args.env, - node_id=args.node, node_role=args.role) + node_ids=args.node, node_role=args.role) msg = "OpenStack configuration with id {0} " \ "uploaded from file '{0}'\n".format(config.id, args.file) @@ -148,7 +148,7 @@ class OpenstackConfigExecute(OpenstackConfigMixin, base.BaseCommand): parser = super(OpenstackConfigExecute, self).get_parser(prog_name) self.add_env_arg(parser) - self.add_node_id_arg(parser) + self.add_node_ids_arg(parser) self.add_node_role_arg(parser) self.add_force_arg(parser) @@ -156,7 +156,7 @@ class OpenstackConfigExecute(OpenstackConfigMixin, base.BaseCommand): def take_action(self, args): self.client.execute( - cluster_id=args.env, node_id=args.node, node_role=args.role, + cluster_id=args.env, node_ids=args.node, node_role=args.role, force=args.force) msg = "OpenStack configuration execution started.\n" diff --git a/fuelclient/objects/openstack_config.py b/fuelclient/objects/openstack_config.py index e1ac3b2c..c5683546 100644 --- a/fuelclient/objects/openstack_config.py +++ b/fuelclient/objects/openstack_config.py @@ -50,6 +50,11 @@ class OpenstackConfig(BaseObject): def get_filtered_data(cls, **kwargs): url = cls.class_api_path params = cls._prepare_params(kwargs) + + node_ids = params.get('node_ids') + if node_ids is not None: + params['node_ids'] = ','.join([str(n) for n in node_ids]) + return cls.connection.get_request(url, params=params) @classmethod diff --git a/fuelclient/tests/unit/v2/cli/test_openstack_config.py b/fuelclient/tests/unit/v2/cli/test_openstack_config.py index 673a3ef5..33658409 100644 --- a/fuelclient/tests/unit/v2/cli/test_openstack_config.py +++ b/fuelclient/tests/unit/v2/cli/test_openstack_config.py @@ -36,21 +36,21 @@ class TestOpenstackConfig(test_engine.BaseCLITest): cmd_line='--env {0} --node {1}'.format(self.CLUSTER_ID, self.NODE_ID), expected_kwargs={'cluster_id': self.CLUSTER_ID, - 'node_id': self.NODE_ID, 'node_role': None, + 'node_ids': [self.NODE_ID], 'node_role': None, 'is_active': True} ) def test_config_list_for_role(self): self._test_config_list( cmd_line='--env {0} --role compute'.format(self.CLUSTER_ID), - expected_kwargs={'cluster_id': self.CLUSTER_ID, 'node_id': None, + expected_kwargs={'cluster_id': self.CLUSTER_ID, 'node_ids': None, 'node_role': 'compute', 'is_active': True} ) def test_config_list_for_cluster(self): self._test_config_list( cmd_line='--env {0}'.format(self.CLUSTER_ID), - expected_kwargs={'cluster_id': self.CLUSTER_ID, 'node_id': None, + expected_kwargs={'cluster_id': self.CLUSTER_ID, 'node_ids': None, 'node_role': None, 'is_active': True} ) @@ -71,7 +71,7 @@ class TestOpenstackConfig(test_engine.BaseCLITest): self.m_get_client.assert_called_once_with('openstack-config', mock.ANY) self.m_client.upload.assert_called_once_with( path='config.yaml', cluster_id=self.CLUSTER_ID, - node_id=self.NODE_ID, node_role=None) + node_ids=[self.NODE_ID], node_role=None) @mock.patch('sys.stderr') def test_config_upload_fail(self, mocked_stderr): @@ -111,8 +111,8 @@ class TestOpenstackConfig(test_engine.BaseCLITest): self.m_get_client.assert_called_once_with('openstack-config', mock.ANY) self.m_client.execute.assert_called_once_with( - cluster_id=self.CLUSTER_ID, node_id=self.NODE_ID, node_role=None, - force=False) + cluster_id=self.CLUSTER_ID, node_ids=[self.NODE_ID], + node_role=None, force=False) def test_config_force_execute(self): cmd = 'openstack-config execute --env {0} --node {1} --force' \ @@ -121,5 +121,5 @@ class TestOpenstackConfig(test_engine.BaseCLITest): self.m_get_client.assert_called_once_with('openstack-config', mock.ANY) self.m_client.execute.assert_called_once_with( - cluster_id=self.CLUSTER_ID, node_id=self.NODE_ID, node_role=None, - force=True) + cluster_id=self.CLUSTER_ID, node_ids=[self.NODE_ID], + node_role=None, force=True) diff --git a/fuelclient/tests/unit/v2/lib/test_openstack_config.py b/fuelclient/tests/unit/v2/lib/test_openstack_config.py new file mode 100644 index 00000000..7780ec88 --- /dev/null +++ b/fuelclient/tests/unit/v2/lib/test_openstack_config.py @@ -0,0 +1,137 @@ +# Copyright 2016 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, 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. + +import mock +import yaml + +import fuelclient +from fuelclient.tests.unit.v2.lib import test_api +from fuelclient.tests import utils + + +class TestOpenstackConfigClient(test_api.BaseLibTest): + + def setUp(self): + super(TestOpenstackConfigClient, self).setUp() + + self.version = 'v1' + self.uri = '/api/{version}/openstack-config/'.format( + version=self.version) + + self.client = fuelclient.get_client('openstack-config', self.version) + + def test_config_list_for_cluster(self): + cluster_id = 1 + fake_configs = [ + utils.get_fake_openstack_config(cluster_id=cluster_id) + ] + + uri = self.uri + '?cluster_id={0}&is_active=True'.format(cluster_id) + m_get = self.m_request.get(uri, complete_qs=True, json=fake_configs) + data = self.client.get_filtered(cluster_id=1) + + self.assertTrue(m_get.called) + self.assertEqual(data[0]['cluster_id'], cluster_id) + + def test_config_list_for_node(self): + cluster_id = 1 + fake_configs = [ + utils.get_fake_openstack_config( + cluster_id=cluster_id, node_id=22), + ] + + uri = self.uri + '?cluster_id={0}&node_ids=22' \ + '&is_active=True'.format(cluster_id) + m_get = self.m_request.get(uri, complete_qs=True, json=fake_configs) + data = self.client.get_filtered(cluster_id=1, node_ids=[22]) + + self.assertTrue(m_get.called) + self.assertEqual(data[0]['cluster_id'], cluster_id) + + def test_config_list_for_multinode(self): + cluster_id = 1 + fake_configs = [ + utils.get_fake_openstack_config( + cluster_id=cluster_id, node_id=22), + utils.get_fake_openstack_config( + cluster_id=cluster_id, node_id=44), + ] + + uri = self.uri + '?cluster_id={0}&node_ids=22,44' \ + '&is_active=True'.format(cluster_id) + m_get = self.m_request.get(uri, complete_qs=True, json=fake_configs) + data = self.client.get_filtered(cluster_id=1, node_ids=[22, 44]) + + self.assertTrue(m_get.called) + self.assertEqual(data[0]['cluster_id'], cluster_id) + + def test_config_download(self): + config_id = 42 + uri = self.uri + '{0}/'.format(42) + fake_config = utils.get_fake_openstack_config(id=config_id) + + m_get = self.m_request.get(uri, json=fake_config) + + m_open = mock.mock_open() + with mock.patch('fuelclient.cli.serializers.open', + m_open, create=True): + self.client.download(config_id, '/path/to/config') + + self.assertTrue(m_get.called) + written_yaml = yaml.safe_load(m_open().write.mock_calls[0][1][0]) + self.assertEqual(written_yaml, + {'configuration': fake_config['configuration']}) + + def test_config_upload(self): + cluster_id = 1 + fake_config = utils.get_fake_openstack_config( + cluster_id=cluster_id) + + m_post = self.m_request.post(self.uri, json=fake_config) + + m_open = mock.mock_open(read_data=yaml.safe_dump({ + 'configuration': fake_config['configuration'] + })) + with mock.patch( + 'fuelclient.cli.serializers.open', m_open, create=True), \ + mock.patch('os.path.exists', mock.Mock(return_value=True)): + self.client.upload('/path/to/config', cluster_id) + + self.assertTrue(m_post.called) + body = m_post.last_request.json() + self.assertEqual(body['cluster_id'], cluster_id) + self.assertNotIn('node_ids', body) + self.assertNotIn('node_role', body) + + def test_config_upload_multinode(self): + cluster_id = 1 + fake_config = utils.get_fake_openstack_config( + cluster_id=cluster_id) + + m_post = self.m_request.post(self.uri, json={'id': 42}) + + m_open = mock.mock_open(read_data=yaml.safe_dump({ + 'configuration': fake_config['configuration'] + })) + with mock.patch( + 'fuelclient.cli.serializers.open', m_open, create=True), \ + mock.patch('os.path.exists', mock.Mock(return_value=True)): + self.client.upload( + '/path/to/config', cluster_id, node_ids=[42, 44]) + + self.assertTrue(m_post.called) + body = m_post.last_request.json() + self.assertEqual(body['cluster_id'], cluster_id) + self.assertEqual(body['node_ids'], [42, 44]) + self.assertNotIn('node_role', body) diff --git a/fuelclient/v1/openstack_config.py b/fuelclient/v1/openstack_config.py index 7fb13d94..0bd589a2 100644 --- a/fuelclient/v1/openstack_config.py +++ b/fuelclient/v1/openstack_config.py @@ -20,11 +20,11 @@ class OpenstackConfigClient(base_v1.BaseV1Client): _entity_wrapper = objects.OpenstackConfig - def upload(self, path, cluster_id, node_id=None, node_role=None): + def upload(self, path, cluster_id, node_ids=None, node_role=None): data = self._entity_wrapper.read_file(path) return self._entity_wrapper.create( cluster_id=cluster_id, configuration=data['configuration'], - node_id=node_id, node_role=node_role) + node_ids=node_ids, node_role=node_role) def download(self, config_id, path): config = self._entity_wrapper(config_id) @@ -33,15 +33,17 @@ class OpenstackConfigClient(base_v1.BaseV1Client): return path - def execute(self, config_id, cluster_id, node_id, node_role, force=False): + def execute(self, cluster_id, config_id=None, node_ids=None, + node_role=None, force=False): return self._entity_wrapper.execute( - config_id=config_id, cluster_id=cluster_id, node_id=node_id, + cluster_id=cluster_id, config_id=config_id, node_ids=node_ids, node_role=node_role, force=force) - def get_filtered(self, cluster_id, node_id, node_role, is_active=True): + def get_filtered(self, cluster_id, node_ids=None, + node_role=None, is_active=True): return self._entity_wrapper.get_filtered_data( - cluster_id=cluster_id, node_id=node_id, node_role=node_role, - is_active=is_active) + cluster_id=cluster_id, node_ids=node_ids, + node_role=node_role, is_active=is_active) def get_client():