From 6cac288a87d0eba448394ac39981bf9f63238125 Mon Sep 17 00:00:00 2001 From: Eoghan Glynn Date: Fri, 20 Jan 2012 20:27:02 +0000 Subject: [PATCH] More flexible specification of auth credentials. Fixes bug 853933 Add new --username|--password|--tenant|--auth_url|--auth_strategy switches to bin/glance to allow the username, password, tenant name, and authentication URL & strategy be specified on the command line. Avoid needlessly falling back to keystone v2 auth after a successful: GET /v1.0/tokens returns with the X-Image-Management-Url or X-Glance header set, as opposed to X-Server-Management-Url. Extend the keystone functional test support to ensure that the URL returned by keystone via the X-*-Url header contains the appropriate dynamically allocated port for the glance API service. Ensure the underlying $OS_* environment variables do not leak into the TestPrivateImagesCli functional tests, also explicitly exercise both noauth and keystone strategies. Change-Id: Iee8bf3745d65a9c57a9da803d5cf9ae5f343a159 --- bin/glance | 26 ++++++-- doc/source/authentication.rst | 26 ++++++++ glance/common/auth.py | 12 +++- glance/tests/functional/data/keystone_data.py | 31 ++++++++- glance/tests/functional/keystone_utils.py | 6 +- .../tests/functional/test_private_images.py | 66 ++++++++++++++++--- 6 files changed, 148 insertions(+), 19 deletions(-) diff --git a/bin/glance b/bin/glance index 36c4ac7b54..ea642d4c71 100755 --- a/bin/glance +++ b/bin/glance @@ -731,11 +731,12 @@ def get_client(options): specified by the --host and --port options supplied to the CLI """ - creds = dict(username=os.getenv('OS_AUTH_USER'), - password=os.getenv('OS_AUTH_KEY'), - tenant=os.getenv('OS_AUTH_TENANT'), - auth_url=os.getenv('OS_AUTH_URL'), - strategy=os.getenv('OS_AUTH_STRATEGY', 'noauth')) + creds = dict(username=options.username or os.getenv('OS_AUTH_USER'), + password=options.password or os.getenv('OS_AUTH_KEY'), + tenant=options.tenant or os.getenv('OS_AUTH_TENANT'), + auth_url=options.auth_url or os.getenv('OS_AUTH_URL'), + strategy=options.auth_strategy or \ + os.getenv('OS_AUTH_STRATEGY', 'noauth')) use_ssl = (options.use_ssl or ( creds['auth_url'] is not None and @@ -774,6 +775,21 @@ def create_options(parser): metavar="TOKEN", default=None, help="Authentication token to use to identify the " "client to the glance server") + parser.add_option('-I', '--username', dest="username", + metavar="USER", default=None, + help="User name used to acquire an authentication token") + parser.add_option('-K', '--password', dest="password", + metavar="PASSWORD", default=None, + help="Password used to acquire an authentication token") + parser.add_option('-T', '--tenant', dest="tenant", + metavar="TENANT", default=None, + help="Tenant name") + parser.add_option('-N', '--auth_url', dest="auth_url", + metavar="AUTH_URL", default=None, + help="Authentication URL") + parser.add_option('-S', '--auth_strategy', dest="auth_strategy", + metavar="STRATEGY", default=None, + help="Authentication strategy (keystone or noauth)") parser.add_option('--limit', dest="limit", metavar="LIMIT", default=10, type="int", help="Page size to use while " "requesting image metadata") diff --git a/doc/source/authentication.rst b/doc/source/authentication.rst index 66685adb24..7af30e35ba 100644 --- a/doc/source/authentication.rst +++ b/doc/source/authentication.rst @@ -84,6 +84,32 @@ The final step is to verify that the `OS_AUTH_` crednetials are present:: OS_AUTH_URL= OS_AUTH_STRATEGY=keystone +Alternatively, these credentials may be specified using the following +switches to the ``bin/glance`` command: + + -I USER, --username=USER + User name used to acquire an authentication token + -K PASSWORD, --password=PASSWORD + Password used to acquire an authentication token + -T TENANT, --tenant=TENANT + Tenant name + -N AUTH_URL, --auth_url=AUTH_URL + Authentication URL + -S STRATEGY, --auth_strategy=STRATEGY + Authentication strategy (keystone or noauth) + +Or, if a pre-authenticated token is preferred, the following option allows +the client-side interaction with keystone to be by-passed (useful if a long +sequence of commands is being scripted): + + -A TOKEN, --auth_token=TOKEN + Authentication token to use to identify the client to + the glance server + +In general the command line switch takes precedence over the corresponding +OS_AUTH_* environment variable, if both are set. + + Configuring the Glance servers to use Keystone ---------------------------------------------- diff --git a/glance/common/auth.py b/glance/common/auth.py index 029eba112a..6ea3a4121d 100644 --- a/glance/common/auth.py +++ b/glance/common/auth.py @@ -140,9 +140,19 @@ class KeystoneStrategy(BaseStrategy): resp, resp_body = self._do_request(token_url, 'GET', headers=headers) + def _management_url(self, resp): + for url_header in ('x-image-management-url', + 'x-server-management-url', + 'x-glance'): + try: + return resp[url_header] + except KeyError as e: + not_found = e + raise not_found + if resp.status in (200, 204): try: - self.management_url = resp['x-server-management-url'] + self.management_url = _management_url(self, resp) self.auth_token = resp['x-auth-token'] except KeyError: raise exception.AuthorizationFailure() diff --git a/glance/tests/functional/data/keystone_data.py b/glance/tests/functional/data/keystone_data.py index 6c4bbefb34..62df83901b 100644 --- a/glance/tests/functional/data/keystone_data.py +++ b/glance/tests/functional/data/keystone_data.py @@ -15,6 +15,9 @@ # License for the specific language governing permissions and limitations # under the License. +import re +import sys + import keystone.manage DEFAULT_FIXTURE = [ @@ -85,7 +88,7 @@ DEFAULT_FIXTURE = [ 'http://nova.publicinternets.com/v1.1/', 'http://127.0.0.1:8774/v1.1', 'http://localhost:8774/v1.1', '1', '0'), ('endpointTemplates', 'add', 'RegionOne', 'glance', - 'http://glance.publicinternets.com/v1.1/%tenant_id%', + 'http://127.0.0.1:%api_port%/v1', 'http://nova.admin-nets.local/v1.1/%tenant_id%', 'http://127.0.0.1:9292/v1.1/%tenant_id%', '1', '0'), ('endpointTemplates', 'add', 'RegionOne', 'cdn', @@ -140,10 +143,34 @@ DEFAULT_FIXTURE = [ ] +def _get_subsitutions(args): + substitutions = {} + for arg in args: + matches = re.match('(.*)=(.*)', arg) + if matches: + token = '%%%s%%' % matches.group(1) + value = matches.group(2) + substitutions[token] = value + return substitutions + + +def _expand(cmd, substitutions): + expanded = () + for word in cmd: + for token in substitutions.keys(): + word = word.replace(token, substitutions[token]) + expanded = expanded + (word,) + return expanded + + def load_fixture(fixture=DEFAULT_FIXTURE, args=None): + substitutions = _get_subsitutions(sys.argv) + keystone.manage.parse_args(args) + for cmd in fixture: - keystone.manage.process(*cmd) + expanded = _expand(cmd, substitutions) + keystone.manage.process(*expanded) def main(): diff --git a/glance/tests/functional/keystone_utils.py b/glance/tests/functional/keystone_utils.py index 8e459876f6..85f270596f 100644 --- a/glance/tests/functional/keystone_utils.py +++ b/glance/tests/functional/keystone_utils.py @@ -66,6 +66,7 @@ log_file = %(log_file)s backends = keystone.backends.sqlalchemy service-header-mappings = { 'nova' : 'X-Server-Management-Url', + 'glance' : 'X-Image-Management-Url', 'swift' : 'X-Storage-Url', 'cdn' : 'X-CDN-Management-Url'} service_host = 0.0.0.0 @@ -247,7 +248,10 @@ class KeystoneTests(functional.FunctionalTest): keystone_conf = self.auth_server.write_conf(**kwargs) datafile = os.path.join(os.path.dirname(__file__), 'data', 'keystone_data.py') - execute("python %s -c %s" % (datafile, keystone_conf)) + + cmd = "python %s -c %s api_port=%d" % \ + (datafile, keystone_conf, self.api_server.bind_port) + execute(cmd) # Start keystone-auth exitcode, out, err = self.auth_server.start(**kwargs) diff --git a/glance/tests/functional/test_private_images.py b/glance/tests/functional/test_private_images.py index 73b0e3f2ba..91abe42640 100644 --- a/glance/tests/functional/test_private_images.py +++ b/glance/tests/functional/test_private_images.py @@ -19,6 +19,7 @@ import httplib2 import json +import os from glance.tests import functional from glance.tests.functional import keystone_utils @@ -741,21 +742,25 @@ class TestPrivateImagesCli(keystone_utils.KeystoneTests): bin/glance. """ - @skip_if_disabled - def test_glance_cli(self): + def setUp(self): """ - Test that we can upload an owned image; that we can manipulate - its is_public setting; and that appropriate authorization - checks are applied to other (non-admin) users. + Clear environment to ensure that pre-existing $OS_* variables + do not leak into test. + """ + self._clear_os_env() + super(TestPrivateImagesCli, self).setUp() + + def _do_test_glance_cli(self, cmd): + """ + Test that we can upload an owned image with a given command line; + that we can manipulate its is_public setting; and that appropriate + authorization checks are applied to other (non-admin) users. """ self.cleanup() self.start_servers() - # Add a non-public image - cmd = ("echo testdata | bin/glance --port=%d --auth_token=%s add " - "name=MyImage" % - (self.api_port, keystone_utils.pattieblack_token)) - exitcode, out, err = execute(cmd) + # Add a non-public image using the given glance command line + exitcode, out, err = execute("echo testdata | %s" % cmd) self.assertEqual(0, exitcode) image_id = out.strip()[25:] @@ -833,3 +838,44 @@ class TestPrivateImagesCli(keystone_utils.KeystoneTests): self.assertEqual(response['x-image-meta-owner'], '') self.stop_servers() + + def _clear_os_env(self): + os.environ.pop('OS_AUTH_URL', None) + os.environ.pop('OS_AUTH_STRATEGY', None) + os.environ.pop('OS_AUTH_USER', None) + os.environ.pop('OS_AUTH_KEY', None) + + @skip_if_disabled + def test_glance_cli_noauth_strategy(self): + """ + Test the CLI with the noauth strategy defaulted to. + """ + cmd = ("bin/glance --port=%d --auth_token=%s add name=MyImage" % + (self.api_port, keystone_utils.pattieblack_token)) + self._do_test_glance_cli(cmd) + + @skip_if_disabled + def test_glance_cli_keystone_strategy_switches(self): + """ + Test the CLI with the keystone (v1) strategy enabled via + command line switches. + """ + substitutions = (self.api_port, self.auth_port, 'keystone', + 'pattieblack', 'secrete') + cmd = ("bin/glance --port=%d --auth_url=http://localhost:%d/v1.0 " + "--auth_strategy=%s --username=%s --password=%s " + " add name=MyImage" % substitutions) + self._do_test_glance_cli(cmd) + + @skip_if_disabled + def test_glance_cli_keystone_strategy_environment(self): + """ + Test the CLI with the keystone strategy enabled via + environment variables. + """ + os.environ['OS_AUTH_URL'] = 'http://localhost:%d/v1.0' % self.auth_port + os.environ['OS_AUTH_STRATEGY'] = 'keystone' + os.environ['OS_AUTH_USER'] = 'pattieblack' + os.environ['OS_AUTH_KEY'] = 'secrete' + cmd = "bin/glance --port=%d add name=MyImage" % self.api_port + self._do_test_glance_cli(cmd)