summaryrefslogtreecommitdiffstats
path: root/qa/tasks/keystone.py
diff options
context:
space:
mode:
Diffstat (limited to 'qa/tasks/keystone.py')
-rw-r--r--qa/tasks/keystone.py481
1 files changed, 481 insertions, 0 deletions
diff --git a/qa/tasks/keystone.py b/qa/tasks/keystone.py
new file mode 100644
index 000000000..7aa785055
--- /dev/null
+++ b/qa/tasks/keystone.py
@@ -0,0 +1,481 @@
+"""
+Deploy and configure Keystone for Teuthology
+"""
+import argparse
+import contextlib
+import logging
+
+# still need this for python3.6
+from collections import OrderedDict
+from itertools import chain
+
+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
+
+log = logging.getLogger(__name__)
+
+
+def get_keystone_dir(ctx):
+ return '{tdir}/keystone'.format(tdir=teuthology.get_testdir(ctx))
+
+def run_in_keystone_dir(ctx, client, args, **kwargs):
+ return ctx.cluster.only(client).run(
+ args=[ 'cd', get_keystone_dir(ctx), run.Raw('&&'), ] + args,
+ **kwargs
+ )
+
+def get_toxvenv_dir(ctx):
+ return ctx.tox.venv_path
+
+def toxvenv_sh(ctx, remote, args, **kwargs):
+ activate = get_toxvenv_dir(ctx) + '/bin/activate'
+ return remote.sh(['source', activate, run.Raw('&&')] + args, **kwargs)
+
+def run_in_keystone_venv(ctx, client, args):
+ run_in_keystone_dir(ctx, client,
+ [ 'source',
+ '.tox/venv/bin/activate',
+ run.Raw('&&')
+ ] + args)
+
+def get_keystone_venved_cmd(ctx, cmd, args, env=[]):
+ kbindir = get_keystone_dir(ctx) + '/.tox/venv/bin/'
+ return env + [ kbindir + 'python', kbindir + cmd ] + args
+
+@contextlib.contextmanager
+def download(ctx, config):
+ """
+ Download the Keystone 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 keystone...')
+ keystonedir = get_keystone_dir(ctx)
+
+ for (client, cconf) in config.items():
+ ctx.cluster.only(client).run(
+ args=[
+ 'git', 'clone',
+ '-b', cconf.get('force-branch', 'master'),
+ 'https://github.com/openstack/keystone.git',
+ keystonedir,
+ ],
+ )
+
+ sha1 = cconf.get('sha1')
+ if sha1 is not None:
+ run_in_keystone_dir(ctx, client, [
+ 'git', 'reset', '--hard', sha1,
+ ],
+ )
+
+ # hax for http://tracker.ceph.com/issues/23659
+ run_in_keystone_dir(ctx, client, [
+ 'sed', '-i',
+ 's/pysaml2<4.0.3,>=2.4.0/pysaml2>=4.5.0/',
+ 'requirements.txt'
+ ],
+ )
+ try:
+ yield
+ finally:
+ log.info('Removing keystone...')
+ for client in config:
+ ctx.cluster.only(client).run(
+ args=[ 'rm', '-rf', keystonedir ],
+ )
+
+patch_bindep_template = """\
+import fileinput
+import sys
+import os
+fixed=False
+os.chdir("{keystone_dir}")
+for line in fileinput.input("bindep.txt", inplace=True):
+ if line == "python34-devel [platform:centos]\\n":
+ line="python34-devel [platform:centos-7]\\npython36-devel [platform:centos-8]\\n"
+ fixed=True
+ print(line,end="")
+
+print("Fixed line" if fixed else "No fix necessary", file=sys.stderr)
+exit(0)
+"""
+
+@contextlib.contextmanager
+def install_packages(ctx, config):
+ """
+ Download the packaged dependencies of Keystone.
+ 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 packages for Keystone...')
+
+ patch_bindep = patch_bindep_template \
+ .replace("{keystone_dir}", get_keystone_dir(ctx))
+ packages = {}
+ for (client, _) in config.items():
+ (remote,) = ctx.cluster.only(client).remotes.keys()
+ toxvenv_sh(ctx, remote, ['python'], stdin=patch_bindep)
+ # use bindep to read which dependencies we need from keystone/bindep.txt
+ toxvenv_sh(ctx, remote, ['pip', 'install', 'bindep'])
+ packages[client] = toxvenv_sh(ctx, remote,
+ ['bindep', '--brief', '--file', '{}/bindep.txt'.format(get_keystone_dir(ctx))],
+ check_status=False).splitlines() # returns 1 on success?
+ for dep in packages[client]:
+ install_package(dep, remote)
+ try:
+ yield
+ finally:
+ log.info('Removing packaged dependencies of Keystone...')
+
+ for (client, _) in config.items():
+ (remote,) = ctx.cluster.only(client).remotes.keys()
+ for dep in packages[client]:
+ remove_package(dep, remote)
+
+def run_mysql_query(ctx, remote, query):
+ query_arg = '--execute="{}"'.format(query)
+ args = ['sudo', 'mysql', run.Raw(query_arg)]
+ remote.run(args=args)
+
+@contextlib.contextmanager
+def setup_database(ctx, config):
+ """
+ Setup database for Keystone.
+ """
+ assert isinstance(config, dict)
+ log.info('Setting up database for keystone...')
+
+ for (client, cconf) in config.items():
+ (remote,) = ctx.cluster.only(client).remotes.keys()
+
+ # MariaDB on RHEL/CentOS needs service started after package install
+ # while Ubuntu starts service by default.
+ if remote.os.name == 'rhel' or remote.os.name == 'centos':
+ remote.run(args=['sudo', 'systemctl', 'restart', 'mariadb'])
+
+ run_mysql_query(ctx, remote, "CREATE USER 'keystone'@'localhost' IDENTIFIED BY 'SECRET';")
+ run_mysql_query(ctx, remote, "CREATE DATABASE keystone;")
+ run_mysql_query(ctx, remote, "GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'localhost';")
+ run_mysql_query(ctx, remote, "FLUSH PRIVILEGES;")
+
+ try:
+ yield
+ finally:
+ pass
+
+@contextlib.contextmanager
+def setup_venv(ctx, config):
+ """
+ Setup the virtualenv for Keystone using tox.
+ """
+ assert isinstance(config, dict)
+ log.info('Setting up virtualenv for keystone...')
+ for (client, _) in config.items():
+ run_in_keystone_dir(ctx, client,
+ ['sed', '-i', 's/usedevelop.*/usedevelop=false/g', 'tox.ini'])
+
+ run_in_keystone_dir(ctx, client,
+ [ 'source',
+ '{tvdir}/bin/activate'.format(tvdir=get_toxvenv_dir(ctx)),
+ run.Raw('&&'),
+ 'tox', '-e', 'venv', '--notest'
+ ])
+
+ run_in_keystone_venv(ctx, client,
+ [ 'pip', 'install',
+ 'python-openstackclient==5.2.1',
+ 'osc-lib==2.0.0'
+ ])
+ try:
+ yield
+ finally:
+ pass
+
+@contextlib.contextmanager
+def configure_instance(ctx, config):
+ assert isinstance(config, dict)
+ log.info('Configuring keystone...')
+
+ kdir = get_keystone_dir(ctx)
+ keyrepo_dir = '{kdir}/etc/fernet-keys'.format(kdir=kdir)
+ for (client, _) in config.items():
+ # prepare the config file
+ run_in_keystone_dir(ctx, client,
+ [
+ 'source',
+ f'{get_toxvenv_dir(ctx)}/bin/activate',
+ run.Raw('&&'),
+ 'tox', '-e', 'genconfig'
+ ])
+ run_in_keystone_dir(ctx, client,
+ [
+ 'cp', '-f',
+ 'etc/keystone.conf.sample',
+ 'etc/keystone.conf'
+ ])
+ run_in_keystone_dir(ctx, client,
+ [
+ 'sed',
+ '-e', 's^#key_repository =.*^key_repository = {kr}^'.format(kr = keyrepo_dir),
+ '-i', 'etc/keystone.conf'
+ ])
+ run_in_keystone_dir(ctx, client,
+ [
+ 'sed',
+ '-e', 's^#connection =.*^connection = mysql+pymysql://keystone:SECRET@localhost/keystone^',
+ '-i', 'etc/keystone.conf'
+ ])
+ # log to a file that gets archived
+ log_file = '{p}/archive/keystone.{c}.log'.format(p=teuthology.get_testdir(ctx), c=client)
+ run_in_keystone_dir(ctx, client,
+ [
+ 'sed',
+ '-e', 's^#log_file =.*^log_file = {}^'.format(log_file),
+ '-i', 'etc/keystone.conf'
+ ])
+ # copy the config to archive
+ run_in_keystone_dir(ctx, client, [
+ 'cp', 'etc/keystone.conf',
+ '{}/archive/keystone.{}.conf'.format(teuthology.get_testdir(ctx), client)
+ ])
+
+ conf_file = '{kdir}/etc/keystone.conf'.format(kdir=get_keystone_dir(ctx))
+
+ # prepare key repository for Fetnet token authenticator
+ run_in_keystone_dir(ctx, client, [ 'mkdir', '-p', keyrepo_dir ])
+ run_in_keystone_venv(ctx, client, [ 'keystone-manage', '--config-file', conf_file, 'fernet_setup' ])
+
+ # sync database
+ run_in_keystone_venv(ctx, client, [ 'keystone-manage', '--config-file', conf_file, 'db_sync' ])
+ yield
+
+@contextlib.contextmanager
+def run_keystone(ctx, config):
+ assert isinstance(config, dict)
+ log.info('Configuring keystone...')
+
+ conf_file = '{kdir}/etc/keystone.conf'.format(kdir=get_keystone_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 = 'keystone.public' + '.' + client_id
+
+ public_host, public_port = ctx.keystone.public_endpoints[client]
+ run_cmd = get_keystone_venved_cmd(ctx, 'keystone-wsgi-public',
+ [ '--host', public_host, '--port', str(public_port),
+ # Let's put the Keystone in background, wait for EOF
+ # and after receiving it, send SIGTERM to the daemon.
+ # This crazy hack is because Keystone, in contrast to
+ # our other daemons, doesn't quit on stdin.close().
+ # Teuthology relies on this behaviour.
+ run.Raw('& { read; kill %1; }')
+ ],
+ [
+ run.Raw('OS_KEYSTONE_CONFIG_FILES={}'.format(conf_file)),
+ ],
+ )
+ ctx.daemons.add_daemon(
+ remote, 'keystone', client_public_with_id,
+ cluster=cluster_name,
+ args=run_cmd,
+ logger=log.getChild(client),
+ stdin=run.PIPE,
+ wait=False,
+ check_status=False,
+ )
+
+ # sleep driven synchronization
+ run_in_keystone_venv(ctx, client, [ 'sleep', '15' ])
+ try:
+ yield
+ finally:
+ log.info('Stopping Keystone public instance')
+ ctx.daemons.get_daemon('keystone', client_public_with_id,
+ cluster_name).stop()
+
+
+def dict_to_args(specials, items):
+ """
+ Transform
+ [(key1, val1), (special, val_special), (key3, val3) ]
+ into:
+ [ '--key1', 'val1', '--key3', 'val3', 'val_special' ]
+ """
+ args = []
+ special_vals = OrderedDict((k, '') for k in specials.split(','))
+ for (k, v) in items:
+ if k in special_vals:
+ special_vals[k] = v
+ else:
+ args.append('--{k}'.format(k=k))
+ args.append(v)
+ args.extend(arg for arg in special_vals.values() if arg)
+ return args
+
+def run_section_cmds(ctx, cclient, section_cmd, specials,
+ section_config_list):
+ public_host, public_port = ctx.keystone.public_endpoints[cclient]
+
+ auth_section = [
+ ( 'os-username', 'admin' ),
+ ( 'os-password', 'ADMIN' ),
+ ( 'os-user-domain-id', 'default' ),
+ ( 'os-project-name', 'admin' ),
+ ( 'os-project-domain-id', 'default' ),
+ ( 'os-identity-api-version', '3' ),
+ ( 'os-auth-url', 'http://{host}:{port}/v3'.format(host=public_host,
+ port=public_port) ),
+ ]
+
+ for section_item in section_config_list:
+ run_in_keystone_venv(ctx, cclient,
+ [ 'openstack' ] + section_cmd.split() +
+ dict_to_args(specials, auth_section + list(section_item.items())) +
+ [ '--debug' ])
+
+def create_endpoint(ctx, cclient, service, url, adminurl=None):
+ endpoint_sections = [
+ {'service': service, 'interface': 'public', 'url': url},
+ ]
+ if adminurl:
+ endpoint_sections.append(
+ {'service': service, 'interface': 'admin', 'url': adminurl}
+ )
+ run_section_cmds(ctx, cclient, 'endpoint create',
+ 'service,interface,url',
+ endpoint_sections)
+
+@contextlib.contextmanager
+def fill_keystone(ctx, config):
+ assert isinstance(config, dict)
+
+ for (cclient, cconfig) in config.items():
+ public_host, public_port = ctx.keystone.public_endpoints[cclient]
+ url = 'http://{host}:{port}/v3'.format(host=public_host,
+ port=public_port)
+ opts = {'password': 'ADMIN',
+ 'region-id': 'RegionOne',
+ 'internal-url': url,
+ 'admin-url': url,
+ 'public-url': url}
+ bootstrap_args = chain.from_iterable(('--bootstrap-{}'.format(k), v)
+ for k, v in opts.items())
+ conf_file = '{kdir}/etc/keystone.conf'.format(kdir=get_keystone_dir(ctx))
+ run_in_keystone_venv(ctx, cclient,
+ ['keystone-manage', '--config-file', conf_file, 'bootstrap'] +
+ list(bootstrap_args))
+
+ # configure tenants/projects
+ run_section_cmds(ctx, cclient, 'domain create --or-show', 'name',
+ cconfig.get('domains', []))
+ run_section_cmds(ctx, cclient, 'project create --or-show', 'name',
+ cconfig.get('projects', []))
+ run_section_cmds(ctx, cclient, 'user create --or-show', 'name',
+ cconfig.get('users', []))
+ run_section_cmds(ctx, cclient, 'role create --or-show', 'name',
+ cconfig.get('roles', []))
+ run_section_cmds(ctx, cclient, 'role add', 'name',
+ cconfig.get('role-mappings', []))
+ run_section_cmds(ctx, cclient, 'service create', 'type',
+ cconfig.get('services', []))
+
+ # for the deferred endpoint creation; currently it's used in rgw.py
+ ctx.keystone.create_endpoint = create_endpoint
+
+ # sleep driven synchronization -- just in case
+ run_in_keystone_venv(ctx, cclient, [ 'sleep', '3' ])
+ try:
+ yield
+ finally:
+ pass
+
+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
+
+@contextlib.contextmanager
+def task(ctx, config):
+ """
+ Deploy and configure Keystone
+
+ Example of configuration:
+
+ - install:
+ - ceph:
+ - tox: [ client.0 ]
+ - keystone:
+ client.0:
+ force-branch: master
+ domains:
+ - name: custom
+ description: Custom domain
+ projects:
+ - name: custom
+ description: Custom project
+ users:
+ - name: custom
+ password: SECRET
+ project: custom
+ roles: [ name: custom ]
+ role-mappings:
+ - name: custom
+ user: custom
+ project: custom
+ services:
+ - name: swift
+ type: object-store
+ description: Swift Service
+ """
+ assert config is None or isinstance(config, list) \
+ or isinstance(config, dict), \
+ "task keystone only supports a list or dictionary for configuration"
+
+ if not hasattr(ctx, 'tox'):
+ raise ConfigError('keystone must run after the tox task')
+
+ 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)
+
+ log.debug('Keystone config is %s', config)
+
+ ctx.keystone = argparse.Namespace()
+ ctx.keystone.public_endpoints = assign_ports(ctx, config, 5000)
+
+ with contextutil.nested(
+ lambda: download(ctx=ctx, config=config),
+ lambda: install_packages(ctx=ctx, config=config),
+ lambda: setup_database(ctx=ctx, config=config),
+ lambda: setup_venv(ctx=ctx, config=config),
+ lambda: configure_instance(ctx=ctx, config=config),
+ lambda: run_keystone(ctx=ctx, config=config),
+ lambda: fill_keystone(ctx=ctx, config=config),
+ ):
+ yield