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
This commit is contained in:
Hiroki Ito 2017-08-28 06:48:54 +00:00
parent 443adb3659
commit a3448e5510
8 changed files with 132 additions and 317 deletions

View File

@ -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

View File

@ -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='<auth-project-name>',
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='<auth-project-id>',
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='<auth-project-domain-name>',
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='<auth-project-domain-id>',
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='<auth-tenant-name>',
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='<auth-user-domain-name>',
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='<auth-user-domain-id>',
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='<auth-password>',
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='<service-type>',
default=env('BLAZAR_SERVICE_TYPE', default='reservation'),
help='Defaults to env[BLAZAR_SERVICE_TYPE] or reservation.')
parser.add_argument(
'--endpoint-type', metavar='<endpoint-type>',
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):

View File

@ -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)

View File

@ -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'}

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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