summaryrefslogtreecommitdiffstats
path: root/qa/tasks/pykmip.py
diff options
context:
space:
mode:
Diffstat (limited to 'qa/tasks/pykmip.py')
-rw-r--r--qa/tasks/pykmip.py465
1 files changed, 465 insertions, 0 deletions
diff --git a/qa/tasks/pykmip.py b/qa/tasks/pykmip.py
new file mode 100644
index 000000000..45a5af689
--- /dev/null
+++ b/qa/tasks/pykmip.py
@@ -0,0 +1,465 @@
+"""
+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 dependenies 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