Allow us to use keystoneclient's session

The session object is a cross-client means of standardizing the
transport layer.

Novaclient's HTTPClient object has diverged significantly from other
clients. It is easier to simply replace it if a session is provided. If
a session is provided then users of the library need to be aware that
functions such as authenticate() will no longer have any effect/are in
error because this is no longer managed by nova.

Change-Id: I8f146b878908239d9b6c1c7d6cdc01c7e124f4e5
This commit is contained in:
Jamie Lennox 2014-04-08 11:40:41 +10:00 committed by Mark McLoughlin
parent b9538d8280
commit cc7364067f
17 changed files with 276 additions and 83 deletions

View File

@ -21,6 +21,7 @@ OpenStack Client interface. Handles the REST calls and responses.
"""
import errno
import functools
import glob
import hashlib
import logging
@ -132,6 +133,86 @@ class CompletionCache(object):
self._write_attribute(resource, attribute, value)
class SessionClient(object):
def __init__(self, session, auth, interface, service_type, region_name):
self.session = session
self.auth = auth
self.interface = interface
self.service_type = service_type
self.region_name = region_name
def request(self, url, method, **kwargs):
kwargs.setdefault('user_agent', 'python-novaclient')
kwargs.setdefault('auth', self.auth)
kwargs.setdefault('authenticated', False)
try:
kwargs['json'] = kwargs.pop('body')
except KeyError:
pass
headers = kwargs.setdefault('headers', {})
headers.setdefault('Accept', 'application/json')
endpoint_filter = kwargs.setdefault('endpoint_filter', {})
endpoint_filter.setdefault('interface', self.interface)
endpoint_filter.setdefault('service_type', self.service_type)
endpoint_filter.setdefault('region_name', self.region_name)
resp = self.session.request(url, method, raise_exc=False, **kwargs)
body = None
if resp.text:
try:
body = resp.json()
except ValueError:
pass
if resp.status_code >= 400:
raise exceptions.from_response(resp, body, url, method)
return resp, body
def _cs_request(self, url, method, **kwargs):
# this function is mostly redundant but makes compatibility easier
kwargs.setdefault('authenticated', True)
return self.request(url, method, **kwargs)
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 _original_only(f):
"""Indicates and enforces that this function can only be used if we are
using the original HTTPClient object.
We use this to specify that if you use the newer Session HTTP client then
you are aware that the way you use your client has been updated and certain
functions are no longer allowed to be used.
"""
@functools.wraps(f)
def wrapper(self, *args, **kwargs):
if isinstance(self.client, SessionClient):
msg = ('This call is no longer available. The operation should '
'be performed on the session object instead.')
raise exceptions.InvalidUsage(msg)
return f(self, *args, **kwargs)
return wrapper
class HTTPClient(object):
USER_AGENT = 'python-novaclient'
@ -606,6 +687,53 @@ class HTTPClient(object):
return self._extract_service_catalog(url, resp, respbody)
def _construct_http_client(username=None, password=None, project_id=None,
auth_url=None, insecure=False, timeout=None,
proxy_tenant_id=None, proxy_token=None,
region_name=None, endpoint_type='publicURL',
extensions=None, service_type='compute',
service_name=None, volume_service_name=None,
timings=False, bypass_url=None, os_cache=False,
no_cache=True, http_log_debug=False,
auth_system='keystone', auth_plugin=None,
auth_token=None, cacert=None, tenant_id=None,
user_id=None, connection_pool=False, session=None,
auth=None):
if session:
return SessionClient(session=session,
auth=auth,
interface=endpoint_type,
service_type=service_type,
region_name=region_name)
else:
# FIXME(jamielennox): username and password are now optional. Need
# to test that they were provided in this mode.
return HTTPClient(username,
password,
user_id=user_id,
projectid=project_id,
tenant_id=tenant_id,
auth_url=auth_url,
auth_token=auth_token,
insecure=insecure,
timeout=timeout,
auth_system=auth_system,
auth_plugin=auth_plugin,
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,
volume_service_name=volume_service_name,
timings=timings,
bypass_url=bypass_url,
os_cache=os_cache,
http_log_debug=http_log_debug,
cacert=cacert,
connection_pool=connection_pool)
def get_client_class(version):
version_map = {
'1.1': 'novaclient.v1_1.client.Client',

View File

@ -64,6 +64,16 @@ _code_map = dict(
)
class InvalidUsage(RuntimeError):
"""This function call is invalid in the way you are using this client.
Due to the transition to using keystoneclient some function calls are no
longer available. You should make a similar call to the session object
instead.
"""
pass
def from_response(response, body, url, method=None):
"""
Return an instance of an HttpError or subclass

View File

@ -12,6 +12,8 @@
import fixtures
import httpretty
from keystoneclient.auth.identity import v2
from keystoneclient import session
from novaclient.openstack.common import jsonutils
from novaclient.v1_1 import client as v1_1client
@ -107,3 +109,19 @@ class V3(V1):
password='xx',
project_id='xx',
auth_url=self.identity_url)
class SessionV1(V1):
def new_client(self):
self.session = session.Session()
self.session.auth = v2.Password(self.identity_url, 'xx', 'xx')
return v1_1client.Client(session=self.session)
class SessionV3(V1):
def new_client(self):
self.session = session.Session()
self.session.auth = v2.Password(self.identity_url, 'xx', 'xx')
return v3client.Client(session=self.session)

View File

@ -17,6 +17,7 @@ import fixtures
import httpretty
import requests
import six
import testscenarios
import testtools
from novaclient.openstack.common import jsonutils
@ -43,7 +44,7 @@ class TestCase(testtools.TestCase):
self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
class FixturedTestCase(TestCase):
class FixturedTestCase(testscenarios.TestWithScenarios, TestCase):
client_fixture_class = None
data_fixture_class = None

View File

@ -24,9 +24,11 @@ from novaclient.v1_1 import agents
class AgentsTest(utils.FixturedTestCase):
client_fixture_class = client.V1
data_fixture_class = data.Fixture
scenarios = [('original', {'client_fixture_class': client.V1}),
('session', {'client_fixture_class': client.SessionV1})]
def stub_hypervisors(self, hypervisor='kvm'):
get_os_agents = {'agents':
[

View File

@ -21,9 +21,11 @@ from novaclient.v1_1 import aggregates
class AggregatesTest(utils.FixturedTestCase):
client_fixture_class = client.V1
data_fixture_class = data.Fixture
scenarios = [('original', {'client_fixture_class': client.V1}),
('session', {'client_fixture_class': client.SessionV1})]
def test_list_aggregates(self):
result = self.cs.aggregates.list()
self.assert_called('GET', '/os-aggregates')

View File

@ -27,9 +27,11 @@ class AvailabilityZoneTest(utils.FixturedTestCase):
# this class can inherit off the v3 version of shell
from novaclient.v1_1 import shell # noqa
client_fixture_class = client.V1
data_fixture_class = data.V1
scenarios = [('original', {'client_fixture_class': client.V1}),
('session', {'client_fixture_class': client.SessionV1})]
def setUp(self):
super(AvailabilityZoneTest, self).setUp()
self.availability_zone_type = self._get_availability_zone_type()

View File

@ -19,10 +19,12 @@ from novaclient.v1_1 import certs
class CertsTest(utils.FixturedTestCase):
client_fixture_class = client.V1
data_fixture_class = data.Fixture
cert_type = certs.Certificate
scenarios = [('original', {'client_fixture_class': client.V1}),
('session', {'client_fixture_class': client.SessionV1})]
def test_create_cert(self):
cert = self.cs.certs.create()
self.assert_called('POST', '/os-certificates')

View File

@ -21,9 +21,11 @@ from novaclient.v1_1 import cloudpipe
class CloudpipeTest(utils.FixturedTestCase):
client_fixture_class = client.V1
data_fixture_class = data.Fixture
scenarios = [('original', {'client_fixture_class': client.V1}),
('session', {'client_fixture_class': client.SessionV1})]
def test_list_cloudpipes(self):
cp = self.cs.cloudpipe.list()
self.assert_called('GET', '/os-cloudpipe')

View File

@ -20,9 +20,11 @@ from novaclient.tests import utils
class FixedIpsTest(utils.FixturedTestCase):
client_fixture_class = client.V1
data_fixture_class = data.Fixture
scenarios = [('original', {'client_fixture_class': client.V1}),
('session', {'client_fixture_class': client.SessionV1})]
def test_get_fixed_ip(self):
info = self.cs.fixed_ips.get(fixed_ip='192.168.1.1')
self.assert_called('GET', '/os-fixed-ips/192.168.1.1')

View File

@ -19,7 +19,8 @@ from novaclient.tests.v1_1 import test_agents
class AgentsTest(test_agents.AgentsTest):
client_fixture_class = client.V3
scenarios = [('original', {'client_fixture_class': client.V3}),
('session', {'client_fixture_class': client.SessionV3})]
def _build_example_update_body(self):
return {"agent": {

View File

@ -17,4 +17,6 @@ from novaclient.tests.v1_1 import test_aggregates
class AggregatesTest(test_aggregates.AggregatesTest):
client_fixture = client.V3
scenarios = [('original', {'client_fixture_class': client.V3}),
('session', {'client_fixture_class': client.SessionV3})]

View File

@ -23,9 +23,11 @@ from novaclient.v3 import availability_zones
class AvailabilityZoneTest(test_availability_zone.AvailabilityZoneTest):
from novaclient.v3 import shell # noqa
client_fixture_class = client.V3
data_fixture_class = data.V3
scenarios = [('original', {'client_fixture_class': client.V3}),
('session', {'client_fixture_class': client.SessionV3})]
def _assertZone(self, zone, name, status):
self.assertEqual(zone.zone_name, name)
self.assertEqual(zone.zone_state, status)

View File

@ -17,4 +17,5 @@ from novaclient.tests.v1_1 import test_certs
class CertsTest(test_certs.CertsTest):
client_fixture_data = client.V3
scenarios = [('original', {'client_fixture_class': client.V3}),
('session', {'client_fixture_class': client.SessionV3})]

View File

@ -78,18 +78,17 @@ class Client(object):
... AUTH_URL, connection_pool=True)
"""
# FIXME(jesse): project_id isn't required to authenticate
def __init__(self, username, api_key, project_id, auth_url=None,
insecure=False, timeout=None, proxy_tenant_id=None,
proxy_token=None, region_name=None,
endpoint_type='publicURL', extensions=None,
service_type='compute', service_name=None,
volume_service_name=None, timings=False,
bypass_url=None, os_cache=False, no_cache=True,
http_log_debug=False, auth_system='keystone',
auth_plugin=None, auth_token=None,
cacert=None, tenant_id=None, user_id=None,
connection_pool=False, completion_cache=None):
def __init__(self, username=None, api_key=None, project_id=None,
auth_url=None, insecure=False, timeout=None,
proxy_tenant_id=None, proxy_token=None, region_name=None,
endpoint_type='publicURL', extensions=None,
service_type='compute', service_name=None,
volume_service_name=None, timings=False, bypass_url=None,
os_cache=False, no_cache=True, http_log_debug=False,
auth_system='keystone', auth_plugin=None, auth_token=None,
cacert=None, tenant_id=None, user_id=None,
connection_pool=False, session=None, auth=None,
completion_cache=None):
# FIXME(comstud): Rename the api_key argument above when we
# know it's not being used as keyword argument
@ -149,30 +148,33 @@ class Client(object):
setattr(self, extension.name,
extension.manager_class(self))
self.client = client.HTTPClient(username,
password,
user_id=user_id,
projectid=project_id,
tenant_id=tenant_id,
auth_url=auth_url,
auth_token=auth_token,
insecure=insecure,
timeout=timeout,
auth_system=auth_system,
auth_plugin=auth_plugin,
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,
volume_service_name=volume_service_name,
timings=timings,
bypass_url=bypass_url,
os_cache=self.os_cache,
http_log_debug=http_log_debug,
cacert=cacert,
connection_pool=connection_pool)
self.client = client._construct_http_client(
username=username,
password=password,
user_id=user_id,
project_id=project_id,
tenant_id=tenant_id,
auth_url=auth_url,
auth_token=auth_token,
insecure=insecure,
timeout=timeout,
auth_system=auth_system,
auth_plugin=auth_plugin,
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,
volume_service_name=volume_service_name,
timings=timings,
bypass_url=bypass_url,
os_cache=self.os_cache,
http_log_debug=http_log_debug,
cacert=cacert,
connection_pool=connection_pool,
session=session,
auth=auth)
self.completion_cache = completion_cache
@ -184,22 +186,28 @@ class Client(object):
if self.completion_cache:
self.completion_cache.clear_class(obj_class)
@client._original_only
def __enter__(self):
self.client.open_session()
return self
@client._original_only
def __exit__(self, t, v, tb):
self.client.close_session()
@client._original_only
def set_management_url(self, url):
self.client.set_management_url(url)
@client._original_only
def get_timings(self):
return self.client.get_timings()
@client._original_only
def reset_timings(self):
self.client.reset_timings()
@client._original_only
def authenticate(self):
"""
Authenticate against the server.

View File

@ -63,18 +63,17 @@ class Client(object):
... AUTH_URL, connection_pool=True)
"""
# FIXME(jesse): project_id isn't required to authenticate
def __init__(self, username, password, project_id, auth_url=None,
insecure=False, timeout=None, proxy_tenant_id=None,
proxy_token=None, region_name=None,
endpoint_type='publicURL', extensions=None,
service_type='computev3', service_name=None,
volume_service_name=None, timings=False,
bypass_url=None, os_cache=False, no_cache=True,
http_log_debug=False, auth_system='keystone',
auth_plugin=None, auth_token=None,
cacert=None, tenant_id=None, user_id=None,
connection_pool=False, completion_cache=None):
def __init__(self, username=None, password=None, project_id=None,
auth_url=None, insecure=False, timeout=None,
proxy_tenant_id=None, proxy_token=None, region_name=None,
endpoint_type='publicURL', extensions=None,
service_type='computev3', service_name=None,
volume_service_name=None, timings=False, bypass_url=None,
os_cache=False, no_cache=True, http_log_debug=False,
auth_system='keystone', auth_plugin=None, auth_token=None,
cacert=None, tenant_id=None, user_id=None,
connection_pool=False, session=None, auth=None,
completion_cache=None):
# NOTE(cyeoh): In the novaclient context (unlike Nova) the
# project_id is not the same as the tenant_id. Here project_id
# is a name (what the Nova API often refers to as a project or
@ -111,30 +110,33 @@ class Client(object):
setattr(self, extension.name,
extension.manager_class(self))
self.client = client.HTTPClient(username,
password,
user_id=user_id,
projectid=project_id,
tenant_id=tenant_id,
auth_url=auth_url,
insecure=insecure,
timeout=timeout,
auth_system=auth_system,
auth_plugin=auth_plugin,
auth_token=auth_token,
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,
volume_service_name=volume_service_name,
timings=timings,
bypass_url=bypass_url,
os_cache=os_cache,
http_log_debug=http_log_debug,
cacert=cacert,
connection_pool=connection_pool)
self.client = client._construct_http_client(
username=username,
password=password,
user_id=user_id,
project_id=project_id,
tenant_id=tenant_id,
auth_url=auth_url,
auth_token=auth_token,
insecure=insecure,
timeout=timeout,
auth_system=auth_system,
auth_plugin=auth_plugin,
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,
volume_service_name=volume_service_name,
timings=timings,
bypass_url=bypass_url,
os_cache=self.os_cache,
http_log_debug=http_log_debug,
cacert=cacert,
connection_pool=connection_pool,
session=session,
auth=auth)
self.completion_cache = completion_cache
@ -146,22 +148,28 @@ class Client(object):
if self.completion_cache:
self.completion_cache.clear_class(obj_class)
@client._original_only
def __enter__(self):
self.client.open_session()
return self
@client._original_only
def __exit__(self, t, v, tb):
self.client.close_session()
@client._original_only
def set_management_url(self, url):
self.client.set_management_url(url)
@client._original_only
def get_timings(self):
return self.client.get_timings()
@client._original_only
def reset_timings(self):
self.client.reset_timings()
@client._original_only
def authenticate(self):
"""
Authenticate against the server.

View File

@ -7,6 +7,8 @@ httpretty>=0.8.0,!=0.8.1,!=0.8.2
keyring>=2.1
mock>=1.0
sphinx>=1.1.2,!=1.2.0,<1.3
python-keystoneclient>=0.9.0
oslosphinx
testrepository>=0.0.18
testscenarios>=0.4
testtools>=0.9.34