Add keystone-session support
Currently python-manilaclient doesn't support keystone sessions unlike to others clients. keystoneclient.session.Session gives end-user possibility to share common authentication and request parameters between a variety of services. - Add python-keystoneclient to requirements.txt - Move authentication logic to v1/client.py - Move HttpClient to separate module - Simplify SecretsHelper in shell.py - Replace service_catalog module by keystoneclient.service_catalog - Add appropriate unit tests Implements bp add-keystone-session-support Change-Id: I7d1c50fa31d9e2f32bb518a78a02bc12b114d0ab
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# Copyright 2011 Piston Cloud Computing, Inc.
|
||||
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@@ -19,388 +20,14 @@
|
||||
OpenStack Client interface. Handles the REST calls and responses.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
try:
|
||||
from eventlet import sleep
|
||||
except ImportError:
|
||||
from time import sleep # noqa
|
||||
|
||||
from oslo.serialization import jsonutils
|
||||
from oslo.utils import importutils
|
||||
import requests
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from manilaclient import exceptions
|
||||
from manilaclient import service_catalog
|
||||
|
||||
|
||||
class HTTPClient(object):
|
||||
|
||||
USER_AGENT = 'python-manilaclient'
|
||||
|
||||
def __init__(self,
|
||||
user,
|
||||
password,
|
||||
projectid,
|
||||
auth_url,
|
||||
insecure=False,
|
||||
timeout=None,
|
||||
tenant_id=None,
|
||||
proxy_tenant_id=None,
|
||||
proxy_token=None,
|
||||
region_name=None,
|
||||
endpoint_type='publicURL',
|
||||
service_type=None,
|
||||
service_name=None,
|
||||
share_service_name=None,
|
||||
retries=None,
|
||||
http_log_debug=False,
|
||||
cacert=None,
|
||||
os_cache=False):
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.projectid = projectid
|
||||
self.tenant_id = tenant_id
|
||||
self.auth_url = auth_url.rstrip('/')
|
||||
self.version = 'v1'
|
||||
self.region_name = region_name
|
||||
self.endpoint_type = endpoint_type
|
||||
self.service_type = service_type
|
||||
self.service_name = service_name
|
||||
self.share_service_name = share_service_name
|
||||
self.retries = int(retries or 0)
|
||||
self.http_log_debug = http_log_debug
|
||||
|
||||
self.management_url = None
|
||||
self.auth_token = None
|
||||
self.proxy_token = proxy_token
|
||||
self.proxy_tenant_id = proxy_tenant_id
|
||||
|
||||
self.os_cache = os_cache
|
||||
|
||||
if insecure:
|
||||
self.verify_cert = False
|
||||
else:
|
||||
if cacert:
|
||||
self.verify_cert = cacert
|
||||
else:
|
||||
self.verify_cert = True
|
||||
|
||||
self._logger = logging.getLogger(__name__)
|
||||
if self.http_log_debug:
|
||||
ch = logging.StreamHandler()
|
||||
self._logger.setLevel(logging.DEBUG)
|
||||
self._logger.addHandler(ch)
|
||||
if hasattr(requests, 'logging'):
|
||||
requests.logging.getLogger(requests.__name__).addHandler(ch)
|
||||
|
||||
def http_log_req(self, args, kwargs):
|
||||
if not self.http_log_debug:
|
||||
return
|
||||
|
||||
string_parts = ['curl -i']
|
||||
for element in args:
|
||||
if element in ('GET', 'POST', 'DELETE', 'PUT'):
|
||||
string_parts.append(' -X %s' % element)
|
||||
else:
|
||||
string_parts.append(' %s' % element)
|
||||
|
||||
for element in kwargs['headers']:
|
||||
header = ' -H "%s: %s"' % (element, kwargs['headers'][element])
|
||||
string_parts.append(header)
|
||||
|
||||
if 'data' in kwargs:
|
||||
string_parts.append(" -d '%s'" % (kwargs['data']))
|
||||
self._logger.debug("\nREQ: %s\n" % "".join(string_parts))
|
||||
|
||||
def http_log_resp(self, resp):
|
||||
if not self.http_log_debug:
|
||||
return
|
||||
self._logger.debug(
|
||||
"RESP: [%s] %s\nRESP BODY: %s\n",
|
||||
resp.status_code,
|
||||
resp.headers,
|
||||
resp.text)
|
||||
|
||||
def request(self, url, method, **kwargs):
|
||||
kwargs.setdefault('headers', kwargs.get('headers', {}))
|
||||
kwargs['headers']['User-Agent'] = self.USER_AGENT
|
||||
kwargs['headers']['Accept'] = 'application/json'
|
||||
if 'body' in kwargs:
|
||||
kwargs['headers']['Content-Type'] = 'application/json'
|
||||
kwargs['data'] = jsonutils.dumps(kwargs['body'])
|
||||
del kwargs['body']
|
||||
|
||||
self.http_log_req((url, method,), kwargs)
|
||||
resp = requests.request(
|
||||
method,
|
||||
url,
|
||||
verify=self.verify_cert,
|
||||
**kwargs)
|
||||
self.http_log_resp(resp)
|
||||
|
||||
if resp.text:
|
||||
try:
|
||||
body = jsonutils.loads(resp.text)
|
||||
except ValueError:
|
||||
pass
|
||||
body = None
|
||||
else:
|
||||
body = None
|
||||
|
||||
if resp.status_code >= 400:
|
||||
raise exceptions.from_response(resp, method, url)
|
||||
|
||||
return resp, body
|
||||
|
||||
def _cs_request(self, url, method, **kwargs):
|
||||
auth_attempts = 0
|
||||
attempts = 0
|
||||
backoff = 1
|
||||
while True:
|
||||
attempts += 1
|
||||
if not self.management_url or not self.auth_token:
|
||||
self.authenticate()
|
||||
kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token
|
||||
if self.projectid:
|
||||
|
||||
kwargs['headers']['X-Auth-Project-Id'] = self.projectid
|
||||
try:
|
||||
resp, body = self.request(self.management_url + url, method,
|
||||
**kwargs)
|
||||
return resp, body
|
||||
except exceptions.BadRequest as e:
|
||||
if attempts > self.retries:
|
||||
raise
|
||||
except exceptions.Unauthorized:
|
||||
if auth_attempts > 0:
|
||||
raise
|
||||
self._logger.debug("Unauthorized, reauthenticating.")
|
||||
self.management_url = self.auth_token = None
|
||||
# First reauth. Discount this attempt.
|
||||
attempts -= 1
|
||||
auth_attempts += 1
|
||||
continue
|
||||
except exceptions.ClientException as e:
|
||||
if attempts > self.retries:
|
||||
raise
|
||||
if 500 <= e.http_status <= 599:
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
# Catch a connection refused from requests.request
|
||||
self._logger.debug("Connection refused: %s" % e)
|
||||
raise
|
||||
self._logger.debug(
|
||||
"Failed attempt(%s of %s), retrying in %s seconds" %
|
||||
(attempts, self.retries, backoff))
|
||||
sleep(backoff)
|
||||
backoff *= 2
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
return self._cs_request(url, 'GET', **kwargs)
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
return self._cs_request(url, 'POST', **kwargs)
|
||||
|
||||
def put(self, url, **kwargs):
|
||||
return self._cs_request(url, 'PUT', **kwargs)
|
||||
|
||||
def delete(self, url, **kwargs):
|
||||
return self._cs_request(url, 'DELETE', **kwargs)
|
||||
|
||||
def _extract_service_catalog(self, url, resp, body, extract_token=True):
|
||||
"""See what the auth service told us and process the response.
|
||||
|
||||
We may get redirected to another site, fail or actually get
|
||||
back a service catalog with a token and our endpoints.
|
||||
"""
|
||||
method = None
|
||||
if hasattr(resp, 'request') and hasattr(resp.request, 'method'):
|
||||
method = resp.request.method
|
||||
if resp.status_code == 200: # content must always present
|
||||
try:
|
||||
self.auth_url = url
|
||||
self.service_catalog = service_catalog.ServiceCatalog(body)
|
||||
|
||||
if extract_token:
|
||||
self.auth_token = self.service_catalog.get_token()
|
||||
self.tenant_id = self.service_catalog.get_tenant_id()
|
||||
|
||||
management_url = self.service_catalog.url_for(
|
||||
attr='region',
|
||||
filter_value=self.region_name,
|
||||
endpoint_type=self.endpoint_type,
|
||||
service_type=self.service_type,
|
||||
service_name=self.service_name,
|
||||
share_service_name=self.share_service_name)
|
||||
self.management_url = management_url.rstrip('/')
|
||||
return None
|
||||
except exceptions.AmbiguousEndpoints:
|
||||
print("Found more than one valid endpoint. Use a more "
|
||||
"restrictive filter")
|
||||
raise
|
||||
except KeyError:
|
||||
raise exceptions.AuthorizationFailure()
|
||||
except exceptions.EndpointNotFound:
|
||||
print("Could not find any suitable endpoint. Correct region?")
|
||||
raise
|
||||
|
||||
elif resp.status_code == 305:
|
||||
return resp['location']
|
||||
else:
|
||||
raise exceptions.from_response(resp, method, url)
|
||||
|
||||
def _fetch_endpoints_from_auth(self, url):
|
||||
"""Fetch endpoints from auth.
|
||||
|
||||
We have a token, but don't know the final endpoint for
|
||||
the region. We have to go back to the auth service and
|
||||
ask again. This request requires an admin-level token
|
||||
to work. The proxy token supplied could be from a low-level enduser.
|
||||
|
||||
We can't get this from the keystone service endpoint, we have to use
|
||||
the admin endpoint.
|
||||
|
||||
This will overwrite our admin token with the user token.
|
||||
"""
|
||||
|
||||
# GET ...:5001/v2.0/tokens/#####/endpoints
|
||||
url = '/'.join([url, 'tokens', '%s?belongsTo=%s'
|
||||
% (self.proxy_token, self.proxy_tenant_id)])
|
||||
self._logger.debug("Using Endpoint URL: %s" % url)
|
||||
resp, body = self.request(url, "GET",
|
||||
headers={'X-Auth-Token': self.auth_token})
|
||||
return self._extract_service_catalog(url, resp, body,
|
||||
extract_token=False)
|
||||
|
||||
def _save_keys(self):
|
||||
# Store the token/mgmt url in the keyring for later requests.
|
||||
if (self.os_cache):
|
||||
self.keyring_saver.save(self.auth_token,
|
||||
self.management_url,
|
||||
self.tenant_id)
|
||||
self.keyring_saver.save_password()
|
||||
|
||||
def authenticate(self):
|
||||
magic_tuple = parse.urlsplit(self.auth_url)
|
||||
scheme, netloc, path, query, frag = magic_tuple
|
||||
port = magic_tuple.port
|
||||
if port is None:
|
||||
port = 80
|
||||
path_parts = path.split('/')
|
||||
for part in path_parts:
|
||||
if len(part) > 0 and part[0] == 'v':
|
||||
self.version = part
|
||||
break
|
||||
|
||||
# TODO(sandy): Assume admin endpoint is 35357 for now.
|
||||
# Ideally this is going to have to be provided by the service catalog.
|
||||
new_netloc = netloc.replace(':%d' % port, ':%d' % (35357,))
|
||||
admin_url = parse.urlunsplit((scheme, new_netloc,
|
||||
path, query, frag))
|
||||
|
||||
auth_url = self.auth_url
|
||||
if self.version == "v2.0":
|
||||
while auth_url:
|
||||
if "MANILA_RAX_AUTH" in os.environ:
|
||||
auth_url = self._rax_auth(auth_url)
|
||||
else:
|
||||
auth_url = self._v2_auth(auth_url)
|
||||
|
||||
# Are we acting on behalf of another user via an
|
||||
# existing token? If so, our actual endpoints may
|
||||
# be different than that of the admin token.
|
||||
if self.proxy_token:
|
||||
self._fetch_endpoints_from_auth(admin_url)
|
||||
# Since keystone no longer returns the user token
|
||||
# with the endpoints any more, we need to replace
|
||||
# our service account token with the user token.
|
||||
self.auth_token = self.proxy_token
|
||||
else:
|
||||
try:
|
||||
while auth_url:
|
||||
auth_url = self._v1_auth(auth_url)
|
||||
# In some configurations manila makes redirection to
|
||||
# v2.0 keystone endpoint. Also, new location does not contain
|
||||
# real endpoint, only hostname and port.
|
||||
except exceptions.AuthorizationFailure:
|
||||
if auth_url.find('v2.0') < 0:
|
||||
auth_url = auth_url + '/v2.0'
|
||||
self._v2_auth(auth_url)
|
||||
|
||||
self._save_keys()
|
||||
|
||||
def _v1_auth(self, url):
|
||||
if self.proxy_token:
|
||||
raise exceptions.NoTokenLookupException()
|
||||
|
||||
headers = {'X-Auth-User': self.user,
|
||||
'X-Auth-Key': self.password}
|
||||
if self.projectid:
|
||||
headers['X-Auth-Project-Id'] = self.projectid
|
||||
|
||||
method = 'GET'
|
||||
resp, body = self.request(url, method, headers=headers)
|
||||
if resp.status_code in (200, 204): # in some cases we get No Content
|
||||
try:
|
||||
mgmt_header = 'x-server-management-url'
|
||||
self.management_url = resp.headers[mgmt_header].rstrip('/')
|
||||
self.auth_token = resp.headers['x-auth-token']
|
||||
self.auth_url = url
|
||||
except (KeyError, TypeError):
|
||||
raise exceptions.AuthorizationFailure()
|
||||
elif resp.status_code == 305:
|
||||
return resp.headers['location']
|
||||
else:
|
||||
raise exceptions.from_response(resp, method, url)
|
||||
|
||||
def _v2_auth(self, url):
|
||||
"""Authenticate against a v2.0 auth service."""
|
||||
body = {"auth": {
|
||||
"passwordCredentials": {"username": self.user,
|
||||
"password": self.password}}}
|
||||
|
||||
if self.projectid:
|
||||
body['auth']['tenantName'] = self.projectid
|
||||
elif self.tenant_id:
|
||||
body['auth']['tenantId'] = self.tenant_id
|
||||
|
||||
self._authenticate(url, body)
|
||||
|
||||
def _rax_auth(self, url):
|
||||
"""Authenticate against the Rackspace auth service."""
|
||||
body = {"auth": {
|
||||
"RAX-KSKEY:apiKeyCredentials": {
|
||||
"username": self.user,
|
||||
"apiKey": self.password,
|
||||
"tenantName": self.projectid}}}
|
||||
|
||||
self._authenticate(url, body)
|
||||
|
||||
def _authenticate(self, url, body):
|
||||
"""Authenticate and extract the service catalog."""
|
||||
token_url = url + "/tokens"
|
||||
|
||||
# Make sure we follow redirects when trying to reach Keystone
|
||||
resp, body = self.request(
|
||||
token_url,
|
||||
"POST",
|
||||
body=body,
|
||||
allow_redirects=True)
|
||||
|
||||
return self._extract_service_catalog(url, resp, body)
|
||||
|
||||
|
||||
def get_client_class(version):
|
||||
version_map = {
|
||||
'1': 'manilaclient.v1.client.Client',
|
||||
'2': 'manilaclient.v2.client.Client',
|
||||
}
|
||||
try:
|
||||
client_path = version_map[str(version)]
|
||||
|
||||
163
manilaclient/httpclient.py
Normal file
163
manilaclient/httpclient.py
Normal file
@@ -0,0 +1,163 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# Copyright 2011 Piston Cloud Computing, Inc.
|
||||
# Copyright 2015 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 copy
|
||||
import logging
|
||||
|
||||
from oslo.serialization import jsonutils
|
||||
from oslo_utils import strutils
|
||||
import requests
|
||||
import six
|
||||
|
||||
from manilaclient import exceptions
|
||||
|
||||
try:
|
||||
from eventlet import sleep
|
||||
except ImportError:
|
||||
from time import sleep # noqa
|
||||
|
||||
|
||||
class HTTPClient(object):
|
||||
def __init__(self, base_url, token, user_agent,
|
||||
insecure=False, cacert=None, timeout=None, retries=None,
|
||||
http_log_debug=False):
|
||||
self.base_url = base_url
|
||||
self.retries = retries
|
||||
self.http_log_debug = http_log_debug
|
||||
|
||||
self.request_options = self._set_request_options(
|
||||
insecure, cacert, timeout)
|
||||
|
||||
self.default_headers = {
|
||||
'X-Auth-Token': token,
|
||||
'User-Agent': user_agent,
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
|
||||
self._logger = logging.getLogger(__name__)
|
||||
if self.http_log_debug:
|
||||
ch = logging.StreamHandler()
|
||||
self._logger.setLevel(logging.DEBUG)
|
||||
self._logger.addHandler(ch)
|
||||
if hasattr(requests, 'logging'):
|
||||
requests.logging.getLogger(requests.__name__).addHandler(ch)
|
||||
|
||||
def _set_request_options(self, insecure, cacert, timeout=None):
|
||||
options = {'verify': True}
|
||||
|
||||
if insecure:
|
||||
options['verify'] = False
|
||||
elif cacert:
|
||||
options['verify'] = cacert
|
||||
|
||||
if timeout:
|
||||
options['timeout'] = timeout
|
||||
|
||||
return options
|
||||
|
||||
def request(self, url, method, **kwargs):
|
||||
headers = copy.deepcopy(self.default_headers)
|
||||
headers.update(kwargs.get('headers', {}))
|
||||
|
||||
options = copy.deepcopy(self.request_options)
|
||||
|
||||
if 'body' in kwargs:
|
||||
headers['Content-Type'] = 'application/json'
|
||||
options['data'] = jsonutils.dumps(kwargs['body'])
|
||||
|
||||
self.log_request(method, url, headers, options.get('data', None))
|
||||
resp = requests.request(method, url, headers=headers, **options)
|
||||
self.log_response(resp)
|
||||
|
||||
body = None
|
||||
|
||||
if resp.text:
|
||||
try:
|
||||
body = jsonutils.loads(resp.text)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if resp.status_code >= 400:
|
||||
raise exceptions.from_response(resp, method, url)
|
||||
|
||||
return resp, body
|
||||
|
||||
def _cs_request(self, url, method, **kwargs):
|
||||
attempts = 0
|
||||
timeout = 1
|
||||
while True:
|
||||
attempts += 1
|
||||
try:
|
||||
resp, body = self.request(self.base_url + url, method,
|
||||
**kwargs)
|
||||
return resp, body
|
||||
except (exceptions.BadRequest,
|
||||
requests.exceptions.RequestException,
|
||||
exceptions.ClientException) as e:
|
||||
if attempts > self.retries:
|
||||
raise
|
||||
|
||||
self._logger.debug("Request error: %s" % six.text_type(e))
|
||||
|
||||
self._logger.debug(
|
||||
"Failed attempt(%(current)s of %(total)s), "
|
||||
" retrying in %(sec)s seconds" % {
|
||||
'current': attempts,
|
||||
'total': self.retries,
|
||||
'sec': timeout
|
||||
})
|
||||
sleep(timeout)
|
||||
timeout *= 2
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
return self._cs_request(url, 'GET', **kwargs)
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
return self._cs_request(url, 'POST', **kwargs)
|
||||
|
||||
def put(self, url, **kwargs):
|
||||
return self._cs_request(url, 'PUT', **kwargs)
|
||||
|
||||
def delete(self, url, **kwargs):
|
||||
return self._cs_request(url, 'DELETE', **kwargs)
|
||||
|
||||
def log_request(self, method, url, headers, data=None):
|
||||
if not self.http_log_debug:
|
||||
return
|
||||
|
||||
string_parts = ['curl -i', ' -X %s' % method, ' %s' % url]
|
||||
|
||||
for element in headers:
|
||||
header = ' -H "%s: %s"' % (element, headers[element])
|
||||
string_parts.append(header)
|
||||
|
||||
if data:
|
||||
if "password" in data:
|
||||
data = strutils.mask_password(data)
|
||||
string_parts.append(" -d '%s'" % data)
|
||||
self._logger.debug("\nREQ: %s\n" % "".join(string_parts))
|
||||
|
||||
def log_response(self, resp):
|
||||
if not self.http_log_debug:
|
||||
return
|
||||
self._logger.debug(
|
||||
"RESP: [%(code)s] %(headers)s\nRESP BODY: %(body)s\n" % {
|
||||
'code': resp.status_code,
|
||||
'headers': resp.headers,
|
||||
'body': resp.text
|
||||
})
|
||||
@@ -1,83 +0,0 @@
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# Copyright 2011, Piston Cloud Computing, Inc.
|
||||
#
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 manilaclient.exceptions
|
||||
|
||||
|
||||
class ServiceCatalog(object):
|
||||
"""Helper methods for dealing with a Keystone Service Catalog."""
|
||||
|
||||
def __init__(self, resource_dict):
|
||||
self.catalog = resource_dict
|
||||
|
||||
def get_token(self):
|
||||
return self.catalog['access']['token']['id']
|
||||
|
||||
def get_tenant_id(self):
|
||||
return self.catalog['access']['token']['tenant']['id']
|
||||
|
||||
def url_for(self, attr=None, filter_value=None,
|
||||
service_type=None, endpoint_type='publicURL',
|
||||
service_name=None, share_service_name=None):
|
||||
"""Returns url for specified endpoint type.
|
||||
|
||||
Fetch the public URL from the Compute service for
|
||||
a particular endpoint attribute.
|
||||
If none given, return the first. See tests for sample service catalog.
|
||||
"""
|
||||
matching_endpoints = []
|
||||
if 'endpoints' in self.catalog:
|
||||
# We have a bastardized service catalog. Treat it special. :/
|
||||
for endpoint in self.catalog['endpoints']:
|
||||
if not filter_value or endpoint[attr] == filter_value:
|
||||
matching_endpoints.append(endpoint)
|
||||
if not matching_endpoints:
|
||||
raise manilaclient.exceptions.EndpointNotFound()
|
||||
|
||||
# We don't always get a service catalog back ...
|
||||
if 'serviceCatalog' not in self.catalog['access']:
|
||||
return None
|
||||
|
||||
# Full catalog ...
|
||||
catalog = self.catalog['access']['serviceCatalog']
|
||||
|
||||
for service in catalog:
|
||||
if service.get("type") != service_type:
|
||||
continue
|
||||
|
||||
if (service_name and service_type == 'compute' and
|
||||
service.get('name') != service_name):
|
||||
continue
|
||||
|
||||
if (share_service_name and service_type == 'share' and
|
||||
service.get('name') != share_service_name):
|
||||
continue
|
||||
|
||||
endpoints = service['endpoints']
|
||||
for endpoint in endpoints:
|
||||
if not filter_value or endpoint.get(attr) == filter_value:
|
||||
endpoint["serviceName"] = service.get("name")
|
||||
matching_endpoints.append(endpoint)
|
||||
|
||||
if not matching_endpoints:
|
||||
raise manilaclient.exceptions.EndpointNotFound()
|
||||
elif len(matching_endpoints) > 1:
|
||||
raise manilaclient.exceptions.AmbiguousEndpoints(
|
||||
endpoints=matching_endpoints)
|
||||
else:
|
||||
return matching_endpoints[0][endpoint_type]
|
||||
@@ -21,7 +21,6 @@ Command-line interface to the OpenStack Manila API.
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import getpass
|
||||
import glob
|
||||
import imp
|
||||
import itertools
|
||||
@@ -30,9 +29,7 @@ import os
|
||||
import pkgutil
|
||||
import sys
|
||||
|
||||
import keyring
|
||||
from oslo.utils import encodeutils
|
||||
from oslo.utils import strutils
|
||||
import six
|
||||
|
||||
from manilaclient import client
|
||||
@@ -104,188 +101,6 @@ class ManilaClientArgumentParser(argparse.ArgumentParser):
|
||||
return option_tuples
|
||||
|
||||
|
||||
class ManilaKeyring(keyring.backends.file.EncryptedKeyring):
|
||||
def delete_password(self, keyring_keys):
|
||||
"""Delete passwords from keyring.
|
||||
|
||||
Delete the passwords for given usernames of the services.
|
||||
|
||||
:param keyring_key: dictionary containing pairs {service:username}
|
||||
"""
|
||||
if self._check_file():
|
||||
self._unlock()
|
||||
for key, value in six.iteritems(keyring_keys):
|
||||
super(ManilaKeyring, self).delete_password(key, value)
|
||||
|
||||
|
||||
class SecretsHelper(object):
|
||||
"""Helper for working with keyring."""
|
||||
|
||||
def __init__(self, args, client):
|
||||
self.args = args
|
||||
self.client = client
|
||||
self.key = None
|
||||
self.keyring = ManilaKeyring()
|
||||
|
||||
def _validate_string(self, text):
|
||||
if text is None or len(text) == 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _make_key(self):
|
||||
if self.key is not None:
|
||||
return self.key
|
||||
keys = [
|
||||
self.client.auth_url,
|
||||
self.client.user,
|
||||
self.client.projectid,
|
||||
self.client.region_name,
|
||||
self.client.endpoint_type,
|
||||
self.client.service_type,
|
||||
self.client.service_name,
|
||||
self.client.share_service_name,
|
||||
]
|
||||
for (index, key) in enumerate(keys):
|
||||
if key is None:
|
||||
keys[index] = '?'
|
||||
else:
|
||||
keys[index] = str(keys[index])
|
||||
self.key = "/".join(keys)
|
||||
return self.key
|
||||
|
||||
def _prompt_password(self, verify=True):
|
||||
"""Suggest user to enter password from keyboard."""
|
||||
pw = None
|
||||
if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty():
|
||||
# Check for Ctl-D
|
||||
try:
|
||||
while True:
|
||||
pw1 = getpass.getpass('OS Password: ')
|
||||
if verify:
|
||||
pw2 = getpass.getpass('Please verify: ')
|
||||
else:
|
||||
pw2 = pw1
|
||||
if pw1 == pw2 and self._validate_string(pw1):
|
||||
pw = pw1
|
||||
break
|
||||
except EOFError:
|
||||
pass
|
||||
return pw
|
||||
|
||||
def save(self, auth_token, management_url, tenant_id):
|
||||
"""Save auth token, management url and tenant id in keyring.
|
||||
|
||||
If params are different from already cached ones, save new auth token,
|
||||
management url and tenant id in keyring.
|
||||
|
||||
Raise ValueError in case of empty auth_token, management_url or
|
||||
tenant_id.
|
||||
"""
|
||||
if (auth_token == self.auth_token and
|
||||
management_url == self.management_url):
|
||||
# Nothing changed....
|
||||
return
|
||||
if not all([management_url, auth_token, tenant_id]):
|
||||
raise ValueError("Unable to save empty management url/auth "
|
||||
"token")
|
||||
value = "|".join([str(auth_token),
|
||||
str(management_url),
|
||||
str(tenant_id)])
|
||||
self.keyring.set_password('manilaclient_auth', self._make_key(), value)
|
||||
|
||||
def reset(self):
|
||||
"""Delete cached password and auth token."""
|
||||
args = {'openstack': self.args.os_username,
|
||||
'manilaclient_auth': self._make_key()}
|
||||
self.keyring.delete_password(args)
|
||||
|
||||
def save_password(self):
|
||||
self.keyring.set_password('openstack', self.args.os_username,
|
||||
self.password)
|
||||
|
||||
def check_cached_password(self):
|
||||
"""Check if os_password is equal to cached password."""
|
||||
if self.args.os_cache:
|
||||
cached_password = self.keyring.get_password('openstack',
|
||||
self.args.os_username)
|
||||
cached_token = self.keyring.get_password('manilaclient_auth',
|
||||
self._make_key())
|
||||
if cached_password and self.password != cached_password:
|
||||
return False
|
||||
if cached_token and not cached_password:
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def password(self):
|
||||
"""Return user password.
|
||||
|
||||
Return os_password value or suggest user to enter new password from
|
||||
keyboard.
|
||||
"""
|
||||
password = None
|
||||
if self._validate_string(self.args.os_password):
|
||||
password = self.args.os_password
|
||||
else:
|
||||
verify_pass = strutils.bool_from_string(
|
||||
cliutils.env("OS_VERIFY_PASSWORD", default=False))
|
||||
password = self._prompt_password(verify_pass)
|
||||
if not password:
|
||||
raise exc.CommandError(
|
||||
'Expecting a password provided via either '
|
||||
'--os-password, env[OS_PASSWORD], or '
|
||||
'prompted response')
|
||||
return password
|
||||
|
||||
@property
|
||||
def management_url(self):
|
||||
"""Return cached management url.
|
||||
|
||||
If os_cache enabled and management url already cached, return
|
||||
management url. Otherwise return None.
|
||||
"""
|
||||
if not self.args.os_cache:
|
||||
return None
|
||||
management_url = None
|
||||
block = self.keyring.get_password('manilaclient_auth',
|
||||
self._make_key())
|
||||
if block:
|
||||
_token, management_url, _tenant_id = block.split('|', 2)
|
||||
return management_url
|
||||
|
||||
@property
|
||||
def auth_token(self):
|
||||
"""Return cached auth token.
|
||||
|
||||
If os_cache enabled and auth token already cached, return auth token.
|
||||
Otherwise return None.
|
||||
"""
|
||||
if not self.args.os_cache:
|
||||
return None
|
||||
token = None
|
||||
block = self.keyring.get_password('manilaclient_auth',
|
||||
self._make_key())
|
||||
if block:
|
||||
token, _management_url, _tenant_id = block.split('|', 2)
|
||||
return token
|
||||
|
||||
@property
|
||||
def tenant_id(self):
|
||||
"""Return cached tenant id.
|
||||
|
||||
If os_cache enabled and tenant id already cached, return tenant id.
|
||||
Otherwise return None.
|
||||
"""
|
||||
if not self.args.os_cache:
|
||||
return None
|
||||
tenant_id = None
|
||||
block = self.keyring.get_password('manilaclient_auth',
|
||||
self._make_key())
|
||||
if block:
|
||||
_token, _management_url, tenant_id = block.split('|', 2)
|
||||
return tenant_id
|
||||
|
||||
|
||||
class OpenStackManilaShell(object):
|
||||
|
||||
def get_base_parser(self):
|
||||
@@ -576,6 +391,9 @@ class OpenStackManilaShell(object):
|
||||
args.service_name, args.share_service_name,
|
||||
args.os_cacert, args.os_cache, args.os_reset_cache)
|
||||
|
||||
if share_service_name:
|
||||
service_name = share_service_name
|
||||
|
||||
if not endpoint_type:
|
||||
endpoint_type = DEFAULT_MANILA_ENDPOINT_TYPE
|
||||
|
||||
@@ -612,39 +430,22 @@ class OpenStackManilaShell(object):
|
||||
"You must provide an auth url "
|
||||
"via either --os-auth-url or env[OS_AUTH_URL]")
|
||||
|
||||
self.cs = client.Client(options.os_share_api_version, os_username,
|
||||
os_password, os_tenant_name, os_auth_url,
|
||||
insecure, region_name=os_region_name,
|
||||
self.cs = client.Client(options.os_share_api_version,
|
||||
username=os_username,
|
||||
api_key=os_password,
|
||||
project_name=os_tenant_name,
|
||||
auth_url=os_auth_url,
|
||||
insecure=insecure, region_name=os_region_name,
|
||||
tenant_id=os_tenant_id,
|
||||
endpoint_type=endpoint_type,
|
||||
extensions=self.extensions,
|
||||
service_type=service_type,
|
||||
service_name=service_name,
|
||||
share_service_name=share_service_name,
|
||||
retries=options.retries,
|
||||
http_log_debug=args.debug,
|
||||
cacert=cacert,
|
||||
os_cache=os_cache)
|
||||
if not cliutils.isunauthenticated(args.func):
|
||||
helper = SecretsHelper(args, self.cs.client)
|
||||
if os_reset_cache:
|
||||
helper.reset()
|
||||
self.cs.client.keyring_saver = helper
|
||||
if (helper.tenant_id and helper.auth_token and
|
||||
helper.management_url and
|
||||
helper.check_cached_password()):
|
||||
self.cs.client.tenant_id = helper.tenant_id
|
||||
self.cs.client.auth_token = helper.auth_token
|
||||
self.cs.client.management_url = helper.management_url
|
||||
else:
|
||||
self.cs.client.password = helper.password
|
||||
try:
|
||||
self.cs.authenticate()
|
||||
except exc.Unauthorized:
|
||||
raise exc.CommandError("Invalid OpenStack Manila "
|
||||
"credentials.")
|
||||
except exc.AuthorizationFailure:
|
||||
raise exc.CommandError("Unable to authorize user")
|
||||
use_keyring=os_cache,
|
||||
force_new_token=os_reset_cache)
|
||||
|
||||
args.func(self.cs, args)
|
||||
|
||||
|
||||
@@ -13,10 +13,11 @@
|
||||
import mock
|
||||
import requests
|
||||
|
||||
from manilaclient import client
|
||||
from manilaclient import exceptions
|
||||
from manilaclient import httpclient
|
||||
from manilaclient.tests.unit import utils
|
||||
|
||||
fake_user_agent = "fake"
|
||||
|
||||
fake_response = utils.TestResponse({
|
||||
"status_code": 200,
|
||||
@@ -42,17 +43,36 @@ bad_500_response = utils.TestResponse({
|
||||
})
|
||||
bad_500_request = mock.Mock(return_value=(bad_500_response))
|
||||
|
||||
retry_after_response = utils.TestResponse({
|
||||
"status_code": 413,
|
||||
"text": '',
|
||||
"headers": {
|
||||
"retry-after": "5"
|
||||
},
|
||||
})
|
||||
retry_after_mock_request = mock.Mock(return_value=retry_after_response)
|
||||
|
||||
def get_client(retries=0):
|
||||
cl = client.HTTPClient("username", "password",
|
||||
"project_id", "auth_test", retries=retries)
|
||||
return cl
|
||||
retry_after_no_headers_response = utils.TestResponse({
|
||||
"status_code": 413,
|
||||
"text": '',
|
||||
})
|
||||
retry_after_no_headers_mock_request = mock.Mock(
|
||||
return_value=retry_after_no_headers_response)
|
||||
|
||||
retry_after_non_supporting_response = utils.TestResponse({
|
||||
"status_code": 403,
|
||||
"text": '',
|
||||
"headers": {
|
||||
"retry-after": "5"
|
||||
},
|
||||
})
|
||||
retry_after_non_supporting_mock_request = mock.Mock(
|
||||
return_value=retry_after_non_supporting_response)
|
||||
|
||||
|
||||
def get_authed_client(retries=0):
|
||||
cl = get_client(retries=retries)
|
||||
cl.management_url = "http://example.com"
|
||||
cl.auth_token = "token"
|
||||
cl = httpclient.HTTPClient("http://example.com", "token", fake_user_agent,
|
||||
retries=retries, http_log_debug=True)
|
||||
return cl
|
||||
|
||||
|
||||
@@ -66,8 +86,7 @@ class ClientTest(utils.TestCase):
|
||||
def test_get_call():
|
||||
resp, body = cl.get("/hi")
|
||||
headers = {"X-Auth-Token": "token",
|
||||
"X-Auth-Project-Id": "project_id",
|
||||
"User-Agent": cl.USER_AGENT,
|
||||
"User-Agent": fake_user_agent,
|
||||
'Accept': 'application/json', }
|
||||
mock_request.assert_called_with(
|
||||
"GET",
|
||||
@@ -79,28 +98,6 @@ class ClientTest(utils.TestCase):
|
||||
|
||||
test_get_call()
|
||||
|
||||
def test_get_reauth_0_retries(self):
|
||||
cl = get_authed_client(retries=0)
|
||||
|
||||
self.requests = [bad_401_request, mock_request]
|
||||
|
||||
def request(*args, **kwargs):
|
||||
next_request = self.requests.pop(0)
|
||||
return next_request(*args, **kwargs)
|
||||
|
||||
def reauth():
|
||||
cl.management_url = "http://example.com"
|
||||
cl.auth_token = "token"
|
||||
|
||||
@mock.patch.object(cl, 'authenticate', reauth)
|
||||
@mock.patch.object(requests, "request", request)
|
||||
@mock.patch('time.time', mock.Mock(return_value=1234))
|
||||
def test_get_call():
|
||||
resp, body = cl.get("/hi")
|
||||
|
||||
test_get_call()
|
||||
self.assertEqual(self.requests, [])
|
||||
|
||||
def test_get_retry_500(self):
|
||||
cl = get_authed_client(retries=1)
|
||||
|
||||
@@ -177,10 +174,9 @@ class ClientTest(utils.TestCase):
|
||||
cl.post("/hi", body=[1, 2, 3])
|
||||
headers = {
|
||||
"X-Auth-Token": "token",
|
||||
"X-Auth-Project-Id": "project_id",
|
||||
"Content-Type": "application/json",
|
||||
'Accept': 'application/json',
|
||||
"User-Agent": cl.USER_AGENT
|
||||
"User-Agent": fake_user_agent
|
||||
}
|
||||
mock_request.assert_called_with(
|
||||
"POST",
|
||||
@@ -190,13 +186,3 @@ class ClientTest(utils.TestCase):
|
||||
**self.TEST_REQUEST_BASE)
|
||||
|
||||
test_post_call()
|
||||
|
||||
def test_auth_failure(self):
|
||||
cl = get_client()
|
||||
|
||||
# response must not have x-server-management-url header
|
||||
@mock.patch.object(requests, "request", mock_request)
|
||||
def test_auth_call():
|
||||
self.assertRaises(exceptions.AuthorizationFailure, cl.authenticate)
|
||||
|
||||
test_auth_call()
|
||||
@@ -1,139 +0,0 @@
|
||||
# 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 manilaclient import exceptions
|
||||
from manilaclient import service_catalog
|
||||
from manilaclient.tests.unit import utils
|
||||
|
||||
|
||||
# Taken directly from keystone/content/common/samples/auth.json
|
||||
# Do not edit this structure. Instead, grab the latest from there.
|
||||
|
||||
SERVICE_CATALOG = {
|
||||
"access": {
|
||||
"token": {
|
||||
"id": "ab48a9efdfedb23ty3494",
|
||||
"expires": "2010-11-01T03:32:15-05:00",
|
||||
"tenant": {
|
||||
"id": "345",
|
||||
"name": "My Project"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"id": "123",
|
||||
"name": "jqsmith",
|
||||
"roles": [
|
||||
{
|
||||
"id": "234",
|
||||
"name": "compute:admin",
|
||||
},
|
||||
{
|
||||
"id": "235",
|
||||
"name": "object-store:admin",
|
||||
"tenantId": "1",
|
||||
}
|
||||
],
|
||||
"roles_links": [],
|
||||
},
|
||||
"serviceCatalog": [
|
||||
{
|
||||
"name": "Cloud Servers",
|
||||
"type": "compute",
|
||||
"endpoints": [
|
||||
{
|
||||
"tenantId": "1",
|
||||
"publicURL": "https://compute1.host/v1/1234",
|
||||
"internalURL": "https://compute1.host/v1/1234",
|
||||
"region": "North",
|
||||
"versionId": "1.0",
|
||||
"versionInfo": "https://compute1.host/v1/",
|
||||
"versionList": "https://compute1.host/"
|
||||
},
|
||||
{
|
||||
"tenantId": "2",
|
||||
"publicURL": "https://compute1.host/v1/3456",
|
||||
"internalURL": "https://compute1.host/v1/3456",
|
||||
"region": "North",
|
||||
"versionId": "1.1",
|
||||
"versionInfo": "https://compute1.host/v1/",
|
||||
"versionList": "https://compute1.host/"
|
||||
},
|
||||
],
|
||||
"endpoints_links": [],
|
||||
},
|
||||
{
|
||||
"name": "Nova Shares",
|
||||
"type": "share",
|
||||
"endpoints": [
|
||||
{
|
||||
"tenantId": "1",
|
||||
"publicURL": "https://share1.host/v1/1234",
|
||||
"internalURL": "https://share1.host/v1/1234",
|
||||
"region": "South",
|
||||
"versionId": "1.0",
|
||||
"versionInfo": "uri",
|
||||
"versionList": "uri"
|
||||
},
|
||||
{
|
||||
"tenantId": "2",
|
||||
"publicURL": "https://share1.host/v1/3456",
|
||||
"internalURL": "https://share1.host/v1/3456",
|
||||
"region": "South",
|
||||
"versionId": "1.1",
|
||||
"versionInfo": "https://share1.host/v1/",
|
||||
"versionList": "https://share1.host/"
|
||||
},
|
||||
],
|
||||
"endpoints_links": [
|
||||
{
|
||||
"rel": "next",
|
||||
"href": "https://identity1.host/v2.0/endpoints"
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
"serviceCatalog_links": [
|
||||
{
|
||||
"rel": "next",
|
||||
"href": "https://identity.host/v2.0/endpoints?session=2hfh8Ar",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class ServiceCatalogTest(utils.TestCase):
|
||||
def test_building_a_service_catalog(self):
|
||||
sc = service_catalog.ServiceCatalog(SERVICE_CATALOG)
|
||||
|
||||
self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for,
|
||||
service_type='compute')
|
||||
self.assertEqual(sc.url_for('tenantId', '1', service_type='compute'),
|
||||
"https://compute1.host/v1/1234")
|
||||
self.assertEqual(sc.url_for('tenantId', '2', service_type='compute'),
|
||||
"https://compute1.host/v1/3456")
|
||||
|
||||
self.assertRaises(exceptions.EndpointNotFound, sc.url_for,
|
||||
"region", "South", service_type='compute')
|
||||
|
||||
def test_alternate_service_type(self):
|
||||
sc = service_catalog.ServiceCatalog(SERVICE_CATALOG)
|
||||
|
||||
self.assertRaises(exceptions.AmbiguousEndpoints, sc.url_for,
|
||||
service_type='share')
|
||||
self.assertEqual(sc.url_for('tenantId', '1', service_type='share'),
|
||||
"https://share1.host/v1/1234")
|
||||
self.assertEqual(sc.url_for('tenantId', '2', service_type='share'),
|
||||
"https://share1.host/v1/3456")
|
||||
|
||||
self.assertRaises(exceptions.EndpointNotFound, sc.url_for,
|
||||
"region", "North", service_type='share')
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from manilaclient import client as base_client
|
||||
from manilaclient import httpclient
|
||||
from manilaclient.tests.unit import fakes
|
||||
from manilaclient.tests.unit import utils
|
||||
from manilaclient.v1 import client
|
||||
@@ -29,7 +29,7 @@ class FakeClient(fakes.FakeClient, client.Client):
|
||||
self.client = FakeHTTPClient(**kwargs)
|
||||
|
||||
|
||||
class FakeHTTPClient(base_client.HTTPClient):
|
||||
class FakeHTTPClient(httpclient.HTTPClient):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.username = 'username'
|
||||
|
||||
@@ -24,7 +24,9 @@ class FakeClient(fakes.FakeClient):
|
||||
def __init__(self, *args, **kwargs):
|
||||
client.Client.__init__(self, 'username', 'password',
|
||||
'project_id', 'auth_url',
|
||||
extensions=kwargs.get('extensions'))
|
||||
input_auth_token='token',
|
||||
extensions=kwargs.get('extensions'),
|
||||
service_catalog_url='http://localhost:8786')
|
||||
self.client = FakeHTTPClient(**kwargs)
|
||||
|
||||
|
||||
|
||||
@@ -1,399 +0,0 @@
|
||||
# Copyright (c) 2013 OpenStack, LLC.
|
||||
#
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 mock
|
||||
import requests
|
||||
|
||||
from manilaclient import exceptions
|
||||
from manilaclient.tests.unit import utils
|
||||
from manilaclient.v1 import client
|
||||
|
||||
|
||||
class AuthenticateAgainstKeystoneTests(utils.TestCase):
|
||||
def test_authenticate_success(self):
|
||||
cs = client.Client("username", "password", "project_id",
|
||||
"auth_url/v2.0", service_type='compute')
|
||||
resp = {
|
||||
"access": {
|
||||
"token": {
|
||||
"expires": "12345",
|
||||
"id": "FAKE_ID",
|
||||
"tenant": {
|
||||
"id": "FAKE_TENANT_ID"
|
||||
},
|
||||
},
|
||||
"serviceCatalog": [
|
||||
{
|
||||
"type": "compute",
|
||||
"endpoints": [
|
||||
{
|
||||
"region": "RegionOne",
|
||||
"adminURL": "http://localhost:8774/v2",
|
||||
"internalURL": "http://localhost:8774/v2",
|
||||
"publicURL": "http://localhost:8774/v2/",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
auth_response = utils.TestResponse({
|
||||
"status_code": 200,
|
||||
"text": json.dumps(resp),
|
||||
})
|
||||
|
||||
mock_request = mock.Mock(return_value=(auth_response))
|
||||
|
||||
@mock.patch.object(requests, "request", mock_request)
|
||||
def test_auth_call():
|
||||
cs.client.authenticate()
|
||||
headers = {
|
||||
'User-Agent': cs.client.USER_AGENT,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
body = {
|
||||
'auth': {
|
||||
'passwordCredentials': {
|
||||
'username': cs.client.user,
|
||||
'password': cs.client.password,
|
||||
},
|
||||
'tenantName': cs.client.projectid,
|
||||
},
|
||||
}
|
||||
|
||||
token_url = cs.client.auth_url + "/tokens"
|
||||
mock_request.assert_called_with(
|
||||
"POST",
|
||||
token_url,
|
||||
headers=headers,
|
||||
data=json.dumps(body),
|
||||
allow_redirects=True,
|
||||
**self.TEST_REQUEST_BASE)
|
||||
|
||||
endpoints = resp["access"]["serviceCatalog"][0]['endpoints']
|
||||
public_url = endpoints[0]["publicURL"].rstrip('/')
|
||||
self.assertEqual(cs.client.management_url, public_url)
|
||||
token_id = resp["access"]["token"]["id"]
|
||||
self.assertEqual(cs.client.auth_token, token_id)
|
||||
|
||||
test_auth_call()
|
||||
|
||||
def test_authenticate_tenant_id(self):
|
||||
cs = client.Client("username", "password", auth_url="auth_url/v2.0",
|
||||
tenant_id='tenant_id', service_type='compute')
|
||||
resp = {
|
||||
"access": {
|
||||
"token": {
|
||||
"expires": "12345",
|
||||
"id": "FAKE_ID",
|
||||
"tenant": {
|
||||
"description": None,
|
||||
"enabled": True,
|
||||
"id": "tenant_id",
|
||||
"name": "demo"
|
||||
} # tenant associated with token
|
||||
},
|
||||
"serviceCatalog": [
|
||||
{
|
||||
"type": "compute",
|
||||
"endpoints": [
|
||||
{
|
||||
"region": "RegionOne",
|
||||
"adminURL": "http://localhost:8774/v2",
|
||||
"internalURL": "http://localhost:8774/v2",
|
||||
"publicURL": "http://localhost:8774/v2/",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
auth_response = utils.TestResponse({
|
||||
"status_code": 200,
|
||||
"text": json.dumps(resp),
|
||||
})
|
||||
|
||||
mock_request = mock.Mock(return_value=(auth_response))
|
||||
|
||||
@mock.patch.object(requests, "request", mock_request)
|
||||
def test_auth_call():
|
||||
cs.client.authenticate()
|
||||
headers = {
|
||||
'User-Agent': cs.client.USER_AGENT,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
body = {
|
||||
'auth': {
|
||||
'passwordCredentials': {
|
||||
'username': cs.client.user,
|
||||
'password': cs.client.password,
|
||||
},
|
||||
'tenantId': cs.client.tenant_id,
|
||||
},
|
||||
}
|
||||
|
||||
token_url = cs.client.auth_url + "/tokens"
|
||||
mock_request.assert_called_with(
|
||||
"POST",
|
||||
token_url,
|
||||
headers=headers,
|
||||
data=json.dumps(body),
|
||||
allow_redirects=True,
|
||||
**self.TEST_REQUEST_BASE)
|
||||
|
||||
endpoints = resp["access"]["serviceCatalog"][0]['endpoints']
|
||||
public_url = endpoints[0]["publicURL"].rstrip('/')
|
||||
self.assertEqual(cs.client.management_url, public_url)
|
||||
token_id = resp["access"]["token"]["id"]
|
||||
self.assertEqual(cs.client.auth_token, token_id)
|
||||
tenant_id = resp["access"]["token"]["tenant"]["id"]
|
||||
self.assertEqual(cs.client.tenant_id, tenant_id)
|
||||
|
||||
test_auth_call()
|
||||
|
||||
def test_authenticate_failure(self):
|
||||
cs = client.Client("username", "password", "project_id",
|
||||
"auth_url/v2.0")
|
||||
resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}}
|
||||
auth_response = utils.TestResponse({
|
||||
"status_code": 401,
|
||||
"text": json.dumps(resp),
|
||||
})
|
||||
|
||||
mock_request = mock.Mock(return_value=(auth_response))
|
||||
|
||||
@mock.patch.object(requests, "request", mock_request)
|
||||
def test_auth_call():
|
||||
self.assertRaises(exceptions.Unauthorized, cs.client.authenticate)
|
||||
|
||||
test_auth_call()
|
||||
|
||||
def test_auth_redirect(self):
|
||||
cs = client.Client("username", "password", "project_id",
|
||||
"auth_url/v2", service_type='compute')
|
||||
dict_correct_response = {
|
||||
"access": {
|
||||
"token": {
|
||||
"expires": "12345",
|
||||
"id": "FAKE_ID",
|
||||
"tenant": {
|
||||
"id": "FAKE_TENANT_ID"
|
||||
},
|
||||
},
|
||||
"serviceCatalog": [
|
||||
{
|
||||
"type": "compute",
|
||||
"endpoints": [
|
||||
{
|
||||
"adminURL": "http://localhost:8774/v2",
|
||||
"region": "RegionOne",
|
||||
"internalURL": "http://localhost:8774/v2",
|
||||
"publicURL": "http://localhost:8774/v2/",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
correct_response = json.dumps(dict_correct_response)
|
||||
dict_responses = [
|
||||
{"headers": {'location': 'http://127.0.0.1:5001'},
|
||||
"status_code": 305,
|
||||
"text": "Use proxy"},
|
||||
# Configured on admin port, manila redirects to v2.0 port.
|
||||
# When trying to connect on it, keystone auth succeed by v1.0
|
||||
# protocol (through headers) but tokens are being returned in
|
||||
# body (looks like keystone bug). Leaved for compatibility.
|
||||
{"headers": {},
|
||||
"status_code": 200,
|
||||
"text": correct_response},
|
||||
{"headers": {},
|
||||
"status_code": 200,
|
||||
"text": correct_response}
|
||||
]
|
||||
|
||||
responses = [(utils.TestResponse(resp)) for resp in dict_responses]
|
||||
|
||||
def side_effect(*args, **kwargs):
|
||||
return responses.pop(0)
|
||||
|
||||
mock_request = mock.Mock(side_effect=side_effect)
|
||||
|
||||
@mock.patch.object(requests, "request", mock_request)
|
||||
def test_auth_call():
|
||||
cs.client.authenticate()
|
||||
headers = {
|
||||
'User-Agent': cs.client.USER_AGENT,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
body = {
|
||||
'auth': {
|
||||
'passwordCredentials': {
|
||||
'username': cs.client.user,
|
||||
'password': cs.client.password,
|
||||
},
|
||||
'tenantName': cs.client.projectid,
|
||||
},
|
||||
}
|
||||
|
||||
token_url = cs.client.auth_url + "/tokens"
|
||||
mock_request.assert_called_with(
|
||||
"POST",
|
||||
token_url,
|
||||
headers=headers,
|
||||
data=json.dumps(body),
|
||||
allow_redirects=True,
|
||||
**self.TEST_REQUEST_BASE)
|
||||
|
||||
resp = dict_correct_response
|
||||
endpoints = resp["access"]["serviceCatalog"][0]['endpoints']
|
||||
public_url = endpoints[0]["publicURL"].rstrip('/')
|
||||
self.assertEqual(cs.client.management_url, public_url)
|
||||
token_id = resp["access"]["token"]["id"]
|
||||
self.assertEqual(cs.client.auth_token, token_id)
|
||||
|
||||
test_auth_call()
|
||||
|
||||
def test_ambiguous_endpoints(self):
|
||||
cs = client.Client("username", "password", "project_id",
|
||||
"auth_url/v2.0", service_type='compute')
|
||||
resp = {
|
||||
"access": {
|
||||
"token": {
|
||||
"expires": "12345",
|
||||
"id": "FAKE_ID",
|
||||
"tenant": {
|
||||
"id": "FAKE_TENANT_ID"
|
||||
},
|
||||
},
|
||||
"serviceCatalog": [
|
||||
{
|
||||
"adminURL": "http://localhost:8774/v2",
|
||||
"type": "compute",
|
||||
"name": "Compute CLoud",
|
||||
"endpoints": [
|
||||
{
|
||||
"region": "RegionOne",
|
||||
"internalURL": "http://localhost:8774/v2",
|
||||
"publicURL": "http://localhost:8774/v2/",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"adminURL": "http://localhost:8774/v2",
|
||||
"type": "compute",
|
||||
"name": "Hyper-compute Cloud",
|
||||
"endpoints": [
|
||||
{
|
||||
"internalURL": "http://localhost:8774/v2",
|
||||
"publicURL": "http://localhost:8774/v2/",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
auth_response = utils.TestResponse({
|
||||
"status_code": 200,
|
||||
"text": json.dumps(resp),
|
||||
})
|
||||
|
||||
mock_request = mock.Mock(return_value=(auth_response))
|
||||
|
||||
@mock.patch.object(requests, "request", mock_request)
|
||||
def test_auth_call():
|
||||
self.assertRaises(exceptions.AmbiguousEndpoints,
|
||||
cs.client.authenticate)
|
||||
|
||||
test_auth_call()
|
||||
|
||||
|
||||
class AuthenticationTests(utils.TestCase):
|
||||
def test_authenticate_success(self):
|
||||
cs = client.Client("username", "password", "project_id", "auth_url")
|
||||
management_url = 'https://localhost/v2.1/443470'
|
||||
auth_response = utils.TestResponse({
|
||||
'status_code': 204,
|
||||
'headers': {
|
||||
'x-server-management-url': management_url,
|
||||
'x-auth-token': '1b751d74-de0c-46ae-84f0-915744b582d1',
|
||||
},
|
||||
})
|
||||
mock_request = mock.Mock(return_value=(auth_response))
|
||||
|
||||
@mock.patch.object(requests, "request", mock_request)
|
||||
def test_auth_call():
|
||||
cs.client.authenticate()
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'X-Auth-User': 'username',
|
||||
'X-Auth-Key': 'password',
|
||||
'X-Auth-Project-Id': 'project_id',
|
||||
'User-Agent': cs.client.USER_AGENT
|
||||
}
|
||||
mock_request.assert_called_with(
|
||||
"GET",
|
||||
cs.client.auth_url,
|
||||
headers=headers,
|
||||
**self.TEST_REQUEST_BASE)
|
||||
|
||||
self.assertEqual(cs.client.management_url,
|
||||
auth_response.headers['x-server-management-url'])
|
||||
self.assertEqual(cs.client.auth_token,
|
||||
auth_response.headers['x-auth-token'])
|
||||
|
||||
test_auth_call()
|
||||
|
||||
def test_authenticate_failure(self):
|
||||
cs = client.Client("username", "password", "project_id", "auth_url")
|
||||
auth_response = utils.TestResponse({"status_code": 401})
|
||||
mock_request = mock.Mock(return_value=(auth_response))
|
||||
|
||||
@mock.patch.object(requests, "request", mock_request)
|
||||
def test_auth_call():
|
||||
self.assertRaises(exceptions.Unauthorized, cs.client.authenticate)
|
||||
|
||||
test_auth_call()
|
||||
|
||||
def test_auth_automatic(self):
|
||||
cs = client.Client("username", "password", "project_id", "auth_url")
|
||||
http_client = cs.client
|
||||
http_client.management_url = ''
|
||||
mock_request = mock.Mock(return_value=(None, None))
|
||||
|
||||
@mock.patch.object(http_client, 'request', mock_request)
|
||||
@mock.patch.object(http_client, 'authenticate')
|
||||
def test_auth_call(m):
|
||||
http_client.get('/')
|
||||
m.assert_called()
|
||||
mock_request.assert_called()
|
||||
|
||||
test_auth_call()
|
||||
|
||||
def test_auth_manual(self):
|
||||
cs = client.Client("username", "password", "project_id", "auth_url")
|
||||
|
||||
@mock.patch.object(cs.client, 'authenticate')
|
||||
def test_auth_call(m):
|
||||
cs.authenticate()
|
||||
m.assert_called()
|
||||
|
||||
test_auth_call()
|
||||
33
manilaclient/tests/unit/v1/test_client.py
Normal file
33
manilaclient/tests/unit/v1/test_client.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# 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 uuid
|
||||
|
||||
from keystoneclient import session
|
||||
|
||||
from manilaclient.tests.unit import utils
|
||||
from manilaclient.v1 import client
|
||||
|
||||
|
||||
class ClientTest(utils.TestCase):
|
||||
|
||||
def test_adapter_properties(self):
|
||||
# sample of properties, there are many more
|
||||
retries = 3
|
||||
base_url = uuid.uuid4().hex
|
||||
|
||||
s = session.Session()
|
||||
c = client.Client(session=s, service_catalog_url=base_url,
|
||||
retries=retries, input_auth_token='token')
|
||||
|
||||
self.assertEqual(base_url, c.client.base_url)
|
||||
self.assertEqual(retries, c.client.retries)
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
import fixtures
|
||||
import mock
|
||||
from oslo.serialization import jsonutils
|
||||
import requests
|
||||
import six
|
||||
from six.moves.urllib import parse
|
||||
|
||||
@@ -29,7 +27,6 @@ from manilaclient.openstack.common import cliutils
|
||||
from manilaclient import shell
|
||||
from manilaclient.tests.unit import utils as test_utils
|
||||
from manilaclient.tests.unit.v1 import fakes
|
||||
from manilaclient.v1 import client as client_v1
|
||||
from manilaclient.v1 import shell as shell_v1
|
||||
|
||||
|
||||
@@ -922,273 +919,3 @@ class ShellTest(test_utils.TestCase):
|
||||
cliutils.print_list.assert_called_once_with(
|
||||
mock.ANY,
|
||||
fields=['id', 'name', 'status', 'type'])
|
||||
|
||||
@mock.patch.object(fakes.FakeClient, 'authenticate', mock.Mock())
|
||||
@mock.patch.object(shell.SecretsHelper, '_make_key', mock.Mock())
|
||||
@mock.patch.object(shell.SecretsHelper, 'password', mock.Mock())
|
||||
@mock.patch.object(shell.SecretsHelper, 'check_cached_password',
|
||||
mock.Mock())
|
||||
def test_os_cache_enabled_keys_saved_password_not_changed(self):
|
||||
shell.SecretsHelper.tenant_id = 'fake'
|
||||
shell.SecretsHelper.auth_token = 'fake'
|
||||
shell.SecretsHelper.management_url = 'fake'
|
||||
self.run_command('--os-cache list')
|
||||
self.assertFalse(shell.SecretsHelper.password.called)
|
||||
self.assertFalse(fakes.FakeClient.authenticate.called)
|
||||
|
||||
@mock.patch.object(shell.SecretsHelper, '_validate_string', mock.Mock())
|
||||
@mock.patch.object(shell.SecretsHelper, '_prompt_password', mock.Mock())
|
||||
@mock.patch.object(shell.ManilaKeyring, 'get_password', mock.Mock())
|
||||
def test_os_cache_enabled_keys_not_saved_with_password(self):
|
||||
shell.SecretsHelper._validate_string.return_value = True
|
||||
shell.ManilaKeyring.get_password.return_value = None
|
||||
shell.SecretsHelper.tenant_id = None
|
||||
shell.SecretsHelper.auth_token = None
|
||||
shell.SecretsHelper.management_url = None
|
||||
self.run_command('--os-cache list')
|
||||
self.assertFalse(shell.SecretsHelper._prompt_password.called)
|
||||
shell.SecretsHelper._validate_string.assert_called_once_with(
|
||||
'password')
|
||||
|
||||
@mock.patch.object(shell.SecretsHelper, '_prompt_password', mock.Mock())
|
||||
@mock.patch.object(shell.ManilaKeyring, 'get_password', mock.Mock())
|
||||
def test_os_cache_enabled_keys_not_saved_no_password(self):
|
||||
shell.ManilaKeyring.get_password.return_value = None
|
||||
shell.SecretsHelper._prompt_password.return_value = 'password'
|
||||
self.useFixture(fixtures.EnvironmentVariable('MANILA_PASSWORD', ''))
|
||||
shell.SecretsHelper.tenant_id = None
|
||||
shell.SecretsHelper.auth_token = None
|
||||
shell.SecretsHelper.management_url = None
|
||||
self.run_command('--os-cache list')
|
||||
self.assertTrue(shell.SecretsHelper._prompt_password.called)
|
||||
|
||||
@mock.patch.object(shell.SecretsHelper, '_validate_string', mock.Mock())
|
||||
@mock.patch.object(shell.ManilaKeyring, 'get_password', mock.Mock())
|
||||
@mock.patch.object(shell.SecretsHelper, 'reset', mock.Mock())
|
||||
def test_os_cache_enabled_keys_reset_cached_password(self):
|
||||
shell.ManilaKeyring.get_password.return_value = 'old_password'
|
||||
shell.SecretsHelper._validate_string.return_value = True
|
||||
shell.SecretsHelper.tenant_id = None
|
||||
shell.SecretsHelper.auth_token = None
|
||||
shell.SecretsHelper.management_url = None
|
||||
self.run_command('--os-cache --os-reset-cache list')
|
||||
shell.SecretsHelper._validate_string.assert_called_once_with(
|
||||
'password')
|
||||
shell.SecretsHelper.reset.assert_called_once_with()
|
||||
|
||||
@mock.patch.object(shell.SecretsHelper, '_validate_string', mock.Mock())
|
||||
@mock.patch.object(shell.SecretsHelper, 'reset', mock.Mock())
|
||||
def test_os_cache_disabled_keys_reset_cached_password(self):
|
||||
shell.SecretsHelper._validate_string.return_value = True
|
||||
self.run_command('--os-reset-cache list')
|
||||
shell.SecretsHelper._validate_string.assert_called_once_with(
|
||||
'password')
|
||||
shell.SecretsHelper.reset.assert_called_once_with()
|
||||
|
||||
@mock.patch.object(fakes.FakeClient, 'authenticate', mock.Mock())
|
||||
@mock.patch.object(shell.ManilaKeyring, 'get_password', mock.Mock())
|
||||
@mock.patch.object(shell.SecretsHelper, '_make_key', mock.Mock())
|
||||
def test_os_cache_enabled_os_password_differs_from_the_cached_one(self):
|
||||
def _fake_get_password(service, username):
|
||||
if service == 'openstack':
|
||||
return 'old_cached_password'
|
||||
else:
|
||||
return 'old_cached_token'
|
||||
shell.SecretsHelper.tenant_id = 'fake'
|
||||
shell.SecretsHelper.auth_token = 'fake'
|
||||
shell.SecretsHelper.management_url = 'fake'
|
||||
self.run_command('--os-cache list')
|
||||
fakes.FakeClient.authenticate.assert_called_once_with()
|
||||
|
||||
@mock.patch.object(requests, 'request', mock.Mock())
|
||||
@mock.patch.object(client.HTTPClient, '_save_keys', mock.Mock())
|
||||
def test_os_cache_token_expired(self):
|
||||
def _fake_request(method, url, **kwargs):
|
||||
headers = None
|
||||
if url == 'new_url/shares/detail':
|
||||
resp_text = {"shares": []}
|
||||
return test_utils.TestResponse({
|
||||
"status_code": 200,
|
||||
"text": jsonutils.dumps(resp_text),
|
||||
})
|
||||
elif url == 'fake/shares/detail':
|
||||
resp_text = {"unauthorized": {"message": "Unauthorized",
|
||||
"code": "401"}}
|
||||
return test_utils.TestResponse({
|
||||
"status_code": 401,
|
||||
"text": jsonutils.dumps(resp_text),
|
||||
})
|
||||
else:
|
||||
headers = {
|
||||
'x-server-management-url': 'new_url',
|
||||
'x-auth-token': 'new_token',
|
||||
}
|
||||
resp_text = 'some_text'
|
||||
return test_utils.TestResponse({
|
||||
"status_code": 200,
|
||||
"text": jsonutils.dumps(resp_text),
|
||||
"headers": headers
|
||||
})
|
||||
|
||||
client.get_client_class = lambda *_: client_v1.Client
|
||||
shell.SecretsHelper.tenant_id = 'fake'
|
||||
shell.SecretsHelper.auth_token = 'fake'
|
||||
shell.SecretsHelper.management_url = 'fake'
|
||||
requests.request.side_effect = _fake_request
|
||||
|
||||
self.run_command('--os-cache list')
|
||||
|
||||
client.HTTPClient._save_keys.assert_called_once_with()
|
||||
expected_headers = {
|
||||
'X-Auth-Project-Id': 'project_id',
|
||||
'User-Agent': 'python-manilaclient',
|
||||
'Accept': 'application/json',
|
||||
'X-Auth-Token': 'new_token'}
|
||||
requests.request.assert_called_with(
|
||||
'GET',
|
||||
'new_url/shares/detail',
|
||||
headers=expected_headers,
|
||||
verify=True,
|
||||
)
|
||||
|
||||
|
||||
class SecretsHelperTestCase(test_utils.TestCase):
|
||||
def setUp(self):
|
||||
super(SecretsHelperTestCase, self).setUp()
|
||||
self.cs = client.Client(1, 'user', 'password',
|
||||
project_id='project',
|
||||
auth_url='http://111.11.11.11:5000',
|
||||
region_name='region',
|
||||
endpoint_type='publicURL',
|
||||
service_type='share',
|
||||
service_name='fake',
|
||||
share_service_name='fake')
|
||||
self.args = mock.Mock()
|
||||
self.args.os_cache = True
|
||||
self.args.reset_cached_password = False
|
||||
self.helper = shell.SecretsHelper(self.args, self.cs.client)
|
||||
mock.patch.object(shell.ManilaKeyring, 'set_password',
|
||||
mock.Mock()).start()
|
||||
mock.patch.object(shell.ManilaKeyring, 'get_password', mock.Mock(
|
||||
return_value='fake_token|fake_url|fake_tenant_id')).start()
|
||||
self.addCleanup(mock.patch.stopall)
|
||||
|
||||
def test_validate_string_empty_string(self):
|
||||
self.assertFalse(self.helper._validate_string(''))
|
||||
|
||||
def test_validate_string_void_string(self):
|
||||
self.assertFalse(self.helper._validate_string(None))
|
||||
|
||||
def test_validate_string_good_string(self):
|
||||
self.assertTrue(self.helper._validate_string('this is a string'))
|
||||
|
||||
def test_make_key(self):
|
||||
expected_key = ('http://111.11.11.11:5000/user/project/region/'
|
||||
'publicURL/share/fake/fake')
|
||||
self.assertEqual(self.helper._make_key(), expected_key)
|
||||
|
||||
def test_make_key_missing_attrs(self):
|
||||
self.cs.client.service_name = self.cs.client.region_name = None
|
||||
expected_key = ('http://111.11.11.11:5000/user/project/?/'
|
||||
'publicURL/share/?/fake')
|
||||
self.assertEqual(self.helper._make_key(), expected_key)
|
||||
|
||||
def test_save(self):
|
||||
shell.ManilaKeyring.get_password.return_value = ''
|
||||
expected_key = ('http://111.11.11.11:5000/user/project/region/'
|
||||
'publicURL/share/fake/fake')
|
||||
self.helper.save('fake_token', 'fake_url', 'fake_tenant_id')
|
||||
shell.ManilaKeyring.set_password.assert_called_once_with(
|
||||
'manilaclient_auth',
|
||||
expected_key,
|
||||
'fake_token|fake_url|fake_tenant_id',
|
||||
)
|
||||
|
||||
def test_save_params_already_cached(self):
|
||||
self.helper.save('fake_token', 'fake_url', 'fake_tenant_id')
|
||||
self.assertFalse(shell.ManilaKeyring.set_password.called)
|
||||
|
||||
def test_save_missing_params(self):
|
||||
self.assertRaises(ValueError, self.helper.save, None, 'fake_url',
|
||||
'fake_tenant_id')
|
||||
|
||||
def test_password_os_password(self):
|
||||
self.args.os_password = 'fake_password'
|
||||
self.args.os_username = 'fake_username'
|
||||
self.helper = shell.SecretsHelper(self.args, self.cs.client)
|
||||
self.assertEqual(self.helper.password, 'fake_password')
|
||||
|
||||
@mock.patch.object(shell.SecretsHelper, '_prompt_password', mock.Mock())
|
||||
def test_password_from_keyboard(self):
|
||||
self.args.os_password = ''
|
||||
self.helper = shell.SecretsHelper(self.args, self.cs.client)
|
||||
shell.SecretsHelper._prompt_password.return_value = None
|
||||
self.assertRaises(exceptions.CommandError, getattr, self.helper,
|
||||
'password')
|
||||
|
||||
def test_management_url_os_cache_false(self):
|
||||
self.args.os_cache = False
|
||||
self.helper = shell.SecretsHelper(self.args, self.cs.client)
|
||||
self.assertIsNone(self.helper.management_url)
|
||||
|
||||
def test_management_url_os_cache_true(self):
|
||||
self.assertEqual(self.helper.management_url, 'fake_url')
|
||||
expected_key = ('http://111.11.11.11:5000/user/project/region/'
|
||||
'publicURL/share/fake/fake')
|
||||
shell.ManilaKeyring.get_password.assert_called_once_with(
|
||||
'manilaclient_auth', expected_key)
|
||||
|
||||
def test_auth_token_os_cache_false(self):
|
||||
self.args.os_cache = False
|
||||
self.helper = shell.SecretsHelper(self.args, self.cs.client)
|
||||
self.assertIsNone(self.helper.auth_token)
|
||||
|
||||
def test_auth_token_os_cache_true(self):
|
||||
self.assertEqual(self.helper.auth_token, 'fake_token')
|
||||
expected_key = ('http://111.11.11.11:5000/user/project/region/'
|
||||
'publicURL/share/fake/fake')
|
||||
shell.ManilaKeyring.get_password.assert_called_once_with(
|
||||
'manilaclient_auth', expected_key)
|
||||
|
||||
def test_tenant_id_os_cache_false(self):
|
||||
self.args.os_cache = False
|
||||
self.helper = shell.SecretsHelper(self.args, self.cs.client)
|
||||
self.assertIsNone(self.helper.tenant_id)
|
||||
|
||||
def test_tenant_id_os_cache_true(self):
|
||||
self.assertEqual(self.helper.tenant_id, 'fake_tenant_id')
|
||||
expected_key = ('http://111.11.11.11:5000/user/project/region/'
|
||||
'publicURL/share/fake/fake')
|
||||
shell.ManilaKeyring.get_password.assert_called_once_with(
|
||||
'manilaclient_auth', expected_key)
|
||||
|
||||
def test_check_cached_password_os_cache_false(self):
|
||||
self.args.os_cache = False
|
||||
self.helper = shell.SecretsHelper(self.args, self.cs.client)
|
||||
self.assertTrue(self.helper.check_cached_password())
|
||||
|
||||
def test_check_cached_password_same_passwords(self):
|
||||
self.args.os_password = 'user_password'
|
||||
self.helper = shell.SecretsHelper(self.args, self.cs.client)
|
||||
shell.ManilaKeyring.get_password.return_value = 'user_password'
|
||||
self.assertTrue(self.helper.check_cached_password())
|
||||
|
||||
def test_check_cached_password_no_cache(self):
|
||||
shell.ManilaKeyring.get_password.return_value = None
|
||||
self.assertTrue(self.helper.check_cached_password())
|
||||
|
||||
def test_check_cached_password_different_passwords(self):
|
||||
self.args.os_password = 'new_user_password'
|
||||
self.helper = shell.SecretsHelper(self.args, self.cs.client)
|
||||
shell.ManilaKeyring.get_password.return_value = 'cached_password'
|
||||
self.assertFalse(self.helper.check_cached_password())
|
||||
|
||||
def test_check_cached_password_cached_password_deleted(self):
|
||||
def _fake_get_password(service, username):
|
||||
if service == 'openstack':
|
||||
return None
|
||||
else:
|
||||
return 'fake_token'
|
||||
|
||||
shell.ManilaKeyring.get_password.side_effect = _fake_get_password
|
||||
self.assertFalse(self.helper.check_cached_password())
|
||||
|
||||
@@ -10,7 +10,14 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from manilaclient import client as httpclient
|
||||
import warnings
|
||||
|
||||
from keystoneclient import adapter
|
||||
from keystoneclient.v2_0 import client as keystone_client_v2
|
||||
from keystoneclient.v3 import client as keystone_client_v3
|
||||
import six
|
||||
|
||||
from manilaclient import httpclient
|
||||
from manilaclient.v1 import limits
|
||||
from manilaclient.v1 import quota_classes
|
||||
from manilaclient.v1 import quotas
|
||||
@@ -30,23 +37,117 @@ class Client(object):
|
||||
|
||||
>>> client = Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL)
|
||||
|
||||
Or, alternatively, you can create a client instance using the
|
||||
keystoneclient.session API::
|
||||
|
||||
>>> from keystoneclient.auth.identity import v2
|
||||
>>> from keystoneclient import session
|
||||
>>> from manilaclient import client
|
||||
>>> auth = v2.Password(auth_url=AUTH_URL,
|
||||
username=USERNAME,
|
||||
password=PASSWORD,
|
||||
tenant_name=PROJECT_ID)
|
||||
>>> sess = session.Session(auth=auth)
|
||||
>>> manila = client.Client(VERSION, session=sess)
|
||||
|
||||
Then call methods on its managers::
|
||||
|
||||
>>> client.share.list()
|
||||
>>> client.shares.list()
|
||||
...
|
||||
"""
|
||||
|
||||
def __init__(self, username, api_key, project_id=None, auth_url='',
|
||||
insecure=False, timeout=None, tenant_id=None,
|
||||
proxy_tenant_id=None, proxy_token=None, region_name=None,
|
||||
def __init__(self, username=None, api_key=None, project_id=None,
|
||||
auth_url=None, insecure=False, timeout=None, tenant_id=None,
|
||||
project_name=None, region_name=None,
|
||||
endpoint_type='publicURL', extensions=None,
|
||||
service_type='share', service_name=None,
|
||||
share_service_name=None, retries=None,
|
||||
http_log_debug=False,
|
||||
cacert=None, os_cache=False):
|
||||
# FIXME(comstud): Rename the api_key argument above when we
|
||||
# know it's not being used as keyword argument
|
||||
password = api_key
|
||||
service_type='share', service_name=None, retries=None,
|
||||
http_log_debug=False, input_auth_token=None, session=None,
|
||||
auth=None, cacert=None, service_catalog_url=None,
|
||||
user_agent='python-manilaclient',
|
||||
use_keyring=False, force_new_token=False,
|
||||
cached_token_lifetime=300, **kwargs):
|
||||
service_name = kwargs.get("share_service_name", service_name)
|
||||
|
||||
def check_deprecated_arguments():
|
||||
deprecated = {
|
||||
'share_service_name': 'service_name',
|
||||
'proxy_tenant_id': None,
|
||||
'proxy_token': None,
|
||||
'os_cache': 'use_keyring'
|
||||
}
|
||||
|
||||
for arg, replacement in six.iteritems(deprecated):
|
||||
if kwargs.get(arg, None) is None:
|
||||
continue
|
||||
|
||||
replacement_msg = ""
|
||||
|
||||
if replacement is not None:
|
||||
replacement_msg = " Use %s instead." % replacement
|
||||
|
||||
msg = "Argument %(arg)s is deprecated.%(repl)s" % {
|
||||
'arg': arg,
|
||||
'repl': replacement_msg
|
||||
}
|
||||
warnings.warn(msg)
|
||||
|
||||
check_deprecated_arguments()
|
||||
|
||||
self.project_id = tenant_id if tenant_id is not None else project_id
|
||||
self.keystone_client = None
|
||||
self.session = session
|
||||
|
||||
if not input_auth_token:
|
||||
if session:
|
||||
self.keystone_client = adapter.LegacyJsonAdapter(
|
||||
session=session,
|
||||
auth=auth,
|
||||
interface=endpoint_type,
|
||||
service_type=service_type,
|
||||
service_name=service_name,
|
||||
region_name=region_name)
|
||||
input_auth_token = self.keystone_client.session.get_token(auth)
|
||||
|
||||
else:
|
||||
self.keystone_client = self._get_keystone_client(
|
||||
username=username,
|
||||
api_key=api_key,
|
||||
auth_url=auth_url,
|
||||
project_id=self.project_id,
|
||||
project_name=project_name,
|
||||
use_keyring=use_keyring,
|
||||
force_new_token=force_new_token,
|
||||
stale_duration=cached_token_lifetime)
|
||||
input_auth_token = self.keystone_client.auth_token
|
||||
|
||||
if not input_auth_token:
|
||||
raise RuntimeError("Not Authorized")
|
||||
|
||||
if session and not service_catalog_url:
|
||||
service_catalog_url = self.keystone_client.session.get_endpoint(
|
||||
auth, interface=endpoint_type,
|
||||
service_type=service_type)
|
||||
elif not service_catalog_url:
|
||||
catalog = self.keystone_client.service_catalog.get_endpoints(
|
||||
service_type)
|
||||
|
||||
if service_type in catalog:
|
||||
for e_type, endpoint in catalog.get(service_type)[0].items():
|
||||
if str(e_type).lower() == str(endpoint_type).lower():
|
||||
service_catalog_url = endpoint
|
||||
break
|
||||
|
||||
if not service_catalog_url:
|
||||
raise RuntimeError("Could not find Manila endpoint in catalog")
|
||||
|
||||
self.client = httpclient.HTTPClient(service_catalog_url,
|
||||
input_auth_token,
|
||||
user_agent,
|
||||
insecure=insecure,
|
||||
cacert=cacert,
|
||||
timeout=timeout,
|
||||
retries=retries,
|
||||
http_log_debug=http_log_debug)
|
||||
|
||||
self.limits = limits.LimitsManager(self)
|
||||
self.services = services.ServiceManager(self)
|
||||
self.security_services = security_services.SecurityServiceManager(self)
|
||||
@@ -61,32 +162,42 @@ class Client(object):
|
||||
self.share_types = share_types.ShareTypeManager(self)
|
||||
self.share_servers = share_servers.ShareServerManager(self)
|
||||
|
||||
# Add in any extensions...
|
||||
if extensions:
|
||||
for extension in extensions:
|
||||
if extension.manager_class:
|
||||
setattr(self, extension.name,
|
||||
extension.manager_class(self))
|
||||
self._load_extensions(extensions)
|
||||
|
||||
self.client = httpclient.HTTPClient(
|
||||
username,
|
||||
password,
|
||||
project_id,
|
||||
auth_url,
|
||||
insecure=insecure,
|
||||
timeout=timeout,
|
||||
tenant_id=tenant_id,
|
||||
proxy_token=proxy_token,
|
||||
proxy_tenant_id=proxy_tenant_id,
|
||||
region_name=region_name,
|
||||
endpoint_type=endpoint_type,
|
||||
service_type=service_type,
|
||||
service_name=service_name,
|
||||
share_service_name=share_service_name,
|
||||
retries=retries,
|
||||
http_log_debug=http_log_debug,
|
||||
cacert=cacert,
|
||||
os_cache=os_cache)
|
||||
def _load_extensions(self, extensions):
|
||||
if not extensions:
|
||||
return
|
||||
|
||||
for extension in extensions:
|
||||
if extension.manager_class:
|
||||
setattr(self, extension.name, extension.manager_class(self))
|
||||
|
||||
def _get_keystone_client(self, username=None, api_key=None, auth_url=None,
|
||||
token=None, project_id=None, project_name=None,
|
||||
use_keyring=False, force_new_token=False,
|
||||
stale_duration=0):
|
||||
if not auth_url:
|
||||
raise RuntimeError("No auth url specified")
|
||||
|
||||
if not getattr(self, "keystone_client", None):
|
||||
imported_client = (keystone_client_v2 if "v2.0" in auth_url
|
||||
else keystone_client_v3)
|
||||
|
||||
self.keystone_client = imported_client.Client(
|
||||
username=username,
|
||||
password=api_key,
|
||||
token=token,
|
||||
tenant_id=project_id,
|
||||
tenant_name=project_name,
|
||||
auth_url=auth_url,
|
||||
endpoint=auth_url,
|
||||
use_keyring=use_keyring,
|
||||
force_new_token=force_new_token,
|
||||
stale_duration=stale_duration)
|
||||
|
||||
self.keystone_client.authenticate()
|
||||
|
||||
return self.keystone_client
|
||||
|
||||
def authenticate(self):
|
||||
"""Authenticate against the server.
|
||||
@@ -97,4 +208,6 @@ class Client(object):
|
||||
Returns on success; raises :exc:`exceptions.Unauthorized` if the
|
||||
credentials are wrong.
|
||||
"""
|
||||
self.client.authenticate()
|
||||
warnings.warn("authenticate() method is deprecated. "
|
||||
"Client automatically makes authentication call "
|
||||
"in the constructor.")
|
||||
|
||||
@@ -132,16 +132,16 @@ def _extract_extra_specs(args):
|
||||
|
||||
def do_endpoints(cs, args):
|
||||
"""Discover endpoints that get returned from the authenticate services."""
|
||||
catalog = cs.client.service_catalog.catalog
|
||||
for e in catalog['access']['serviceCatalog']:
|
||||
catalog = cs.keystone_client.service_catalog.catalog
|
||||
for e in catalog['serviceCatalog']:
|
||||
cliutils.print_dict(e['endpoints'][0], e['name'])
|
||||
|
||||
|
||||
def do_credentials(cs, args):
|
||||
"""Show user credentials returned from auth."""
|
||||
catalog = cs.client.service_catalog.catalog
|
||||
cliutils.print_dict(catalog['access']['user'], "User Credentials")
|
||||
cliutils.print_dict(catalog['access']['token'], "Token")
|
||||
catalog = cs.keystone_client.service_catalog.catalog
|
||||
cliutils.print_dict(catalog['user'], "User Credentials")
|
||||
cliutils.print_dict(catalog['token'], "Token")
|
||||
|
||||
_quota_resources = ['shares', 'snapshots', 'gigabytes', 'share_networks']
|
||||
|
||||
@@ -184,9 +184,9 @@ def _quota_update(manager, identifier, args):
|
||||
help='ID of user to list the quotas for.')
|
||||
def do_quota_show(cs, args):
|
||||
"""List the quotas for a tenant/user."""
|
||||
|
||||
project_id = cs.keystone_client.project_id
|
||||
if not args.tenant:
|
||||
_quota_show(cs.quotas.get(cs.client.tenant_id, user_id=args.user))
|
||||
_quota_show(cs.quotas.get(project_id, user_id=args.user))
|
||||
else:
|
||||
_quota_show(cs.quotas.get(args.tenant, user_id=args.user))
|
||||
|
||||
@@ -198,9 +198,9 @@ def do_quota_show(cs, args):
|
||||
help='ID of tenant to list the default quotas for.')
|
||||
def do_quota_defaults(cs, args):
|
||||
"""List the default quotas for a tenant."""
|
||||
|
||||
project_id = cs.keystone_client.project_id
|
||||
if not args.tenant:
|
||||
_quota_show(cs.quotas.defaults(cs.client.tenant_id))
|
||||
_quota_show(cs.quotas.defaults(project_id))
|
||||
else:
|
||||
_quota_show(cs.quotas.defaults(args.tenant))
|
||||
|
||||
@@ -265,9 +265,9 @@ def do_quota_delete(cs, args):
|
||||
|
||||
The quota will revert back to default.
|
||||
"""
|
||||
|
||||
if not args.tenant:
|
||||
cs.quotas.delete(cs.client.tenant_id, user_id=args.user)
|
||||
project_id = cs.keystone_client.project_id
|
||||
cs.quotas.delete(project_id, user_id=args.user)
|
||||
else:
|
||||
cs.quotas.delete(args.tenant, user_id=args.user)
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ pbr>=0.6,!=0.7,<1.0
|
||||
|
||||
argparse
|
||||
iso8601>=0.1.9
|
||||
keyring>=2.1,!=3.3
|
||||
oslo.config>=1.6.0 # Apache-2.0
|
||||
oslo.serialization>=1.2.0 # Apache-2.0
|
||||
oslo.utils>=1.2.0 # Apache-2.0
|
||||
@@ -17,3 +16,4 @@ requests>=2.2.0,!=2.4.0
|
||||
simplejson>=2.2.0
|
||||
Babel>=1.3
|
||||
six>=1.9.0
|
||||
python-keystoneclient>=1.1.0
|
||||
|
||||
Reference in New Issue
Block a user