]> git.proxmox.com Git - ceph.git/blame - ceph/qa/tasks/vault.py
import 15.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
e306af50
TL
11from six.moves import http_client
12from six.moves.urllib.parse import urljoin
9f95a23c
TL
13
14from teuthology import misc as teuthology
15from teuthology import contextutil
16from teuthology.orchestra import run
17from teuthology.exceptions import ConfigError
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
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
81def get_vault_dir(ctx):
82 return '{tdir}/vault'.format(tdir=teuthology.get_testdir(ctx))
83
84
85@contextlib.contextmanager
86def 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
127def setup_vault(ctx, config):
128 """
129 Mount Transit or KV version 2 secrets engine
130 """
e306af50 131 (cclient, cconfig) = next(iter(config.items()))
9f95a23c
TL
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
156def send_req(ctx, cconfig, client, path, body, method='POST'):
157 host, port = ctx.vault.endpoints[client]
e306af50 158 req = http_client.HTTPConnection(host, port, timeout=30)
9f95a23c
TL
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
171def create_secrets(ctx, config):
e306af50 172 (cclient, cconfig) = next(iter(config.items()))
9f95a23c
TL
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
206def 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