kitchen-azurerm 2.1.4 → 2.4.0
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f2e163e7f873cd30975117e7959c9f860980e67c6a57a57cceea3b5784a15c88
|
|
4
|
+
data.tar.gz: 842ce181806cd18fd1ce064222311cb24f7702b5c569f9e57c1971ef0e8a2843
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 57922ce563572966e695342e34e01eb89c04bb30528c0931baa41b28bcf043ed2d6ccbd45141df8febbbc1739b038d9307ed666b0bd349b33b626df685f2ca70
|
|
7
|
+
data.tar.gz: 44e874be227fa2625d1b73091c80b1402e548b5bf7851da030737d80f58f479409585c7b5248d95c67e42d536bbada211127612cd037a38bf4b2257b214c66bc
|
|
@@ -25,6 +25,18 @@ module Kitchen
|
|
|
25
25
|
# @return [String]
|
|
26
26
|
NETWORK_API_VERSION = "2025-07-01".freeze
|
|
27
27
|
|
|
28
|
+
# ARM API version used for compute resource requests. Matches the
|
|
29
|
+
# version the bundled deployment templates declare for virtual
|
|
30
|
+
# machines.
|
|
31
|
+
#
|
|
32
|
+
# @return [String]
|
|
33
|
+
COMPUTE_API_VERSION = "2025-04-01".freeze
|
|
34
|
+
|
|
35
|
+
# ARM API version used to read the subscription itself.
|
|
36
|
+
#
|
|
37
|
+
# @return [String]
|
|
38
|
+
SUBSCRIPTION_API_VERSION = "2022-12-01".freeze
|
|
39
|
+
|
|
28
40
|
# @param subscription_id [String]
|
|
29
41
|
# @param environment [Environments::Environment]
|
|
30
42
|
# @param token_provider [TokenProvider]
|
|
@@ -43,6 +55,19 @@ module Kitchen
|
|
|
43
55
|
# @return [TokenProvider]
|
|
44
56
|
attr_reader :token_provider
|
|
45
57
|
|
|
58
|
+
# Reads the subscription itself.
|
|
59
|
+
#
|
|
60
|
+
# Unlike a resource group HEAD - which answers 404 both for a
|
|
61
|
+
# subscription that does not exist and for one that merely has no such
|
|
62
|
+
# group - this distinguishes a reachable subscription from a wrong
|
|
63
|
+
# one, so it is what {Azurerm#doctor} probes with.
|
|
64
|
+
#
|
|
65
|
+
# @return [Hash]
|
|
66
|
+
def subscription
|
|
67
|
+
call(:get, "/subscriptions/#{escape(subscription_id)}",
|
|
68
|
+
api_version: SUBSCRIPTION_API_VERSION).json
|
|
69
|
+
end
|
|
70
|
+
|
|
46
71
|
# Whether a resource group exists.
|
|
47
72
|
#
|
|
48
73
|
# @param name [String] resource group name, case-insensitive.
|
|
@@ -115,6 +140,17 @@ module Kitchen
|
|
|
115
140
|
payload.is_a?(Hash) ? Array(payload["value"]) : []
|
|
116
141
|
end
|
|
117
142
|
|
|
143
|
+
# Reads a virtual machine's instance view, which carries its current
|
|
144
|
+
# power state.
|
|
145
|
+
#
|
|
146
|
+
# @param resource_group [String]
|
|
147
|
+
# @param name [String] virtual machine name.
|
|
148
|
+
# @return [Hash]
|
|
149
|
+
def virtual_machine_instance_view(resource_group, name)
|
|
150
|
+
call(:get, "#{compute_path(resource_group, "virtualMachines", name)}/instanceView",
|
|
151
|
+
api_version: COMPUTE_API_VERSION).json
|
|
152
|
+
end
|
|
153
|
+
|
|
118
154
|
# Reads a public IP address resource.
|
|
119
155
|
#
|
|
120
156
|
# @param resource_group [String]
|
|
@@ -158,6 +194,14 @@ module Kitchen
|
|
|
158
194
|
"#{resource_group_path(resource_group)}/providers/Microsoft.Network/#{type}/#{escape(name)}"
|
|
159
195
|
end
|
|
160
196
|
|
|
197
|
+
# @param resource_group [String]
|
|
198
|
+
# @param type [String] e.g. +"virtualMachines"+.
|
|
199
|
+
# @param name [String]
|
|
200
|
+
# @return [String]
|
|
201
|
+
def compute_path(resource_group, type, name)
|
|
202
|
+
"#{resource_group_path(resource_group)}/providers/Microsoft.Compute/#{type}/#{escape(name)}"
|
|
203
|
+
end
|
|
204
|
+
|
|
161
205
|
# @param value [String]
|
|
162
206
|
# @return [String] path-escaped
|
|
163
207
|
def escape(value)
|
|
@@ -63,6 +63,10 @@ module Kitchen
|
|
|
63
63
|
# Every supported cloud, keyed by its downcased name so that lookups are
|
|
64
64
|
# case-insensitive.
|
|
65
65
|
#
|
|
66
|
+
# +AzureGermanCloud+ is kept only so that an existing kitchen.yml naming
|
|
67
|
+
# it still loads: Microsoft closed that cloud in 2021, and its endpoints
|
|
68
|
+
# no longer answer.
|
|
69
|
+
#
|
|
66
70
|
# @return [Hash{String => Environment}]
|
|
67
71
|
ALL = [
|
|
68
72
|
Environment.new("Azure",
|
|
@@ -22,11 +22,12 @@ module Kitchen
|
|
|
22
22
|
|
|
23
23
|
# The HTTP status code of the failing response.
|
|
24
24
|
#
|
|
25
|
-
# @return [Integer]
|
|
25
|
+
# @return [Integer, nil] nil when the error was raised before a
|
|
26
|
+
# response arrived, as the token providers do.
|
|
26
27
|
attr_reader :status
|
|
27
28
|
|
|
28
29
|
# @param message [String] human-readable summary.
|
|
29
|
-
# @param status [Integer] HTTP status code.
|
|
30
|
+
# @param status [Integer, nil] HTTP status code, when there was one.
|
|
30
31
|
# @param body [Hash] parsed ARM error body.
|
|
31
32
|
def initialize(message, status: nil, body: {})
|
|
32
33
|
super(message)
|
|
@@ -40,6 +41,17 @@ module Kitchen
|
|
|
40
41
|
def code
|
|
41
42
|
body.is_a?(Hash) ? body.dig("error", "code") : nil
|
|
42
43
|
end
|
|
44
|
+
|
|
45
|
+
# Azure's own explanation, when ARM supplied one.
|
|
46
|
+
#
|
|
47
|
+
# +message+ is this class's summary of the request that failed; this
|
|
48
|
+
# is what Azure said about it, which is usually the part worth showing
|
|
49
|
+
# somebody.
|
|
50
|
+
#
|
|
51
|
+
# @return [String, nil]
|
|
52
|
+
def detail
|
|
53
|
+
body.is_a?(Hash) ? body.dig("error", "message") : nil
|
|
54
|
+
end
|
|
43
55
|
end
|
|
44
56
|
|
|
45
57
|
# Raised when a request could not be completed and is worth retrying:
|
|
@@ -2,6 +2,7 @@ require "kitchen"
|
|
|
2
2
|
|
|
3
3
|
require_relative "azure_credentials"
|
|
4
4
|
require_relative "azure/errors"
|
|
5
|
+
require_relative "azurerm_version"
|
|
5
6
|
require "securerandom" unless defined?(SecureRandom)
|
|
6
7
|
require "base64" unless defined?(Base64)
|
|
7
8
|
autoload :SSHKey, "sshkey"
|
|
@@ -29,8 +30,21 @@ module Kitchen
|
|
|
29
30
|
# @return [Azure::ArmClient, nil]
|
|
30
31
|
attr_accessor :arm_client
|
|
31
32
|
|
|
33
|
+
# The writer needs a docstring of its own: one comment above an
|
|
34
|
+
# attr_accessor is copied to both methods, so the argument the writer
|
|
35
|
+
# takes can only be described here. The directive has to come after the
|
|
36
|
+
# accessor - placed before it, the accessor overwrites it.
|
|
37
|
+
|
|
38
|
+
# @!method arm_client=(client)
|
|
39
|
+
# Replaces the ARM client.
|
|
40
|
+
#
|
|
41
|
+
# @param client [Azure::ArmClient, nil] the client to use.
|
|
42
|
+
# @return [Azure::ArmClient, nil] the client that was set.
|
|
43
|
+
|
|
32
44
|
kitchen_driver_api_version 2
|
|
33
45
|
|
|
46
|
+
plugin_version Kitchen::Driver::AZURERM_VERSION
|
|
47
|
+
|
|
34
48
|
# Settings that Azure retirements have made inoperable. They are still
|
|
35
49
|
# accepted so that an existing kitchen.yml keeps loading, but they no
|
|
36
50
|
# longer do anything and {#warn_about_deprecated_config} says so.
|
|
@@ -66,8 +80,12 @@ module Kitchen
|
|
|
66
80
|
|
|
67
81
|
# Ubuntu 22.04 LTS, generation 2. Canonical renamed their offers after
|
|
68
82
|
# 18.04, so the old "UbuntuServer" offer no longer resolves at all.
|
|
83
|
+
#
|
|
84
|
+
# @return [String]
|
|
85
|
+
DEFAULT_IMAGE_URN = "Canonical:0001-com-ubuntu-server-jammy:22_04-lts-gen2:latest".freeze
|
|
86
|
+
|
|
69
87
|
default_config(:image_urn) do |_config|
|
|
70
|
-
|
|
88
|
+
DEFAULT_IMAGE_URN
|
|
71
89
|
end
|
|
72
90
|
|
|
73
91
|
default_config(:image_id) do |_config|
|
|
@@ -249,6 +267,8 @@ module Kitchen
|
|
|
249
267
|
# @param state [Hash] the instance state, mutated in place.
|
|
250
268
|
# @return [void]
|
|
251
269
|
# @raise [RuntimeError] if no +subscription_id+ can be resolved.
|
|
270
|
+
# @raise [Kitchen::UserError] if +os_disk_size_gb+ is not a whole number,
|
|
271
|
+
# or the public half of the transport's SSH key cannot be found.
|
|
252
272
|
# @raise [Azure::OperationError] if an Azure API call fails
|
|
253
273
|
# for any reason other than an already-running deployment.
|
|
254
274
|
def create(state)
|
|
@@ -273,12 +293,12 @@ module Kitchen
|
|
|
273
293
|
end
|
|
274
294
|
|
|
275
295
|
begin
|
|
276
|
-
run_deployment(state, "pre-deploy", pre_deployment(config[:pre_deployment_template], config[:pre_deployment_parameters])) if File.file?(config[:pre_deployment_template])
|
|
296
|
+
run_deployment(state, "pre-deploy", pre_deployment(config[:pre_deployment_template], config[:pre_deployment_parameters])) if File.file?(config[:pre_deployment_template].to_s)
|
|
277
297
|
|
|
278
298
|
run_deployment(state, "deploy", deployment(deployment_parameters))
|
|
279
299
|
store_deployment_credentials(state, deployment_parameters)
|
|
280
300
|
|
|
281
|
-
run_deployment(state, "post-deploy", post_deployment(config[:post_deployment_template], config[:post_deployment_parameters])) if File.file?(config[:post_deployment_template])
|
|
301
|
+
run_deployment(state, "post-deploy", post_deployment(config[:post_deployment_template], config[:post_deployment_parameters])) if File.file?(config[:post_deployment_template].to_s)
|
|
282
302
|
rescue Azure::OperationError => operation_error
|
|
283
303
|
info operation_error.body["error"]
|
|
284
304
|
raise operation_error
|
|
@@ -287,6 +307,79 @@ module Kitchen
|
|
|
287
307
|
state[:hostname] = resolve_hostname(state, deployment_parameters["nicName"])
|
|
288
308
|
end
|
|
289
309
|
|
|
310
|
+
# Settings the driver cannot supply a default for, and what they are.
|
|
311
|
+
#
|
|
312
|
+
# @return [Hash{Symbol => String}]
|
|
313
|
+
REQUIRED_CONFIG = {
|
|
314
|
+
subscription_id: "the Azure subscription to deploy into",
|
|
315
|
+
location: "the Azure region to deploy into, e.g. eastus",
|
|
316
|
+
machine_size: "the VM size to deploy, e.g. Standard_D2s_v3",
|
|
317
|
+
}.freeze
|
|
318
|
+
|
|
319
|
+
# Checks configuration and credentials, for +kitchen doctor+.
|
|
320
|
+
#
|
|
321
|
+
# Deployment failures are usually one of two things: a required setting
|
|
322
|
+
# nobody filled in, or credentials that do not work. Both otherwise
|
|
323
|
+
# surface as an Azure error partway through a create, once the resource
|
|
324
|
+
# group already exists, so this looks for them up front.
|
|
325
|
+
#
|
|
326
|
+
# @param _state [Hash] the instance state. Test Kitchen passes it, but
|
|
327
|
+
# the checks are about configuration and credentials, which exist
|
|
328
|
+
# before any instance does, so it is ignored.
|
|
329
|
+
# @return [Boolean] true when a problem was found, as +kitchen doctor+
|
|
330
|
+
# expects.
|
|
331
|
+
def doctor(_state)
|
|
332
|
+
problems = missing_required_config
|
|
333
|
+
problems += unreachable_azure if problems.empty?
|
|
334
|
+
problems.each { |problem| error("kitchen-azurerm: #{problem}") }
|
|
335
|
+
problems.any?
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Required settings that were left unset.
|
|
339
|
+
#
|
|
340
|
+
# @return [Array<String>]
|
|
341
|
+
def missing_required_config
|
|
342
|
+
REQUIRED_CONFIG.select { |option, _| config[option].to_s.empty? }
|
|
343
|
+
.map { |option, purpose| "#{option} is not set. It has no default: give it #{purpose}." }
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Whether Azure answers, accepts the credentials, and knows the
|
|
347
|
+
# subscription.
|
|
348
|
+
#
|
|
349
|
+
# Reads the subscription rather than probing a resource group: ARM
|
|
350
|
+
# answers a resource group HEAD with 404 both for a subscription that
|
|
351
|
+
# does not exist and for one that merely has no such group, so it cannot
|
|
352
|
+
# tell a wrong subscription from a healthy one. Reading the subscription
|
|
353
|
+
# gives back either its details or +SubscriptionNotFound+.
|
|
354
|
+
#
|
|
355
|
+
# @return [Array<String>]
|
|
356
|
+
def unreachable_azure
|
|
357
|
+
Kitchen::Driver::AzureCredentials.new(subscription_id: config[:subscription_id],
|
|
358
|
+
environment: config[:azure_environment]).arm_client.subscription
|
|
359
|
+
[]
|
|
360
|
+
rescue Azure::OperationError => operation_error
|
|
361
|
+
[azure_problem(operation_error)]
|
|
362
|
+
rescue Azure::TransientError => transient_error
|
|
363
|
+
["Could not reach Azure (#{transient_error.message})."]
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Describes an OperationError raised while checking Azure.
|
|
367
|
+
#
|
|
368
|
+
# An error carrying no Azure error code never reached ARM - it comes
|
|
369
|
+
# from acquiring the token, and already explains itself. Announcing that
|
|
370
|
+
# Azure rejected a request that was never made only sends the reader
|
|
371
|
+
# looking in the wrong place.
|
|
372
|
+
#
|
|
373
|
+
# @param operation_error [Azure::OperationError]
|
|
374
|
+
# @return [String]
|
|
375
|
+
def azure_problem(operation_error)
|
|
376
|
+
return operation_error.message unless operation_error.code
|
|
377
|
+
|
|
378
|
+
"Azure rejected the request (#{operation_error.code}: " \
|
|
379
|
+
"#{operation_error.detail || operation_error.message}). " \
|
|
380
|
+
"Check the credentials, and that they can reach subscription #{config[:subscription_id]}."
|
|
381
|
+
end
|
|
382
|
+
|
|
290
383
|
# Builds the ARM parameter values for the virtual machine deployment.
|
|
291
384
|
#
|
|
292
385
|
# @param state [Hash] instance state, already through {#validate_state}.
|
|
@@ -329,10 +422,23 @@ module Kitchen
|
|
|
329
422
|
def image_parameters
|
|
330
423
|
return { "imageId" => config[:image_id] } if config[:image_id].to_s != ""
|
|
331
424
|
|
|
332
|
-
publisher, offer, sku, version =
|
|
425
|
+
publisher, offer, sku, version = image_urn.split(":", 4)
|
|
333
426
|
{ "imagePublisher" => publisher, "imageOffer" => offer, "imageSku" => sku, "imageVersion" => version }
|
|
334
427
|
end
|
|
335
428
|
|
|
429
|
+
# The Marketplace image URN to deploy from.
|
|
430
|
+
#
|
|
431
|
+
# An +image_urn:+ written with no value is nil rather than "", which
|
|
432
|
+
# counts as unset here the way it does everywhere else in the driver -
|
|
433
|
+
# so it falls back to {DEFAULT_IMAGE_URN} rather than failing with a
|
|
434
|
+
# Ruby error naming neither the setting nor the file it came from.
|
|
435
|
+
#
|
|
436
|
+
# @return [String]
|
|
437
|
+
def image_urn
|
|
438
|
+
urn = config[:image_urn].to_s
|
|
439
|
+
urn.empty? ? DEFAULT_IMAGE_URN : urn
|
|
440
|
+
end
|
|
441
|
+
|
|
336
442
|
# The OS disk size, as a number ARM will accept.
|
|
337
443
|
#
|
|
338
444
|
# YAML makes +os_disk_size_gb: 64+ an Integer and +os_disk_size_gb: "64"+
|
|
@@ -451,14 +557,29 @@ module Kitchen
|
|
|
451
557
|
|
|
452
558
|
# Whether a state property is already populated.
|
|
453
559
|
#
|
|
560
|
+
# A blank value does not count. A run that fails early still writes its
|
|
561
|
+
# state, so an empty +subscription_id+ - which is what a quoted ERB
|
|
562
|
+
# interpolation of an unset variable produces - used to be taken as
|
|
563
|
+
# authoritative from then on, shadowing the real value in config for the
|
|
564
|
+
# rest of the instance's life.
|
|
565
|
+
#
|
|
566
|
+
# +false+ is a value, so this asks whether the value is blank rather
|
|
567
|
+
# than whether it is truthy.
|
|
568
|
+
#
|
|
454
569
|
# @param state [Hash] Hash of existing state values.
|
|
455
570
|
# @param property [Symbol, String] the property to check.
|
|
456
|
-
# @return [Boolean] true when the key exists and its value is not
|
|
571
|
+
# @return [Boolean] true when the key exists and its value is not blank.
|
|
457
572
|
def existing_state_value?(state, property)
|
|
458
|
-
state.key?(property) && !state[property].
|
|
573
|
+
state.key?(property) && !state[property].to_s.empty?
|
|
459
574
|
end
|
|
460
575
|
|
|
461
|
-
# Fills in any state values that are not already present
|
|
576
|
+
# Fills in any state values that are not already present, and drops the
|
|
577
|
+
# generated password when the transport authenticates with an SSH key.
|
|
578
|
+
#
|
|
579
|
+
# +use_managed_disks+ is copied across with the settings that are still
|
|
580
|
+
# read, even though Azure's retirement of unmanaged disks left nothing
|
|
581
|
+
# reading it - see {DEPRECATED_CONFIG}. It is written as nil, since the
|
|
582
|
+
# setting has no default.
|
|
462
583
|
#
|
|
463
584
|
# @param state [Hash] existing Hash of state values.
|
|
464
585
|
# @return [Hash] the same Hash, with defaults applied.
|
|
@@ -605,7 +726,12 @@ module Kitchen
|
|
|
605
726
|
KEY_GENERATION_MUTEX.synchronize do
|
|
606
727
|
generate_key_pair(private_key_filename) unless File.file?(private_key_filename)
|
|
607
728
|
|
|
608
|
-
|
|
729
|
+
explicit = instance.transport[:ssh_public_key]
|
|
730
|
+
public_key_filename = explicit || "#{private_key_filename}.pub"
|
|
731
|
+
unless File.file?(public_key_filename)
|
|
732
|
+
raise Kitchen::UserError, missing_public_key_message(private_key_filename, public_key_filename, explicit)
|
|
733
|
+
end
|
|
734
|
+
|
|
609
735
|
File.read(public_key_filename).strip
|
|
610
736
|
end
|
|
611
737
|
end
|
|
@@ -626,6 +752,33 @@ module Kitchen
|
|
|
626
752
|
File.chmod(0600, "#{private_key_filename}.pub")
|
|
627
753
|
end
|
|
628
754
|
|
|
755
|
+
# Explains that the public half of the transport's key could not be found.
|
|
756
|
+
#
|
|
757
|
+
# +ssh_key+ names the *private* key, so a user who keeps only that has
|
|
758
|
+
# nothing wrong with their kitchen.yml. They used to get a raw
|
|
759
|
+
# Errno::ENOENT for a ".pub" path they never wrote, with nothing to say
|
|
760
|
+
# where it came from or what to do about it.
|
|
761
|
+
#
|
|
762
|
+
# The key cannot simply be derived: sshkey only reads PEM-encoded RSA,
|
|
763
|
+
# while ssh-keygen has emitted the OpenSSH format by default since 7.8
|
|
764
|
+
# and ed25519 keys are never PEM. +ssh-keygen -y+ handles every format,
|
|
765
|
+
# so point at that instead.
|
|
766
|
+
#
|
|
767
|
+
# @param private_key_filename [String] the configured private key.
|
|
768
|
+
# @param public_key_filename [String] where the public key was looked for.
|
|
769
|
+
# @param explicit [String, nil] +ssh_public_key+, when the user set it.
|
|
770
|
+
# @return [String]
|
|
771
|
+
def missing_public_key_message(private_key_filename, public_key_filename, explicit)
|
|
772
|
+
if explicit
|
|
773
|
+
"The transport's ssh_public_key setting points at #{public_key_filename}, which does not exist. " \
|
|
774
|
+
"Correct the path, or remove the setting to use #{private_key_filename}.pub."
|
|
775
|
+
else
|
|
776
|
+
"No public key was found at #{public_key_filename}. The transport's ssh_key setting names the " \
|
|
777
|
+
"private key, and the public half of it has to go on the virtual machine. Create it with: " \
|
|
778
|
+
"ssh-keygen -y -f #{private_key_filename} > #{public_key_filename}"
|
|
779
|
+
end
|
|
780
|
+
end
|
|
781
|
+
|
|
629
782
|
# Builds the pre-deployment from a caller-supplied ARM template file.
|
|
630
783
|
#
|
|
631
784
|
# @param pre_deployment_template_filename [String] path to an ARM template.
|
|
@@ -772,6 +925,52 @@ module Kitchen
|
|
|
772
925
|
end
|
|
773
926
|
end
|
|
774
927
|
|
|
928
|
+
# How Azure's power states map onto Test Kitchen's liveness question.
|
|
929
|
+
#
|
|
930
|
+
# Anything absent from this table is passed through as-is with +live+
|
|
931
|
+
# left nil: reporting a state we have not seen before is more use than
|
|
932
|
+
# guessing whether it counts as running.
|
|
933
|
+
#
|
|
934
|
+
# @return [Hash{String => Boolean}]
|
|
935
|
+
POWER_STATES = {
|
|
936
|
+
"running" => true,
|
|
937
|
+
"starting" => true,
|
|
938
|
+
"stopping" => true,
|
|
939
|
+
"stopped" => false,
|
|
940
|
+
"deallocating" => false,
|
|
941
|
+
"deallocated" => false,
|
|
942
|
+
}.freeze
|
|
943
|
+
|
|
944
|
+
# Reports whether the virtual machine backing this instance is running.
|
|
945
|
+
#
|
|
946
|
+
# Drives +kitchen list --live+ (and its +kitchen status+ alias). Test
|
|
947
|
+
# Kitchen rescues anything raised here and reports "unknown", but a
|
|
948
|
+
# listing should not be where an Azure outage first shows up, so the
|
|
949
|
+
# failure modes are handled explicitly and reported as data.
|
|
950
|
+
#
|
|
951
|
+
# No retries: this is a status probe behind an interactive command, and
|
|
952
|
+
# waiting out the retry budget on every unreachable instance would be
|
|
953
|
+
# worse than saying so promptly.
|
|
954
|
+
#
|
|
955
|
+
# @param state [Hash] the instance state.
|
|
956
|
+
# @return [Hash] always +:live+, +:state+ and +:message+; +:resource_id+
|
|
957
|
+
# only when the virtual machine was found, since nothing else knows
|
|
958
|
+
# which resource to name.
|
|
959
|
+
def status(state)
|
|
960
|
+
resource_group = state[:azure_resource_group_name]
|
|
961
|
+
vm_name = state[:vm_name]
|
|
962
|
+
return { live: false, state: "not_created", message: "No Azure virtual machine has been created yet." } unless
|
|
963
|
+
state[:server_id] && resource_group && vm_name
|
|
964
|
+
|
|
965
|
+
power_state_status(state, resource_group, vm_name)
|
|
966
|
+
rescue Azure::OperationError => operation_error
|
|
967
|
+
return { live: false, state: "not_created", message: "The virtual machine no longer exists in Azure." } if operation_error.status == 404
|
|
968
|
+
|
|
969
|
+
{ live: nil, state: "unknown", message: "#{operation_error.code}: #{operation_error.message}" }
|
|
970
|
+
rescue Azure::TransientError => transient_error
|
|
971
|
+
{ live: nil, state: "unknown", message: "Could not reach Azure (#{transient_error.message})." }
|
|
972
|
+
end
|
|
973
|
+
|
|
775
974
|
# Tears down whatever {#create} built.
|
|
776
975
|
#
|
|
777
976
|
# @param state [Hash] the instance state, mutated in place.
|
|
@@ -779,8 +978,8 @@ module Kitchen
|
|
|
779
978
|
# @raise [Azure::OperationError] if an Azure API call fails.
|
|
780
979
|
def destroy(state)
|
|
781
980
|
# TODO: We have some not so fun state issues we need to clean up
|
|
782
|
-
state[:azure_environment] = config[:azure_environment] unless state
|
|
783
|
-
state[:subscription_id] = config[:subscription_id] unless state
|
|
981
|
+
state[:azure_environment] = config[:azure_environment] unless existing_state_value?(state, :azure_environment)
|
|
982
|
+
state[:subscription_id] = config[:subscription_id] unless existing_state_value?(state, :subscription_id)
|
|
784
983
|
|
|
785
984
|
@arm_client = Kitchen::Driver::AzureCredentials.new(subscription_id: state[:subscription_id],
|
|
786
985
|
environment: state[:azure_environment]).arm_client
|
|
@@ -816,6 +1015,53 @@ module Kitchen
|
|
|
816
1015
|
state.delete(:password)
|
|
817
1016
|
end
|
|
818
1017
|
|
|
1018
|
+
# Asks Azure for the virtual machine's power state.
|
|
1019
|
+
#
|
|
1020
|
+
# @param state [Hash] the instance state.
|
|
1021
|
+
# @param resource_group [String]
|
|
1022
|
+
# @param vm_name [String]
|
|
1023
|
+
# @return [Hash] as {#status} returns.
|
|
1024
|
+
def power_state_status(state, resource_group, vm_name)
|
|
1025
|
+
view = status_arm_client(state).virtual_machine_instance_view(resource_group, vm_name)
|
|
1026
|
+
status = Array(view["statuses"]).find { |entry| entry["code"].to_s.start_with?("PowerState/") }
|
|
1027
|
+
power = status && status["code"].to_s.split("/", 2).last
|
|
1028
|
+
|
|
1029
|
+
{
|
|
1030
|
+
live: power ? POWER_STATES[power] : nil,
|
|
1031
|
+
state: power || "unknown",
|
|
1032
|
+
resource_id: virtual_machine_id(status_subscription_id(state), resource_group, vm_name),
|
|
1033
|
+
message: status && status["displayStatus"],
|
|
1034
|
+
}
|
|
1035
|
+
end
|
|
1036
|
+
|
|
1037
|
+
# An ARM client for a status probe, built from state the way {#destroy}
|
|
1038
|
+
# builds one, so that an instance created against another subscription
|
|
1039
|
+
# or cloud is still asked about in the right place.
|
|
1040
|
+
#
|
|
1041
|
+
# @param state [Hash] the instance state.
|
|
1042
|
+
# @return [Azure::ArmClient]
|
|
1043
|
+
def status_arm_client(state)
|
|
1044
|
+
@arm_client ||= Kitchen::Driver::AzureCredentials.new(
|
|
1045
|
+
subscription_id: status_subscription_id(state),
|
|
1046
|
+
environment: state[:azure_environment] || config[:azure_environment]
|
|
1047
|
+
).arm_client
|
|
1048
|
+
end
|
|
1049
|
+
|
|
1050
|
+
# @param state [Hash] the instance state.
|
|
1051
|
+
# @return [String, nil] the subscription the instance was created in.
|
|
1052
|
+
def status_subscription_id(state)
|
|
1053
|
+
state[:subscription_id] || config[:subscription_id]
|
|
1054
|
+
end
|
|
1055
|
+
|
|
1056
|
+
# @param subscription_id [String]
|
|
1057
|
+
# @param resource_group [String]
|
|
1058
|
+
# @param vm_name [String]
|
|
1059
|
+
# @return [String] the virtual machine's ARM resource id.
|
|
1060
|
+
def virtual_machine_id(subscription_id, resource_group, vm_name)
|
|
1061
|
+
"/subscriptions/#{subscription_id}/resourceGroups/#{resource_group}" \
|
|
1062
|
+
"/providers/Microsoft.Compute/virtualMachines/#{vm_name}"
|
|
1063
|
+
end
|
|
1064
|
+
|
|
819
1065
|
# Deletes an explicitly-named resource group when the instance itself was
|
|
820
1066
|
# never created but the user asked for the group to be removed.
|
|
821
1067
|
#
|
|
@@ -839,7 +1085,15 @@ module Kitchen
|
|
|
839
1085
|
end
|
|
840
1086
|
|
|
841
1087
|
# Empties a resource group by deploying an empty template in Complete
|
|
842
|
-
# mode, then
|
|
1088
|
+
# mode, then clears the group's tags unless asked to keep them.
|
|
1089
|
+
#
|
|
1090
|
+
# An empty Complete-mode deployment removes the resources in the group
|
|
1091
|
+
# and leaves the group's own tags alone, so keeping them means leaving
|
|
1092
|
+
# the group be. Rewriting it is what used to remove them: ARM's PUT on a
|
|
1093
|
+
# resource group replaces its tags rather than merging them, so putting
|
|
1094
|
+
# the *configured* tags back overwrote whatever the group actually
|
|
1095
|
+
# carried - which, with +resource_group_tags+ unset, meant erasing them
|
|
1096
|
+
# while announcing they would be kept.
|
|
843
1097
|
#
|
|
844
1098
|
# @param state [Hash] the instance state.
|
|
845
1099
|
# @return [void]
|
|
@@ -850,11 +1104,11 @@ module Kitchen
|
|
|
850
1104
|
|
|
851
1105
|
if config[:destroy_explicit_resource_group_tags] == false
|
|
852
1106
|
warn 'The "destroy_explicit_resource_group_tags" setting value is set to "false". The tags on the resource group will NOT be removed.'
|
|
853
|
-
|
|
854
|
-
else
|
|
855
|
-
warn 'The "destroy_explicit_resource_group_tags" setting value is set to "true". The tags on the resource group will be removed.'
|
|
856
|
-
create_resource_group(state[:azure_resource_group_name], get_resource_group.merge(tags: {}))
|
|
1107
|
+
return
|
|
857
1108
|
end
|
|
1109
|
+
|
|
1110
|
+
warn 'The "destroy_explicit_resource_group_tags" setting value is set to "true". The tags on the resource group will be removed.'
|
|
1111
|
+
create_resource_group(state[:azure_resource_group_name], get_resource_group.merge(tags: {}))
|
|
858
1112
|
rescue Azure::OperationError => operation_error
|
|
859
1113
|
error operation_error.body
|
|
860
1114
|
raise operation_error
|
|
@@ -1162,7 +1416,11 @@ module Kitchen
|
|
|
1162
1416
|
|
|
1163
1417
|
# Base64-encoded custom data for the VM.
|
|
1164
1418
|
#
|
|
1165
|
-
#
|
|
1419
|
+
# An unset +custom_data+ is "" rather than nil, which encodes to "" - so
|
|
1420
|
+
# callers guard on the setting being empty rather than on this returning
|
|
1421
|
+
# nil.
|
|
1422
|
+
#
|
|
1423
|
+
# @return [String, nil] nil only when +custom_data+ is explicitly nil.
|
|
1166
1424
|
def prepared_custom_data
|
|
1167
1425
|
return nil if config[:custom_data].nil?
|
|
1168
1426
|
|
|
@@ -1311,7 +1569,7 @@ module Kitchen
|
|
|
1311
1569
|
# Requests deletion of a resource group without waiting for it to finish.
|
|
1312
1570
|
#
|
|
1313
1571
|
# @param resource_group_name [String] the resource group name.
|
|
1314
|
-
# @return [
|
|
1572
|
+
# @return [void] ARM answers a delete with no body worth returning.
|
|
1315
1573
|
def delete_resource_group_async(resource_group_name)
|
|
1316
1574
|
with_azure_retries("while sending resource group deletion request for '#{resource_group_name}'.") do
|
|
1317
1575
|
arm_client.delete_resource_group(resource_group_name)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kitchen-azurerm
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stuart Preston
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: inifile
|