]> git.proxmox.com Git - ceph.git/blame - ceph/qa/tasks/vault.py
import ceph quincy 17.2.4
[ceph.git] / ceph / qa / tasks / vault.py
CommitLineData
9f95a23c
TL
1"""
2Deploy and configure Vault for Teuthology
3"""
4
5import argparse
6import contextlib
7import logging
8import time
9f95a23c
TL
9import json
10from os import path
f67539c2
TL
11from http import client as http_client
12from urllib.parse import urljoin
9f95a23c
TL
13
14from teuthology import misc as teuthology
15from teuthology import contextutil
16from teuthology.orchestra import run
f67539c2 17from teuthology.exceptions import ConfigError, CommandFailedError
9f95a23c
TL
18
19
20log = logging.getLogger(__name__)
21
22
23def 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
39def 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
93def get_vault_dir(ctx):
94 return '{tdir}/vault'.format(tdir=teuthology.get_testdir(ctx))
95
96
97@contextlib.contextmanager
98def 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
139def 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
168def 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
183def 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
223def 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