From a3448e55107c77d3bbeb8914005b31783127ab34 Mon Sep 17 00:00:00 2001 From: Hiroki Ito Date: Mon, 28 Aug 2017 06:48:54 +0000 Subject: [PATCH] Migrate to keystoneauth1 For authentication, the community recommends the use of keystoneauth1 instead of python-keystoneclient. Therefore, Blazar should follow this trend and migrate to keystoneauth1. This patch enables blazarclient to use keystoneauth1 for authentications and REST API requests and also enables use of project_id, project_name, project_domain_id, project_domain_name, user_domain_id and user_domain_name for authentication. Change-Id: I08c8b753972c27b4e6bbe07a8aa51e0e72fbc56d Closes-Bug: #1661215 --- blazarclient/base.py | 136 ------------------------------ blazarclient/shell.py | 138 +++++++++++++++++++------------ blazarclient/tests/test_base.py | 102 ----------------------- blazarclient/tests/test_shell.py | 3 + blazarclient/v1/client.py | 18 ++-- blazarclient/v1/hosts.py | 26 ++++-- blazarclient/v1/leases.py | 23 ++++-- requirements.txt | 3 +- 8 files changed, 132 insertions(+), 317 deletions(-) delete mode 100644 blazarclient/base.py delete mode 100644 blazarclient/tests/test_base.py diff --git a/blazarclient/base.py b/blazarclient/base.py deleted file mode 100644 index 1c3b61b..0000000 --- a/blazarclient/base.py +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright (c) 2013 Mirantis Inc. -# -# 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. - - -import json - -import requests - -from blazarclient import exception -from blazarclient.i18n import _ - - -class BaseClientManager(object): - """Base manager to interact with a particular type of API. - - There are environments, nodes and jobs types of API requests. - Manager provides CRUD operations for them. - """ - def __init__(self, blazar_url, auth_token): - self.blazar_url = blazar_url - self.auth_token = auth_token - - USER_AGENT = 'python-blazarclient' - - def _get(self, url, response_key): - """Sends get request to Blazar. - - :param url: URL to the wanted Blazar resource. - :type url: str - - :param response_key: Type of resource (environment, node, job). - :type response_key: str - - :returns: Resource entity (entities) that was (were) asked. - :rtype: dict | list - """ - resp, body = self.request(url, 'GET') - return body[response_key] - - def _create(self, url, body, response_key): - """Sends create request to Blazar. - - :param url: URL to the wanted Blazar resource. - :type url: str - - :param body: Values resource to be created from. - :type body: dict - - :param response_key: Type of resource (environment, node, job). - :type response_key: str - - :returns: Resource entity that was created. - :rtype: dict - """ - resp, body = self.request(url, 'POST', body=body) - return body[response_key] - - def _delete(self, url): - """Sends delete request to Blazar. - - :param url: URL to the wanted Blazar resource. - :type url: str - """ - resp, body = self.request(url, 'DELETE') - - def _update(self, url, body, response_key=None): - """Sends update request to Blazar. - - :param url: URL to the wanted Blazar resource. - :type url: str - - :param body: Values resource to be updated from. - :type body: dict - - :param response_key: Type of resource (environment, node, job). - :type response_key: str - - :returns: Resource entity that was updated. - :rtype: dict - """ - resp, body = self.request(url, 'PUT', body=body) - return body[response_key] - - def request(self, url, method, **kwargs): - """Base request method. - - Adds specific headers and URL prefix to the request. - - :param url: Resource URL. - :type url: str - - :param method: Method to be called (GET, POST, PUT, DELETE). - :type method: str - - :returns: Response and body. - :rtype: tuple - """ - kwargs.setdefault('headers', kwargs.get('headers', {})) - kwargs['headers']['User-Agent'] = self.USER_AGENT - kwargs['headers']['Accept'] = 'application/json' - kwargs['headers']['x-auth-token'] = self.auth_token - - if 'body' in kwargs: - kwargs['headers']['Content-Type'] = 'application/json' - kwargs['data'] = json.dumps(kwargs['body']) - del kwargs['body'] - - resp = requests.request(method, self.blazar_url + url, **kwargs) - - try: - body = json.loads(resp.text) - except ValueError: - body = None - - if resp.status_code >= 400: - if body is not None: - error_message = body.get('error_message', body) - else: - error_message = resp.text - - body = _("ERROR: {0}").format(error_message) - raise exception.BlazarClientException(body, code=resp.status_code) - - return resp, body diff --git a/blazarclient/shell.py b/blazarclient/shell.py index 89cd931..69138a8 100644 --- a/blazarclient/shell.py +++ b/blazarclient/shell.py @@ -25,8 +25,8 @@ import sys from cliff import app from cliff import commandmanager -from keystoneclient import client as keystone_client -from keystoneclient import exceptions as keystone_exceptions +from keystoneauth1 import identity +from keystoneauth1 import session from oslo_utils import encodeutils import six @@ -195,6 +195,36 @@ class BlazarShell(app.App): parser.add_argument( '--os_auth_url', help=argparse.SUPPRESS) + parser.add_argument( + '--os-project-name', metavar='', + default=env('OS_PROJECT_NAME'), + help='Authentication project name (Env: OS_PROJECT_NAME)') + parser.add_argument( + '--os_project_name', + help=argparse.SUPPRESS) + parser.add_argument( + '--os-project-id', metavar='', + default=env('OS_PROJECT_ID'), + help='Authentication project ID (Env: OS_PROJECT_ID)') + parser.add_argument( + '--os_project_id', + help=argparse.SUPPRESS) + parser.add_argument( + '--os-project-domain-name', metavar='', + default=env('OS_PROJECT_DOMAIN_NAME'), + help='Authentication project domain name ' + '(Env: OS_PROJECT_DOMAIN_NAME)') + parser.add_argument( + '--os_project_domain_name', + help=argparse.SUPPRESS) + parser.add_argument( + '--os-project-domain-id', metavar='', + default=env('OS_PROJECT_DOMAIN_ID'), + help='Authentication project domain ID ' + '(Env: OS_PROJECT_DOMAIN_ID)') + parser.add_argument( + '--os_project_domain_id', + help=argparse.SUPPRESS) parser.add_argument( '--os-tenant-name', metavar='', default=env('OS_TENANT_NAME'), @@ -213,6 +243,20 @@ class BlazarShell(app.App): parser.add_argument( '--os_username', help=argparse.SUPPRESS) + parser.add_argument( + '--os-user-domain-name', metavar='', + default=env('OS_USER_DOMAIN_NAME'), + help='Authentication user domain name (Env: OS_USER_DOMAIN_NAME)') + parser.add_argument( + '--os_user_domain_name', + help=argparse.SUPPRESS) + parser.add_argument( + '--os-user-domain-id', metavar='', + default=env('OS_USER_DOMAIN_ID'), + help='Authentication user domain ID (Env: OS_USER_DOMAIN_ID)') + parser.add_argument( + '--os_user_domain_id', + help=argparse.SUPPRESS) parser.add_argument( '--os-password', metavar='', default=utils.env('OS_PASSWORD'), @@ -234,6 +278,10 @@ class BlazarShell(app.App): parser.add_argument( '--os_token', help=argparse.SUPPRESS) + parser.add_argument( + '--service-type', metavar='', + default=env('BLAZAR_SERVICE_TYPE', default='reservation'), + help='Defaults to env[BLAZAR_SERVICE_TYPE] or reservation.') parser.add_argument( '--endpoint-type', metavar='', default=env('OS_ENDPOINT_TYPE', default='publicURL'), @@ -373,61 +421,45 @@ class BlazarShell(app.App): return result def authenticate_user(self): - """Make sure the user has provided all of the authentication - info we need. - """ - if not self.options.os_token: - if not self.options.os_username: - raise exception.CommandError( - "You must provide a username via" - " either --os-username or env[OS_USERNAME]") + """Authenticate user and set client by using passed params.""" - if not self.options.os_password: - raise exception.CommandError( - "You must provide a password via" - " either --os-password or env[OS_PASSWORD]") + if self.options.os_token: + auth = identity.Token( + auth_url=self.options.os_auth_url, + token=self.options.os_token, + tenant_id=self.options.os_tenant_id, + tenant_name=self.options.os_tenant_name, + project_id=self.options.os_project_id, + project_name=self.options.os_project_name, + project_domain_id=self.options.os_project_domain_id, + project_domain_name=self.options.os_project_domain_name + ) + else: + auth = identity.Password( + auth_url=self.options.os_auth_url, + username=self.options.os_username, + tenant_id=self.options.os_tenant_id, + tenant_name=self.options.os_tenant_name, + password=self.options.os_password, + project_id=self.options.os_project_id, + project_name=self.options.os_project_name, + project_domain_id=self.options.os_project_domain_id, + project_domain_name=self.options.os_project_domain_name, + user_domain_id=self.options.os_user_domain_id, + user_domain_name=self.options.os_user_domain_name + ) - if (not self.options.os_tenant_name and - not self.options.os_tenant_id): - raise exception.CommandError( - "You must provide a tenant_name or tenant_id via" - " --os-tenant-name, env[OS_TENANT_NAME]" - " --os-tenant-id, or via env[OS_TENANT_ID]") - - if not self.options.os_auth_url: - raise exception.CommandError( - "You must provide an auth url via" - " either --os-auth-url or via env[OS_AUTH_URL]") - - keystone = keystone_client.Client( - token=self.options.os_token, - auth_url=self.options.os_auth_url, - tenant_id=self.options.os_tenant_id, - tenant_name=self.options.os_tenant_name, - password=self.options.os_password, - region_name=self.options.os_region_name, - username=self.options.os_username, - insecure=self.options.insecure, - cert=self.options.os_cacert + sess = session.Session( + auth=auth, + verify=(self.options.os_cacert or not self.options.insecure) ) - auth = keystone.authenticate() - - if auth: - try: - blazar_url = keystone.service_catalog.url_for( - service_type='reservation' - ) - except keystone_exceptions.EndpointNotFound: - raise exception.NoBlazarEndpoint() - else: - raise exception.NotAuthorized("User %s is not authorized." % - self.options.os_username) - - client = blazar_client.Client(self.options.os_reservation_api_version, - blazar_url=blazar_url, - auth_token=keystone.auth_token) - self.client = client + self.client = blazar_client.Client( + self.options.os_reservation_api_version, + session=sess, + service_type=self.options.service_type, + interface=self.options.endpoint_type + ) return def initialize_app(self, argv): diff --git a/blazarclient/tests/test_base.py b/blazarclient/tests/test_base.py deleted file mode 100644 index e5f6c50..0000000 --- a/blazarclient/tests/test_base.py +++ /dev/null @@ -1,102 +0,0 @@ -# Copyright (c) 2014 Mirantis Inc. -# -# 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. - - -import requests - -from blazarclient import base -from blazarclient import exception -from blazarclient import tests - - -class BaseClientManagerTestCase(tests.TestCase): - - def setUp(self): - super(BaseClientManagerTestCase, self).setUp() - - self.url = "www.fake.com/reservation" - self.token = "aaa-bbb-ccc" - self.fake_key = "fake_key" - self.response = "RESPONSE" - self.exception = exception - - self.manager = base.BaseClientManager(self.url, self.token) - self.request = self.patch(requests, "request") - - def test_get(self): - self.patch( - self.manager, "request").return_value = ( - self.response, {"fake_key": "FAKE"}) - self.assertEqual(self.manager._get(self.url, self.fake_key), "FAKE") - - def test_create(self): - self.patch( - self.manager, "request").return_value = ( - self.response, {"fake_key": "FAKE"}) - self.assertEqual(self.manager._create(self.url, {}, self.fake_key), - "FAKE") - - def test_delete(self): - request = self.patch(self.manager, "request") - request.return_value = (self.response, {"fake_key": "FAKE"}) - self.manager._delete(self.url) - request.assert_called_once_with(self.url, "DELETE") - - def test_update(self): - self.patch( - self.manager, "request").return_value = ( - self.response, {"fake_key": "FAKE"}) - self.assertEqual(self.manager._update(self.url, {}, self.fake_key), - "FAKE") - - def test_request_ok_with_body(self): - self.request.return_value.status_code = 200 - self.request.return_value.text = '{"key": "value"}' - - kwargs = {"body": {"key": "value"}} - - self.assertEqual(( - self.request(), {"key": "value"}), - self.manager.request(self.url, "POST", **kwargs)) - - def test_request_ok_without_body(self): - self.request.return_value.status_code = 200 - self.request.return_value.text = "key" - - kwargs = {"body": "key"} - - self.assertEqual(( - self.request(), None), - self.manager.request(self.url, "POST", **kwargs)) - - def test_request_fail_with_body(self): - self.request.return_value.status_code = 400 - self.request.return_value.text = '{"key": "value"}' - - kwargs = {"body": {"key": "value"}} - - self.assertRaises(exception.BlazarClientException, - self.manager.request, - self.url, "POST", **kwargs) - - def test_request_fail_without_body(self): - self.request.return_value.status_code = 400 - self.request.return_value.text = "REAL_ERROR" - - kwargs = {"body": "key"} - - self.assertRaises(exception.BlazarClientException, - self.manager.request, - self.url, "POST", **kwargs) diff --git a/blazarclient/tests/test_shell.py b/blazarclient/tests/test_shell.py index 8e7d1fe..4f813f2 100644 --- a/blazarclient/tests/test_shell.py +++ b/blazarclient/tests/test_shell.py @@ -29,8 +29,11 @@ from blazarclient import shell from blazarclient import tests FAKE_ENV = {'OS_USERNAME': 'username', + 'OS_USER_DOMAIN_ID': 'user_domain_id', 'OS_PASSWORD': 'password', 'OS_TENANT_NAME': 'tenant_name', + 'OS_PROJECT_NAME': 'project_name', + 'OS_PROJECT_DOMAIN_ID': 'project_domain_id', 'OS_AUTH_URL': 'http://no.where'} diff --git a/blazarclient/v1/client.py b/blazarclient/v1/client.py index 946d78a..7c0d423 100644 --- a/blazarclient/v1/client.py +++ b/blazarclient/v1/client.py @@ -31,11 +31,15 @@ class Client(object): ... """ - def __init__(self, blazar_url, auth_token): - self.blazar_url = blazar_url - self.auth_token = auth_token + def __init__(self, session, *args, **kwargs): + self.session = session + self.version = '1' - self.lease = leases.LeaseClientManager(self.blazar_url, - self.auth_token) - self.host = hosts.ComputeHostClientManager(self.blazar_url, - self.auth_token) + self.lease = leases.LeaseClientManager(session=self.session, + version=self.version, + *args, + **kwargs) + self.host = hosts.ComputeHostClientManager(session=self.session, + version=self.version, + *args, + **kwargs) diff --git a/blazarclient/v1/hosts.py b/blazarclient/v1/hosts.py index de6dddd..2303f44 100644 --- a/blazarclient/v1/hosts.py +++ b/blazarclient/v1/hosts.py @@ -13,38 +13,46 @@ # See the License for the specific language governing permissions and # limitations under the License. -from blazarclient import base +from keystoneauth1 import adapter + from blazarclient.i18n import _ -class ComputeHostClientManager(base.BaseClientManager): +class ComputeHostClientManager(adapter.LegacyJsonAdapter): """Manager for the ComputeHost connected requests.""" + client_name = 'python-blazarclient' + def create(self, name, **kwargs): """Creates host from values passed.""" values = {'name': name} values.update(**kwargs) - - return self._create('/os-hosts', values, response_key='host') + resp, body = self.post('/os-hosts', body=values) + return body['host'] def get(self, host_id): """Describes host specifications such as name and details.""" - return self._get('/os-hosts/%s' % host_id, 'host') + resp, body = super(ComputeHostClientManager, + self).get('/os-hosts/%s' % host_id) + return body['host'] def update(self, host_id, values): """Update attributes of the host.""" if not values: return _('No values to update passed.') - return self._update('/os-hosts/%s' % host_id, values, - response_key='host') + resp, body = self.put('/os-hosts/%s' % host_id, body=values) + return body['host'] def delete(self, host_id): """Deletes host with specified ID.""" - self._delete('/os-hosts/%s' % host_id) + resp, body = super(ComputeHostClientManager, + self).delete('/os-hosts/%s' % host_id) def list(self, sort_by=None): """List all hosts.""" - hosts = self._get('/os-hosts', 'hosts') + resp, body = super(ComputeHostClientManager, + self).get('/os-hosts') + hosts = body['hosts'] if sort_by: hosts = sorted(hosts, key=lambda l: l[sort_by]) return hosts diff --git a/blazarclient/v1/leases.py b/blazarclient/v1/leases.py index dd0ba05..45207e3 100644 --- a/blazarclient/v1/leases.py +++ b/blazarclient/v1/leases.py @@ -13,29 +13,34 @@ # See the License for the specific language governing permissions and # limitations under the License. +from keystoneauth1 import adapter from oslo_utils import timeutils -from blazarclient import base from blazarclient.i18n import _ from blazarclient import utils -class LeaseClientManager(base.BaseClientManager): +class LeaseClientManager(adapter.LegacyJsonAdapter): """Manager for the lease connected requests.""" + client_name = 'python-blazarclient' + def create(self, name, start, end, reservations, events, before_end=None): """Creates lease from values passed.""" values = {'name': name, 'start_date': start, 'end_date': end, 'reservations': reservations, 'events': events, 'before_end_date': before_end} - return self._create('/leases', values, 'lease') + resp, body = self.post('/leases', body=values) + return body['lease'] def get(self, lease_id): """Describes lease specifications such as name, status and locked condition. """ - return self._get('/leases/%s' % lease_id, 'lease') + resp, body = super(LeaseClientManager, + self).get('/leases/%s' % lease_id) + return body['lease'] def update(self, lease_id, name=None, prolong_for=None, reduce_by=None, end_date=None, advance_by=None, defer_by=None, start_date=None, @@ -76,16 +81,18 @@ class LeaseClientManager(base.BaseClientManager): if not values: return _('No values to update passed.') - return self._update('/leases/%s' % lease_id, values, - response_key='lease') + resp, body = self.put('/leases/%s' % lease_id, body=values) + return body['lease'] def delete(self, lease_id): """Deletes lease with specified ID.""" - self._delete('/leases/%s' % lease_id) + resp, body = super(LeaseClientManager, + self).delete('/leases/%s' % lease_id) def list(self, sort_by=None): """List all leases.""" - leases = self._get('/leases', 'leases') + resp, body = super(LeaseClientManager, self).get('/leases') + leases = body['leases'] if sort_by: leases = sorted(leases, key=lambda l: l[sort_by]) return leases diff --git a/requirements.txt b/requirements.txt index 1bf7a94..dbf8863 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,10 +4,9 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 cliff>=2.8.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD -python-keystoneclient>=3.8.0 # Apache-2.0 -requests>=2.14.2 # Apache-2.0 six>=1.9.0 # MIT Babel!=2.4.0,>=2.3.4 # BSD oslo.i18n>=3.15.3 # Apache-2.0 oslo.log>=3.30.0 # Apache-2.0 oslo.utils>=3.28.0 # Apache-2.0 +keystoneauth1>=3.2.0 # Apache-2.0