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.
- checksums.yaml +4 -4
- data/lib/azure/azure_interface.rb +1 -1
- data/lib/azure/custom_errors.rb +2 -2
- data/lib/azure/helpers.rb +1 -1
- data/lib/azure/resource_management/ARM_deployment_template.rb +82 -27
- data/lib/azure/resource_management/ARM_interface.rb +112 -118
- data/lib/azure/resource_management/rest/arm_client.rb +208 -0
- data/lib/azure/resource_management/rest/environments.rb +89 -0
- data/lib/azure/resource_management/rest/errors.rb +57 -0
- data/lib/azure/resource_management/rest/http.rb +335 -0
- data/lib/azure/resource_management/rest/rest_object.rb +103 -0
- data/lib/azure/resource_management/rest/secure_connection.rb +57 -0
- data/lib/azure/resource_management/rest/token_provider.rb +125 -0
- data/lib/azure/resource_management/vnet_config.rb +4 -4
- data/lib/azure/resource_management/windows_credentials.rb +3 -3
- data/lib/chef/knife/azurerm_server_create.rb +22 -10
- data/lib/chef/knife/azurerm_server_delete.rb +1 -1
- data/lib/chef/knife/azurerm_server_list.rb +1 -1
- data/lib/chef/knife/azurerm_server_show.rb +1 -1
- data/lib/chef/knife/bootstrap/bootstrapper.rb +142 -14
- data/lib/chef/knife/bootstrap/common_bootstrap_options.rb +4 -6
- data/lib/chef/knife/bootstrap_azurerm.rb +1 -1
- data/lib/chef/knife/helpers/azurerm_base.rb +170 -9
- data/lib/knife-azure/version.rb +2 -2
- metadata +37 -95
- data/lib/azure/service_management/ASM_interface.rb +0 -310
- data/lib/azure/service_management/ag.rb +0 -99
- data/lib/azure/service_management/certificate.rb +0 -235
- data/lib/azure/service_management/connection.rb +0 -102
- data/lib/azure/service_management/deploy.rb +0 -221
- data/lib/azure/service_management/disk.rb +0 -68
- data/lib/azure/service_management/host.rb +0 -184
- data/lib/azure/service_management/image.rb +0 -94
- data/lib/azure/service_management/loadbalancer.rb +0 -78
- data/lib/azure/service_management/rest.rb +0 -126
- data/lib/azure/service_management/role.rb +0 -717
- data/lib/azure/service_management/storageaccount.rb +0 -127
- data/lib/azure/service_management/utility.rb +0 -40
- data/lib/azure/service_management/vnet.rb +0 -134
- data/lib/chef/knife/azure_ag_create.rb +0 -73
- data/lib/chef/knife/azure_ag_list.rb +0 -35
- data/lib/chef/knife/azure_image_list.rb +0 -56
- data/lib/chef/knife/azure_internal-lb_create.rb +0 -74
- data/lib/chef/knife/azure_internal-lb_list.rb +0 -35
- data/lib/chef/knife/azure_server_create.rb +0 -531
- data/lib/chef/knife/azure_server_delete.rb +0 -136
- data/lib/chef/knife/azure_server_list.rb +0 -38
- data/lib/chef/knife/azure_server_show.rb +0 -41
- data/lib/chef/knife/azure_vnet_create.rb +0 -74
- data/lib/chef/knife/azure_vnet_list.rb +0 -35
- data/lib/chef/knife/bootstrap_azure.rb +0 -191
- data/lib/chef/knife/helpers/azure_base.rb +0 -392
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#
|
|
2
|
-
# Copyright:: Copyright (c)
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
|
58
|
-
@
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
57
|
+
arm_client.list_all_virtual_machines
|
|
94
58
|
else
|
|
95
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
174
|
+
arm_client.get_virtual_machine(resource_group_name, vm_name)
|
|
213
175
|
true
|
|
214
|
-
rescue
|
|
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
|
-
|
|
188
|
+
arm_client.get_network_security_group(resource_group_name, security_group_name)
|
|
227
189
|
true
|
|
228
|
-
rescue
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
386
|
+
deployment_body = {
|
|
387
|
+
"properties" => {
|
|
388
|
+
"template" => template,
|
|
389
|
+
"parameters" => parameters,
|
|
390
|
+
"mode" => "Incremental",
|
|
391
|
+
},
|
|
392
|
+
}
|
|
421
393
|
|
|
422
|
-
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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 =
|
|
413
|
+
vm_extension = arm_client.create_vm_extension(
|
|
440
414
|
params[:azure_resource_group_name],
|
|
441
415
|
params[:azure_vm_name],
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
483
|
-
err_json =
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
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
|