knife-azure 3.0.6 → 5.0.1

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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/lib/azure/azure_interface.rb +1 -1
  3. data/lib/azure/custom_errors.rb +2 -2
  4. data/lib/azure/helpers.rb +1 -1
  5. data/lib/azure/resource_management/ARM_deployment_template.rb +82 -27
  6. data/lib/azure/resource_management/ARM_interface.rb +112 -118
  7. data/lib/azure/resource_management/rest/arm_client.rb +208 -0
  8. data/lib/azure/resource_management/rest/environments.rb +89 -0
  9. data/lib/azure/resource_management/rest/errors.rb +57 -0
  10. data/lib/azure/resource_management/rest/http.rb +335 -0
  11. data/lib/azure/resource_management/rest/rest_object.rb +103 -0
  12. data/lib/azure/resource_management/rest/secure_connection.rb +57 -0
  13. data/lib/azure/resource_management/rest/token_provider.rb +125 -0
  14. data/lib/azure/resource_management/vnet_config.rb +4 -4
  15. data/lib/azure/resource_management/windows_credentials.rb +3 -3
  16. data/lib/chef/knife/azurerm_server_create.rb +22 -10
  17. data/lib/chef/knife/azurerm_server_delete.rb +1 -1
  18. data/lib/chef/knife/azurerm_server_list.rb +1 -1
  19. data/lib/chef/knife/azurerm_server_show.rb +1 -1
  20. data/lib/chef/knife/bootstrap/bootstrapper.rb +142 -14
  21. data/lib/chef/knife/bootstrap/common_bootstrap_options.rb +4 -6
  22. data/lib/chef/knife/bootstrap_azurerm.rb +1 -1
  23. data/lib/chef/knife/helpers/azurerm_base.rb +170 -9
  24. data/lib/knife-azure/version.rb +2 -2
  25. metadata +37 -95
  26. data/lib/azure/service_management/ASM_interface.rb +0 -310
  27. data/lib/azure/service_management/ag.rb +0 -99
  28. data/lib/azure/service_management/certificate.rb +0 -235
  29. data/lib/azure/service_management/connection.rb +0 -102
  30. data/lib/azure/service_management/deploy.rb +0 -221
  31. data/lib/azure/service_management/disk.rb +0 -68
  32. data/lib/azure/service_management/host.rb +0 -184
  33. data/lib/azure/service_management/image.rb +0 -94
  34. data/lib/azure/service_management/loadbalancer.rb +0 -78
  35. data/lib/azure/service_management/rest.rb +0 -126
  36. data/lib/azure/service_management/role.rb +0 -717
  37. data/lib/azure/service_management/storageaccount.rb +0 -127
  38. data/lib/azure/service_management/utility.rb +0 -40
  39. data/lib/azure/service_management/vnet.rb +0 -134
  40. data/lib/chef/knife/azure_ag_create.rb +0 -73
  41. data/lib/chef/knife/azure_ag_list.rb +0 -35
  42. data/lib/chef/knife/azure_image_list.rb +0 -56
  43. data/lib/chef/knife/azure_internal-lb_create.rb +0 -74
  44. data/lib/chef/knife/azure_internal-lb_list.rb +0 -35
  45. data/lib/chef/knife/azure_server_create.rb +0 -531
  46. data/lib/chef/knife/azure_server_delete.rb +0 -136
  47. data/lib/chef/knife/azure_server_list.rb +0 -38
  48. data/lib/chef/knife/azure_server_show.rb +0 -41
  49. data/lib/chef/knife/azure_vnet_create.rb +0 -74
  50. data/lib/chef/knife/azure_vnet_list.rb +0 -35
  51. data/lib/chef/knife/bootstrap_azure.rb +0 -191
  52. data/lib/chef/knife/helpers/azure_base.rb +0 -392
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright:: Copyright (c) Chef Software Inc.
2
+ # Copyright:: Copyright (c) 2012-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
3
3
  # License:: Apache License, Version 2.0
4
4
  #
5
5
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,10 +18,11 @@
18
18
  require_relative "../azure_interface"
19
19
  require_relative "ARM_deployment_template"
20
20
  require_relative "vnet_config"
21
- require "azure_mgmt_resources"
22
- require "azure_mgmt_compute"
23
- require "azure_mgmt_storage"
24
- require "azure_mgmt_network"
21
+ require_relative "rest/environments"
22
+ require_relative "rest/errors"
23
+ require_relative "rest/token_provider"
24
+ require_relative "rest/http"
25
+ require_relative "rest/arm_client"
25
26
 
26
27
  module Azure
27
28
  class ResourceManagement
@@ -29,60 +30,23 @@ module Azure
29
30
  include Azure::ARM::ARMDeploymentTemplate
30
31
  include Azure::ARM::VnetConfig
31
32
 
32
- include Azure::Resources::Mgmt::V2018_05_01
33
- include Azure::Resources::Mgmt::V2018_05_01::Models
34
-
35
- include Azure::Compute::Mgmt::V2018_06_01
36
- include Azure::Compute::Mgmt::V2018_06_01::Models
37
-
38
- include Azure::Storage::Mgmt::V2018_07_01
39
- include Azure::Storage::Mgmt::V2018_07_01::Models
40
-
41
- include Azure::Network::Mgmt::V2018_08_01
42
- include Azure::Network::Mgmt::V2018_08_01::Models
33
+ # Backwards-compatible alias so existing rescue clauses and consumers can
34
+ # keep referring to a single error type for Azure API failures.
35
+ OperationError = Azure::ResourceManagement::Rest::OperationError
43
36
 
44
37
  attr_accessor :connection
45
38
 
46
39
  def initialize(params = {})
47
- token_provider = if params[:azure_client_secret]
48
- MsRestAzure::ApplicationTokenProvider.new(params[:azure_tenant_id], params[:azure_client_id], params[:azure_client_secret])
49
- else
50
- MsRest::StringTokenProvider.new(params[:token], params[:tokentype])
51
- end
52
- @credentials = MsRest::TokenCredentials.new(token_provider)
40
+ @environment = Azure::ResourceManagement::Rest::Environments.from_name(params[:azure_environment])
41
+ @token_provider = Azure::ResourceManagement::Rest::TokenProvider.new(params, @environment)
53
42
  @azure_subscription_id = params[:azure_subscription_id]
54
43
  super
55
44
  end
56
45
 
57
- def resource_management_client
58
- @resource_management_client ||= begin
59
- resource_management_client = ResourceManagementClient.new(@credentials)
60
- resource_management_client.subscription_id = @azure_subscription_id
61
- resource_management_client
62
- end
63
- end
64
-
65
- def compute_management_client
66
- @compute_management_client ||= begin
67
- compute_management_client = ComputeManagementClient.new(@credentials)
68
- compute_management_client.subscription_id = @azure_subscription_id
69
- compute_management_client
70
- end
71
- end
72
-
73
- def storage_management_client
74
- @storage_management_client ||= begin
75
- storage_management_client = StorageManagementClient.new(@credentials)
76
- storage_management_client.subscription_id = @azure_subscription_id
77
- storage_management_client
78
- end
79
- end
80
-
81
- def network_resource_client
82
- @network_resource_client ||= begin
83
- network_resource_client = NetworkManagementClient.new(@credentials)
84
- network_resource_client.subscription_id = @azure_subscription_id
85
- network_resource_client
46
+ def arm_client
47
+ @arm_client ||= begin
48
+ http = Azure::ResourceManagement::Rest::Http.new(@token_provider, environment: @environment)
49
+ Azure::ResourceManagement::Rest::ArmClient.new(@azure_subscription_id, http, @environment)
86
50
  end
87
51
  end
88
52
 
@@ -90,9 +54,9 @@ module Azure
90
54
 
91
55
  def list_servers(resource_group_name = nil)
92
56
  servers = if resource_group_name.nil?
93
- compute_management_client.virtual_machines.list_all
57
+ arm_client.list_all_virtual_machines
94
58
  else
95
- compute_management_client.virtual_machines.list(resource_group_name)
59
+ arm_client.list_virtual_machines(resource_group_name)
96
60
  end
97
61
 
98
62
  cols = ["VM Name", "Resource Group Name", "Location", "Provisioning State", "OS Type"]
@@ -119,7 +83,7 @@ module Azure
119
83
  end
120
84
 
121
85
  def delete_server(resource_group_name, vm_name)
122
- server = compute_management_client.virtual_machines.get(resource_group_name, vm_name)
86
+ server = arm_client.get_virtual_machine(resource_group_name, vm_name)
123
87
  if server && server.name == vm_name
124
88
  puts "\n\n"
125
89
  msg_pair(ui, "VM Name", server.name)
@@ -136,9 +100,7 @@ module Azure
136
100
 
137
101
  ui.info "Deleting .."
138
102
 
139
- begin
140
- server_detail = compute_management_client.virtual_machines.delete(resource_group_name, vm_name)
141
- end until server_detail.nil?
103
+ arm_client.delete_virtual_machine(resource_group_name, vm_name)
142
104
 
143
105
  puts "\n"
144
106
  ui.warn "Deleted server #{vm_name}"
@@ -149,13 +111,13 @@ module Azure
149
111
  server = find_server(resource_group, name)
150
112
  if server
151
113
  network_interface_name = server.network_profile.network_interfaces[0].id.split("/")[-1]
152
- network_interface_data = network_resource_client.network_interfaces.get(resource_group, network_interface_name)
114
+ network_interface_data = arm_client.get_network_interface(resource_group, network_interface_name)
153
115
  public_ip_id_data = network_interface_data.ip_configurations[0].public_ipaddress
154
116
  if public_ip_id_data.nil?
155
117
  public_ip_data = nil
156
118
  else
157
119
  public_ip_name = public_ip_id_data.id.split("/")[-1]
158
- public_ip_data = network_resource_client.public_ipaddresses.get(resource_group, public_ip_name)
120
+ public_ip_data = arm_client.get_public_ip_address(resource_group, public_ip_name)
159
121
  end
160
122
 
161
123
  details = []
@@ -205,13 +167,13 @@ module Azure
205
167
  end
206
168
 
207
169
  def find_server(resource_group, name)
208
- compute_management_client.virtual_machines.get(resource_group, name)
170
+ arm_client.get_virtual_machine(resource_group, name)
209
171
  end
210
172
 
211
173
  def virtual_machine_exist?(resource_group_name, vm_name)
212
- compute_management_client.virtual_machines.get(resource_group_name, vm_name)
174
+ arm_client.get_virtual_machine(resource_group_name, vm_name)
213
175
  true
214
- rescue MsRestAzure::AzureOperationError => e
176
+ rescue OperationError => e
215
177
  if e.body
216
178
  err_json = JSON.parse(e.response.body)
217
179
  if err_json["error"]["code"] == "ResourceNotFound"
@@ -223,9 +185,9 @@ module Azure
223
185
  end
224
186
 
225
187
  def security_group_exist?(resource_group_name, security_group_name)
226
- network_resource_client.network_security_groups.get(resource_group_name, security_group_name)
188
+ arm_client.get_network_security_group(resource_group_name, security_group_name)
227
189
  true
228
- rescue MsRestAzure::AzureOperationError => e
190
+ rescue OperationError => e
229
191
  if e.body
230
192
  err_json = JSON.parse(e.response.body)
231
193
  if err_json["error"]["code"] == "ResourceNotFound"
@@ -236,8 +198,30 @@ module Azure
236
198
  end
237
199
  end
238
200
 
201
+ # Returns the sku name (e.g. "Aligned") of an existing availability set, nil if it
202
+ # exists but is a legacy "Classic" set (which has no sku property at all), or the
203
+ # :not_found symbol if it doesn't exist yet. Used to detect the case where a caller
204
+ # reuses an existing Classic availability set name: since Azure availability set
205
+ # SKUs are immutable once created, redeploying it as "Aligned" (required for managed
206
+ # disks) would fail remotely with a cryptic ARM error instead of the actionable one
207
+ # raised in validate_params!.
208
+ def existing_availability_set_sku(resource_group_name, availability_set_name)
209
+ availability_set = arm_client.get_availability_set(resource_group_name, availability_set_name)
210
+ availability_set.sku && availability_set.sku.name
211
+ rescue OperationError => e
212
+ if e.body
213
+ err_json = JSON.parse(e.response.body)
214
+ # ResourceNotFound: the resource group exists but the availability set doesn't.
215
+ # ResourceGroupNotFound: the resource group itself doesn't exist yet (e.g. when
216
+ # creating a VM + availability set together in a brand-new resource group).
217
+ # Both mean "no existing availability set to conflict with".
218
+ return :not_found if %w{ResourceNotFound ResourceGroupNotFound}.include?(err_json["error"]["code"])
219
+ end
220
+ raise e
221
+ end
222
+
239
223
  def resource_group_exist?(resource_group_name)
240
- resource_management_client.resource_groups.check_existence(resource_group_name)
224
+ arm_client.resource_group_exist?(resource_group_name)
241
225
  end
242
226
 
243
227
  def platform(image_reference)
@@ -256,7 +240,7 @@ module Azure
256
240
  end
257
241
 
258
242
  def fetch_substatus(resource_group_name, virtual_machine_name, chef_extension_name)
259
- substatuses = compute_management_client.virtual_machine_extensions.get(
243
+ substatuses = arm_client.get_vm_extension(
260
244
  resource_group_name,
261
245
  virtual_machine_name,
262
246
  chef_extension_name,
@@ -307,7 +291,7 @@ module Azure
307
291
  ## chef-client run did not complete within maximum timeout of 30 minutes ##
308
292
  ## fetch whatever logs available under the chef-client.log file ##
309
293
  color = :yellow
310
- end
294
+ end
311
295
  puts ui.color(status, color, :bold).to_s
312
296
  puts "----> chef-client run logs: "
313
297
  puts "\n#{message}\n" ## message field of substatus contains the chef-client run logs ##
@@ -381,27 +365,12 @@ module Azure
381
365
  end
382
366
  end
383
367
 
384
- def vm_public_ip(params = {})
385
- network_resource_client.public_ipaddresses.get(
386
- params[:azure_resource_group_name],
387
- params[:azure_vm_name]
388
- ).value!.body.properties.ip_address
389
- end
390
-
391
- def vm_default_port(params = {})
392
- network_resource_client.network_security_groups.get(
393
- params[:azure_resource_group_name],
394
- params[:azure_vm_name]
395
- ).value!.body.properties.security_rules[0].properties.destination_port_range
396
- end
397
-
398
368
  def create_resource_group(params = {})
399
- resource_group = ResourceGroup.new
400
- resource_group.name = params[:azure_resource_group_name]
401
- resource_group.location = params[:azure_service_location]
402
-
403
369
  begin
404
- resource_group = resource_management_client.resource_groups.create_or_update(resource_group.name, resource_group)
370
+ resource_group = arm_client.create_resource_group(
371
+ params[:azure_resource_group_name],
372
+ params[:azure_service_location]
373
+ )
405
374
  rescue Exception => e
406
375
  Chef::Log.error("Failed to create the Resource Group -- exception being rescued: #{e}")
407
376
  common_arm_rescue_block(e)
@@ -414,33 +383,38 @@ module Azure
414
383
  template = create_deployment_template(params)
415
384
  parameters = create_deployment_parameters(params)
416
385
 
417
- deploy_prop = DeploymentProperties.new
418
- deploy_prop.template = template
419
- deploy_prop.parameters = parameters
420
- deploy_prop.mode = "Incremental"
386
+ deployment_body = {
387
+ "properties" => {
388
+ "template" => template,
389
+ "parameters" => parameters,
390
+ "mode" => "Incremental",
391
+ },
392
+ }
421
393
 
422
- deploy_params = Deployment.new
423
- deploy_params.properties = deploy_prop
424
-
425
- resource_management_client.deployments.create_or_update(params[:azure_resource_group_name], "#{params[:azure_vm_name]}_deploy", deploy_params)
394
+ arm_client.create_deployment(params[:azure_resource_group_name], "#{params[:azure_vm_name]}_deploy", deployment_body)
426
395
  end
427
396
 
428
397
  def create_vm_extension(params)
429
- vm_ext = VirtualMachineExtension.new
430
- vm_ext.name = params[:chef_extension]
431
- vm_ext.location = params[:azure_service_location]
432
- vm_ext.publisher = params[:chef_extension_publisher]
433
- vm_ext.virtual_machine_extension_type = params[:chef_extension]
434
- vm_ext.type_handler_version = params[:chef_extension_version].nil? ? get_latest_chef_extension_version(params) : params[:chef_extension_version]
435
- vm_ext.auto_upgrade_minor_version = false
436
- vm_ext.settings = params[:chef_extension_public_param]
437
- vm_ext.protected_settings = params[:chef_extension_private_param]
398
+ extension_name = params[:chef_extension]
399
+ extension_body = {
400
+ "name" => extension_name,
401
+ "location" => params[:azure_service_location],
402
+ "properties" => {
403
+ "publisher" => params[:chef_extension_publisher],
404
+ "type" => extension_name,
405
+ "typeHandlerVersion" => params[:chef_extension_version].nil? ? get_latest_chef_extension_version(params) : params[:chef_extension_version],
406
+ "autoUpgradeMinorVersion" => false,
407
+ "settings" => params[:chef_extension_public_param],
408
+ "protectedSettings" => params[:chef_extension_private_param],
409
+ },
410
+ }
411
+
438
412
  begin
439
- vm_extension = compute_management_client.virtual_machine_extensions.create_or_update(
413
+ vm_extension = arm_client.create_vm_extension(
440
414
  params[:azure_resource_group_name],
441
415
  params[:azure_vm_name],
442
- vm_ext.name,
443
- vm_ext
416
+ extension_name,
417
+ extension_body
444
418
  )
445
419
  rescue Exception => e
446
420
  Chef::Log.error("Failed to create the Virtual Machine Extension -- exception being rescued.")
@@ -453,14 +427,21 @@ module Azure
453
427
  def extension_already_installed?(server)
454
428
  if server.resources
455
429
  server.resources.each do |extension|
456
- return true if extension.virtual_machine_extension_type == "ChefClient" || extension.virtual_machine_extension_type == "LinuxChefClient"
430
+ # Azure returns the extension handler name (ChefClient/LinuxChefClient)
431
+ # as the nested properties.type field, not a top-level
432
+ # virtual_machine_extension_type attribute -- that name was an
433
+ # artifact of the retired azure_mgmt_compute SDK's generated
434
+ # model, which doesn't exist on the raw ARM JSON this REST
435
+ # client wraps.
436
+ extension_type = extension.properties && extension.properties.type
437
+ return true if extension_type == "ChefClient" || extension_type == "LinuxChefClient"
457
438
  end
458
439
  end
459
440
  false
460
441
  end
461
442
 
462
443
  def get_latest_chef_extension_version(params)
463
- ext_version = compute_management_client.virtual_machine_extension_images.list_versions(
444
+ ext_version = arm_client.list_vm_extension_versions(
464
445
  params[:azure_service_location],
465
446
  params[:chef_extension_publisher],
466
447
  params[:chef_extension]
@@ -472,26 +453,39 @@ module Azure
472
453
  def delete_resource_group(resource_group_name)
473
454
  ui.info "Resource group deletion takes some time. Please wait ..."
474
455
 
475
- begin
476
- server = resource_management_client.resource_groups.delete(resource_group_name)
477
- end until server.nil?
456
+ arm_client.delete_resource_group(resource_group_name)
478
457
  puts "\n"
479
458
  end
480
459
 
481
460
  def common_arm_rescue_block(error)
482
- if error.class == MsRestAzure::AzureOperationError && error.body
483
- err_json = JSON.parse(error.response.body)
484
- err_details = err_json["error"]["details"] if err_json["error"]
485
- if err_details
486
- err_details.each do |err|
487
- begin
461
+ if error.is_a?(OperationError) && error.body
462
+ err_json = begin
463
+ JSON.parse(error.response.body)
464
+ rescue JSON::ParserError, TypeError
465
+ nil
466
+ end
467
+ unless err_json.is_a?(Hash)
468
+ ui.error(error.message)
469
+ Chef::Log.debug(error.response.body)
470
+ return
471
+ end
472
+
473
+ arm_error = err_json["error"]
474
+ if arm_error.is_a?(Hash)
475
+ err_details = arm_error["details"]
476
+ if err_details
477
+ err_details.each do |err|
488
478
  ui.error(JSON.parse(err["message"])["error"]["message"])
489
479
  rescue JSON::ParserError => e
490
480
  ui.error(err["message"])
491
481
  end
482
+ else
483
+ ui.error(arm_error["message"])
492
484
  end
493
485
  else
494
- ui.error(err_json["error"]["message"])
486
+ # OAuth token endpoint errors are flat: the "error" value is a
487
+ # string code and the human-readable text is in "error_description".
488
+ ui.error(err_json["error_description"] || arm_error || error.message)
495
489
  end
496
490
  Chef::Log.debug(error.response.body)
497
491
  else
@@ -0,0 +1,208 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2012-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require "erb" unless defined?(ERB::Util)
19
+ require "uri" unless defined?(URI)
20
+ require_relative "environments"
21
+ require_relative "errors"
22
+ require_relative "http"
23
+ require_relative "rest_object"
24
+
25
+ module Azure
26
+ class ResourceManagement
27
+ module Rest
28
+ # One method per ARM operation used by knife-azure, replacing the four
29
+ # retired azure_mgmt_* SDK clients. Responses are wrapped in RestObject so
30
+ # callers keep using SDK-style dot notation.
31
+ class ArmClient
32
+ RESOURCES_API_VERSION = "2025-04-01".freeze
33
+ COMPUTE_API_VERSION = "2024-11-01".freeze
34
+ NETWORK_API_VERSION = "2025-07-01".freeze
35
+
36
+ def initialize(subscription_id, http, environment = Environments.default)
37
+ @subscription_id = subscription_id
38
+ @http = http
39
+ @environment = environment
40
+ end
41
+
42
+ # ----- Resource groups / deployments (Microsoft.Resources) -----
43
+
44
+ def resource_group_exist?(resource_group_name)
45
+ @http.head(rg_url(resource_group_name, RESOURCES_API_VERSION))
46
+ true
47
+ rescue OperationError => e
48
+ return false if e.http_status == 404
49
+
50
+ raise
51
+ end
52
+
53
+ def create_resource_group(resource_group_name, location)
54
+ body = { "location" => location }
55
+ response = @http.put(rg_url(resource_group_name, RESOURCES_API_VERSION), body)
56
+ RestObject.wrap(response.body)
57
+ end
58
+
59
+ def delete_resource_group(resource_group_name)
60
+ @http.request_and_poll(:delete, rg_url(resource_group_name, RESOURCES_API_VERSION))
61
+ nil
62
+ end
63
+
64
+ def create_deployment(resource_group_name, deployment_name, deployment_body)
65
+ url = deployment_url(resource_group_name, deployment_name)
66
+ @http.request_and_poll(:put, url, deployment_body)
67
+ response = @http.get(url)
68
+ RestObject.wrap(response.body)
69
+ end
70
+
71
+ # ----- Virtual machines / extensions (Microsoft.Compute) -----
72
+
73
+ def list_all_virtual_machines
74
+ url = base_url("/providers/Microsoft.Compute/virtualMachines", COMPUTE_API_VERSION)
75
+ @http.get_all(url).map { |vm| RestObject.wrap(vm) }
76
+ end
77
+
78
+ def list_virtual_machines(resource_group_name)
79
+ url = base_url(
80
+ "/resourceGroups/#{e(resource_group_name)}/providers/Microsoft.Compute/virtualMachines",
81
+ COMPUTE_API_VERSION
82
+ )
83
+ @http.get_all(url).map { |vm| RestObject.wrap(vm) }
84
+ end
85
+
86
+ def get_virtual_machine(resource_group_name, vm_name)
87
+ response = @http.get(vm_url(resource_group_name, vm_name))
88
+ RestObject.wrap(response.body)
89
+ end
90
+
91
+ def get_availability_set(resource_group_name, availability_set_name)
92
+ url = base_url(
93
+ "/resourceGroups/#{e(resource_group_name)}/providers/Microsoft.Compute/availabilitySets/#{e(availability_set_name)}",
94
+ COMPUTE_API_VERSION
95
+ )
96
+ response = @http.get(url)
97
+ RestObject.wrap(response.body)
98
+ end
99
+
100
+ def delete_virtual_machine(resource_group_name, vm_name)
101
+ @http.request_and_poll(:delete, vm_url(resource_group_name, vm_name))
102
+ nil
103
+ end
104
+
105
+ def get_vm_extension(resource_group_name, vm_name, extension_name, expand: nil)
106
+ url = vm_extension_url(resource_group_name, vm_name, extension_name)
107
+ url += "&$expand=#{e(expand)}" if expand
108
+ response = @http.get(url)
109
+ RestObject.wrap(response.body)
110
+ end
111
+
112
+ def create_vm_extension(resource_group_name, vm_name, extension_name, extension_body)
113
+ url = vm_extension_url(resource_group_name, vm_name, extension_name)
114
+ # The async PUT resolves to an operation-status document, not the
115
+ # extension itself, so GET the resource once polling completes.
116
+ @http.request_and_poll(:put, url, extension_body)
117
+ response = @http.get(url)
118
+ RestObject.wrap(response.body)
119
+ end
120
+
121
+ def list_vm_extension_versions(location, publisher, type)
122
+ path = "/providers/Microsoft.Compute/locations/#{e(location)}/publishers/#{e(publisher)}" \
123
+ "/artifacttypes/vmextension/types/#{e(type)}/versions"
124
+ # This endpoint returns a bare JSON array; get_all handles that shape
125
+ # as well as the paged { "value": ... } envelope.
126
+ @http.get_all(base_url(path, COMPUTE_API_VERSION)).map { |v| RestObject.wrap(v) }
127
+ end
128
+
129
+ # ----- Network (Microsoft.Network) -----
130
+
131
+ def get_network_interface(resource_group_name, name)
132
+ response = @http.get(network_url(resource_group_name, "networkInterfaces", name))
133
+ RestObject.wrap(response.body)
134
+ end
135
+
136
+ def get_public_ip_address(resource_group_name, name)
137
+ response = @http.get(network_url(resource_group_name, "publicIPAddresses", name))
138
+ RestObject.wrap(response.body)
139
+ end
140
+
141
+ def get_network_security_group(resource_group_name, name)
142
+ response = @http.get(network_url(resource_group_name, "networkSecurityGroups", name))
143
+ RestObject.wrap(response.body)
144
+ end
145
+
146
+ def get_virtual_network(resource_group_name, vnet_name)
147
+ response = @http.get(network_url(resource_group_name, "virtualNetworks", vnet_name))
148
+ RestObject.wrap(response.body)
149
+ end
150
+
151
+ def list_subnets(resource_group_name, vnet_name)
152
+ path = "/resourceGroups/#{e(resource_group_name)}/providers/Microsoft.Network" \
153
+ "/virtualNetworks/#{e(vnet_name)}/subnets"
154
+ url = base_url(path, NETWORK_API_VERSION)
155
+ @http.get_all(url).map { |subnet| RestObject.wrap(subnet) }
156
+ end
157
+
158
+ private
159
+
160
+ def base_url(path, api_version)
161
+ "#{@environment.resource_manager_endpoint_url}/subscriptions/#{e(@subscription_id)}" \
162
+ "#{path}?api-version=#{api_version}"
163
+ end
164
+
165
+ def rg_url(resource_group_name, api_version)
166
+ base_url("/resourceGroups/#{e(resource_group_name)}", api_version)
167
+ end
168
+
169
+ def deployment_url(resource_group_name, deployment_name)
170
+ base_url(
171
+ "/resourceGroups/#{e(resource_group_name)}/providers/Microsoft.Resources/deployments/#{e(deployment_name)}",
172
+ RESOURCES_API_VERSION
173
+ )
174
+ end
175
+
176
+ def vm_url(resource_group_name, vm_name)
177
+ base_url(
178
+ "/resourceGroups/#{e(resource_group_name)}/providers/Microsoft.Compute/virtualMachines/#{e(vm_name)}",
179
+ COMPUTE_API_VERSION
180
+ )
181
+ end
182
+
183
+ def vm_extension_url(resource_group_name, vm_name, extension_name)
184
+ base_url(
185
+ "/resourceGroups/#{e(resource_group_name)}/providers/Microsoft.Compute/virtualMachines/" \
186
+ "#{e(vm_name)}/extensions/#{e(extension_name)}",
187
+ COMPUTE_API_VERSION
188
+ )
189
+ end
190
+
191
+ def network_url(resource_group_name, resource_type, name)
192
+ base_url(
193
+ "/resourceGroups/#{e(resource_group_name)}/providers/Microsoft.Network/#{resource_type}/#{e(name)}",
194
+ NETWORK_API_VERSION
195
+ )
196
+ end
197
+
198
+ # Percent-encode a value for use as a single URI *path* segment.
199
+ # URI.encode_www_form_component is for query strings (it turns spaces
200
+ # into "+"), which is wrong inside a path; ERB::Util.url_encode emits
201
+ # %20 and escapes "/" so a name can never break out of its segment.
202
+ def e(value)
203
+ ERB::Util.url_encode(value.to_s)
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,89 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2012-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ module Azure
19
+ class ResourceManagement
20
+ module Rest
21
+ # ARM + Azure AD endpoints per cloud. Public cloud is the default; the
22
+ # other clouds are provided for completeness and parity with the retired
23
+ # Azure SDK, which supported them too.
24
+ class Environments
25
+ ENVIRONMENTS = {
26
+ "AzureCloud" => {
27
+ resource_manager_endpoint_url: "https://management.azure.com",
28
+ active_directory_endpoint_url: "https://login.microsoftonline.com",
29
+ token_audience: "https://management.azure.com/",
30
+ },
31
+ "AzureUSGovernment" => {
32
+ resource_manager_endpoint_url: "https://management.usgovcloudapi.net",
33
+ active_directory_endpoint_url: "https://login.microsoftonline.us",
34
+ token_audience: "https://management.usgovcloudapi.net/",
35
+ },
36
+ "AzureChinaCloud" => {
37
+ resource_manager_endpoint_url: "https://management.chinacloudapi.cn",
38
+ active_directory_endpoint_url: "https://login.chinacloudapi.cn",
39
+ token_audience: "https://management.chinacloudapi.cn/",
40
+ },
41
+ "AzureGermanCloud" => {
42
+ resource_manager_endpoint_url: "https://management.microsoftazure.de",
43
+ active_directory_endpoint_url: "https://login.microsoftonline.de",
44
+ token_audience: "https://management.microsoftazure.de/",
45
+ },
46
+ }.freeze
47
+
48
+ DEFAULT_ENVIRONMENT = "AzureCloud".freeze
49
+
50
+ attr_reader :resource_manager_endpoint_url, :active_directory_endpoint_url, :token_audience
51
+
52
+ def initialize(name = DEFAULT_ENVIRONMENT)
53
+ config = ENVIRONMENTS[name] || ENVIRONMENTS[DEFAULT_ENVIRONMENT]
54
+ @resource_manager_endpoint_url = config[:resource_manager_endpoint_url]
55
+ @active_directory_endpoint_url = config[:active_directory_endpoint_url]
56
+ @token_audience = config[:token_audience]
57
+ end
58
+
59
+ def self.default
60
+ new(DEFAULT_ENVIRONMENT)
61
+ end
62
+
63
+ # Resolves a caller-supplied cloud name (e.g. "AzureUSGovernment") to an
64
+ # Environments instance, defaulting to the public cloud only when no
65
+ # value was given. An unrecognized name is rejected outright: silently
66
+ # falling back to the public cloud would otherwise post service-principal
67
+ # secrets and ARM requests to the wrong (public) endpoints whenever an
68
+ # operator intending a sovereign cloud makes a typo.
69
+ def self.from_name(name)
70
+ normalized = name.to_s.strip
71
+ return new(DEFAULT_ENVIRONMENT) if normalized.empty?
72
+
73
+ unless ENVIRONMENTS.key?(normalized)
74
+ raise ArgumentError,
75
+ "Unknown Azure environment '#{normalized}'. Valid values are: #{names.join(", ")}."
76
+ end
77
+
78
+ new(normalized)
79
+ end
80
+
81
+ # Names of the clouds this client understands, exposed so callers can
82
+ # validate a user-provided setting.
83
+ def self.names
84
+ ENVIRONMENTS.keys
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end