]>
Commit | Line | Data |
---|---|---|
9f95a23c TL |
1 | """ |
2 | Deploy and configure Vault for Teuthology | |
3 | """ | |
4 | ||
5 | import argparse | |
6 | import contextlib | |
7 | import logging | |
8 | import time | |
9 | import httplib | |
10 | import json | |
11 | from os import path | |
12 | from urlparse import urljoin | |
13 | ||
14 | from teuthology import misc as teuthology | |
15 | from teuthology import contextutil | |
16 | from teuthology.orchestra import run | |
17 | from teuthology.exceptions import ConfigError | |
18 | ||
19 | ||
20 | log = logging.getLogger(__name__) | |
21 | ||
22 | ||
23 | def assign_ports(ctx, config, initial_port): | |
24 | """ | |
25 | Assign port numbers starting from @initial_port | |
26 | """ | |
27 | port = initial_port | |
28 | role_endpoints = {} | |
29 | for remote, roles_for_host in ctx.cluster.remotes.items(): | |
30 | for role in roles_for_host: | |
31 | if role in config: | |
32 | role_endpoints[role] = (remote.name.split('@')[1], port) | |
33 | port += 1 | |
34 | ||
35 | return role_endpoints | |
36 | ||
37 | ||
38 | @contextlib.contextmanager | |
39 | def download(ctx, config): | |
40 | """ | |
41 | Download Vault Release from Hashicopr website. | |
42 | Remove downloaded file upon exit. | |
43 | """ | |
44 | assert isinstance(config, dict) | |
45 | log.info('Downloading Vault...') | |
46 | testdir = teuthology.get_testdir(ctx) | |
47 | ||
48 | for (client, cconf) in config.items(): | |
49 | install_url = cconf.get('install_url') | |
50 | install_sha256 = cconf.get('install_sha256') | |
51 | if not install_url or not install_sha256: | |
52 | raise ConfigError("Missing Vault install_url and/or install_sha256") | |
53 | install_zip = path.join(testdir, 'vault.zip') | |
54 | install_dir = path.join(testdir, 'vault') | |
55 | ||
56 | log.info('Downloading Vault...') | |
57 | ctx.cluster.only(client).run( | |
58 | args=['curl', '-L', install_url, '-o', install_zip]) | |
59 | ||
60 | log.info('Verifying SHA256 signature...') | |
61 | ctx.cluster.only(client).run( | |
62 | args=['echo', ' '.join([install_sha256, install_zip]), run.Raw('|'), | |
63 | 'sha256sum', '--check', '--status']) | |
64 | ||
65 | log.info('Extracting vault...') | |
66 | ctx.cluster.only(client).run(args=['mkdir', '-p', install_dir]) | |
67 | # Using python in case unzip is not installed on hosts | |
68 | ctx.cluster.only(client).run( | |
69 | args=['python', '-m', 'zipfile', '-e', install_zip, install_dir]) | |
70 | ||
71 | try: | |
72 | yield | |
73 | finally: | |
74 | log.info('Removing Vault...') | |
75 | testdir = teuthology.get_testdir(ctx) | |
76 | for client in config: | |
77 | ctx.cluster.only(client).run( | |
78 | args=['rm', '-rf', install_dir, install_zip]) | |
79 | ||
80 | ||
81 | def get_vault_dir(ctx): | |
82 | return '{tdir}/vault'.format(tdir=teuthology.get_testdir(ctx)) | |
83 | ||
84 | ||
85 | @contextlib.contextmanager | |
86 | def run_vault(ctx, config): | |
87 | assert isinstance(config, dict) | |
88 | ||
89 | for (client, cconf) in config.items(): | |
90 | (remote,) = ctx.cluster.only(client).remotes.keys() | |
91 | cluster_name, _, client_id = teuthology.split_role(client) | |
92 | ||
93 | _, port = ctx.vault.endpoints[client] | |
94 | listen_addr = "0.0.0.0:{}".format(port) | |
95 | ||
96 | root_token = ctx.vault.root_token = cconf.get('root_token', 'root') | |
97 | ||
98 | log.info("Starting Vault listening on %s ...", listen_addr) | |
99 | v_params = [ | |
100 | '-dev', | |
101 | '-dev-listen-address={}'.format(listen_addr), | |
102 | '-dev-no-store-token', | |
103 | '-dev-root-token-id={}'.format(root_token) | |
104 | ] | |
105 | ||
106 | cmd = "chmod +x {vdir}/vault && {vdir}/vault server {vargs}".format(vdir=get_vault_dir(ctx), vargs=" ".join(v_params)) | |
107 | ||
108 | ctx.daemons.add_daemon( | |
109 | remote, 'vault', client_id, | |
110 | cluster=cluster_name, | |
111 | args=['bash', '-c', cmd, run.Raw('& { read; kill %1; }')], | |
112 | logger=log.getChild(client), | |
113 | stdin=run.PIPE, | |
114 | cwd=get_vault_dir(ctx), | |
115 | wait=False, | |
116 | check_status=False, | |
117 | ) | |
118 | time.sleep(10) | |
119 | try: | |
120 | yield | |
121 | finally: | |
122 | log.info('Stopping Vault instance') | |
123 | ctx.daemons.get_daemon('vault', client_id, cluster_name).stop() | |
124 | ||
125 | ||
126 | @contextlib.contextmanager | |
127 | def setup_vault(ctx, config): | |
128 | """ | |
129 | Mount Transit or KV version 2 secrets engine | |
130 | """ | |
131 | (cclient, cconfig) = config.items()[0] | |
132 | engine = cconfig.get('engine') | |
133 | ||
134 | if engine == 'kv': | |
135 | log.info('Mounting kv version 2 secrets engine') | |
136 | mount_path = '/v1/sys/mounts/kv' | |
137 | data = { | |
138 | "type": "kv", | |
139 | "options": { | |
140 | "version": "2" | |
141 | } | |
142 | } | |
143 | elif engine == 'transit': | |
144 | log.info('Mounting transit secrets engine') | |
145 | mount_path = '/v1/sys/mounts/transit' | |
146 | data = { | |
147 | "type": "transit" | |
148 | } | |
149 | else: | |
150 | raise Exception("Unknown or missing secrets engine") | |
151 | ||
152 | send_req(ctx, cconfig, cclient, mount_path, json.dumps(data)) | |
153 | yield | |
154 | ||
155 | ||
156 | def send_req(ctx, cconfig, client, path, body, method='POST'): | |
157 | host, port = ctx.vault.endpoints[client] | |
158 | req = httplib.HTTPConnection(host, port, timeout=30) | |
159 | token = cconfig.get('root_token', 'atoken') | |
160 | log.info("Send request to Vault: %s:%s at %s with token: %s", host, port, path, token) | |
161 | headers = {'X-Vault-Token': token} | |
162 | req.request(method, path, headers=headers, body=body) | |
163 | resp = req.getresponse() | |
164 | log.info(resp.read()) | |
165 | if not (resp.status >= 200 and resp.status < 300): | |
166 | raise Exception("Request to Vault server failed with status %d" % resp.status) | |
167 | return resp | |
168 | ||
169 | ||
170 | @contextlib.contextmanager | |
171 | def create_secrets(ctx, config): | |
172 | (cclient, cconfig) = config.items()[0] | |
173 | engine = cconfig.get('engine') | |
174 | prefix = cconfig.get('prefix') | |
175 | secrets = cconfig.get('secrets') | |
176 | if secrets is None: | |
177 | raise ConfigError("No secrets specified, please specify some.") | |
178 | ||
179 | for secret in secrets: | |
180 | try: | |
181 | path = secret['path'] | |
182 | except KeyError: | |
183 | raise ConfigError('Missing "path" field in secret') | |
184 | ||
185 | if engine == 'kv': | |
186 | try: | |
187 | data = { | |
188 | "data": { | |
189 | "key": secret['secret'] | |
190 | } | |
191 | } | |
192 | except KeyError: | |
193 | raise ConfigError('Missing "secret" field in secret') | |
194 | elif engine == 'transit': | |
195 | data = {"exportable": "true"} | |
196 | else: | |
197 | raise Exception("Unknown or missing secrets engine") | |
198 | ||
199 | send_req(ctx, cconfig, cclient, urljoin(prefix, path), json.dumps(data)) | |
200 | ||
201 | log.info("secrets created") | |
202 | yield | |
203 | ||
204 | ||
205 | @contextlib.contextmanager | |
206 | def task(ctx, config): | |
207 | """ | |
208 | Deploy and configure Vault | |
209 | ||
210 | Example of configuration: | |
211 | ||
212 | tasks: | |
213 | - vault: | |
214 | client.0: | |
215 | version: 1.2.2 | |
216 | root_token: test_root_token | |
217 | engine: kv | |
218 | prefix: /v1/kv/data/ | |
219 | secrets: | |
220 | - path: kv/teuthology/key_a | |
221 | secret: YmluCmJvb3N0CmJvb3N0LWJ1aWxkCmNlcGguY29uZgo= | |
222 | - path: kv/teuthology/key_b | |
223 | secret: aWIKTWFrZWZpbGUKbWFuCm91dApzcmMKVGVzdGluZwo= | |
224 | """ | |
225 | all_clients = ['client.{id}'.format(id=id_) | |
226 | for id_ in teuthology.all_roles_of_type(ctx.cluster, 'client')] | |
227 | if config is None: | |
228 | config = all_clients | |
229 | if isinstance(config, list): | |
230 | config = dict.fromkeys(config) | |
231 | ||
232 | overrides = ctx.config.get('overrides', {}) | |
233 | # merge each client section, not the top level. | |
234 | for client in config.keys(): | |
235 | if not config[client]: | |
236 | config[client] = {} | |
237 | teuthology.deep_merge(config[client], overrides.get('vault', {})) | |
238 | ||
239 | log.debug('Vault config is %s', config) | |
240 | ||
241 | ctx.vault = argparse.Namespace() | |
242 | ctx.vault.endpoints = assign_ports(ctx, config, 8200) | |
243 | ctx.vault.root_token = None | |
244 | ctx.vault.prefix = config[client].get('prefix') | |
245 | ctx.vault.engine = config[client].get('engine') | |
246 | ||
247 | with contextutil.nested( | |
248 | lambda: download(ctx=ctx, config=config), | |
249 | lambda: run_vault(ctx=ctx, config=config), | |
250 | lambda: setup_vault(ctx=ctx, config=config), | |
251 | lambda: create_secrets(ctx=ctx, config=config) | |
252 | ): | |
253 | yield | |
254 |