]> git.proxmox.com Git - ceph.git/blob - ceph/qa/tasks/vault.py
import 15.2.0 Octopus source
[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 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