Scrubber to communicate with trustedauth registry
Glance scrubber needs admin context when attempting to talk to registry to list, update and delete images. However, when registry is deployed in trusted-auth mode, appropriate admin role is required to gain admin context on registry. Closes-bug: #1439666 Change-Id: I5ff68ef8f30e73642889f8e4cde7ba06628cb0e5
This commit is contained in:
parent
1225ab48e5
commit
dcbf54672c
|
@ -131,8 +131,8 @@ def get_registry_client(cxt):
|
||||||
|
|
||||||
if CONF.send_identity_headers:
|
if CONF.send_identity_headers:
|
||||||
identity_headers = {
|
identity_headers = {
|
||||||
'X-User-Id': cxt.user,
|
'X-User-Id': cxt.user or '',
|
||||||
'X-Tenant-Id': cxt.tenant,
|
'X-Tenant-Id': cxt.tenant or '',
|
||||||
'X-Roles': ','.join(cxt.roles),
|
'X-Roles': ','.join(cxt.roles),
|
||||||
'X-Identity-Status': 'Confirmed',
|
'X-Identity-Status': 'Confirmed',
|
||||||
'X-Service-Catalog': jsonutils.dumps(cxt.service_catalog),
|
'X-Service-Catalog': jsonutils.dumps(cxt.service_catalog),
|
||||||
|
|
|
@ -42,6 +42,19 @@ scrubber_opts = [
|
||||||
'performing a delete.')),
|
'performing a delete.')),
|
||||||
cfg.BoolOpt('delayed_delete', default=False,
|
cfg.BoolOpt('delayed_delete', default=False,
|
||||||
help=_('Turn on/off delayed delete.')),
|
help=_('Turn on/off delayed delete.')),
|
||||||
|
cfg.StrOpt('admin_role', default='admin',
|
||||||
|
help=_('Role used to identify an authenticated user as '
|
||||||
|
'administrator.')),
|
||||||
|
cfg.BoolOpt('send_identity_headers', default=False,
|
||||||
|
help=_("Whether to pass through headers containing user "
|
||||||
|
"and tenant information when making requests to "
|
||||||
|
"the registry. This allows the registry to use the "
|
||||||
|
"context middleware without keystonemiddleware's "
|
||||||
|
"auth_token middleware, removing calls to the keystone "
|
||||||
|
"auth service. It is recommended that when using this "
|
||||||
|
"option, secure communication between glance api and "
|
||||||
|
"glance registry is ensured by means other than "
|
||||||
|
"auth_token middleware.")),
|
||||||
]
|
]
|
||||||
|
|
||||||
scrubber_cmd_opts = [
|
scrubber_cmd_opts = [
|
||||||
|
@ -73,11 +86,23 @@ class ScrubDBQueue(object):
|
||||||
self.metadata_encryption_key = CONF.metadata_encryption_key
|
self.metadata_encryption_key = CONF.metadata_encryption_key
|
||||||
registry.configure_registry_client()
|
registry.configure_registry_client()
|
||||||
registry.configure_registry_admin_creds()
|
registry.configure_registry_admin_creds()
|
||||||
self.registry = registry.get_registry_client(context.RequestContext())
|
admin_user = CONF.admin_user
|
||||||
admin_tenant_name = CONF.admin_tenant_name
|
admin_tenant = CONF.admin_tenant_name
|
||||||
|
|
||||||
|
if CONF.send_identity_headers:
|
||||||
|
# When registry is operating in trusted-auth mode
|
||||||
|
roles = [CONF.admin_role]
|
||||||
|
self.admin_context = context.RequestContext(user=admin_user,
|
||||||
|
tenant=admin_tenant,
|
||||||
|
auth_token=None,
|
||||||
|
roles=roles)
|
||||||
|
self.registry = registry.get_registry_client(self.admin_context)
|
||||||
|
else:
|
||||||
|
ctxt = context.RequestContext()
|
||||||
|
self.registry = registry.get_registry_client(ctxt)
|
||||||
admin_token = self.registry.auth_token
|
admin_token = self.registry.auth_token
|
||||||
self.admin_context = context.RequestContext(user=CONF.admin_user,
|
self.admin_context = context.RequestContext(user=admin_user,
|
||||||
tenant=admin_tenant_name,
|
tenant=admin_tenant,
|
||||||
auth_token=admin_token)
|
auth_token=admin_token)
|
||||||
|
|
||||||
def add_location(self, image_id, location):
|
def add_location(self, image_id, location):
|
||||||
|
@ -215,13 +240,25 @@ class Scrubber(object):
|
||||||
|
|
||||||
registry.configure_registry_client()
|
registry.configure_registry_client()
|
||||||
registry.configure_registry_admin_creds()
|
registry.configure_registry_admin_creds()
|
||||||
self.registry = registry.get_registry_client(context.RequestContext())
|
|
||||||
|
|
||||||
# Here we create a request context with credentials to support
|
# Here we create a request context with credentials to support
|
||||||
# delayed delete when using multi-tenant backend storage
|
# delayed delete when using multi-tenant backend storage
|
||||||
|
admin_user = CONF.admin_user
|
||||||
admin_tenant = CONF.admin_tenant_name
|
admin_tenant = CONF.admin_tenant_name
|
||||||
|
|
||||||
|
if CONF.send_identity_headers:
|
||||||
|
# When registry is operating in trusted-auth mode
|
||||||
|
roles = [CONF.admin_role]
|
||||||
|
self.admin_context = context.RequestContext(user=admin_user,
|
||||||
|
tenant=admin_tenant,
|
||||||
|
auth_token=None,
|
||||||
|
roles=roles)
|
||||||
|
self.registry = registry.get_registry_client(self.admin_context)
|
||||||
|
else:
|
||||||
|
ctxt = context.RequestContext()
|
||||||
|
self.registry = registry.get_registry_client(ctxt)
|
||||||
auth_token = self.registry.auth_token
|
auth_token = self.registry.auth_token
|
||||||
self.admin_context = context.RequestContext(user=CONF.admin_user,
|
self.admin_context = context.RequestContext(user=admin_user,
|
||||||
tenant=admin_tenant,
|
tenant=admin_tenant,
|
||||||
auth_token=auth_token)
|
auth_token=auth_token)
|
||||||
|
|
||||||
|
|
|
@ -317,6 +317,8 @@ class ApiServer(Server):
|
||||||
self.location_strategy = 'location_order'
|
self.location_strategy = 'location_order'
|
||||||
self.store_type_location_strategy_preference = ""
|
self.store_type_location_strategy_preference = ""
|
||||||
|
|
||||||
|
self.send_identity_headers = False
|
||||||
|
|
||||||
self.conf_base = """[DEFAULT]
|
self.conf_base = """[DEFAULT]
|
||||||
verbose = %(verbose)s
|
verbose = %(verbose)s
|
||||||
debug = %(debug)s
|
debug = %(debug)s
|
||||||
|
@ -336,6 +338,7 @@ delayed_delete = %(delayed_delete)s
|
||||||
owner_is_tenant = %(owner_is_tenant)s
|
owner_is_tenant = %(owner_is_tenant)s
|
||||||
workers = %(workers)s
|
workers = %(workers)s
|
||||||
scrub_time = %(scrub_time)s
|
scrub_time = %(scrub_time)s
|
||||||
|
send_identity_headers = %(send_identity_headers)s
|
||||||
image_cache_dir = %(image_cache_dir)s
|
image_cache_dir = %(image_cache_dir)s
|
||||||
image_cache_driver = %(image_cache_driver)s
|
image_cache_driver = %(image_cache_driver)s
|
||||||
data_api = %(data_api)s
|
data_api = %(data_api)s
|
||||||
|
@ -541,6 +544,9 @@ class ScrubberDaemon(Server):
|
||||||
self.policy_file = policy_file
|
self.policy_file = policy_file
|
||||||
self.policy_default_rule = 'default'
|
self.policy_default_rule = 'default'
|
||||||
|
|
||||||
|
self.send_identity_headers = False
|
||||||
|
self.admin_role = 'admin'
|
||||||
|
|
||||||
self.conf_base = """[DEFAULT]
|
self.conf_base = """[DEFAULT]
|
||||||
verbose = %(verbose)s
|
verbose = %(verbose)s
|
||||||
debug = %(debug)s
|
debug = %(debug)s
|
||||||
|
@ -555,6 +561,8 @@ metadata_encryption_key = %(metadata_encryption_key)s
|
||||||
lock_path = %(lock_path)s
|
lock_path = %(lock_path)s
|
||||||
sql_connection = %(sql_connection)s
|
sql_connection = %(sql_connection)s
|
||||||
sql_idle_timeout = 3600
|
sql_idle_timeout = 3600
|
||||||
|
send_identity_headers = %(send_identity_headers)s
|
||||||
|
admin_role = %(admin_role)s
|
||||||
[oslo_policy]
|
[oslo_policy]
|
||||||
policy_file = %(policy_file)s
|
policy_file = %(policy_file)s
|
||||||
policy_default_rule = %(policy_default_rule)s
|
policy_default_rule = %(policy_default_rule)s
|
||||||
|
|
|
@ -76,6 +76,55 @@ class TestScrubber(functional.FunctionalTest):
|
||||||
|
|
||||||
self.stop_servers()
|
self.stop_servers()
|
||||||
|
|
||||||
|
def test_delayed_delete_with_trustedauth_registry(self):
|
||||||
|
"""
|
||||||
|
test that images don't get deleted immediately and that the scrubber
|
||||||
|
scrubs them when registry is operating in trustedauth mode
|
||||||
|
"""
|
||||||
|
self.cleanup()
|
||||||
|
self.api_server.deployment_flavor = 'noauth'
|
||||||
|
self.registry_server.deployment_flavor = 'trusted-auth'
|
||||||
|
self.start_servers(delayed_delete=True, daemon=True,
|
||||||
|
metadata_encryption_key='',
|
||||||
|
send_identity_headers=True)
|
||||||
|
base_headers = {
|
||||||
|
'X-Identity-Status': 'Confirmed',
|
||||||
|
'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96',
|
||||||
|
'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e',
|
||||||
|
'X-Tenant-Id': 'deae8923-075d-4287-924b-840fb2644874',
|
||||||
|
'X-Roles': 'admin',
|
||||||
|
}
|
||||||
|
headers = {
|
||||||
|
'x-image-meta-name': 'test_image',
|
||||||
|
'x-image-meta-is_public': 'true',
|
||||||
|
'x-image-meta-disk_format': 'raw',
|
||||||
|
'x-image-meta-container_format': 'ovf',
|
||||||
|
'content-type': 'application/octet-stream',
|
||||||
|
}
|
||||||
|
headers.update(base_headers)
|
||||||
|
path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port)
|
||||||
|
http = httplib2.Http()
|
||||||
|
response, content = http.request(path, 'POST', body='XXX',
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual(201, response.status)
|
||||||
|
image = jsonutils.loads(content)['image']
|
||||||
|
self.assertEqual('active', image['status'])
|
||||||
|
image_id = image['id']
|
||||||
|
|
||||||
|
path = "http://%s:%d/v1/images/%s" % ("127.0.0.1", self.api_port,
|
||||||
|
image_id)
|
||||||
|
http = httplib2.Http()
|
||||||
|
response, content = http.request(path, 'DELETE', headers=base_headers)
|
||||||
|
self.assertEqual(200, response.status)
|
||||||
|
|
||||||
|
response, content = http.request(path, 'HEAD', headers=base_headers)
|
||||||
|
self.assertEqual(200, response.status)
|
||||||
|
self.assertEqual('pending_delete', response['x-image-meta-status'])
|
||||||
|
|
||||||
|
self.wait_for_scrub(path, headers=base_headers)
|
||||||
|
|
||||||
|
self.stop_servers()
|
||||||
|
|
||||||
def test_scrubber_app(self):
|
def test_scrubber_app(self):
|
||||||
"""
|
"""
|
||||||
test that the glance-scrubber script runs successfully when not in
|
test that the glance-scrubber script runs successfully when not in
|
||||||
|
@ -114,6 +163,65 @@ class TestScrubber(functional.FunctionalTest):
|
||||||
|
|
||||||
self.stop_servers()
|
self.stop_servers()
|
||||||
|
|
||||||
|
def test_scrubber_app_with_trustedauth_registry(self):
|
||||||
|
"""
|
||||||
|
test that the glance-scrubber script runs successfully when not in
|
||||||
|
daemon mode and with a registry that operates in trustedauth mode
|
||||||
|
"""
|
||||||
|
self.cleanup()
|
||||||
|
self.api_server.deployment_flavor = 'noauth'
|
||||||
|
self.registry_server.deployment_flavor = 'trusted-auth'
|
||||||
|
self.start_servers(delayed_delete=True, daemon=False,
|
||||||
|
metadata_encryption_key='',
|
||||||
|
send_identity_headers=True)
|
||||||
|
base_headers = {
|
||||||
|
'X-Identity-Status': 'Confirmed',
|
||||||
|
'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96',
|
||||||
|
'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e',
|
||||||
|
'X-Tenant-Id': 'deae8923-075d-4287-924b-840fb2644874',
|
||||||
|
'X-Roles': 'admin',
|
||||||
|
}
|
||||||
|
headers = {
|
||||||
|
'x-image-meta-name': 'test_image',
|
||||||
|
'x-image-meta-is_public': 'true',
|
||||||
|
'x-image-meta-disk_format': 'raw',
|
||||||
|
'x-image-meta-container_format': 'ovf',
|
||||||
|
'content-type': 'application/octet-stream',
|
||||||
|
}
|
||||||
|
headers.update(base_headers)
|
||||||
|
path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port)
|
||||||
|
http = httplib2.Http()
|
||||||
|
response, content = http.request(path, 'POST', body='XXX',
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual(201, response.status)
|
||||||
|
image = jsonutils.loads(content)['image']
|
||||||
|
self.assertEqual('active', image['status'])
|
||||||
|
image_id = image['id']
|
||||||
|
|
||||||
|
path = "http://%s:%d/v1/images/%s" % ("127.0.0.1", self.api_port,
|
||||||
|
image_id)
|
||||||
|
http = httplib2.Http()
|
||||||
|
response, content = http.request(path, 'DELETE', headers=base_headers)
|
||||||
|
self.assertEqual(200, response.status)
|
||||||
|
|
||||||
|
response, content = http.request(path, 'HEAD', headers=base_headers)
|
||||||
|
self.assertEqual(200, response.status)
|
||||||
|
self.assertEqual('pending_delete', response['x-image-meta-status'])
|
||||||
|
|
||||||
|
# wait for the scrub time on the image to pass
|
||||||
|
time.sleep(self.api_server.scrub_time)
|
||||||
|
|
||||||
|
# scrub images and make sure they get deleted
|
||||||
|
exe_cmd = "%s -m glance.cmd.scrubber" % sys.executable
|
||||||
|
cmd = ("%s --config-file %s" %
|
||||||
|
(exe_cmd, self.scrubber_daemon.conf_file_name))
|
||||||
|
exitcode, out, err = execute(cmd, raise_error=False)
|
||||||
|
self.assertEqual(0, exitcode)
|
||||||
|
|
||||||
|
self.wait_for_scrub(path, headers=base_headers)
|
||||||
|
|
||||||
|
self.stop_servers()
|
||||||
|
|
||||||
def test_scrubber_delete_handles_exception(self):
|
def test_scrubber_delete_handles_exception(self):
|
||||||
"""
|
"""
|
||||||
Test that the scrubber handles the case where an
|
Test that the scrubber handles the case where an
|
||||||
|
@ -165,7 +273,7 @@ class TestScrubber(functional.FunctionalTest):
|
||||||
|
|
||||||
self.stop_servers()
|
self.stop_servers()
|
||||||
|
|
||||||
def wait_for_scrub(self, path):
|
def wait_for_scrub(self, path, headers=None):
|
||||||
"""
|
"""
|
||||||
NOTE(jkoelker) The build servers sometimes take longer than 15 seconds
|
NOTE(jkoelker) The build servers sometimes take longer than 15 seconds
|
||||||
to scrub. Give it up to 5 min, checking checking every 15 seconds.
|
to scrub. Give it up to 5 min, checking checking every 15 seconds.
|
||||||
|
@ -177,7 +285,7 @@ class TestScrubber(functional.FunctionalTest):
|
||||||
for _ in range(wait_for / check_every):
|
for _ in range(wait_for / check_every):
|
||||||
time.sleep(check_every)
|
time.sleep(check_every)
|
||||||
|
|
||||||
response, content = http.request(path, 'HEAD')
|
response, content = http.request(path, 'HEAD', headers=headers)
|
||||||
if (response['x-image-meta-status'] == 'deleted' and
|
if (response['x-image-meta-status'] == 'deleted' and
|
||||||
response['x-image-meta-deleted'] == 'True'):
|
response['x-image-meta-deleted'] == 'True'):
|
||||||
break
|
break
|
||||||
|
|
|
@ -855,8 +855,8 @@ class TestRegistryV1ClientApi(base.IsolatedUnitTest):
|
||||||
def test_get_registry_client_with_identity_headers(self):
|
def test_get_registry_client_with_identity_headers(self):
|
||||||
self.config(send_identity_headers=True)
|
self.config(send_identity_headers=True)
|
||||||
expected_identity_headers = {
|
expected_identity_headers = {
|
||||||
'X-User-Id': self.context.user,
|
'X-User-Id': '',
|
||||||
'X-Tenant-Id': self.context.tenant,
|
'X-Tenant-Id': '',
|
||||||
'X-Roles': ','.join(self.context.roles),
|
'X-Roles': ','.join(self.context.roles),
|
||||||
'X-Identity-Status': 'Confirmed',
|
'X-Identity-Status': 'Confirmed',
|
||||||
'X-Service-Catalog': 'null',
|
'X-Service-Catalog': 'null',
|
||||||
|
|
Loading…
Reference in New Issue