Skip to content

Commit

Permalink
Merge "Improve authentication plugins management."
Browse files Browse the repository at this point in the history
  • Loading branch information
Jenkins authored and openstack-gerrit committed Apr 2, 2013
2 parents 3eedc20 + abd75f2 commit d195c6a
Show file tree
Hide file tree
Showing 6 changed files with 377 additions and 48 deletions.
141 changes: 141 additions & 0 deletions novaclient/auth_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Copyright 2013 OpenStack Foundation
# Copyright 2013 Spanish National Research Council.
# 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 logging
import pkg_resources

from novaclient import exceptions
from novaclient import utils


logger = logging.getLogger(__name__)


_discovered_plugins = {}


def discover_auth_systems():
"""Discover the available auth-systems.
This won't take into account the old style auth-systems.
"""
ep_name = 'openstack.client.auth_plugin'
for ep in pkg_resources.iter_entry_points(ep_name):
try:
auth_plugin = ep.load()
except (ImportError, pkg_resources.UnknownExtra, AttributeError) as e:
logger.debug("ERROR: Cannot load auth plugin %s" % ep.name)
logger.debug(e, exc_info=1)
else:
_discovered_plugins[ep.name] = auth_plugin


def load_auth_system_opts(parser):
"""Load options needed by the available auth-systems into a parser.
This function will try to populate the parser with options from the
available plugins.
"""
for name, auth_plugin in _discovered_plugins.iteritems():
add_opts_fn = getattr(auth_plugin, "add_opts", None)
if add_opts_fn:
group = parser.add_argument_group("Auth-system '%s' options" %
name)
add_opts_fn(group)


def load_plugin(auth_system):
if auth_system in _discovered_plugins:
return _discovered_plugins[auth_system]()

# NOTE(aloga): If we arrive here, the plugin will be an old-style one,
# so we have to create a fake AuthPlugin for it.
return DeprecatedAuthPlugin(auth_system)


class BaseAuthPlugin(object):
"""Base class for authentication plugins.
An authentication plugin needs to override at least the authenticate
method to be a valid plugin.
"""
def __init__(self):
self.opts = {}

def get_auth_url(self):
"""Return the auth url for the plugin (if any)."""
return None

@staticmethod
def add_opts(parser):
"""Populate and return the parser with the options for this plugin.
If the plugin does not need any options, it should return the same
parser untouched.
"""
return parser

def parse_opts(self, args):
"""Parse the actual auth-system options if any.
This method is expected to populate the attribute self.opts with a
dict containing the options and values needed to make authentication.
If the dict is empty, the client should assume that it needs the same
options as the 'keystone' auth system (i.e. os_username and
os_password).
Returns the self.opts dict.
"""
return self.opts

def authenticate(self, cls, auth_url):
"""Authenticate using plugin defined method."""
raise exceptions.AuthSystemNotFound(self.auth_system)


class DeprecatedAuthPlugin(object):
"""Class to mimic the AuthPlugin class for deprecated auth systems.
Old auth systems only define two entry points: openstack.client.auth_url
and openstack.client.authenticate. This class will load those entry points
into a class similar to a valid AuthPlugin.
"""
def __init__(self, auth_system):
self.auth_system = auth_system

def authenticate(cls, auth_url):
raise exceptions.AuthSystemNotFound(self.auth_system)

self.opts = {}

self.get_auth_url = lambda: None
self.authenticate = authenticate

self._load_endpoints()

def _load_endpoints(self):
ep_name = 'openstack.client.auth_url'
fn = utils._load_entry_point(ep_name, name=self.auth_system)
if fn:
self.get_auth_url = fn

ep_name = 'openstack.client.authenticate'
fn = utils._load_entry_point(ep_name, name=self.auth_system)
if fn:
self.authenticate = fn

def parse_opts(self, args):
return self.opts
29 changes: 11 additions & 18 deletions novaclient/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,6 @@
from novaclient import utils


def get_auth_system_url(auth_system):
"""Load plugin-based auth_url"""
ep_name = 'openstack.client.auth_url'
for ep in pkg_resources.iter_entry_points(ep_name):
if ep.name == auth_system:
return ep.load()()
raise exceptions.AuthSystemNotFound(auth_system)


class HTTPClient(object):

USER_AGENT = 'python-novaclient'
Expand All @@ -52,12 +43,17 @@ def __init__(self, user, password, projectid, auth_url=None,
timings=False, bypass_url=None,
os_cache=False, no_cache=True,
http_log_debug=False, auth_system='keystone',
auth_plugin=None,
cacert=None):
self.user = user
self.password = password
self.projectid = projectid

if auth_system and auth_system != 'keystone' and not auth_plugin:
raise exceptions.AuthSystemNotFound(auth_system)

if not auth_url and auth_system and auth_system != 'keystone':
auth_url = get_auth_system_url(auth_system)
auth_url = auth_plugin.get_auth_url()
if not auth_url:
raise exceptions.EndpointNotFound()
self.auth_url = auth_url.rstrip('/')
Expand Down Expand Up @@ -94,6 +90,7 @@ def __init__(self, user, password, projectid, auth_url=None,
self.verify_cert = True

self.auth_system = auth_system
self.auth_plugin = auth_plugin

self._logger = logging.getLogger(__name__)
if self.http_log_debug:
Expand Down Expand Up @@ -392,12 +389,7 @@ def _v1_auth(self, url):
raise exceptions.from_response(resp, body, url)

def _plugin_auth(self, auth_url):
"""Load plugin-based authentication"""
ep_name = 'openstack.client.authenticate'
for ep in pkg_resources.iter_entry_points(ep_name):
if ep.name == self.auth_system:
return ep.load()(self, auth_url)
raise exceptions.AuthSystemNotFound(self.auth_system)
self.auth_plugin.authenticate(self, auth_url)

def _v2_auth(self, url):
"""Authenticate against a v2.0 auth service."""
Expand All @@ -414,7 +406,7 @@ def _v2_auth(self, url):

self._authenticate(url, body)

def _authenticate(self, url, body):
def _authenticate(self, url, body, **kwargs):
"""Authenticate and extract the service catalog."""
token_url = url + "/tokens"

Expand All @@ -423,7 +415,8 @@ def _authenticate(self, url, body):
token_url,
"POST",
body=body,
allow_redirects=True)
allow_redirects=True,
**kwargs)

return self._extract_service_catalog(url, resp, body)

Expand Down
40 changes: 31 additions & 9 deletions novaclient/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
pass

import novaclient
import novaclient.auth_plugin
from novaclient import client
from novaclient import exceptions as exc
import novaclient.extension
Expand Down Expand Up @@ -398,6 +399,9 @@ def get_base_parser(self):
parser.add_argument('--bypass_url',
help=argparse.SUPPRESS)

# The auth-system-plugins might require some extra options
novaclient.auth_plugin.load_auth_system_opts(parser)

return parser

def get_subcommand_parser(self, version):
Expand Down Expand Up @@ -514,11 +518,15 @@ def setup_debugging(self, debug):
format=streamformat)

def main(self, argv):

# Parse args once to find version and debug settings
parser = self.get_base_parser()
(options, args) = parser.parse_known_args(argv)
self.setup_debugging(options.debug)

# Discover available auth plugins
novaclient.auth_plugin.discover_auth_systems()

# build available subcommands based on version
self.extensions = self._discover_extensions(
options.os_compute_api_version)
Expand Down Expand Up @@ -566,6 +574,11 @@ def main(self, argv):
args.bypass_url, args.os_cache,
args.os_cacert, args.timeout)

if os_auth_system and os_auth_system != "keystone":
auth_plugin = novaclient.auth_plugin.load_plugin(os_auth_system)
else:
auth_plugin = None

# Fetched and set later as needed
os_password = None

Expand All @@ -579,12 +592,16 @@ def main(self, argv):
#FIXME(usrleon): Here should be restrict for project id same as
# for os_username or os_password but for compatibility it is not.
if not utils.isunauthenticated(args.func):
if not os_username:
if not username:
raise exc.CommandError("You must provide a username "
"via either --os-username or env[OS_USERNAME]")
else:
os_username = username
if auth_plugin:
auth_plugin.parse_opts(args)

if not auth_plugin or not auth_plugin.opts:
if not os_username:
if not username:
raise exc.CommandError("You must provide a username "
"via either --os-username or env[OS_USERNAME]")
else:
os_username = username

if not os_tenant_name:
if not projectid:
Expand All @@ -597,8 +614,7 @@ def main(self, argv):
if not os_auth_url:
if not url:
if os_auth_system and os_auth_system != 'keystone':
os_auth_url = \
client.get_auth_system_url(os_auth_system)
os_auth_url = auth_plugin.get_auth_url()
else:
os_auth_url = url

Expand Down Expand Up @@ -627,6 +643,7 @@ def main(self, argv):
region_name=os_region_name, endpoint_type=endpoint_type,
extensions=self.extensions, service_type=service_type,
service_name=service_name, auth_system=os_auth_system,
auth_plugin=auth_plugin,
volume_service_name=volume_service_name,
timings=args.timings, bypass_url=bypass_url,
os_cache=os_cache, http_log_debug=options.debug,
Expand All @@ -636,7 +653,12 @@ def main(self, argv):
# identifying keyring key can come from the underlying client
if not utils.isunauthenticated(args.func):
helper = SecretsHelper(args, self.cs.client)
use_pw = True
if (auth_plugin and auth_plugin.opts and
"os_password" not in auth_plugin.opts):
use_pw = False
else:
use_pw = True

tenant_id, auth_token, management_url = (helper.tenant_id,
helper.auth_token,
helper.management_url)
Expand Down
10 changes: 10 additions & 0 deletions novaclient/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import pkg_resources
import re
import sys
import textwrap
Expand Down Expand Up @@ -369,3 +370,12 @@ def check_uuid_like(val):
raise exceptions.CommandError(
"error: Invalid tenant-id %s supplied"
% val)


def _load_entry_point(ep_name, name=None):
"""Try to load the entry point ep_name that matches name."""
for ep in pkg_resources.iter_entry_points(ep_name, name=name):
try:
return ep.load()
except (ImportError, pkg_resources.UnknownExtra, AttributeError):
continue
2 changes: 2 additions & 0 deletions novaclient/v1_1/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def __init__(self, username, api_key, project_id, auth_url=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,
cacert=None):
# FIXME(comstud): Rename the api_key argument above when we
# know it's not being used as keyword argument
Expand Down Expand Up @@ -132,6 +133,7 @@ def __init__(self, username, api_key, project_id, auth_url=None,
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,
Expand Down
Loading

0 comments on commit d195c6a

Please sign in to comment.