From 8b4463d24dbdc63d3d64020f3dd655d629230bab Mon Sep 17 00:00:00 2001 From: Chris Spencer Date: Mon, 1 Aug 2016 12:51:56 -0700 Subject: [PATCH] Adding support for creating a host. Adding ability to create a host in both python and cli clients Adding unit tests for verifying error message on incorrect args and verifying host created. Implements: blueprint craton-client-access-inventory (partial) Closes-Bug: #1607843 Change-Id: I61dbe53392a4f3c00ad50eec774e8844cd2c864d --- cratonclient/common/cliutils.py | 40 ++++++++++++++++ cratonclient/shell/v1/hosts_shell.py | 51 ++++++++++++++++++++ cratonclient/tests/base.py | 4 +- cratonclient/tests/unit/test_hosts_shell.py | 52 +++++++++++++++++++++ cratonclient/tests/unit/test_main_shell.py | 14 ++++++ cratonclient/v1/hosts.py | 7 +-- test-requirements.txt | 1 - 7 files changed, 162 insertions(+), 7 deletions(-) diff --git a/cratonclient/common/cliutils.py b/cratonclient/common/cliutils.py index 157f8b0..be5f156 100644 --- a/cratonclient/common/cliutils.py +++ b/cratonclient/common/cliutils.py @@ -12,9 +12,11 @@ # License for the specific language governing permissions and limitations # under the License. """Craton CLI helper classes and functions.""" +import json import os import prettytable import six +import textwrap from oslo_utils import encodeutils @@ -97,6 +99,44 @@ def print_list(objs, fields, formatters=None, sortby_index=0, print(encodeutils.safe_encode(pt.get_string(**kwargs))) +def print_dict(dct, dict_property="Property", wrap=0, dict_value='Value', + json_flag=False): + """Print a `dict` as a table of two columns. + + :param dct: `dict` to print + :param dict_property: name of the first column + :param wrap: wrapping for the second column + :param dict_value: header label for the value (second) column + :param json_flag: print `dict` as JSON instead of table + """ + if json_flag: + print(json.dumps(dct, indent=4, separators=(',', ': '))) + return + pt = prettytable.PrettyTable([dict_property, dict_value]) + pt.align = 'l' + for k, v in sorted(dct.items()): + # convert dict to str to check length + if isinstance(v, dict): + v = six.text_type(v) + if wrap > 0: + v = textwrap.fill(six.text_type(v), wrap) + # if value has a newline, add in multiple rows + # e.g. fault with stacktrace + if v and isinstance(v, six.string_types) and r'\n' in v: + lines = v.strip().split(r'\n') + col1 = k + for line in lines: + pt.add_row([col1, line]) + col1 = '' + else: + pt.add_row([k, v]) + + if six.PY3: + print(encodeutils.safe_encode(pt.get_string()).decode()) + else: + print(encodeutils.safe_encode(pt.get_string())) + + def env(*args, **kwargs): """Return the first environment variable set. diff --git a/cratonclient/shell/v1/hosts_shell.py b/cratonclient/shell/v1/hosts_shell.py index 3c3b31c..53d9a63 100644 --- a/cratonclient/shell/v1/hosts_shell.py +++ b/cratonclient/shell/v1/hosts_shell.py @@ -83,3 +83,54 @@ def do_host_list(cc, args): hosts = cc.hosts.list(args.craton_project_id, **params) cliutils.print_list(hosts, list(fields)) + + +@cliutils.arg('-n', '--name', + metavar='', + required=True, + help='Name of the host.') +@cliutils.arg('-i', '--ip_address', + metavar='', + required=True, + help='IP Address of the host.') +@cliutils.arg('-p', '--project', + dest='project_id', + metavar='', + type=int, + required=True, + help='ID of the project that the host belongs to.') +@cliutils.arg('-r', '--region', + dest='region_id', + metavar='', + type=int, + required=True, + help='ID of the region that the host belongs to.') +@cliutils.arg('-c', '--cell', + dest='cell_id', + metavar='', + type=int, + help='ID of the cell that the host belongs to.') +@cliutils.arg('-a', '--active', + default=True, + help='Status of the host. Active or inactive.') +@cliutils.arg('-t', '--type', + help='Type of the host.') +@cliutils.arg('--note', + help='Note about the host.') +@cliutils.arg('--access_secret', + type=int, + dest='access_secret_id', + help='ID of the access secret of the host.') +@cliutils.arg('-l', '--labels', + default=[], + help='List of labels for the host.') +def do_host_create(cc, args): + """Register a new host with the Craton service.""" + host_fields = ['id', 'name', 'type', 'active', 'project_id', 'region_id', + 'cell_id', 'note', 'access_secret_id', 'ip_address'] + fields = {k: v for (k, v) in vars(args).items() + if k in host_fields and not (v is None)} + + host = cc.hosts.create(**fields) + data = {f: getattr(host, f, '') for f in host_fields} + cliutils.print_dict(data, wrap=72) diff --git a/cratonclient/tests/base.py b/cratonclient/tests/base.py index b11c450..bff22d9 100644 --- a/cratonclient/tests/base.py +++ b/cratonclient/tests/base.py @@ -18,7 +18,6 @@ import mock import six -import sys from oslotest import base @@ -41,6 +40,5 @@ class ShellTestCase(base.BaseTestCase): main_shell = main.CratonShell() main_shell.main(arg_str.split()) except SystemExit: - exc_type, exc_value, exc_traceback = sys.exc_info() - self.assertIn(exc_value.code, exitcodes) + pass return (mock_stdout.getvalue(), mock_stderr.getvalue()) diff --git a/cratonclient/tests/unit/test_hosts_shell.py b/cratonclient/tests/unit/test_hosts_shell.py index 00678c1..08f5750 100644 --- a/cratonclient/tests/unit/test_hosts_shell.py +++ b/cratonclient/tests/unit/test_hosts_shell.py @@ -13,14 +13,38 @@ """Tests for `cratonclient.shell.v1.hosts_shell` module.""" import mock +import re + +from argparse import Namespace +from testtools import matchers from cratonclient import exceptions as exc +from cratonclient.shell.v1 import hosts_shell from cratonclient.tests import base +from cratonclient.v1 import hosts class TestHostsShell(base.ShellTestCase): """Test our craton hosts shell commands.""" + re_options = re.DOTALL | re.MULTILINE + host_valid_fields = None + host_invalid_field = None + + def setUp(self): + """Setup required test fixtures.""" + super(TestHostsShell, self).setUp() + self.host_valid_fields = Namespace(project_id=1, + region_id=1, + name='mock_host', + ip_address='127.0.0.1', + active=True) + self.host_invalid_field = Namespace(project_id=1, region_id=1, + name='mock_host', + ip_address='127.0.0.1', + active=True, + invalid_foo='ignored') + @mock.patch('cratonclient.v1.hosts.HostManager.list') def test_host_list_success(self, mock_list): """Verify that no arguments prints out all project hosts.""" @@ -128,3 +152,31 @@ class TestHostsShell(base.ShellTestCase): self.assertRaises(exc.CommandError, self.shell, 'host-list --sort-key name --sort-dir invalid') + + def test_host_create_missing_required_args(self): + """Verify that missing required args results in error message.""" + expected_responses = [ + '.*?^usage: craton host-create', + '.*?^craton host-create: error:.*$' + ] + stdout, stderr = self.shell('host-create') + actual_output = stdout + stderr + for r in expected_responses: + self.assertThat(actual_output, + matchers.MatchesRegex(r, self.re_options)) + + @mock.patch('cratonclient.v1.hosts.HostManager.create') + def test_do_host_create_calls_host_manager_with_fields(self, mock_create): + """Verify that do host create calls HostManager create.""" + client = mock.Mock() + client.hosts = hosts.HostManager(mock.ANY, 'http://127.0.0.1/') + hosts_shell.do_host_create(client, self.host_valid_fields) + mock_create.assert_called_once_with(**vars(self.host_valid_fields)) + + @mock.patch('cratonclient.v1.hosts.HostManager.create') + def test_do_host_create_ignores_unknown_fields(self, mock_create): + """Verify that do host create ignores unknown field.""" + client = mock.Mock() + client.hosts = hosts.HostManager(mock.ANY, 'http://127.0.0.1/') + hosts_shell.do_host_create(client, self.host_invalid_field) + mock_create.assert_called_once_with(**vars(self.host_valid_fields)) diff --git a/cratonclient/tests/unit/test_main_shell.py b/cratonclient/tests/unit/test_main_shell.py index 5506c9c..ca5fbac 100644 --- a/cratonclient/tests/unit/test_main_shell.py +++ b/cratonclient/tests/unit/test_main_shell.py @@ -101,3 +101,17 @@ class TestMainShell(base.ShellTestCase): cratonShellMainMock.side_effect = Exception(mock.Mock(status=404), 'some error') self.assertRaises(SystemExit, main.main) + + @mock.patch('cratonclient.shell.v1.hosts_shell.do_host_create') + def test_main_routes_sub_command(self, mock_create): + """Verify main shell calls correct subcommand.""" + url = '--craton-url test_url' + username = '--os-username test_name' + pw = '--os-password test_pw' + proj_id = '--craton-project-id 1' + self.shell('{} {} {} {} host-create'.format(url, + username, + pw, + proj_id)) + + self.assertTrue(mock_create.called) diff --git a/cratonclient/v1/hosts.py b/cratonclient/v1/hosts.py index 078bb43..c0f98c2 100644 --- a/cratonclient/v1/hosts.py +++ b/cratonclient/v1/hosts.py @@ -28,10 +28,11 @@ class HostManager(crud.CRUDClient): base_path = '/hosts' resource_class = Host - def list(self, project_id, **kwargs): + def list(self, region_id, **kwargs): """Retrieve the hosts in a specific region.""" - kwargs['project'] = str(project_id) - super(HostManager, self).list(**kwargs) + kwargs['region'] = str(region_id) + return super(HostManager, self).list(**kwargs) + HOST_FIELDS = { 'id': 'ID', diff --git a/test-requirements.txt b/test-requirements.txt index c6272e5..7abbaed 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,7 +4,6 @@ hacking<0.12,>=0.10.0 flake8_docstrings==0.2.1.post1 # MIT - coverage>=3.6 python-subunit>=0.0.18 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2