diff --git a/cratonclient/shell/v1/cells_shell.py b/cratonclient/shell/v1/cells_shell.py index 1acec3c..39e07c5 100644 --- a/cratonclient/shell/v1/cells_shell.py +++ b/cratonclient/shell/v1/cells_shell.py @@ -35,6 +35,10 @@ def do_cell_show(cc, args): type=int, required=True, help='ID of the region that the cell belongs to.') +@cliutils.arg('--cloud', + metavar='', + type=int, + help='ID of the cloud that the cell belongs to.') @cliutils.arg('--detail', action='store_true', default=False, @@ -72,6 +76,8 @@ def do_cell_list(cc, args): """Print list of cells which are registered with the Craton service.""" params = {} default_fields = ['id', 'name'] + if args.cloud is not None: + params['cloud_id'] = args.cloud if args.limit is not None: if args.limit < 0: raise exc.CommandError('Invalid limit specified. Expected ' @@ -124,6 +130,12 @@ def do_cell_list(cc, args): type=int, required=True, help='ID of the region that the cell belongs to.') +@cliutils.arg('--cloud', + dest='cloud_id', + metavar='', + type=int, + required=True, + help='ID of the cloud that the cell belongs to.') @cliutils.arg('--note', help='Note about the cell.') def do_cell_create(cc, args): @@ -147,6 +159,11 @@ def do_cell_create(cc, args): metavar='', type=int, help='Desired ID of the region that the cell should change to.') +@cliutils.arg('--cloud', + dest='cloud_id', + metavar='', + type=int, + help='Desired ID of the cloud that the cell should change to.') @cliutils.arg('--note', help='Note about the cell.') def do_cell_update(cc, args): @@ -157,7 +174,7 @@ def do_cell_update(cc, args): if not fields: raise exc.CommandError( 'Nothing to update... Please specify one of --name, --region, ' - 'or --note' + '--cloud, or --note' ) cell = cc.cells.update(cell_id, **fields) data = {f: getattr(cell, f, '') for f in cells.CELL_FIELDS} diff --git a/cratonclient/shell/v1/clouds_shell.py b/cratonclient/shell/v1/clouds_shell.py new file mode 100644 index 0000000..d542ca3 --- /dev/null +++ b/cratonclient/shell/v1/clouds_shell.py @@ -0,0 +1,135 @@ +# 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. +"""Hosts resource and resource shell wrapper.""" +from __future__ import print_function + +from cratonclient.common import cliutils +from cratonclient import exceptions as exc +from cratonclient.v1 import clouds + + +@cliutils.arg('-n', '--name', + metavar='', + required=True, + help='Name of the host.') +@cliutils.arg('--note', + help='Note about the host.') +def do_cloud_create(cc, args): + """Register a new cloud with the Craton service.""" + fields = {k: v for (k, v) in vars(args).items() + if k in clouds.CLOUD_FIELDS and not (v is None)} + + cloud = cc.clouds.create(**fields) + data = {f: getattr(cloud, f, '') for f in clouds.CLOUD_FIELDS} + cliutils.print_dict(data, wrap=72) + + +@cliutils.arg('--fields', + nargs='+', + metavar='', + default=[], + help='Comma-separated list of fields to display. ' + 'Only these fields will be fetched from the server. ' + 'Can not be used when "--detail" is specified') +@cliutils.arg('--all', + action='store_true', + default=False, + help='Retrieve and show all clouds. This will override ' + 'the provided value for --limit and automatically ' + 'retrieve each page of results.') +@cliutils.arg('--limit', + metavar='', + type=int, + help='Maximum number of clouds to return.') +@cliutils.arg('--marker', + metavar='', + default=None, + help='ID of the cell to use to resume listing clouds.') +def do_cloud_list(cc, args): + """List all clouds.""" + params = {} + default_fields = ['id', 'name'] + if args.limit is not None: + if args.limit < 0: + raise exc.CommandError('Invalid limit specified. Expected ' + 'non-negative limit, got {0}' + .format(args.limit)) + params['limit'] = args.limit + if args.all is True: + params['limit'] = 100 + + if args.fields: + try: + fields = {x: clouds.CLOUD_FIELDS[x] for x in args.fields} + except KeyError as err: + raise exc.CommandError('Invalid field "{}"'.format(err.args[0])) + else: + fields = default_fields + params['marker'] = args.marker + params['autopaginate'] = args.all + + clouds_list = cc.clouds.list(**params) + cliutils.print_list(clouds_list, list(fields)) + + +@cliutils.arg('id', + metavar='', + type=int, + help='ID of the cloud.') +def do_cloud_show(cc, args): + """Show detailed information about a cloud.""" + cloud = cc.clouds.get(args.id) + data = {f: getattr(cloud, f, '') for f in clouds.CLOUD_FIELDS} + cliutils.print_dict(data, wrap=72) + + +@cliutils.arg('id', + metavar='', + type=int, + help='ID of the cloud') +@cliutils.arg('-n', '--name', + metavar='', + help='Name of the cloud.') +@cliutils.arg('--note', + help='Note about the cloud.') +def do_cloud_update(cc, args): + """Update a cloud that is registered with the Craton service.""" + fields = {k: v for (k, v) in vars(args).items() + if k in clouds.CLOUD_FIELDS and not (v is None)} + item_id = fields.pop('id') + if not fields: + raise exc.CommandError( + 'Nothing to update... Please specify one or more of --name, or ' + '--note' + ) + cloud = cc.clouds.update(item_id, **fields) + data = {f: getattr(cloud, f, '') for f in clouds.CLOUD_FIELDS} + cliutils.print_dict(data, wrap=72) + + +@cliutils.arg('id', + metavar='', + type=int, + help='ID of the cloud.') +def do_cloud_delete(cc, args): + """Delete a cloud that is registered with the Craton service.""" + try: + response = cc.clouds.delete(args.id) + except exc.ClientException as client_exc: + raise exc.CommandError( + 'Failed to delete cloud {} due to "{}:{}"'.format( + args.id, client_exc.__class__, str(client_exc), + ) + ) + else: + print("Cloud {0} was {1} deleted.". + format(args.id, 'successfully' if response else 'not')) diff --git a/cratonclient/shell/v1/hosts_shell.py b/cratonclient/shell/v1/hosts_shell.py index c6edd89..f69541b 100644 --- a/cratonclient/shell/v1/hosts_shell.py +++ b/cratonclient/shell/v1/hosts_shell.py @@ -35,6 +35,10 @@ def do_host_show(cc, args): type=int, required=True, help='ID of the region that the host belongs to.') +@cliutils.arg('--cloud', + metavar='', + type=int, + help='ID of the cloud that the host belongs to.') @cliutils.arg('-c', '--cell', metavar='', type=int, @@ -79,6 +83,8 @@ def do_host_list(cc, args): default_fields = ['id', 'name', 'device_type', 'active', 'cell_id'] if args.cell is not None: params['cell_id'] = args.cell + if args.cloud is not None: + params['cloud_id'] = args.cloud if args.limit is not None: if args.limit < 0: raise exc.CommandError('Invalid limit specified. Expected ' @@ -134,6 +140,12 @@ def do_host_list(cc, args): type=int, required=True, help='ID of the region that the host belongs to.') +@cliutils.arg('--cloud', + dest='cloud_id', + metavar='', + type=int, + required=True, + help='ID of the cloud that the host belongs to.') @cliutils.arg('-c', '--cell', dest='cell_id', metavar='', @@ -176,6 +188,11 @@ def do_host_create(cc, args): metavar='', type=int, help='Desired ID of the region that the host should change to.') +@cliutils.arg('--cloud', + dest='cloud_id', + metavar='', + type=int, + help='Desired ID of the cloud that the host should change to.') @cliutils.arg('-c', '--cell', dest='cell_id', metavar='', diff --git a/cratonclient/shell/v1/regions_shell.py b/cratonclient/shell/v1/regions_shell.py index c537de3..0b8f902 100644 --- a/cratonclient/shell/v1/regions_shell.py +++ b/cratonclient/shell/v1/regions_shell.py @@ -21,6 +21,12 @@ from cratonclient.v1 import regions metavar='', required=True, help='Name of the host.') +@cliutils.arg('--cloud', + dest='cloud_id', + metavar='', + type=int, + required=True, + help='ID of the cloud that the region belongs to.') @cliutils.arg('--note', help='Note about the host.') def do_region_create(cc, args): @@ -33,6 +39,10 @@ def do_region_create(cc, args): cliutils.print_dict(data, wrap=72) +@cliutils.arg('--cloud', + metavar='', + type=int, + help='ID of the cloud that the region belongs to.') @cliutils.arg('--fields', nargs='+', metavar='', @@ -53,11 +63,13 @@ def do_region_create(cc, args): @cliutils.arg('--marker', metavar='', default=None, - help='ID of the cell to use to resume listing regions.') + help='ID of the region to use to resume listing regions.') def do_region_list(cc, args): """List all regions.""" params = {} default_fields = ['id', 'name'] + if args.cloud is not None: + params['cloud_id'] = args.cloud if args.limit is not None: if args.limit < 0: raise exc.CommandError('Invalid limit specified. Expected ' @@ -74,6 +86,7 @@ def do_region_list(cc, args): raise exc.CommandError('Invalid field "{}"'.format(err.args[0])) else: fields = default_fields + params['marker'] = args.marker params['autopaginate'] = args.all @@ -99,6 +112,11 @@ def do_region_show(cc, args): @cliutils.arg('-n', '--name', metavar='', help='Name of the region.') +@cliutils.arg('--cloud', + dest='cloud_id', + metavar='', + type=int, + help='Desired ID of the cloud that the region should change to.') @cliutils.arg('--note', help='Note about the region.') def do_region_update(cc, args): @@ -108,8 +126,8 @@ def do_region_update(cc, args): item_id = fields.pop('id') if not fields: raise exc.CommandError( - 'Nothing to update... Please specify one or more of --name, or ' - '--note' + 'Nothing to update... Please specify one or more of --name, ' + '--cloud, or --note' ) region = cc.regions.update(item_id, **fields) data = {f: getattr(region, f, '') for f in regions.REGION_FIELDS} diff --git a/cratonclient/shell/v1/shell.py b/cratonclient/shell/v1/shell.py index 39136cb..e78e351 100644 --- a/cratonclient/shell/v1/shell.py +++ b/cratonclient/shell/v1/shell.py @@ -11,6 +11,7 @@ # limitations under the License. """Command-line interface to the OpenStack Craton API V1.""" from cratonclient.shell.v1 import cells_shell +from cratonclient.shell.v1 import clouds_shell from cratonclient.shell.v1 import hosts_shell from cratonclient.shell.v1 import projects_shell from cratonclient.shell.v1 import regions_shell @@ -19,6 +20,7 @@ from cratonclient.shell.v1 import regions_shell COMMAND_MODULES = [ # TODO(cmspence): project_shell, cell_shell, device_shell, user_shell, etc. projects_shell, + clouds_shell, regions_shell, hosts_shell, cells_shell, diff --git a/cratonclient/tests/integration/test_clouds_shell.py b/cratonclient/tests/integration/test_clouds_shell.py new file mode 100644 index 0000000..b852ba8 --- /dev/null +++ b/cratonclient/tests/integration/test_clouds_shell.py @@ -0,0 +1,153 @@ +# 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. +"""Tests for `cratonclient.shell.v1.clouds_shell` module.""" +import mock +import re + +from argparse import Namespace +from testtools import matchers + +from cratonclient.shell.v1 import clouds_shell +from cratonclient.tests.integration import base +from cratonclient.v1 import clouds + + +class TestCloudsShell(base.ShellTestCase): + """Test craton clouds shell commands.""" + + re_options = re.DOTALL | re.MULTILINE + cloud_valid_fields = None + cloud_invalid_field = None + + def setUp(self): + """Setup required test fixtures.""" + super(TestCloudsShell, self).setUp() + self.cloud_valid_fields = Namespace(project_id=1, + id=1, + name='mock_cloud') + self.cloud_invalid_field = Namespace(project_id=1, + id=1, + name='mock_cloud', + invalid_foo='ignored') + + def test_cloud_create_missing_required_args(self): + """Verify that missing required args results in error message.""" + expected_responses = [ + '.*?^usage: craton cloud-create', + '.*?^craton cloud-create: error:.*$' + ] + stdout, stderr = self.shell('cloud-create') + actual_output = stdout + stderr + for r in expected_responses: + self.assertThat(actual_output, + matchers.MatchesRegex(r, self.re_options)) + + @mock.patch('cratonclient.v1.clouds.CloudManager.create') + def test_do_cloud_create_calls_cloud_manager(self, mock_create): + """Verify that do cloud create calls CloudManager create.""" + client = mock.Mock() + session = mock.Mock() + session.project_id = 1 + client.clouds = clouds.CloudManager(session, 'http://127.0.0.1/') + clouds_shell.do_cloud_create(client, self.cloud_valid_fields) + mock_create.assert_called_once_with(**vars(self.cloud_valid_fields)) + + @mock.patch('cratonclient.v1.clouds.CloudManager.create') + def test_do_cloud_create_ignores_unknown_fields(self, mock_create): + """Verify that do cloud create ignores unknown field.""" + client = mock.Mock() + session = mock.Mock() + session.project_id = 1 + client.clouds = clouds.CloudManager(session, 'http://127.0.0.1/') + clouds_shell.do_cloud_create(client, self.cloud_invalid_field) + mock_create.assert_called_once_with(**vars(self.cloud_valid_fields)) + + def test_cloud_show_missing_required_args(self): + """Verify that missing required args results in error message.""" + expected_responses = [ + '.*?^usage: craton cloud-show', + '.*?^craton cloud-show: error:.*$', + ] + stdout, stderr = self.shell('cloud-show') + actual_output = stdout + stderr + for r in expected_responses: + self.assertThat(actual_output, + matchers.MatchesRegex(r, self.re_options)) + + @mock.patch('cratonclient.v1.clouds.CloudManager.get') + def test_do_cloud_show_calls_cloud_manager_with_fields(self, mock_get): + """Verify that do cloud show calls CloudManager get.""" + client = mock.Mock() + session = mock.Mock() + session.project_id = 1 + client.clouds = clouds.CloudManager(session, 'http://127.0.0.1/') + test_args = Namespace(id=1) + clouds_shell.do_cloud_show(client, test_args) + mock_get.assert_called_once_with(vars(test_args)['id']) + + def test_cloud_delete_missing_required_args(self): + """Verify that missing required args results in error message.""" + expected_responses = [ + '.*?^usage: craton cloud-delete', + '.*?^craton cloud-delete: error:.*$', + ] + stdout, stderr = self.shell('cloud-delete') + for r in expected_responses: + self.assertThat((stdout + stderr), + matchers.MatchesRegex(r, self.re_options)) + + @mock.patch('cratonclient.v1.clouds.CloudManager.delete') + def test_do_cloud_delete_calls_cloud_manager(self, mock_delete): + """Verify that do cloud delete calls CloudManager delete.""" + client = mock.Mock() + session = mock.Mock() + session.project_id = 1 + client.clouds = clouds.CloudManager(session, 'http://127.0.0.1/') + test_args = Namespace(id=1) + clouds_shell.do_cloud_delete(client, test_args) + mock_delete.assert_called_once_with(vars(test_args)['id']) + + def test_cloud_update_missing_required_args(self): + """Verify that missing required args results in error message.""" + expected_responses = [ + '.*?^usage: craton cloud-update', + '.*?^craton cloud-update: error:.*$', + ] + stdout, stderr = self.shell('cloud-update') + for r in expected_responses: + self.assertThat((stdout + stderr), + matchers.MatchesRegex(r, self.re_options)) + + @mock.patch('cratonclient.v1.clouds.CloudManager.update') + def test_do_cloud_update_calls_cloud_manager(self, mock_update): + """Verify that do cloud update calls CloudManager update.""" + client = mock.Mock() + session = mock.Mock() + session.project_id = 1 + client.clouds = clouds.CloudManager(session, 'http://127.0.0.1/') + valid_input = Namespace(id=1, + name='mock_cloud') + clouds_shell.do_cloud_update(client, valid_input) + mock_update.assert_called_once_with(1, name='mock_cloud') + + @mock.patch('cratonclient.v1.clouds.CloudManager.update') + def test_do_cloud_update_ignores_unknown_fields(self, mock_update): + """Verify that do cloud update ignores unknown field.""" + client = mock.Mock() + session = mock.Mock() + session.project_id = 1 + client.clouds = clouds.CloudManager(session, 'http://127.0.0.1/') + invalid_input = Namespace(id=1, + name='mock_cloud', + invalid=True) + clouds_shell.do_cloud_update(client, invalid_input) + mock_update.assert_called_once_with(1, name='mock_cloud') diff --git a/cratonclient/tests/unit/shell/v1/test_cells_shell.py b/cratonclient/tests/unit/shell/v1/test_cells_shell.py index 4d447f7..3e2c854 100644 --- a/cratonclient/tests/unit/shell/v1/test_cells_shell.py +++ b/cratonclient/tests/unit/shell/v1/test_cells_shell.py @@ -50,6 +50,7 @@ class TestDoCellList(base.TestShellCommandUsingPrintList): def args_for(self, **kwargs): """Generate the default argument list for cell-list.""" kwargs.setdefault('region', 123) + kwargs.setdefault('cloud', None) kwargs.setdefault('detail', False) kwargs.setdefault('limit', None) kwargs.setdefault('sort_key', None) @@ -75,6 +76,23 @@ class TestDoCellList(base.TestShellCommandUsingPrintList): self.assertEqual(['id', 'name'], sorted(self.print_list.call_args[0][-1])) + def test_with_cloud_id(self): + """Verify the behaviour of do_cell_list with mostly default values.""" + args = self.args_for(cloud=456) + + cells_shell.do_cell_list(self.craton_client, args) + + self.craton_client.cells.list.assert_called_once_with( + sort_dir='asc', + cloud_id=456, + region_id=123, + autopaginate=False, + marker=None, + ) + self.assertTrue(self.print_list.called) + self.assertEqual(['id', 'name'], + sorted(self.print_list.call_args[0][-1])) + def test_negative_limit(self): """Ensure we raise an exception for negative limits.""" args = self.args_for(limit=-1) diff --git a/cratonclient/tests/unit/shell/v1/test_clouds_shell.py b/cratonclient/tests/unit/shell/v1/test_clouds_shell.py new file mode 100644 index 0000000..7ca4bdc --- /dev/null +++ b/cratonclient/tests/unit/shell/v1/test_clouds_shell.py @@ -0,0 +1,290 @@ +# -*- coding: utf-8 -*- + +# 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. +"""Tests for the shell functions for the clouds resource.""" +import mock + +from cratonclient import exceptions +from cratonclient.shell.v1 import clouds_shell +from cratonclient.tests.unit.shell import base +from cratonclient.v1 import clouds + + +class TestDoCloudShow(base.TestShellCommandUsingPrintDict): + """Unit tests for the cloud-show command.""" + + def test_prints_cloud_data(self): + """Verify we print the data for the cloud.""" + args = self.args_for(id=1234) + + clouds_shell.do_cloud_show(self.craton_client, args) + + self.craton_client.clouds.get.assert_called_once_with(1234) + self.print_dict.assert_called_once_with( + {field: mock.ANY for field in clouds.CLOUD_FIELDS}, + wrap=72, + ) + + +class TestDoCloudCreate(base.TestShellCommandUsingPrintDict): + """Unit tests for the cloud-create command.""" + + def args_for(self, **kwargs): + """Generate arguments for cloud-create.""" + kwargs.setdefault('name', 'New cloud') + kwargs.setdefault('note', None) + return super(TestDoCloudCreate, self).args_for(**kwargs) + + def test_accepts_only_required_arguments(self): + """Verify operation with only --name provided.""" + args = self.args_for() + + clouds_shell.do_cloud_create(self.craton_client, args) + + self.craton_client.clouds.create.assert_called_once_with( + name='New cloud', + ) + self.print_dict.assert_called_once_with( + {field: mock.ANY for field in clouds.CLOUD_FIELDS}, + wrap=72, + ) + + def test_accepts_optional_arguments(self): + """Verify operation with --note passed as well.""" + args = self.args_for(note='This is a note') + + clouds_shell.do_cloud_create(self.craton_client, args) + + self.craton_client.clouds.create.assert_called_once_with( + name='New cloud', + note='This is a note', + ) + self.print_dict.assert_called_once_with( + {field: mock.ANY for field in clouds.CLOUD_FIELDS}, + wrap=72, + ) + + +class TestDoCloudUpdate(base.TestShellCommandUsingPrintDict): + """Unit tests for cloud-update command.""" + + def args_for(self, **kwargs): + """Generate arguments for cloud-update.""" + kwargs.setdefault('id', 12345) + kwargs.setdefault('name', None) + kwargs.setdefault('note', None) + return super(TestDoCloudUpdate, self).args_for(**kwargs) + + def test_nothing_to_update_raises_error(self): + """Verify specifying nothing raises a CommandError.""" + args = self.args_for() + + self.assertRaisesCommandErrorWith( + clouds_shell.do_cloud_update, + args, + ) + self.assertFalse(self.craton_client.clouds.update.called) + self.assertFalse(self.print_dict.called) + + def test_name_is_updated(self): + """Verify the name attribute update is sent.""" + args = self.args_for(name='A New Name') + + clouds_shell.do_cloud_update(self.craton_client, args) + + self.craton_client.clouds.update.assert_called_once_with( + 12345, + name='A New Name', + ) + self.print_dict.assert_called_once_with( + {field: mock.ANY for field in clouds.CLOUD_FIELDS}, + wrap=72, + ) + + def test_note_is_updated(self): + """Verify the note attribute is updated.""" + args = self.args_for(note='A New Note') + + clouds_shell.do_cloud_update(self.craton_client, args) + + self.craton_client.clouds.update.assert_called_once_with( + 12345, + note='A New Note', + ) + self.print_dict.assert_called_once_with( + {field: mock.ANY for field in clouds.CLOUD_FIELDS}, + wrap=72, + ) + + def test_everything_is_updated(self): + """Verify the note and name are updated.""" + args = self.args_for( + note='A New Note', + name='A New Name', + ) + + clouds_shell.do_cloud_update(self.craton_client, args) + + self.craton_client.clouds.update.assert_called_once_with( + 12345, + note='A New Note', + name='A New Name', + ) + self.print_dict.assert_called_once_with( + {field: mock.ANY for field in clouds.CLOUD_FIELDS}, + wrap=72, + ) + + +class TestDoCloudDelete(base.TestShellCommand): + """Unit tests for the cloud-delete command.""" + + def setUp(self): + """Mock the print function.""" + super(TestDoCloudDelete, self).setUp() + self.print_mock = mock.patch( + 'cratonclient.shell.v1.clouds_shell.print' + ) + self.print_func = self.print_mock.start() + + def tearDown(self): + """Clean up our print function mock.""" + super(TestDoCloudDelete, self).tearDown() + self.print_mock.stop() + + def args_for(self, **kwargs): + """Generate args for the cloud-delete command.""" + kwargs.setdefault('id', 123456) + return super(TestDoCloudDelete, self).args_for(**kwargs) + + def test_successful(self): + """Verify successful deletion.""" + self.craton_client.clouds.delete.return_value = True + args = self.args_for() + + clouds_shell.do_cloud_delete(self.craton_client, args) + + self.craton_client.clouds.delete.assert_called_once_with(123456) + self.print_func.assert_called_once_with( + 'Cloud 123456 was successfully deleted.' + ) + + def test_failed(self): + """Verify failed deletion.""" + self.craton_client.clouds.delete.return_value = False + args = self.args_for() + + clouds_shell.do_cloud_delete(self.craton_client, args) + + self.craton_client.clouds.delete.assert_called_once_with(123456) + self.print_func.assert_called_once_with( + 'Cloud 123456 was not deleted.' + ) + + def test_failed_with_exception(self): + """Verify we raise a CommandError on client exceptions.""" + self.craton_client.clouds.delete.side_effect = exceptions.NotFound + args = self.args_for() + + self.assertRaisesCommandErrorWith(clouds_shell.do_cloud_delete, args) + + self.craton_client.clouds.delete.assert_called_once_with(123456) + self.assertFalse(self.print_func.called) + + +class TestDoCloudList(base.TestShellCommandUsingPrintList): + """Test cloud-list command.""" + + def args_for(self, **kwargs): + """Generate the default argument list for cloud-list.""" + kwargs.setdefault('detail', False) + kwargs.setdefault('limit', None) + kwargs.setdefault('fields', []) + kwargs.setdefault('marker', None) + kwargs.setdefault('all', False) + return super(TestDoCloudList, self).args_for(**kwargs) + + def test_with_defaults(self): + """Test cloud-list with default values.""" + args = self.args_for() + clouds_shell.do_cloud_list(self.craton_client, args) + + self.assertTrue(self.print_list.called) + self.assertEqual(['id', 'name'], + sorted(self.print_list.call_args[0][-1])) + + def test_negative_limit(self): + """Ensure we raise an exception for negative limits.""" + args = self.args_for(limit=-1) + self.assertRaisesCommandErrorWith(clouds_shell.do_cloud_list, args) + + def test_positive_limit(self): + """Verify that we pass positive limits to the call to list.""" + args = self.args_for(limit=5) + clouds_shell.do_cloud_list(self.craton_client, args) + self.craton_client.clouds.list.assert_called_once_with( + limit=5, + marker=None, + autopaginate=False, + ) + self.assertTrue(self.print_list.called) + self.assertEqual(['id', 'name'], + sorted(self.print_list.call_args[0][-1])) + + def test_fields(self): + """Verify that we print out specific fields.""" + args = self.args_for(fields=['id', 'name', 'note']) + clouds_shell.do_cloud_list(self.craton_client, args) + self.assertEqual(['id', 'name', 'note'], + sorted(self.print_list.call_args[0][-1])) + + def test_invalid_fields(self): + """Verify that we error out with invalid fields.""" + args = self.args_for(fields=['uuid', 'not-name', 'nate']) + self.assertRaisesCommandErrorWith(clouds_shell.do_cloud_list, args) + self.assertNothingWasCalled() + + def test_autopagination(self): + """Verify autopagination is controlled by --all.""" + args = self.args_for(all=True) + + clouds_shell.do_cloud_list(self.craton_client, args) + + self.craton_client.clouds.list.assert_called_once_with( + limit=100, + marker=None, + autopaginate=True, + ) + + def test_autopagination_overrides_limit(self): + """Verify --all overrides --limit.""" + args = self.args_for(all=True, limit=35) + + clouds_shell.do_cloud_list(self.craton_client, args) + + self.craton_client.clouds.list.assert_called_once_with( + limit=100, + marker=None, + autopaginate=True, + ) + + def test_marker_pass_through(self): + """Verify we pass our marker through to the client.""" + args = self.args_for(marker=31) + + clouds_shell.do_cloud_list(self.craton_client, args) + + self.craton_client.clouds.list.assert_called_once_with( + marker=31, + autopaginate=False, + ) diff --git a/cratonclient/tests/unit/shell/v1/test_hosts_shell.py b/cratonclient/tests/unit/shell/v1/test_hosts_shell.py index 2d2da2e..e5af751 100644 --- a/cratonclient/tests/unit/shell/v1/test_hosts_shell.py +++ b/cratonclient/tests/unit/shell/v1/test_hosts_shell.py @@ -44,6 +44,7 @@ class TestDoHostList(base.TestShellCommandUsingPrintList): def args_for(self, **kwargs): """Generate a Namespace for do_host_list.""" + kwargs.setdefault('cloud', None) kwargs.setdefault('region', 246) kwargs.setdefault('cell', None) kwargs.setdefault('detail', False) @@ -88,6 +89,23 @@ class TestDoHostList(base.TestShellCommandUsingPrintList): 'active', 'cell_id', 'device_type', 'id', 'name', ]) + def test_with_cloud_id(self): + """Verify that we include the cell_id in the params.""" + args = self.args_for(cloud=123) + + hosts_shell.do_host_list(self.craton_client, args) + + self.craton_client.hosts.list.assert_called_once_with( + sort_dir='asc', + cloud_id=123, + region_id=246, + autopaginate=False, + marker=None, + ) + self.assertSortedPrintListFieldsEqualTo([ + 'active', 'cell_id', 'device_type', 'id', 'name', + ]) + def test_with_detail(self): """Verify the behaviour of specifying --detail.""" args = self.args_for(detail=True) @@ -104,6 +122,7 @@ class TestDoHostList(base.TestShellCommandUsingPrintList): self.assertSortedPrintListFieldsEqualTo([ 'active', 'cell_id', + 'cloud_id', 'created_at', 'device_type', 'id', diff --git a/cratonclient/tests/unit/shell/v1/test_regions_shell.py b/cratonclient/tests/unit/shell/v1/test_regions_shell.py index e45371e..df3f7b8 100644 --- a/cratonclient/tests/unit/shell/v1/test_regions_shell.py +++ b/cratonclient/tests/unit/shell/v1/test_regions_shell.py @@ -42,6 +42,7 @@ class TestDoRegionCreate(base.TestShellCommandUsingPrintDict): def args_for(self, **kwargs): """Generate arguments for region-create.""" kwargs.setdefault('name', 'New region') + kwargs.setdefault('cloud_id', 1) kwargs.setdefault('note', None) return super(TestDoRegionCreate, self).args_for(**kwargs) @@ -53,6 +54,7 @@ class TestDoRegionCreate(base.TestShellCommandUsingPrintDict): self.craton_client.regions.create.assert_called_once_with( name='New region', + cloud_id=1, ) self.print_dict.assert_called_once_with( {field: mock.ANY for field in regions.REGION_FIELDS}, @@ -67,6 +69,7 @@ class TestDoRegionCreate(base.TestShellCommandUsingPrintDict): self.craton_client.regions.create.assert_called_once_with( name='New region', + cloud_id=1, note='This is a note', ) self.print_dict.assert_called_once_with( @@ -81,6 +84,7 @@ class TestDoRegionUpdate(base.TestShellCommandUsingPrintDict): def args_for(self, **kwargs): """Generate arguments for region-update.""" kwargs.setdefault('id', 12345) + kwargs.setdefault('cloud_id', None) kwargs.setdefault('name', None) kwargs.setdefault('note', None) return super(TestDoRegionUpdate, self).args_for(**kwargs) @@ -131,6 +135,7 @@ class TestDoRegionUpdate(base.TestShellCommandUsingPrintDict): args = self.args_for( note='A New Note', name='A New Name', + cloud_id=2, ) regions_shell.do_region_update(self.craton_client, args) @@ -139,6 +144,7 @@ class TestDoRegionUpdate(base.TestShellCommandUsingPrintDict): 12345, note='A New Note', name='A New Name', + cloud_id=2, ) self.print_dict.assert_called_once_with( {field: mock.ANY for field in regions.REGION_FIELDS}, @@ -208,6 +214,7 @@ class TestDoRegionList(base.TestShellCommandUsingPrintList): def args_for(self, **kwargs): """Generate the default argument list for region-list.""" kwargs.setdefault('detail', False) + kwargs.setdefault('cloud', None) kwargs.setdefault('limit', None) kwargs.setdefault('fields', []) kwargs.setdefault('marker', None) @@ -223,6 +230,19 @@ class TestDoRegionList(base.TestShellCommandUsingPrintList): self.assertEqual(['id', 'name'], sorted(self.print_list.call_args[0][-1])) + def test_with_cloud_id(self): + """Test region-list with default values.""" + args = self.args_for(cloud=123) + regions_shell.do_region_list(self.craton_client, args) + self.craton_client.regions.list.assert_called_once_with( + cloud_id=123, + marker=None, + autopaginate=False, + ) + self.assertTrue(self.print_list.called) + self.assertEqual(['id', 'name'], + sorted(self.print_list.call_args[0][-1])) + def test_negative_limit(self): """Ensure we raise an exception for negative limits.""" args = self.args_for(limit=-1) diff --git a/cratonclient/tests/unit/v1/test_clouds.py b/cratonclient/tests/unit/v1/test_clouds.py new file mode 100644 index 0000000..6e97fa2 --- /dev/null +++ b/cratonclient/tests/unit/v1/test_clouds.py @@ -0,0 +1,38 @@ +# 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. +"""Tests for `cratonclient.v1.clouds` module.""" + +from cratonclient import crud +from cratonclient.tests import base +from cratonclient.v1 import clouds + +import mock + + +class TestCloud(base.TestCase): + """Tests for the Cloud Resource.""" + + def test_is_a_resource_instance(self): + """Verify that a Cloud instance is an instance of a Resource.""" + manager = mock.Mock() + self.assertIsInstance(clouds.Cloud(manager, {}), + crud.Resource) + + +class TestCloudManager(base.TestCase): + """Tests for the CloudManager class.""" + + def test_is_a_crudclient(self): + """Verify our CloudManager is a CRUDClient.""" + session = mock.Mock() + cloud_mgr = clouds.CloudManager(session, '') + self.assertIsInstance(cloud_mgr, crud.CRUDClient) diff --git a/cratonclient/v1/cells.py b/cratonclient/v1/cells.py index 6a40a22..6382b75 100644 --- a/cratonclient/v1/cells.py +++ b/cratonclient/v1/cells.py @@ -33,6 +33,7 @@ CELL_FIELDS = { 'id': 'ID', 'region_id': 'Region ID', 'project_id': 'Project ID', + 'cloud_id': 'Cloud ID', 'name': 'Name', 'note': 'Note', 'created_at': 'Created At', diff --git a/cratonclient/v1/client.py b/cratonclient/v1/client.py index 085bd64..89cea19 100644 --- a/cratonclient/v1/client.py +++ b/cratonclient/v1/client.py @@ -13,6 +13,7 @@ # under the License. """Top-level client for version 1 of Craton's API.""" from cratonclient.v1 import cells +from cratonclient.v1 import clouds from cratonclient.v1 import hosts from cratonclient.v1 import projects from cratonclient.v1 import regions @@ -42,4 +43,5 @@ class Client(object): self.hosts = hosts.HostManager(**manager_kwargs) self.cells = cells.CellManager(**manager_kwargs) self.projects = projects.ProjectManager(**manager_kwargs) + self.clouds = clouds.CloudManager(**manager_kwargs) self.regions = regions.RegionManager(**manager_kwargs) diff --git a/cratonclient/v1/clouds.py b/cratonclient/v1/clouds.py new file mode 100644 index 0000000..ab705f3 --- /dev/null +++ b/cratonclient/v1/clouds.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- + +# 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. +"""Clouds manager code.""" +from cratonclient import crud + + +class Cloud(crud.Resource): + """Representation of a Cloud.""" + + pass + + +class CloudManager(crud.CRUDClient): + """A manager for clouds.""" + + key = 'cloud' + base_path = '/clouds' + resource_class = Cloud + +CLOUD_FIELDS = { + 'id': 'ID', + 'project_id': 'Project ID', + 'name': 'Name', + 'note': 'Note', + 'created_at': 'Created At', + 'updated_at': 'Updated At' +} diff --git a/cratonclient/v1/hosts.py b/cratonclient/v1/hosts.py index 51f15fd..309d55c 100644 --- a/cratonclient/v1/hosts.py +++ b/cratonclient/v1/hosts.py @@ -34,6 +34,7 @@ HOST_FIELDS = { 'name': 'Name', 'device_type': 'Device Type', 'project_id': 'Project ID', + 'cloud_id': 'Cloud ID', 'region_id': 'Region ID', 'cell_id': 'Cell ID', 'ip_address': 'IP Address', diff --git a/cratonclient/v1/regions.py b/cratonclient/v1/regions.py index ae5c9f1..eddef3a 100644 --- a/cratonclient/v1/regions.py +++ b/cratonclient/v1/regions.py @@ -32,6 +32,7 @@ class RegionManager(crud.CRUDClient): REGION_FIELDS = { 'id': 'ID', 'project_id': 'Project ID', + 'cloud_id': 'Cloud ID', 'name': 'Name', 'note': 'Note', 'created_at': 'Created At',