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:
@@ -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}
|
||||
|
||||
135
cratonclient/shell/v1/clouds_shell.py
Normal file
135
cratonclient/shell/v1/clouds_shell.py
Normal 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'))
|
||||
@@ -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>',
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
153
cratonclient/tests/integration/test_clouds_shell.py
Normal file
153
cratonclient/tests/integration/test_clouds_shell.py
Normal 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')
|
||||
@@ -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)
|
||||
|
||||
290
cratonclient/tests/unit/shell/v1/test_clouds_shell.py
Normal file
290
cratonclient/tests/unit/shell/v1/test_clouds_shell.py
Normal 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,
|
||||
)
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
38
cratonclient/tests/unit/v1/test_clouds.py
Normal file
38
cratonclient/tests/unit/v1/test_clouds.py
Normal 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)
|
||||
@@ -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',
|
||||
|
||||
@@ -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
38
cratonclient/v1/clouds.py
Normal 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'
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user