]>
git.proxmox.com Git - ceph.git/blob - ceph/qa/tasks/vault.py
2 Deploy and configure Vault for Teuthology
11 from http
import client
as http_client
12 from urllib
.parse
import urljoin
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
20 log
= logging
.getLogger(__name__
)
23 def assign_ports(ctx
, config
, initial_port
):
25 Assign port numbers starting from @initial_port
29 for remote
, roles_for_host
in ctx
.cluster
.remotes
.items():
30 for role
in roles_for_host
:
32 role_endpoints
[role
] = (remote
.name
.split('@')[1], port
)
38 @contextlib.contextmanager
39 def download(ctx
, config
):
41 Download Vault Release from Hashicopr website.
42 Remove downloaded file upon exit.
44 assert isinstance(config
, dict)
45 log
.info('Downloading Vault...')
46 testdir
= teuthology
.get_testdir(ctx
)
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')
56 log
.info('Downloading Vault...')
57 ctx
.cluster
.only(client
).run(
58 args
=['curl', '-L', install_url
, '-o', install_zip
])
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'])
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
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
]]:
75 ctx
.cluster
.only(client
).run(args
=f(install_zip
, install_dir
))
78 except CommandFailedError
as e
:
86 log
.info('Removing Vault...')
87 testdir
= teuthology
.get_testdir(ctx
)
89 ctx
.cluster
.only(client
).run(
90 args
=['rm', '-rf', install_dir
, install_zip
])
93 def get_vault_dir(ctx
):
94 return '{tdir}/vault'.format(tdir
=teuthology
.get_testdir(ctx
))
97 @contextlib.contextmanager
98 def run_vault(ctx
, config
):
99 assert isinstance(config
, dict)
101 for (client
, cconf
) in config
.items():
102 (remote
,) = ctx
.cluster
.only(client
).remotes
.keys()
103 cluster_name
, _
, client_id
= teuthology
.split_role(client
)
105 _
, port
= ctx
.vault
.endpoints
[client
]
106 listen_addr
= "0.0.0.0:{}".format(port
)
108 root_token
= ctx
.vault
.root_token
= cconf
.get('root_token', 'root')
110 log
.info("Starting Vault listening on %s ...", listen_addr
)
113 '-dev-listen-address={}'.format(listen_addr
),
114 '-dev-no-store-token',
115 '-dev-root-token-id={}'.format(root_token
)
118 cmd
= "chmod +x {vdir}/vault && {vdir}/vault server {vargs}".format(vdir
=get_vault_dir(ctx
), vargs
=" ".join(v_params
))
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
),
126 cwd
=get_vault_dir(ctx
),
134 log
.info('Stopping Vault instance')
135 ctx
.daemons
.get_daemon('vault', client_id
, cluster_name
).stop()
138 @contextlib.contextmanager
139 def setup_vault(ctx
, config
):
141 Mount Transit or KV version 2 secrets engine
143 (cclient
, cconfig
) = next(iter(config
.items()))
144 engine
= cconfig
.get('engine')
147 log
.info('Mounting kv version 2 secrets engine')
148 mount_path
= '/v1/sys/mounts/kv'
155 elif engine
== 'transit':
156 log
.info('Mounting transit secrets engine')
157 mount_path
= '/v1/sys/mounts/transit'
162 raise Exception("Unknown or missing secrets engine")
164 send_req(ctx
, cconfig
, cclient
, mount_path
, json
.dumps(data
))
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
)
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')
190 raise ConfigError("No secrets specified, please specify some.")
192 ctx
.vault
.keys
[cclient
] = []
193 for secret
in secrets
:
195 path
= secret
['path']
197 raise ConfigError('Missing "path" field in secret')
198 exportable
= secret
.get("exportable", flavor
== "old")
204 "key": secret
['secret']
208 raise ConfigError('Missing "secret" field in secret')
209 elif engine
== 'transit':
210 data
= {"exportable": "true" if exportable
else "false"}
212 raise Exception("Unknown or missing secrets engine")
214 send_req(ctx
, cconfig
, cclient
, urljoin(prefix
, path
), json
.dumps(data
))
216 ctx
.vault
.keys
[cclient
].append({ 'Path': path
});
218 log
.info("secrets created")
222 @contextlib.contextmanager
223 def task(ctx
, config
):
225 Deploy and configure Vault
227 Example of configuration:
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
237 prefix: /v1/transit/keys
239 - path: kv/teuthology/key_a
240 secret: base64_only_if_using_kv_aWxkCmNlcGguY29uZgo=
242 - path: kv/teuthology/key_b
243 secret: base64_only_if_using_kv_dApzcmMKVGVzdGluZwo=
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
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.
255 all_clients
= ['client.{id}'.format(id=id_
)
256 for id_
in teuthology
.all_roles_of_type(ctx
.cluster
, 'client')]
259 if isinstance(config
, list):
260 config
= dict.fromkeys(config
)
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
]:
267 teuthology
.deep_merge(config
[client
], overrides
.get('vault', {}))
269 log
.debug('Vault config is %s', config
)
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')
277 q
=config
[client
].get('flavor')
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
)