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.
data/README.md CHANGED
@@ -1,63 +1,292 @@
1
1
  # kitchen-vra
2
2
 
3
- A driver to allow Test Kitchen to consume vRealize Automation (vRA) resources to perform testing.
3
+ [![Gem Version](https://badge.fury.io/rb/kitchen-vra.svg)](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
- Add this line to your application's Gemfile:
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 'kitchen-vra'
31
+ gem "kitchen-vra"
11
32
  ```
12
33
 
13
- And then execute:
34
+ ...then run `bundle install`.
35
+
36
+ ## Authentication
37
+
38
+ Credentials are resolved in this order:
14
39
 
15
- ```shell
16
- bundle
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
- Or install it yourself as:
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
- ```shell
22
- gem install kitchen-vra
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
- Or even better, install it via ChefDK:
83
+ Then run the full test cycle:
26
84
 
27
- ```shell
28
- chef gem install kitchen-vra
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
- See the [kitchen.ci kitchen-vra docs](https://kitchen.ci/docs/drivers/vra/) for a complete list of configuration options.
100
+ All options below are set under the `driver:` key in `kitchen.yml`.
34
101
 
35
- ## License and Authors
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
- Author:: Chef Partner Engineering (<partnereng@chef.io>)
140
+ ### Connectivity
38
141
 
39
- Copyright:: Copyright (c) 2022 Chef Software, Inc.
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
- License:: Apache License, Version 2.0
148
+ ### Timing
42
149
 
43
- Licensed under the Apache License, Version 2.0 (the "License"); you may not use
44
- this file except in compliance with the License. You may obtain a copy of the License at
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
- ```text
47
- http://www.apache.org/licenses/LICENSE-2.0
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
- Unless required by applicable law or agreed to in writing, software distributed under the
51
- License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
52
- either express or implied. See the License for the specific language governing permissions
53
- and limitations under the License.
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. Please log a GitHub issue, or even better, submit a Pull Request with a fix!
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
- 1. Fork it ( <https://github.com/chef-partners/kitchen-vra/fork> )
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 chefstyle to do style checking."
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 Commmunity Tools Team"]
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"
@@ -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/sha1" unless defined?(Digest::SHA1)
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
- cipher = OpenSSL::Cipher.new("cip-her-aes")
87
- cipher.encrypt
88
- cipher.key = Digest::SHA1.hexdigest(config[:base_url])
89
- iv_user = cipher.random_iv
90
- cipher.iv = iv_user
91
- username = cipher.update(config[:username]) + cipher.final
92
- iv_pwd = cipher.random_iv
93
- cipher.iv = iv_pwd
94
- password = cipher.update(config[:password]) + cipher.final
95
- output = "#{Base64.encode64(iv_user).strip!}:#{Base64.encode64(username).strip!}:#{Base64.encode64(iv_pwd).strip!}:#{Base64.encode64(password).strip!}"
96
- file = File.open(".kitchen/cached_vra", "w")
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
- if File.exist? ".kitchen/cached_vra"
105
- encrypted = File.read(".kitchen/cached_vra")
106
- iv_user = Base64.decode64(encrypted.split(":")[0] + '\n')
107
- username = Base64.decode64(encrypted.split(":")[1] + "\n")
108
- iv_pwd = Base64.decode64(encrypted.split(":")[2] + "\n")
109
- password = Base64.decode64(encrypted.split(":")[3] + "\n")
110
- cipher = OpenSSL::Cipher.new("cip-her-aes")
111
- cipher.decrypt
112
- cipher.key = Digest::SHA1.hexdigest(config[:base_url])
113
- cipher.iv = iv_user
114
- config[:username] = cipher.update(username) + cipher.final
115
- cipher.iv = iv_pwd
116
- config[:password] = cipher.update(password) + cipher.final
117
- end
118
- rescue
119
- puts "Failed to load cached credentials"
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