mirror of
https://github.com/ceph/ceph
synced 2024-12-24 20:33:27 +00:00
ed70d843df
* access * acquire * across * algorithm * alternatively * ambiguous * attributes * authenticated * automatically * based * between * beware * bootstrap * bucket * cache * character * check * cleanup * combinations * committed * comparison * compatibility * concurrent * conditions * conjunction * connections * considered * contains * correct * creates * crypto * dependencies * dequeued * dequeuing * derivative * described * determine * deterministic * detritus * discrepancy * display * either way * elements * entirely * entries * error * everything * expected * explicitly * extend * false * fclose * feature * following * foreign * format * forward * functionality * functions * generated * getting * gracefully * happened * hashicorp * healthcheck * hierarchy * however * http * identify * idleness * ignored * incremental * information * initialization * length * locally * macros * malformed * means * mechanism * multipart * multiple * must * namespace * necessary * nonexistent * notifications * occurred * official * outputs * outputted * overridden * ownership * parallelism * partition * passthrough * pause * penguin * perform * permission * populated * position * preferably * prefixes * presumably * previous * print * quadruple * reference * regardless * registry * renewed * request * requires * reshard * resource * result to * satisfy * secret * separate * separator * setting * shouldn't * skipping * some * somehow * something * specific * struct * succeeded * success * successful * successfully * syntax * testing * truncated * unambiguously * unittest * unmodified * unnecessary * valid_action * verifying * versioning * visible * whatever * whenever Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
466 lines
15 KiB
Python
466 lines
15 KiB
Python
"""
|
|
Deploy and configure PyKMIP for Teuthology
|
|
"""
|
|
import argparse
|
|
import contextlib
|
|
import logging
|
|
import time
|
|
import tempfile
|
|
import json
|
|
import os
|
|
from io import BytesIO
|
|
from teuthology.orchestra.daemon import DaemonGroup
|
|
from teuthology.orchestra.remote import Remote
|
|
|
|
import pprint
|
|
|
|
from teuthology import misc as teuthology
|
|
from teuthology import contextutil
|
|
from teuthology.orchestra import run
|
|
from teuthology.packaging import install_package
|
|
from teuthology.packaging import remove_package
|
|
from teuthology.exceptions import ConfigError
|
|
from tasks.util import get_remote_for_role
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
def get_pykmip_dir(ctx):
|
|
return '{tdir}/pykmip'.format(tdir=teuthology.get_testdir(ctx))
|
|
|
|
def run_in_pykmip_dir(ctx, client, args, **kwargs):
|
|
(remote,) = [client] if isinstance(client,Remote) else ctx.cluster.only(client).remotes.keys()
|
|
return remote.run(
|
|
args=['cd', get_pykmip_dir(ctx), run.Raw('&&'), ] + args,
|
|
**kwargs
|
|
)
|
|
|
|
def run_in_pykmip_venv(ctx, client, args, **kwargs):
|
|
return run_in_pykmip_dir(ctx, client,
|
|
args = ['.', '.pykmipenv/bin/activate',
|
|
run.Raw('&&')
|
|
] + args, **kwargs)
|
|
|
|
@contextlib.contextmanager
|
|
def download(ctx, config):
|
|
"""
|
|
Download PyKMIP from github.
|
|
Remove downloaded file upon exit.
|
|
|
|
The context passed in should be identical to the context
|
|
passed in to the main task.
|
|
"""
|
|
assert isinstance(config, dict)
|
|
log.info('Downloading pykmip...')
|
|
pykmipdir = get_pykmip_dir(ctx)
|
|
|
|
for (client, cconf) in config.items():
|
|
branch = cconf.get('force-branch', 'master')
|
|
repo = cconf.get('force-repo', 'https://github.com/OpenKMIP/PyKMIP')
|
|
sha1 = cconf.get('sha1')
|
|
log.info("Using branch '%s' for pykmip", branch)
|
|
log.info('sha1=%s', sha1)
|
|
|
|
ctx.cluster.only(client).run(
|
|
args=[
|
|
'git', 'clone', '-b', branch, repo,
|
|
pykmipdir,
|
|
],
|
|
)
|
|
if sha1 is not None:
|
|
run_in_pykmip_dir(ctx, client, [
|
|
'git', 'reset', '--hard', sha1,
|
|
],
|
|
)
|
|
try:
|
|
yield
|
|
finally:
|
|
log.info('Removing pykmip...')
|
|
for client in config:
|
|
ctx.cluster.only(client).run(
|
|
args=[ 'rm', '-rf', pykmipdir ],
|
|
)
|
|
|
|
_bindep_txt = """# should be part of PyKMIP
|
|
libffi-dev [platform:dpkg]
|
|
libffi-devel [platform:rpm]
|
|
libssl-dev [platform:dpkg]
|
|
openssl-devel [platform:redhat]
|
|
libopenssl-devel [platform:suse]
|
|
libsqlite3-dev [platform:dpkg]
|
|
sqlite-devel [platform:rpm]
|
|
python-dev [platform:dpkg]
|
|
python-devel [(platform:redhat platform:base-py2)]
|
|
python3-dev [platform:dpkg]
|
|
python3-devel [(platform:redhat platform:base-py3) platform:suse]
|
|
python3 [platform:suse]
|
|
"""
|
|
|
|
@contextlib.contextmanager
|
|
def install_packages(ctx, config):
|
|
"""
|
|
Download the packaged dependencies of PyKMIP.
|
|
Remove install packages upon exit.
|
|
|
|
The context passed in should be identical to the context
|
|
passed in to the main task.
|
|
"""
|
|
assert isinstance(config, dict)
|
|
log.info('Installing system dependencies for PyKMIP...')
|
|
|
|
packages = {}
|
|
for (client, _) in config.items():
|
|
(remote,) = ctx.cluster.only(client).remotes.keys()
|
|
# use bindep to read which dependencies we need from temp/bindep.txt
|
|
fd, local_temp_path = tempfile.mkstemp(suffix='.txt',
|
|
prefix='bindep-')
|
|
os.write(fd, _bindep_txt.encode())
|
|
os.close(fd)
|
|
fd, remote_temp_path = tempfile.mkstemp(suffix='.txt',
|
|
prefix='bindep-')
|
|
os.close(fd)
|
|
remote.put_file(local_temp_path, remote_temp_path)
|
|
os.remove(local_temp_path)
|
|
run_in_pykmip_venv(ctx, remote, ['pip', 'install', 'bindep'])
|
|
r = run_in_pykmip_venv(ctx, remote,
|
|
['bindep', '--brief', '--file', remote_temp_path],
|
|
stdout=BytesIO(),
|
|
check_status=False) # returns 1 on success?
|
|
packages[client] = r.stdout.getvalue().decode().splitlines()
|
|
for dep in packages[client]:
|
|
install_package(dep, remote)
|
|
try:
|
|
yield
|
|
finally:
|
|
log.info('Removing system dependencies of PyKMIP...')
|
|
|
|
for (client, _) in config.items():
|
|
(remote,) = ctx.cluster.only(client).remotes.keys()
|
|
for dep in packages[client]:
|
|
remove_package(dep, remote)
|
|
|
|
@contextlib.contextmanager
|
|
def setup_venv(ctx, config):
|
|
"""
|
|
Setup the virtualenv for PyKMIP using pip.
|
|
"""
|
|
assert isinstance(config, dict)
|
|
log.info('Setting up virtualenv for pykmip...')
|
|
for (client, _) in config.items():
|
|
run_in_pykmip_dir(ctx, client, ['python3', '-m', 'venv', '.pykmipenv'])
|
|
run_in_pykmip_venv(ctx, client, ['pip', 'install', '--upgrade', 'pip'])
|
|
run_in_pykmip_venv(ctx, client, ['pip', 'install', 'pytz', '-e', get_pykmip_dir(ctx)])
|
|
yield
|
|
|
|
def assign_ports(ctx, config, initial_port):
|
|
"""
|
|
Assign port numbers starting from @initial_port
|
|
"""
|
|
port = initial_port
|
|
role_endpoints = {}
|
|
for remote, roles_for_host in ctx.cluster.remotes.items():
|
|
for role in roles_for_host:
|
|
if role in config:
|
|
r = get_remote_for_role(ctx, role)
|
|
role_endpoints[role] = r.ip_address, port, r.hostname
|
|
port += 1
|
|
|
|
return role_endpoints
|
|
|
|
def copy_policy_json(ctx, cclient, cconfig):
|
|
run_in_pykmip_dir(ctx, cclient,
|
|
['cp',
|
|
get_pykmip_dir(ctx)+'/examples/policy.json',
|
|
get_pykmip_dir(ctx)])
|
|
|
|
_pykmip_configuration = """# configuration for pykmip
|
|
[server]
|
|
hostname={ipaddr}
|
|
port={port}
|
|
certificate_path={servercert}
|
|
key_path={serverkey}
|
|
ca_path={clientca}
|
|
auth_suite=TLS1.2
|
|
policy_path={confdir}
|
|
enable_tls_client_auth=False
|
|
tls_cipher_suites=
|
|
TLS_RSA_WITH_AES_128_CBC_SHA256
|
|
TLS_RSA_WITH_AES_256_CBC_SHA256
|
|
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384
|
|
logging_level=DEBUG
|
|
database_path={confdir}/pykmip.sqlite
|
|
[client]
|
|
host={hostname}
|
|
port=5696
|
|
certfile={clientcert}
|
|
keyfile={clientkey}
|
|
ca_certs={clientca}
|
|
ssl_version=PROTOCOL_TLSv1_2
|
|
"""
|
|
|
|
def create_pykmip_conf(ctx, cclient, cconfig):
|
|
log.info('#0 cclient={} cconfig={}'.format(pprint.pformat(cclient),pprint.pformat(cconfig)))
|
|
(remote,) = ctx.cluster.only(cclient).remotes.keys()
|
|
pykmip_ipaddr, pykmip_port, pykmip_hostname = ctx.pykmip.endpoints[cclient]
|
|
log.info('#1 ip,p,h {} {} {}'.format(pykmip_ipaddr, pykmip_port, pykmip_hostname))
|
|
clientca = cconfig.get('clientca', None)
|
|
log.info('#2 clientca {}'.format(clientca))
|
|
serverkey = None
|
|
servercert = cconfig.get('servercert', None)
|
|
log.info('#3 servercert {}'.format(servercert))
|
|
servercert = ctx.ssl_certificates.get(servercert)
|
|
log.info('#4 servercert {}'.format(servercert))
|
|
clientkey = None
|
|
clientcert = cconfig.get('clientcert', None)
|
|
log.info('#3 clientcert {}'.format(clientcert))
|
|
clientcert = ctx.ssl_certificates.get(clientcert)
|
|
log.info('#4 clientcert {}'.format(clientcert))
|
|
clientca = ctx.ssl_certificates.get(clientca)
|
|
log.info('#5 clientca {}'.format(clientca))
|
|
if servercert != None:
|
|
serverkey = servercert.key
|
|
servercert = servercert.certificate
|
|
log.info('#6 serverkey {} servercert {}'.format(serverkey, servercert))
|
|
if clientcert != None:
|
|
clientkey = clientcert.key
|
|
clientcert = clientcert.certificate
|
|
log.info('#6 clientkey {} clientcert {}'.format(clientkey, clientcert))
|
|
if clientca != None:
|
|
clientca = clientca.certificate
|
|
log.info('#7 clientca {}'.format(clientca))
|
|
if servercert == None or clientca == None or serverkey == None:
|
|
log.info('#8 clientca {} serverkey {} servercert {}'.format(clientca, serverkey, servercert))
|
|
raise ConfigError('pykmip: Missing/bad servercert or clientca')
|
|
pykmipdir = get_pykmip_dir(ctx)
|
|
kmip_conf = _pykmip_configuration.format(
|
|
ipaddr=pykmip_ipaddr,
|
|
port=pykmip_port,
|
|
confdir=pykmipdir,
|
|
hostname=pykmip_hostname,
|
|
clientca=clientca,
|
|
clientkey=clientkey,
|
|
clientcert=clientcert,
|
|
serverkey=serverkey,
|
|
servercert=servercert
|
|
)
|
|
fd, local_temp_path = tempfile.mkstemp(suffix='.conf',
|
|
prefix='pykmip')
|
|
os.write(fd, kmip_conf.encode())
|
|
os.close(fd)
|
|
remote.put_file(local_temp_path, pykmipdir+'/pykmip.conf')
|
|
os.remove(local_temp_path)
|
|
|
|
@contextlib.contextmanager
|
|
def configure_pykmip(ctx, config):
|
|
"""
|
|
Configure pykmip paste-api and pykmip-api.
|
|
"""
|
|
assert isinstance(config, dict)
|
|
(cclient, cconfig) = next(iter(config.items()))
|
|
|
|
copy_policy_json(ctx, cclient, cconfig)
|
|
create_pykmip_conf(ctx, cclient, cconfig)
|
|
try:
|
|
yield
|
|
finally:
|
|
pass
|
|
|
|
def has_ceph_task(tasks):
|
|
for task in tasks:
|
|
for name, conf in task.items():
|
|
if name == 'ceph':
|
|
return True
|
|
return False
|
|
|
|
@contextlib.contextmanager
|
|
def run_pykmip(ctx, config):
|
|
assert isinstance(config, dict)
|
|
if hasattr(ctx, 'daemons'):
|
|
pass
|
|
elif has_ceph_task(ctx.config['tasks']):
|
|
log.info('Delay start pykmip so ceph can do once-only daemon logic')
|
|
try:
|
|
yield
|
|
finally:
|
|
pass
|
|
else:
|
|
ctx.daemons = DaemonGroup()
|
|
log.info('Running pykmip...')
|
|
|
|
pykmipdir = get_pykmip_dir(ctx)
|
|
|
|
for (client, _) in config.items():
|
|
(remote,) = ctx.cluster.only(client).remotes.keys()
|
|
cluster_name, _, client_id = teuthology.split_role(client)
|
|
|
|
# start the public endpoint
|
|
client_public_with_id = 'pykmip.public' + '.' + client_id
|
|
|
|
run_cmd = 'cd ' + pykmipdir + ' && ' + \
|
|
'. .pykmipenv/bin/activate && ' + \
|
|
'HOME={}'.format(pykmipdir) + ' && ' + \
|
|
'exec pykmip-server -f pykmip.conf -l ' + \
|
|
pykmipdir + '/pykmip.log & { read; kill %1; }'
|
|
|
|
ctx.daemons.add_daemon(
|
|
remote, 'pykmip', client_public_with_id,
|
|
cluster=cluster_name,
|
|
args=['bash', '-c', run_cmd],
|
|
logger=log.getChild(client),
|
|
stdin=run.PIPE,
|
|
cwd=pykmipdir,
|
|
wait=False,
|
|
check_status=False,
|
|
)
|
|
|
|
# sleep driven synchronization
|
|
time.sleep(10)
|
|
try:
|
|
yield
|
|
finally:
|
|
log.info('Stopping PyKMIP instance')
|
|
ctx.daemons.get_daemon('pykmip', client_public_with_id,
|
|
cluster_name).stop()
|
|
|
|
make_keys_template = """
|
|
from kmip.pie import client
|
|
from kmip import enums
|
|
import ssl
|
|
import sys
|
|
import json
|
|
from io import BytesIO
|
|
|
|
c = client.ProxyKmipClient(config_file="{replace-with-config-file-path}")
|
|
|
|
rl=[]
|
|
for kwargs in {replace-with-secrets}:
|
|
with c:
|
|
key_id = c.create(
|
|
enums.CryptographicAlgorithm.AES,
|
|
256,
|
|
operation_policy_name='default',
|
|
cryptographic_usage_mask=[
|
|
enums.CryptographicUsageMask.ENCRYPT,
|
|
enums.CryptographicUsageMask.DECRYPT
|
|
],
|
|
**kwargs
|
|
)
|
|
c.activate(key_id)
|
|
attrs = c.get_attributes(uid=key_id)
|
|
r = {}
|
|
for a in attrs[1]:
|
|
r[str(a.attribute_name)] = str(a.attribute_value)
|
|
rl.append(r)
|
|
print(json.dumps(rl))
|
|
"""
|
|
|
|
@contextlib.contextmanager
|
|
def create_secrets(ctx, config):
|
|
"""
|
|
Create and activate any requested keys in kmip
|
|
"""
|
|
assert isinstance(config, dict)
|
|
|
|
pykmipdir = get_pykmip_dir(ctx)
|
|
pykmip_conf_path = pykmipdir + '/pykmip.conf'
|
|
my_output = BytesIO()
|
|
for (client,cconf) in config.items():
|
|
(remote,) = ctx.cluster.only(client).remotes.keys()
|
|
secrets=cconf.get('secrets')
|
|
if secrets:
|
|
secrets_json = json.dumps(cconf['secrets'])
|
|
make_keys = make_keys_template \
|
|
.replace("{replace-with-secrets}",secrets_json) \
|
|
.replace("{replace-with-config-file-path}",pykmip_conf_path)
|
|
my_output.truncate()
|
|
remote.run(args=[run.Raw('. cephtest/pykmip/.pykmipenv/bin/activate;' \
|
|
+ 'python')], stdin=make_keys, stdout = my_output)
|
|
ctx.pykmip.keys[client] = json.loads(my_output.getvalue().decode())
|
|
try:
|
|
yield
|
|
finally:
|
|
pass
|
|
|
|
@contextlib.contextmanager
|
|
def task(ctx, config):
|
|
"""
|
|
Deploy and configure PyKMIP
|
|
|
|
Example of configuration:
|
|
|
|
tasks:
|
|
- install:
|
|
- ceph:
|
|
conf:
|
|
client:
|
|
rgw crypt s3 kms backend: kmip
|
|
rgw crypt kmip ca path: /home/ubuntu/cephtest/ca/kmiproot.crt
|
|
rgw crypt kmip client cert: /home/ubuntu/cephtest/ca/kmip-client.crt
|
|
rgw crypt kmip client key: /home/ubuntu/cephtest/ca/kmip-client.key
|
|
rgw crypt kmip kms key template: pykmip-$keyid
|
|
- openssl_keys:
|
|
kmiproot:
|
|
client: client.0
|
|
cn: kmiproot
|
|
key-type: rsa:4096
|
|
- openssl_keys:
|
|
kmip-server:
|
|
client: client.0
|
|
ca: kmiproot
|
|
kmip-client:
|
|
client: client.0
|
|
ca: kmiproot
|
|
cn: rgw-client
|
|
- pykmip:
|
|
client.0:
|
|
force-branch: master
|
|
clientca: kmiproot
|
|
servercert: kmip-server
|
|
clientcert: kmip-client
|
|
secrets:
|
|
- name: pykmip-key-1
|
|
- name: pykmip-key-2
|
|
- rgw:
|
|
client.0:
|
|
use-pykmip-role: client.0
|
|
- s3tests:
|
|
client.0:
|
|
force-branch: master
|
|
"""
|
|
assert config is None or isinstance(config, list) \
|
|
or isinstance(config, dict), \
|
|
"task pykmip only supports a list or dictionary for configuration"
|
|
all_clients = ['client.{id}'.format(id=id_)
|
|
for id_ in teuthology.all_roles_of_type(ctx.cluster, 'client')]
|
|
if config is None:
|
|
config = all_clients
|
|
if isinstance(config, list):
|
|
config = dict.fromkeys(config)
|
|
|
|
overrides = ctx.config.get('overrides', {})
|
|
# merge each client section, not the top level.
|
|
for client in config.keys():
|
|
if not config[client]:
|
|
config[client] = {}
|
|
teuthology.deep_merge(config[client], overrides.get('pykmip', {}))
|
|
|
|
log.debug('PyKMIP config is %s', config)
|
|
|
|
if not hasattr(ctx, 'ssl_certificates'):
|
|
raise ConfigError('pykmip must run after the openssl_keys task')
|
|
|
|
|
|
ctx.pykmip = argparse.Namespace()
|
|
ctx.pykmip.endpoints = assign_ports(ctx, config, 5696)
|
|
ctx.pykmip.keys = {}
|
|
|
|
with contextutil.nested(
|
|
lambda: download(ctx=ctx, config=config),
|
|
lambda: setup_venv(ctx=ctx, config=config),
|
|
lambda: install_packages(ctx=ctx, config=config),
|
|
lambda: configure_pykmip(ctx=ctx, config=config),
|
|
lambda: run_pykmip(ctx=ctx, config=config),
|
|
lambda: create_secrets(ctx=ctx, config=config),
|
|
):
|
|
yield
|