kitchen-azurerm 2.1.3 → 2.3.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 +4 -4
- data/lib/kitchen/driver/azure/arm_client.rb +53 -1
- data/lib/kitchen/driver/azure/errors.rb +11 -0
- data/lib/kitchen/driver/azurerm.rb +296 -24
- data/lib/kitchen/driver/azurerm_version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fbc95dc73706d3eb94349e23d021b1747f838b1dde2ea375d3be1fe2d27b920d
|
|
4
|
+
data.tar.gz: 6719ba4591a556c5eeb6a1187d21fddd5ad2b72e4faf74b8fc5d483158c09ad1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fbda874b618647705e59cd47b1ae9ef4f128b7bdfacebef707824eabc158f875b011ae666c33b5e6bff478553af14fc9206f7c23bcc41b0955c58f3b6b57fc70
|
|
7
|
+
data.tar.gz: 654610bdad5017605ece2dbede6907952b605e7607f5024a8e15a6f3e786af8b69ef8b19dfab78f79898afe945cf4415f5600806c1bbedf4edc8b2125b845828
|
|
@@ -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.
|
|
@@ -67,10 +92,18 @@ module Kitchen
|
|
|
67
92
|
# Requests deletion of a resource group, returning as soon as ARM
|
|
68
93
|
# accepts the request rather than waiting for it to finish.
|
|
69
94
|
#
|
|
95
|
+
# A group that is already gone counts as deleted: that is the outcome
|
|
96
|
+
# the caller asked for, and ARM answers 404. Treating that as an error
|
|
97
|
+
# wedged +kitchen destroy+ permanently for any instance whose resource
|
|
98
|
+
# group had been removed out of band - deleted in the portal to save
|
|
99
|
+
# money, or never created because +kitchen create+ failed early. Every
|
|
100
|
+
# subsequent destroy failed the same way, so the instance could only be
|
|
101
|
+
# cleared by hand-deleting its state file.
|
|
102
|
+
#
|
|
70
103
|
# @param name [String] resource group name.
|
|
71
104
|
# @return [void]
|
|
72
105
|
def delete_resource_group(name)
|
|
73
|
-
call(:delete, resource_group_path(name), api_version: RESOURCES_API_VERSION)
|
|
106
|
+
call(:delete, resource_group_path(name), api_version: RESOURCES_API_VERSION, allow: [404])
|
|
74
107
|
nil
|
|
75
108
|
end
|
|
76
109
|
|
|
@@ -107,6 +140,17 @@ module Kitchen
|
|
|
107
140
|
payload.is_a?(Hash) ? Array(payload["value"]) : []
|
|
108
141
|
end
|
|
109
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
|
+
|
|
110
154
|
# Reads a public IP address resource.
|
|
111
155
|
#
|
|
112
156
|
# @param resource_group [String]
|
|
@@ -150,6 +194,14 @@ module Kitchen
|
|
|
150
194
|
"#{resource_group_path(resource_group)}/providers/Microsoft.Network/#{type}/#{escape(name)}"
|
|
151
195
|
end
|
|
152
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
|
+
|
|
153
205
|
# @param value [String]
|
|
154
206
|
# @return [String] path-escaped
|
|
155
207
|
def escape(value)
|
|
@@ -40,6 +40,17 @@ module Kitchen
|
|
|
40
40
|
def code
|
|
41
41
|
body.is_a?(Hash) ? body.dig("error", "code") : nil
|
|
42
42
|
end
|
|
43
|
+
|
|
44
|
+
# Azure's own explanation, when ARM supplied one.
|
|
45
|
+
#
|
|
46
|
+
# +message+ is this class's summary of the request that failed; this
|
|
47
|
+
# is what Azure said about it, which is usually the part worth showing
|
|
48
|
+
# somebody.
|
|
49
|
+
#
|
|
50
|
+
# @return [String, nil]
|
|
51
|
+
def detail
|
|
52
|
+
body.is_a?(Hash) ? body.dig("error", "message") : nil
|
|
53
|
+
end
|
|
43
54
|
end
|
|
44
55
|
|
|
45
56
|
# Raised when a request could not be completed and is worth retrying:
|
|
@@ -66,8 +66,12 @@ module Kitchen
|
|
|
66
66
|
|
|
67
67
|
# Ubuntu 22.04 LTS, generation 2. Canonical renamed their offers after
|
|
68
68
|
# 18.04, so the old "UbuntuServer" offer no longer resolves at all.
|
|
69
|
+
#
|
|
70
|
+
# @return [String]
|
|
71
|
+
DEFAULT_IMAGE_URN = "Canonical:0001-com-ubuntu-server-jammy:22_04-lts-gen2:latest".freeze
|
|
72
|
+
|
|
69
73
|
default_config(:image_urn) do |_config|
|
|
70
|
-
|
|
74
|
+
DEFAULT_IMAGE_URN
|
|
71
75
|
end
|
|
72
76
|
|
|
73
77
|
default_config(:image_id) do |_config|
|
|
@@ -273,12 +277,12 @@ module Kitchen
|
|
|
273
277
|
end
|
|
274
278
|
|
|
275
279
|
begin
|
|
276
|
-
run_deployment(state, "pre-deploy", pre_deployment(config[:pre_deployment_template], config[:pre_deployment_parameters])) if File.file?(config[:pre_deployment_template])
|
|
280
|
+
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
281
|
|
|
278
282
|
run_deployment(state, "deploy", deployment(deployment_parameters))
|
|
279
283
|
store_deployment_credentials(state, deployment_parameters)
|
|
280
284
|
|
|
281
|
-
run_deployment(state, "post-deploy", post_deployment(config[:post_deployment_template], config[:post_deployment_parameters])) if File.file?(config[:post_deployment_template])
|
|
285
|
+
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
286
|
rescue Azure::OperationError => operation_error
|
|
283
287
|
info operation_error.body["error"]
|
|
284
288
|
raise operation_error
|
|
@@ -287,6 +291,78 @@ module Kitchen
|
|
|
287
291
|
state[:hostname] = resolve_hostname(state, deployment_parameters["nicName"])
|
|
288
292
|
end
|
|
289
293
|
|
|
294
|
+
# Settings the driver cannot supply a default for, and what they are.
|
|
295
|
+
#
|
|
296
|
+
# @return [Hash{Symbol => String}]
|
|
297
|
+
REQUIRED_CONFIG = {
|
|
298
|
+
subscription_id: "the Azure subscription to deploy into",
|
|
299
|
+
location: "the Azure region to deploy into, e.g. eastus",
|
|
300
|
+
machine_size: "the VM size to deploy, e.g. Standard_D2s_v3",
|
|
301
|
+
}.freeze
|
|
302
|
+
|
|
303
|
+
# Checks configuration and credentials, for +kitchen doctor+.
|
|
304
|
+
#
|
|
305
|
+
# Deployment failures are usually one of two things: a required setting
|
|
306
|
+
# nobody filled in, or credentials that do not work. Both otherwise
|
|
307
|
+
# surface as an Azure error partway through a create, once the resource
|
|
308
|
+
# group already exists, so this looks for them up front.
|
|
309
|
+
#
|
|
310
|
+
# @param state [Hash] the instance state, unused - the checks are about
|
|
311
|
+
# configuration and credentials, which exist before any instance does.
|
|
312
|
+
# @return [Boolean] true when a problem was found, as +kitchen doctor+
|
|
313
|
+
# expects.
|
|
314
|
+
def doctor(_state)
|
|
315
|
+
problems = missing_required_config
|
|
316
|
+
problems += unreachable_azure if problems.empty?
|
|
317
|
+
problems.each { |problem| error("kitchen-azurerm: #{problem}") }
|
|
318
|
+
problems.any?
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Required settings that were left unset.
|
|
322
|
+
#
|
|
323
|
+
# @return [Array<String>]
|
|
324
|
+
def missing_required_config
|
|
325
|
+
REQUIRED_CONFIG.select { |option, _| config[option].to_s.empty? }
|
|
326
|
+
.map { |option, purpose| "#{option} is not set. It has no default: give it #{purpose}." }
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Whether Azure answers, accepts the credentials, and knows the
|
|
330
|
+
# subscription.
|
|
331
|
+
#
|
|
332
|
+
# Reads the subscription rather than probing a resource group: ARM
|
|
333
|
+
# answers a resource group HEAD with 404 both for a subscription that
|
|
334
|
+
# does not exist and for one that merely has no such group, so it cannot
|
|
335
|
+
# tell a wrong subscription from a healthy one. Reading the subscription
|
|
336
|
+
# gives back either its details or +SubscriptionNotFound+.
|
|
337
|
+
#
|
|
338
|
+
# @return [Array<String>]
|
|
339
|
+
def unreachable_azure
|
|
340
|
+
Kitchen::Driver::AzureCredentials.new(subscription_id: config[:subscription_id],
|
|
341
|
+
environment: config[:azure_environment]).arm_client.subscription
|
|
342
|
+
[]
|
|
343
|
+
rescue Azure::OperationError => operation_error
|
|
344
|
+
[azure_problem(operation_error)]
|
|
345
|
+
rescue Azure::TransientError => transient_error
|
|
346
|
+
["Could not reach Azure (#{transient_error.message})."]
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Describes an OperationError raised while checking Azure.
|
|
350
|
+
#
|
|
351
|
+
# An error carrying no Azure error code never reached ARM - it comes
|
|
352
|
+
# from acquiring the token, and already explains itself. Announcing that
|
|
353
|
+
# Azure rejected a request that was never made only sends the reader
|
|
354
|
+
# looking in the wrong place.
|
|
355
|
+
#
|
|
356
|
+
# @param operation_error [Azure::OperationError]
|
|
357
|
+
# @return [String]
|
|
358
|
+
def azure_problem(operation_error)
|
|
359
|
+
return operation_error.message unless operation_error.code
|
|
360
|
+
|
|
361
|
+
"Azure rejected the request (#{operation_error.code}: " \
|
|
362
|
+
"#{operation_error.detail || operation_error.message}). " \
|
|
363
|
+
"Check the credentials, and that they can reach subscription #{config[:subscription_id]}."
|
|
364
|
+
end
|
|
365
|
+
|
|
290
366
|
# Builds the ARM parameter values for the virtual machine deployment.
|
|
291
367
|
#
|
|
292
368
|
# @param state [Hash] instance state, already through {#validate_state}.
|
|
@@ -315,7 +391,7 @@ module Kitchen
|
|
|
315
391
|
|
|
316
392
|
parameters["nicName"] = nic_name(state)
|
|
317
393
|
parameters["customData"] = prepared_custom_data unless config[:custom_data].to_s.empty?
|
|
318
|
-
parameters["osDiskSizeGb"] =
|
|
394
|
+
parameters["osDiskSizeGb"] = os_disk_size_gb unless config[:os_disk_size_gb].to_s.empty?
|
|
319
395
|
parameters["nsgId"] = config[:nsg_id] unless config[:nsg_id].to_s.empty?
|
|
320
396
|
|
|
321
397
|
parameters.merge(image_parameters)
|
|
@@ -329,10 +405,45 @@ module Kitchen
|
|
|
329
405
|
def image_parameters
|
|
330
406
|
return { "imageId" => config[:image_id] } if config[:image_id].to_s != ""
|
|
331
407
|
|
|
332
|
-
publisher, offer, sku, version =
|
|
408
|
+
publisher, offer, sku, version = image_urn.split(":", 4)
|
|
333
409
|
{ "imagePublisher" => publisher, "imageOffer" => offer, "imageSku" => sku, "imageVersion" => version }
|
|
334
410
|
end
|
|
335
411
|
|
|
412
|
+
# The Marketplace image URN to deploy from.
|
|
413
|
+
#
|
|
414
|
+
# An +image_urn:+ written with no value is nil rather than "", which
|
|
415
|
+
# counts as unset here the way it does everywhere else in the driver -
|
|
416
|
+
# so it falls back to {DEFAULT_IMAGE_URN} rather than failing with a
|
|
417
|
+
# Ruby error naming neither the setting nor the file it came from.
|
|
418
|
+
#
|
|
419
|
+
# @return [String]
|
|
420
|
+
def image_urn
|
|
421
|
+
urn = config[:image_urn].to_s
|
|
422
|
+
urn.empty? ? DEFAULT_IMAGE_URN : urn
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# The OS disk size, as a number ARM will accept.
|
|
426
|
+
#
|
|
427
|
+
# YAML makes +os_disk_size_gb: 64+ an Integer and +os_disk_size_gb: "64"+
|
|
428
|
+
# a String, and the ARM parameter is typed +int+ - Azure rejects the
|
|
429
|
+
# String outright rather than coercing it:
|
|
430
|
+
#
|
|
431
|
+
# The provided value for the template parameter 'osDiskSizeGb' is not
|
|
432
|
+
# valid. Expected a value of type 'Integer', but received a value of
|
|
433
|
+
# type 'String'.
|
|
434
|
+
#
|
|
435
|
+
# Quoting a number in kitchen.yml is an easy thing to do, and the driver
|
|
436
|
+
# already tolerates the same ambiguity for +boot_diagnostics_enabled+.
|
|
437
|
+
#
|
|
438
|
+
# @return [Integer]
|
|
439
|
+
# @raise [Kitchen::UserError] if the value is not a whole number.
|
|
440
|
+
def os_disk_size_gb
|
|
441
|
+
Integer(config[:os_disk_size_gb])
|
|
442
|
+
rescue ArgumentError, TypeError
|
|
443
|
+
raise Kitchen::UserError,
|
|
444
|
+
"os_disk_size_gb must be a whole number of gigabytes, but was #{config[:os_disk_size_gb].inspect}."
|
|
445
|
+
end
|
|
446
|
+
|
|
336
447
|
# Whether managed boot diagnostics should be switched on.
|
|
337
448
|
#
|
|
338
449
|
# Historically this setting defaulted to the *string* +"true"+, and plenty
|
|
@@ -429,11 +540,20 @@ module Kitchen
|
|
|
429
540
|
|
|
430
541
|
# Whether a state property is already populated.
|
|
431
542
|
#
|
|
543
|
+
# A blank value does not count. A run that fails early still writes its
|
|
544
|
+
# state, so an empty +subscription_id+ - which is what a quoted ERB
|
|
545
|
+
# interpolation of an unset variable produces - used to be taken as
|
|
546
|
+
# authoritative from then on, shadowing the real value in config for the
|
|
547
|
+
# rest of the instance's life.
|
|
548
|
+
#
|
|
549
|
+
# +false+ is a value, so this asks whether the value is blank rather
|
|
550
|
+
# than whether it is truthy.
|
|
551
|
+
#
|
|
432
552
|
# @param state [Hash] Hash of existing state values.
|
|
433
553
|
# @param property [Symbol, String] the property to check.
|
|
434
|
-
# @return [Boolean] true when the key exists and its value is not
|
|
554
|
+
# @return [Boolean] true when the key exists and its value is not blank.
|
|
435
555
|
def existing_state_value?(state, property)
|
|
436
|
-
state.key?(property) && !state[property].
|
|
556
|
+
state.key?(property) && !state[property].to_s.empty?
|
|
437
557
|
end
|
|
438
558
|
|
|
439
559
|
# Fills in any state values that are not already present.
|
|
@@ -561,26 +681,79 @@ module Kitchen
|
|
|
561
681
|
template["resources"].select { |resource| resource["type"] == "Microsoft.Compute/virtualMachines" }
|
|
562
682
|
end
|
|
563
683
|
|
|
684
|
+
# Serializes the generate-and-write below.
|
|
685
|
+
#
|
|
686
|
+
# Test Kitchen runs instances as threads inside one process, so
|
|
687
|
+
# +kitchen create -c+ had every thread reach the "does the key exist?"
|
|
688
|
+
# check before any of them had written one. Each generated its own pair
|
|
689
|
+
# and wrote it over the last, so only the instance whose key happened to
|
|
690
|
+
# land on disk last was reachable - the rest were deployed with a public
|
|
691
|
+
# key nobody held the private half of, and failed at converge with
|
|
692
|
+
# +Permission denied (publickey)+.
|
|
693
|
+
#
|
|
694
|
+
# @return [Mutex]
|
|
695
|
+
KEY_GENERATION_MUTEX = Mutex.new
|
|
696
|
+
|
|
564
697
|
# Returns the public key to inject into the deployment, generating a new
|
|
565
698
|
# key pair on disk when the configured private key does not yet exist.
|
|
566
699
|
#
|
|
567
700
|
# @param private_key_filename [String] path to the transport's private key.
|
|
568
701
|
# @return [String] the OpenSSH-format public key, stripped of whitespace.
|
|
569
702
|
def public_key_for_deployment(private_key_filename)
|
|
570
|
-
|
|
571
|
-
|
|
703
|
+
KEY_GENERATION_MUTEX.synchronize do
|
|
704
|
+
generate_key_pair(private_key_filename) unless File.file?(private_key_filename)
|
|
572
705
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
File.
|
|
576
|
-
|
|
577
|
-
|
|
706
|
+
explicit = instance.transport[:ssh_public_key]
|
|
707
|
+
public_key_filename = explicit || "#{private_key_filename}.pub"
|
|
708
|
+
unless File.file?(public_key_filename)
|
|
709
|
+
raise Kitchen::UserError, missing_public_key_message(private_key_filename, public_key_filename, explicit)
|
|
710
|
+
end
|
|
578
711
|
|
|
579
|
-
|
|
712
|
+
File.read(public_key_filename).strip
|
|
580
713
|
end
|
|
714
|
+
end
|
|
581
715
|
|
|
582
|
-
|
|
583
|
-
|
|
716
|
+
# Writes a fresh SSH key pair to disk.
|
|
717
|
+
#
|
|
718
|
+
# Always called with {KEY_GENERATION_MUTEX} held.
|
|
719
|
+
#
|
|
720
|
+
# @param private_key_filename [String] path to write the private key to.
|
|
721
|
+
# @return [void]
|
|
722
|
+
def generate_key_pair(private_key_filename)
|
|
723
|
+
key = SSHKey.generate
|
|
724
|
+
|
|
725
|
+
::FileUtils.mkdir_p(File.dirname(private_key_filename))
|
|
726
|
+
File.write(private_key_filename, key.private_key)
|
|
727
|
+
File.chmod(0600, private_key_filename)
|
|
728
|
+
File.write("#{private_key_filename}.pub", key.ssh_public_key)
|
|
729
|
+
File.chmod(0600, "#{private_key_filename}.pub")
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
# Explains that the public half of the transport's key could not be found.
|
|
733
|
+
#
|
|
734
|
+
# +ssh_key+ names the *private* key, so a user who keeps only that has
|
|
735
|
+
# nothing wrong with their kitchen.yml. They used to get a raw
|
|
736
|
+
# Errno::ENOENT for a ".pub" path they never wrote, with nothing to say
|
|
737
|
+
# where it came from or what to do about it.
|
|
738
|
+
#
|
|
739
|
+
# The key cannot simply be derived: sshkey only reads PEM-encoded RSA,
|
|
740
|
+
# while ssh-keygen has emitted the OpenSSH format by default since 7.8
|
|
741
|
+
# and ed25519 keys are never PEM. +ssh-keygen -y+ handles every format,
|
|
742
|
+
# so point at that instead.
|
|
743
|
+
#
|
|
744
|
+
# @param private_key_filename [String] the configured private key.
|
|
745
|
+
# @param public_key_filename [String] where the public key was looked for.
|
|
746
|
+
# @param explicit [String, nil] +ssh_public_key+, when the user set it.
|
|
747
|
+
# @return [String]
|
|
748
|
+
def missing_public_key_message(private_key_filename, public_key_filename, explicit)
|
|
749
|
+
if explicit
|
|
750
|
+
"The transport's ssh_public_key setting points at #{public_key_filename}, which does not exist. " \
|
|
751
|
+
"Correct the path, or remove the setting to use #{private_key_filename}.pub."
|
|
752
|
+
else
|
|
753
|
+
"No public key was found at #{public_key_filename}. The transport's ssh_key setting names the " \
|
|
754
|
+
"private key, and the public half of it has to go on the virtual machine. Create it with: " \
|
|
755
|
+
"ssh-keygen -y -f #{private_key_filename} > #{public_key_filename}"
|
|
756
|
+
end
|
|
584
757
|
end
|
|
585
758
|
|
|
586
759
|
# Builds the pre-deployment from a caller-supplied ARM template file.
|
|
@@ -729,6 +902,50 @@ module Kitchen
|
|
|
729
902
|
end
|
|
730
903
|
end
|
|
731
904
|
|
|
905
|
+
# How Azure's power states map onto Test Kitchen's liveness question.
|
|
906
|
+
#
|
|
907
|
+
# Anything absent from this table is passed through as-is with +live+
|
|
908
|
+
# left nil: reporting a state we have not seen before is more use than
|
|
909
|
+
# guessing whether it counts as running.
|
|
910
|
+
#
|
|
911
|
+
# @return [Hash{String => Boolean}]
|
|
912
|
+
POWER_STATES = {
|
|
913
|
+
"running" => true,
|
|
914
|
+
"starting" => true,
|
|
915
|
+
"stopping" => true,
|
|
916
|
+
"stopped" => false,
|
|
917
|
+
"deallocating" => false,
|
|
918
|
+
"deallocated" => false,
|
|
919
|
+
}.freeze
|
|
920
|
+
|
|
921
|
+
# Reports whether the virtual machine backing this instance is running.
|
|
922
|
+
#
|
|
923
|
+
# Drives +kitchen list --live+ (and its +kitchen status+ alias). Test
|
|
924
|
+
# Kitchen rescues anything raised here and reports "unknown", but a
|
|
925
|
+
# listing should not be where an Azure outage first shows up, so the
|
|
926
|
+
# failure modes are handled explicitly and reported as data.
|
|
927
|
+
#
|
|
928
|
+
# No retries: this is a status probe behind an interactive command, and
|
|
929
|
+
# waiting out the retry budget on every unreachable instance would be
|
|
930
|
+
# worse than saying so promptly.
|
|
931
|
+
#
|
|
932
|
+
# @param state [Hash] the instance state.
|
|
933
|
+
# @return [Hash] +:live+, +:state+, +:resource_id+ and +:message+.
|
|
934
|
+
def status(state)
|
|
935
|
+
resource_group = state[:azure_resource_group_name]
|
|
936
|
+
vm_name = state[:vm_name]
|
|
937
|
+
return { live: false, state: "not_created", message: "No Azure virtual machine has been created yet." } unless
|
|
938
|
+
state[:server_id] && resource_group && vm_name
|
|
939
|
+
|
|
940
|
+
power_state_status(state, resource_group, vm_name)
|
|
941
|
+
rescue Azure::OperationError => operation_error
|
|
942
|
+
return { live: false, state: "not_created", message: "The virtual machine no longer exists in Azure." } if operation_error.status == 404
|
|
943
|
+
|
|
944
|
+
{ live: nil, state: "unknown", message: "#{operation_error.code}: #{operation_error.message}" }
|
|
945
|
+
rescue Azure::TransientError => transient_error
|
|
946
|
+
{ live: nil, state: "unknown", message: "Could not reach Azure (#{transient_error.message})." }
|
|
947
|
+
end
|
|
948
|
+
|
|
732
949
|
# Tears down whatever {#create} built.
|
|
733
950
|
#
|
|
734
951
|
# @param state [Hash] the instance state, mutated in place.
|
|
@@ -736,8 +953,8 @@ module Kitchen
|
|
|
736
953
|
# @raise [Azure::OperationError] if an Azure API call fails.
|
|
737
954
|
def destroy(state)
|
|
738
955
|
# TODO: We have some not so fun state issues we need to clean up
|
|
739
|
-
state[:azure_environment] = config[:azure_environment] unless state
|
|
740
|
-
state[:subscription_id] = config[:subscription_id] unless state
|
|
956
|
+
state[:azure_environment] = config[:azure_environment] unless existing_state_value?(state, :azure_environment)
|
|
957
|
+
state[:subscription_id] = config[:subscription_id] unless existing_state_value?(state, :subscription_id)
|
|
741
958
|
|
|
742
959
|
@arm_client = Kitchen::Driver::AzureCredentials.new(subscription_id: state[:subscription_id],
|
|
743
960
|
environment: state[:azure_environment]).arm_client
|
|
@@ -773,6 +990,53 @@ module Kitchen
|
|
|
773
990
|
state.delete(:password)
|
|
774
991
|
end
|
|
775
992
|
|
|
993
|
+
# Asks Azure for the virtual machine's power state.
|
|
994
|
+
#
|
|
995
|
+
# @param state [Hash] the instance state.
|
|
996
|
+
# @param resource_group [String]
|
|
997
|
+
# @param vm_name [String]
|
|
998
|
+
# @return [Hash] as {#status} returns.
|
|
999
|
+
def power_state_status(state, resource_group, vm_name)
|
|
1000
|
+
view = status_arm_client(state).virtual_machine_instance_view(resource_group, vm_name)
|
|
1001
|
+
status = Array(view["statuses"]).find { |entry| entry["code"].to_s.start_with?("PowerState/") }
|
|
1002
|
+
power = status && status["code"].to_s.split("/", 2).last
|
|
1003
|
+
|
|
1004
|
+
{
|
|
1005
|
+
live: power ? POWER_STATES[power] : nil,
|
|
1006
|
+
state: power || "unknown",
|
|
1007
|
+
resource_id: virtual_machine_id(status_subscription_id(state), resource_group, vm_name),
|
|
1008
|
+
message: status && status["displayStatus"],
|
|
1009
|
+
}
|
|
1010
|
+
end
|
|
1011
|
+
|
|
1012
|
+
# An ARM client for a status probe, built from state the way {#destroy}
|
|
1013
|
+
# builds one, so that an instance created against another subscription
|
|
1014
|
+
# or cloud is still asked about in the right place.
|
|
1015
|
+
#
|
|
1016
|
+
# @param state [Hash] the instance state.
|
|
1017
|
+
# @return [Azure::ArmClient]
|
|
1018
|
+
def status_arm_client(state)
|
|
1019
|
+
@arm_client ||= Kitchen::Driver::AzureCredentials.new(
|
|
1020
|
+
subscription_id: status_subscription_id(state),
|
|
1021
|
+
environment: state[:azure_environment] || config[:azure_environment]
|
|
1022
|
+
).arm_client
|
|
1023
|
+
end
|
|
1024
|
+
|
|
1025
|
+
# @param state [Hash] the instance state.
|
|
1026
|
+
# @return [String, nil] the subscription the instance was created in.
|
|
1027
|
+
def status_subscription_id(state)
|
|
1028
|
+
state[:subscription_id] || config[:subscription_id]
|
|
1029
|
+
end
|
|
1030
|
+
|
|
1031
|
+
# @param subscription_id [String]
|
|
1032
|
+
# @param resource_group [String]
|
|
1033
|
+
# @param vm_name [String]
|
|
1034
|
+
# @return [String] the virtual machine's ARM resource id.
|
|
1035
|
+
def virtual_machine_id(subscription_id, resource_group, vm_name)
|
|
1036
|
+
"/subscriptions/#{subscription_id}/resourceGroups/#{resource_group}" \
|
|
1037
|
+
"/providers/Microsoft.Compute/virtualMachines/#{vm_name}"
|
|
1038
|
+
end
|
|
1039
|
+
|
|
776
1040
|
# Deletes an explicitly-named resource group when the instance itself was
|
|
777
1041
|
# never created but the user asked for the group to be removed.
|
|
778
1042
|
#
|
|
@@ -796,7 +1060,15 @@ module Kitchen
|
|
|
796
1060
|
end
|
|
797
1061
|
|
|
798
1062
|
# Empties a resource group by deploying an empty template in Complete
|
|
799
|
-
# mode, then
|
|
1063
|
+
# mode, then clears the group's tags unless asked to keep them.
|
|
1064
|
+
#
|
|
1065
|
+
# An empty Complete-mode deployment removes the resources in the group
|
|
1066
|
+
# and leaves the group's own tags alone, so keeping them means leaving
|
|
1067
|
+
# the group be. Rewriting it is what used to remove them: ARM's PUT on a
|
|
1068
|
+
# resource group replaces its tags rather than merging them, so putting
|
|
1069
|
+
# the *configured* tags back overwrote whatever the group actually
|
|
1070
|
+
# carried - which, with +resource_group_tags+ unset, meant erasing them
|
|
1071
|
+
# while announcing they would be kept.
|
|
800
1072
|
#
|
|
801
1073
|
# @param state [Hash] the instance state.
|
|
802
1074
|
# @return [void]
|
|
@@ -807,11 +1079,11 @@ module Kitchen
|
|
|
807
1079
|
|
|
808
1080
|
if config[:destroy_explicit_resource_group_tags] == false
|
|
809
1081
|
warn 'The "destroy_explicit_resource_group_tags" setting value is set to "false". The tags on the resource group will NOT be removed.'
|
|
810
|
-
|
|
811
|
-
else
|
|
812
|
-
warn 'The "destroy_explicit_resource_group_tags" setting value is set to "true". The tags on the resource group will be removed.'
|
|
813
|
-
create_resource_group(state[:azure_resource_group_name], get_resource_group.merge(tags: {}))
|
|
1082
|
+
return
|
|
814
1083
|
end
|
|
1084
|
+
|
|
1085
|
+
warn 'The "destroy_explicit_resource_group_tags" setting value is set to "true". The tags on the resource group will be removed.'
|
|
1086
|
+
create_resource_group(state[:azure_resource_group_name], get_resource_group.merge(tags: {}))
|
|
815
1087
|
rescue Azure::OperationError => operation_error
|
|
816
1088
|
error operation_error.body
|
|
817
1089
|
raise operation_error
|
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.3.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-08-
|
|
11
|
+
date: 2026-08-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: inifile
|
|
@@ -56,7 +56,7 @@ dependencies:
|
|
|
56
56
|
requirements:
|
|
57
57
|
- - ">="
|
|
58
58
|
- !ruby/object:Gem::Version
|
|
59
|
-
version: '
|
|
59
|
+
version: '3.0'
|
|
60
60
|
- - "<"
|
|
61
61
|
- !ruby/object:Gem::Version
|
|
62
62
|
version: '5.0'
|
|
@@ -66,7 +66,7 @@ dependencies:
|
|
|
66
66
|
requirements:
|
|
67
67
|
- - ">="
|
|
68
68
|
- !ruby/object:Gem::Version
|
|
69
|
-
version: '
|
|
69
|
+
version: '3.0'
|
|
70
70
|
- - "<"
|
|
71
71
|
- !ruby/object:Gem::Version
|
|
72
72
|
version: '5.0'
|