Class: Kitchen::Driver::Azurerm

Inherits:
Base
  • Object
show all
Defined in:
lib/kitchen/driver/azurerm.rb

Overview

Test Kitchen driver for the Microsoft Azure Resource Manager API.

Provisions each Test Kitchen instance as an ARM deployment inside its own resource group, then tears the whole group down again on destroy. The deployment template is rendered from the ERB files in templates/ - see #virtual_machine_deployment_template.

Constant Summary collapse

DEPRECATED_CONFIG =

Settings that Azure retirements have made inoperable. They are still accepted so that an existing kitchen.yml keeps loading, but they no longer do anything and #warn_about_deprecated_config says so.

Returns:

  • (Hash{Symbol => String})
{
  use_managed_disks: "Azure retired unmanaged disks on 31 March 2026; every deployment now uses managed disks.",
  image_url: "Deploying from a VHD URL required unmanaged disks, which Azure retired on 31 March 2026. Use image_id with a managed image or an Azure Compute Gallery image instead.",
  os_type: "os_type only ever applied to VHD (image_url) deployments, which Azure retired on 31 March 2026.",
  existing_storage_account_blob_url: "Azure retired unmanaged disks on 31 March 2026, so OS disks are no longer placed in a storage account you supply.",
  existing_storage_account_container: "Azure retired unmanaged disks on 31 March 2026, so OS disks are no longer placed in a storage account you supply.",
}.freeze
DEFAULT_IMAGE_URN =

Ubuntu 22.04 LTS, generation 2. Canonical renamed their offers after 18.04, so the old "UbuntuServer" offer no longer resolves at all.

Returns:

  • (String)
"Canonical:0001-com-ubuntu-server-jammy:22_04-lts-gen2:latest".freeze
REQUIRED_CONFIG =

Settings the driver cannot supply a default for, and what they are.

Returns:

  • (Hash{Symbol => String})
{
  subscription_id: "the Azure subscription to deploy into",
  location: "the Azure region to deploy into, e.g. eastus",
  machine_size: "the VM size to deploy, e.g. Standard_D2s_v3",
}.freeze
MAX_VM_NAME_LENGTH =

Maximum length of a generated VM name.

Windows computer names are capped at 15 characters, which is the lower of the two Azure limits, so we honour it for every platform.

Returns:

  • (Integer)
15
MAX_RESOURCE_GROUP_NAME_LENGTH =

Maximum length of an Azure resource group name.

Returns:

  • (Integer)
90
KEY_GENERATION_MUTEX =

Serializes the generate-and-write below.

Test Kitchen runs instances as threads inside one process, so kitchen create -c had every thread reach the "does the key exist?" check before any of them had written one. Each generated its own pair and wrote it over the last, so only the instance whose key happened to land on disk last was reachable - the rest were deployed with a public key nobody held the private half of, and failed at converge with Permission denied (publickey).

Returns:

  • (Mutex)
Mutex.new
POWER_STATES =

How Azure's power states map onto Test Kitchen's liveness question.

Anything absent from this table is passed through as-is with live left nil: reporting a state we have not seen before is more use than guessing whether it counts as running.

Returns:

  • (Hash{String => Boolean})
{
  "running" => true,
  "starting" => true,
  "stopping" => true,
  "stopped" => false,
  "deallocating" => false,
  "deallocated" => false,
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#arm_clientAzure::ArmClient?

Client for the Azure Resource Manager API, built during #create or #destroy once credentials have been resolved.

Returns:



30
31
32
# File 'lib/kitchen/driver/azurerm.rb', line 30

def arm_client
  @arm_client
end

Instance Method Details

#attach_nsg?Boolean

Whether the network interface references a security group at all.

Returns:

  • (Boolean)


1298
1299
1300
# File 'lib/kitchen/driver/azurerm.rb', line 1298

def attach_nsg?
  create_nsg? || !config[:nsg_id].to_s.empty?
end

#azure_problem(operation_error) ⇒ String

Describes an OperationError raised while checking Azure.

An error carrying no Azure error code never reached ARM - it comes from acquiring the token, and already explains itself. Announcing that Azure rejected a request that was never made only sends the reader looking in the wrong place.

Parameters:

Returns:

  • (String)


358
359
360
361
362
363
364
# File 'lib/kitchen/driver/azurerm.rb', line 358

def azure_problem(operation_error)
  return operation_error.message unless operation_error.code

  "Azure rejected the request (#{operation_error.code}: " \
    "#{operation_error.detail || operation_error.message}). " \
    "Check the credentials, and that they can reach subscription #{config[:subscription_id]}."
end

#azure_resource_group_nameString

Name of the resource group this instance deploys into.

The instance name is the suite and platform joined together, so a descriptive suite on a long platform overruns Azure's limit and kitchen create fails on its very first call - over a name the user never chose. Only that part is shortened: the prefix and suffix were asked for explicitly, and the timestamp is what keeps the name unique.

Returns:

  • (String)

    explicit_resource_group_name when set, otherwise prefix + instance name + UTC timestamp + suffix.



620
621
622
623
624
625
626
627
628
629
630
# File 'lib/kitchen/driver/azurerm.rb', line 620

def azure_resource_group_name
  return config[:explicit_resource_group_name] if config[:explicit_resource_group_name]

  formatted_time = Time.now.utc.strftime "%Y%m%dT%H%M%S"
  prefix = config[:azure_resource_group_prefix].to_s
  suffix = config[:azure_resource_group_suffix].to_s
  room = MAX_RESOURCE_GROUP_NAME_LENGTH - prefix.length - suffix.length - formatted_time.length - 1
  name = config[:azure_resource_group_name].to_s[0, [room, 0].max]

  "#{prefix}#{name}-#{formatted_time}#{suffix}"
end

#boot_diagnostics_enabled?Boolean

Whether managed boot diagnostics should be switched on.

Historically this setting defaulted to the string "true", and plenty of kitchen.yml files still say "false", so both spellings are honoured.

Returns:

  • (Boolean)


453
454
455
456
457
458
# File 'lib/kitchen/driver/azurerm.rb', line 453

def boot_diagnostics_enabled?
  value = config[:boot_diagnostics_enabled]
  return false if value.to_s.casecmp("false") == 0

  !!value
end

#build_deployment(template, parameters, mode: "Incremental") ⇒ Hash

Assembles an ARM deployment object.

Parameters:

  • template (String)

    the ARM template as JSON.

  • parameters (Hash, nil)

    parameter name to value, or nil for none.

  • mode (String) (defaults to: "Incremental")

    the ARM deployment mode.

Returns:

  • (Hash)

    the deployment body



803
804
805
806
807
808
809
# File 'lib/kitchen/driver/azurerm.rb', line 803

def build_deployment(template, parameters, mode: "Incremental")
  properties = { "mode" => mode, "template" => JSON.parse(template) }
  formatted = parameters_in_values_format(parameters)
  properties["parameters"] = formatted if formatted

  { "properties" => properties }
end

#build_deployment_parameters(state) ⇒ Hash

Builds the ARM parameter values for the virtual machine deployment.

Parameters:

Returns:



371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
# File 'lib/kitchen/driver/azurerm.rb', line 371

def build_deployment_parameters(state)
  parameters = {
    location: config[:location],
    vmSize: config[:machine_size],
    storageAccountType: config[:storage_account_type],
    bootDiagnosticsEnabled: boot_diagnostics_enabled?,
    adminUsername: config[:username],
    dnsNameForPublicIP: "kitchen-#{state[:uuid]}",
    vmName: state[:vm_name],
    systemAssignedIdentity: config[:system_assigned_identity],
    userAssignedIdentities: Array(config[:user_assigned_identities]).to_h { |identity| [identity, {}] },
    secretUrl: config[:secret_url],
    vaultName: config[:vault_name],
    vaultResourceGroup: config[:vault_resource_group],
  }

  parameters[:adminPassword] = config[:password] if instance.transport[:ssh_key].nil?

  parameters[:publicIPSKU] = config[:public_ip_sku]
  parameters[:publicIPAddressType] = "Static" if config[:public_ip_sku] == "Standard"

  parameters["nicName"] = nic_name(state)
  parameters["customData"] = prepared_custom_data unless config[:custom_data].to_s.empty?
  parameters["osDiskSizeGb"] = os_disk_size_gb unless config[:os_disk_size_gb].to_s.empty?
  parameters["nsgId"] = config[:nsg_id] unless config[:nsg_id].to_s.empty?

  parameters.merge(image_parameters)
end

#create(state) ⇒ void

This method returns an undefined value.

Provisions the Azure resource group and ARM deployment backing this Test Kitchen instance.

Runs, in order: the optional pre-deployment template, the virtual machine deployment, and the optional post-deployment template. On success state gains a :hostname that the transport can connect to.

Parameters:

  • state (Hash)

    the instance state, mutated in place.

Raises:

  • (RuntimeError)

    if no subscription_id can be resolved.

  • (Azure::OperationError)

    if an Azure API call fails for any reason other than an already-running deployment.



258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/kitchen/driver/azurerm.rb', line 258

def create(state)
  warn_about_deprecated_config
  state = validate_state(state)
  deployment_parameters = build_deployment_parameters(state)

  if config[:subscription_id].to_s == ""
    raise "A subscription_id config value was not detected and kitchen-azurerm cannot continue. Please check your kitchen.yml configuration. Exiting."
  end

  debug "Azure environment: #{config[:azure_environment]}"
  @arm_client = Kitchen::Driver::AzureCredentials.new(subscription_id: config[:subscription_id],
    environment: config[:azure_environment]).arm_client

  begin
    info "Creating Resource Group: #{state[:azure_resource_group_name]}"
    create_resource_group(state[:azure_resource_group_name], get_resource_group)
  rescue Azure::OperationError => operation_error
    error operation_error.body
    raise operation_error
  end

  begin
    run_deployment(state, "pre-deploy", pre_deployment(config[:pre_deployment_template], config[:pre_deployment_parameters])) if File.file?(config[:pre_deployment_template].to_s)

    run_deployment(state, "deploy", deployment(deployment_parameters))
    store_deployment_credentials(state, deployment_parameters)

    run_deployment(state, "post-deploy", post_deployment(config[:post_deployment_template], config[:post_deployment_parameters])) if File.file?(config[:post_deployment_template].to_s)
  rescue Azure::OperationError => operation_error
    info operation_error.body["error"]
    raise operation_error
  end

  state[:hostname] = resolve_hostname(state, deployment_parameters["nicName"])
end

#create_nsg?Boolean

Whether the deployment should create its own network security group.

Standard SKU public IPs are closed to inbound traffic unless a security group opens it, so one is generated whenever a public IP is created and the user has not supplied their own group. Instances that live purely inside a caller-supplied vnet are left alone - their subnet may already carry the rules the user wants.

Returns:

  • (Boolean)


1291
1292
1293
# File 'lib/kitchen/driver/azurerm.rb', line 1291

def create_nsg?
  config[:nsg_id].to_s.empty? && public_ip?
end

#custom_data_contentString

The configured custom data, as content.

custom_data may be either the literal content or a path to a file holding it, so this resolves whichever was given.

Returns:

  • (String)

    empty when no custom_data is configured.



1407
1408
1409
1410
1411
1412
1413
# File 'lib/kitchen/driver/azurerm.rb', line 1407

def custom_data_content
  @custom_data_content ||= if readable_file?(config[:custom_data])
                             File.read(config[:custom_data])
                           else
                             config[:custom_data].to_s
                           end
end

#custom_data_script_windowsString

The full first-boot script handed to a Windows VM as custom data.

A Windows VM has exactly one custom data slot, and the driver needs it for the WinRM bootstrap. Any custom_data the user configured has to travel in the same slot, so it is appended here rather than assigned over the top - which is what used to happen, silently discarding it.

It runs after WinRM is listening and the data disks are formatted, and before the logoff that ends the first-logon session.

Returns:

  • (String)


1154
1155
1156
1157
1158
1159
1160
1161
# File 'lib/kitchen/driver/azurerm.rb', line 1154

def custom_data_script_windows
  <<-EOH
  #{enable_winrm_powershell_script}
  #{format_data_disks_powershell_script}
  #{custom_data_content}
  logoff
  EOH
end

#custom_linux_configuration(public_key) ⇒ String

The ARM linuxConfiguration block that installs an SSH public key and disables password authentication.

Parameters:

  • public_key (String)

    an OpenSSH-format public key.

Returns:

  • (String)

    JSON.



1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
# File 'lib/kitchen/driver/azurerm.rb', line 1168

def custom_linux_configuration(public_key)
  {
    "disablePasswordAuthentication" => "true",
    "ssh" => {
      "publicKeys" => [
        {
          "path" => "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]",
          "keyData" => public_key,
        },
      ],
    },
  }.to_json
end

#data_disks_for_vm_jsonString?

JSON fragment describing the data disks to attach to the VM.

Returns:

  • (String, nil)

    a JSON array, or nil when no data_disks are configured.



636
637
638
639
640
641
642
643
644
# File 'lib/kitchen/driver/azurerm.rb', line 636

def data_disks_for_vm_json
  return nil if config[:data_disks].nil?

  disks = config[:data_disks].map do |data_disk|
    { name: "datadisk#{data_disk[:lun]}", lun: data_disk[:lun], diskSizeGB: data_disk[:disk_size_gb], createOption: "Empty" }
  end
  debug "Additional disks being added to configuration: #{disks.inspect}"
  disks.to_json
end

#deployment(parameters) ⇒ Hash

Builds the virtual machine deployment.

Parameters:

  • parameters (Hash)

    parameter name to value.

Returns:

  • (Hash)

    the deployment body



772
773
774
775
776
# File 'lib/kitchen/driver/azurerm.rb', line 772

def deployment(parameters)
  deployment = build_deployment(template_for_transport_name, parameters)
  debug(JSON.pretty_generate(deployment_template(deployment)))
  deployment
end

#deployment_template(deployment) ⇒ Hash

The parsed ARM template inside a deployment body.

Parameters:

Returns:

  • (Hash)


815
816
817
# File 'lib/kitchen/driver/azurerm.rb', line 815

def deployment_template(deployment)
  deployment["properties"]["template"]
end

#destroy(state) ⇒ void

This method returns an undefined value.

Tears down whatever #create built.

Parameters:

  • state (Hash)

    the instance state, mutated in place.

Raises:



954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
# File 'lib/kitchen/driver/azurerm.rb', line 954

def destroy(state)
  # TODO: We have some not so fun state issues we need to clean up
  state[:azure_environment] = config[:azure_environment] unless existing_state_value?(state, :azure_environment)
  state[:subscription_id] = config[:subscription_id] unless existing_state_value?(state, :subscription_id)

  @arm_client = Kitchen::Driver::AzureCredentials.new(subscription_id: state[:subscription_id],
    environment: state[:azure_environment]).arm_client

  return if destroy_orphaned_explicit_resource_group(state)

  info "Azure environment: #{state[:azure_environment]}"

  # Nothing was ever created for this instance.
  return if state[:server_id].nil?

  destroy_resource_group_contents(state) if config[:destroy_resource_group_contents] == true

  if config[:destroy_explicit_resource_group] == false && !config[:explicit_resource_group_name].nil?
    warn 'The "destroy_explicit_resource_group" setting value is set to "false". The resource group will not be deleted.'
    warn 'Remember to manually destroy resources, or set "destroy_resource_group_contents: true" to save costs!' unless config[:destroy_resource_group_contents] == true
    return state
  end

  begin
    info "Destroying Resource Group: #{state[:azure_resource_group_name]}"
    delete_resource_group_async(state[:azure_resource_group_name])
    info "Destroy operation accepted and will continue in the background."
    state.delete(:azure_resource_group_name)
  rescue Azure::OperationError => operation_error
    error operation_error.body
    raise operation_error
  end

  state.delete(:server_id)
  state.delete(:hostname)
  state.delete(:username)
  state.delete(:password)
end

#destroy_orphaned_explicit_resource_group(state) ⇒ Boolean

Deletes an explicitly-named resource group when the instance itself was never created but the user asked for the group to be removed.

Parameters:

  • state (Hash)

    the instance state.

Returns:

  • (Boolean)

    true when the group was deleted and #destroy should stop.

Raises:



1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
# File 'lib/kitchen/driver/azurerm.rb', line 1046

def destroy_orphaned_explicit_resource_group(state)
  return false unless state[:server_id].nil? && state[:azure_resource_group_name].nil?
  return false if config[:explicit_resource_group_name].nil?
  return false unless config[:destroy_explicit_resource_group]
  return false unless resource_group_exists?(config[:explicit_resource_group_name])

  info "This instance doesn't exist but you asked to delete the resource group."
  info "Destroying Resource Group: #{config[:explicit_resource_group_name]}"
  delete_resource_group_async(config[:explicit_resource_group_name])
  info "Destroy operation accepted and will continue in the background."
  true
rescue Azure::OperationError => operation_error
  error operation_error.body
  raise operation_error
end

#destroy_resource_group_contents(state) ⇒ void

This method returns an undefined value.

Empties a resource group by deploying an empty template in Complete mode, then clears the group's tags unless asked to keep them.

An empty Complete-mode deployment removes the resources in the group and leaves the group's own tags alone, so keeping them means leaving the group be. Rewriting it is what used to remove them: ARM's PUT on a resource group replaces its tags rather than merging them, so putting the configured tags back overwrote whatever the group actually carried - which, with resource_group_tags unset, meant erasing them while announcing they would be kept.

Parameters:

  • state (Hash)

    the instance state.

Raises:



1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
# File 'lib/kitchen/driver/azurerm.rb', line 1076

def destroy_resource_group_contents(state)
  info "Destroying individual resources within the Resource Group."
  run_deployment(state, "empty-deploy", empty_deployment)

  if config[:destroy_explicit_resource_group_tags] == false
    warn 'The "destroy_explicit_resource_group_tags" setting value is set to "false". The tags on the resource group will NOT be removed.'
    return
  end

  warn 'The "destroy_explicit_resource_group_tags" setting value is set to "true". The tags on the resource group will be removed.'
  create_resource_group(state[:azure_resource_group_name], get_resource_group.merge(tags: {}))
rescue Azure::OperationError => operation_error
  error operation_error.body
  raise operation_error
end

#doctor(_state) ⇒ Boolean

Checks configuration and credentials, for kitchen doctor.

Deployment failures are usually one of two things: a required setting nobody filled in, or credentials that do not work. Both otherwise surface as an Azure error partway through a create, once the resource group already exists, so this looks for them up front.

Parameters:

  • state (Hash)

    the instance state, unused - the checks are about configuration and credentials, which exist before any instance does.

Returns:

  • (Boolean)

    true when a problem was found, as kitchen doctor expects.



314
315
316
317
318
319
# File 'lib/kitchen/driver/azurerm.rb', line 314

def doctor(_state)
  problems = missing_required_config
  problems += unreachable_azure if problems.empty?
  problems.each { |problem| error("kitchen-azurerm: #{problem}") }
  problems.any?
end

#empty_deploymentHash

An empty Complete-mode deployment, used to delete every resource inside a resource group while leaving the group itself in place.

Returns:

  • (Hash)

    the deployment body



791
792
793
794
795
# File 'lib/kitchen/driver/azurerm.rb', line 791

def empty_deployment
  deployment = build_deployment(virtual_machine_deployment_template_file("empty.erb", nil), nil, mode: "Complete")
  debug(JSON.pretty_generate(deployment_template(deployment)))
  deployment
end

#enable_winrm_powershell_scriptString

PowerShell that opens the WinRM HTTP and HTTPS listeners and firewall ports.

Returns:

  • (String)

    the configured script, or the built-in default.



1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
# File 'lib/kitchen/driver/azurerm.rb', line 1095

def enable_winrm_powershell_script
  config[:winrm_powershell_script] ||
    <<-PS1
  $cert = New-SelfSignedCertificate -DnsName $env:COMPUTERNAME -CertStoreLocation Cert:\\LocalMachine\\My
  $config = '@{CertificateThumbprint="' + $cert.Thumbprint + '"}'
  winrm create winrm/config/listener?Address=*+Transport=HTTPS $config
  winrm create winrm/config/Listener?Address=*+Transport=HTTP
  winrm set winrm/config/service/auth '@{Basic="true";Kerberos="false";Negotiate="true";Certificate="false";CredSSP="true"}'
  New-NetFirewallRule -DisplayName "Windows Remote Management (HTTPS-In)" -Name "Windows Remote Management (HTTPS-In)" -Profile Any -LocalPort 5986 -Protocol TCP
  winrm set winrm/config/service '@{AllowUnencrypted="true"}'
  New-NetFirewallRule -DisplayName "Windows Remote Management (HTTP-In)" -Name "Windows Remote Management (HTTP-In)" -Profile Any -LocalPort 5985 -Protocol TCP
    PS1
end

#existing_state_value?(state, property) ⇒ Boolean

Whether a state property is already populated.

A blank value does not count. A run that fails early still writes its state, so an empty subscription_id - which is what a quoted ERB interpolation of an unset variable produces - used to be taken as authoritative from then on, shadowing the real value in config for the rest of the instance's life.

false is a value, so this asks whether the value is blank rather than whether it is truthy.

Parameters:

  • state (Hash)

    Hash of existing state values.

  • property (Symbol, String)

    the property to check.

Returns:

  • (Boolean)

    true when the key exists and its value is not blank.



555
556
557
# File 'lib/kitchen/driver/azurerm.rb', line 555

def existing_state_value?(state, property)
  state.key?(property) && !state[property].to_s.empty?
end

#follow_deployment_until_end_state(resource_group, deployment_name) ⇒ void

This method returns an undefined value.

Polls a deployment until it reaches a terminal provisioning state, logging the resources still in flight along the way.

Parameters:

  • resource_group (String)

    the resource group name.

  • deployment_name (String)

    the deployment name.

Raises:

  • (RuntimeError)

    with the Azure status message if the deployment failed.



852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
# File 'lib/kitchen/driver/azurerm.rb', line 852

def follow_deployment_until_end_state(resource_group, deployment_name)
  end_provisioning_states = %w{Canceled Failed Deleted Succeeded}
  deployment_provisioning_state = nil

  until end_provisioning_states.include?(deployment_provisioning_state)
    list_outstanding_deployment_operations(resource_group, deployment_name)
    sleep config[:deployment_sleep]
    deployment_provisioning_state = get_deployment_state(resource_group, deployment_name)
  end

  info "Resource Template deployment reached end state of '#{deployment_provisioning_state}'."
  return if deployment_provisioning_state == "Succeeded"

  show_failed_operations(resource_group, deployment_name)
  raise "Deployment '#{deployment_name}' in resource group '#{resource_group}' " \
        "ended in state '#{deployment_provisioning_state}'."
end

#format_data_disks_powershell_scriptString?

PowerShell that initialises and NTFS-formats every raw data disk.

Returns:

  • (String, nil)

    nil unless format_data_disks is enabled.



1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
# File 'lib/kitchen/driver/azurerm.rb', line 1112

def format_data_disks_powershell_script
  return unless config[:format_data_disks]

  info "Data disks will be initialized and formatted NTFS automatically." unless config[:data_disks].nil?
  config[:format_data_disks_powershell_script] ||
    <<-PS1
  Write-Host "Initializing and formatting raw disks"
  $disks = Get-Disk | where partitionstyle -eq 'raw'
  $letters = New-Object System.Collections.ArrayList
  $letters.AddRange( ('F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z') )
  Function AvailableVolumes() {
  $currentDrives = get-volume
  ForEach ($v in $currentDrives) {
    if ($letters -contains $v.DriveLetter.ToString()) {
  Write-Host "Drive letter $($v.DriveLetter) is taken, moving to next letter"
  $letters.Remove($v.DriveLetter.ToString())
}
    }
  }
  ForEach ($d in $disks) {
    AvailableVolumes
    $driveLetter = $letters[0]
    Write-Host "Creating volume $($driveLetter)"
    $d | Initialize-Disk -PartitionStyle GPT -PassThru | New-Partition -DriveLetter $driveLetter  -UseMaximumSize
    # Prevent error ' Cannot perform the requested operation while the drive is read only'
    Start-Sleep 1
    Format-Volume -FileSystem NTFS -NewFileSystemLabel "datadisk" -DriveLetter $driveLetter -Confirm:$false
  }
    PS1
end

#generate_key_pair(private_key_filename) ⇒ void

This method returns an undefined value.

Writes a fresh SSH key pair to disk.

Always called with KEY_GENERATION_MUTEX held.

Parameters:

  • private_key_filename (String)

    path to write the private key to.



722
723
724
725
726
727
728
729
730
# File 'lib/kitchen/driver/azurerm.rb', line 722

def generate_key_pair(private_key_filename)
  key = SSHKey.generate

  ::FileUtils.mkdir_p(File.dirname(private_key_filename))
  File.write(private_key_filename, key.private_key)
  File.chmod(0600, private_key_filename)
  File.write("#{private_key_filename}.pub", key.ssh_public_key)
  File.chmod(0600, "#{private_key_filename}.pub")
end

#generated_vm_name(state) ⇒ String

The VM name to use, either the configured one or one generated from vm_prefix plus part of the instance uuid.

The prefix is capped one character short of MAX_VM_NAME_LENGTH so that a vm_prefix longer than the documented three characters still yields a name Azure will accept. Leaving room for at least one uuid character does two things: it keeps some entropy in every generated name, and it guarantees the name ends with one, because a prefix that filled the whole budget could end on the separator it was written with. Azure rejects that outright - both for the VM and for the network interface named after it, which must end with a word character.

Parameters:

  • state (Hash)

    instance state, must already have a :uuid.

Returns:

  • (String)


598
599
600
601
602
603
# File 'lib/kitchen/driver/azurerm.rb', line 598

def generated_vm_name(state)
  return config[:vm_name] if config[:vm_name]

  prefix = config[:vm_prefix].to_s[0, MAX_VM_NAME_LENGTH - 1]
  "#{prefix}#{state[:uuid][0, MAX_VM_NAME_LENGTH - prefix.length]}"
end

#image_parametersHash

ARM parameters describing which image the VM boots from: either a managed image (or Azure Compute Gallery image) by resource id, or a Marketplace image URN.

Returns:

  • (Hash)


405
406
407
408
409
410
# File 'lib/kitchen/driver/azurerm.rb', line 405

def image_parameters
  return { "imageId" => config[:image_id] } if config[:image_id].to_s != ""

  publisher, offer, sku, version = image_urn.split(":", 4)
  { "imagePublisher" => publisher, "imageOffer" => offer, "imageSku" => sku, "imageVersion" => version }
end

#image_urnString

The Marketplace image URN to deploy from.

An image_urn: written with no value is nil rather than "", which counts as unset here the way it does everywhere else in the driver - so it falls back to DEFAULT_IMAGE_URN rather than failing with a Ruby error naming neither the setting nor the file it came from.

Returns:

  • (String)


420
421
422
423
# File 'lib/kitchen/driver/azurerm.rb', line 420

def image_urn
  urn = config[:image_urn].to_s
  urn.empty? ? DEFAULT_IMAGE_URN : urn
end

#list_outstanding_deployment_operations(resource_group, deployment_name) ⇒ void

This method returns an undefined value.

Logs every deployment operation that has not yet reached a terminal state.

Parameters:

  • resource_group (String)

    the resource group name.

  • deployment_name (String)

    the deployment name.



894
895
896
897
898
899
900
901
902
903
# File 'lib/kitchen/driver/azurerm.rb', line 894

def list_outstanding_deployment_operations(resource_group, deployment_name)
  end_operation_states = %w{Failed Succeeded}
  list_deployment_operations(resource_group, deployment_name).each do |operation|
    resource_provisioning_state = operation.dig("properties", "provisioningState")
    next if end_operation_states.include?(resource_provisioning_state)

    target = operation.dig("properties", "targetResource") || {}
    info "Resource #{target["resourceType"]} '#{target["resourceName"]}' provisioning status is #{resource_provisioning_state}"
  end
end

#missing_public_key_message(private_key_filename, public_key_filename, explicit) ⇒ String

Explains that the public half of the transport's key could not be found.

ssh_key names the private key, so a user who keeps only that has nothing wrong with their kitchen.yml. They used to get a raw Errno::ENOENT for a ".pub" path they never wrote, with nothing to say where it came from or what to do about it.

The key cannot simply be derived: sshkey only reads PEM-encoded RSA, while ssh-keygen has emitted the OpenSSH format by default since 7.8 and ed25519 keys are never PEM. ssh-keygen -y handles every format, so point at that instead.

Parameters:

  • private_key_filename (String)

    the configured private key.

  • public_key_filename (String)

    where the public key was looked for.

  • explicit (String, nil)

    ssh_public_key, when the user set it.

Returns:

  • (String)


748
749
750
751
752
753
754
755
756
757
# File 'lib/kitchen/driver/azurerm.rb', line 748

def missing_public_key_message(private_key_filename, public_key_filename, explicit)
  if explicit
    "The transport's ssh_public_key setting points at #{public_key_filename}, which does not exist. " \
      "Correct the path, or remove the setting to use #{private_key_filename}.pub."
  else
    "No public key was found at #{public_key_filename}. The transport's ssh_key setting names the " \
      "private key, and the public half of it has to go on the virtual machine. Create it with: " \
      "ssh-keygen -y -f #{private_key_filename} > #{public_key_filename}"
  end
end

#missing_required_configArray<String>

Required settings that were left unset.

Returns:

  • (Array<String>)


324
325
326
327
# File 'lib/kitchen/driver/azurerm.rb', line 324

def missing_required_config
  REQUIRED_CONFIG.select { |option, _| config[option].to_s.empty? }
    .map { |option, purpose| "#{option} is not set. It has no default: give it #{purpose}." }
end

#nic_name(state) ⇒ String

Name of the network interface the VM is attached to.

Parameters:

  • state (Hash)

    instance state.

Returns:

  • (String)

    nic_name from config, or one derived from the VM name.



464
465
466
# File 'lib/kitchen/driver/azurerm.rb', line 464

def nic_name(state)
  config[:nic_name].to_s.empty? ? "nic-#{state[:vm_name]}" : config[:nic_name].to_s
end

#nsg_portsArray<Integer>

Inbound TCP ports the generated security group opens.

Returns:

  • (Array<Integer>)

    the transport's own port(s) plus open_ports.



1305
1306
1307
# File 'lib/kitchen/driver/azurerm.rb', line 1305

def nsg_ports
  (transport_ports + Array(config[:open_ports]).map(&:to_i)).uniq
end

#nsg_rulesArray<Hash>

ARM security rules for the generated network security group.

The source prefix is left wide open, which matches the connectivity a Basic SKU public IP used to give with no security group at all. Narrow it by supplying your own group through nsg_id.

Returns:

  • (Array<Hash>)


1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
# File 'lib/kitchen/driver/azurerm.rb', line 1347

def nsg_rules
  nsg_ports.each_with_index.map do |port, index|
    {
      "name" => "allow-tcp-#{port}",
      "properties" => {
        "protocol" => "Tcp",
        "sourcePortRange" => "*",
        "destinationPortRange" => port.to_s,
        "sourceAddressPrefix" => "*",
        "destinationAddressPrefix" => "*",
        "access" => "Allow",
        "priority" => 1000 + index,
        "direction" => "Inbound",
      },
    }
  end
end

#os_disk_size_gbInteger

The OS disk size, as a number ARM will accept.

YAML makes os_disk_size_gb: 64 an Integer and os_disk_size_gb: "64" a String, and the ARM parameter is typed int - Azure rejects the String outright rather than coercing it:

The provided value for the template parameter 'osDiskSizeGb' is not
valid. Expected a value of type 'Integer', but received a value of
type 'String'.

Quoting a number in kitchen.yml is an easy thing to do, and the driver already tolerates the same ambiguity for boot_diagnostics_enabled.

Returns:

  • (Integer)

Raises:

  • (Kitchen::UserError)

    if the value is not a whole number.



440
441
442
443
444
445
# File 'lib/kitchen/driver/azurerm.rb', line 440

def os_disk_size_gb
  Integer(config[:os_disk_size_gb])
rescue ArgumentError, TypeError
  raise Kitchen::UserError,
    "os_disk_size_gb must be a whole number of gigabytes, but was #{config[:os_disk_size_gb].inspect}."
end

#parameters_in_values_format(parameters_in) ⇒ Hash?

Converts a flat parameter Hash into the ARM {name: {"value" => v}} shape.

Parameters:

  • parameters_in (Hash)

    parameter name to value.

Returns:

  • (Hash, nil)

    nil when parameters_in is empty.



837
838
839
840
841
842
843
# File 'lib/kitchen/driver/azurerm.rb', line 837

def parameters_in_values_format(parameters_in)
  return nil if parameters_in.nil? || parameters_in.empty?

  parameters_in.each_with_object({}) do |(key, value), acc|
    acc[key.to_s] = { "value" => value }
  end
end

#plan_jsonString?

Marketplace purchase plan for the image, when one is configured.

Returns:

  • (String, nil)

    JSON, or nil when no plan is configured.



1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
# File 'lib/kitchen/driver/azurerm.rb', line 1368

def plan_json
  plan_config = config[:plan]
  return nil if plan_config.nil? || plan_config.empty?

  plan = {}
  plan["name"] = plan_config[:name]                    if plan_config[:name]
  plan["product"] = plan_config[:product]              if plan_config[:product]
  plan["promotionCode"] = plan_config[:promotion_code] if plan_config[:promotion_code]
  plan["publisher"] = plan_config[:publisher]          if plan_config[:publisher]

  plan.to_json
end

#post_deployment(post_deployment_template_filename, post_deployment_parameters) ⇒ Hash

Builds the post-deployment from a caller-supplied ARM template file.

Parameters:

  • post_deployment_template_filename (String)

    path to an ARM template.

  • post_deployment_parameters (Hash)

    parameter name to value.

Returns:

  • (Hash)

    the deployment body



783
784
785
# File 'lib/kitchen/driver/azurerm.rb', line 783

def post_deployment(post_deployment_template_filename, post_deployment_parameters)
  build_deployment(::File.read(post_deployment_template_filename), post_deployment_parameters)
end

#power_state_status(state, resource_group, vm_name) ⇒ Hash

Asks Azure for the virtual machine's power state.

Parameters:

  • state (Hash)

    the instance state.

  • resource_group (String)
  • vm_name (String)

Returns:



999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
# File 'lib/kitchen/driver/azurerm.rb', line 999

def power_state_status(state, resource_group, vm_name)
  view = status_arm_client(state).virtual_machine_instance_view(resource_group, vm_name)
  status = Array(view["statuses"]).find { |entry| entry["code"].to_s.start_with?("PowerState/") }
  power = status && status["code"].to_s.split("/", 2).last

  {
    live: power ? POWER_STATES[power] : nil,
    state: power || "unknown",
    resource_id: virtual_machine_id(status_subscription_id(state), resource_group, vm_name),
    message: status && status["displayStatus"],
  }
end

#pre_deployment(pre_deployment_template_filename, pre_deployment_parameters) ⇒ Hash

Builds the pre-deployment from a caller-supplied ARM template file.

Parameters:

  • pre_deployment_template_filename (String)

    path to an ARM template.

  • pre_deployment_parameters (Hash)

    parameter name to value.

Returns:

  • (Hash)

    the deployment body



764
765
766
# File 'lib/kitchen/driver/azurerm.rb', line 764

def pre_deployment(pre_deployment_template_filename, pre_deployment_parameters)
  build_deployment(::File.read(pre_deployment_template_filename), pre_deployment_parameters)
end

#prepared_custom_dataString?

Base64-encoded custom data for the VM.

Returns:

  • (String, nil)

    nil when no custom_data is configured.



1395
1396
1397
1398
1399
# File 'lib/kitchen/driver/azurerm.rb', line 1395

def prepared_custom_data
  return nil if config[:custom_data].nil?

  @prepared_custom_data ||= Base64.strict_encode64(custom_data_content)
end

#public_ip?Boolean

Whether this deployment gets a public IP address.

Returns:

  • (Boolean)


1278
1279
1280
# File 'lib/kitchen/driver/azurerm.rb', line 1278

def public_ip?
  config[:vnet_id].to_s.empty? || !!config[:public_ip]
end

#public_key_for_deployment(private_key_filename) ⇒ String

Returns the public key to inject into the deployment, generating a new key pair on disk when the configured private key does not yet exist.

Parameters:

  • private_key_filename (String)

    path to the transport's private key.

Returns:

  • (String)

    the OpenSSH-format public key, stripped of whitespace.



702
703
704
705
706
707
708
709
710
711
712
713
714
# File 'lib/kitchen/driver/azurerm.rb', line 702

def public_key_for_deployment(private_key_filename)
  KEY_GENERATION_MUTEX.synchronize do
    generate_key_pair(private_key_filename) unless File.file?(private_key_filename)

    explicit = instance.transport[:ssh_public_key]
    public_key_filename = explicit || "#{private_key_filename}.pub"
    unless File.file?(public_key_filename)
      raise Kitchen::UserError, missing_public_key_message(private_key_filename, public_key_filename, explicit)
    end

    File.read(public_key_filename).strip
  end
end

#resolve_hostname(state, vmnic) ⇒ String

Determines the address the transport should connect to.

Uses the public IP (or its FQDN, when use_fqdn_hostname is set) unless the instance was deployed into a caller-supplied vnet without a public IP, in which case the NIC's private address is used.

Parameters:

  • state (Hash)

    instance state.

  • vmnic (String)

    name of the network interface.

Returns:

  • (String)

    IP address or fully-qualified domain name.



521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
# File 'lib/kitchen/driver/azurerm.rb', line 521

def resolve_hostname(state, vmnic)
  if public_ip?
    result = get_public_ip(state[:azure_resource_group_name], "publicip")
    ip_address = result.dig("properties", "ipAddress")
    fqdn = result.dig("properties", "dnsSettings", "fqdn")
    info "IP Address is: #{ip_address} [#{fqdn}]"
    if config[:use_fqdn_hostname]
      info "Using FQDN to communicate instead of IP"
      fqdn
    else
      ip_address
    end
  else
    result = get_network_interface(state[:azure_resource_group_name], vmnic.to_s)
    private_ip = result.dig("properties", "ipConfigurations", 0, "properties", "privateIPAddress")
    info "IP Address is: #{private_ip}"
    private_ip
  end
end

#run_deployment(state, prefix, deployment) ⇒ void

This method returns an undefined value.

Submits a named deployment and blocks until it reaches an end state.

Parameters:

  • state (Hash)

    instance state, used for the resource group and uuid.

  • prefix (String)

    deployment name prefix, e.g. "pre-deploy".

  • deployment (Hash)

    the deployment body



474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
# File 'lib/kitchen/driver/azurerm.rb', line 474

def run_deployment(state, prefix, deployment)
  name = "#{prefix}-#{state[:uuid]}"
  info "Creating deployment: #{name}"
  begin
    create_deployment_async(state[:azure_resource_group_name], name, deployment)
  rescue Azure::OperationError => operation_error
    raise unless operation_error.code == "DeploymentActive"

    # An interrupted `kitchen create` leaves its deployment running in
    # Azure. Wait for that one instead of abandoning the rest of create:
    # the deployment already in flight is the one we wanted, and the
    # steps after this still have to run for the instance to be usable.
    info "Deployment #{name} is already running; waiting for it rather than submitting it again."
    info "To deploy a changed template, run `kitchen destroy` for this instance first."
  end
  follow_deployment_until_end_state(state[:azure_resource_group_name], name)
end

#show_failed_operations(resource_group, deployment_name) ⇒ void

This method returns an undefined value.

Raises with the status messages of every failed operation in a deployment.

Returns quietly when no single operation reported a failure, leaving the caller to raise: a deployment can fail without one, and its own provisioning state is the authority on whether it worked.

Parameters:

  • resource_group (String)

    the resource group name.

  • deployment_name (String)

    the deployment name.

Raises:

  • (RuntimeError)

    if any operation reported a non-OK status code.



880
881
882
883
884
885
886
887
# File 'lib/kitchen/driver/azurerm.rb', line 880

def show_failed_operations(resource_group, deployment_name)
  failures = list_deployment_operations(resource_group, deployment_name).reject do |operation|
    operation.dig("properties", "statusCode") == "OK"
  end
  return if failures.empty?

  raise failures.map { |operation| operation.dig("properties", "statusMessage").inspect }.join("\n")
end

#status(state) ⇒ Hash

Reports whether the virtual machine backing this instance is running.

Drives kitchen list --live (and its kitchen status alias). Test Kitchen rescues anything raised here and reports "unknown", but a listing should not be where an Azure outage first shows up, so the failure modes are handled explicitly and reported as data.

No retries: this is a status probe behind an interactive command, and waiting out the retry budget on every unreachable instance would be worse than saying so promptly.

Parameters:

  • state (Hash)

    the instance state.

Returns:

  • (Hash)

    :live, :state, :resource_id and :message.



934
935
936
937
938
939
940
941
942
943
944
945
946
947
# File 'lib/kitchen/driver/azurerm.rb', line 934

def status(state)
  resource_group = state[:azure_resource_group_name]
  vm_name = state[:vm_name]
  return { live: false, state: "not_created", message: "No Azure virtual machine has been created yet." } unless
    state[:server_id] && resource_group && vm_name

  power_state_status(state, resource_group, vm_name)
rescue Azure::OperationError => operation_error
  return { live: false, state: "not_created", message: "The virtual machine no longer exists in Azure." } if operation_error.status == 404

  { live: nil, state: "unknown", message: "#{operation_error.code}: #{operation_error.message}" }
rescue Azure::TransientError => transient_error
  { live: nil, state: "unknown", message: "Could not reach Azure (#{transient_error.message})." }
end

#status_arm_client(state) ⇒ Azure::ArmClient

An ARM client for a status probe, built from state the way #destroy builds one, so that an instance created against another subscription or cloud is still asked about in the right place.

Parameters:

  • state (Hash)

    the instance state.

Returns:



1018
1019
1020
1021
1022
1023
# File 'lib/kitchen/driver/azurerm.rb', line 1018

def status_arm_client(state)
  @arm_client ||= Kitchen::Driver::AzureCredentials.new(
    subscription_id: status_subscription_id(state),
    environment: state[:azure_environment] || config[:azure_environment]
  ).arm_client
end

#status_subscription_id(state) ⇒ String?

Returns the subscription the instance was created in.

Parameters:

  • state (Hash)

    the instance state.

Returns:

  • (String, nil)

    the subscription the instance was created in.



1027
1028
1029
# File 'lib/kitchen/driver/azurerm.rb', line 1027

def status_subscription_id(state)
  state[:subscription_id] || config[:subscription_id]
end

#store_deployment_credentials(state, deployment_parameters) ⇒ void

This method returns an undefined value.

Persists the generated admin credentials into instance state, when store_deployment_credentials_in_state is enabled.

No password is stored when the transport authenticates with an SSH key - there is no password in that case, and writing a nil one leaves a misleading empty entry in the state file.

Parameters:



502
503
504
505
506
507
508
509
510
# File 'lib/kitchen/driver/azurerm.rb', line 502

def store_deployment_credentials(state, deployment_parameters)
  return unless config[:store_deployment_credentials_in_state] == true

  state[:username] = deployment_parameters[:adminUsername] unless existing_state_value?(state, :username)

  return unless instance.transport[:ssh_key].nil?

  state[:password] = deployment_parameters[:adminPassword] unless existing_state_value?(state, :password)
end

#subnet_referenceString

Resource id of the subnet the network interface attaches to.

subnet_id has always held the subnet's name, resolved against vnet_id. The setting is named like a resource id and sits directly beside vnet_id, which really is one, so supplying a full subnet resource id is an easy mistake to make - and it used to be appended to the vnet id, leaving ARM to reject a path that appears nowhere in the user's kitchen.yml. Both spellings are accepted.

Returns:

  • (String)


1257
1258
1259
1260
1261
1262
# File 'lib/kitchen/driver/azurerm.rb', line 1257

def subnet_reference
  subnet = config[:subnet_id].to_s
  return subnet if subnet.start_with?("/subscriptions/")

  "#{config[:vnet_id]}/subnets/#{subnet}"
end

#template_for_transport_nameString

The deployment template, adjusted for the transport in use.

WinRM instances get a custom data bootstrap script and unattend content; SSH instances get the public half of the transport's key injected into the Linux configuration.

Returns:

  • (String)

    the deployment template as JSON.



653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
# File 'lib/kitchen/driver/azurerm.rb', line 653

def template_for_transport_name
  template = JSON.parse(virtual_machine_deployment_template)

  if instance.transport.name.casecmp("winrm") == 0 && instance.platform.name.to_s.index("nano").nil?
    info "Adding WinRM configuration to provisioning profile."
    encoded_command = Base64.strict_encode64(custom_data_script_windows)
    virtual_machine_resources(template).each do |resource|
      resource["properties"]["osProfile"]["customData"] = encoded_command
      resource["properties"]["osProfile"]["windowsConfiguration"] = windows_unattend_content
    end
  end

  unless instance.transport[:ssh_key].nil?
    info "Adding public key from #{File.expand_path(instance.transport[:ssh_key])}.pub to the deployment."
    public_key = public_key_for_deployment(File.expand_path(instance.transport[:ssh_key]))
    virtual_machine_resources(template).each do |resource|
      resource["properties"]["osProfile"]["linuxConfiguration"] = JSON.parse(custom_linux_configuration(public_key))
    end
  end

  template.to_json
end

#transport_portsArray<Integer>

The port(s) the configured transport connects on.

The transport already knows which port it will dial, so a port set on it is authoritative: assuming the default instead produced an instance nothing could reach, and left the user repeating the port in open_ports to get in.

WinRM keeps both standard ports regardless, because #enable_winrm_powershell_script creates both listeners whatever the transport was pointed at.

Returns:

  • (Array<Integer>)


1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
# File 'lib/kitchen/driver/azurerm.rb', line 1321

def transport_ports
  configured = instance.transport[:port].to_i

  if winrm_transport?
    ([5985, 5986] + [configured]).reject(&:zero?).uniq
  elsif configured == 0
    [22]
  else
    [configured]
  end
end

#unreachable_azureArray<String>

Whether Azure answers, accepts the credentials, and knows the subscription.

Reads the subscription rather than probing a resource group: ARM answers a resource group HEAD with 404 both for a subscription that does not exist and for one that merely has no such group, so it cannot tell a wrong subscription from a healthy one. Reading the subscription gives back either its details or SubscriptionNotFound.

Returns:

  • (Array<String>)


339
340
341
342
343
344
345
346
347
# File 'lib/kitchen/driver/azurerm.rb', line 339

def unreachable_azure
  Kitchen::Driver::AzureCredentials.new(subscription_id: config[:subscription_id],
    environment: config[:azure_environment]).arm_client.subscription
  []
rescue Azure::OperationError => operation_error
  [azure_problem(operation_error)]
rescue Azure::TransientError => transient_error
  ["Could not reach Azure (#{transient_error.message})."]
end

#validate_state(state = {}) ⇒ Hash

Fills in any state values that are not already present.

Parameters:

  • state (Hash) (defaults to: {})

    existing Hash of state values.

Returns:

  • (Hash)

    the same Hash, with defaults applied.



563
564
565
566
567
568
569
570
571
572
573
# File 'lib/kitchen/driver/azurerm.rb', line 563

def validate_state(state = {})
  state[:uuid] = SecureRandom.hex(8) unless existing_state_value?(state, :uuid)
  state[:vm_name] = generated_vm_name(state) unless existing_state_value?(state, :vm_name)
  state[:server_id] = "vm#{state[:uuid]}" unless existing_state_value?(state, :server_id)
  state[:azure_resource_group_name] = azure_resource_group_name unless existing_state_value?(state, :azure_resource_group_name)
  %i{subscription_id azure_environment use_managed_disks}.each do |config_element|
    state[config_element] = config[config_element] unless existing_state_value?(state, config_element)
  end
  state.delete(:password) unless instance.transport[:ssh_key].nil?
  state
end

#virtual_machine_deployment_templateString

Renders the virtual machine ARM template for the current configuration.

Uses internal.erb when the instance deploys into a caller-supplied vnet, otherwise public.erb.

Returns:

  • (String)

    the rendered ARM template as JSON.



1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
# File 'lib/kitchen/driver/azurerm.rb', line 1211

def virtual_machine_deployment_template
  data = {
    vm_tags: vm_tag_string(config[:vm_tags]),
    storage_account_type: config[:storage_account_type],
    # A setting written with no value is nil, not "". The templates ask
    # these two whether they are empty, so give them something that can
    # answer.
    image_id: config[:image_id].to_s,
    custom_data: config[:custom_data].to_s,
    os_disk_size_gb: config[:os_disk_size_gb],
    data_disks_for_vm_json:,
    use_ephemeral_osdisk: config[:use_ephemeral_osdisk],
    ssh_key: instance.transport[:ssh_key],
    plan_json:,
    secret_url: config[:secret_url],
    vault_name: config[:vault_name],
    vault_resource_group: config[:vault_resource_group],
    create_nsg: create_nsg?,
    attach_nsg: attach_nsg?,
    nsg_id: config[:nsg_id],
    nsg_rules_json: nsg_rules.to_json,
  }

  if config[:vnet_id].to_s.empty?
    virtual_machine_deployment_template_file("public.erb", data)
  else
    info "Using custom vnet: #{config[:vnet_id]}"
    virtual_machine_deployment_template_file("internal.erb", data.merge(
      vnet_id: config[:vnet_id],
      subnet_ref: subnet_reference,
      public_ip: config[:public_ip],
      public_ip_sku: config[:public_ip_sku]
    ))
  end
end

#virtual_machine_deployment_template_file(template_file, data = {}) ⇒ String

Renders one of the bundled ERB templates.

Parameters:

  • template_file (String)

    file name within templates/.

  • data (Hash, nil) (defaults to: {})

    values exposed to the template.

Returns:

  • (String)

    the rendered template.



1386
1387
1388
1389
1390
# File 'lib/kitchen/driver/azurerm.rb', line 1386

def virtual_machine_deployment_template_file(template_file, data = {})
  template = File.read(File.expand_path(File.join(__dir__, "../../../templates", template_file)))
  render_binding = OpenStruct.new(data)
  ERB.new(template, trim_mode: "-").result(render_binding.instance_eval { binding })
end

#virtual_machine_id(subscription_id, resource_group, vm_name) ⇒ String

Returns the virtual machine's ARM resource id.

Parameters:

  • subscription_id (String)
  • resource_group (String)
  • vm_name (String)

Returns:

  • (String)

    the virtual machine's ARM resource id.



1035
1036
1037
1038
# File 'lib/kitchen/driver/azurerm.rb', line 1035

def virtual_machine_id(subscription_id, resource_group, vm_name)
  "/subscriptions/#{subscription_id}/resourceGroups/#{resource_group}" \
    "/providers/Microsoft.Compute/virtualMachines/#{vm_name}"
end

#virtual_machine_resources(template) ⇒ Array<Hash>

Selects the virtual machine resources from a parsed ARM template.

Parameters:

  • template (Hash)

    a parsed ARM template.

Returns:

  • (Array<Hash>)


680
681
682
# File 'lib/kitchen/driver/azurerm.rb', line 680

def virtual_machine_resources(template)
  template["resources"].select { |resource| resource["type"] == "Microsoft.Compute/virtualMachines" }
end

#vm_tag_string(vm_tags_in) ⇒ String

Renders resource tags as a JSON object body (no surrounding braces), for interpolation into the ERB deployment templates.

Keys and values are JSON-encoded so that tags containing quotes or backslashes cannot produce an unparseable template.

Parameters:

  • vm_tags_in (Hash)

    tag name to value.

Returns:

  • (String)

    e.g. "os_type": "linux",\n"distro": "redhat"



827
828
829
830
831
# File 'lib/kitchen/driver/azurerm.rb', line 827

def vm_tag_string(vm_tags_in)
  return "" if vm_tags_in.nil? || vm_tags_in.empty?

  vm_tags_in.map { |key, value| "#{key.to_s.to_json}: #{value.to_s.to_json}" }.join(",\n")
end

#warn_about_deprecated_configvoid

This method returns an undefined value.

Warns about settings that Azure retirements have made inoperable.



1267
1268
1269
1270
1271
1272
1273
# File 'lib/kitchen/driver/azurerm.rb', line 1267

def warn_about_deprecated_config
  DEPRECATED_CONFIG.each do |option, reason|
    next unless config.key?(option)

    warn "The '#{option}' setting is no longer supported and is being ignored. #{reason}"
  end
end

#windows_unattend_contentHash

The ARM windowsConfiguration block that runs the custom data script on first logon.

Returns:

  • (Hash)


1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
# File 'lib/kitchen/driver/azurerm.rb', line 1186

def windows_unattend_content
  {
    additionalUnattendContent: [
      {
        passName: "oobeSystem",
        componentName: "Microsoft-Windows-Shell-Setup",
        settingName: "FirstLogonCommands",
        content: '<FirstLogonCommands><SynchronousCommand><CommandLine>cmd /c "copy C:\\AzureData\\CustomData.bin C:\\Config.ps1"</CommandLine><Description>copy</Description><Order>1</Order></SynchronousCommand><SynchronousCommand><CommandLine>%windir%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoProfile -ExecutionPolicy Bypass -file C:\\Config.ps1</CommandLine><Description>script</Description><Order>2</Order></SynchronousCommand></FirstLogonCommands>',
      },
      {
        passName: "oobeSystem",
        componentName: "Microsoft-Windows-Shell-Setup",
        settingName: "AutoLogon",
        content: "[concat('<AutoLogon><Password><Value>', parameters('adminPassword'), '</Value></Password><Enabled>true</Enabled><LogonCount>1</LogonCount><Username>', parameters('adminUserName'), '</Username></AutoLogon>')]",
      },
    ],
  }
end

#winrm_transport?Boolean

Whether the instance is driven over WinRM.

Returns:

  • (Boolean)


1336
1337
1338
# File 'lib/kitchen/driver/azurerm.rb', line 1336

def winrm_transport?
  instance.transport.name.to_s.casecmp("winrm") == 0
end