From 7c93a452425290ae11707f101a1f766874e3440b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Tue, 16 May 2017 11:08:46 -0400 Subject: [PATCH] Add command to update instance entity --- .../{tenant_entities.py => list_entities.py} | 12 ++-- .../commands/update_instance_entity.py | 67 +++++++++++++++++++ almanachclient/http_client.py | 14 +++- almanachclient/shell.py | 6 +- almanachclient/tests/commands/__init__.py | 0 .../commands/test_update_instance_entity.py | 55 +++++++++++++++ almanachclient/tests/v1/test_client.py | 20 +++++- almanachclient/v1/client.py | 20 +++++- doc/source/usage.rst | 37 +++++++++- 9 files changed, 219 insertions(+), 12 deletions(-) rename almanachclient/commands/{tenant_entities.py => list_entities.py} (86%) create mode 100644 almanachclient/commands/update_instance_entity.py create mode 100644 almanachclient/tests/commands/__init__.py create mode 100644 almanachclient/tests/commands/test_update_instance_entity.py diff --git a/almanachclient/commands/tenant_entities.py b/almanachclient/commands/list_entities.py similarity index 86% rename from almanachclient/commands/tenant_entities.py rename to almanachclient/commands/list_entities.py index da2ed7c..31080a1 100644 --- a/almanachclient/commands/tenant_entities.py +++ b/almanachclient/commands/list_entities.py @@ -16,9 +16,11 @@ from cliff.lister import Lister from dateutil import parser -class TenantEntityCommand(Lister): +class ListEntityCommand(Lister): """Show all entities for a given tenant""" + columns = ('Entity ID', 'Type', 'Name', 'Start', 'End', 'Properties') + def get_parser(self, prog_name): parser = super().get_parser(prog_name) parser.add_argument('tenant_id', help='Tenant ID') @@ -30,17 +32,19 @@ class TenantEntityCommand(Lister): start = parser.parse(parsed_args.start) end = parser.parse(parsed_args.end) entities = self.app.get_client().get_tenant_entities(parsed_args.tenant_id, start, end) + return self.columns, self._format_rows(entities) + + def _format_rows(self, entities): rows = [] for entity in entities: entity_type = entity.get('entity_type') if entity_type == 'instance': - properties = dict(flavor=entity.get('flavor'), image=entity.get('image_meta')) + properties = dict(flavor=entity.get('flavor'), image=entity.get('image_meta', entity.get('os'))) else: properties = dict(volume_type=entity.get('volume_type'), attached_to=entity.get('attached_to')) rows.append((entity.get('entity_id'), entity_type, entity.get('name'), entity.get('start'), entity.get('end'), properties)) - - return ('Entity ID', 'Type', 'Name', 'Start', 'End', 'Properties'), rows + return rows diff --git a/almanachclient/commands/update_instance_entity.py b/almanachclient/commands/update_instance_entity.py new file mode 100644 index 0000000..2ea16b5 --- /dev/null +++ b/almanachclient/commands/update_instance_entity.py @@ -0,0 +1,67 @@ +# Copyright 2017 INAP +# +# 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. + +from cliff.show import ShowOne +from dateutil import parser + + +class UpdateInstanceEntityCommand(ShowOne): + """Update instance entity""" + + columns = ('Tenant ID', 'Instance ID', 'Start', 'End', 'Name', 'Flavor', 'Image') + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument('instance_id', help='Instance ID') + parser.add_argument('--start', help='Start Date') + parser.add_argument('--end', help='End Date') + parser.add_argument('--flavor', help='Flavor') + parser.add_argument('--name', help='Instance Name') + return parser + + def take_action(self, parsed_args): + params = self._parse_arguments(parsed_args) + entity = self.app.get_client().update_instance_entity(parsed_args.instance_id, **params) + return self.columns, self._format_entity(entity) + + def _parse_arguments(self, parsed_args): + params = dict() + + if parsed_args.start: + params['start'] = parser.parse(parsed_args.start) + + if parsed_args.end: + params['end'] = parser.parse(parsed_args.end) + + if parsed_args.flavor: + params['flavor'] = parsed_args.flavor + + if parsed_args.name: + params['name'] = parsed_args.name + + if len(params) == 0: + raise RuntimeError('At least one argument must be provided: start, end, flavor or name') + + return params + + def _format_entity(self, entity): + return ( + entity.get('project_id'), + entity.get('entity_id'), + entity.get('start'), + entity.get('end'), + entity.get('name'), + entity.get('flavor'), + entity.get('image_meta', entity.get('os')), + ) diff --git a/almanachclient/http_client.py b/almanachclient/http_client.py index 2e44688..a4d770b 100644 --- a/almanachclient/http_client.py +++ b/almanachclient/http_client.py @@ -13,6 +13,7 @@ # limitations under the License. import abc +import json import logging import requests @@ -31,10 +32,19 @@ class HttpClient(metaclass=abc.ABCMeta): def _get(self, url, params=None): logger.debug(url) - response = requests.get(url, headers=self._get_headers(), params=params) + return self._parse_response(requests.get(url, headers=self._get_headers(), params=params)) + + def _put(self, url, data, params=None): + logger.debug(url) + return self._parse_response(requests.put(url, + headers=self._get_headers(), + params=params, + data=json.dumps(data))) + + def _parse_response(self, response, expected_status=200): body = response.json() - if response.status_code != 200: + if response.status_code != expected_status: raise exceptions.HTTPError('{} ({})'.format(body.get('error') or 'HTTP Error', response.status_code)) return body diff --git a/almanachclient/shell.py b/almanachclient/shell.py index 09fd697..9dede15 100644 --- a/almanachclient/shell.py +++ b/almanachclient/shell.py @@ -19,7 +19,8 @@ from cliff import app from cliff import commandmanager from almanachclient.commands.endpoint import EndpointCommand -from almanachclient.commands.tenant_entities import TenantEntityCommand +from almanachclient.commands.list_entities import ListEntityCommand +from almanachclient.commands.update_instance_entity import UpdateInstanceEntityCommand from almanachclient.commands.version import VersionCommand from almanachclient.keystone_client import KeystoneClient from almanachclient.v1.client import Client @@ -30,7 +31,8 @@ class AlmanachCommandManager(commandmanager.CommandManager): SHELL_COMMANDS = { 'version': VersionCommand, 'endpoint': EndpointCommand, - 'tenant entities': TenantEntityCommand, + 'list entities': ListEntityCommand, + 'update instance': UpdateInstanceEntityCommand, } def load_commands(self, namespace): diff --git a/almanachclient/tests/commands/__init__.py b/almanachclient/tests/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/almanachclient/tests/commands/test_update_instance_entity.py b/almanachclient/tests/commands/test_update_instance_entity.py new file mode 100644 index 0000000..5e8617b --- /dev/null +++ b/almanachclient/tests/commands/test_update_instance_entity.py @@ -0,0 +1,55 @@ +# Copyright 2017 INAP +# +# 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. + +from argparse import Namespace +import datetime +from unittest import mock + + +from almanachclient.commands.update_instance_entity import UpdateInstanceEntityCommand + +from almanachclient.tests import base + + +class TestUpdateInstanceEntityCommand(base.TestCase): + + def setUp(self): + super().setUp() + self.app = mock.Mock() + self.app_args = mock.Mock() + self.args = Namespace(instance_id=None, start=None, end=None, flavor=None, name=None) + + self.client = mock.Mock() + self.app.get_client.return_value = self.client + self.command = UpdateInstanceEntityCommand(self.app, self.app_args) + + def test_without_required_params(self): + self.assertRaises(RuntimeError, self.command.take_action, self.args) + + def test_without_optional_params(self): + self.args.instance_id = 'some uuid' + self.assertRaises(RuntimeError, self.command.take_action, self.args) + + def test_with_date_arguments(self): + self.args.instance_id = 'some uuid' + self.args.start = '2017-01-01' + self.client.update_instance_entity.return_value = {'entity_id': 'some uuid', 'project_id': 'tenant id'} + + expected = (('Tenant ID', 'Instance ID', 'Start', 'End', 'Name', 'Flavor', 'Image'), + ('tenant id', 'some uuid', None, None, None, None, None)) + + self.assertEqual(expected, self.command.take_action(self.args)) + + self.client.update_instance_entity.assert_called_once_with(self.args.instance_id, + start=datetime.datetime(2017, 1, 1, 0, 0)) diff --git a/almanachclient/tests/v1/test_client.py b/almanachclient/tests/v1/test_client.py index 0ed17e7..5f3721d 100644 --- a/almanachclient/tests/v1/test_client.py +++ b/almanachclient/tests/v1/test_client.py @@ -13,6 +13,7 @@ # limitations under the License. from datetime import datetime +import json from unittest import mock from almanachclient import exceptions @@ -21,6 +22,7 @@ from almanachclient.v1.client import Client class TestClient(base.TestCase): + def setUp(self): super().setUp() self.url = 'http://almanach_url' @@ -66,10 +68,26 @@ class TestClient(base.TestCase): start = datetime.now() end = datetime.now() - params = dict(start=start.strftime(Client.DATE_FORMAT), end=end.strftime(Client.DATE_FORMAT)) + params = dict(start=start.strftime(Client.DATE_FORMAT_QS), end=end.strftime(Client.DATE_FORMAT_QS)) self.assertEqual(expected, self.client.get_tenant_entities('my_tenant_id', start, end)) requests.assert_called_once_with('{}{}'.format(self.url, '/v1/project/my_tenant_id/entities'), params=params, headers=self.headers) + + @mock.patch('requests.put') + def test_update_instance_entity(self, requests): + response = mock.Mock() + expected = dict(name='some entity') + + requests.return_value = response + response.json.return_value = expected + response.status_code = 200 + + self.assertEqual(expected, self.client.update_instance_entity('my_instance_id', name='some entity')) + + requests.assert_called_once_with('{}{}'.format(self.url, '/v1/entity/instance/my_instance_id'), + params=None, + data=json.dumps({'name': 'some entity'}), + headers=self.headers) diff --git a/almanachclient/v1/client.py b/almanachclient/v1/client.py index 31ee3d2..e25d45b 100644 --- a/almanachclient/v1/client.py +++ b/almanachclient/v1/client.py @@ -16,7 +16,8 @@ from almanachclient.http_client import HttpClient class Client(HttpClient): - DATE_FORMAT = '%Y-%m-%d %H:%M:%S.%f' + DATE_FORMAT_QS = '%Y-%m-%d %H:%M:%S.%f' + DATE_FORMAT_BODY = '%Y-%m-%dT%H:%M:%S.%fZ' api_version = 'v1' @@ -28,5 +29,20 @@ class Client(HttpClient): def get_tenant_entities(self, tenant_id, start, end): url = '{}/{}/project/{}/entities'.format(self.url, self.api_version, tenant_id) - params = {'start': start.strftime(self.DATE_FORMAT), 'end': end.strftime(self.DATE_FORMAT)} + params = {'start': self._format_qs_datetime(start), 'end': self._format_qs_datetime(end)} return self._get(url, params) + + def update_instance_entity(self, instance_id, **kwargs): + url = '{}/{}/entity/instance/{}'.format(self.url, self.api_version, instance_id) + + for param in ['start', 'end']: + if param in kwargs: + kwargs[param] = self._format_body_datetime(kwargs[param]) + + return self._put(url, kwargs) + + def _format_body_datetime(self, value): + return value.strftime(self.DATE_FORMAT_BODY) + + def _format_qs_datetime(self, value): + return value.strftime(self.DATE_FORMAT_QS) diff --git a/doc/source/usage.rst b/doc/source/usage.rst index eaa4162..52a397d 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -14,6 +14,8 @@ Environment variables Get server version ------------------ +Usage: :code:`almanach-client version` + .. code:: bash almanach-client version @@ -23,6 +25,8 @@ Get server version Get Endpoint URL ---------------- +Usage: :code:`almanach-client endpoint` + .. code:: bash almanach-client endpoint @@ -32,9 +36,11 @@ Get Endpoint URL Get tenant entities ------------------- +Usage: :code:`almanach-client list entities ` + .. code:: bash - almanach-client tenant entities bca89ae64dba46b8b74653d8d9ae8364 2016-01-01 2017-05-30 + almanach-client list entities bca89ae64dba46b8b74653d8d9ae8364 2016-01-01 2017-05-30 +--------------------------------------+----------+--------+---------------------------+------+---------------------------------------------------------------------------------------+ | Entity ID | Type | Name | Start | End | Properties | @@ -43,3 +49,32 @@ Get tenant entities | f0690323-c394-4848-a272-964aad6431aa | instance | vm02 | 2017-05-15 18:31:42+00:00 | None | {'image': {'distro': 'centos', 'version': '7', 'os_type': 'linux'}, 'flavor': 'A1.1'} | | 3e3b22e6-a10c-4c00-b8e5-05fcc8422b11 | volume | vol01 | 2017-05-15 19:11:14+00:00 | None | {'attached_to': [], 'volume_type': 'solidfire0'} | +--------------------------------------+----------+--------+---------------------------+------+---------------------------------------------------------------------------------------+ + +Update Instance Entity +---------------------- + +Usage: :code:`almanach-client update instance --start --end --name --flavor ` + +.. code:: bash + + almanach-client update instance 8c3bc3aa-28d6-4863-b5ae-72e1b415f79d --name vm03 + + +-------------+----------------------------------------------------------+ + | Field | Value | + +-------------+----------------------------------------------------------+ + | Tenant ID | bca89ae64dba46b8b74653d8d9ae8364 | + | Instance ID | 8c3bc3aa-28d6-4863-b5ae-72e1b415f79d | + | Start | 2017-05-09 14:19:14+00:00 | + | End | None | + | Name | vm03 | + | Flavor | A1.1 | + | Image | {'distro': 'centos', 'version': '7', 'os_type': 'linux'} | + +-------------+----------------------------------------------------------+ + +Arguments: + +* :code:`instance_id`: Instance ID +* :code:`start`: Start date (ISO8601 format) +* :code:`end`: Start date (ISO8601 format) +* :code:`name`: Start date (ISO8601 format) +* :code:`flavor`: Start date (ISO8601 format)