From d263c9ae17563d99e20dc85e853511b355cd706d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Mon, 15 May 2017 15:57:03 -0400 Subject: [PATCH] Add API call to fetch tenant entities --- almanachclient/commands/endpoint.py | 2 +- almanachclient/commands/tenant_entities.py | 46 ++++++++++++++++++++++ almanachclient/commands/version.py | 2 +- almanachclient/http_client.py | 15 +++++-- almanachclient/shell.py | 10 ++++- almanachclient/tests/v1/test_client.py | 40 ++++++++++++++++++- almanachclient/v1/client.py | 10 +++-- requirements.txt | 3 +- 8 files changed, 114 insertions(+), 14 deletions(-) create mode 100644 almanachclient/commands/tenant_entities.py diff --git a/almanachclient/commands/endpoint.py b/almanachclient/commands/endpoint.py index 04855b2..3569da2 100644 --- a/almanachclient/commands/endpoint.py +++ b/almanachclient/commands/endpoint.py @@ -16,7 +16,7 @@ from cliff.command import Command class EndpointCommand(Command): - """Show the Almanach Endpoint URL""" + """Show Almanach Endpoint URL""" def take_action(self, parsed_args): self.app.stdout.write('{}\n'.format(self.app.get_client().get_url())) diff --git a/almanachclient/commands/tenant_entities.py b/almanachclient/commands/tenant_entities.py new file mode 100644 index 0000000..da2ed7c --- /dev/null +++ b/almanachclient/commands/tenant_entities.py @@ -0,0 +1,46 @@ +# 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.lister import Lister +from dateutil import parser + + +class TenantEntityCommand(Lister): + """Show all entities for a given tenant""" + + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) + parser.add_argument('tenant_id', help='Tenant ID') + parser.add_argument('start', help='Start Date') + parser.add_argument('end', help='End Date') + return parser + + def take_action(self, parsed_args): + 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) + 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')) + 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 diff --git a/almanachclient/commands/version.py b/almanachclient/commands/version.py index 25c1261..41b0aed 100644 --- a/almanachclient/commands/version.py +++ b/almanachclient/commands/version.py @@ -16,7 +16,7 @@ from cliff.command import Command class VersionCommand(Command): - """Show the Almanach version number""" + """Show Almanach version number""" def take_action(self, parsed_args): info = self.app.get_client().get_info() diff --git a/almanachclient/http_client.py b/almanachclient/http_client.py index 6bdb544..2e44688 100644 --- a/almanachclient/http_client.py +++ b/almanachclient/http_client.py @@ -24,18 +24,25 @@ logger = logging.getLogger(__name__) class HttpClient(metaclass=abc.ABCMeta): - def _get(self, url): + + def __init__(self, url, token=None): + self.url = url + self.token = token + + def _get(self, url, params=None): logger.debug(url) - response = requests.get(url, headers=self._get_headers()) + response = requests.get(url, headers=self._get_headers(), params=params) + body = response.json() if response.status_code != 200: - raise exceptions.HTTPError('HTTP Error ({})'.format(response.status_code)) + raise exceptions.HTTPError('{} ({})'.format(body.get('error') or 'HTTP Error', response.status_code)) - return response.json() + return body def _get_headers(self): return { 'Content-Type': 'application/json', 'Accept': 'application/json', 'User-Agent': 'python-almanachclient/{}'.format(client_version.__version__), + 'X-Auth-Token': self.token, } diff --git a/almanachclient/shell.py b/almanachclient/shell.py index d545d31..a289e48 100644 --- a/almanachclient/shell.py +++ b/almanachclient/shell.py @@ -19,6 +19,7 @@ from cliff import app from cliff import commandmanager from almanachclient.commands.endpoint import EndpointCommand +from almanachclient.commands.tenant_entities import TenantEntityCommand from almanachclient.commands.version import VersionCommand from almanachclient.keystone_client import KeystoneClient from almanachclient.v1.client import Client @@ -29,6 +30,7 @@ class AlmanachCommandManager(commandmanager.CommandManager): SHELL_COMMANDS = { 'version': VersionCommand, 'endpoint': EndpointCommand, + 'tenant entities': TenantEntityCommand, } def load_commands(self, namespace): @@ -66,8 +68,12 @@ class AlmanachApp(app.App): help='OpenStack username (Env: OS_USERNAME).') parser.add_argument('--almanach-service', - default=os.environ.get('ALMANACH_SERVICE'), + default=os.environ.get('ALMANACH_SERVICE', 'almanach'), help='Almanach keystone service name (Env: ALMANACH_SERVICE).') + + parser.add_argument('--almanach-token', + default=os.environ.get('ALMANACH_TOKEN'), + help='Almanach API token (Env: ALMANACH_TOKEN).') return parser def get_client(self): @@ -77,7 +83,7 @@ class AlmanachApp(app.App): service=self.options.almanach_service, region_name=self.options.os_region_name) - return Client(keystone.get_endpoint_url()) + return Client(keystone.get_endpoint_url(), token=self.options.almanach_token) def main(argv=sys.argv[1:]): diff --git a/almanachclient/tests/v1/test_client.py b/almanachclient/tests/v1/test_client.py index ed3a6f6..0ed17e7 100644 --- a/almanachclient/tests/v1/test_client.py +++ b/almanachclient/tests/v1/test_client.py @@ -12,8 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +from datetime import datetime from unittest import mock +from almanachclient import exceptions from almanachclient.tests import base from almanachclient.v1.client import Client @@ -21,8 +23,14 @@ from almanachclient.v1.client import Client class TestClient(base.TestCase): def setUp(self): super().setUp() - self.almanach_url = 'http://almanach_url' - self.client = Client(self.almanach_url) + self.url = 'http://almanach_url' + self.token = 'token' + self.headers = {'Content-Type': 'application/json', + 'User-Agent': 'python-almanachclient/0.0.1', + 'X-Auth-Token': self.token, + 'Accept': 'application/json'} + + self.client = Client(self.url, self.token) @mock.patch('requests.get') def test_get_info(self, requests): @@ -37,3 +45,31 @@ class TestClient(base.TestCase): response.status_code = 200 self.assertEqual(expected, self.client.get_info()) + requests.assert_called_once_with('{}{}'.format(self.url, '/v1/info'), headers=self.headers, params=None) + + @mock.patch('requests.get') + def test_get_info_with_http_error(self, requests): + response = mock.Mock() + requests.return_value = response + response.status_code = 500 + + self.assertRaises(exceptions.HTTPError, self.client.get_info) + + @mock.patch('requests.get') + def test_get_tenant_entities(self, requests): + response = mock.Mock() + expected = [mock.Mock()] + + requests.return_value = response + response.json.return_value = expected + response.status_code = 200 + + start = datetime.now() + end = datetime.now() + params = dict(start=start.strftime(Client.DATE_FORMAT), end=end.strftime(Client.DATE_FORMAT)) + + 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) diff --git a/almanachclient/v1/client.py b/almanachclient/v1/client.py index 187e119..31ee3d2 100644 --- a/almanachclient/v1/client.py +++ b/almanachclient/v1/client.py @@ -16,13 +16,17 @@ from almanachclient.http_client import HttpClient class Client(HttpClient): - api_version = 'v1' + DATE_FORMAT = '%Y-%m-%d %H:%M:%S.%f' - def __init__(self, url): - self.url = url + api_version = 'v1' def get_url(self): return self.url def get_info(self): return self._get('{}/{}/info'.format(self.url, self.api_version)) + + 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)} + return self._get(url, params) diff --git a/requirements.txt b/requirements.txt index b50f9f1..f0c02a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ pbr>=2.0.0,!=2.1.0 # Apache-2.0 cliff>=2.6.0 # Apache-2.0 requests>=2.10.0,!=2.12.2,!=2.13.0 # Apache-2.0 -python-keystoneclient>=3.8.0 # Apache-2.0 \ No newline at end of file +python-keystoneclient>=3.8.0 # Apache-2.0 +python-dateutil>=2.4.2 # BSD \ No newline at end of file