kitchen-azurerm 0.3.5 → 0.3.6

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
  SHA1:
3
- metadata.gz: 8651b3f882557721799353ed9098d5a3f12b9e57
4
- data.tar.gz: c1bb1f88699542c1760eed48bb95065aa64e976b
3
+ metadata.gz: c5bf6aca89882e1148668e693edae5dc6e37d1f7
4
+ data.tar.gz: d4c53242bf99c9aa1d2a0bfb43e3bf694360b5b6
5
5
  SHA512:
6
- metadata.gz: c1e2ef35e30913b144048b6415bf6acca33ad163f88b855158d76cfb36cde02695b6d76dfdeda304bf6f94bf3c11ca5f003450e855bd76a5566f59c1d52f3fd3
7
- data.tar.gz: d9aa055ba5e111a9e8c66bac9f4c0972558bde77be98335c1ef974300598be58e1ae28b79c986a471af47c985ddcd7f1be3f3239618a4432f2cf421ca8cbba54
6
+ metadata.gz: 60be9d3b4755c863e35cc42446604d09c8707886cf8bd65c0fcfa43a6f58181cb0901b654301ef1a95ed425879167cbdc9a88da4e0589b4935bc4e88197778e5
7
+ data.tar.gz: f94c74f78b15c39b75544e152f04b1ce915ebf72272f972ce8f32af9c0596c36a36ef0bd66951f2ce5456db4a3a58920be0461c7527949a743558185896302b7
@@ -1,5 +1,8 @@
1
1
  # kitchen-azurerm Changelog
2
2
 
3
+ ## [0.3.6] - 2016-05-10
4
+ - Remove version pin on inifile gem dependency, compatible with newer ChefDK (@stuartpreston)
5
+
3
6
  ## [0.3.5] - 2016-03-21
4
7
  - Remove transport name restriction on SSH key upload (allow rsync support) (@stuartpreston)
5
8
  - Support SSH public keys with newlines as generated by ssh-keygen (@stuartpreston)
data/README.md CHANGED
@@ -25,7 +25,7 @@ You are now ready to configure kitchen-azurerm to use the credentials from the s
25
25
  1. **Subscription ID**: available from the Azure portal
26
26
  2. **Client ID**: this will be the Application Id from the application in step 2.
27
27
  3. **Client Secret/Password**: this will be the password you supplied in the command in step 2.
28
- 4. **Tenant ID**: listed after the command in step 5.
28
+ 4. **Tenant ID**: use the command detailed in "Manually provide credentials through Azure CLI" step 1 to get the TenantId.
29
29
 
30
30
  Using a text editor, open or create the file ```~/.azure/credentials``` and add the following section, noting there is one section per Subscription ID. **Make sure you save the file with UTF-8 encoding**
31
31
 
@@ -1,550 +1,550 @@
1
- require 'kitchen'
2
- require 'kitchen/driver/credentials'
3
- require 'securerandom'
4
- require 'azure_mgmt_resources'
5
- require 'azure_mgmt_network'
6
- require 'base64'
7
- require 'sshkey'
8
- require 'fileutils'
9
-
10
- module Kitchen
11
- module Driver
12
- #
13
- # Azurerm
14
- #
15
- class Azurerm < Kitchen::Driver::Base
16
- attr_accessor :resource_management_client
17
-
18
- default_config(:azure_resource_group_name) do |config|
19
- "kitchen-#{config.instance.name}"
20
- end
21
-
22
- default_config(:image_urn) do |_config|
23
- 'Canonical:UbuntuServer:14.04.3-LTS:latest'
24
- end
25
-
26
- default_config(:username) do |_config|
27
- 'azure'
28
- end
29
-
30
- default_config(:password) do |_config|
31
- 'P2ssw0rd'
32
- end
33
-
34
- default_config(:vm_name) do |_config|
35
- 'vm'
36
- end
37
-
38
- default_config(:storage_account_type) do |_config|
39
- 'Standard_LRS'
40
- end
41
-
42
- default_config(:boot_diagnostics_enabled) do |_config|
43
- 'true'
44
- end
45
-
46
- default_config(:winrm_powershell_script) do |_config|
47
- false
48
- end
49
-
50
- default_config(:azure_management_url) do |_config|
51
- 'https://management.azure.com'
52
- end
53
-
54
- def create(state)
55
- state = validate_state(state)
56
-
57
- image_publisher, image_offer, image_sku, image_version = config[:image_urn].split(':', 4)
58
- deployment_parameters = {
59
- location: config[:location],
60
- vmSize: config[:machine_size],
61
- storageAccountType: config[:storage_account_type],
62
- bootDiagnosticsEnabled: config[:boot_diagnostics_enabled],
63
- newStorageAccountName: "storage#{state[:uuid]}",
64
- adminUsername: state[:username],
65
- adminPassword: state[:password],
66
- dnsNameForPublicIP: "kitchen-#{state[:uuid]}",
67
- imagePublisher: image_publisher,
68
- imageOffer: image_offer,
69
- imageSku: image_sku,
70
- imageVersion: image_version,
71
- vmName: state[:vm_name]
72
- }
73
-
74
- credentials = Kitchen::Driver::Credentials.new.azure_credentials_for_subscription(config[:subscription_id])
75
- @resource_management_client = ::Azure::ARM::Resources::ResourceManagementClient.new(credentials)
76
- @resource_management_client.subscription_id = config[:subscription_id]
77
-
78
- # Create Resource Group
79
- resource_group = ::Azure::ARM::Resources::Models::ResourceGroup.new
80
- resource_group.location = config[:location]
81
- begin
82
- info "Creating Resource Group: #{state[:azure_resource_group_name]}"
83
- resource_management_client.resource_groups.create_or_update(state[:azure_resource_group_name], resource_group).value!
84
- rescue ::MsRestAzure::AzureOperationError => operation_error
85
- error_message = if operation_error.body.nil? == true
86
- operation_error.inspect
87
- else
88
- operation_error.body['error']
89
- end
90
- info error_message
91
- raise error_message
92
- end
93
-
94
- # Execute deployment steps
95
- begin
96
- deployment_name = "deploy-#{state[:uuid]}"
97
- info "Creating Deployment: #{deployment_name}"
98
- resource_management_client.deployments.create_or_update(state[:azure_resource_group_name], deployment_name, deployment(deployment_parameters)).value!
99
- rescue ::MsRestAzure::AzureOperationError => operation_error
100
- rest_error = operation_error.body['error']
101
- deployment_active = rest_error['code'] == 'DeploymentActive'
102
- if deployment_active
103
- info "Deployment for resource group #{state[:azure_resource_group_name]} is ongoing."
104
- info "If you need to change the deployment template you'll need to rerun `kitchen create` for this instance."
105
- else
106
- info rest_error
107
- raise operation_error
108
- end
109
- end
110
-
111
- # Monitor all operations until completion
112
- follow_deployment_until_end_state(state[:azure_resource_group_name], deployment_name)
113
-
114
- # Now retrieve the public IP from the resource group:
115
- network_management_client = ::Azure::ARM::Network::NetworkResourceProviderClient.new(credentials)
116
- network_management_client.subscription_id = config[:subscription_id]
117
- result = network_management_client.public_ip_addresses.get(state[:azure_resource_group_name], 'publicip').value!
118
- info "IP Address is: #{result.body.properties.ip_address} [#{result.body.properties.dns_settings.fqdn}]"
119
- state[:hostname] = result.body.properties.ip_address
120
- end
121
-
122
- def existing_state_value?(state, property)
123
- state.key?(property) && !state[property].nil?
124
- end
125
-
126
- def validate_state(state = {})
127
- state[:uuid] = SecureRandom.hex(8) unless existing_state_value?(state, :uuid)
128
- state[:server_id] = "vm#{state[:uuid]}" unless existing_state_value?(state, :server_id)
129
- state[:azure_resource_group_name] = azure_resource_group_name unless existing_state_value?(state, :azure_resource_group_name)
130
- [:subscription_id, :username, :password, :vm_name, :azure_management_url].each do |config_element|
131
- state[config_element] = config[config_element] unless existing_state_value?(state, config_element)
132
- end
133
-
134
- state
135
- end
136
-
137
- def azure_resource_group_name
138
- formatted_time = Time.now.utc.strftime '%Y%m%dT%H%M%S'
139
- "#{config[:azure_resource_group_name]}-#{formatted_time}"
140
- end
141
-
142
- def template_for_transport_name
143
- template = JSON.parse(virtual_machine_deployment_template)
144
- if instance.transport.name.casecmp('winrm') == 0
145
- encoded_command = Base64.strict_encode64(enable_winrm_powershell_script)
146
- command = command_to_execute
147
- template['resources'].select { |h| h['type'] == 'Microsoft.Compute/virtualMachines' }.each do |resource|
148
- resource['properties']['osProfile']['customData'] = encoded_command
149
- end
150
- template['resources'] << JSON.parse(custom_script_extension_template(command))
151
- end
152
-
153
- unless instance.transport[:ssh_key].nil?
154
- info "Adding public key from #{File.expand_path(instance.transport[:ssh_key])}.pub to the deployment."
155
- public_key = public_key_for_deployment(File.expand_path(instance.transport[:ssh_key]))
156
- template['resources'].select { |h| h['type'] == 'Microsoft.Compute/virtualMachines' }.each do |resource|
157
- resource['properties']['osProfile']['linuxConfiguration'] = JSON.parse(custom_linux_configuration(public_key))
158
- end
159
- end
160
- template.to_json
161
- end
162
-
163
- def public_key_for_deployment(private_key_filename)
164
- if File.file?(private_key_filename) == false
165
- k = SSHKey.generate
166
-
167
- ::FileUtils.mkdir_p(File.dirname(private_key_filename))
168
-
169
- private_key_file = File.new(private_key_filename, 'w')
170
- private_key_file.syswrite(k.private_key)
171
- private_key_file.chmod(0600)
172
- private_key_file.close
173
-
174
- public_key_file = File.new("#{private_key_filename}.pub", 'w')
175
- public_key_file.syswrite(k.ssh_public_key)
176
- public_key_file.chmod(0600)
177
- public_key_file.close
178
-
179
- output = k.ssh_public_key
180
- else
181
- output = File.read("#{private_key_filename}.pub")
182
- end
183
- output.strip
184
- end
185
-
186
- def deployment(parameters)
187
- template = template_for_transport_name
188
- deployment = ::Azure::ARM::Resources::Models::Deployment.new
189
- deployment.properties = ::Azure::ARM::Resources::Models::DeploymentProperties.new
190
- deployment.properties.mode = Azure::ARM::Resources::Models::DeploymentMode::Incremental
191
- deployment.properties.template = JSON.parse(template)
192
- deployment.properties.parameters = parameters_in_values_format(parameters)
193
- debug(deployment.properties.template)
194
- deployment
195
- end
196
-
197
- def parameters_in_values_format(parameters_in)
198
- parameters = parameters_in.map do |key, value|
199
- { key.to_sym => { 'value' => value } }
200
- end
201
- parameters.reduce(:merge!)
202
- end
203
-
204
- def follow_deployment_until_end_state(resource_group, deployment_name)
205
- end_provisioning_states = 'Canceled,Failed,Deleted,Succeeded'
206
- end_provisioning_state_reached = false
207
- until end_provisioning_state_reached
208
- list_outstanding_deployment_operations(resource_group, deployment_name)
209
- sleep 10
210
- deployment_provisioning_state = deployment_state(resource_group, deployment_name)
211
- end_provisioning_state_reached = end_provisioning_states.split(',').include?(deployment_provisioning_state)
212
- end
213
- info "Resource Template deployment reached end state of '#{deployment_provisioning_state}'."
214
- show_failed_operations(resource_group, deployment_name) if deployment_provisioning_state == 'Failed'
215
- end
216
-
217
- def show_failed_operations(resource_group, deployment_name)
218
- failed_operations = resource_management_client.deployment_operations.list(resource_group, deployment_name).value!
219
- failed_operations.body.value.each do |val|
220
- resource_code = val.properties.status_code
221
- raise val.properties.status_message.inspect.to_s if resource_code != 'OK'
222
- end
223
- end
224
-
225
- def list_outstanding_deployment_operations(resource_group, deployment_name)
226
- end_operation_states = 'Failed,Succeeded'
227
- deployment_operations = resource_management_client.deployment_operations.list(resource_group, deployment_name).value!
228
- deployment_operations.body.value.each do |val|
229
- resource_provisioning_state = val.properties.provisioning_state
230
- resource_name = val.properties.target_resource.resource_name
231
- resource_type = val.properties.target_resource.resource_type
232
- end_operation_state_reached = end_operation_states.split(',').include?(resource_provisioning_state)
233
- unless end_operation_state_reached
234
- info "Resource #{resource_type} '#{resource_name}' provisioning status is #{resource_provisioning_state}"
235
- end
236
- end
237
- end
238
-
239
- def deployment_state(resource_group, deployment_name)
240
- deployments = resource_management_client.deployments.get(resource_group, deployment_name).value!
241
- deployments.body.properties.provisioning_state
242
- end
243
-
244
- def destroy(state)
245
- return if state[:server_id].nil?
246
- credentials = Kitchen::Driver::Credentials.new.azure_credentials_for_subscription(state[:subscription_id])
247
- resource_management_client = ::Azure::ARM::Resources::ResourceManagementClient.new(credentials, state[:azure_management_url])
248
- resource_management_client.subscription_id = state[:subscription_id]
249
- begin
250
- info "Destroying Resource Group: #{state[:azure_resource_group_name]}"
251
- resource_management_client.resource_groups.begin_delete(state[:azure_resource_group_name]).value!
252
- info 'Destroy operation accepted and will continue in the background.'
253
- rescue ::MsRestAzure::AzureOperationError => operation_error
254
- info operation_error.body['error']
255
- raise operation_error
256
- end
257
- state.delete(:server_id)
258
- state.delete(:hostname)
259
- state.delete(:username)
260
- state.delete(:password)
261
- end
262
-
263
- def enable_winrm_powershell_script
264
- config[:winrm_powershell_script] || <<-PS1
265
- $cert = New-SelfSignedCertificate -DnsName $env:COMPUTERNAME -CertStoreLocation Cert:\\LocalMachine\\My
266
- $config = '@{CertificateThumbprint="' + $cert.Thumbprint + '"}'
267
- winrm create winrm/config/listener?Address=*+Transport=HTTPS $config
268
- winrm set winrm/config/service/auth '@{Basic="true";Kerberos="false";Negotiate="true";Certificate="false";CredSSP="true"}'
269
- New-NetFirewallRule -DisplayName "Windows Remote Management (HTTPS-In)" -Name "Windows Remote Management (HTTPS-In)" -Profile Any -LocalPort 5986 -Protocol TCP
270
- winrm set winrm/config/service '@{AllowUnencrypted="true"}'
271
- New-NetFirewallRule -DisplayName "Windows Remote Management (HTTP-In)" -Name "Windows Remote Management (HTTP-In)" -Profile Any -LocalPort 5985 -Protocol TCP
272
- PS1
273
- end
274
-
275
- def command_to_execute
276
- 'copy /y c:\\\\azuredata\\\\customdata.bin c:\\\\azuredata\\\\customdata.ps1 && powershell.exe -ExecutionPolicy Unrestricted -Command \\"start-process powershell.exe -verb runas -argumentlist c:\\\\azuredata\\\\customdata.ps1\\"'
277
- end
278
-
279
- def custom_linux_configuration(public_key)
280
- <<-EOH
281
- {
282
- "disablePasswordAuthentication": "true",
283
- "ssh": {
284
- "publicKeys": [
285
- {
286
- "path": "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]",
287
- "keyData": "#{public_key}"
288
- }
289
- ]
290
- }
291
- }
292
- EOH
293
- end
294
-
295
- def custom_script_extension_template(command)
296
- <<-EOH
297
- {
298
- "type": "Microsoft.Compute/virtualMachines/extensions",
299
- "name": "[concat(variables('vmName'),'/','enableWinRM')]",
300
- "apiVersion": "2015-05-01-preview",
301
- "location": "[variables('location')]",
302
- "dependsOn": [
303
- "[concat('Microsoft.Compute/virtualMachines/',variables('vmName'))]"
304
- ],
305
- "properties": {
306
- "publisher": "Microsoft.Compute",
307
- "type": "CustomScriptExtension",
308
- "typeHandlerVersion": "1.4",
309
- "settings": {
310
- "commandToExecute": "#{command}"
311
- }
312
- }
313
- }
314
- EOH
315
- end
316
-
317
- def virtual_machine_deployment_template
318
- <<-EOH
319
- {
320
- "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
321
- "contentVersion": "1.0.0.0",
322
- "parameters": {
323
- "location": {
324
- "type": "string",
325
- "metadata": {
326
- "description": "The location where the resources will be created."
327
- }
328
- },
329
- "vmSize": {
330
- "type": "string",
331
- "metadata": {
332
- "description": "The size of the VM to be created"
333
- }
334
- },
335
- "newStorageAccountName": {
336
- "type": "string",
337
- "metadata": {
338
- "description": "Unique DNS Name for the Storage Account where the Virtual Machine's disks will be placed."
339
- }
340
- },
341
- "adminUsername": {
342
- "type": "string",
343
- "metadata": {
344
- "description": "User name for the Virtual Machine."
345
- }
346
- },
347
- "adminPassword": {
348
- "type": "securestring",
349
- "metadata": {
350
- "description": "Password for the Virtual Machine."
351
- }
352
- },
353
- "dnsNameForPublicIP": {
354
- "type": "string",
355
- "metadata": {
356
- "description": "Unique DNS Name for the Public IP used to access the Virtual Machine."
357
- }
358
- },
359
- "imagePublisher": {
360
- "type": "string",
361
- "defaultValue": "Canonical",
362
- "metadata": {
363
- "description": "Publisher for the VM, e.g. Canonical, MicrosoftWindowsServer"
364
- }
365
- },
366
- "imageOffer": {
367
- "type": "string",
368
- "defaultValue": "UbuntuServer",
369
- "metadata": {
370
- "description": "Offer for the VM, e.g. UbuntuServer, WindowsServer."
371
- }
372
- },
373
- "imageSku": {
374
- "type": "string",
375
- "defaultValue": "14.04.3-LTS",
376
- "metadata": {
377
- "description": "Sku for the VM, e.g. 14.04.3-LTS"
378
- }
379
- },
380
- "imageVersion": {
381
- "type": "string",
382
- "defaultValue": "latest",
383
- "metadata": {
384
- "description": "Either a date or latest."
385
- }
386
- },
387
- "vmName": {
388
- "type": "string",
389
- "defaultValue": "vm",
390
- "metadata": {
391
- "description": "The vm name created inside of the resource group."
392
- }
393
- },
394
- "storageAccountType": {
395
- "type": "string",
396
- "defaultValue": "Standard_LRS",
397
- "metadata": {
398
- "description": "The type of storage to use (e.g. Standard_LRS or Premium_LRS)."
399
- }
400
- },
401
- "bootDiagnosticsEnabled": {
402
- "type": "string",
403
- "defaultValue": "true",
404
- "metadata": {
405
- "description": "Whether to enable (true) or disable (false) boot diagnostics. Default: true (requires Standard storage)."
406
- }
407
- }
408
- },
409
- "variables": {
410
- "location": "[parameters('location')]",
411
- "OSDiskName": "osdisk",
412
- "nicName": "nic",
413
- "addressPrefix": "10.0.0.0/16",
414
- "subnetName": "Subnet",
415
- "subnetPrefix": "10.0.0.0/24",
416
- "storageAccountType": "[parameters('storageAccountType')]",
417
- "publicIPAddressName": "publicip",
418
- "publicIPAddressType": "Dynamic",
419
- "vmStorageAccountContainerName": "vhds",
420
- "vmName": "[parameters('vmName')]",
421
- "vmSize": "[parameters('vmSize')]",
422
- "virtualNetworkName": "vnet",
423
- "vnetID": "[resourceId('Microsoft.Network/virtualNetworks',variables('virtualNetworkName'))]",
424
- "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]"
425
- },
426
- "resources": [
427
- {
428
- "type": "Microsoft.Storage/storageAccounts",
429
- "name": "[parameters('newStorageAccountName')]",
430
- "apiVersion": "2015-05-01-preview",
431
- "location": "[variables('location')]",
432
- "properties": {
433
- "accountType": "[variables('storageAccountType')]"
434
- }
435
- },
436
- {
437
- "apiVersion": "2015-05-01-preview",
438
- "type": "Microsoft.Network/publicIPAddresses",
439
- "name": "[variables('publicIPAddressName')]",
440
- "location": "[variables('location')]",
441
- "properties": {
442
- "publicIPAllocationMethod": "[variables('publicIPAddressType')]",
443
- "dnsSettings": {
444
- "domainNameLabel": "[parameters('dnsNameForPublicIP')]"
445
- }
446
- }
447
- },
448
- {
449
- "apiVersion": "2015-05-01-preview",
450
- "type": "Microsoft.Network/virtualNetworks",
451
- "name": "[variables('virtualNetworkName')]",
452
- "location": "[variables('location')]",
453
- "properties": {
454
- "addressSpace": {
455
- "addressPrefixes": [
456
- "[variables('addressPrefix')]"
457
- ]
458
- },
459
- "subnets": [
460
- {
461
- "name": "[variables('subnetName')]",
462
- "properties": {
463
- "addressPrefix": "[variables('subnetPrefix')]"
464
- }
465
- }
466
- ]
467
- }
468
- },
469
- {
470
- "apiVersion": "2015-05-01-preview",
471
- "type": "Microsoft.Network/networkInterfaces",
472
- "name": "[variables('nicName')]",
473
- "location": "[variables('location')]",
474
- "dependsOn": [
475
- "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]",
476
- "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]"
477
- ],
478
- "properties": {
479
- "ipConfigurations": [
480
- {
481
- "name": "ipconfig1",
482
- "properties": {
483
- "privateIPAllocationMethod": "Dynamic",
484
- "publicIPAddress": {
485
- "id": "[resourceId('Microsoft.Network/publicIPAddresses',variables('publicIPAddressName'))]"
486
- },
487
- "subnet": {
488
- "id": "[variables('subnetRef')]"
489
- }
490
- }
491
- }
492
- ]
493
- }
494
- },
495
- {
496
- "apiVersion": "2015-06-15",
497
- "type": "Microsoft.Compute/virtualMachines",
498
- "name": "[variables('vmName')]",
499
- "location": "[variables('location')]",
500
- "dependsOn": [
501
- "[concat('Microsoft.Storage/storageAccounts/', parameters('newStorageAccountName'))]",
502
- "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
503
- ],
504
- "properties": {
505
- "hardwareProfile": {
506
- "vmSize": "[variables('vmSize')]"
507
- },
508
- "osProfile": {
509
- "computername": "[variables('vmName')]",
510
- "adminUsername": "[parameters('adminUsername')]",
511
- "adminPassword": "[parameters('adminPassword')]"
512
- },
513
- "storageProfile": {
514
- "imageReference": {
515
- "publisher": "[parameters('imagePublisher')]",
516
- "offer": "[parameters('imageOffer')]",
517
- "sku": "[parameters('imageSku')]",
518
- "version": "[parameters('imageVersion')]"
519
- },
520
- "osDisk": {
521
- "name": "osdisk",
522
- "vhd": {
523
- "uri": "[concat('http://',parameters('newStorageAccountName'),'.blob.core.windows.net/',variables('vmStorageAccountContainerName'),'/',variables('OSDiskName'),'.vhd')]"
524
- },
525
- "caching": "ReadWrite",
526
- "createOption": "FromImage"
527
- }
528
- },
529
- "networkProfile": {
530
- "networkInterfaces": [
531
- {
532
- "id": "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]"
533
- }
534
- ]
535
- },
536
- "diagnosticsProfile": {
537
- "bootDiagnostics": {
538
- "enabled": "[parameters('bootDiagnosticsEnabled')]",
539
- "storageUri": "[concat('http://',parameters('newStorageAccountName'),'.blob.core.windows.net')]"
540
- }
541
- }
542
- }
543
- }
544
- ]
545
- }
546
- EOH
547
- end
548
- end
549
- end
550
- end
1
+ require 'kitchen'
2
+ require 'kitchen/driver/credentials'
3
+ require 'securerandom'
4
+ require 'azure_mgmt_resources'
5
+ require 'azure_mgmt_network'
6
+ require 'base64'
7
+ require 'sshkey'
8
+ require 'fileutils'
9
+
10
+ module Kitchen
11
+ module Driver
12
+ #
13
+ # Azurerm
14
+ #
15
+ class Azurerm < Kitchen::Driver::Base
16
+ attr_accessor :resource_management_client
17
+
18
+ default_config(:azure_resource_group_name) do |config|
19
+ "kitchen-#{config.instance.name}"
20
+ end
21
+
22
+ default_config(:image_urn) do |_config|
23
+ 'Canonical:UbuntuServer:14.04.3-LTS:latest'
24
+ end
25
+
26
+ default_config(:username) do |_config|
27
+ 'azure'
28
+ end
29
+
30
+ default_config(:password) do |_config|
31
+ 'P2ssw0rd'
32
+ end
33
+
34
+ default_config(:vm_name) do |_config|
35
+ 'vm'
36
+ end
37
+
38
+ default_config(:storage_account_type) do |_config|
39
+ 'Standard_LRS'
40
+ end
41
+
42
+ default_config(:boot_diagnostics_enabled) do |_config|
43
+ 'true'
44
+ end
45
+
46
+ default_config(:winrm_powershell_script) do |_config|
47
+ false
48
+ end
49
+
50
+ default_config(:azure_management_url) do |_config|
51
+ 'https://management.azure.com'
52
+ end
53
+
54
+ def create(state)
55
+ state = validate_state(state)
56
+
57
+ image_publisher, image_offer, image_sku, image_version = config[:image_urn].split(':', 4)
58
+ deployment_parameters = {
59
+ location: config[:location],
60
+ vmSize: config[:machine_size],
61
+ storageAccountType: config[:storage_account_type],
62
+ bootDiagnosticsEnabled: config[:boot_diagnostics_enabled],
63
+ newStorageAccountName: "storage#{state[:uuid]}",
64
+ adminUsername: state[:username],
65
+ adminPassword: state[:password],
66
+ dnsNameForPublicIP: "kitchen-#{state[:uuid]}",
67
+ imagePublisher: image_publisher,
68
+ imageOffer: image_offer,
69
+ imageSku: image_sku,
70
+ imageVersion: image_version,
71
+ vmName: state[:vm_name]
72
+ }
73
+
74
+ credentials = Kitchen::Driver::Credentials.new.azure_credentials_for_subscription(config[:subscription_id])
75
+ @resource_management_client = ::Azure::ARM::Resources::ResourceManagementClient.new(credentials)
76
+ @resource_management_client.subscription_id = config[:subscription_id]
77
+
78
+ # Create Resource Group
79
+ resource_group = ::Azure::ARM::Resources::Models::ResourceGroup.new
80
+ resource_group.location = config[:location]
81
+ begin
82
+ info "Creating Resource Group: #{state[:azure_resource_group_name]}"
83
+ resource_management_client.resource_groups.create_or_update(state[:azure_resource_group_name], resource_group).value!
84
+ rescue ::MsRestAzure::AzureOperationError => operation_error
85
+ error_message = if operation_error.body.nil? == true
86
+ operation_error.inspect
87
+ else
88
+ operation_error.body['error']
89
+ end
90
+ info error_message
91
+ raise error_message
92
+ end
93
+
94
+ # Execute deployment steps
95
+ begin
96
+ deployment_name = "deploy-#{state[:uuid]}"
97
+ info "Creating Deployment: #{deployment_name}"
98
+ resource_management_client.deployments.create_or_update(state[:azure_resource_group_name], deployment_name, deployment(deployment_parameters)).value!
99
+ rescue ::MsRestAzure::AzureOperationError => operation_error
100
+ rest_error = operation_error.body['error']
101
+ deployment_active = rest_error['code'] == 'DeploymentActive'
102
+ if deployment_active
103
+ info "Deployment for resource group #{state[:azure_resource_group_name]} is ongoing."
104
+ info "If you need to change the deployment template you'll need to rerun `kitchen create` for this instance."
105
+ else
106
+ info rest_error
107
+ raise operation_error
108
+ end
109
+ end
110
+
111
+ # Monitor all operations until completion
112
+ follow_deployment_until_end_state(state[:azure_resource_group_name], deployment_name)
113
+
114
+ # Now retrieve the public IP from the resource group:
115
+ network_management_client = ::Azure::ARM::Network::NetworkResourceProviderClient.new(credentials)
116
+ network_management_client.subscription_id = config[:subscription_id]
117
+ result = network_management_client.public_ip_addresses.get(state[:azure_resource_group_name], 'publicip').value!
118
+ info "IP Address is: #{result.body.properties.ip_address} [#{result.body.properties.dns_settings.fqdn}]"
119
+ state[:hostname] = result.body.properties.ip_address
120
+ end
121
+
122
+ def existing_state_value?(state, property)
123
+ state.key?(property) && !state[property].nil?
124
+ end
125
+
126
+ def validate_state(state = {})
127
+ state[:uuid] = SecureRandom.hex(8) unless existing_state_value?(state, :uuid)
128
+ state[:server_id] = "vm#{state[:uuid]}" unless existing_state_value?(state, :server_id)
129
+ state[:azure_resource_group_name] = azure_resource_group_name unless existing_state_value?(state, :azure_resource_group_name)
130
+ [:subscription_id, :username, :password, :vm_name, :azure_management_url].each do |config_element|
131
+ state[config_element] = config[config_element] unless existing_state_value?(state, config_element)
132
+ end
133
+
134
+ state
135
+ end
136
+
137
+ def azure_resource_group_name
138
+ formatted_time = Time.now.utc.strftime '%Y%m%dT%H%M%S'
139
+ "#{config[:azure_resource_group_name]}-#{formatted_time}"
140
+ end
141
+
142
+ def template_for_transport_name
143
+ template = JSON.parse(virtual_machine_deployment_template)
144
+ if instance.transport.name.casecmp('winrm') == 0
145
+ encoded_command = Base64.strict_encode64(enable_winrm_powershell_script)
146
+ command = command_to_execute
147
+ template['resources'].select { |h| h['type'] == 'Microsoft.Compute/virtualMachines' }.each do |resource|
148
+ resource['properties']['osProfile']['customData'] = encoded_command
149
+ end
150
+ template['resources'] << JSON.parse(custom_script_extension_template(command))
151
+ end
152
+
153
+ unless instance.transport[:ssh_key].nil?
154
+ info "Adding public key from #{File.expand_path(instance.transport[:ssh_key])}.pub to the deployment."
155
+ public_key = public_key_for_deployment(File.expand_path(instance.transport[:ssh_key]))
156
+ template['resources'].select { |h| h['type'] == 'Microsoft.Compute/virtualMachines' }.each do |resource|
157
+ resource['properties']['osProfile']['linuxConfiguration'] = JSON.parse(custom_linux_configuration(public_key))
158
+ end
159
+ end
160
+ template.to_json
161
+ end
162
+
163
+ def public_key_for_deployment(private_key_filename)
164
+ if File.file?(private_key_filename) == false
165
+ k = SSHKey.generate
166
+
167
+ ::FileUtils.mkdir_p(File.dirname(private_key_filename))
168
+
169
+ private_key_file = File.new(private_key_filename, 'w')
170
+ private_key_file.syswrite(k.private_key)
171
+ private_key_file.chmod(0600)
172
+ private_key_file.close
173
+
174
+ public_key_file = File.new("#{private_key_filename}.pub", 'w')
175
+ public_key_file.syswrite(k.ssh_public_key)
176
+ public_key_file.chmod(0600)
177
+ public_key_file.close
178
+
179
+ output = k.ssh_public_key
180
+ else
181
+ output = File.read("#{private_key_filename}.pub")
182
+ end
183
+ output.strip
184
+ end
185
+
186
+ def deployment(parameters)
187
+ template = template_for_transport_name
188
+ deployment = ::Azure::ARM::Resources::Models::Deployment.new
189
+ deployment.properties = ::Azure::ARM::Resources::Models::DeploymentProperties.new
190
+ deployment.properties.mode = Azure::ARM::Resources::Models::DeploymentMode::Incremental
191
+ deployment.properties.template = JSON.parse(template)
192
+ deployment.properties.parameters = parameters_in_values_format(parameters)
193
+ debug(deployment.properties.template)
194
+ deployment
195
+ end
196
+
197
+ def parameters_in_values_format(parameters_in)
198
+ parameters = parameters_in.map do |key, value|
199
+ { key.to_sym => { 'value' => value } }
200
+ end
201
+ parameters.reduce(:merge!)
202
+ end
203
+
204
+ def follow_deployment_until_end_state(resource_group, deployment_name)
205
+ end_provisioning_states = 'Canceled,Failed,Deleted,Succeeded'
206
+ end_provisioning_state_reached = false
207
+ until end_provisioning_state_reached
208
+ list_outstanding_deployment_operations(resource_group, deployment_name)
209
+ sleep 10
210
+ deployment_provisioning_state = deployment_state(resource_group, deployment_name)
211
+ end_provisioning_state_reached = end_provisioning_states.split(',').include?(deployment_provisioning_state)
212
+ end
213
+ info "Resource Template deployment reached end state of '#{deployment_provisioning_state}'."
214
+ show_failed_operations(resource_group, deployment_name) if deployment_provisioning_state == 'Failed'
215
+ end
216
+
217
+ def show_failed_operations(resource_group, deployment_name)
218
+ failed_operations = resource_management_client.deployment_operations.list(resource_group, deployment_name).value!
219
+ failed_operations.body.value.each do |val|
220
+ resource_code = val.properties.status_code
221
+ raise val.properties.status_message.inspect.to_s if resource_code != 'OK'
222
+ end
223
+ end
224
+
225
+ def list_outstanding_deployment_operations(resource_group, deployment_name)
226
+ end_operation_states = 'Failed,Succeeded'
227
+ deployment_operations = resource_management_client.deployment_operations.list(resource_group, deployment_name).value!
228
+ deployment_operations.body.value.each do |val|
229
+ resource_provisioning_state = val.properties.provisioning_state
230
+ resource_name = val.properties.target_resource.resource_name
231
+ resource_type = val.properties.target_resource.resource_type
232
+ end_operation_state_reached = end_operation_states.split(',').include?(resource_provisioning_state)
233
+ unless end_operation_state_reached
234
+ info "Resource #{resource_type} '#{resource_name}' provisioning status is #{resource_provisioning_state}"
235
+ end
236
+ end
237
+ end
238
+
239
+ def deployment_state(resource_group, deployment_name)
240
+ deployments = resource_management_client.deployments.get(resource_group, deployment_name).value!
241
+ deployments.body.properties.provisioning_state
242
+ end
243
+
244
+ def destroy(state)
245
+ return if state[:server_id].nil?
246
+ credentials = Kitchen::Driver::Credentials.new.azure_credentials_for_subscription(state[:subscription_id])
247
+ resource_management_client = ::Azure::ARM::Resources::ResourceManagementClient.new(credentials, state[:azure_management_url])
248
+ resource_management_client.subscription_id = state[:subscription_id]
249
+ begin
250
+ info "Destroying Resource Group: #{state[:azure_resource_group_name]}"
251
+ resource_management_client.resource_groups.begin_delete(state[:azure_resource_group_name]).value!
252
+ info 'Destroy operation accepted and will continue in the background.'
253
+ rescue ::MsRestAzure::AzureOperationError => operation_error
254
+ info operation_error.body['error']
255
+ raise operation_error
256
+ end
257
+ state.delete(:server_id)
258
+ state.delete(:hostname)
259
+ state.delete(:username)
260
+ state.delete(:password)
261
+ end
262
+
263
+ def enable_winrm_powershell_script
264
+ config[:winrm_powershell_script] || <<-PS1
265
+ $cert = New-SelfSignedCertificate -DnsName $env:COMPUTERNAME -CertStoreLocation Cert:\\LocalMachine\\My
266
+ $config = '@{CertificateThumbprint="' + $cert.Thumbprint + '"}'
267
+ winrm create winrm/config/listener?Address=*+Transport=HTTPS $config
268
+ winrm set winrm/config/service/auth '@{Basic="true";Kerberos="false";Negotiate="true";Certificate="false";CredSSP="true"}'
269
+ New-NetFirewallRule -DisplayName "Windows Remote Management (HTTPS-In)" -Name "Windows Remote Management (HTTPS-In)" -Profile Any -LocalPort 5986 -Protocol TCP
270
+ winrm set winrm/config/service '@{AllowUnencrypted="true"}'
271
+ New-NetFirewallRule -DisplayName "Windows Remote Management (HTTP-In)" -Name "Windows Remote Management (HTTP-In)" -Profile Any -LocalPort 5985 -Protocol TCP
272
+ PS1
273
+ end
274
+
275
+ def command_to_execute
276
+ 'copy /y c:\\\\azuredata\\\\customdata.bin c:\\\\azuredata\\\\customdata.ps1 && powershell.exe -ExecutionPolicy Unrestricted -Command \\"start-process powershell.exe -verb runas -argumentlist c:\\\\azuredata\\\\customdata.ps1\\"'
277
+ end
278
+
279
+ def custom_linux_configuration(public_key)
280
+ <<-EOH
281
+ {
282
+ "disablePasswordAuthentication": "true",
283
+ "ssh": {
284
+ "publicKeys": [
285
+ {
286
+ "path": "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]",
287
+ "keyData": "#{public_key}"
288
+ }
289
+ ]
290
+ }
291
+ }
292
+ EOH
293
+ end
294
+
295
+ def custom_script_extension_template(command)
296
+ <<-EOH
297
+ {
298
+ "type": "Microsoft.Compute/virtualMachines/extensions",
299
+ "name": "[concat(variables('vmName'),'/','enableWinRM')]",
300
+ "apiVersion": "2015-05-01-preview",
301
+ "location": "[variables('location')]",
302
+ "dependsOn": [
303
+ "[concat('Microsoft.Compute/virtualMachines/',variables('vmName'))]"
304
+ ],
305
+ "properties": {
306
+ "publisher": "Microsoft.Compute",
307
+ "type": "CustomScriptExtension",
308
+ "typeHandlerVersion": "1.4",
309
+ "settings": {
310
+ "commandToExecute": "#{command}"
311
+ }
312
+ }
313
+ }
314
+ EOH
315
+ end
316
+
317
+ def virtual_machine_deployment_template
318
+ <<-EOH
319
+ {
320
+ "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
321
+ "contentVersion": "1.0.0.0",
322
+ "parameters": {
323
+ "location": {
324
+ "type": "string",
325
+ "metadata": {
326
+ "description": "The location where the resources will be created."
327
+ }
328
+ },
329
+ "vmSize": {
330
+ "type": "string",
331
+ "metadata": {
332
+ "description": "The size of the VM to be created"
333
+ }
334
+ },
335
+ "newStorageAccountName": {
336
+ "type": "string",
337
+ "metadata": {
338
+ "description": "Unique DNS Name for the Storage Account where the Virtual Machine's disks will be placed."
339
+ }
340
+ },
341
+ "adminUsername": {
342
+ "type": "string",
343
+ "metadata": {
344
+ "description": "User name for the Virtual Machine."
345
+ }
346
+ },
347
+ "adminPassword": {
348
+ "type": "securestring",
349
+ "metadata": {
350
+ "description": "Password for the Virtual Machine."
351
+ }
352
+ },
353
+ "dnsNameForPublicIP": {
354
+ "type": "string",
355
+ "metadata": {
356
+ "description": "Unique DNS Name for the Public IP used to access the Virtual Machine."
357
+ }
358
+ },
359
+ "imagePublisher": {
360
+ "type": "string",
361
+ "defaultValue": "Canonical",
362
+ "metadata": {
363
+ "description": "Publisher for the VM, e.g. Canonical, MicrosoftWindowsServer"
364
+ }
365
+ },
366
+ "imageOffer": {
367
+ "type": "string",
368
+ "defaultValue": "UbuntuServer",
369
+ "metadata": {
370
+ "description": "Offer for the VM, e.g. UbuntuServer, WindowsServer."
371
+ }
372
+ },
373
+ "imageSku": {
374
+ "type": "string",
375
+ "defaultValue": "14.04.3-LTS",
376
+ "metadata": {
377
+ "description": "Sku for the VM, e.g. 14.04.3-LTS"
378
+ }
379
+ },
380
+ "imageVersion": {
381
+ "type": "string",
382
+ "defaultValue": "latest",
383
+ "metadata": {
384
+ "description": "Either a date or latest."
385
+ }
386
+ },
387
+ "vmName": {
388
+ "type": "string",
389
+ "defaultValue": "vm",
390
+ "metadata": {
391
+ "description": "The vm name created inside of the resource group."
392
+ }
393
+ },
394
+ "storageAccountType": {
395
+ "type": "string",
396
+ "defaultValue": "Standard_LRS",
397
+ "metadata": {
398
+ "description": "The type of storage to use (e.g. Standard_LRS or Premium_LRS)."
399
+ }
400
+ },
401
+ "bootDiagnosticsEnabled": {
402
+ "type": "string",
403
+ "defaultValue": "true",
404
+ "metadata": {
405
+ "description": "Whether to enable (true) or disable (false) boot diagnostics. Default: true (requires Standard storage)."
406
+ }
407
+ }
408
+ },
409
+ "variables": {
410
+ "location": "[parameters('location')]",
411
+ "OSDiskName": "osdisk",
412
+ "nicName": "nic",
413
+ "addressPrefix": "10.0.0.0/16",
414
+ "subnetName": "Subnet",
415
+ "subnetPrefix": "10.0.0.0/24",
416
+ "storageAccountType": "[parameters('storageAccountType')]",
417
+ "publicIPAddressName": "publicip",
418
+ "publicIPAddressType": "Dynamic",
419
+ "vmStorageAccountContainerName": "vhds",
420
+ "vmName": "[parameters('vmName')]",
421
+ "vmSize": "[parameters('vmSize')]",
422
+ "virtualNetworkName": "vnet",
423
+ "vnetID": "[resourceId('Microsoft.Network/virtualNetworks',variables('virtualNetworkName'))]",
424
+ "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]"
425
+ },
426
+ "resources": [
427
+ {
428
+ "type": "Microsoft.Storage/storageAccounts",
429
+ "name": "[parameters('newStorageAccountName')]",
430
+ "apiVersion": "2015-05-01-preview",
431
+ "location": "[variables('location')]",
432
+ "properties": {
433
+ "accountType": "[variables('storageAccountType')]"
434
+ }
435
+ },
436
+ {
437
+ "apiVersion": "2015-05-01-preview",
438
+ "type": "Microsoft.Network/publicIPAddresses",
439
+ "name": "[variables('publicIPAddressName')]",
440
+ "location": "[variables('location')]",
441
+ "properties": {
442
+ "publicIPAllocationMethod": "[variables('publicIPAddressType')]",
443
+ "dnsSettings": {
444
+ "domainNameLabel": "[parameters('dnsNameForPublicIP')]"
445
+ }
446
+ }
447
+ },
448
+ {
449
+ "apiVersion": "2015-05-01-preview",
450
+ "type": "Microsoft.Network/virtualNetworks",
451
+ "name": "[variables('virtualNetworkName')]",
452
+ "location": "[variables('location')]",
453
+ "properties": {
454
+ "addressSpace": {
455
+ "addressPrefixes": [
456
+ "[variables('addressPrefix')]"
457
+ ]
458
+ },
459
+ "subnets": [
460
+ {
461
+ "name": "[variables('subnetName')]",
462
+ "properties": {
463
+ "addressPrefix": "[variables('subnetPrefix')]"
464
+ }
465
+ }
466
+ ]
467
+ }
468
+ },
469
+ {
470
+ "apiVersion": "2015-05-01-preview",
471
+ "type": "Microsoft.Network/networkInterfaces",
472
+ "name": "[variables('nicName')]",
473
+ "location": "[variables('location')]",
474
+ "dependsOn": [
475
+ "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]",
476
+ "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]"
477
+ ],
478
+ "properties": {
479
+ "ipConfigurations": [
480
+ {
481
+ "name": "ipconfig1",
482
+ "properties": {
483
+ "privateIPAllocationMethod": "Dynamic",
484
+ "publicIPAddress": {
485
+ "id": "[resourceId('Microsoft.Network/publicIPAddresses',variables('publicIPAddressName'))]"
486
+ },
487
+ "subnet": {
488
+ "id": "[variables('subnetRef')]"
489
+ }
490
+ }
491
+ }
492
+ ]
493
+ }
494
+ },
495
+ {
496
+ "apiVersion": "2015-06-15",
497
+ "type": "Microsoft.Compute/virtualMachines",
498
+ "name": "[variables('vmName')]",
499
+ "location": "[variables('location')]",
500
+ "dependsOn": [
501
+ "[concat('Microsoft.Storage/storageAccounts/', parameters('newStorageAccountName'))]",
502
+ "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
503
+ ],
504
+ "properties": {
505
+ "hardwareProfile": {
506
+ "vmSize": "[variables('vmSize')]"
507
+ },
508
+ "osProfile": {
509
+ "computername": "[variables('vmName')]",
510
+ "adminUsername": "[parameters('adminUsername')]",
511
+ "adminPassword": "[parameters('adminPassword')]"
512
+ },
513
+ "storageProfile": {
514
+ "imageReference": {
515
+ "publisher": "[parameters('imagePublisher')]",
516
+ "offer": "[parameters('imageOffer')]",
517
+ "sku": "[parameters('imageSku')]",
518
+ "version": "[parameters('imageVersion')]"
519
+ },
520
+ "osDisk": {
521
+ "name": "osdisk",
522
+ "vhd": {
523
+ "uri": "[concat('http://',parameters('newStorageAccountName'),'.blob.core.windows.net/',variables('vmStorageAccountContainerName'),'/',variables('OSDiskName'),'.vhd')]"
524
+ },
525
+ "caching": "ReadWrite",
526
+ "createOption": "FromImage"
527
+ }
528
+ },
529
+ "networkProfile": {
530
+ "networkInterfaces": [
531
+ {
532
+ "id": "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]"
533
+ }
534
+ ]
535
+ },
536
+ "diagnosticsProfile": {
537
+ "bootDiagnostics": {
538
+ "enabled": "[parameters('bootDiagnosticsEnabled')]",
539
+ "storageUri": "[concat('http://',parameters('newStorageAccountName'),'.blob.core.windows.net')]"
540
+ }
541
+ }
542
+ }
543
+ }
544
+ ]
545
+ }
546
+ EOH
547
+ end
548
+ end
549
+ end
550
+ end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kitchen-azurerm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.5
4
+ version: 0.3.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stuart Preston
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-03-21 00:00:00.000000000 Z
11
+ date: 2016-05-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: inifile
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '2.0'
19
+ version: 3.0.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '2.0'
26
+ version: 3.0.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: azure_mgmt_resources
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -182,7 +182,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
182
182
  version: '0'
183
183
  requirements: []
184
184
  rubyforge_project:
185
- rubygems_version: 2.5.2
185
+ rubygems_version: 2.6.3
186
186
  signing_key:
187
187
  specification_version: 4
188
188
  summary: Test Kitchen driver for Azure Resource Manager.