]>
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 | |
9f95a23c TL |
9 | import json |
10 | from os import path | |
f67539c2 TL |
11 | from http import client as http_client |
12 | from urllib.parse import urljoin | |
9f95a23c TL |
13 | |
14 | from teuthology import misc as teuthology | |
15 | from teuthology import contextutil | |
16 | from teuthology.orchestra import run | |
f67539c2 | 17 | from teuthology.exceptions import ConfigError, CommandFailedError |
9f95a23c TL |
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 | |
f67539c2 TL |
68 | # Using python3 in case python is not installed on hosts |
69 | failed=True | |
70 | for f in [ | |
71 | lambda z,d: ['unzip', z, '-d', d], | |
72 | lambda z,d: ['python3', '-m', 'zipfile', '-e', z, d], | |
73 | lambda z,d: ['python', '-m', 'zipfile', '-e', z, d]]: | |
74 | try: | |
75 | ctx.cluster.only(client).run(args=f(install_zip, install_dir)) | |
76 | failed = False | |
77 | break | |
78 | except CommandFailedError as e: | |
79 | failed = e | |
80 | if failed: | |
81 | raise failed | |
9f95a23c TL |
82 | |
83 | try: | |
84 | yield | |
85 | finally: | |
86 | log.info('Removing Vault...') | |
87 | testdir = teuthology.get_testdir(ctx) | |
88 | for client in config: | |
89 | ctx.cluster.only(client).run( | |
90 | args=['rm', '-rf', install_dir, install_zip]) | |
91 | ||
92 | ||
93 | def get_vault_dir(ctx): | |
94 | return '{tdir}/vault'.format(tdir=teuthology.get_testdir(ctx)) | |
95 | ||
96 | ||
97 | @contextlib.contextmanager | |
98 | def run_vault(ctx, config): | |
99 | assert isinstance(config, dict) | |
100 | ||
101 | for (client, cconf) in config.items(): | |
102 | (remote,) = ctx.cluster.only(client).remotes.keys() | |
103 | cluster_name, _, client_id = teuthology.split_role(client) | |
104 | ||
105 | _, port = ctx.vault.endpoints[client] | |
106 | listen_addr = "0.0.0.0:{}".format(port) | |
107 | ||
108 | root_token = ctx.vault.root_token = cconf.get('root_token', 'root') | |
109 | ||
110 | log.info("Starting Vault listening on %s ...", listen_addr) | |
111 | v_params = [ | |
112 | '-dev', | |
113 | '-dev-listen-address={}'.format(listen_addr), | |
114 | '-dev-no-store-token', | |
115 | '-dev-root-token-id={}'.format(root_token) | |
116 | ] | |
117 | ||
118 | cmd = "chmod +x {vdir}/vault && {vdir}/vault server {vargs}".format(vdir=get_vault_dir(ctx), vargs=" ".join(v_params)) | |
119 | ||
120 | ctx.daemons.add_daemon( | |
121 | remote, 'vault', client_id, | |
122 | cluster=cluster_name, | |
123 | args=['bash', '-c', cmd, run.Raw('& { read; kill %1; }')], | |
124 | logger=log.getChild(client), | |
125 | stdin=run.PIPE, | |
126 | cwd=get_vault_dir(ctx), | |
127 | wait=False, | |
128 | check_status=False, | |
129 | ) | |
130 | time.sleep(10) | |
131 | try: | |
132 | yield | |
133 | finally: | |
134 | log.info('Stopping Vault instance') | |
135 | ctx.daemons.get_daemon('vault', client_id, cluster_name).stop() | |
136 | ||
137 | ||
138 | @contextlib.contextmanager | |
139 | def setup_vault(ctx, config): | |
140 | """ | |
141 | Mount Transit or KV version 2 secrets engine | |
142 | """ | |
e306af50 | 143 | (cclient, cconfig) = next(iter(config.items())) |
9f95a23c TL |
144 | engine = cconfig.get('engine') |
145 | ||
146 | if engine == 'kv': | |
147 | log.info('Mounting kv version 2 secrets engine') | |
148 | mount_path = '/v1/sys/mounts/kv' | |
149 | data = { | |
150 | "type": "kv", | |
151 | "options": { | |
152 | "version": "2" | |
153 | } | |
154 | } | |
155 | elif engine == 'transit': | |
156 | log.info('Mounting transit secrets engine') | |
157 | mount_path = '/v1/sys/mounts/transit' | |
158 | data = { | |
159 | "type": "transit" | |
160 | } | |
161 | else: | |
162 | raise Exception("Unknown or missing secrets engine") | |
163 | ||
164 | send_req(ctx, cconfig, cclient, mount_path, json.dumps(data)) | |
165 | yield | |
166 | ||
167 | ||
168 | def send_req(ctx, cconfig, client, path, body, method='POST'): | |
169 | host, port = ctx.vault.endpoints[client] | |
e306af50 | 170 | req = http_client.HTTPConnection(host, port, timeout=30) |
9f95a23c TL |
171 | token = cconfig.get('root_token', 'atoken') |
172 | log.info("Send request to Vault: %s:%s at %s with token: %s", host, port, path, token) | |
173 | headers = {'X-Vault-Token': token} | |
174 | req.request(method, path, headers=headers, body=body) | |
175 | resp = req.getresponse() | |
176 | log.info(resp.read()) | |
177 | if not (resp.status >= 200 and resp.status < 300): | |
178 | raise Exception("Request to Vault server failed with status %d" % resp.status) | |
179 | return resp | |
180 | ||
181 | ||
182 | @contextlib.contextmanager | |
183 | def create_secrets(ctx, config): | |
e306af50 | 184 | (cclient, cconfig) = next(iter(config.items())) |
9f95a23c TL |
185 | engine = cconfig.get('engine') |
186 | prefix = cconfig.get('prefix') | |
187 | secrets = cconfig.get('secrets') | |
f67539c2 | 188 | flavor = cconfig.get('flavor') |
9f95a23c TL |
189 | if secrets is None: |
190 | raise ConfigError("No secrets specified, please specify some.") | |
191 | ||
f67539c2 | 192 | ctx.vault.keys[cclient] = [] |
9f95a23c TL |
193 | for secret in secrets: |
194 | try: | |
195 | path = secret['path'] | |
196 | except KeyError: | |
197 | raise ConfigError('Missing "path" field in secret') | |
f67539c2 | 198 | exportable = secret.get("exportable", flavor == "old") |
9f95a23c TL |
199 | |
200 | if engine == 'kv': | |
201 | try: | |
202 | data = { | |
203 | "data": { | |
204 | "key": secret['secret'] | |
205 | } | |
206 | } | |
207 | except KeyError: | |
208 | raise ConfigError('Missing "secret" field in secret') | |
209 | elif engine == 'transit': | |
f67539c2 | 210 | data = {"exportable": "true" if exportable else "false"} |
9f95a23c TL |
211 | else: |
212 | raise Exception("Unknown or missing secrets engine") | |
213 | ||
214 | send_req(ctx, cconfig, cclient, urljoin(prefix, path), json.dumps(data)) | |
215 | ||
f67539c2 TL |
216 | ctx.vault.keys[cclient].append({ 'Path': path }); |
217 | ||
9f95a23c TL |
218 | log.info("secrets created") |
219 | yield | |
220 | ||
221 | ||
222 | @contextlib.contextmanager | |
223 | def task(ctx, config): | |
224 | """ | |
225 | Deploy and configure Vault | |
226 | ||
227 | Example of configuration: | |
228 | ||
229 | tasks: | |
230 | - vault: | |
231 | client.0: | |
f67539c2 TL |
232 | install_url: http://my.special.place/vault.zip |
233 | install_sha256: zipfiles-sha256-sum-much-larger-than-this | |
9f95a23c | 234 | root_token: test_root_token |
f67539c2 TL |
235 | engine: transit |
236 | flavor: old | |
237 | prefix: /v1/transit/keys | |
9f95a23c TL |
238 | secrets: |
239 | - path: kv/teuthology/key_a | |
f67539c2 TL |
240 | secret: base64_only_if_using_kv_aWxkCmNlcGguY29uZgo= |
241 | exportable: true | |
9f95a23c | 242 | - path: kv/teuthology/key_b |
f67539c2 TL |
243 | secret: base64_only_if_using_kv_dApzcmMKVGVzdGluZwo= |
244 | ||
245 | engine can be 'kv' or 'transit' | |
246 | prefix should be /v1/kv/data/ for kv, /v1/transit/keys/ for transit | |
247 | flavor should be 'old' only if testing the original transit logic | |
248 | otherwise omit. | |
249 | for kv only: 256-bit key value should be specified via secret, | |
250 | otherwise should omit. | |
251 | for transit: exportable may be used to make individual keys exportable. | |
252 | flavor may be set to 'old' to make all keys exportable by default, | |
253 | which is required by the original transit logic. | |
9f95a23c TL |
254 | """ |
255 | all_clients = ['client.{id}'.format(id=id_) | |
256 | for id_ in teuthology.all_roles_of_type(ctx.cluster, 'client')] | |
257 | if config is None: | |
258 | config = all_clients | |
259 | if isinstance(config, list): | |
260 | config = dict.fromkeys(config) | |
261 | ||
262 | overrides = ctx.config.get('overrides', {}) | |
263 | # merge each client section, not the top level. | |
264 | for client in config.keys(): | |
265 | if not config[client]: | |
266 | config[client] = {} | |
267 | teuthology.deep_merge(config[client], overrides.get('vault', {})) | |
268 | ||
269 | log.debug('Vault config is %s', config) | |
270 | ||
271 | ctx.vault = argparse.Namespace() | |
272 | ctx.vault.endpoints = assign_ports(ctx, config, 8200) | |
273 | ctx.vault.root_token = None | |
274 | ctx.vault.prefix = config[client].get('prefix') | |
275 | ctx.vault.engine = config[client].get('engine') | |
f67539c2 TL |
276 | ctx.vault.keys = {} |
277 | q=config[client].get('flavor') | |
278 | if q: | |
279 | ctx.vault.flavor = q | |
9f95a23c TL |
280 | |
281 | with contextutil.nested( | |
282 | lambda: download(ctx=ctx, config=config), | |
283 | lambda: run_vault(ctx=ctx, config=config), | |
284 | lambda: setup_vault(ctx=ctx, config=config), | |
285 | lambda: create_secrets(ctx=ctx, config=config) | |
286 | ): | |
287 | yield | |
288 |