]> git.proxmox.com Git - ceph.git/blob - ceph/qa/tasks/keystone.py
update ceph source to reef 18.2.0
[ceph.git] / ceph / qa / tasks / keystone.py
1 """
2 Deploy and configure Keystone for Teuthology
3 """
4 import argparse
5 import contextlib
6 import logging
7
8 # still need this for python3.6
9 from collections import OrderedDict
10 from itertools import chain
11
12 from teuthology import misc as teuthology
13 from teuthology import contextutil
14 from teuthology.orchestra import run
15 from teuthology.packaging import install_package
16 from teuthology.packaging import remove_package
17 from teuthology.exceptions import ConfigError
18
19 log = logging.getLogger(__name__)
20
21
22 def get_keystone_dir(ctx):
23 return '{tdir}/keystone'.format(tdir=teuthology.get_testdir(ctx))
24
25 def run_in_keystone_dir(ctx, client, args, **kwargs):
26 return ctx.cluster.only(client).run(
27 args=[ 'cd', get_keystone_dir(ctx), run.Raw('&&'), ] + args,
28 **kwargs
29 )
30
31 def get_toxvenv_dir(ctx):
32 return ctx.tox.venv_path
33
34 def toxvenv_sh(ctx, remote, args, **kwargs):
35 activate = get_toxvenv_dir(ctx) + '/bin/activate'
36 return remote.sh(['source', activate, run.Raw('&&')] + args, **kwargs)
37
38 def run_in_keystone_venv(ctx, client, args):
39 run_in_keystone_dir(ctx, client,
40 [ 'source',
41 '.tox/venv/bin/activate',
42 run.Raw('&&')
43 ] + args)
44
45 def get_keystone_venved_cmd(ctx, cmd, args, env=[]):
46 kbindir = get_keystone_dir(ctx) + '/.tox/venv/bin/'
47 return env + [ kbindir + 'python', kbindir + cmd ] + args
48
49 @contextlib.contextmanager
50 def download(ctx, config):
51 """
52 Download the Keystone from github.
53 Remove downloaded file upon exit.
54
55 The context passed in should be identical to the context
56 passed in to the main task.
57 """
58 assert isinstance(config, dict)
59 log.info('Downloading keystone...')
60 keystonedir = get_keystone_dir(ctx)
61
62 for (client, cconf) in config.items():
63 ctx.cluster.only(client).run(
64 args=[
65 'git', 'clone',
66 '-b', cconf.get('force-branch', 'master'),
67 'https://github.com/openstack/keystone.git',
68 keystonedir,
69 ],
70 )
71
72 sha1 = cconf.get('sha1')
73 if sha1 is not None:
74 run_in_keystone_dir(ctx, client, [
75 'git', 'reset', '--hard', sha1,
76 ],
77 )
78
79 # hax for http://tracker.ceph.com/issues/23659
80 run_in_keystone_dir(ctx, client, [
81 'sed', '-i',
82 's/pysaml2<4.0.3,>=2.4.0/pysaml2>=4.5.0/',
83 'requirements.txt'
84 ],
85 )
86 try:
87 yield
88 finally:
89 log.info('Removing keystone...')
90 for client in config:
91 ctx.cluster.only(client).run(
92 args=[ 'rm', '-rf', keystonedir ],
93 )
94
95 patch_bindep_template = """\
96 import fileinput
97 import sys
98 import os
99 fixed=False
100 os.chdir("{keystone_dir}")
101 for line in fileinput.input("bindep.txt", inplace=True):
102 if line == "python34-devel [platform:centos]\\n":
103 line="python34-devel [platform:centos-7]\\npython36-devel [platform:centos-8]\\n"
104 fixed=True
105 print(line,end="")
106
107 print("Fixed line" if fixed else "No fix necessary", file=sys.stderr)
108 exit(0)
109 """
110
111 @contextlib.contextmanager
112 def install_packages(ctx, config):
113 """
114 Download the packaged dependencies of Keystone.
115 Remove install packages upon exit.
116
117 The context passed in should be identical to the context
118 passed in to the main task.
119 """
120 assert isinstance(config, dict)
121 log.info('Installing packages for Keystone...')
122
123 patch_bindep = patch_bindep_template \
124 .replace("{keystone_dir}", get_keystone_dir(ctx))
125 packages = {}
126 for (client, _) in config.items():
127 (remote,) = ctx.cluster.only(client).remotes.keys()
128 toxvenv_sh(ctx, remote, ['python'], stdin=patch_bindep)
129 # use bindep to read which dependencies we need from keystone/bindep.txt
130 toxvenv_sh(ctx, remote, ['pip', 'install', 'bindep'])
131 packages[client] = toxvenv_sh(ctx, remote,
132 ['bindep', '--brief', '--file', '{}/bindep.txt'.format(get_keystone_dir(ctx))],
133 check_status=False).splitlines() # returns 1 on success?
134 for dep in packages[client]:
135 install_package(dep, remote)
136 try:
137 yield
138 finally:
139 log.info('Removing packaged dependencies of Keystone...')
140
141 for (client, _) in config.items():
142 (remote,) = ctx.cluster.only(client).remotes.keys()
143 for dep in packages[client]:
144 remove_package(dep, remote)
145
146 def run_mysql_query(ctx, remote, query):
147 query_arg = '--execute="{}"'.format(query)
148 args = ['sudo', 'mysql', run.Raw(query_arg)]
149 remote.run(args=args)
150
151 @contextlib.contextmanager
152 def setup_database(ctx, config):
153 """
154 Setup database for Keystone.
155 """
156 assert isinstance(config, dict)
157 log.info('Setting up database for keystone...')
158
159 for (client, cconf) in config.items():
160 (remote,) = ctx.cluster.only(client).remotes.keys()
161
162 # MariaDB on RHEL/CentOS needs service started after package install
163 # while Ubuntu starts service by default.
164 if remote.os.name == 'rhel' or remote.os.name == 'centos':
165 remote.run(args=['sudo', 'systemctl', 'restart', 'mariadb'])
166
167 run_mysql_query(ctx, remote, "CREATE USER 'keystone'@'localhost' IDENTIFIED BY 'SECRET';")
168 run_mysql_query(ctx, remote, "CREATE DATABASE keystone;")
169 run_mysql_query(ctx, remote, "GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'localhost';")
170 run_mysql_query(ctx, remote, "FLUSH PRIVILEGES;")
171
172 try:
173 yield
174 finally:
175 pass
176
177 @contextlib.contextmanager
178 def setup_venv(ctx, config):
179 """
180 Setup the virtualenv for Keystone using tox.
181 """
182 assert isinstance(config, dict)
183 log.info('Setting up virtualenv for keystone...')
184 for (client, _) in config.items():
185 run_in_keystone_dir(ctx, client,
186 ['sed', '-i', 's/usedevelop.*/usedevelop=false/g', 'tox.ini'])
187
188 run_in_keystone_dir(ctx, client,
189 [ 'source',
190 '{tvdir}/bin/activate'.format(tvdir=get_toxvenv_dir(ctx)),
191 run.Raw('&&'),
192 'tox', '-e', 'venv', '--notest'
193 ])
194
195 run_in_keystone_venv(ctx, client,
196 [ 'pip', 'install',
197 'python-openstackclient==5.2.1',
198 'osc-lib==2.0.0'
199 ])
200 try:
201 yield
202 finally:
203 pass
204
205 @contextlib.contextmanager
206 def configure_instance(ctx, config):
207 assert isinstance(config, dict)
208 log.info('Configuring keystone...')
209
210 kdir = get_keystone_dir(ctx)
211 keyrepo_dir = '{kdir}/etc/fernet-keys'.format(kdir=kdir)
212 for (client, _) in config.items():
213 # prepare the config file
214 run_in_keystone_dir(ctx, client,
215 [
216 'source',
217 f'{get_toxvenv_dir(ctx)}/bin/activate',
218 run.Raw('&&'),
219 'tox', '-e', 'genconfig'
220 ])
221 run_in_keystone_dir(ctx, client,
222 [
223 'cp', '-f',
224 'etc/keystone.conf.sample',
225 'etc/keystone.conf'
226 ])
227 run_in_keystone_dir(ctx, client,
228 [
229 'sed',
230 '-e', 's^#key_repository =.*^key_repository = {kr}^'.format(kr = keyrepo_dir),
231 '-i', 'etc/keystone.conf'
232 ])
233 run_in_keystone_dir(ctx, client,
234 [
235 'sed',
236 '-e', 's^#connection =.*^connection = mysql+pymysql://keystone:SECRET@localhost/keystone^',
237 '-i', 'etc/keystone.conf'
238 ])
239 # log to a file that gets archived
240 log_file = '{p}/archive/keystone.{c}.log'.format(p=teuthology.get_testdir(ctx), c=client)
241 run_in_keystone_dir(ctx, client,
242 [
243 'sed',
244 '-e', 's^#log_file =.*^log_file = {}^'.format(log_file),
245 '-i', 'etc/keystone.conf'
246 ])
247 # copy the config to archive
248 run_in_keystone_dir(ctx, client, [
249 'cp', 'etc/keystone.conf',
250 '{}/archive/keystone.{}.conf'.format(teuthology.get_testdir(ctx), client)
251 ])
252
253 conf_file = '{kdir}/etc/keystone.conf'.format(kdir=get_keystone_dir(ctx))
254
255 # prepare key repository for Fetnet token authenticator
256 run_in_keystone_dir(ctx, client, [ 'mkdir', '-p', keyrepo_dir ])
257 run_in_keystone_venv(ctx, client, [ 'keystone-manage', '--config-file', conf_file, 'fernet_setup' ])
258
259 # sync database
260 run_in_keystone_venv(ctx, client, [ 'keystone-manage', '--config-file', conf_file, 'db_sync' ])
261 yield
262
263 @contextlib.contextmanager
264 def run_keystone(ctx, config):
265 assert isinstance(config, dict)
266 log.info('Configuring keystone...')
267
268 conf_file = '{kdir}/etc/keystone.conf'.format(kdir=get_keystone_dir(ctx))
269
270 for (client, _) in config.items():
271 (remote,) = ctx.cluster.only(client).remotes.keys()
272 cluster_name, _, client_id = teuthology.split_role(client)
273
274 # start the public endpoint
275 client_public_with_id = 'keystone.public' + '.' + client_id
276
277 public_host, public_port = ctx.keystone.public_endpoints[client]
278 run_cmd = get_keystone_venved_cmd(ctx, 'keystone-wsgi-public',
279 [ '--host', public_host, '--port', str(public_port),
280 # Let's put the Keystone in background, wait for EOF
281 # and after receiving it, send SIGTERM to the daemon.
282 # This crazy hack is because Keystone, in contrast to
283 # our other daemons, doesn't quit on stdin.close().
284 # Teuthology relies on this behaviour.
285 run.Raw('& { read; kill %1; }')
286 ],
287 [
288 run.Raw('OS_KEYSTONE_CONFIG_FILES={}'.format(conf_file)),
289 ],
290 )
291 ctx.daemons.add_daemon(
292 remote, 'keystone', client_public_with_id,
293 cluster=cluster_name,
294 args=run_cmd,
295 logger=log.getChild(client),
296 stdin=run.PIPE,
297 wait=False,
298 check_status=False,
299 )
300
301 # sleep driven synchronization
302 run_in_keystone_venv(ctx, client, [ 'sleep', '15' ])
303 try:
304 yield
305 finally:
306 log.info('Stopping Keystone public instance')
307 ctx.daemons.get_daemon('keystone', client_public_with_id,
308 cluster_name).stop()
309
310
311 def dict_to_args(specials, items):
312 """
313 Transform
314 [(key1, val1), (special, val_special), (key3, val3) ]
315 into:
316 [ '--key1', 'val1', '--key3', 'val3', 'val_special' ]
317 """
318 args = []
319 special_vals = OrderedDict((k, '') for k in specials.split(','))
320 for (k, v) in items:
321 if k in special_vals:
322 special_vals[k] = v
323 else:
324 args.append('--{k}'.format(k=k))
325 args.append(v)
326 args.extend(arg for arg in special_vals.values() if arg)
327 return args
328
329 def run_section_cmds(ctx, cclient, section_cmd, specials,
330 section_config_list):
331 public_host, public_port = ctx.keystone.public_endpoints[cclient]
332
333 auth_section = [
334 ( 'os-username', 'admin' ),
335 ( 'os-password', 'ADMIN' ),
336 ( 'os-user-domain-id', 'default' ),
337 ( 'os-project-name', 'admin' ),
338 ( 'os-project-domain-id', 'default' ),
339 ( 'os-identity-api-version', '3' ),
340 ( 'os-auth-url', 'http://{host}:{port}/v3'.format(host=public_host,
341 port=public_port) ),
342 ]
343
344 for section_item in section_config_list:
345 run_in_keystone_venv(ctx, cclient,
346 [ 'openstack' ] + section_cmd.split() +
347 dict_to_args(specials, auth_section + list(section_item.items())) +
348 [ '--debug' ])
349
350 def create_endpoint(ctx, cclient, service, url, adminurl=None):
351 endpoint_sections = [
352 {'service': service, 'interface': 'public', 'url': url},
353 ]
354 if adminurl:
355 endpoint_sections.append(
356 {'service': service, 'interface': 'admin', 'url': adminurl}
357 )
358 run_section_cmds(ctx, cclient, 'endpoint create',
359 'service,interface,url',
360 endpoint_sections)
361
362 @contextlib.contextmanager
363 def fill_keystone(ctx, config):
364 assert isinstance(config, dict)
365
366 for (cclient, cconfig) in config.items():
367 public_host, public_port = ctx.keystone.public_endpoints[cclient]
368 url = 'http://{host}:{port}/v3'.format(host=public_host,
369 port=public_port)
370 opts = {'password': 'ADMIN',
371 'region-id': 'RegionOne',
372 'internal-url': url,
373 'admin-url': url,
374 'public-url': url}
375 bootstrap_args = chain.from_iterable(('--bootstrap-{}'.format(k), v)
376 for k, v in opts.items())
377 conf_file = '{kdir}/etc/keystone.conf'.format(kdir=get_keystone_dir(ctx))
378 run_in_keystone_venv(ctx, cclient,
379 ['keystone-manage', '--config-file', conf_file, 'bootstrap'] +
380 list(bootstrap_args))
381
382 # configure tenants/projects
383 run_section_cmds(ctx, cclient, 'domain create --or-show', 'name',
384 cconfig.get('domains', []))
385 run_section_cmds(ctx, cclient, 'project create --or-show', 'name',
386 cconfig.get('projects', []))
387 run_section_cmds(ctx, cclient, 'user create --or-show', 'name',
388 cconfig.get('users', []))
389 run_section_cmds(ctx, cclient, 'role create --or-show', 'name',
390 cconfig.get('roles', []))
391 run_section_cmds(ctx, cclient, 'role add', 'name',
392 cconfig.get('role-mappings', []))
393 run_section_cmds(ctx, cclient, 'service create', 'type',
394 cconfig.get('services', []))
395
396 # for the deferred endpoint creation; currently it's used in rgw.py
397 ctx.keystone.create_endpoint = create_endpoint
398
399 # sleep driven synchronization -- just in case
400 run_in_keystone_venv(ctx, cclient, [ 'sleep', '3' ])
401 try:
402 yield
403 finally:
404 pass
405
406 def assign_ports(ctx, config, initial_port):
407 """
408 Assign port numbers starting from @initial_port
409 """
410 port = initial_port
411 role_endpoints = {}
412 for remote, roles_for_host in ctx.cluster.remotes.items():
413 for role in roles_for_host:
414 if role in config:
415 role_endpoints[role] = (remote.name.split('@')[1], port)
416 port += 1
417
418 return role_endpoints
419
420 @contextlib.contextmanager
421 def task(ctx, config):
422 """
423 Deploy and configure Keystone
424
425 Example of configuration:
426
427 - install:
428 - ceph:
429 - tox: [ client.0 ]
430 - keystone:
431 client.0:
432 force-branch: master
433 domains:
434 - name: custom
435 description: Custom domain
436 projects:
437 - name: custom
438 description: Custom project
439 users:
440 - name: custom
441 password: SECRET
442 project: custom
443 roles: [ name: custom ]
444 role-mappings:
445 - name: custom
446 user: custom
447 project: custom
448 services:
449 - name: swift
450 type: object-store
451 description: Swift Service
452 """
453 assert config is None or isinstance(config, list) \
454 or isinstance(config, dict), \
455 "task keystone only supports a list or dictionary for configuration"
456
457 if not hasattr(ctx, 'tox'):
458 raise ConfigError('keystone must run after the tox task')
459
460 all_clients = ['client.{id}'.format(id=id_)
461 for id_ in teuthology.all_roles_of_type(ctx.cluster, 'client')]
462 if config is None:
463 config = all_clients
464 if isinstance(config, list):
465 config = dict.fromkeys(config)
466
467 log.debug('Keystone config is %s', config)
468
469 ctx.keystone = argparse.Namespace()
470 ctx.keystone.public_endpoints = assign_ports(ctx, config, 5000)
471
472 with contextutil.nested(
473 lambda: download(ctx=ctx, config=config),
474 lambda: install_packages(ctx=ctx, config=config),
475 lambda: setup_database(ctx=ctx, config=config),
476 lambda: setup_venv(ctx=ctx, config=config),
477 lambda: configure_instance(ctx=ctx, config=config),
478 lambda: run_keystone(ctx=ctx, config=config),
479 lambda: fill_keystone(ctx=ctx, config=config),
480 ):
481 yield