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,
required=True,
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',
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='<cloud>',
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='<region>',
type=int,
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',
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}

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,
required=True,
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',
metavar='<cell>',
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='<cloud>',
type=int,
required=True,
help='ID of the cloud that the host belongs to.')
@cliutils.arg('-c', '--cell',
dest='cell_id',
metavar='<cell>',
@@ -176,6 +188,11 @@ def do_host_create(cc, args):
metavar='<region>',
type=int,
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',
dest='cell_id',
metavar='<cell>',

View File

@@ -21,6 +21,12 @@ from cratonclient.v1 import regions
metavar='<name>',
required=True,
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',
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='<cloud>',
type=int,
help='ID of the cloud that the region belongs to.')
@cliutils.arg('--fields',
nargs='+',
metavar='<fields>',
@@ -53,11 +63,13 @@ def do_region_create(cc, args):
@cliutils.arg('--marker',
metavar='<marker>',
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='<name>',
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',
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}

View File

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

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):
"""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)

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):
"""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',

View File

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

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',
'region_id': 'Region ID',
'project_id': 'Project ID',
'cloud_id': 'Cloud ID',
'name': 'Name',
'note': 'Note',
'created_at': 'Created At',

View File

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

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',
'device_type': 'Device Type',
'project_id': 'Project ID',
'cloud_id': 'Cloud ID',
'region_id': 'Region ID',
'cell_id': 'Cell ID',
'ip_address': 'IP Address',

View File

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