]> git.proxmox.com Git - ceph.git/blob - ceph/qa/tasks/vault.py
import ceph quincy 17.2.4
[ceph.git] / ceph / qa / tasks / vault.py
1 """
2 Deploy and configure Vault for Teuthology
3 """
4
5 import argparse
6 import contextlib
7 import logging
8 import time
9 import json
10 from os import path
11 from http import client as http_client
12 from urllib.parse 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, CommandFailedError
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 # 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
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 """
143 (cclient, cconfig) = next(iter(config.items()))
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]
170 req = http_client.HTTPConnection(host, port, timeout=30)
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):
184 (cclient, cconfig) = next(iter(config.items()))
185 engine = cconfig.get('engine')
186 prefix = cconfig.get('prefix')
187 secrets = cconfig.get('secrets')
188 flavor = cconfig.get('flavor')
189 if secrets is None:
190 raise ConfigError("No secrets specified, please specify some.")
191
192 ctx.vault.keys[cclient] = []
193 for secret in secrets:
194 try:
195 path = secret['path']
196 except KeyError:
197 raise ConfigError('Missing "path" field in secret')
198 exportable = secret.get("exportable", flavor == "old")
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':
210 data = {"exportable": "true" if exportable else "false"}
211 else:
212 raise Exception("Unknown or missing secrets engine")
213
214 send_req(ctx, cconfig, cclient, urljoin(prefix, path), json.dumps(data))
215
216 ctx.vault.keys[cclient].append({ 'Path': path });
217
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:
232 install_url: http://my.special.place/vault.zip
233 install_sha256: zipfiles-sha256-sum-much-larger-than-this
234 root_token: test_root_token
235 engine: transit
236 flavor: old
237 prefix: /v1/transit/keys
238 secrets:
239 - path: kv/teuthology/key_a
240 secret: base64_only_if_using_kv_aWxkCmNlcGguY29uZgo=
241 exportable: true
242 - path: kv/teuthology/key_b
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.
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')
276 ctx.vault.keys = {}
277 q=config[client].get('flavor')
278 if q:
279 ctx.vault.flavor = q
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