kitchen-vra 3.3.3 → 3.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/publish.yml +2 -2
- data/.gitignore +3 -1
- data/.release-please-manifest.json +1 -1
- data/.rubocop.yml +7 -1
- data/.yamllint +6 -0
- data/.yardopts +11 -0
- data/CHANGELOG.md +92 -49
- data/CONTRIBUTING.md +84 -0
- data/Gemfile +7 -4
- data/README.md +260 -31
- data/Rakefile +20 -2
- data/kitchen-vra.gemspec +1 -2
- data/lib/kitchen/driver/vra.rb +183 -32
- data/lib/kitchen/driver/vra_version.rb +4 -1
- data/renovate.json +3 -1
- data/spec/vra_spec.rb +98 -1
- metadata +8 -8
- data/.github/dependabot.yml +0 -8
data/README.md
CHANGED
|
@@ -1,63 +1,292 @@
|
|
|
1
1
|
# kitchen-vra
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://badge.fury.io/rb/kitchen-vra)
|
|
4
|
+
|
|
5
|
+
A [Test Kitchen](https://kitchen.ci/) driver that provisions and destroys machines through [VMware vRealize Automation](https://www.vmware.com/products/vrealize-automation.html) (vRA, now Aria Automation), so you can test your cookbooks and infrastructure code against instances from your own vRA catalog.
|
|
6
|
+
|
|
7
|
+
Rather than talking to a hypervisor directly, this driver submits a catalog request to vRA and waits for the resulting deployment, so test instances follow the same blueprints, approvals, and policies as the rest of your estate.
|
|
8
|
+
|
|
9
|
+
> This documentation uses [Cinc Workstation](https://cinc.sh/) and the `cinc` commands throughout. Everything here works identically with Chef Workstation — see [Using with Chef](#using-with-chef).
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- Ruby 3.1 or later (already satisfied if you use Cinc Workstation)
|
|
14
|
+
- Access to a vRA 8.x appliance
|
|
15
|
+
- A catalog item that provisions exactly **one** virtual machine — the driver rejects a request that returns more than one server
|
|
16
|
+
- Permission to request that catalog item, and to delete the resulting deployment
|
|
4
17
|
|
|
5
18
|
## Installation
|
|
6
19
|
|
|
7
|
-
|
|
20
|
+
This driver ships as part of [Cinc Workstation](https://cinc.sh/start/workstation/). If you have Cinc Workstation installed, there is nothing else to install.
|
|
21
|
+
|
|
22
|
+
To install it into a standalone Ruby:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
gem install kitchen-vra
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or with Bundler, add it to your `Gemfile`:
|
|
8
29
|
|
|
9
30
|
```ruby
|
|
10
|
-
gem
|
|
31
|
+
gem "kitchen-vra"
|
|
11
32
|
```
|
|
12
33
|
|
|
13
|
-
|
|
34
|
+
...then run `bundle install`.
|
|
35
|
+
|
|
36
|
+
## Authentication
|
|
37
|
+
|
|
38
|
+
Credentials are resolved in this order:
|
|
14
39
|
|
|
15
|
-
|
|
16
|
-
|
|
40
|
+
1. The `username` and `password` driver options
|
|
41
|
+
2. The `VRA_USER_NAME` and `VRA_USER_PASSWORD` environment variables
|
|
42
|
+
3. Cached credentials, if `cache_credentials` was enabled on a previous run
|
|
43
|
+
4. An interactive prompt
|
|
44
|
+
|
|
45
|
+
Keep credentials out of `kitchen.yml`. The usual approach is the environment:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
export VRA_USER_NAME='myuser@corp.local'
|
|
49
|
+
export VRA_USER_PASSWORD='mypassword'
|
|
17
50
|
```
|
|
18
51
|
|
|
19
|
-
|
|
52
|
+
Setting `cache_credentials: true` stores the credentials after a successful run so later runs do not prompt. They are written to `.kitchen/cached_vra`, encrypted under a key derived from `base_url` and readable only by your user. Since `base_url` is not a secret, this hides the password from casual view rather than protecting it — treat the file as sensitive and avoid the option on shared machines.
|
|
20
53
|
|
|
21
|
-
|
|
22
|
-
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
---
|
|
58
|
+
driver:
|
|
59
|
+
name: vra
|
|
60
|
+
base_url: https://vra.corp.local
|
|
61
|
+
domain: corp.local
|
|
62
|
+
project_id: 6ba69375-2d1e-4a5e-9e9b-1a1a3f0e6d4c
|
|
63
|
+
image_mapping: Ubuntu 22.04
|
|
64
|
+
flavor_mapping: Small
|
|
65
|
+
catalog_name: Ubuntu Server
|
|
66
|
+
verify_ssl: true
|
|
67
|
+
|
|
68
|
+
provisioner:
|
|
69
|
+
name: cinc_infra
|
|
70
|
+
|
|
71
|
+
verifier:
|
|
72
|
+
name: cinc_auditor
|
|
73
|
+
|
|
74
|
+
platforms:
|
|
75
|
+
- name: ubuntu-22.04
|
|
76
|
+
|
|
77
|
+
suites:
|
|
78
|
+
- name: default
|
|
79
|
+
run_list:
|
|
80
|
+
- recipe[my_cookbook::default]
|
|
23
81
|
```
|
|
24
82
|
|
|
25
|
-
|
|
83
|
+
Then run the full test cycle:
|
|
26
84
|
|
|
27
|
-
```
|
|
28
|
-
|
|
85
|
+
```sh
|
|
86
|
+
cinc kitchen test
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Or step through it:
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
cinc kitchen create # submit the catalog request and wait for the deployment
|
|
93
|
+
cinc kitchen converge # apply your cookbook
|
|
94
|
+
cinc kitchen verify # run your tests
|
|
95
|
+
cinc kitchen destroy # destroy the vRA deployment
|
|
29
96
|
```
|
|
30
97
|
|
|
31
98
|
## Configuration
|
|
32
99
|
|
|
33
|
-
|
|
100
|
+
All options below are set under the `driver:` key in `kitchen.yml`.
|
|
34
101
|
|
|
35
|
-
|
|
102
|
+
### Required
|
|
103
|
+
|
|
104
|
+
| Option | Default | Description |
|
|
105
|
+
| --- | --- | --- |
|
|
106
|
+
| `base_url` | *none* | Base URL of the vRA appliance, e.g. `https://vra.corp.local`. Required. |
|
|
107
|
+
| `domain` | *none* | Authentication domain, e.g. `corp.local`. Required. |
|
|
108
|
+
| `project_id` | *none* | ID of the vRA project the deployment is created under. Required. |
|
|
109
|
+
| `image_mapping` | *none* | Name of the vRA image mapping to deploy, e.g. `Ubuntu 22.04`. Required. |
|
|
110
|
+
| `flavor_mapping` | *none* | Name of the vRA flavor mapping, which determines CPU and memory, e.g. `Small`. Required. |
|
|
111
|
+
|
|
112
|
+
You must also identify the catalog item with either `catalog_id` or `catalog_name`.
|
|
113
|
+
|
|
114
|
+
### Catalog item
|
|
115
|
+
|
|
116
|
+
| Option | Default | Description |
|
|
117
|
+
| --- | --- | --- |
|
|
118
|
+
| `catalog_id` | `nil` | ID of the catalog item to request. |
|
|
119
|
+
| `catalog_name` | `nil` | Name of the catalog item to request, resolved to an ID. Use instead of `catalog_id`. |
|
|
120
|
+
| `version` | `nil` | Version of the catalog item to request. Uses the latest if unset. |
|
|
121
|
+
| `extra_parameters` | `{}` | Additional catalog request parameters, keyed by parameter name. See [Extra parameters](#extra-parameters). |
|
|
122
|
+
|
|
123
|
+
### Credentials
|
|
124
|
+
|
|
125
|
+
| Option | Default | Description |
|
|
126
|
+
| --- | --- | --- |
|
|
127
|
+
| `username` | `$VRA_USER_NAME` | vRA username. Prompted for if not set anywhere. |
|
|
128
|
+
| `password` | `$VRA_USER_PASSWORD` | vRA password. Prompted for if not set anywhere. |
|
|
129
|
+
| `cache_credentials` | `false` | Cache credentials to disk after a successful run so later runs do not prompt. |
|
|
130
|
+
| `verify_ssl` | `true` | Verify the appliance's TLS certificate. Only disable against a lab with a self-signed certificate. |
|
|
131
|
+
| `tenant` | `nil` | **Deprecated.** Not used for authentication in vRA 8.x. Use `domain` instead. |
|
|
132
|
+
|
|
133
|
+
### Deployment
|
|
134
|
+
|
|
135
|
+
| Option | Default | Description |
|
|
136
|
+
| --- | --- | --- |
|
|
137
|
+
| `deployment_name` | the platform name | Name given to the vRA deployment. Ignored when `unique_name` is enabled. |
|
|
138
|
+
| `unique_name` | `false` | Name the deployment `deployment_<request id>` instead of using `deployment_name`, so concurrent runs do not collide. |
|
|
36
139
|
|
|
37
|
-
|
|
140
|
+
### Connectivity
|
|
38
141
|
|
|
39
|
-
|
|
142
|
+
| Option | Default | Description |
|
|
143
|
+
| --- | --- | --- |
|
|
144
|
+
| `use_dns` | `false` | Connect using the server's DNS name instead of its IP address. Needed when vRA does not report a reachable IP. |
|
|
145
|
+
| `dns_suffix` | `nil` | Suffix appended to the server name when `use_dns` is enabled, e.g. `corp.local`. |
|
|
146
|
+
| `private_key_path` | `~/.ssh/id_rsa` or `~/.ssh/id_dsa` | SSH private key used to connect. The first of those two files that exists is used. |
|
|
40
147
|
|
|
41
|
-
|
|
148
|
+
### Timing
|
|
42
149
|
|
|
43
|
-
|
|
44
|
-
|
|
150
|
+
| Option | Default | Description |
|
|
151
|
+
| --- | --- | --- |
|
|
152
|
+
| `request_timeout` | `600` | Seconds to wait for the catalog request to complete. |
|
|
153
|
+
| `request_refresh_rate` | `2` | Seconds between polls while waiting for the request. |
|
|
154
|
+
| `server_ready_retries` | `1` | Number of times to retry when the server is reported ready but is not yet reachable. Increase this on slow environments. |
|
|
45
155
|
|
|
46
|
-
|
|
47
|
-
|
|
156
|
+
## Extra parameters
|
|
157
|
+
|
|
158
|
+
`extra_parameters` passes additional inputs to the catalog request. Each entry is
|
|
159
|
+
keyed by the vRA parameter name, and gives the value along with its type:
|
|
160
|
+
|
|
161
|
+
```yaml
|
|
162
|
+
driver:
|
|
163
|
+
name: vra
|
|
164
|
+
base_url: https://vra.corp.local
|
|
165
|
+
domain: corp.local
|
|
166
|
+
project_id: 6ba69375-2d1e-4a5e-9e9b-1a1a3f0e6d4c
|
|
167
|
+
image_mapping: Ubuntu 22.04
|
|
168
|
+
flavor_mapping: Small
|
|
169
|
+
catalog_name: Ubuntu Server
|
|
170
|
+
extra_parameters:
|
|
171
|
+
environment:
|
|
172
|
+
type: string
|
|
173
|
+
value: test
|
|
174
|
+
disk_size:
|
|
175
|
+
type: integer
|
|
176
|
+
value: 40
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Examples
|
|
180
|
+
|
|
181
|
+
### Selecting the catalog item by ID
|
|
182
|
+
|
|
183
|
+
```yaml
|
|
184
|
+
driver:
|
|
185
|
+
name: vra
|
|
186
|
+
base_url: https://vra.corp.local
|
|
187
|
+
domain: corp.local
|
|
188
|
+
project_id: 6ba69375-2d1e-4a5e-9e9b-1a1a3f0e6d4c
|
|
189
|
+
image_mapping: Ubuntu 22.04
|
|
190
|
+
flavor_mapping: Small
|
|
191
|
+
catalog_id: 9f4d4a7e-1234-4b8b-9c2f-77b6c9f0e111
|
|
192
|
+
version: "2"
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Running several suites at once
|
|
196
|
+
|
|
197
|
+
Without `unique_name`, concurrent deployments share a name derived from the
|
|
198
|
+
platform, which is confusing in the vRA UI and can collide.
|
|
199
|
+
|
|
200
|
+
```yaml
|
|
201
|
+
driver:
|
|
202
|
+
name: vra
|
|
203
|
+
base_url: https://vra.corp.local
|
|
204
|
+
domain: corp.local
|
|
205
|
+
project_id: 6ba69375-2d1e-4a5e-9e9b-1a1a3f0e6d4c
|
|
206
|
+
image_mapping: Ubuntu 22.04
|
|
207
|
+
flavor_mapping: Small
|
|
208
|
+
catalog_name: Ubuntu Server
|
|
209
|
+
unique_name: true
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Connecting by DNS name
|
|
213
|
+
|
|
214
|
+
```yaml
|
|
215
|
+
driver:
|
|
216
|
+
name: vra
|
|
217
|
+
base_url: https://vra.corp.local
|
|
218
|
+
domain: corp.local
|
|
219
|
+
project_id: 6ba69375-2d1e-4a5e-9e9b-1a1a3f0e6d4c
|
|
220
|
+
image_mapping: Ubuntu 22.04
|
|
221
|
+
flavor_mapping: Small
|
|
222
|
+
catalog_name: Ubuntu Server
|
|
223
|
+
use_dns: true
|
|
224
|
+
dns_suffix: corp.local
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### A slow environment
|
|
228
|
+
|
|
229
|
+
```yaml
|
|
230
|
+
driver:
|
|
231
|
+
name: vra
|
|
232
|
+
base_url: https://vra.corp.local
|
|
233
|
+
domain: corp.local
|
|
234
|
+
project_id: 6ba69375-2d1e-4a5e-9e9b-1a1a3f0e6d4c
|
|
235
|
+
image_mapping: Ubuntu 22.04
|
|
236
|
+
flavor_mapping: Small
|
|
237
|
+
catalog_name: Ubuntu Server
|
|
238
|
+
request_timeout: 1800
|
|
239
|
+
request_refresh_rate: 10
|
|
240
|
+
server_ready_retries: 5
|
|
48
241
|
```
|
|
49
242
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
243
|
+
### Lab appliance with a self-signed certificate
|
|
244
|
+
|
|
245
|
+
```yaml
|
|
246
|
+
driver:
|
|
247
|
+
name: vra
|
|
248
|
+
base_url: https://vra.lab.local
|
|
249
|
+
domain: lab.local
|
|
250
|
+
project_id: 6ba69375-2d1e-4a5e-9e9b-1a1a3f0e6d4c
|
|
251
|
+
image_mapping: Ubuntu 22.04
|
|
252
|
+
flavor_mapping: Small
|
|
253
|
+
catalog_name: Ubuntu Server
|
|
254
|
+
verify_ssl: false
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Troubleshooting
|
|
258
|
+
|
|
259
|
+
**"The vRA request created more than one server."** The driver requires a catalog
|
|
260
|
+
item that provisions exactly one VM. Point it at a single-machine blueprint.
|
|
261
|
+
|
|
262
|
+
**The request succeeds but Test Kitchen cannot connect.** vRA often reports a
|
|
263
|
+
deployment as ready slightly before the guest accepts connections. Raise
|
|
264
|
+
`server_ready_retries`. If vRA reports an unreachable IP, set `use_dns: true`
|
|
265
|
+
along with `dns_suffix`.
|
|
266
|
+
|
|
267
|
+
**Authentication fails on vRA 8.x.** Use `domain`, not `tenant`. The `tenant`
|
|
268
|
+
option is deprecated and is not used for authentication in 8.x.
|
|
269
|
+
|
|
270
|
+
## Using with Chef
|
|
271
|
+
|
|
272
|
+
This driver is not tied to Cinc. The examples above use Cinc Workstation and the `cinc_infra` provisioner, but the driver works exactly the same with [Chef Workstation](https://www.chef.io/downloads/tools/workstation) — run `kitchen` instead of `cinc kitchen`, and use `chef_infra` instead of `cinc_infra`:
|
|
273
|
+
|
|
274
|
+
```yaml
|
|
275
|
+
provisioner:
|
|
276
|
+
name: chef_infra
|
|
277
|
+
|
|
278
|
+
verifier:
|
|
279
|
+
name: inspec
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
No driver configuration changes are needed.
|
|
54
283
|
|
|
55
284
|
## Contributing
|
|
56
285
|
|
|
57
|
-
We'd love to hear from you if this doesn't work in your vRA environment.
|
|
286
|
+
We'd love to hear from you if this doesn't work in your vRA environment. Bug reports and pull requests are welcome on [GitHub](https://github.com/test-kitchen/kitchen-vra). See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, how to run the tests, and the release process.
|
|
287
|
+
|
|
288
|
+
## License and Authors
|
|
289
|
+
|
|
290
|
+
Author: Chef Partner Engineering (<partnereng@chef.io>)
|
|
58
291
|
|
|
59
|
-
|
|
60
|
-
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
61
|
-
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
62
|
-
4. Push to the branch (`git push origin my-new-feature`)
|
|
63
|
-
5. Create a new Pull Request
|
|
292
|
+
Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for details.
|
data/Rakefile
CHANGED
|
@@ -3,13 +3,31 @@ require "rspec/core/rake_task"
|
|
|
3
3
|
RSpec::Core::RakeTask.new(:test)
|
|
4
4
|
|
|
5
5
|
begin
|
|
6
|
-
require "chefstyle"
|
|
6
|
+
require "cookstyle/chefstyle"
|
|
7
7
|
require "rubocop/rake_task"
|
|
8
8
|
RuboCop::RakeTask.new(:style) do |task|
|
|
9
9
|
task.options += ["--display-cop-names", "--no-color"]
|
|
10
10
|
end
|
|
11
11
|
rescue LoadError
|
|
12
|
-
puts "chefstyle is not available. (sudo) gem install
|
|
12
|
+
puts "cookstyle/chefstyle is not available. (sudo) gem install cookstyle to do style checking."
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
task default: %i{test style}
|
|
16
|
+
|
|
17
|
+
begin
|
|
18
|
+
require "yard"
|
|
19
|
+
|
|
20
|
+
# Options and the file list live in .yardopts so that a bare `yard` from the
|
|
21
|
+
# command line produces exactly what `rake doc` does.
|
|
22
|
+
YARD::Rake::YardocTask.new(:doc)
|
|
23
|
+
|
|
24
|
+
desc "List anything in lib/ that is still undocumented"
|
|
25
|
+
task :doc_coverage do
|
|
26
|
+
sh "yard stats --list-undoc"
|
|
27
|
+
end
|
|
28
|
+
rescue LoadError
|
|
29
|
+
desc "Generate YARD documentation (not installed)"
|
|
30
|
+
task :doc do
|
|
31
|
+
abort "YARD is not installed. Run: bundle install"
|
|
32
|
+
end
|
|
33
|
+
end
|
data/kitchen-vra.gemspec
CHANGED
|
@@ -7,7 +7,7 @@ require "kitchen/driver/vra_version"
|
|
|
7
7
|
Gem::Specification.new do |spec|
|
|
8
8
|
spec.name = "kitchen-vra"
|
|
9
9
|
spec.version = Kitchen::Driver::VRA_VERSION
|
|
10
|
-
spec.authors = ["Chef
|
|
10
|
+
spec.authors = ["Chef Community Tools Team"]
|
|
11
11
|
spec.email = ["oss@chef.io"]
|
|
12
12
|
spec.summary = "A Test Kitchen driver for VMware vRealize Automation (vRA)"
|
|
13
13
|
spec.description = spec.summary
|
|
@@ -16,7 +16,6 @@ Gem::Specification.new do |spec|
|
|
|
16
16
|
|
|
17
17
|
spec.files = `git ls-files -z`.split("\x0")
|
|
18
18
|
spec.executables = []
|
|
19
|
-
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
20
19
|
spec.require_paths = ["lib"]
|
|
21
20
|
|
|
22
21
|
spec.required_ruby_version = ">= 3.1"
|
data/lib/kitchen/driver/vra.rb
CHANGED
|
@@ -22,16 +22,42 @@ require "kitchen"
|
|
|
22
22
|
require "highline/import"
|
|
23
23
|
require "openssl" unless defined?(OpenSSL)
|
|
24
24
|
require "base64" unless defined?(Base64)
|
|
25
|
-
require "digest
|
|
25
|
+
require "digest" unless defined?(Digest)
|
|
26
|
+
require "fileutils" unless defined?(FileUtils)
|
|
26
27
|
require "vra"
|
|
27
28
|
require_relative "vra_version"
|
|
28
29
|
|
|
29
30
|
module Kitchen
|
|
31
|
+
# Test Kitchen's driver plugins.
|
|
30
32
|
module Driver
|
|
33
|
+
# Test Kitchen driver for VMware vRealize Automation (vRA) 8.x.
|
|
34
|
+
#
|
|
35
|
+
# Unlike a cloud driver that creates a machine directly, this driver
|
|
36
|
+
# submits a request against a vRA catalog item and waits for vRA's own
|
|
37
|
+
# automation to build a deployment. The blueprint behind that catalog item
|
|
38
|
+
# decides what gets built; this driver only supplies the image, flavor,
|
|
39
|
+
# and project, then waits for a single VM to come back.
|
|
40
|
+
#
|
|
41
|
+
# The blueprint must return exactly one VM. A blueprint returning several,
|
|
42
|
+
# or none, fails the +create+ rather than guessing which one to test.
|
|
43
|
+
#
|
|
44
|
+
# @see https://www.vmware.com/products/vrealize-automation.html vRealize Automation
|
|
31
45
|
class Vra < Kitchen::Driver::Base # rubocop:disable Metrics/ClassLength
|
|
32
46
|
kitchen_driver_api_version 2
|
|
33
47
|
plugin_version Kitchen::Driver::VRA_VERSION
|
|
34
48
|
|
|
49
|
+
# Location of the credential cache, relative to the working directory.
|
|
50
|
+
CREDENTIALS_CACHE_FILE = ".kitchen/cached_vra"
|
|
51
|
+
|
|
52
|
+
# Cipher used for the credential cache. GCM is authenticated, so a cache
|
|
53
|
+
# written for a different +base_url+, or one that has been altered on
|
|
54
|
+
# disk, fails to decrypt rather than yielding garbage credentials.
|
|
55
|
+
CREDENTIALS_CIPHER = "aes-256-gcm"
|
|
56
|
+
|
|
57
|
+
# Marker for the cache file layout, so a later format change can reject
|
|
58
|
+
# old files instead of misreading them.
|
|
59
|
+
CREDENTIALS_CACHE_VERSION = "v1"
|
|
60
|
+
|
|
35
61
|
default_config :username, nil
|
|
36
62
|
default_config :password, nil
|
|
37
63
|
required_config :base_url
|
|
@@ -68,10 +94,20 @@ module Kitchen
|
|
|
68
94
|
In vRA 8.x, the 'tenant' configuration is no longer relevant for authentication.
|
|
69
95
|
Please use the 'domain' configuration in its place.".dup)
|
|
70
96
|
|
|
97
|
+
# @return [String] the driver's display name in `kitchen list`
|
|
71
98
|
def name
|
|
72
99
|
"vRA"
|
|
73
100
|
end
|
|
74
101
|
|
|
102
|
+
# Resolves the vRA username and password, prompting if necessary.
|
|
103
|
+
#
|
|
104
|
+
# Sources are tried in order: explicit config, then the +VRA_USER_NAME+
|
|
105
|
+
# and +VRA_USER_PASSWORD+ environment variables, then the credential
|
|
106
|
+
# cache, then an interactive prompt.
|
|
107
|
+
#
|
|
108
|
+
# @param force_change [Boolean] prompt even if credentials already
|
|
109
|
+
# resolved, used to re-ask after an authentication failure
|
|
110
|
+
# @return [void]
|
|
75
111
|
def check_config(force_change = false)
|
|
76
112
|
config[:username] = config[:username] || ENV["VRA_USER_NAME"]
|
|
77
113
|
config[:password] = config[:password] || ENV["VRA_USER_PASSWORD"]
|
|
@@ -82,43 +118,104 @@ module Kitchen
|
|
|
82
118
|
c_save if config[:cache_credentials]
|
|
83
119
|
end
|
|
84
120
|
|
|
121
|
+
# Writes the resolved credentials to {CREDENTIALS_CACHE_FILE}.
|
|
122
|
+
#
|
|
123
|
+
# The file is obfuscated rather than secured: the key is derived from
|
|
124
|
+
# +base_url+, which is not a secret, so anyone holding both the file and
|
|
125
|
+
# the kitchen config can recover the credentials. It keeps passwords out
|
|
126
|
+
# of plain sight on disk; it is not a substitute for a secret store.
|
|
127
|
+
#
|
|
128
|
+
# @return [void]
|
|
85
129
|
def c_save
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
file.write(output)
|
|
98
|
-
file.close
|
|
99
|
-
rescue
|
|
100
|
-
puts "Unable to save credentials"
|
|
130
|
+
FileUtils.mkdir_p(File.dirname(CREDENTIALS_CACHE_FILE))
|
|
131
|
+
fields = [config[:username], config[:password]].flat_map { |value| encrypt_credential(value) }
|
|
132
|
+
|
|
133
|
+
File.open(CREDENTIALS_CACHE_FILE, File::WRONLY | File::CREAT | File::TRUNC, 0o600) do |file|
|
|
134
|
+
file.write(([CREDENTIALS_CACHE_VERSION] + fields).join(":"))
|
|
135
|
+
end
|
|
136
|
+
# The mode above only applies when the file is created, so narrow the
|
|
137
|
+
# permissions of a cache left behind by an earlier run as well.
|
|
138
|
+
File.chmod(0o600, CREDENTIALS_CACHE_FILE)
|
|
139
|
+
rescue => e
|
|
140
|
+
warn("Unable to save credentials to #{CREDENTIALS_CACHE_FILE}: #{e.message}")
|
|
101
141
|
end
|
|
102
142
|
|
|
143
|
+
# Reads credentials back from {CREDENTIALS_CACHE_FILE}.
|
|
144
|
+
#
|
|
145
|
+
# A cache that cannot be decrypted -- a different +base_url+, a truncated
|
|
146
|
+
# or edited file, an older layout -- is reported and ignored, leaving the
|
|
147
|
+
# credentials unset so the caller falls through to prompting.
|
|
148
|
+
#
|
|
149
|
+
# @return [void]
|
|
103
150
|
def c_load
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
151
|
+
return unless File.exist?(CREDENTIALS_CACHE_FILE)
|
|
152
|
+
|
|
153
|
+
version, *fields = File.read(CREDENTIALS_CACHE_FILE).strip.split(":")
|
|
154
|
+
raise "unrecognized cache format" unless version == CREDENTIALS_CACHE_VERSION && fields.length == 6
|
|
155
|
+
|
|
156
|
+
# Decrypt both before assigning either, so a partially readable cache
|
|
157
|
+
# cannot leave half the credentials set.
|
|
158
|
+
username = decrypt_credential(*fields[0, 3])
|
|
159
|
+
password = decrypt_credential(*fields[3, 3])
|
|
160
|
+
|
|
161
|
+
config[:username] = username
|
|
162
|
+
config[:password] = password
|
|
163
|
+
rescue => e
|
|
164
|
+
warn("Failed to load cached credentials from #{CREDENTIALS_CACHE_FILE}: #{e.message}")
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Encrypts one credential for the cache file.
|
|
168
|
+
#
|
|
169
|
+
# @param value [String] the credential to encrypt
|
|
170
|
+
# @return [Array<String>] Base64-encoded IV, authentication tag, and
|
|
171
|
+
# ciphertext, in that order
|
|
172
|
+
def encrypt_credential(value)
|
|
173
|
+
cipher = OpenSSL::Cipher.new(CREDENTIALS_CIPHER)
|
|
174
|
+
cipher.encrypt
|
|
175
|
+
cipher.key = credentials_key
|
|
176
|
+
iv = cipher.random_iv
|
|
177
|
+
encrypted = cipher.update(value.to_s) + cipher.final
|
|
178
|
+
|
|
179
|
+
[iv, cipher.auth_tag, encrypted].map { |part| Base64.strict_encode64(part) }
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Decrypts one credential from the cache file.
|
|
183
|
+
#
|
|
184
|
+
# @param iv [String] Base64-encoded initialization vector
|
|
185
|
+
# @param auth_tag [String] Base64-encoded GCM authentication tag
|
|
186
|
+
# @param encrypted [String] Base64-encoded ciphertext
|
|
187
|
+
# @return [String] the decrypted credential
|
|
188
|
+
# @raise [OpenSSL::Cipher::CipherError] if the key or tag does not match
|
|
189
|
+
def decrypt_credential(iv, auth_tag, encrypted)
|
|
190
|
+
cipher = OpenSSL::Cipher.new(CREDENTIALS_CIPHER)
|
|
191
|
+
cipher.decrypt
|
|
192
|
+
cipher.key = credentials_key
|
|
193
|
+
cipher.iv = Base64.strict_decode64(iv)
|
|
194
|
+
cipher.auth_tag = Base64.strict_decode64(auth_tag)
|
|
195
|
+
|
|
196
|
+
cipher.update(Base64.strict_decode64(encrypted)) + cipher.final
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Derives the cache key from +base_url+.
|
|
200
|
+
#
|
|
201
|
+
# SHA-256 is used for its digest length: {CREDENTIALS_CIPHER} requires a
|
|
202
|
+
# 32-byte key, which +Digest::SHA256.digest+ returns exactly.
|
|
203
|
+
#
|
|
204
|
+
# @return [String] a 32-byte key
|
|
205
|
+
def credentials_key
|
|
206
|
+
Digest::SHA256.digest(config[:base_url].to_s)
|
|
120
207
|
end
|
|
121
208
|
|
|
209
|
+
# Requests a deployment from vRA and waits until it can be logged into.
|
|
210
|
+
#
|
|
211
|
+
# Returns immediately if the state already names a deployment, so a
|
|
212
|
+
# re-run does not build a second one.
|
|
213
|
+
#
|
|
214
|
+
# @param state [Hash] mutable instance state; gains +deployment_id+,
|
|
215
|
+
# +hostname+, and +ssh_key+
|
|
216
|
+
# @return [void]
|
|
217
|
+
# @raise [RuntimeError] if the vRA request fails or does not yield
|
|
218
|
+
# exactly one VM
|
|
122
219
|
def create(state)
|
|
123
220
|
return if state[:deployment_id]
|
|
124
221
|
|
|
@@ -131,6 +228,15 @@ module Kitchen
|
|
|
131
228
|
info("Server #{server.deployment_id} (#{server.name}) ready.")
|
|
132
229
|
end
|
|
133
230
|
|
|
231
|
+
# Works out the address Test Kitchen should connect to.
|
|
232
|
+
#
|
|
233
|
+
# With +use_dns+ set, the server's name is used, optionally suffixed with
|
|
234
|
+
# +dns_suffix+. Otherwise the IP address is preferred, falling back to
|
|
235
|
+
# the name with a warning when vRA reports no address.
|
|
236
|
+
#
|
|
237
|
+
# @param server [Vra::Resource] the VM vRA built
|
|
238
|
+
# @return [String] a hostname or IP address
|
|
239
|
+
# @raise [RuntimeError] if +use_dns+ is set but vRA returned no name
|
|
134
240
|
def hostname_for(server)
|
|
135
241
|
if config[:use_dns]
|
|
136
242
|
raise "No server name returned for the vRA request" if server.name.nil?
|
|
@@ -147,6 +253,11 @@ module Kitchen
|
|
|
147
253
|
end
|
|
148
254
|
end
|
|
149
255
|
|
|
256
|
+
# Submits the catalog request and waits for vRA to finish building it.
|
|
257
|
+
#
|
|
258
|
+
# @return [Vra::Resource] the single VM the blueprint produced
|
|
259
|
+
# @raise [RuntimeError] if the request failed, or produced zero or more
|
|
260
|
+
# than one VM
|
|
150
261
|
def request_server
|
|
151
262
|
info("Building vRA catalog request...")
|
|
152
263
|
|
|
@@ -167,6 +278,16 @@ module Kitchen
|
|
|
167
278
|
servers.first
|
|
168
279
|
end
|
|
169
280
|
|
|
281
|
+
# Waits for the transport to accept a connection, retrying on failure.
|
|
282
|
+
#
|
|
283
|
+
# Backs off in five-second steps up to thirty seconds between attempts.
|
|
284
|
+
# Once +server_ready_retries+ is exceeded the deployment is destroyed
|
|
285
|
+
# before the error is re-raised, so a machine that never comes up is not
|
|
286
|
+
# left running and billable.
|
|
287
|
+
#
|
|
288
|
+
# @param state [Hash] instance state describing how to connect
|
|
289
|
+
# @param server [Vra::Resource] the VM being waited on
|
|
290
|
+
# @return [void]
|
|
170
291
|
def wait_for_server(state, server)
|
|
171
292
|
info("Server #{server.id} (#{server.name}) created. Waiting until ready...")
|
|
172
293
|
|
|
@@ -193,6 +314,14 @@ module Kitchen
|
|
|
193
314
|
end
|
|
194
315
|
end
|
|
195
316
|
|
|
317
|
+
# Destroys the vRA deployment, if one exists.
|
|
318
|
+
#
|
|
319
|
+
# A deployment that vRA no longer knows about, or that offers no destroy
|
|
320
|
+
# action, is treated as already gone rather than an error. The cached
|
|
321
|
+
# credentials file is removed afterwards.
|
|
322
|
+
#
|
|
323
|
+
# @param state [Hash] instance state naming the deployment
|
|
324
|
+
# @return [void]
|
|
196
325
|
def destroy(state)
|
|
197
326
|
return if state[:deployment_id].nil?
|
|
198
327
|
|
|
@@ -217,6 +346,14 @@ module Kitchen
|
|
|
217
346
|
info("Removed cached file")
|
|
218
347
|
end
|
|
219
348
|
|
|
349
|
+
# Builds the vRA catalog request for the configured blueprint.
|
|
350
|
+
#
|
|
351
|
+
# When +catalog_name+ is given it is resolved to a catalog ID first.
|
|
352
|
+
# A deployment name is only sent when +unique_name+ is false; otherwise
|
|
353
|
+
# vRA is left to name the deployment after its request ID.
|
|
354
|
+
#
|
|
355
|
+
# @return [Vra::CatalogRequest] a request ready to submit
|
|
356
|
+
# @raise [Kitchen::InstanceFailure] if no catalog could be resolved
|
|
220
357
|
def catalog_request # rubocop:disable Metrics/MethodLength
|
|
221
358
|
unless config[:catalog_name].nil?
|
|
222
359
|
info("Fetching Catalog ID by Catalog Name")
|
|
@@ -251,6 +388,12 @@ module Kitchen
|
|
|
251
388
|
catalog_request
|
|
252
389
|
end
|
|
253
390
|
|
|
391
|
+
# The vRA API client, built from the resolved credentials.
|
|
392
|
+
#
|
|
393
|
+
# On any failure the credentials are re-prompted, on the assumption that
|
|
394
|
+
# what failed was authentication.
|
|
395
|
+
#
|
|
396
|
+
# @return [Vra::Client]
|
|
254
397
|
def vra_client
|
|
255
398
|
check_config config[:cache_credentials]
|
|
256
399
|
@client ||= ::Vra::Client.new(
|
|
@@ -264,6 +407,14 @@ module Kitchen
|
|
|
264
407
|
check_config true
|
|
265
408
|
end
|
|
266
409
|
|
|
410
|
+
# Polls a vRA request until it completes or times out.
|
|
411
|
+
#
|
|
412
|
+
# Polls every +request_refresh_rate+ seconds, logging each change of
|
|
413
|
+
# status, and gives up after +request_timeout+ seconds.
|
|
414
|
+
#
|
|
415
|
+
# @param request [Vra::Request] the request to poll
|
|
416
|
+
# @return [void]
|
|
417
|
+
# @raise [Timeout::Error] if the request does not complete in time
|
|
267
418
|
def wait_for_request(request)
|
|
268
419
|
# config = check_config config
|
|
269
420
|
|