diff options
Diffstat (limited to 'qa/tasks/barbican.py')
-rw-r--r-- | qa/tasks/barbican.py | 524 |
1 files changed, 524 insertions, 0 deletions
diff --git a/qa/tasks/barbican.py b/qa/tasks/barbican.py new file mode 100644 index 000000000..771304fba --- /dev/null +++ b/qa/tasks/barbican.py @@ -0,0 +1,524 @@ +""" +Deploy and configure Barbican for Teuthology +""" +import argparse +import contextlib +import logging +import http +import json +import time +import math + +from urllib.parse import urlparse + +from teuthology import misc as teuthology +from teuthology import contextutil +from teuthology.orchestra import run +from teuthology.exceptions import ConfigError + +log = logging.getLogger(__name__) + + +@contextlib.contextmanager +def download(ctx, config): + """ + Download the Barbican 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 barbican...') + testdir = teuthology.get_testdir(ctx) + for (client, cconf) in config.items(): + branch = cconf.get('force-branch', 'master') + log.info("Using branch '%s' for barbican", branch) + + sha1 = cconf.get('sha1') + log.info('sha1=%s', sha1) + + ctx.cluster.only(client).run( + args=[ + 'bash', '-l' + ], + ) + ctx.cluster.only(client).run( + args=[ + 'git', 'clone', + '-b', branch, + 'https://github.com/openstack/barbican.git', + '{tdir}/barbican'.format(tdir=testdir), + ], + ) + if sha1 is not None: + ctx.cluster.only(client).run( + args=[ + 'cd', '{tdir}/barbican'.format(tdir=testdir), + run.Raw('&&'), + 'git', 'reset', '--hard', sha1, + ], + ) + try: + yield + finally: + log.info('Removing barbican...') + testdir = teuthology.get_testdir(ctx) + for client in config: + ctx.cluster.only(client).run( + args=[ + 'rm', + '-rf', + '{tdir}/barbican'.format(tdir=testdir), + ], + ) + +def get_barbican_dir(ctx): + return '{tdir}/barbican'.format(tdir=teuthology.get_testdir(ctx)) + +def run_in_barbican_dir(ctx, client, args): + ctx.cluster.only(client).run( + args=['cd', get_barbican_dir(ctx), run.Raw('&&'), ] + args, + ) + +def run_in_barbican_venv(ctx, client, args): + run_in_barbican_dir(ctx, client, + ['.', + '.barbicanenv/bin/activate', + run.Raw('&&') + ] + args) + +@contextlib.contextmanager +def setup_venv(ctx, config): + """ + Setup the virtualenv for Barbican using pip. + """ + assert isinstance(config, dict) + log.info('Setting up virtualenv for barbican...') + for (client, _) in config.items(): + run_in_barbican_dir(ctx, client, + ['python3', '-m', 'venv', '.barbicanenv']) + run_in_barbican_venv(ctx, client, + ['pip', 'install', '--upgrade', 'pip']) + run_in_barbican_venv(ctx, client, + ['pip', 'install', 'pytz', + '-e', get_barbican_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: + role_endpoints[role] = (remote.name.split('@')[1], port) + port += 1 + + return role_endpoints + +def set_authtoken_params(ctx, cclient, cconfig): + section_config_list = cconfig['keystone_authtoken'].items() + for config in section_config_list: + (name, val) = config + run_in_barbican_dir(ctx, cclient, + ['sed', '-i', + '/[[]filter:authtoken]/{p;s##'+'{} = {}'.format(name, val)+'#;}', + 'etc/barbican/barbican-api-paste.ini']) + + keystone_role = cconfig.get('use-keystone-role', None) + public_host, public_port = ctx.keystone.public_endpoints[keystone_role] + url = 'http://{host}:{port}/v3'.format(host=public_host, + port=public_port) + run_in_barbican_dir(ctx, cclient, + ['sed', '-i', + '/[[]filter:authtoken]/{p;s##'+'auth_uri = {}'.format(url)+'#;}', + 'etc/barbican/barbican-api-paste.ini']) + admin_url = 'http://{host}:{port}/v3'.format(host=public_host, + port=public_port) + run_in_barbican_dir(ctx, cclient, + ['sed', '-i', + '/[[]filter:authtoken]/{p;s##'+'auth_url = {}'.format(admin_url)+'#;}', + 'etc/barbican/barbican-api-paste.ini']) + +def fix_barbican_api_paste(ctx, cclient): + run_in_barbican_dir(ctx, cclient, + ['sed', '-i', '-n', + '/\\[pipeline:barbican_api]/ {p;n; /^pipeline =/ '+ + '{ s/.*/pipeline = unauthenticated-context apiapp/;p;d } } ; p', + './etc/barbican/barbican-api-paste.ini']) + +def fix_barbican_api(ctx, cclient): + run_in_barbican_dir(ctx, cclient, + ['sed', '-i', + '/prop_dir =/ s#etc/barbican#{}/etc/barbican#'.format(get_barbican_dir(ctx)), + 'bin/barbican-api']) + +def create_barbican_conf(ctx, cclient): + barbican_host, barbican_port = ctx.barbican.endpoints[cclient] + barbican_url = 'http://{host}:{port}'.format(host=barbican_host, + port=barbican_port) + log.info("barbican url=%s", barbican_url) + + run_in_barbican_dir(ctx, cclient, + ['bash', '-c', + 'echo -n -e "[DEFAULT]\nhost_href=' + barbican_url + '\n" ' + \ + '>barbican.conf']) + + log.info("run barbican db upgrade") + config_path = get_barbican_dir(ctx) + '/barbican.conf' + run_in_barbican_venv(ctx, cclient, ['barbican-manage', '--config-file', config_path, + 'db', 'upgrade']) + log.info("run barbican db sync_secret_stores") + run_in_barbican_venv(ctx, cclient, ['barbican-manage', '--config-file', config_path, + 'db', 'sync_secret_stores']) + +@contextlib.contextmanager +def configure_barbican(ctx, config): + """ + Configure barbican paste-api and barbican-api. + """ + assert isinstance(config, dict) + (cclient, cconfig) = next(iter(config.items())) + + keystone_role = cconfig.get('use-keystone-role', None) + if keystone_role is None: + raise ConfigError('use-keystone-role not defined in barbican task') + + set_authtoken_params(ctx, cclient, cconfig) + fix_barbican_api(ctx, cclient) + fix_barbican_api_paste(ctx, cclient) + create_barbican_conf(ctx, cclient) + try: + yield + finally: + pass + +@contextlib.contextmanager +def run_barbican(ctx, config): + assert isinstance(config, dict) + log.info('Running barbican...') + + 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 = 'barbican.public' + '.' + client_id + + run_cmd = ['cd', get_barbican_dir(ctx), run.Raw('&&'), + '.', '.barbicanenv/bin/activate', run.Raw('&&'), + 'HOME={}'.format(get_barbican_dir(ctx)), run.Raw('&&'), + 'bin/barbican-api', + run.Raw('& { read; kill %1; }')] + #run.Raw('1>/dev/null') + + run_cmd = 'cd ' + get_barbican_dir(ctx) + ' && ' + \ + '. .barbicanenv/bin/activate && ' + \ + 'HOME={}'.format(get_barbican_dir(ctx)) + ' && ' + \ + 'exec bin/barbican-api & { read; kill %1; }' + + ctx.daemons.add_daemon( + remote, 'barbican', client_public_with_id, + cluster=cluster_name, + args=['bash', '-c', run_cmd], + logger=log.getChild(client), + stdin=run.PIPE, + cwd=get_barbican_dir(ctx), + wait=False, + check_status=False, + ) + + # sleep driven synchronization + run_in_barbican_venv(ctx, client, ['sleep', '15']) + try: + yield + finally: + log.info('Stopping Barbican instance') + ctx.daemons.get_daemon('barbican', client_public_with_id, + cluster_name).stop() + + +@contextlib.contextmanager +def create_secrets(ctx, config): + """ + Create a main and an alternate s3 user. + """ + assert isinstance(config, dict) + (cclient, cconfig) = next(iter(config.items())) + + rgw_user = cconfig['rgw_user'] + + keystone_role = cconfig.get('use-keystone-role', None) + keystone_host, keystone_port = ctx.keystone.public_endpoints[keystone_role] + barbican_host, barbican_port = ctx.barbican.endpoints[cclient] + barbican_url = 'http://{host}:{port}'.format(host=barbican_host, + port=barbican_port) + log.info("barbican_url=%s", barbican_url) + #fetching user_id of user that gets secrets for radosgw + token_req = http.client.HTTPConnection(keystone_host, keystone_port, timeout=30) + token_req.request( + 'POST', + '/v3/auth/tokens', + headers={'Content-Type':'application/json'}, + body=json.dumps({ + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "domain": {"id": "default"}, + "name": rgw_user["username"], + "password": rgw_user["password"] + } + } + }, + "scope": { + "project": { + "domain": {"id": "default"}, + "name": rgw_user["tenantName"] + } + } + } + })) + rgw_access_user_resp = token_req.getresponse() + if not (rgw_access_user_resp.status >= 200 and + rgw_access_user_resp.status < 300): + raise Exception("Cannot authenticate user "+rgw_user["username"]+" for secret creation") + # baru_resp = json.loads(baru_req.data) + rgw_access_user_data = json.loads(rgw_access_user_resp.read().decode()) + rgw_user_id = rgw_access_user_data['token']['user']['id'] + if 'secrets' in cconfig: + for secret in cconfig['secrets']: + if 'name' not in secret: + raise ConfigError('barbican.secrets must have "name" field') + if 'base64' not in secret: + raise ConfigError('barbican.secrets must have "base64" field') + if 'tenantName' not in secret: + raise ConfigError('barbican.secrets must have "tenantName" field') + if 'username' not in secret: + raise ConfigError('barbican.secrets must have "username" field') + if 'password' not in secret: + raise ConfigError('barbican.secrets must have "password" field') + + token_req = http.client.HTTPConnection(keystone_host, keystone_port, timeout=30) + token_req.request( + 'POST', + '/v3/auth/tokens', + headers={'Content-Type':'application/json'}, + body=json.dumps({ + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "domain": {"id": "default"}, + "name": secret["username"], + "password": secret["password"] + } + } + }, + "scope": { + "project": { + "domain": {"id": "default"}, + "name": secret["tenantName"] + } + } + } + })) + token_resp = token_req.getresponse() + if not (token_resp.status >= 200 and + token_resp.status < 300): + raise Exception("Cannot authenticate user "+secret["username"]+" for secret creation") + + expire = time.time() + 5400 # now + 90m + (expire_fract,dummy) = math.modf(expire) + expire_format = "%%FT%%T.%06d" % (round(expire_fract*1000000)) + expiration = time.strftime(expire_format, time.gmtime(expire)) + token_id = token_resp.getheader('x-subject-token') + + key1_json = json.dumps( + { + "name": secret['name'], + "expiration": expiration, + "algorithm": "aes", + "bit_length": 256, + "mode": "cbc", + "payload": secret['base64'], + "payload_content_type": "application/octet-stream", + "payload_content_encoding": "base64" + }) + + sec_req = http.client.HTTPConnection(barbican_host, barbican_port, timeout=30) + try: + sec_req.request( + 'POST', + '/v1/secrets', + headers={'Content-Type': 'application/json', + 'Accept': '*/*', + 'X-Auth-Token': token_id}, + body=key1_json + ) + except: + log.info("catched exception!") + run_in_barbican_venv(ctx, cclient, ['sleep', '900']) + + barbican_sec_resp = sec_req.getresponse() + if not (barbican_sec_resp.status >= 200 and + barbican_sec_resp.status < 300): + raise Exception("Cannot create secret") + barbican_data = json.loads(barbican_sec_resp.read().decode()) + if 'secret_ref' not in barbican_data: + raise ValueError("Malformed secret creation response") + secret_ref = barbican_data["secret_ref"] + log.info("secret_ref=%s", secret_ref) + secret_url_parsed = urlparse(secret_ref) + acl_json = json.dumps( + { + "read": { + "users": [rgw_user_id], + "project-access": True + } + }) + acl_req = http.client.HTTPConnection(secret_url_parsed.netloc, timeout=30) + acl_req.request( + 'PUT', + secret_url_parsed.path+'/acl', + headers={'Content-Type': 'application/json', + 'Accept': '*/*', + 'X-Auth-Token': token_id}, + body=acl_json + ) + barbican_acl_resp = acl_req.getresponse() + if not (barbican_acl_resp.status >= 200 and + barbican_acl_resp.status < 300): + raise Exception("Cannot set ACL for secret") + + key = {'id': secret_ref.split('secrets/')[1], 'payload': secret['base64']} + ctx.barbican.keys[secret['name']] = key + + run_in_barbican_venv(ctx, cclient, ['sleep', '3']) + try: + yield + finally: + pass + + +@contextlib.contextmanager +def task(ctx, config): + """ + Deploy and configure Keystone + + Example of configuration: + + tasks: + - local_cluster: + cluster_path: /home/adam/ceph-1/build + - local_rgw: + - tox: [ client.0 ] + - keystone: + client.0: + sha1: 17.0.0.0rc2 + force-branch: master + projects: + - name: rgwcrypt + description: Encryption Tenant + - name: barbican + description: Barbican + - name: s3 + description: S3 project + users: + - name: rgwcrypt-user + password: rgwcrypt-pass + project: rgwcrypt + - name: barbican-user + password: barbican-pass + project: barbican + - name: s3-user + password: s3-pass + project: s3 + roles: [ name: Member, name: creator ] + role-mappings: + - name: Member + user: rgwcrypt-user + project: rgwcrypt + - name: admin + user: barbican-user + project: barbican + - name: creator + user: s3-user + project: s3 + services: + - name: keystone + type: identity + description: Keystone Identity Service + - barbican: + client.0: + force-branch: master + use-keystone-role: client.0 + keystone_authtoken: + auth_plugin: password + username: barbican-user + password: barbican-pass + user_domain_name: Default + rgw_user: + tenantName: rgwcrypt + username: rgwcrypt-user + password: rgwcrypt-pass + secrets: + - name: my-key-1 + base64: a2V5MS5GcWVxKzhzTGNLaGtzQkg5NGVpb1FKcFpGb2c= + tenantName: s3 + username: s3-user + password: s3-pass + - name: my-key-2 + base64: a2V5Mi5yNUNNMGFzMVdIUVZxcCt5NGVmVGlQQ1k4YWg= + tenantName: s3 + username: s3-user + password: s3-pass + - s3tests: + client.0: + force-branch: master + kms_key: my-key-1 + - rgw: + client.0: + use-keystone-role: client.0 + use-barbican-role: client.0 + """ + assert config is None or isinstance(config, list) \ + or isinstance(config, dict), \ + "task keystone 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('barbican', {})) + + log.debug('Barbican config is %s', config) + + if not hasattr(ctx, 'keystone'): + raise ConfigError('barbican must run after the keystone task') + + + ctx.barbican = argparse.Namespace() + ctx.barbican.endpoints = assign_ports(ctx, config, 9311) + ctx.barbican.keys = {} + + with contextutil.nested( + lambda: download(ctx=ctx, config=config), + lambda: setup_venv(ctx=ctx, config=config), + lambda: configure_barbican(ctx=ctx, config=config), + lambda: run_barbican(ctx=ctx, config=config), + lambda: create_secrets(ctx=ctx, config=config), + ): + yield |