Adds basic Cloud resource CRUD to CLI

This patch adds basic CRUD operations for
the Cloud resource in the Craton API and
integrates cloud_id throughout dependent
child resources.

Change-Id: If33c13082e31ee9f76278926538c707c559e41d0
Closes-Bug: 1666947
Depends-On: Ib1c16a504430760f2a7234221f91429a7ea72596
This commit is contained in:
Thomas Maddox
2017-02-22 19:41:52 +00:00
parent 2f9e474b4d
commit a2787af05b
16 changed files with 774 additions and 4 deletions

View File

@@ -35,6 +35,10 @@ def do_cell_show(cc, args):
type=int, type=int,
required=True, required=True,
help='ID of the region that the cell belongs to.') help='ID of the region that the cell belongs to.')
@cliutils.arg('--cloud',
metavar='<cloud>',
type=int,
help='ID of the cloud that the cell belongs to.')
@cliutils.arg('--detail', @cliutils.arg('--detail',
action='store_true', action='store_true',
default=False, default=False,
@@ -72,6 +76,8 @@ def do_cell_list(cc, args):
"""Print list of cells which are registered with the Craton service.""" """Print list of cells which are registered with the Craton service."""
params = {} params = {}
default_fields = ['id', 'name'] default_fields = ['id', 'name']
if args.cloud is not None:
params['cloud_id'] = args.cloud
if args.limit is not None: if args.limit is not None:
if args.limit < 0: if args.limit < 0:
raise exc.CommandError('Invalid limit specified. Expected ' raise exc.CommandError('Invalid limit specified. Expected '
@@ -124,6 +130,12 @@ def do_cell_list(cc, args):
type=int, type=int,
required=True, required=True,
help='ID of the region that the cell belongs to.') help='ID of the region that the cell belongs to.')
@cliutils.arg('--cloud',
dest='cloud_id',
metavar='<cloud>',
type=int,
required=True,
help='ID of the cloud that the cell belongs to.')
@cliutils.arg('--note', @cliutils.arg('--note',
help='Note about the cell.') help='Note about the cell.')
def do_cell_create(cc, args): def do_cell_create(cc, args):
@@ -147,6 +159,11 @@ def do_cell_create(cc, args):
metavar='<region>', metavar='<region>',
type=int, type=int,
help='Desired ID of the region that the cell should change to.') help='Desired ID of the region that the cell should change to.')
@cliutils.arg('--cloud',
dest='cloud_id',
metavar='<cloud>',
type=int,
help='Desired ID of the cloud that the cell should change to.')
@cliutils.arg('--note', @cliutils.arg('--note',
help='Note about the cell.') help='Note about the cell.')
def do_cell_update(cc, args): def do_cell_update(cc, args):
@@ -157,7 +174,7 @@ def do_cell_update(cc, args):
if not fields: if not fields:
raise exc.CommandError( raise exc.CommandError(
'Nothing to update... Please specify one of --name, --region, ' 'Nothing to update... Please specify one of --name, --region, '
'or --note' '--cloud, or --note'
) )
cell = cc.cells.update(cell_id, **fields) cell = cc.cells.update(cell_id, **fields)
data = {f: getattr(cell, f, '') for f in cells.CELL_FIELDS} data = {f: getattr(cell, f, '') for f in cells.CELL_FIELDS}

View File

@@ -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='<name>',
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='<fields>',
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='<limit>',
type=int,
help='Maximum number of clouds to return.')
@cliutils.arg('--marker',
metavar='<marker>',
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='<cloud>',
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='<cloud>',
type=int,
help='ID of the cloud')
@cliutils.arg('-n', '--name',
metavar='<name>',
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='<cloud>',
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'))

View File

@@ -35,6 +35,10 @@ def do_host_show(cc, args):
type=int, type=int,
required=True, required=True,
help='ID of the region that the host belongs to.') help='ID of the region that the host belongs to.')
@cliutils.arg('--cloud',
metavar='<cloud>',
type=int,
help='ID of the cloud that the host belongs to.')
@cliutils.arg('-c', '--cell', @cliutils.arg('-c', '--cell',
metavar='<cell>', metavar='<cell>',
type=int, type=int,
@@ -79,6 +83,8 @@ def do_host_list(cc, args):
default_fields = ['id', 'name', 'device_type', 'active', 'cell_id'] default_fields = ['id', 'name', 'device_type', 'active', 'cell_id']
if args.cell is not None: if args.cell is not None:
params['cell_id'] = args.cell 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 is not None:
if args.limit < 0: if args.limit < 0:
raise exc.CommandError('Invalid limit specified. Expected ' raise exc.CommandError('Invalid limit specified. Expected '
@@ -134,6 +140,12 @@ def do_host_list(cc, args):
type=int, type=int,
required=True, required=True,
help='ID of the region that the host belongs to.') help='ID of the region that the host belongs to.')
@cliutils.arg('--cloud',
dest='cloud_id',
metavar='<cloud>',
type=int,
required=True,
help='ID of the cloud that the host belongs to.')
@cliutils.arg('-c', '--cell', @cliutils.arg('-c', '--cell',
dest='cell_id', dest='cell_id',
metavar='<cell>', metavar='<cell>',
@@ -176,6 +188,11 @@ def do_host_create(cc, args):
metavar='<region>', metavar='<region>',
type=int, type=int,
help='Desired ID of the region that the host should change to.') help='Desired ID of the region that the host should change to.')
@cliutils.arg('--cloud',
dest='cloud_id',
metavar='<cloud>',
type=int,
help='Desired ID of the cloud that the host should change to.')
@cliutils.arg('-c', '--cell', @cliutils.arg('-c', '--cell',
dest='cell_id', dest='cell_id',
metavar='<cell>', metavar='<cell>',

View File

@@ -21,6 +21,12 @@ from cratonclient.v1 import regions
metavar='<name>', metavar='<name>',
required=True, required=True,
help='Name of the host.') help='Name of the host.')
@cliutils.arg('--cloud',
dest='cloud_id',
metavar='<cloud>',
type=int,
required=True,
help='ID of the cloud that the region belongs to.')
@cliutils.arg('--note', @cliutils.arg('--note',
help='Note about the host.') help='Note about the host.')
def do_region_create(cc, args): def do_region_create(cc, args):
@@ -33,6 +39,10 @@ def do_region_create(cc, args):
cliutils.print_dict(data, wrap=72) cliutils.print_dict(data, wrap=72)
@cliutils.arg('--cloud',
metavar='<cloud>',
type=int,
help='ID of the cloud that the region belongs to.')
@cliutils.arg('--fields', @cliutils.arg('--fields',
nargs='+', nargs='+',
metavar='<fields>', metavar='<fields>',
@@ -53,11 +63,13 @@ def do_region_create(cc, args):
@cliutils.arg('--marker', @cliutils.arg('--marker',
metavar='<marker>', metavar='<marker>',
default=None, 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): def do_region_list(cc, args):
"""List all regions.""" """List all regions."""
params = {} params = {}
default_fields = ['id', 'name'] default_fields = ['id', 'name']
if args.cloud is not None:
params['cloud_id'] = args.cloud
if args.limit is not None: if args.limit is not None:
if args.limit < 0: if args.limit < 0:
raise exc.CommandError('Invalid limit specified. Expected ' 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])) raise exc.CommandError('Invalid field "{}"'.format(err.args[0]))
else: else:
fields = default_fields fields = default_fields
params['marker'] = args.marker params['marker'] = args.marker
params['autopaginate'] = args.all params['autopaginate'] = args.all
@@ -99,6 +112,11 @@ def do_region_show(cc, args):
@cliutils.arg('-n', '--name', @cliutils.arg('-n', '--name',
metavar='<name>', metavar='<name>',
help='Name of the region.') help='Name of the region.')
@cliutils.arg('--cloud',
dest='cloud_id',
metavar='<cloud>',
type=int,
help='Desired ID of the cloud that the region should change to.')
@cliutils.arg('--note', @cliutils.arg('--note',
help='Note about the region.') help='Note about the region.')
def do_region_update(cc, args): def do_region_update(cc, args):
@@ -108,8 +126,8 @@ def do_region_update(cc, args):
item_id = fields.pop('id') item_id = fields.pop('id')
if not fields: if not fields:
raise exc.CommandError( raise exc.CommandError(
'Nothing to update... Please specify one or more of --name, or ' 'Nothing to update... Please specify one or more of --name, '
'--note' '--cloud, or --note'
) )
region = cc.regions.update(item_id, **fields) region = cc.regions.update(item_id, **fields)
data = {f: getattr(region, f, '') for f in regions.REGION_FIELDS} data = {f: getattr(region, f, '') for f in regions.REGION_FIELDS}

View File

@@ -11,6 +11,7 @@
# limitations under the License. # limitations under the License.
"""Command-line interface to the OpenStack Craton API V1.""" """Command-line interface to the OpenStack Craton API V1."""
from cratonclient.shell.v1 import cells_shell 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 hosts_shell
from cratonclient.shell.v1 import projects_shell from cratonclient.shell.v1 import projects_shell
from cratonclient.shell.v1 import regions_shell from cratonclient.shell.v1 import regions_shell
@@ -19,6 +20,7 @@ from cratonclient.shell.v1 import regions_shell
COMMAND_MODULES = [ COMMAND_MODULES = [
# TODO(cmspence): project_shell, cell_shell, device_shell, user_shell, etc. # TODO(cmspence): project_shell, cell_shell, device_shell, user_shell, etc.
projects_shell, projects_shell,
clouds_shell,
regions_shell, regions_shell,
hosts_shell, hosts_shell,
cells_shell, cells_shell,

View File

@@ -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')

View File

@@ -50,6 +50,7 @@ class TestDoCellList(base.TestShellCommandUsingPrintList):
def args_for(self, **kwargs): def args_for(self, **kwargs):
"""Generate the default argument list for cell-list.""" """Generate the default argument list for cell-list."""
kwargs.setdefault('region', 123) kwargs.setdefault('region', 123)
kwargs.setdefault('cloud', None)
kwargs.setdefault('detail', False) kwargs.setdefault('detail', False)
kwargs.setdefault('limit', None) kwargs.setdefault('limit', None)
kwargs.setdefault('sort_key', None) kwargs.setdefault('sort_key', None)
@@ -75,6 +76,23 @@ class TestDoCellList(base.TestShellCommandUsingPrintList):
self.assertEqual(['id', 'name'], self.assertEqual(['id', 'name'],
sorted(self.print_list.call_args[0][-1])) 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): def test_negative_limit(self):
"""Ensure we raise an exception for negative limits.""" """Ensure we raise an exception for negative limits."""
args = self.args_for(limit=-1) args = self.args_for(limit=-1)

View File

@@ -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,
)

View File

@@ -44,6 +44,7 @@ class TestDoHostList(base.TestShellCommandUsingPrintList):
def args_for(self, **kwargs): def args_for(self, **kwargs):
"""Generate a Namespace for do_host_list.""" """Generate a Namespace for do_host_list."""
kwargs.setdefault('cloud', None)
kwargs.setdefault('region', 246) kwargs.setdefault('region', 246)
kwargs.setdefault('cell', None) kwargs.setdefault('cell', None)
kwargs.setdefault('detail', False) kwargs.setdefault('detail', False)
@@ -88,6 +89,23 @@ class TestDoHostList(base.TestShellCommandUsingPrintList):
'active', 'cell_id', 'device_type', 'id', 'name', '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): def test_with_detail(self):
"""Verify the behaviour of specifying --detail.""" """Verify the behaviour of specifying --detail."""
args = self.args_for(detail=True) args = self.args_for(detail=True)
@@ -104,6 +122,7 @@ class TestDoHostList(base.TestShellCommandUsingPrintList):
self.assertSortedPrintListFieldsEqualTo([ self.assertSortedPrintListFieldsEqualTo([
'active', 'active',
'cell_id', 'cell_id',
'cloud_id',
'created_at', 'created_at',
'device_type', 'device_type',
'id', 'id',

View File

@@ -42,6 +42,7 @@ class TestDoRegionCreate(base.TestShellCommandUsingPrintDict):
def args_for(self, **kwargs): def args_for(self, **kwargs):
"""Generate arguments for region-create.""" """Generate arguments for region-create."""
kwargs.setdefault('name', 'New region') kwargs.setdefault('name', 'New region')
kwargs.setdefault('cloud_id', 1)
kwargs.setdefault('note', None) kwargs.setdefault('note', None)
return super(TestDoRegionCreate, self).args_for(**kwargs) return super(TestDoRegionCreate, self).args_for(**kwargs)
@@ -53,6 +54,7 @@ class TestDoRegionCreate(base.TestShellCommandUsingPrintDict):
self.craton_client.regions.create.assert_called_once_with( self.craton_client.regions.create.assert_called_once_with(
name='New region', name='New region',
cloud_id=1,
) )
self.print_dict.assert_called_once_with( self.print_dict.assert_called_once_with(
{field: mock.ANY for field in regions.REGION_FIELDS}, {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( self.craton_client.regions.create.assert_called_once_with(
name='New region', name='New region',
cloud_id=1,
note='This is a note', note='This is a note',
) )
self.print_dict.assert_called_once_with( self.print_dict.assert_called_once_with(
@@ -81,6 +84,7 @@ class TestDoRegionUpdate(base.TestShellCommandUsingPrintDict):
def args_for(self, **kwargs): def args_for(self, **kwargs):
"""Generate arguments for region-update.""" """Generate arguments for region-update."""
kwargs.setdefault('id', 12345) kwargs.setdefault('id', 12345)
kwargs.setdefault('cloud_id', None)
kwargs.setdefault('name', None) kwargs.setdefault('name', None)
kwargs.setdefault('note', None) kwargs.setdefault('note', None)
return super(TestDoRegionUpdate, self).args_for(**kwargs) return super(TestDoRegionUpdate, self).args_for(**kwargs)
@@ -131,6 +135,7 @@ class TestDoRegionUpdate(base.TestShellCommandUsingPrintDict):
args = self.args_for( args = self.args_for(
note='A New Note', note='A New Note',
name='A New Name', name='A New Name',
cloud_id=2,
) )
regions_shell.do_region_update(self.craton_client, args) regions_shell.do_region_update(self.craton_client, args)
@@ -139,6 +144,7 @@ class TestDoRegionUpdate(base.TestShellCommandUsingPrintDict):
12345, 12345,
note='A New Note', note='A New Note',
name='A New Name', name='A New Name',
cloud_id=2,
) )
self.print_dict.assert_called_once_with( self.print_dict.assert_called_once_with(
{field: mock.ANY for field in regions.REGION_FIELDS}, {field: mock.ANY for field in regions.REGION_FIELDS},
@@ -208,6 +214,7 @@ class TestDoRegionList(base.TestShellCommandUsingPrintList):
def args_for(self, **kwargs): def args_for(self, **kwargs):
"""Generate the default argument list for region-list.""" """Generate the default argument list for region-list."""
kwargs.setdefault('detail', False) kwargs.setdefault('detail', False)
kwargs.setdefault('cloud', None)
kwargs.setdefault('limit', None) kwargs.setdefault('limit', None)
kwargs.setdefault('fields', []) kwargs.setdefault('fields', [])
kwargs.setdefault('marker', None) kwargs.setdefault('marker', None)
@@ -223,6 +230,19 @@ class TestDoRegionList(base.TestShellCommandUsingPrintList):
self.assertEqual(['id', 'name'], self.assertEqual(['id', 'name'],
sorted(self.print_list.call_args[0][-1])) 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): def test_negative_limit(self):
"""Ensure we raise an exception for negative limits.""" """Ensure we raise an exception for negative limits."""
args = self.args_for(limit=-1) args = self.args_for(limit=-1)

View File

@@ -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)

View File

@@ -33,6 +33,7 @@ CELL_FIELDS = {
'id': 'ID', 'id': 'ID',
'region_id': 'Region ID', 'region_id': 'Region ID',
'project_id': 'Project ID', 'project_id': 'Project ID',
'cloud_id': 'Cloud ID',
'name': 'Name', 'name': 'Name',
'note': 'Note', 'note': 'Note',
'created_at': 'Created At', 'created_at': 'Created At',

View File

@@ -13,6 +13,7 @@
# under the License. # under the License.
"""Top-level client for version 1 of Craton's API.""" """Top-level client for version 1 of Craton's API."""
from cratonclient.v1 import cells from cratonclient.v1 import cells
from cratonclient.v1 import clouds
from cratonclient.v1 import hosts from cratonclient.v1 import hosts
from cratonclient.v1 import projects from cratonclient.v1 import projects
from cratonclient.v1 import regions from cratonclient.v1 import regions
@@ -42,4 +43,5 @@ class Client(object):
self.hosts = hosts.HostManager(**manager_kwargs) self.hosts = hosts.HostManager(**manager_kwargs)
self.cells = cells.CellManager(**manager_kwargs) self.cells = cells.CellManager(**manager_kwargs)
self.projects = projects.ProjectManager(**manager_kwargs) self.projects = projects.ProjectManager(**manager_kwargs)
self.clouds = clouds.CloudManager(**manager_kwargs)
self.regions = regions.RegionManager(**manager_kwargs) self.regions = regions.RegionManager(**manager_kwargs)

38
cratonclient/v1/clouds.py Normal file
View File

@@ -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'
}

View File

@@ -34,6 +34,7 @@ HOST_FIELDS = {
'name': 'Name', 'name': 'Name',
'device_type': 'Device Type', 'device_type': 'Device Type',
'project_id': 'Project ID', 'project_id': 'Project ID',
'cloud_id': 'Cloud ID',
'region_id': 'Region ID', 'region_id': 'Region ID',
'cell_id': 'Cell ID', 'cell_id': 'Cell ID',
'ip_address': 'IP Address', 'ip_address': 'IP Address',

View File

@@ -32,6 +32,7 @@ class RegionManager(crud.CRUDClient):
REGION_FIELDS = { REGION_FIELDS = {
'id': 'ID', 'id': 'ID',
'project_id': 'Project ID', 'project_id': 'Project ID',
'cloud_id': 'Cloud ID',
'name': 'Name', 'name': 'Name',
'note': 'Note', 'note': 'Note',
'created_at': 'Created At', 'created_at': 'Created At',