diff --git a/cratonclient/crud.py b/cratonclient/crud.py index 39b48a5..73aafc7 100644 --- a/cratonclient/crud.py +++ b/cratonclient/crud.py @@ -105,12 +105,15 @@ class CRUDClient(object): def list(self, skip_merge=False, **kwargs): """Generate the items from this endpoint.""" autopaginate = kwargs.pop('autopaginate', True) + nested = kwargs.pop('nested', False) self.merge_request_arguments(kwargs, skip_merge) url = self.build_url(path_arguments=kwargs) + response_generator = self.session.paginate( url, autopaginate=autopaginate, items_key=(self.key + 's'), + nested=nested, params=kwargs, ) for response, items in response_generator: diff --git a/cratonclient/session.py b/cratonclient/session.py index d01a79b..c35003b 100644 --- a/cratonclient/session.py +++ b/cratonclient/session.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. """Craton-specific session details.""" +from itertools import chain import logging from keystoneauth1 import session as ksa_session @@ -233,7 +234,8 @@ class Session(object): 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. 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 automatically after the first page. """ - response = self.get(url, **kwargs) - json_body = response.json() - items = json_body[items_key] - links = json_body['links'] - yield response, items - while autopaginate and len(items) > 0: - url = _find_next_link(links) - if url is None: - break - response = self.get(url) + get_items = True + + while get_items: + response = self.get(url, **kwargs) json_body = response.json() - items = json_body[items_key] - links = json_body['links'] + if nested: + items = list(chain(*json_body[items_key].values())) + else: + items = json_body[items_key] + 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): for link in links: diff --git a/cratonclient/shell/v1/devices_shell.py b/cratonclient/shell/v1/devices_shell.py new file mode 100644 index 0000000..bf98503 --- /dev/null +++ b/cratonclient/shell/v1/devices_shell.py @@ -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='', + 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='', + help='Device field that will be used for sorting.') +@cliutils.arg('--sort-dir', + metavar='', + default='asc', + choices=('asc', 'desc'), + help='Sort direction: "asc" (default) or "desc".') +@cliutils.arg('--limit', + metavar='', + type=int, + help='Maximum number of devices to return.') +@cliutils.arg('--marker', + metavar='', + default=None, + help='ID of the device to use to resume listing devices.') +@cliutils.arg('--cloud', + metavar='', + type=int, + help='ID of the cloud that the device belongs to.') +@cliutils.arg('-r', '--region', + metavar='', + type=int, + help='ID of the region that the device belongs to.') +@cliutils.arg('-c', '--cell', + metavar='', + type=int, + help='Integer ID of the cell that contains ' + 'the desired list of devices.') +@cliutils.arg('--parent', + metavar='', + 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='', + 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) diff --git a/cratonclient/shell/v1/shell.py b/cratonclient/shell/v1/shell.py index e78e351..1273ecd 100644 --- a/cratonclient/shell/v1/shell.py +++ b/cratonclient/shell/v1/shell.py @@ -12,6 +12,7 @@ """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 devices_shell from cratonclient.shell.v1 import hosts_shell from cratonclient.shell.v1 import projects_shell from cratonclient.shell.v1 import regions_shell @@ -22,6 +23,7 @@ COMMAND_MODULES = [ projects_shell, clouds_shell, regions_shell, + devices_shell, hosts_shell, cells_shell, ] diff --git a/cratonclient/tests/integration/shell/v1/test_devices_shell.py b/cratonclient/tests/integration/shell/v1/test_devices_shell.py new file mode 100644 index 0000000..13868e2 --- /dev/null +++ b/cratonclient/tests/integration/shell/v1/test_devices_shell.py @@ -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, + ) diff --git a/cratonclient/tests/unit/shell/v1/test_devices_shell.py b/cratonclient/tests/unit/shell/v1/test_devices_shell.py new file mode 100644 index 0000000..6e82687 --- /dev/null +++ b/cratonclient/tests/unit/shell/v1/test_devices_shell.py @@ -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, + ) diff --git a/cratonclient/tests/unit/test_crud.py b/cratonclient/tests/unit/test_crud.py index 9d1dc4f..e3ef84b 100644 --- a/cratonclient/tests/unit/test_crud.py +++ b/cratonclient/tests/unit/test_crud.py @@ -183,6 +183,7 @@ class TestCRUDClient(base.TestCase): autopaginate=True, items_key='test_keys', params={'sort': 'asc'}, + nested=False, ) self.resource_spec.assert_called_once_with( self.client, diff --git a/cratonclient/tests/unit/test_session.py b/cratonclient/tests/unit/test_session.py index 3e92cea..c6db8c4 100644 --- a/cratonclient/tests/unit/test_session.py +++ b/cratonclient/tests/unit/test_session.py @@ -12,6 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. """Session specific unit tests.""" +from itertools import chain +from operator import itemgetter + from keystoneauth1 import session as ksa_session 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'}, + ), + ] + ) diff --git a/cratonclient/v1/client.py b/cratonclient/v1/client.py index 89cea19..050f811 100644 --- a/cratonclient/v1/client.py +++ b/cratonclient/v1/client.py @@ -14,6 +14,7 @@ """Top-level client for version 1 of Craton's API.""" from cratonclient.v1 import cells from cratonclient.v1 import clouds +from cratonclient.v1 import devices from cratonclient.v1 import hosts from cratonclient.v1 import projects from cratonclient.v1 import regions @@ -44,4 +45,5 @@ class Client(object): self.cells = cells.CellManager(**manager_kwargs) self.projects = projects.ProjectManager(**manager_kwargs) self.clouds = clouds.CloudManager(**manager_kwargs) + self.devices = devices.DeviceManager(**manager_kwargs) self.regions = regions.RegionManager(**manager_kwargs) diff --git a/cratonclient/v1/devices.py b/cratonclient/v1/devices.py new file mode 100644 index 0000000..4e11e65 --- /dev/null +++ b/cratonclient/v1/devices.py @@ -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' +}