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
This commit is contained in:
Chris Spencer 2016-08-01 12:51:56 -07:00 committed by Ian Cordasco
parent 052caea612
commit 8b4463d24d
7 changed files with 162 additions and 7 deletions

View File

@ -12,9 +12,11 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
"""Craton CLI helper classes and functions.""" """Craton CLI helper classes and functions."""
import json
import os import os
import prettytable import prettytable
import six import six
import textwrap
from oslo_utils import encodeutils 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))) 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): def env(*args, **kwargs):
"""Return the first environment variable set. """Return the first environment variable set.

View File

@ -83,3 +83,54 @@ def do_host_list(cc, args):
hosts = cc.hosts.list(args.craton_project_id, **params) hosts = cc.hosts.list(args.craton_project_id, **params)
cliutils.print_list(hosts, list(fields)) cliutils.print_list(hosts, list(fields))
@cliutils.arg('-n', '--name',
metavar='<name>',
required=True,
help='Name of the host.')
@cliutils.arg('-i', '--ip_address',
metavar='<ipaddress>',
required=True,
help='IP Address of the host.')
@cliutils.arg('-p', '--project',
dest='project_id',
metavar='<project>',
type=int,
required=True,
help='ID of the project that the host belongs to.')
@cliutils.arg('-r', '--region',
dest='region_id',
metavar='<region>',
type=int,
required=True,
help='ID of the region that the host belongs to.')
@cliutils.arg('-c', '--cell',
dest='cell_id',
metavar='<cell>',
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)

View File

@ -18,7 +18,6 @@
import mock import mock
import six import six
import sys
from oslotest import base from oslotest import base
@ -41,6 +40,5 @@ class ShellTestCase(base.BaseTestCase):
main_shell = main.CratonShell() main_shell = main.CratonShell()
main_shell.main(arg_str.split()) main_shell.main(arg_str.split())
except SystemExit: except SystemExit:
exc_type, exc_value, exc_traceback = sys.exc_info() pass
self.assertIn(exc_value.code, exitcodes)
return (mock_stdout.getvalue(), mock_stderr.getvalue()) return (mock_stdout.getvalue(), mock_stderr.getvalue())

View File

@ -13,14 +13,38 @@
"""Tests for `cratonclient.shell.v1.hosts_shell` module.""" """Tests for `cratonclient.shell.v1.hosts_shell` module."""
import mock import mock
import re
from argparse import Namespace
from testtools import matchers
from cratonclient import exceptions as exc from cratonclient import exceptions as exc
from cratonclient.shell.v1 import hosts_shell
from cratonclient.tests import base from cratonclient.tests import base
from cratonclient.v1 import hosts
class TestHostsShell(base.ShellTestCase): class TestHostsShell(base.ShellTestCase):
"""Test our craton hosts shell commands.""" """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') @mock.patch('cratonclient.v1.hosts.HostManager.list')
def test_host_list_success(self, mock_list): def test_host_list_success(self, mock_list):
"""Verify that no arguments prints out all project hosts.""" """Verify that no arguments prints out all project hosts."""
@ -128,3 +152,31 @@ class TestHostsShell(base.ShellTestCase):
self.assertRaises(exc.CommandError, self.assertRaises(exc.CommandError,
self.shell, self.shell,
'host-list --sort-key name --sort-dir invalid') '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))

View File

@ -101,3 +101,17 @@ class TestMainShell(base.ShellTestCase):
cratonShellMainMock.side_effect = Exception(mock.Mock(status=404), cratonShellMainMock.side_effect = Exception(mock.Mock(status=404),
'some error') 'some error')
self.assertRaises(SystemExit, main.main) 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)

View File

@ -28,10 +28,11 @@ class HostManager(crud.CRUDClient):
base_path = '/hosts' base_path = '/hosts'
resource_class = Host resource_class = Host
def list(self, project_id, **kwargs): def list(self, region_id, **kwargs):
"""Retrieve the hosts in a specific region.""" """Retrieve the hosts in a specific region."""
kwargs['project'] = str(project_id) kwargs['region'] = str(region_id)
super(HostManager, self).list(**kwargs) return super(HostManager, self).list(**kwargs)
HOST_FIELDS = { HOST_FIELDS = {
'id': 'ID', 'id': 'ID',

View File

@ -4,7 +4,6 @@
hacking<0.12,>=0.10.0 hacking<0.12,>=0.10.0
flake8_docstrings==0.2.1.post1 # MIT flake8_docstrings==0.2.1.post1 # MIT
coverage>=3.6 coverage>=3.6
python-subunit>=0.0.18 python-subunit>=0.0.18
sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2