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 errno
import functools
import glob import glob
import hashlib import hashlib
import logging import logging
@ -132,6 +133,86 @@ class CompletionCache(object):
self._write_attribute(resource, attribute, value) 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): class HTTPClient(object):
USER_AGENT = 'python-novaclient' USER_AGENT = 'python-novaclient'
@ -606,6 +687,53 @@ class HTTPClient(object):
return self._extract_service_catalog(url, resp, respbody) 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): def get_client_class(version):
version_map = { version_map = {
'1.1': 'novaclient.v1_1.client.Client', '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): def from_response(response, body, url, method=None):
""" """
Return an instance of an HttpError or subclass Return an instance of an HttpError or subclass

View File

@ -12,6 +12,8 @@
import fixtures import fixtures
import httpretty import httpretty
from keystoneclient.auth.identity import v2
from keystoneclient import session
from novaclient.openstack.common import jsonutils from novaclient.openstack.common import jsonutils
from novaclient.v1_1 import client as v1_1client from novaclient.v1_1 import client as v1_1client
@ -107,3 +109,19 @@ class V3(V1):
password='xx', password='xx',
project_id='xx', project_id='xx',
auth_url=self.identity_url) 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 httpretty
import requests import requests
import six import six
import testscenarios
import testtools import testtools
from novaclient.openstack.common import jsonutils from novaclient.openstack.common import jsonutils
@ -43,7 +44,7 @@ class TestCase(testtools.TestCase):
self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
class FixturedTestCase(TestCase): class FixturedTestCase(testscenarios.TestWithScenarios, TestCase):
client_fixture_class = None client_fixture_class = None
data_fixture_class = None data_fixture_class = None

View File

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

View File

@ -21,9 +21,11 @@ from novaclient.v1_1 import aggregates
class AggregatesTest(utils.FixturedTestCase): class AggregatesTest(utils.FixturedTestCase):
client_fixture_class = client.V1
data_fixture_class = data.Fixture data_fixture_class = data.Fixture
scenarios = [('original', {'client_fixture_class': client.V1}),
('session', {'client_fixture_class': client.SessionV1})]
def test_list_aggregates(self): def test_list_aggregates(self):
result = self.cs.aggregates.list() result = self.cs.aggregates.list()
self.assert_called('GET', '/os-aggregates') 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 # this class can inherit off the v3 version of shell
from novaclient.v1_1 import shell # noqa from novaclient.v1_1 import shell # noqa
client_fixture_class = client.V1
data_fixture_class = data.V1 data_fixture_class = data.V1
scenarios = [('original', {'client_fixture_class': client.V1}),
('session', {'client_fixture_class': client.SessionV1})]
def setUp(self): def setUp(self):
super(AvailabilityZoneTest, self).setUp() super(AvailabilityZoneTest, self).setUp()
self.availability_zone_type = self._get_availability_zone_type() 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): class CertsTest(utils.FixturedTestCase):
client_fixture_class = client.V1
data_fixture_class = data.Fixture data_fixture_class = data.Fixture
cert_type = certs.Certificate cert_type = certs.Certificate
scenarios = [('original', {'client_fixture_class': client.V1}),
('session', {'client_fixture_class': client.SessionV1})]
def test_create_cert(self): def test_create_cert(self):
cert = self.cs.certs.create() cert = self.cs.certs.create()
self.assert_called('POST', '/os-certificates') self.assert_called('POST', '/os-certificates')

View File

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

View File

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

View File

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

View File

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

View File

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