Add support for cephx pool grouping and permissions

Sync charmhelpers and add configuration option to allow access
to ceph pools to be limited based on grouping.

Cinder requires rwx access to pools associated with volumes,
images and vms (to support rbd snapshots).

Change-Id: If1734c430108e193df0a58dc4c06ebe2b79990e3
Partial-Bug: 1424771
This commit is contained in:
Chris MacNaughton 2017-02-10 17:04:37 -05:00 committed by Chris MacNaughton
parent 90965ae671
commit d679c66776
9 changed files with 335 additions and 69 deletions

View File

@ -375,3 +375,9 @@ options:
description: |
Time period for which to generate volume usages. The options are hour,
day, month, or year.
restrict-ceph-pools:
default: False
type: boolean
description: |
Cinder can optionally restrict the key it asks Ceph for to only be able
to access the pools it needs.

View File

@ -426,7 +426,7 @@ def ns_query(address):
try:
answers = dns.resolver.query(address, rtype)
except dns.resolver.NXDOMAIN as e:
except dns.resolver.NXDOMAIN:
return None
if answers:

View File

@ -20,6 +20,7 @@ import re
import six
import time
import urllib
import urlparse
import cinderclient.v1.client as cinder_client
import glanceclient.v1.client as glance_client
@ -311,6 +312,37 @@ class OpenStackAmuletUtils(AmuletUtils):
ept = "http://{}:5000/v2.0".format(keystone_ip.strip().decode('utf-8'))
return cinder_client.Client(username, password, tenant, ept)
def authenticate_keystone(self, keystone_ip, username, password,
api_version=False, admin_port=False,
user_domain_name=None, domain_name=None,
project_domain_name=None, project_name=None):
"""Authenticate with Keystone"""
self.log.debug('Authenticating with keystone...')
port = 5000
if admin_port:
port = 35357
base_ep = "http://{}:{}".format(keystone_ip.strip().decode('utf-8'),
port)
if not api_version or api_version == 2:
ep = base_ep + "/v2.0"
return keystone_client.Client(username=username, password=password,
tenant_name=project_name,
auth_url=ep)
else:
ep = base_ep + "/v3"
auth = keystone_id_v3.Password(
user_domain_name=user_domain_name,
username=username,
password=password,
domain_name=domain_name,
project_domain_name=project_domain_name,
project_name=project_name,
auth_url=ep
)
return keystone_client_v3.Client(
session=keystone_session.Session(auth=auth)
)
def authenticate_keystone_admin(self, keystone_sentry, user, password,
tenant=None, api_version=None,
keystone_ip=None):
@ -319,30 +351,28 @@ class OpenStackAmuletUtils(AmuletUtils):
if not keystone_ip:
keystone_ip = keystone_sentry.info['public-address']
base_ep = "http://{}:35357".format(keystone_ip.strip().decode('utf-8'))
if not api_version or api_version == 2:
ep = base_ep + "/v2.0"
return keystone_client.Client(username=user, password=password,
tenant_name=tenant, auth_url=ep)
else:
ep = base_ep + "/v3"
auth = keystone_id_v3.Password(
user_domain_name='admin_domain',
username=user,
password=password,
domain_name='admin_domain',
auth_url=ep,
)
sess = keystone_session.Session(auth=auth)
return keystone_client_v3.Client(session=sess)
user_domain_name = None
domain_name = None
if api_version == 3:
user_domain_name = 'admin_domain'
domain_name = user_domain_name
return self.authenticate_keystone(keystone_ip, user, password,
project_name=tenant,
api_version=api_version,
user_domain_name=user_domain_name,
domain_name=domain_name,
admin_port=True)
def authenticate_keystone_user(self, keystone, user, password, tenant):
"""Authenticates a regular user with the keystone public endpoint."""
self.log.debug('Authenticating keystone user ({})...'.format(user))
ep = keystone.service_catalog.url_for(service_type='identity',
endpoint_type='publicURL')
return keystone_client.Client(username=user, password=password,
tenant_name=tenant, auth_url=ep)
keystone_ip = urlparse.urlparse(ep).hostname
return self.authenticate_keystone(keystone_ip, user, password,
project_name=tenant)
def authenticate_glance_admin(self, keystone):
"""Authenticates admin user with glance."""

View File

@ -40,6 +40,7 @@ from subprocess import (
)
from charmhelpers.core.hookenv import (
config,
service_name,
local_unit,
relation_get,
relation_ids,
@ -1043,8 +1044,18 @@ class CephBrokerRq(object):
self.request_id = str(uuid.uuid1())
self.ops = []
def add_op_request_access_to_group(self, name, namespace=None,
permission=None):
"""
Adds the requested permissions to the current service's Ceph key,
allowing the key to access only the specified pools
"""
self.ops.append({'op': 'add-permissions-to-key', 'group': name,
'namespace': namespace, 'name': service_name(),
'group-permission': permission})
def add_op_create_pool(self, name, replica_count=3, pg_num=None,
weight=None):
weight=None, group=None, namespace=None):
"""Adds an operation to create a pool.
@param pg_num setting: optional setting. If not provided, this value
@ -1058,7 +1069,8 @@ class CephBrokerRq(object):
self.ops.append({'op': 'create-pool', 'name': name,
'replicas': replica_count, 'pg_num': pg_num,
'weight': weight})
'weight': weight, 'group': group,
'group-namespace': namespace})
def set_ops(self, ops):
"""Set request ops to provided value.

View File

@ -1035,3 +1035,34 @@ def network_get_primary_address(binding):
'''
cmd = ['network-get', '--primary-address', binding]
return subprocess.check_output(cmd).decode('UTF-8').strip()
def add_metric(*args, **kwargs):
"""Add metric values. Values may be expressed with keyword arguments. For
metric names containing dashes, these may be expressed as one or more
'key=value' positional arguments. May only be called from the collect-metrics
hook."""
_args = ['add-metric']
_kvpairs = []
_kvpairs.extend(args)
_kvpairs.extend(['{}={}'.format(k, v) for k, v in kwargs.items()])
_args.extend(sorted(_kvpairs))
try:
subprocess.check_call(_args)
return
except EnvironmentError as e:
if e.errno != errno.ENOENT:
raise
log_message = 'add-metric failed: {}'.format(' '.join(_kvpairs))
log(log_message, level='INFO')
def meter_status():
"""Get the meter status, if running in the meter-status-changed hook."""
return os.environ.get('JUJU_METER_STATUS')
def meter_info():
"""Get the meter status information, if running in the meter-status-changed
hook."""
return os.environ.get('JUJU_METER_INFO')

View File

@ -56,37 +56,136 @@ elif __platform__ == "centos":
UPDATEDB_PATH = '/etc/updatedb.conf'
def service_start(service_name):
"""Start a system service"""
return service('start', service_name)
def service_start(service_name, **kwargs):
"""Start a system service.
The specified service name is managed via the system level init system.
Some init systems (e.g. upstart) require that additional arguments be
provided in order to directly control service instances whereas other init
systems allow for addressing instances of a service directly by name (e.g.
systemd).
The kwargs allow for the additional parameters to be passed to underlying
init systems for those systems which require/allow for them. For example,
the ceph-osd upstart script requires the id parameter to be passed along
in order to identify which running daemon should be reloaded. The follow-
ing example stops the ceph-osd service for instance id=4:
service_stop('ceph-osd', id=4)
:param service_name: the name of the service to stop
:param **kwargs: additional parameters to pass to the init system when
managing services. These will be passed as key=value
parameters to the init system's commandline. kwargs
are ignored for systemd enabled systems.
"""
return service('start', service_name, **kwargs)
def service_stop(service_name):
"""Stop a system service"""
return service('stop', service_name)
def service_stop(service_name, **kwargs):
"""Stop a system service.
The specified service name is managed via the system level init system.
Some init systems (e.g. upstart) require that additional arguments be
provided in order to directly control service instances whereas other init
systems allow for addressing instances of a service directly by name (e.g.
systemd).
The kwargs allow for the additional parameters to be passed to underlying
init systems for those systems which require/allow for them. For example,
the ceph-osd upstart script requires the id parameter to be passed along
in order to identify which running daemon should be reloaded. The follow-
ing example stops the ceph-osd service for instance id=4:
service_stop('ceph-osd', id=4)
:param service_name: the name of the service to stop
:param **kwargs: additional parameters to pass to the init system when
managing services. These will be passed as key=value
parameters to the init system's commandline. kwargs
are ignored for systemd enabled systems.
"""
return service('stop', service_name, **kwargs)
def service_restart(service_name):
"""Restart a system service"""
def service_restart(service_name, **kwargs):
"""Restart a system service.
The specified service name is managed via the system level init system.
Some init systems (e.g. upstart) require that additional arguments be
provided in order to directly control service instances whereas other init
systems allow for addressing instances of a service directly by name (e.g.
systemd).
The kwargs allow for the additional parameters to be passed to underlying
init systems for those systems which require/allow for them. For example,
the ceph-osd upstart script requires the id parameter to be passed along
in order to identify which running daemon should be restarted. The follow-
ing example restarts the ceph-osd service for instance id=4:
service_restart('ceph-osd', id=4)
:param service_name: the name of the service to restart
:param **kwargs: additional parameters to pass to the init system when
managing services. These will be passed as key=value
parameters to the init system's commandline. kwargs
are ignored for init systems not allowing additional
parameters via the commandline (systemd).
"""
return service('restart', service_name)
def service_reload(service_name, restart_on_failure=False):
def service_reload(service_name, restart_on_failure=False, **kwargs):
"""Reload a system service, optionally falling back to restart if
reload fails"""
service_result = service('reload', service_name)
reload fails.
The specified service name is managed via the system level init system.
Some init systems (e.g. upstart) require that additional arguments be
provided in order to directly control service instances whereas other init
systems allow for addressing instances of a service directly by name (e.g.
systemd).
The kwargs allow for the additional parameters to be passed to underlying
init systems for those systems which require/allow for them. For example,
the ceph-osd upstart script requires the id parameter to be passed along
in order to identify which running daemon should be reloaded. The follow-
ing example restarts the ceph-osd service for instance id=4:
service_reload('ceph-osd', id=4)
:param service_name: the name of the service to reload
:param restart_on_failure: boolean indicating whether to fallback to a
restart if the reload fails.
:param **kwargs: additional parameters to pass to the init system when
managing services. These will be passed as key=value
parameters to the init system's commandline. kwargs
are ignored for init systems not allowing additional
parameters via the commandline (systemd).
"""
service_result = service('reload', service_name, **kwargs)
if not service_result and restart_on_failure:
service_result = service('restart', service_name)
service_result = service('restart', service_name, **kwargs)
return service_result
def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d",
**kwargs):
"""Pause a system service.
Stop it, and prevent it from starting again at boot."""
Stop it, and prevent it from starting again at boot.
:param service_name: the name of the service to pause
:param init_dir: path to the upstart init directory
:param initd_dir: path to the sysv init directory
:param **kwargs: additional parameters to pass to the init system when
managing services. These will be passed as key=value
parameters to the init system's commandline. kwargs
are ignored for init systems which do not support
key=value arguments via the commandline.
"""
stopped = True
if service_running(service_name):
stopped = service_stop(service_name)
if service_running(service_name, **kwargs):
stopped = service_stop(service_name, **kwargs)
upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
sysv_file = os.path.join(initd_dir, service_name)
if init_is_systemd():
@ -107,10 +206,19 @@ def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):
def service_resume(service_name, init_dir="/etc/init",
initd_dir="/etc/init.d"):
initd_dir="/etc/init.d", **kwargs):
"""Resume a system service.
Reenable starting again at boot. Start the service"""
Reenable starting again at boot. Start the service.
:param service_name: the name of the service to resume
:param init_dir: the path to the init dir
:param initd dir: the path to the initd dir
:param **kwargs: additional parameters to pass to the init system when
managing services. These will be passed as key=value
parameters to the init system's commandline. kwargs
are ignored for systemd enabled systems.
"""
upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
sysv_file = os.path.join(initd_dir, service_name)
if init_is_systemd():
@ -127,19 +235,28 @@ def service_resume(service_name, init_dir="/etc/init",
"Unable to detect {0} as SystemD, Upstart {1} or"
" SysV {2}".format(
service_name, upstart_file, sysv_file))
started = service_running(service_name, **kwargs)
started = service_running(service_name)
if not started:
started = service_start(service_name)
started = service_start(service_name, **kwargs)
return started
def service(action, service_name):
"""Control a system service"""
def service(action, service_name, **kwargs):
"""Control a system service.
:param action: the action to take on the service
:param service_name: the name of the service to perform th action on
:param **kwargs: additional params to be passed to the service command in
the form of key=value.
"""
if init_is_systemd():
cmd = ['systemctl', action, service_name]
else:
cmd = ['service', service_name, action]
for key, value in six.iteritems(kwargs):
parameter = '%s=%s' % (key, value)
cmd.append(parameter)
return subprocess.call(cmd) == 0
@ -147,15 +264,26 @@ _UPSTART_CONF = "/etc/init/{}.conf"
_INIT_D_CONF = "/etc/init.d/{}"
def service_running(service_name):
"""Determine whether a system service is running"""
def service_running(service_name, **kwargs):
"""Determine whether a system service is running.
:param service_name: the name of the service
:param **kwargs: additional args to pass to the service command. This is
used to pass additional key=value arguments to the
service command line for managing specific instance
units (e.g. service ceph-osd status id=2). The kwargs
are ignored in systemd services.
"""
if init_is_systemd():
return service('is-active', service_name)
else:
if os.path.exists(_UPSTART_CONF.format(service_name)):
try:
output = subprocess.check_output(
['status', service_name],
cmd = ['status', service_name]
for key, value in six.iteritems(kwargs):
parameter = '%s=%s' % (key, value)
cmd.append(parameter)
output = subprocess.check_output(cmd,
stderr=subprocess.STDOUT).decode('UTF-8')
except subprocess.CalledProcessError:
return False
@ -721,6 +849,20 @@ def lchownr(path, owner, group):
chownr(path, owner, group, follow_links=False)
def owner(path):
"""Returns a tuple containing the username & groupname owning the path.
:param str path: the string path to retrieve the ownership
:return tuple(str, str): A (username, groupname) tuple containing the
name of the user and group owning the path.
:raises OSError: if the specified path does not exist
"""
stat = os.stat(path)
username = pwd.getpwuid(stat.st_uid)[0]
groupname = grp.getgrgid(stat.st_gid)[0]
return username, groupname
def get_total_ram():
"""The total amount of system RAM in bytes.
@ -754,12 +896,12 @@ def is_container():
return os.path.exists(UPSTART_CONTAINER_TYPE)
def add_to_updatedb_prunepath(path):
with open(UPDATEDB_PATH, 'r+') as f_id:
def add_to_updatedb_prunepath(path, updatedb_path=UPDATEDB_PATH):
with open(updatedb_path, 'r+') as f_id:
updatedb_text = f_id.read()
output = updatedb(updatedb_text, path)
f_id.seek(0)
f_ids.write(output)
f_id.write(output)
f_id.truncate()

View File

@ -8,12 +8,18 @@ def get_platform():
will be returned (which is the name of the module).
This string is used to decide which platform module should be imported.
"""
# linux_distribution is deprecated and will be removed in Python 3.7
# Warings *not* disabled, as we certainly need to fix this.
tuple_platform = platform.linux_distribution()
current_platform = tuple_platform[0]
if "Ubuntu" in current_platform:
return "ubuntu"
elif "CentOS" in current_platform:
return "centos"
elif "debian" in current_platform:
# Stock Python does not detect Ubuntu and instead returns debian.
# Or at least it does in some build environments like Travis CI
return "ubuntu"
else:
raise RuntimeError("This module is not supported on {}."
.format(current_platform))

View File

@ -379,7 +379,16 @@ def get_ceph_request():
service = service_name()
rq = CephBrokerRq()
replicas = config('ceph-osd-replication-count')
rq.add_op_create_pool(name=service, replica_count=replicas)
rq.add_op_create_pool(name=service,
replica_count=replicas,
group="volumes")
if config('restrict-ceph-pools'):
rq.add_op_request_access_to_group(name="volumes",
permission='rwx')
rq.add_op_request_access_to_group(name="images",
permission='rwx')
rq.add_op_request_access_to_group(name="vms",
permission='rwx')
return rq

View File

@ -20,6 +20,7 @@ import re
import six
import time
import urllib
import urlparse
import cinderclient.v1.client as cinder_client
import glanceclient.v1.client as glance_client
@ -311,6 +312,37 @@ class OpenStackAmuletUtils(AmuletUtils):
ept = "http://{}:5000/v2.0".format(keystone_ip.strip().decode('utf-8'))
return cinder_client.Client(username, password, tenant, ept)
def authenticate_keystone(self, keystone_ip, username, password,
api_version=False, admin_port=False,
user_domain_name=None, domain_name=None,
project_domain_name=None, project_name=None):
"""Authenticate with Keystone"""
self.log.debug('Authenticating with keystone...')
port = 5000
if admin_port:
port = 35357
base_ep = "http://{}:{}".format(keystone_ip.strip().decode('utf-8'),
port)
if not api_version or api_version == 2:
ep = base_ep + "/v2.0"
return keystone_client.Client(username=username, password=password,
tenant_name=project_name,
auth_url=ep)
else:
ep = base_ep + "/v3"
auth = keystone_id_v3.Password(
user_domain_name=user_domain_name,
username=username,
password=password,
domain_name=domain_name,
project_domain_name=project_domain_name,
project_name=project_name,
auth_url=ep
)
return keystone_client_v3.Client(
session=keystone_session.Session(auth=auth)
)
def authenticate_keystone_admin(self, keystone_sentry, user, password,
tenant=None, api_version=None,
keystone_ip=None):
@ -319,30 +351,28 @@ class OpenStackAmuletUtils(AmuletUtils):
if not keystone_ip:
keystone_ip = keystone_sentry.info['public-address']
base_ep = "http://{}:35357".format(keystone_ip.strip().decode('utf-8'))
if not api_version or api_version == 2:
ep = base_ep + "/v2.0"
return keystone_client.Client(username=user, password=password,
tenant_name=tenant, auth_url=ep)
else:
ep = base_ep + "/v3"
auth = keystone_id_v3.Password(
user_domain_name='admin_domain',
username=user,
password=password,
domain_name='admin_domain',
auth_url=ep,
)
sess = keystone_session.Session(auth=auth)
return keystone_client_v3.Client(session=sess)
user_domain_name = None
domain_name = None
if api_version == 3:
user_domain_name = 'admin_domain'
domain_name = user_domain_name
return self.authenticate_keystone(keystone_ip, user, password,
project_name=tenant,
api_version=api_version,
user_domain_name=user_domain_name,
domain_name=domain_name,
admin_port=True)
def authenticate_keystone_user(self, keystone, user, password, tenant):
"""Authenticates a regular user with the keystone public endpoint."""
self.log.debug('Authenticating keystone user ({})...'.format(user))
ep = keystone.service_catalog.url_for(service_type='identity',
endpoint_type='publicURL')
return keystone_client.Client(username=user, password=password,
tenant_name=tenant, auth_url=ep)
keystone_ip = urlparse.urlparse(ep).hostname
return self.authenticate_keystone(keystone_ip, user, password,
project_name=tenant)
def authenticate_glance_admin(self, keystone):
"""Authenticates admin user with glance."""