]>
Commit | Line | Data |
---|---|---|
11fdf7f2 TL |
1 | """ |
2 | Deploy and configure Keystone for Teuthology | |
3 | """ | |
4 | import argparse | |
5 | import contextlib | |
6 | import logging | |
9f95a23c | 7 | from cStringIO import StringIO |
11fdf7f2 TL |
8 | |
9 | from teuthology import misc as teuthology | |
10 | from teuthology import contextutil | |
11 | from teuthology.orchestra import run | |
11fdf7f2 TL |
12 | from teuthology.packaging import install_package |
13 | from teuthology.packaging import remove_package | |
9f95a23c | 14 | from teuthology.exceptions import ConfigError |
11fdf7f2 TL |
15 | |
16 | log = logging.getLogger(__name__) | |
17 | ||
18 | ||
11fdf7f2 TL |
19 | def get_keystone_dir(ctx): |
20 | return '{tdir}/keystone'.format(tdir=teuthology.get_testdir(ctx)) | |
21 | ||
9f95a23c TL |
22 | def run_in_keystone_dir(ctx, client, args, **kwargs): |
23 | return ctx.cluster.only(client).run( | |
11fdf7f2 | 24 | args=[ 'cd', get_keystone_dir(ctx), run.Raw('&&'), ] + args, |
9f95a23c TL |
25 | **kwargs |
26 | ) | |
27 | ||
28 | def get_toxvenv_dir(ctx): | |
29 | return ctx.tox.venv_path | |
30 | ||
31 | def run_in_tox_venv(ctx, remote, args, **kwargs): | |
32 | return remote.run( | |
33 | args=[ 'source', '{}/bin/activate'.format(get_toxvenv_dir(ctx)), run.Raw('&&') ] + args, | |
34 | **kwargs | |
11fdf7f2 TL |
35 | ) |
36 | ||
37 | def run_in_keystone_venv(ctx, client, args): | |
38 | run_in_keystone_dir(ctx, client, | |
39 | [ 'source', | |
40 | '.tox/venv/bin/activate', | |
41 | run.Raw('&&') | |
42 | ] + args) | |
43 | ||
44 | def get_keystone_venved_cmd(ctx, cmd, args): | |
45 | kbindir = get_keystone_dir(ctx) + '/.tox/venv/bin/' | |
46 | return [ kbindir + 'python', kbindir + cmd ] + args | |
47 | ||
11fdf7f2 TL |
48 | @contextlib.contextmanager |
49 | def download(ctx, config): | |
50 | """ | |
51 | Download the Keystone from github. | |
52 | Remove downloaded file upon exit. | |
53 | ||
54 | The context passed in should be identical to the context | |
55 | passed in to the main task. | |
56 | """ | |
57 | assert isinstance(config, dict) | |
58 | log.info('Downloading keystone...') | |
59 | keystonedir = get_keystone_dir(ctx) | |
60 | ||
61 | for (client, cconf) in config.items(): | |
62 | ctx.cluster.only(client).run( | |
63 | args=[ | |
64 | 'git', 'clone', | |
65 | '-b', cconf.get('force-branch', 'master'), | |
66 | 'https://github.com/openstack/keystone.git', | |
67 | keystonedir, | |
68 | ], | |
69 | ) | |
70 | ||
71 | sha1 = cconf.get('sha1') | |
72 | if sha1 is not None: | |
73 | run_in_keystone_dir(ctx, client, [ | |
74 | 'git', 'reset', '--hard', sha1, | |
75 | ], | |
76 | ) | |
77 | ||
78 | # hax for http://tracker.ceph.com/issues/23659 | |
79 | run_in_keystone_dir(ctx, client, [ | |
80 | 'sed', '-i', | |
81 | 's/pysaml2<4.0.3,>=2.4.0/pysaml2>=4.5.0/', | |
82 | 'requirements.txt' | |
83 | ], | |
84 | ) | |
85 | try: | |
86 | yield | |
87 | finally: | |
88 | log.info('Removing keystone...') | |
89 | for client in config: | |
90 | ctx.cluster.only(client).run( | |
91 | args=[ 'rm', '-rf', keystonedir ], | |
92 | ) | |
93 | ||
9f95a23c TL |
94 | @contextlib.contextmanager |
95 | def install_packages(ctx, config): | |
96 | """ | |
97 | Download the packaged dependencies of Keystone. | |
98 | Remove install packages upon exit. | |
99 | ||
100 | The context passed in should be identical to the context | |
101 | passed in to the main task. | |
102 | """ | |
103 | assert isinstance(config, dict) | |
104 | log.info('Installing packages for Keystone...') | |
105 | ||
106 | packages = {} | |
107 | for (client, _) in config.items(): | |
108 | (remote,) = ctx.cluster.only(client).remotes.keys() | |
109 | # use bindep to read which dependencies we need from keystone/bindep.txt | |
110 | run_in_tox_venv(ctx, remote, ['pip', 'install', 'bindep']) | |
111 | r = run_in_tox_venv(ctx, remote, | |
112 | ['bindep', '--brief', '--file', '{}/bindep.txt'.format(get_keystone_dir(ctx))], | |
113 | stdout=StringIO(), | |
114 | check_status=False) # returns 1 on success? | |
115 | packages[client] = r.stdout.getvalue().splitlines() | |
116 | for dep in packages[client]: | |
117 | install_package(dep, remote) | |
118 | try: | |
119 | yield | |
120 | finally: | |
121 | log.info('Removing packaged dependencies of Keystone...') | |
122 | ||
123 | for (client, _) in config.items(): | |
124 | (remote,) = ctx.cluster.only(client).remotes.keys() | |
125 | for dep in packages[client]: | |
126 | remove_package(dep, remote) | |
127 | ||
11fdf7f2 TL |
128 | @contextlib.contextmanager |
129 | def setup_venv(ctx, config): | |
130 | """ | |
131 | Setup the virtualenv for Keystone using tox. | |
132 | """ | |
133 | assert isinstance(config, dict) | |
134 | log.info('Setting up virtualenv for keystone...') | |
135 | for (client, _) in config.items(): | |
136 | run_in_keystone_dir(ctx, client, | |
137 | [ 'source', | |
9f95a23c | 138 | '{tvdir}/bin/activate'.format(tvdir=get_toxvenv_dir(ctx)), |
11fdf7f2 TL |
139 | run.Raw('&&'), |
140 | 'tox', '-e', 'venv', '--notest' | |
141 | ]) | |
142 | ||
143 | run_in_keystone_venv(ctx, client, | |
9f95a23c TL |
144 | [ 'pip', 'install', 'python-openstackclient<=3.19.0', |
145 | '-r', 'requirements.txt' | |
146 | ]) | |
11fdf7f2 TL |
147 | try: |
148 | yield | |
149 | finally: | |
9f95a23c | 150 | pass |
11fdf7f2 TL |
151 | |
152 | @contextlib.contextmanager | |
153 | def configure_instance(ctx, config): | |
154 | assert isinstance(config, dict) | |
155 | log.info('Configuring keystone...') | |
156 | ||
157 | keyrepo_dir = '{kdir}/etc/fernet-keys'.format(kdir=get_keystone_dir(ctx)) | |
158 | for (client, _) in config.items(): | |
159 | # prepare the config file | |
160 | run_in_keystone_dir(ctx, client, | |
161 | [ | |
162 | 'cp', '-f', | |
163 | 'etc/keystone.conf.sample', | |
164 | 'etc/keystone.conf' | |
165 | ]) | |
166 | run_in_keystone_dir(ctx, client, | |
167 | [ | |
168 | 'sed', | |
169 | '-e', 's/#admin_token =.*/admin_token = ADMIN/', | |
170 | '-i', 'etc/keystone.conf' | |
171 | ]) | |
172 | run_in_keystone_dir(ctx, client, | |
173 | [ | |
174 | 'sed', | |
175 | '-e', 's^#key_repository =.*^key_repository = {kr}^'.format(kr = keyrepo_dir), | |
176 | '-i', 'etc/keystone.conf' | |
177 | ]) | |
9f95a23c TL |
178 | # log to a file that gets archived |
179 | log_file = '{p}/archive/keystone.{c}.log'.format(p=teuthology.get_testdir(ctx), c=client) | |
180 | run_in_keystone_dir(ctx, client, | |
181 | [ | |
182 | 'sed', | |
183 | '-e', 's^#log_file =.*^log_file = {}^'.format(log_file), | |
184 | '-i', 'etc/keystone.conf' | |
185 | ]) | |
186 | # copy the config to archive | |
187 | run_in_keystone_dir(ctx, client, [ | |
188 | 'cp', 'etc/keystone.conf', | |
189 | '{}/archive/keystone.{}.conf'.format(teuthology.get_testdir(ctx), client) | |
190 | ]) | |
11fdf7f2 TL |
191 | |
192 | # prepare key repository for Fetnet token authenticator | |
193 | run_in_keystone_dir(ctx, client, [ 'mkdir', '-p', keyrepo_dir ]) | |
194 | run_in_keystone_venv(ctx, client, [ 'keystone-manage', 'fernet_setup' ]) | |
195 | ||
196 | # sync database | |
197 | run_in_keystone_venv(ctx, client, [ 'keystone-manage', 'db_sync' ]) | |
198 | yield | |
199 | ||
200 | @contextlib.contextmanager | |
201 | def run_keystone(ctx, config): | |
202 | assert isinstance(config, dict) | |
203 | log.info('Configuring keystone...') | |
204 | ||
205 | for (client, _) in config.items(): | |
9f95a23c | 206 | (remote,) = ctx.cluster.only(client).remotes.keys() |
11fdf7f2 TL |
207 | cluster_name, _, client_id = teuthology.split_role(client) |
208 | ||
209 | # start the public endpoint | |
210 | client_public_with_id = 'keystone.public' + '.' + client_id | |
11fdf7f2 TL |
211 | |
212 | public_host, public_port = ctx.keystone.public_endpoints[client] | |
213 | run_cmd = get_keystone_venved_cmd(ctx, 'keystone-wsgi-public', | |
214 | [ '--host', public_host, '--port', str(public_port), | |
215 | # Let's put the Keystone in background, wait for EOF | |
216 | # and after receiving it, send SIGTERM to the daemon. | |
217 | # This crazy hack is because Keystone, in contrast to | |
218 | # our other daemons, doesn't quit on stdin.close(). | |
219 | # Teuthology relies on this behaviour. | |
220 | run.Raw('& { read; kill %1; }') | |
221 | ] | |
222 | ) | |
223 | ctx.daemons.add_daemon( | |
224 | remote, 'keystone', client_public_with_id, | |
225 | cluster=cluster_name, | |
226 | args=run_cmd, | |
227 | logger=log.getChild(client), | |
228 | stdin=run.PIPE, | |
229 | cwd=get_keystone_dir(ctx), | |
230 | wait=False, | |
231 | check_status=False, | |
232 | ) | |
233 | ||
234 | # start the admin endpoint | |
235 | client_admin_with_id = 'keystone.admin' + '.' + client_id | |
236 | ||
237 | admin_host, admin_port = ctx.keystone.admin_endpoints[client] | |
238 | run_cmd = get_keystone_venved_cmd(ctx, 'keystone-wsgi-admin', | |
239 | [ '--host', admin_host, '--port', str(admin_port), | |
240 | run.Raw('& { read; kill %1; }') | |
241 | ] | |
242 | ) | |
243 | ctx.daemons.add_daemon( | |
244 | remote, 'keystone', client_admin_with_id, | |
245 | cluster=cluster_name, | |
246 | args=run_cmd, | |
247 | logger=log.getChild(client), | |
248 | stdin=run.PIPE, | |
249 | cwd=get_keystone_dir(ctx), | |
250 | wait=False, | |
251 | check_status=False, | |
252 | ) | |
253 | ||
254 | # sleep driven synchronization | |
255 | run_in_keystone_venv(ctx, client, [ 'sleep', '15' ]) | |
256 | try: | |
257 | yield | |
258 | finally: | |
259 | log.info('Stopping Keystone admin instance') | |
260 | ctx.daemons.get_daemon('keystone', client_admin_with_id, | |
261 | cluster_name).stop() | |
262 | ||
263 | log.info('Stopping Keystone public instance') | |
264 | ctx.daemons.get_daemon('keystone', client_public_with_id, | |
265 | cluster_name).stop() | |
266 | ||
267 | ||
268 | def dict_to_args(special, items): | |
269 | """ | |
270 | Transform | |
271 | [(key1, val1), (special, val_special), (key3, val3) ] | |
272 | into: | |
273 | [ '--key1', 'val1', '--key3', 'val3', 'val_special' ] | |
274 | """ | |
275 | args=[] | |
276 | for (k, v) in items: | |
277 | if k == special: | |
278 | special_val = v | |
279 | else: | |
280 | args.append('--{k}'.format(k=k)) | |
281 | args.append(v) | |
282 | if special_val: | |
283 | args.append(special_val) | |
284 | return args | |
285 | ||
286 | def run_section_cmds(ctx, cclient, section_cmd, special, | |
287 | section_config_list): | |
288 | admin_host, admin_port = ctx.keystone.admin_endpoints[cclient] | |
289 | ||
290 | auth_section = [ | |
291 | ( 'os-token', 'ADMIN' ), | |
9f95a23c | 292 | ( 'os-identity-api-version', '2.0' ), |
11fdf7f2 TL |
293 | ( 'os-url', 'http://{host}:{port}/v2.0'.format(host=admin_host, |
294 | port=admin_port) ), | |
295 | ] | |
296 | ||
297 | for section_item in section_config_list: | |
298 | run_in_keystone_venv(ctx, cclient, | |
299 | [ 'openstack' ] + section_cmd.split() + | |
9f95a23c TL |
300 | dict_to_args(special, auth_section + section_item.items()) + |
301 | [ '--debug' ]) | |
11fdf7f2 | 302 | |
9f95a23c | 303 | def create_endpoint(ctx, cclient, service, url, adminurl=None): |
11fdf7f2 TL |
304 | endpoint_section = { |
305 | 'service': service, | |
306 | 'publicurl': url, | |
307 | } | |
9f95a23c TL |
308 | if adminurl: |
309 | endpoint_section.update( { | |
310 | 'adminurl': adminurl, | |
311 | } ) | |
11fdf7f2 TL |
312 | return run_section_cmds(ctx, cclient, 'endpoint create', 'service', |
313 | [ endpoint_section ]) | |
314 | ||
315 | @contextlib.contextmanager | |
316 | def fill_keystone(ctx, config): | |
317 | assert isinstance(config, dict) | |
318 | ||
319 | for (cclient, cconfig) in config.items(): | |
320 | # configure tenants/projects | |
321 | run_section_cmds(ctx, cclient, 'project create', 'name', | |
322 | cconfig['tenants']) | |
323 | run_section_cmds(ctx, cclient, 'user create', 'name', | |
324 | cconfig['users']) | |
325 | run_section_cmds(ctx, cclient, 'role create', 'name', | |
326 | cconfig['roles']) | |
327 | run_section_cmds(ctx, cclient, 'role add', 'name', | |
328 | cconfig['role-mappings']) | |
329 | run_section_cmds(ctx, cclient, 'service create', 'name', | |
330 | cconfig['services']) | |
331 | ||
332 | public_host, public_port = ctx.keystone.public_endpoints[cclient] | |
333 | url = 'http://{host}:{port}/v2.0'.format(host=public_host, | |
334 | port=public_port) | |
9f95a23c TL |
335 | admin_host, admin_port = ctx.keystone.admin_endpoints[cclient] |
336 | admin_url = 'http://{host}:{port}/v2.0'.format(host=admin_host, | |
337 | port=admin_port) | |
338 | create_endpoint(ctx, cclient, 'keystone', url, admin_url) | |
11fdf7f2 TL |
339 | # for the deferred endpoint creation; currently it's used in rgw.py |
340 | ctx.keystone.create_endpoint = create_endpoint | |
341 | ||
342 | # sleep driven synchronization -- just in case | |
343 | run_in_keystone_venv(ctx, cclient, [ 'sleep', '3' ]) | |
344 | try: | |
345 | yield | |
346 | finally: | |
347 | pass | |
348 | ||
349 | def assign_ports(ctx, config, initial_port): | |
350 | """ | |
351 | Assign port numbers starting from @initial_port | |
352 | """ | |
353 | port = initial_port | |
354 | role_endpoints = {} | |
9f95a23c | 355 | for remote, roles_for_host in ctx.cluster.remotes.items(): |
11fdf7f2 TL |
356 | for role in roles_for_host: |
357 | if role in config: | |
358 | role_endpoints[role] = (remote.name.split('@')[1], port) | |
359 | port += 1 | |
360 | ||
361 | return role_endpoints | |
362 | ||
363 | @contextlib.contextmanager | |
364 | def task(ctx, config): | |
365 | """ | |
366 | Deploy and configure Keystone | |
367 | ||
368 | Example of configuration: | |
369 | ||
370 | - install: | |
371 | - ceph: | |
372 | - tox: [ client.0 ] | |
373 | - keystone: | |
374 | client.0: | |
375 | force-branch: master | |
376 | tenants: | |
377 | - name: admin | |
378 | description: Admin Tenant | |
379 | users: | |
380 | - name: admin | |
381 | password: ADMIN | |
382 | project: admin | |
383 | roles: [ name: admin, name: Member ] | |
384 | role-mappings: | |
385 | - name: admin | |
386 | user: admin | |
387 | project: admin | |
388 | services: | |
389 | - name: keystone | |
390 | type: identity | |
391 | description: Keystone Identity Service | |
392 | - name: swift | |
393 | type: object-store | |
394 | description: Swift Service | |
395 | """ | |
396 | assert config is None or isinstance(config, list) \ | |
397 | or isinstance(config, dict), \ | |
398 | "task keystone only supports a list or dictionary for configuration" | |
399 | ||
9f95a23c | 400 | if not hasattr(ctx, 'tox'): |
11fdf7f2 TL |
401 | raise ConfigError('keystone must run after the tox task') |
402 | ||
403 | all_clients = ['client.{id}'.format(id=id_) | |
404 | for id_ in teuthology.all_roles_of_type(ctx.cluster, 'client')] | |
405 | if config is None: | |
406 | config = all_clients | |
407 | if isinstance(config, list): | |
408 | config = dict.fromkeys(config) | |
409 | ||
410 | log.debug('Keystone config is %s', config) | |
411 | ||
412 | ctx.keystone = argparse.Namespace() | |
413 | ctx.keystone.public_endpoints = assign_ports(ctx, config, 5000) | |
414 | ctx.keystone.admin_endpoints = assign_ports(ctx, config, 35357) | |
415 | ||
416 | with contextutil.nested( | |
11fdf7f2 | 417 | lambda: download(ctx=ctx, config=config), |
9f95a23c | 418 | lambda: install_packages(ctx=ctx, config=config), |
11fdf7f2 TL |
419 | lambda: setup_venv(ctx=ctx, config=config), |
420 | lambda: configure_instance(ctx=ctx, config=config), | |
421 | lambda: run_keystone(ctx=ctx, config=config), | |
422 | lambda: fill_keystone(ctx=ctx, config=config), | |
423 | ): | |
424 | yield |