Add a privileged user for OpenStack services
Currently Cinder makes all requests to other services (Nova, Swift, etc.) with current user context. Sometimes Cinder needs privileged rights for external queries (e.g. asking Nova where an instance is hosted); there is no way to do it yet. This patch adds to ability to configure an account with special rights in the configuration ('os_privileged_user_name', 'os_privileged_user_password' and 'os_privileged_user_tenant' options). Then, requests that need special permissions can be achieved by creating a client(privileged_user=True). Note: This user does not necessarily need to have an admin role associated with it. For instance, policies can be changed to allow a specific user (without any roles) to perform special actions. DocImpact: New configuration options to set a privileged user account Change-Id: I61d8a6de1c5db5ee2ecce124997f9b6447b04e47
This commit is contained in:
parent
17c2f6857b
commit
04003d7c51
|
@ -203,6 +203,20 @@ global_opts = [
|
||||||
help='The full class name of the volume replication API class'),
|
help='The full class name of the volume replication API class'),
|
||||||
cfg.StrOpt('consistencygroup_api_class',
|
cfg.StrOpt('consistencygroup_api_class',
|
||||||
default='cinder.consistencygroup.api.API',
|
default='cinder.consistencygroup.api.API',
|
||||||
help='The full class name of the consistencygroup API class'), ]
|
help='The full class name of the consistencygroup API class'),
|
||||||
|
cfg.StrOpt('os_privileged_user_name',
|
||||||
|
default=None,
|
||||||
|
help='OpenStack privileged account username. Used for requests '
|
||||||
|
'to other services (such as Nova) that require an account '
|
||||||
|
'with special rights.'),
|
||||||
|
cfg.StrOpt('os_privileged_user_password',
|
||||||
|
default=None,
|
||||||
|
help='Password associated with the OpenStack privileged '
|
||||||
|
'account.'),
|
||||||
|
cfg.StrOpt('os_privileged_user_tenant',
|
||||||
|
default=None,
|
||||||
|
help='Tenant name associated with the OpenStack privileged '
|
||||||
|
'account.'),
|
||||||
|
]
|
||||||
|
|
||||||
CONF.register_opts(global_opts)
|
CONF.register_opts(global_opts)
|
||||||
|
|
|
@ -22,6 +22,7 @@ from novaclient.v1_1 import client as nova_client
|
||||||
from novaclient.v1_1.contrib import assisted_volume_snapshots
|
from novaclient.v1_1.contrib import assisted_volume_snapshots
|
||||||
from oslo.config import cfg
|
from oslo.config import cfg
|
||||||
|
|
||||||
|
from cinder import context as ctx
|
||||||
from cinder.db import base
|
from cinder.db import base
|
||||||
from cinder.openstack.common import log as logging
|
from cinder.openstack.common import log as logging
|
||||||
|
|
||||||
|
@ -60,7 +61,15 @@ CONF.register_opts(nova_opts)
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def novaclient(context, admin=False):
|
def novaclient(context, admin_endpoint=False, privileged_user=False):
|
||||||
|
"""Returns a Nova client
|
||||||
|
|
||||||
|
@param admin_endpoint: If True, use the admin endpoint template from
|
||||||
|
configuration ('nova_endpoint_admin_template' and 'nova_catalog_info')
|
||||||
|
@param privileged_user: If True, use the account from configuration
|
||||||
|
(requires 'os_privileged_user_name', 'os_privileged_user_password' and
|
||||||
|
'os_privileged_user_tenant' to be set)
|
||||||
|
"""
|
||||||
# FIXME: the novaclient ServiceCatalog object is mis-named.
|
# FIXME: the novaclient ServiceCatalog object is mis-named.
|
||||||
# It actually contains the entire access blob.
|
# It actually contains the entire access blob.
|
||||||
# Only needed parts of the service catalog are passed in, see
|
# Only needed parts of the service catalog are passed in, see
|
||||||
|
@ -73,43 +82,58 @@ def novaclient(context, admin=False):
|
||||||
nova_endpoint_template = CONF.nova_endpoint_template
|
nova_endpoint_template = CONF.nova_endpoint_template
|
||||||
nova_catalog_info = CONF.nova_catalog_info
|
nova_catalog_info = CONF.nova_catalog_info
|
||||||
|
|
||||||
if admin:
|
if admin_endpoint:
|
||||||
nova_endpoint_template = CONF.nova_endpoint_admin_template
|
nova_endpoint_template = CONF.nova_endpoint_admin_template
|
||||||
nova_catalog_info = CONF.nova_catalog_admin_info
|
nova_catalog_info = CONF.nova_catalog_admin_info
|
||||||
|
|
||||||
if nova_endpoint_template:
|
if privileged_user and CONF.os_privileged_user_name:
|
||||||
url = nova_endpoint_template % context.to_dict()
|
context = ctx.RequestContext(
|
||||||
else:
|
CONF.os_privileged_user_name, None,
|
||||||
info = nova_catalog_info
|
auth_token=CONF.os_privileged_user_password,
|
||||||
service_type, service_name, endpoint_type = info.split(':')
|
project_name=CONF.os_privileged_user_tenant,
|
||||||
# extract the region if set in configuration
|
service_catalog=context.service_catalog)
|
||||||
if CONF.os_region_name:
|
|
||||||
attr = 'region'
|
|
||||||
filter_value = CONF.os_region_name
|
|
||||||
else:
|
|
||||||
attr = None
|
|
||||||
filter_value = None
|
|
||||||
url = sc.url_for(attr=attr,
|
|
||||||
filter_value=filter_value,
|
|
||||||
service_type=service_type,
|
|
||||||
service_name=service_name,
|
|
||||||
endpoint_type=endpoint_type)
|
|
||||||
|
|
||||||
LOG.debug('Novaclient connection created using URL: %s' % url)
|
# The admin user needs to authenticate before querying Nova
|
||||||
|
url = sc.url_for(service_type='identity')
|
||||||
|
|
||||||
|
LOG.debug('Creating a Nova client using "%s" user' %
|
||||||
|
CONF.os_privileged_user_name)
|
||||||
|
else:
|
||||||
|
if nova_endpoint_template:
|
||||||
|
url = nova_endpoint_template % context.to_dict()
|
||||||
|
else:
|
||||||
|
info = nova_catalog_info
|
||||||
|
service_type, service_name, endpoint_type = info.split(':')
|
||||||
|
# extract the region if set in configuration
|
||||||
|
if CONF.os_region_name:
|
||||||
|
attr = 'region'
|
||||||
|
filter_value = CONF.os_region_name
|
||||||
|
else:
|
||||||
|
attr = None
|
||||||
|
filter_value = None
|
||||||
|
url = sc.url_for(attr=attr,
|
||||||
|
filter_value=filter_value,
|
||||||
|
service_type=service_type,
|
||||||
|
service_name=service_name,
|
||||||
|
endpoint_type=endpoint_type)
|
||||||
|
|
||||||
|
LOG.debug('Nova client connection created using URL: %s' % url)
|
||||||
|
|
||||||
extensions = [assisted_volume_snapshots]
|
extensions = [assisted_volume_snapshots]
|
||||||
|
|
||||||
c = nova_client.Client(context.user_id,
|
c = nova_client.Client(context.user_id,
|
||||||
context.auth_token,
|
context.auth_token,
|
||||||
context.project_id,
|
context.project_name,
|
||||||
auth_url=url,
|
auth_url=url,
|
||||||
insecure=CONF.nova_api_insecure,
|
insecure=CONF.nova_api_insecure,
|
||||||
cacert=CONF.nova_ca_certificates_file,
|
cacert=CONF.nova_ca_certificates_file,
|
||||||
extensions=extensions)
|
extensions=extensions)
|
||||||
# noauth extracts user_id:project_id from auth_token
|
|
||||||
c.client.auth_token = context.auth_token or '%s:%s' % (context.user_id,
|
if not privileged_user:
|
||||||
context.project_id)
|
# noauth extracts user_id:project_id from auth_token
|
||||||
c.client.management_url = url
|
c.client.auth_token = (context.auth_token or '%s:%s'
|
||||||
|
% (context.user_id, context.project_id))
|
||||||
|
c.client.management_url = url
|
||||||
return c
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
@ -123,14 +147,14 @@ class API(base.Base):
|
||||||
new_volume_id)
|
new_volume_id)
|
||||||
|
|
||||||
def create_volume_snapshot(self, context, volume_id, create_info):
|
def create_volume_snapshot(self, context, volume_id, create_info):
|
||||||
nova = novaclient(context, admin=True)
|
nova = novaclient(context, admin_endpoint=True)
|
||||||
|
|
||||||
nova.assisted_volume_snapshots.create(
|
nova.assisted_volume_snapshots.create(
|
||||||
volume_id,
|
volume_id,
|
||||||
create_info=create_info)
|
create_info=create_info)
|
||||||
|
|
||||||
def delete_volume_snapshot(self, context, snapshot_id, delete_info):
|
def delete_volume_snapshot(self, context, snapshot_id, delete_info):
|
||||||
nova = novaclient(context, admin=True)
|
nova = novaclient(context, admin_endpoint=True)
|
||||||
|
|
||||||
nova.assisted_volume_snapshots.delete(
|
nova.assisted_volume_snapshots.delete(
|
||||||
snapshot_id,
|
snapshot_id,
|
||||||
|
|
|
@ -84,8 +84,8 @@ class RequestContext(context.RequestContext):
|
||||||
if service_catalog:
|
if service_catalog:
|
||||||
# Only include required parts of service_catalog
|
# Only include required parts of service_catalog
|
||||||
self.service_catalog = [s for s in service_catalog
|
self.service_catalog = [s for s in service_catalog
|
||||||
if s.get('type') in ('compute',
|
if s.get('type') in
|
||||||
'object-store')]
|
('identity', 'compute', 'object-store')]
|
||||||
else:
|
else:
|
||||||
# if list is empty or none
|
# if list is empty or none
|
||||||
self.service_catalog = []
|
self.service_catalog = []
|
||||||
|
|
|
@ -15,12 +15,60 @@
|
||||||
import contextlib
|
import contextlib
|
||||||
|
|
||||||
import mock
|
import mock
|
||||||
|
from novaclient.v1_1.contrib import assisted_volume_snapshots
|
||||||
|
|
||||||
from cinder.compute import nova
|
from cinder.compute import nova
|
||||||
from cinder import context
|
from cinder import context
|
||||||
from cinder import test
|
from cinder import test
|
||||||
|
|
||||||
|
|
||||||
|
class NovaClientTestCase(test.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(NovaClientTestCase, self).setUp()
|
||||||
|
|
||||||
|
self.ctx = context.RequestContext('regularuser', 'e3f0833dc08b4cea',
|
||||||
|
auth_token='token', is_admin=False)
|
||||||
|
self.ctx.service_catalog = \
|
||||||
|
[{'type': 'compute', 'name': 'nova', 'endpoints':
|
||||||
|
[{'publicURL': 'http://novahost:8774/v2/e3f0833dc08b4cea'}]},
|
||||||
|
{'type': 'identity', 'name': 'keystone', 'endpoints':
|
||||||
|
[{'publicURL': 'http://keystonehost:5000/v2.0'}]}]
|
||||||
|
|
||||||
|
self.override_config('nova_endpoint_template',
|
||||||
|
'http://novahost:8774/v2/%(project_id)s')
|
||||||
|
self.override_config('nova_endpoint_admin_template',
|
||||||
|
'http://novaadmhost:4778/v2/%(project_id)s')
|
||||||
|
self.override_config('os_privileged_user_name', 'adminuser')
|
||||||
|
self.override_config('os_privileged_user_password', 'strongpassword')
|
||||||
|
|
||||||
|
@mock.patch('novaclient.v1_1.client.Client')
|
||||||
|
def test_nova_client_regular(self, p_client):
|
||||||
|
nova.novaclient(self.ctx)
|
||||||
|
p_client.assert_called_once_with(
|
||||||
|
'regularuser', 'token', None,
|
||||||
|
auth_url='http://novahost:8774/v2/e3f0833dc08b4cea',
|
||||||
|
insecure=False, cacert=None,
|
||||||
|
extensions=[assisted_volume_snapshots])
|
||||||
|
|
||||||
|
@mock.patch('novaclient.v1_1.client.Client')
|
||||||
|
def test_nova_client_admin_endpoint(self, p_client):
|
||||||
|
nova.novaclient(self.ctx, admin_endpoint=True)
|
||||||
|
p_client.assert_called_once_with(
|
||||||
|
'regularuser', 'token', None,
|
||||||
|
auth_url='http://novaadmhost:4778/v2/e3f0833dc08b4cea',
|
||||||
|
insecure=False, cacert=None,
|
||||||
|
extensions=[assisted_volume_snapshots])
|
||||||
|
|
||||||
|
@mock.patch('novaclient.v1_1.client.Client')
|
||||||
|
def test_nova_client_privileged_user(self, p_client):
|
||||||
|
nova.novaclient(self.ctx, privileged_user=True)
|
||||||
|
p_client.assert_called_once_with(
|
||||||
|
'adminuser', 'strongpassword', None,
|
||||||
|
auth_url='http://keystonehost:5000/v2.0',
|
||||||
|
insecure=False, cacert=None,
|
||||||
|
extensions=[assisted_volume_snapshots])
|
||||||
|
|
||||||
|
|
||||||
class FakeNovaClient(object):
|
class FakeNovaClient(object):
|
||||||
class Volumes(object):
|
class Volumes(object):
|
||||||
def __getattr__(self, item):
|
def __getattr__(self, item):
|
||||||
|
|
|
@ -80,7 +80,7 @@ class ContextTestCase(test.TestCase):
|
||||||
object_catalog = [{u'name': u'swift', u'type': u'object-store'}]
|
object_catalog = [{u'name': u'swift', u'type': u'object-store'}]
|
||||||
ctxt = context.RequestContext('111', '222',
|
ctxt = context.RequestContext('111', '222',
|
||||||
service_catalog=service_catalog)
|
service_catalog=service_catalog)
|
||||||
self.assertEqual(len(ctxt.service_catalog), 2)
|
self.assertEqual(len(ctxt.service_catalog), 3)
|
||||||
return_compute = [v for v in ctxt.service_catalog if
|
return_compute = [v for v in ctxt.service_catalog if
|
||||||
v['type'] == u'compute']
|
v['type'] == u'compute']
|
||||||
return_object = [v for v in ctxt.service_catalog if
|
return_object = [v for v in ctxt.service_catalog if
|
||||||
|
|
Loading…
Reference in New Issue