Merge "Add devices-list to support /v1/devices"
This commit is contained in:
commit
adaf64c626
@ -105,12 +105,15 @@ class CRUDClient(object):
|
|||||||
def list(self, skip_merge=False, **kwargs):
|
def list(self, skip_merge=False, **kwargs):
|
||||||
"""Generate the items from this endpoint."""
|
"""Generate the items from this endpoint."""
|
||||||
autopaginate = kwargs.pop('autopaginate', True)
|
autopaginate = kwargs.pop('autopaginate', True)
|
||||||
|
nested = kwargs.pop('nested', False)
|
||||||
self.merge_request_arguments(kwargs, skip_merge)
|
self.merge_request_arguments(kwargs, skip_merge)
|
||||||
url = self.build_url(path_arguments=kwargs)
|
url = self.build_url(path_arguments=kwargs)
|
||||||
|
|
||||||
response_generator = self.session.paginate(
|
response_generator = self.session.paginate(
|
||||||
url,
|
url,
|
||||||
autopaginate=autopaginate,
|
autopaginate=autopaginate,
|
||||||
items_key=(self.key + 's'),
|
items_key=(self.key + 's'),
|
||||||
|
nested=nested,
|
||||||
params=kwargs,
|
params=kwargs,
|
||||||
)
|
)
|
||||||
for response, items in response_generator:
|
for response, items in response_generator:
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
"""Craton-specific session details."""
|
"""Craton-specific session details."""
|
||||||
|
from itertools import chain
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from keystoneauth1 import session as ksa_session
|
from keystoneauth1 import session as ksa_session
|
||||||
@ -233,7 +234,8 @@ class Session(object):
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def paginate(self, url, items_key, autopaginate=True, **kwargs):
|
def paginate(self, url, items_key, autopaginate=True, nested=False,
|
||||||
|
**kwargs):
|
||||||
"""Make a GET request to a paginated resource.
|
"""Make a GET request to a paginated resource.
|
||||||
|
|
||||||
If :param:`autopaginate` is set to ``True``, this will automatically
|
If :param:`autopaginate` is set to ``True``, this will automatically
|
||||||
@ -256,21 +258,24 @@ class Session(object):
|
|||||||
Determines whether or not this method continues requesting items
|
Determines whether or not this method continues requesting items
|
||||||
automatically after the first page.
|
automatically after the first page.
|
||||||
"""
|
"""
|
||||||
response = self.get(url, **kwargs)
|
get_items = True
|
||||||
json_body = response.json()
|
|
||||||
items = json_body[items_key]
|
while get_items:
|
||||||
links = json_body['links']
|
response = self.get(url, **kwargs)
|
||||||
yield response, items
|
|
||||||
while autopaginate and len(items) > 0:
|
|
||||||
url = _find_next_link(links)
|
|
||||||
if url is None:
|
|
||||||
break
|
|
||||||
response = self.get(url)
|
|
||||||
json_body = response.json()
|
json_body = response.json()
|
||||||
items = json_body[items_key]
|
if nested:
|
||||||
links = json_body['links']
|
items = list(chain(*json_body[items_key].values()))
|
||||||
|
else:
|
||||||
|
items = json_body[items_key]
|
||||||
|
|
||||||
yield response, items
|
yield response, items
|
||||||
|
|
||||||
|
links = json_body['links']
|
||||||
|
url = _find_next_link(links)
|
||||||
|
|
||||||
|
kwargs = {}
|
||||||
|
get_items = url and autopaginate and len(items) > 0
|
||||||
|
|
||||||
|
|
||||||
def _find_next_link(links):
|
def _find_next_link(links):
|
||||||
for link in links:
|
for link in links:
|
||||||
|
122
cratonclient/shell/v1/devices_shell.py
Normal file
122
cratonclient/shell/v1/devices_shell.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
# 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 devices
|
||||||
|
|
||||||
|
|
||||||
|
@cliutils.arg('--fields',
|
||||||
|
nargs='+',
|
||||||
|
metavar='<fields>',
|
||||||
|
default=[],
|
||||||
|
help='Space-separated list of fields to display. '
|
||||||
|
'Only these fields will be fetched from the server.')
|
||||||
|
@cliutils.arg('--all',
|
||||||
|
action='store_true',
|
||||||
|
default=False,
|
||||||
|
help='Retrieve and show all devices. This will override '
|
||||||
|
'the provided value for --limit and automatically '
|
||||||
|
'retrieve each page of results.')
|
||||||
|
@cliutils.arg('--sort-key',
|
||||||
|
metavar='<field>',
|
||||||
|
help='Device field that will be used for sorting.')
|
||||||
|
@cliutils.arg('--sort-dir',
|
||||||
|
metavar='<direction>',
|
||||||
|
default='asc',
|
||||||
|
choices=('asc', 'desc'),
|
||||||
|
help='Sort direction: "asc" (default) or "desc".')
|
||||||
|
@cliutils.arg('--limit',
|
||||||
|
metavar='<limit>',
|
||||||
|
type=int,
|
||||||
|
help='Maximum number of devices to return.')
|
||||||
|
@cliutils.arg('--marker',
|
||||||
|
metavar='<marker>',
|
||||||
|
default=None,
|
||||||
|
help='ID of the device to use to resume listing devices.')
|
||||||
|
@cliutils.arg('--cloud',
|
||||||
|
metavar='<cloud>',
|
||||||
|
type=int,
|
||||||
|
help='ID of the cloud that the device belongs to.')
|
||||||
|
@cliutils.arg('-r', '--region',
|
||||||
|
metavar='<region>',
|
||||||
|
type=int,
|
||||||
|
help='ID of the region that the device belongs to.')
|
||||||
|
@cliutils.arg('-c', '--cell',
|
||||||
|
metavar='<cell>',
|
||||||
|
type=int,
|
||||||
|
help='Integer ID of the cell that contains '
|
||||||
|
'the desired list of devices.')
|
||||||
|
@cliutils.arg('--parent',
|
||||||
|
metavar='<parent>',
|
||||||
|
type=int,
|
||||||
|
help='Parent ID of required devices.')
|
||||||
|
@cliutils.arg('--descendants',
|
||||||
|
default=False,
|
||||||
|
action='store_true',
|
||||||
|
help='When parent is also specified, include all descendants.')
|
||||||
|
@cliutils.arg('--active',
|
||||||
|
metavar='<active>',
|
||||||
|
choices=("true", "false"),
|
||||||
|
help='Filter devices by their active state.')
|
||||||
|
def do_device_list(cc, args):
|
||||||
|
"""List all devices."""
|
||||||
|
params = {}
|
||||||
|
default_fields = [
|
||||||
|
'cloud_id', 'region_id', 'cell_id', 'parent_id', 'id', 'name',
|
||||||
|
'device_type', 'active',
|
||||||
|
]
|
||||||
|
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: devices.DEVICE_FIELDS[x] for x in args.fields}
|
||||||
|
except KeyError as err:
|
||||||
|
raise exc.CommandError('Invalid field "{}"'.format(err.args[0]))
|
||||||
|
else:
|
||||||
|
fields = default_fields
|
||||||
|
sort_key = args.sort_key and args.sort_key.lower()
|
||||||
|
if sort_key is not None:
|
||||||
|
if sort_key not in devices.DEVICE_FIELDS:
|
||||||
|
raise exc.CommandError(
|
||||||
|
'{0} is an invalid key for sorting, valid values for '
|
||||||
|
'--sort-key are: {1}'.format(
|
||||||
|
args.sort_key, devices.DEVICE_FIELDS.keys()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
params['sort_keys'] = sort_key
|
||||||
|
params['sort_dir'] = args.sort_dir
|
||||||
|
params['marker'] = args.marker
|
||||||
|
params['autopaginate'] = args.all
|
||||||
|
if args.parent:
|
||||||
|
params['parent_id'] = args.parent
|
||||||
|
params['descendants'] = args.descendants
|
||||||
|
if args.cloud:
|
||||||
|
params['cloud_id'] = args.cloud
|
||||||
|
if args.region:
|
||||||
|
params['region_id'] = args.region
|
||||||
|
if args.cell:
|
||||||
|
params['cell_id'] = args.cell
|
||||||
|
if args.active:
|
||||||
|
params['active'] = args.active
|
||||||
|
|
||||||
|
devices_list = cc.devices.list(**params)
|
||||||
|
args.formatter.configure(fields=list(fields)).handle(devices_list)
|
@ -12,6 +12,7 @@
|
|||||||
"""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 clouds_shell
|
||||||
|
from cratonclient.shell.v1 import devices_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
|
||||||
@ -22,6 +23,7 @@ COMMAND_MODULES = [
|
|||||||
projects_shell,
|
projects_shell,
|
||||||
clouds_shell,
|
clouds_shell,
|
||||||
regions_shell,
|
regions_shell,
|
||||||
|
devices_shell,
|
||||||
hosts_shell,
|
hosts_shell,
|
||||||
cells_shell,
|
cells_shell,
|
||||||
]
|
]
|
||||||
|
182
cratonclient/tests/integration/shell/v1/test_devices_shell.py
Normal file
182
cratonclient/tests/integration/shell/v1/test_devices_shell.py
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
# 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.devices_shell` module."""
|
||||||
|
|
||||||
|
import mock
|
||||||
|
import re
|
||||||
|
|
||||||
|
from cratonclient import exceptions as exc
|
||||||
|
from cratonclient.tests.integration.shell import base
|
||||||
|
|
||||||
|
|
||||||
|
class TestDevicesShell(base.ShellTestCase):
|
||||||
|
"""Test our craton devices shell commands."""
|
||||||
|
|
||||||
|
re_options = re.DOTALL | re.MULTILINE
|
||||||
|
|
||||||
|
@mock.patch('cratonclient.v1.devices.DeviceManager.list')
|
||||||
|
def test_device_list_success(self, mock_list):
|
||||||
|
"""Verify that no arguments prints out all project devices."""
|
||||||
|
self.shell('device-list')
|
||||||
|
self.assertTrue(mock_list.called)
|
||||||
|
|
||||||
|
@mock.patch('cratonclient.v1.devices.DeviceManager.list')
|
||||||
|
def test_device_list_parse_param_success(self, mock_list):
|
||||||
|
"""Verify that success of parsing a subcommand argument."""
|
||||||
|
self.shell('device-list --limit 0')
|
||||||
|
self.assertTrue(mock_list.called)
|
||||||
|
|
||||||
|
@mock.patch('cratonclient.v1.devices.DeviceManager.list')
|
||||||
|
def test_device_list_limit_0_success(self, mock_list):
|
||||||
|
"""Verify that --limit 0 prints out all project devices."""
|
||||||
|
self.shell('device-list --limit 0')
|
||||||
|
mock_list.assert_called_once_with(
|
||||||
|
limit=0,
|
||||||
|
sort_dir='asc',
|
||||||
|
descendants=False,
|
||||||
|
marker=None,
|
||||||
|
autopaginate=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch('cratonclient.v1.devices.DeviceManager.list')
|
||||||
|
def test_device_list_limit_positive_num_success(self, mock_list):
|
||||||
|
"""Verify --limit X, where X is a positive integer, succeeds.
|
||||||
|
|
||||||
|
The command will print out X number of project devices.
|
||||||
|
"""
|
||||||
|
self.shell('device-list --limit 1')
|
||||||
|
mock_list.assert_called_once_with(
|
||||||
|
limit=1,
|
||||||
|
sort_dir='asc',
|
||||||
|
descendants=False,
|
||||||
|
marker=None,
|
||||||
|
autopaginate=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_device_list_limit_negative_num_failure(self):
|
||||||
|
"""Verify --limit X, where X is a negative integer, fails.
|
||||||
|
|
||||||
|
The command will cause a Command Error message response.
|
||||||
|
"""
|
||||||
|
self.assertRaises(exc.CommandError,
|
||||||
|
self.shell,
|
||||||
|
'device-list -r 1 --limit -1')
|
||||||
|
|
||||||
|
@mock.patch('cratonclient.v1.devices.DeviceManager.list')
|
||||||
|
def test_device_list_cell_success(self, mock_list):
|
||||||
|
"""Verify --cell arguments successfully pass cell to Client."""
|
||||||
|
for cell_arg in ['-c', '--cell']:
|
||||||
|
self.shell('device-list {0} 1'.format(cell_arg))
|
||||||
|
mock_list.assert_called_once_with(
|
||||||
|
cell_id=1,
|
||||||
|
sort_dir='asc',
|
||||||
|
descendants=False,
|
||||||
|
marker=None,
|
||||||
|
autopaginate=False,
|
||||||
|
)
|
||||||
|
mock_list.reset_mock()
|
||||||
|
|
||||||
|
@mock.patch('cratonclient.v1.devices.DeviceManager.list')
|
||||||
|
def test_device_list_fields_success(self, mock_list):
|
||||||
|
"""Verify --fields argument successfully passed to Client."""
|
||||||
|
self.shell('device-list --fields id name')
|
||||||
|
mock_list.assert_called_once_with(
|
||||||
|
sort_dir='asc',
|
||||||
|
descendants=False,
|
||||||
|
marker=None,
|
||||||
|
autopaginate=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch('cratonclient.v1.devices.DeviceManager.list')
|
||||||
|
def test_device_list_sort_keys_field_key_success(self, mock_list):
|
||||||
|
"""Verify --sort-key arguments successfully passed to Client."""
|
||||||
|
self.shell('device-list --sort-key cell_id')
|
||||||
|
mock_list.assert_called_once_with(
|
||||||
|
sort_keys='cell_id',
|
||||||
|
sort_dir='asc',
|
||||||
|
descendants=False,
|
||||||
|
marker=None,
|
||||||
|
autopaginate=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_device_list_sort_keys_invalid(self):
|
||||||
|
"""Verify --sort-key with invalid args, fails with Command Error."""
|
||||||
|
self.assertRaises(exc.CommandError,
|
||||||
|
self.shell,
|
||||||
|
'device-list --sort-key invalid')
|
||||||
|
|
||||||
|
@mock.patch('cratonclient.v1.devices.DeviceManager.list')
|
||||||
|
def test_device_list_sort_dir_not_passed_without_sort_key(self, mock_list):
|
||||||
|
"""Verify --sort-dir arg ignored without --sort-key."""
|
||||||
|
self.shell('device-list --sort-dir desc')
|
||||||
|
mock_list.assert_called_once_with(
|
||||||
|
sort_dir='desc',
|
||||||
|
descendants=False,
|
||||||
|
marker=None,
|
||||||
|
autopaginate=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch('cratonclient.v1.devices.DeviceManager.list')
|
||||||
|
def test_device_list_sort_dir_asc_success(self, mock_list):
|
||||||
|
"""Verify --sort-dir asc successfully passed to Client."""
|
||||||
|
self.shell('device-list --sort-key name --sort-dir asc')
|
||||||
|
mock_list.assert_called_once_with(
|
||||||
|
sort_keys='name',
|
||||||
|
sort_dir='asc',
|
||||||
|
descendants=False,
|
||||||
|
marker=None,
|
||||||
|
autopaginate=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch('cratonclient.v1.devices.DeviceManager.list')
|
||||||
|
def test_device_list_sort_dir_desc_success(self, mock_list):
|
||||||
|
"""Verify --sort-dir desc successfully passed to Client."""
|
||||||
|
self.shell('device-list --sort-key name --sort-dir desc')
|
||||||
|
mock_list.assert_called_once_with(
|
||||||
|
sort_keys='name',
|
||||||
|
sort_dir='desc',
|
||||||
|
descendants=False,
|
||||||
|
marker=None,
|
||||||
|
autopaginate=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_device_list_sort_dir_invalid_value(self):
|
||||||
|
"""Verify --sort-dir with invalid args, fails with Command Error."""
|
||||||
|
(_, error) = self.shell(
|
||||||
|
'device-list -r 1 --sort-key name --sort-dir invalid'
|
||||||
|
)
|
||||||
|
self.assertIn("invalid choice: 'invalid'", error)
|
||||||
|
|
||||||
|
@mock.patch('cratonclient.v1.devices.DeviceManager.list')
|
||||||
|
def test_device_list_filter_by_parent_success(self, mock_list):
|
||||||
|
"""Verify --parent ID successfully passed to Client."""
|
||||||
|
self.shell('device-list --parent 12345')
|
||||||
|
mock_list.assert_called_once_with(
|
||||||
|
sort_dir='asc',
|
||||||
|
descendants=False,
|
||||||
|
parent_id=12345,
|
||||||
|
marker=None,
|
||||||
|
autopaginate=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch('cratonclient.v1.devices.DeviceManager.list')
|
||||||
|
def test_device_list_filter_by_parent_descendants_success(self, mock_list):
|
||||||
|
"""Verify --parent ID successfully passed to Client."""
|
||||||
|
self.shell('device-list --parent 12345 --descendants')
|
||||||
|
mock_list.assert_called_once_with(
|
||||||
|
sort_dir='asc',
|
||||||
|
parent_id=12345,
|
||||||
|
descendants=True,
|
||||||
|
marker=None,
|
||||||
|
autopaginate=False,
|
||||||
|
)
|
257
cratonclient/tests/unit/shell/v1/test_devices_shell.py
Normal file
257
cratonclient/tests/unit/shell/v1/test_devices_shell.py
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
# -*- 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 devices resource."""
|
||||||
|
from cratonclient.shell.v1 import devices_shell
|
||||||
|
from cratonclient.tests.unit.shell import base
|
||||||
|
|
||||||
|
|
||||||
|
class TestDoDeviceList(base.TestShellCommandUsingPrintList):
|
||||||
|
"""Unit tests for the device list command."""
|
||||||
|
|
||||||
|
def args_for(self, **kwargs):
|
||||||
|
"""Generate a Namespace for do_device_list."""
|
||||||
|
kwargs.setdefault('cloud', None)
|
||||||
|
kwargs.setdefault('region', None)
|
||||||
|
kwargs.setdefault('cell', None)
|
||||||
|
kwargs.setdefault('parent', None)
|
||||||
|
kwargs.setdefault('descendants', False)
|
||||||
|
kwargs.setdefault('active', None)
|
||||||
|
kwargs.setdefault('limit', None)
|
||||||
|
kwargs.setdefault('sort_key', None)
|
||||||
|
kwargs.setdefault('sort_dir', 'asc')
|
||||||
|
kwargs.setdefault('fields', [])
|
||||||
|
kwargs.setdefault('marker', None)
|
||||||
|
kwargs.setdefault('all', False)
|
||||||
|
return super(TestDoDeviceList, self).args_for(**kwargs)
|
||||||
|
|
||||||
|
def test_only_required_parameters(self):
|
||||||
|
"""Verify the behaviour with the minimum number of params."""
|
||||||
|
args = self.args_for()
|
||||||
|
|
||||||
|
devices_shell.do_device_list(self.craton_client, args)
|
||||||
|
|
||||||
|
self.craton_client.devices.list.assert_called_once_with(
|
||||||
|
descendants=False,
|
||||||
|
sort_dir='asc',
|
||||||
|
autopaginate=False,
|
||||||
|
marker=None,
|
||||||
|
)
|
||||||
|
self.assertSortedPrintListFieldsEqualTo([
|
||||||
|
'active', 'cell_id', 'cloud_id', 'device_type', 'id', 'name',
|
||||||
|
'parent_id', 'region_id',
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_with_parent_id(self):
|
||||||
|
"""Verify that we include the parent_id in the params."""
|
||||||
|
args = self.args_for(parent=789)
|
||||||
|
|
||||||
|
devices_shell.do_device_list(self.craton_client, args)
|
||||||
|
|
||||||
|
self.craton_client.devices.list.assert_called_once_with(
|
||||||
|
descendants=False,
|
||||||
|
parent_id=789,
|
||||||
|
sort_dir='asc',
|
||||||
|
autopaginate=False,
|
||||||
|
marker=None,
|
||||||
|
)
|
||||||
|
self.assertSortedPrintListFieldsEqualTo([
|
||||||
|
'active', 'cell_id', 'cloud_id', 'device_type', 'id', 'name',
|
||||||
|
'parent_id', 'region_id',
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_with_parent_id_and_descendants(self):
|
||||||
|
"""Verify that the parent_id and descendants is in the params."""
|
||||||
|
args = self.args_for(parent=789, descendants=False)
|
||||||
|
|
||||||
|
devices_shell.do_device_list(self.craton_client, args)
|
||||||
|
|
||||||
|
self.craton_client.devices.list.assert_called_once_with(
|
||||||
|
descendants=False,
|
||||||
|
parent_id=789,
|
||||||
|
sort_dir='asc',
|
||||||
|
autopaginate=False,
|
||||||
|
marker=None,
|
||||||
|
)
|
||||||
|
self.assertSortedPrintListFieldsEqualTo([
|
||||||
|
'active', 'cell_id', 'cloud_id', 'device_type', 'id', 'name',
|
||||||
|
'parent_id', 'region_id',
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_with_region_id(self):
|
||||||
|
"""Verify that we include the region_id in the params."""
|
||||||
|
args = self.args_for(region=789)
|
||||||
|
|
||||||
|
devices_shell.do_device_list(self.craton_client, args)
|
||||||
|
|
||||||
|
self.craton_client.devices.list.assert_called_once_with(
|
||||||
|
descendants=False,
|
||||||
|
region_id=789,
|
||||||
|
sort_dir='asc',
|
||||||
|
autopaginate=False,
|
||||||
|
marker=None,
|
||||||
|
)
|
||||||
|
self.assertSortedPrintListFieldsEqualTo([
|
||||||
|
'active', 'cell_id', 'cloud_id', 'device_type', 'id', 'name',
|
||||||
|
'parent_id', 'region_id',
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_with_cell_id(self):
|
||||||
|
"""Verify that we include the cell_id in the params."""
|
||||||
|
args = self.args_for(cell=789)
|
||||||
|
|
||||||
|
devices_shell.do_device_list(self.craton_client, args)
|
||||||
|
|
||||||
|
self.craton_client.devices.list.assert_called_once_with(
|
||||||
|
descendants=False,
|
||||||
|
cell_id=789,
|
||||||
|
sort_dir='asc',
|
||||||
|
autopaginate=False,
|
||||||
|
marker=None,
|
||||||
|
)
|
||||||
|
self.assertSortedPrintListFieldsEqualTo([
|
||||||
|
'active', 'cell_id', 'cloud_id', 'device_type', 'id', 'name',
|
||||||
|
'parent_id', 'region_id',
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_with_cloud_id(self):
|
||||||
|
"""Verify that we include the cell_id in the params."""
|
||||||
|
args = self.args_for(cloud=123)
|
||||||
|
|
||||||
|
devices_shell.do_device_list(self.craton_client, args)
|
||||||
|
|
||||||
|
self.craton_client.devices.list.assert_called_once_with(
|
||||||
|
descendants=False,
|
||||||
|
sort_dir='asc',
|
||||||
|
cloud_id=123,
|
||||||
|
autopaginate=False,
|
||||||
|
marker=None,
|
||||||
|
)
|
||||||
|
self.assertSortedPrintListFieldsEqualTo([
|
||||||
|
'active', 'cell_id', 'cloud_id', 'device_type', 'id', 'name',
|
||||||
|
'parent_id', 'region_id',
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_with_limit(self):
|
||||||
|
"""Verify the behaviour with --limit specified."""
|
||||||
|
args = self.args_for(limit=20)
|
||||||
|
|
||||||
|
devices_shell.do_device_list(self.craton_client, args)
|
||||||
|
|
||||||
|
self.craton_client.devices.list.assert_called_once_with(
|
||||||
|
descendants=False,
|
||||||
|
limit=20,
|
||||||
|
sort_dir='asc',
|
||||||
|
autopaginate=False,
|
||||||
|
marker=None,
|
||||||
|
)
|
||||||
|
self.assertSortedPrintListFieldsEqualTo([
|
||||||
|
'active', 'cell_id', 'cloud_id', 'device_type', 'id', 'name',
|
||||||
|
'parent_id', 'region_id',
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_negative_limit_raises_command_error(self):
|
||||||
|
"""Verify that we forbid negative limit values."""
|
||||||
|
args = self.args_for(limit=-10)
|
||||||
|
|
||||||
|
self.assertRaisesCommandErrorWith(devices_shell.do_device_list, args)
|
||||||
|
self.assertNothingWasCalled()
|
||||||
|
|
||||||
|
def test_fields(self):
|
||||||
|
"""Verify that we can specify custom fields."""
|
||||||
|
args = self.args_for(fields=['id', 'name', 'cell_id'])
|
||||||
|
|
||||||
|
devices_shell.do_device_list(self.craton_client, args)
|
||||||
|
|
||||||
|
self.craton_client.devices.list.assert_called_once_with(
|
||||||
|
descendants=False,
|
||||||
|
sort_dir='asc',
|
||||||
|
autopaginate=False,
|
||||||
|
marker=None,
|
||||||
|
)
|
||||||
|
self.assertSortedPrintListFieldsEqualTo([
|
||||||
|
'cell_id', 'id', 'name',
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_invalid_sort_key(self):
|
||||||
|
"""Verify that we disallow invalid sort keys."""
|
||||||
|
args = self.args_for(sort_key='my-fake-sort-key')
|
||||||
|
|
||||||
|
self.assertRaisesCommandErrorWith(
|
||||||
|
devices_shell.do_device_list, args
|
||||||
|
)
|
||||||
|
self.assertNothingWasCalled()
|
||||||
|
|
||||||
|
def test_sort_key(self):
|
||||||
|
"""Verify we pass sort_key to our list call."""
|
||||||
|
args = self.args_for(sort_key='ip_address')
|
||||||
|
|
||||||
|
devices_shell.do_device_list(self.craton_client, args)
|
||||||
|
|
||||||
|
self.craton_client.devices.list.assert_called_once_with(
|
||||||
|
descendants=False,
|
||||||
|
sort_keys='ip_address',
|
||||||
|
sort_dir='asc',
|
||||||
|
autopaginate=False,
|
||||||
|
marker=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_invalid_fields_raise_command_error(self):
|
||||||
|
"""Verify sending an invalid field raises a CommandError."""
|
||||||
|
args = self.args_for(fields=['fake-field', 'id'])
|
||||||
|
|
||||||
|
self.assertRaisesCommandErrorWith(
|
||||||
|
devices_shell.do_device_list, args,
|
||||||
|
)
|
||||||
|
self.assertNothingWasCalled()
|
||||||
|
|
||||||
|
def test_autopagination(self):
|
||||||
|
"""Verify autopagination is controlled by --all."""
|
||||||
|
args = self.args_for(all=True)
|
||||||
|
|
||||||
|
devices_shell.do_device_list(self.craton_client, args)
|
||||||
|
|
||||||
|
self.craton_client.devices.list.assert_called_once_with(
|
||||||
|
descendants=False,
|
||||||
|
sort_dir='asc',
|
||||||
|
limit=100,
|
||||||
|
marker=None,
|
||||||
|
autopaginate=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_autopagination_overrides_limit(self):
|
||||||
|
"""Verify --all overrides --limit."""
|
||||||
|
args = self.args_for(all=True, limit=30)
|
||||||
|
|
||||||
|
devices_shell.do_device_list(self.craton_client, args)
|
||||||
|
|
||||||
|
self.craton_client.devices.list.assert_called_once_with(
|
||||||
|
descendants=False,
|
||||||
|
sort_dir='asc',
|
||||||
|
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=42)
|
||||||
|
|
||||||
|
devices_shell.do_device_list(self.craton_client, args)
|
||||||
|
|
||||||
|
self.craton_client.devices.list.assert_called_once_with(
|
||||||
|
descendants=False,
|
||||||
|
sort_dir='asc',
|
||||||
|
marker=42,
|
||||||
|
autopaginate=False,
|
||||||
|
)
|
@ -183,6 +183,7 @@ class TestCRUDClient(base.TestCase):
|
|||||||
autopaginate=True,
|
autopaginate=True,
|
||||||
items_key='test_keys',
|
items_key='test_keys',
|
||||||
params={'sort': 'asc'},
|
params={'sort': 'asc'},
|
||||||
|
nested=False,
|
||||||
)
|
)
|
||||||
self.resource_spec.assert_called_once_with(
|
self.resource_spec.assert_called_once_with(
|
||||||
self.client,
|
self.client,
|
||||||
|
@ -12,6 +12,9 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
"""Session specific unit tests."""
|
"""Session specific unit tests."""
|
||||||
|
from itertools import chain
|
||||||
|
from operator import itemgetter
|
||||||
|
|
||||||
from keystoneauth1 import session as ksa_session
|
from keystoneauth1 import session as ksa_session
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
@ -162,3 +165,57 @@ class TestSession(base.TestCase):
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_paginate_nested_response(self):
|
||||||
|
"""Verify Session#paginate can extract nested lists."""
|
||||||
|
responses = [
|
||||||
|
self.create_response(
|
||||||
|
{
|
||||||
|
"items-sub-type-1": [
|
||||||
|
{"id": 1},
|
||||||
|
],
|
||||||
|
"items-sub-type-2": [
|
||||||
|
{"id": 2},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"http://example.com/v1/items?limit=30&marker=2"
|
||||||
|
),
|
||||||
|
self.create_response(
|
||||||
|
{
|
||||||
|
"items-sub-type-1": [],
|
||||||
|
"items-sub-type-2": [],
|
||||||
|
},
|
||||||
|
""
|
||||||
|
),
|
||||||
|
]
|
||||||
|
mock_session = mock.Mock()
|
||||||
|
mock_session.request.side_effect = responses
|
||||||
|
|
||||||
|
craton_session = session.Session(session=mock_session)
|
||||||
|
paginated_items = list(craton_session.paginate(
|
||||||
|
url='http://example.com/v1/items',
|
||||||
|
items_key='items',
|
||||||
|
autopaginate=True,
|
||||||
|
nested=True,
|
||||||
|
))
|
||||||
|
|
||||||
|
self.assertEqual(2, len(paginated_items))
|
||||||
|
resp_items = sorted(
|
||||||
|
chain(*(resp[1] for resp in paginated_items)), key=itemgetter("id")
|
||||||
|
)
|
||||||
|
self.assertListEqual([{"id": 1}, {"id": 2}], resp_items)
|
||||||
|
self.assertListEqual(
|
||||||
|
mock_session.request.call_args_list,
|
||||||
|
[
|
||||||
|
mock.call(
|
||||||
|
method='GET',
|
||||||
|
url='http://example.com/v1/items',
|
||||||
|
endpoint_filter={'service_type': 'fleet_management'},
|
||||||
|
),
|
||||||
|
mock.call(
|
||||||
|
method='GET',
|
||||||
|
url='http://example.com/v1/items?limit=30&marker=2',
|
||||||
|
endpoint_filter={'service_type': 'fleet_management'},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
"""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 clouds
|
||||||
|
from cratonclient.v1 import devices
|
||||||
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
|
||||||
@ -44,4 +45,5 @@ class Client(object):
|
|||||||
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.clouds = clouds.CloudManager(**manager_kwargs)
|
||||||
|
self.devices = devices.DeviceManager(**manager_kwargs)
|
||||||
self.regions = regions.RegionManager(**manager_kwargs)
|
self.regions = regions.RegionManager(**manager_kwargs)
|
||||||
|
48
cratonclient/v1/devices.py
Normal file
48
cratonclient/v1/devices.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# -*- 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.
|
||||||
|
"""Devices manager code."""
|
||||||
|
from cratonclient import crud
|
||||||
|
|
||||||
|
|
||||||
|
class Device(crud.Resource):
|
||||||
|
"""Representation of a Device."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceManager(crud.CRUDClient):
|
||||||
|
"""A manager for devices."""
|
||||||
|
|
||||||
|
key = 'device'
|
||||||
|
base_path = '/devices'
|
||||||
|
resource_class = Device
|
||||||
|
|
||||||
|
def list(self, **kwargs):
|
||||||
|
"""Generate the items from this endpoint."""
|
||||||
|
return super(DeviceManager, self).list(nested=True, **kwargs)
|
||||||
|
|
||||||
|
DEVICE_FIELDS = {
|
||||||
|
'id': 'ID',
|
||||||
|
'project_id': 'Project ID',
|
||||||
|
'cloud_id': 'Cloud ID',
|
||||||
|
'region_id': 'Region ID',
|
||||||
|
'cell_id': 'Cell ID',
|
||||||
|
'parent_id': 'Parent ID',
|
||||||
|
'name': 'Name',
|
||||||
|
'ip_address': 'IP Address',
|
||||||
|
'device_type': 'Device Type',
|
||||||
|
'note': 'Note',
|
||||||
|
'created_at': 'Created At',
|
||||||
|
'updated_at': 'Updated At'
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user