]>
Commit | Line | Data |
---|---|---|
11fdf7f2 TL |
1 | """ |
2 | Deploy and configure Keystone for Teuthology | |
3 | """ | |
4 | import argparse | |
5 | import contextlib | |
6 | import logging | |
e306af50 TL |
7 | |
8 | # still need this for python3.6 | |
9 | from collections import OrderedDict | |
10 | from itertools import chain | |
11fdf7f2 TL |
11 | |
12 | from teuthology import misc as teuthology | |
13 | from teuthology import contextutil | |
14 | from teuthology.orchestra import run | |
11fdf7f2 TL |
15 | from teuthology.packaging import install_package |
16 | from teuthology.packaging import remove_package | |
9f95a23c | 17 | from teuthology.exceptions import ConfigError |
11fdf7f2 TL |
18 | |
19 | log = logging.getLogger(__name__) | |
20 | ||
21 | ||
11fdf7f2 TL |
22 | def get_keystone_dir(ctx): |
23 | return '{tdir}/keystone'.format(tdir=teuthology.get_testdir(ctx)) | |
24 | ||
9f95a23c TL |
25 | def run_in_keystone_dir(ctx, client, args, **kwargs): |
26 | return ctx.cluster.only(client).run( | |
11fdf7f2 | 27 | args=[ 'cd', get_keystone_dir(ctx), run.Raw('&&'), ] + args, |
9f95a23c TL |
28 | **kwargs |
29 | ) | |
30 | ||
31 | def get_toxvenv_dir(ctx): | |
32 | return ctx.tox.venv_path | |
33 | ||
e306af50 TL |
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) | |
11fdf7f2 TL |
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 | ||
05a536ef | 45 | def get_keystone_venved_cmd(ctx, cmd, args, env=[]): |
11fdf7f2 | 46 | kbindir = get_keystone_dir(ctx) + '/.tox/venv/bin/' |
05a536ef | 47 | return env + [ kbindir + 'python', kbindir + cmd ] + args |
11fdf7f2 | 48 | |
11fdf7f2 TL |
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 | ||
f67539c2 TL |
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 | ||
9f95a23c TL |
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 | ||
f67539c2 TL |
123 | patch_bindep = patch_bindep_template \ |
124 | .replace("{keystone_dir}", get_keystone_dir(ctx)) | |
9f95a23c TL |
125 | packages = {} |
126 | for (client, _) in config.items(): | |
127 | (remote,) = ctx.cluster.only(client).remotes.keys() | |
f67539c2 | 128 | toxvenv_sh(ctx, remote, ['python'], stdin=patch_bindep) |
9f95a23c | 129 | # use bindep to read which dependencies we need from keystone/bindep.txt |
e306af50 TL |
130 | toxvenv_sh(ctx, remote, ['pip', 'install', 'bindep']) |
131 | packages[client] = toxvenv_sh(ctx, remote, | |
9f95a23c | 132 | ['bindep', '--brief', '--file', '{}/bindep.txt'.format(get_keystone_dir(ctx))], |
e306af50 | 133 | check_status=False).splitlines() # returns 1 on success? |
9f95a23c TL |
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 | ||
05a536ef TL |
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 | ||
11fdf7f2 TL |
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(): | |
05a536ef TL |
185 | run_in_keystone_dir(ctx, client, |
186 | ['sed', '-i', 's/usedevelop.*/usedevelop=false/g', 'tox.ini']) | |
187 | ||
11fdf7f2 TL |
188 | run_in_keystone_dir(ctx, client, |
189 | [ 'source', | |
9f95a23c | 190 | '{tvdir}/bin/activate'.format(tvdir=get_toxvenv_dir(ctx)), |
11fdf7f2 TL |
191 | run.Raw('&&'), |
192 | 'tox', '-e', 'venv', '--notest' | |
193 | ]) | |
194 | ||
195 | run_in_keystone_venv(ctx, client, | |
f67539c2 TL |
196 | [ 'pip', 'install', |
197 | 'python-openstackclient==5.2.1', | |
198 | 'osc-lib==2.0.0' | |
199 | ]) | |
11fdf7f2 TL |
200 | try: |
201 | yield | |
202 | finally: | |
9f95a23c | 203 | pass |
11fdf7f2 TL |
204 | |
205 | @contextlib.contextmanager | |
206 | def configure_instance(ctx, config): | |
207 | assert isinstance(config, dict) | |
208 | log.info('Configuring keystone...') | |
209 | ||
05a536ef TL |
210 | kdir = get_keystone_dir(ctx) |
211 | keyrepo_dir = '{kdir}/etc/fernet-keys'.format(kdir=kdir) | |
11fdf7f2 TL |
212 | for (client, _) in config.items(): |
213 | # prepare the config file | |
214 | run_in_keystone_dir(ctx, client, | |
215 | [ | |
e306af50 TL |
216 | 'source', |
217 | f'{get_toxvenv_dir(ctx)}/bin/activate', | |
218 | run.Raw('&&'), | |
219 | 'tox', '-e', 'genconfig' | |
11fdf7f2 TL |
220 | ]) |
221 | run_in_keystone_dir(ctx, client, | |
222 | [ | |
e306af50 TL |
223 | 'cp', '-f', |
224 | 'etc/keystone.conf.sample', | |
225 | 'etc/keystone.conf' | |
11fdf7f2 TL |
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 | ]) | |
05a536ef TL |
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 | ]) | |
9f95a23c TL |
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 | ]) | |
11fdf7f2 | 252 | |
05a536ef TL |
253 | conf_file = '{kdir}/etc/keystone.conf'.format(kdir=get_keystone_dir(ctx)) |
254 | ||
11fdf7f2 TL |
255 | # prepare key repository for Fetnet token authenticator |
256 | run_in_keystone_dir(ctx, client, [ 'mkdir', '-p', keyrepo_dir ]) | |
05a536ef | 257 | run_in_keystone_venv(ctx, client, [ 'keystone-manage', '--config-file', conf_file, 'fernet_setup' ]) |
11fdf7f2 TL |
258 | |
259 | # sync database | |
05a536ef | 260 | run_in_keystone_venv(ctx, client, [ 'keystone-manage', '--config-file', conf_file, 'db_sync' ]) |
11fdf7f2 TL |
261 | yield |
262 | ||
263 | @contextlib.contextmanager | |
264 | def run_keystone(ctx, config): | |
265 | assert isinstance(config, dict) | |
266 | log.info('Configuring keystone...') | |
267 | ||
05a536ef TL |
268 | conf_file = '{kdir}/etc/keystone.conf'.format(kdir=get_keystone_dir(ctx)) |
269 | ||
11fdf7f2 | 270 | for (client, _) in config.items(): |
9f95a23c | 271 | (remote,) = ctx.cluster.only(client).remotes.keys() |
11fdf7f2 TL |
272 | cluster_name, _, client_id = teuthology.split_role(client) |
273 | ||
274 | # start the public endpoint | |
275 | client_public_with_id = 'keystone.public' + '.' + client_id | |
11fdf7f2 TL |
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; }') | |
05a536ef TL |
286 | ], |
287 | [ | |
288 | run.Raw('OS_KEYSTONE_CONFIG_FILES={}'.format(conf_file)), | |
289 | ], | |
11fdf7f2 TL |
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, | |
11fdf7f2 TL |
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: | |
11fdf7f2 TL |
306 | log.info('Stopping Keystone public instance') |
307 | ctx.daemons.get_daemon('keystone', client_public_with_id, | |
308 | cluster_name).stop() | |
309 | ||
310 | ||
e306af50 | 311 | def dict_to_args(specials, items): |
11fdf7f2 TL |
312 | """ |
313 | Transform | |
314 | [(key1, val1), (special, val_special), (key3, val3) ] | |
315 | into: | |
316 | [ '--key1', 'val1', '--key3', 'val3', 'val_special' ] | |
317 | """ | |
e306af50 TL |
318 | args = [] |
319 | special_vals = OrderedDict((k, '') for k in specials.split(',')) | |
11fdf7f2 | 320 | for (k, v) in items: |
e306af50 TL |
321 | if k in special_vals: |
322 | special_vals[k] = v | |
11fdf7f2 TL |
323 | else: |
324 | args.append('--{k}'.format(k=k)) | |
325 | args.append(v) | |
e306af50 | 326 | args.extend(arg for arg in special_vals.values() if arg) |
11fdf7f2 TL |
327 | return args |
328 | ||
e306af50 | 329 | def run_section_cmds(ctx, cclient, section_cmd, specials, |
11fdf7f2 | 330 | section_config_list): |
05a536ef | 331 | public_host, public_port = ctx.keystone.public_endpoints[cclient] |
11fdf7f2 TL |
332 | |
333 | auth_section = [ | |
e306af50 TL |
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' ), | |
05a536ef TL |
340 | ( 'os-auth-url', 'http://{host}:{port}/v3'.format(host=public_host, |
341 | port=public_port) ), | |
11fdf7f2 TL |
342 | ] |
343 | ||
344 | for section_item in section_config_list: | |
345 | run_in_keystone_venv(ctx, cclient, | |
346 | [ 'openstack' ] + section_cmd.split() + | |
e306af50 | 347 | dict_to_args(specials, auth_section + list(section_item.items())) + |
9f95a23c | 348 | [ '--debug' ]) |
11fdf7f2 | 349 | |
9f95a23c | 350 | def create_endpoint(ctx, cclient, service, url, adminurl=None): |
e306af50 TL |
351 | endpoint_sections = [ |
352 | {'service': service, 'interface': 'public', 'url': url}, | |
353 | ] | |
9f95a23c | 354 | if adminurl: |
e306af50 TL |
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) | |
11fdf7f2 TL |
361 | |
362 | @contextlib.contextmanager | |
363 | def fill_keystone(ctx, config): | |
364 | assert isinstance(config, dict) | |
365 | ||
366 | for (cclient, cconfig) in config.items(): | |
e306af50 TL |
367 | public_host, public_port = ctx.keystone.public_endpoints[cclient] |
368 | url = 'http://{host}:{port}/v3'.format(host=public_host, | |
369 | port=public_port) | |
e306af50 | 370 | opts = {'password': 'ADMIN', |
e306af50 | 371 | 'region-id': 'RegionOne', |
f67539c2 | 372 | 'internal-url': url, |
05a536ef | 373 | 'admin-url': url, |
e306af50 TL |
374 | 'public-url': url} |
375 | bootstrap_args = chain.from_iterable(('--bootstrap-{}'.format(k), v) | |
376 | for k, v in opts.items()) | |
05a536ef | 377 | conf_file = '{kdir}/etc/keystone.conf'.format(kdir=get_keystone_dir(ctx)) |
e306af50 | 378 | run_in_keystone_venv(ctx, cclient, |
05a536ef | 379 | ['keystone-manage', '--config-file', conf_file, 'bootstrap'] + |
e306af50 TL |
380 | list(bootstrap_args)) |
381 | ||
11fdf7f2 | 382 | # configure tenants/projects |
05a536ef | 383 | run_section_cmds(ctx, cclient, 'domain create --or-show', 'name', |
e306af50 | 384 | cconfig.get('domains', [])) |
05a536ef | 385 | run_section_cmds(ctx, cclient, 'project create --or-show', 'name', |
e306af50 | 386 | cconfig.get('projects', [])) |
05a536ef | 387 | run_section_cmds(ctx, cclient, 'user create --or-show', 'name', |
e306af50 | 388 | cconfig.get('users', [])) |
05a536ef | 389 | run_section_cmds(ctx, cclient, 'role create --or-show', 'name', |
e306af50 | 390 | cconfig.get('roles', [])) |
11fdf7f2 | 391 | run_section_cmds(ctx, cclient, 'role add', 'name', |
e306af50 TL |
392 | cconfig.get('role-mappings', [])) |
393 | run_section_cmds(ctx, cclient, 'service create', 'type', | |
394 | cconfig.get('services', [])) | |
11fdf7f2 | 395 | |
11fdf7f2 TL |
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 = {} | |
9f95a23c | 412 | for remote, roles_for_host in ctx.cluster.remotes.items(): |
11fdf7f2 TL |
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 | |
e306af50 | 433 | domains: |
05a536ef TL |
434 | - name: custom |
435 | description: Custom domain | |
e306af50 | 436 | projects: |
05a536ef TL |
437 | - name: custom |
438 | description: Custom project | |
11fdf7f2 | 439 | users: |
05a536ef TL |
440 | - name: custom |
441 | password: SECRET | |
442 | project: custom | |
443 | roles: [ name: custom ] | |
11fdf7f2 | 444 | role-mappings: |
05a536ef TL |
445 | - name: custom |
446 | user: custom | |
447 | project: custom | |
11fdf7f2 | 448 | services: |
11fdf7f2 TL |
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 | ||
9f95a23c | 457 | if not hasattr(ctx, 'tox'): |
11fdf7f2 TL |
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) | |
11fdf7f2 TL |
471 | |
472 | with contextutil.nested( | |
11fdf7f2 | 473 | lambda: download(ctx=ctx, config=config), |
9f95a23c | 474 | lambda: install_packages(ctx=ctx, config=config), |
05a536ef | 475 | lambda: setup_database(ctx=ctx, config=config), |
11fdf7f2 TL |
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 |