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,6 +1,6 @@
1
1
  #
2
2
  # Author:: Aliasgar Batterywala (aliasgar.batterywala@clogeny.com)
3
- # Copyright:: Copyright (c) Chef Software Inc.
3
+ # Copyright:: Copyright (c) 2012-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
4
4
  # License:: Apache License, Version 2.0
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -36,8 +36,8 @@ module Azure::ARM
36
36
  end
37
37
 
38
38
  def get_vnet(resource_group_name, vnet_name)
39
- network_resource_client.virtual_networks.get(resource_group_name, vnet_name)
40
- rescue MsRestAzure::AzureOperationError => error
39
+ arm_client.get_virtual_network(resource_group_name, vnet_name)
40
+ rescue Azure::ResourceManagement::Rest::OperationError => error
41
41
  if error.body
42
42
  err_json = JSON.parse(error.response.body)
43
43
  if err_json["error"]["code"] == "ResourceNotFound"
@@ -50,7 +50,7 @@ module Azure::ARM
50
50
 
51
51
  ## lists all subnets under a virtual network or lists subnets of only a particular address space ##
52
52
  def subnets_list(resource_group_name, vnet_name, address_prefix = nil)
53
- list = network_resource_client.subnets.list(resource_group_name, vnet_name)
53
+ list = arm_client.list_subnets(resource_group_name, vnet_name)
54
54
  !address_prefix.nil? && !list.empty? ? subnets_list_for_specific_address_space(address_prefix, list) : list
55
55
  end
56
56
 
@@ -1,6 +1,6 @@
1
1
  #
2
2
  # Author:: Nimisha Sharad (nimisha.sharad@clogeny.com)
3
- # Copyright:: Copyright (c) Chef Software Inc.
3
+ # Copyright:: Copyright (c) 2012-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
4
4
  # License:: Apache License, Version 2.0
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -66,7 +66,7 @@ module Azure
66
66
  :Attributes, CREDENTIAL_ATTRIBUTE,
67
67
  :TargetAlias, :LPTSTR,
68
68
  :UserName, :LPTSTR
69
- end
69
+ end
70
70
 
71
71
  # Ref: https://msdn.microsoft.com/en-us/library/windows/desktop/aa374804(v=vs.85).aspx
72
72
  safe_attach_function :CredReadW, %i{LPCTSTR DWORD DWORD pointer}, :BOOL
@@ -178,5 +178,5 @@ module Azure
178
178
  end
179
179
  end
180
180
  end
181
- end
181
+ end
182
182
  end
@@ -1,6 +1,6 @@
1
1
  #
2
2
  # Author:: Aliasgar Batterywala (aliasgar.batterywala@clogeny.com)
3
- # Copyright:: Copyright (c) Chef Software Inc.
3
+ # Copyright:: Copyright (c) 2012-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
4
4
  # License:: Apache License, Version 2.0
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -41,13 +41,14 @@ class Chef
41
41
 
42
42
  option :azure_storage_account_type,
43
43
  long: "--azure-storage-account-type TYPE",
44
- description: "Optional. One of the following account types (case-sensitive):
45
- Standard_LRS (Standard Locally-redundant storage)
46
- Standard_ZRS (Standard Zone-redundant storage)
47
- Standard_GRS (Standard Geo-redundant storage)
48
- Standard_RAGRS (Standard Read access geo-redundant storage)
49
- Premium_LRS (Premium Locally-redundant storage)",
50
- default: "Standard_GRS"
44
+ description: "Optional. Sets the storage account type (SKU) of the VM's managed OS
45
+ disk (case-sensitive). One of the following:
46
+ Standard_LRS (Standard HDD locally-redundant storage)
47
+ StandardSSD_LRS (Standard SSD locally-redundant storage)
48
+ Premium_LRS (Premium SSD locally-redundant storage)
49
+ StandardSSD_ZRS (Standard SSD zone-redundant storage)
50
+ Premium_ZRS (Premium SSD zone-redundant storage)",
51
+ default: "Standard_LRS"
51
52
 
52
53
  option :azure_image_reference_publisher,
53
54
  long: "--azure-image-reference-publisher PUBLISHER_NAME",
@@ -128,6 +129,7 @@ class Chef
128
129
  :azure_service_location
129
130
  )
130
131
  validate_params!
132
+ warn_if_storage_account_ignored!
131
133
  end
132
134
 
133
135
  def plugin_create_instance!
@@ -181,7 +183,7 @@ class Chef
181
183
 
182
184
  server_def[:tcp_endpoints] = config[:tcp_endpoints] if config[:tcp_endpoints]
183
185
 
184
- # We assign azure_vm_name to chef_node_name If node name is nill because storage account name is combination of hash value and node name.
186
+ # We assign azure_vm_name to chef_node_name if node name is nil.
185
187
  config[:chef_node_name] ||= config[:azure_vm_name]
186
188
 
187
189
  server_def[:azure_storage_account] = config[:azure_vm_name] if server_def[:azure_storage_account].nil?
@@ -239,6 +241,16 @@ class Chef
239
241
 
240
242
  private
241
243
 
244
+ def warn_if_storage_account_ignored!
245
+ return if config[:azure_storage_account].nil?
246
+
247
+ ui.warn("--azure-storage-account is deprecated and has no effect: VM OS disks now use " \
248
+ "Azure managed disks instead of an unmanaged VHD in a storage account. This option " \
249
+ "is kept only for backward compatibility with existing command invocations and will " \
250
+ "be removed in a future release. Use --azure-storage-account-type to control the " \
251
+ "managed disk SKU instead.")
252
+ end
253
+
242
254
  def set_defaults
243
255
  # set_default_image_reference!
244
256
  set_configs
@@ -292,7 +304,7 @@ class Chef
292
304
  def set_os_image(publisher, img_offer, default_os_version)
293
305
  config[:azure_image_reference_publisher] = publisher
294
306
  config[:azure_image_reference_offer] = img_offer
295
- config[:azure_image_reference_sku] = config[:azure_image_reference_sku] ? config[:azure_image_reference_sku] : default_os_version
307
+ config[:azure_image_reference_sku] = config[:azure_image_reference_sku] || default_os_version
296
308
  end
297
309
 
298
310
  def is_image_os_type?
@@ -2,7 +2,7 @@
2
2
  # Author:: Barry Davis (barryd@jetstreamsoftware.com)
3
3
  # Author:: Adam Jacob (<adam@chef.io>)
4
4
  # Author:: Seth Chisamore (<schisamo@chef.io>)
5
- # Copyright:: Copyright (c) Chef Software Inc.
5
+ # Copyright:: Copyright (c) 2012-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
6
6
  # License:: Apache License, Version 2.0
7
7
  #
8
8
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,6 +1,6 @@
1
1
  #
2
2
  # Author:: Adam Jacob (<adam@chef.io>)
3
- # Copyright:: Copyright (c) Chef Software Inc.
3
+ # Copyright:: Copyright (c) 2012-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
4
4
  # License:: Apache License, Version 2.0
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,6 +1,6 @@
1
1
  #
2
2
  # Author:: Meera Navale (meera.navale@msystechnologies.com)
3
- # Copyright:: Copyright (c) Chef Software Inc.
3
+ # Copyright:: Copyright (c) 2012-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
4
4
  # License:: Apache License, Version 2.0
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,6 +1,6 @@
1
1
  #
2
2
  # Author:: Aliasgar Batterywala (aliasgar.batterywala@clogeny.com)
3
- # Copyright:: Copyright (c) Chef Software Inc.
3
+ # Copyright:: Copyright (c) 2012-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
4
4
  # License:: Apache License, Version 2.0
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -49,16 +49,11 @@ class Chef
49
49
  config[:azure_chef_extension_version]
50
50
  else
51
51
  chef_extension_name ||= get_chef_extension_name
52
- if @service.instance_of? Azure::ResourceManagement::ARMInterface
53
- service.get_latest_chef_extension_version(
54
- azure_service_location: config[:azure_service_location],
55
- chef_extension_publisher: get_chef_extension_publisher,
56
- chef_extension: chef_extension_name
57
- )
58
- elsif @service.instance_of? Azure::ServiceManagement::ASMInterface
59
- extensions = service.get_extension(chef_extension_name, get_chef_extension_publisher)
60
- extensions.css("Version").max.text.split(".").first + ".*"
61
- end
52
+ service.get_latest_chef_extension_version(
53
+ azure_service_location: config[:azure_service_location],
54
+ chef_extension_publisher: get_chef_extension_publisher,
55
+ chef_extension: chef_extension_name
56
+ )
62
57
  end
63
58
  end
64
59
 
@@ -73,12 +68,33 @@ class Chef
73
68
 
74
69
  def get_chef_extension_public_params
75
70
  pub_config = {}
71
+
76
72
  if config[:azure_extension_client_config]
77
73
  pub_config[:client_rb] = File.read(File.expand_path(config[:azure_extension_client_config]))
78
74
  else
79
- pub_config[:client_rb] = "chef_server_url \t #{Chef::Config[:chef_server_url].to_json}\nvalidation_client_name\t#{Chef::Config[:validation_client_name].to_json}"
75
+ # `chef_license` is set here (rather than left to interactive/env-var
76
+ # acceptance) because the VM extension's first chef-client run
77
+ # otherwise fails with "Chef Infra Client cannot execute without
78
+ # accepting the license" -- there's no TTY/CHEF_LICENSE env var
79
+ # available inside the freshly-provisioned VM.
80
+ pub_config[:client_rb] = "chef_server_url \t #{Chef::Config[:chef_server_url].to_json}\nvalidation_client_name\t#{Chef::Config[:validation_client_name].to_json}\nchef_license\t\"accept-no-persist\""
80
81
  end
81
82
 
83
+ # The `chef_license` line in client_rb above only covers chef-client
84
+ # invocations that read that config file (`-c client.rb`). The VM
85
+ # extension also runs a separate `chef-apply -e "cron '...' do ... end"`
86
+ # step (with no `-c` flag) to install the periodic chef-client cron job,
87
+ # which does not read client.rb and fails with the same license error.
88
+ # The extension's own shared.sh reads this top-level "CHEF_LICENSE"
89
+ # public setting and exports it as an environment variable before
90
+ # running any step, so setting it here covers that cron step too.
91
+ pub_config[:CHEF_LICENSE] = "accept-no-persist"
92
+
93
+ # chef_license_key is now forwarded via get_chef_extension_private_params
94
+ # below (protectedSettings), not here -- see
95
+ # chef-partners/azure-chef-extension#413 (version 1210.15.11.1,
96
+ # rolled out to all regions), which added a decrypt path for it.
97
+
82
98
  pub_config[:runlist] = config[:run_list].empty? ? "" : config[:run_list].join(",").to_json
83
99
  pub_config[:custom_json_attr] = config[:json_attributes] || {}
84
100
  pub_config[:extendedLogs] = config[:extended_logs] ? "true" : "false"
@@ -88,12 +104,29 @@ class Chef
88
104
 
89
105
  # bootstrap attributes
90
106
  pub_config[:bootstrap_options] = {}
91
- pub_config[:bootstrap_options][:environment] = config[:environment] if config[:environment]
107
+ # The Chef VM extension always renders this value into a "-E <value>"
108
+ # chef-client argument. If it's left blank (nil, or an explicitly
109
+ # empty string, both of which are truthy in Ruby), chef-client's
110
+ # option parser fails with "missing argument: -E" because the flag
111
+ # gets emitted without a value. Default to Chef's standard
112
+ # "_default" environment whenever --environment isn't supplied or
113
+ # was supplied blank.
114
+ pub_config[:bootstrap_options][:environment] = config[:environment].to_s.empty? ? "_default" : config[:environment]
92
115
  pub_config[:bootstrap_options][:chef_node_name] = config[:chef_node_name] if config[:chef_node_name]
93
116
  pub_config[:bootstrap_options][:chef_server_url] = Chef::Config[:chef_server_url] if Chef::Config[:chef_server_url]
94
117
  pub_config[:bootstrap_options][:validation_client_name] = Chef::Config[:validation_client_name] if Chef::Config[:validation_client_name]
95
118
  pub_config[:bootstrap_options][:node_verify_api_cert] = config[:node_verify_api_cert] ? "true" : "false" if config.key?(:node_verify_api_cert)
96
- pub_config[:bootstrap_options][:bootstrap_version] = config[:bootstrap_version] if config[:bootstrap_version]
119
+ # If --bootstrap-version isn't given, the extension's chef-install.sh
120
+ # treats bootstrap_version as blank, can't tell the major version is
121
+ # >= 19, and falls back to installing the legacy "chef" product
122
+ # (currently capped in the 18.x line) instead of "chef-ice" (19.x+).
123
+ # Chef::Knife::Bootstrap#version_to_install (used by the stock
124
+ # SSH-based chef-full.erb bootstrap that knife-ec2/knife-google rely
125
+ # on) defaults to the major version of the `chef` gem bundled
126
+ # alongside `knife` itself (`Chef::VERSION.split(".").first`) in
127
+ # that same situation, so mirror that default here too.
128
+ resolved_bootstrap_version = config[:bootstrap_version] || Chef::VERSION.split(".").first
129
+ pub_config[:bootstrap_options][:bootstrap_version] = resolved_bootstrap_version
97
130
  pub_config[:bootstrap_options][:node_ssl_verify_mode] = config[:node_ssl_verify_mode] if config[:node_ssl_verify_mode]
98
131
  pub_config[:bootstrap_options][:bootstrap_proxy] = config[:bootstrap_proxy] if config[:bootstrap_proxy]
99
132
  pub_config
@@ -163,9 +196,104 @@ class Chef
163
196
  # encrypted_data_bag_secret key for encrypting/decrypting the data bags
164
197
  pri_config[:encrypted_data_bag_secret] = load_correct_secret
165
198
 
199
+ # The extension's install scripts (chef-install.sh/shared.sh,
200
+ # chef-install.psm1/shared.ps1) now decrypt protectedSettings for
201
+ # chef_license_key and prefer it over the deprecated publicSettings
202
+ # location (chef-partners/azure-chef-extension#413, shipped as
203
+ # version 1210.15.11.1, rolled out to all regions for both
204
+ # ChefClient and LinuxChefClient). Forwarding it here instead of via
205
+ # get_chef_extension_public_params means it is delivered to the VM
206
+ # encrypted (CMS/PKCS7, decrypted locally using the cert the Azure
207
+ # Guest Agent provisions), matching the treatment already given to
208
+ # validation_key/chef_server_crt/encrypted_data_bag_secret above,
209
+ # instead of as recoverable plaintext.
210
+ #
211
+ # chef_license_key is optional, not required: when it (and
212
+ # config[:license_id]) are both absent, the extension's
213
+ # chef-install.sh/shared.sh simply fall back to the unlicensed
214
+ # omnitruck.chef.io download host with a warning; there is no
215
+ # "chef_license_bypass" setting to set and no exit/failure path
216
+ # to work around.
217
+ #
218
+ # NOTE: chef-partners/azure-chef-extension#384 briefly made
219
+ # chef_license_key required-by-default with a chef_license_bypass
220
+ # escape hatch, but #409 ("Route to omnitruck when no license_id
221
+ # is specified, instead of chef_license_bypass") removed
222
+ # chef_license_bypass entirely and restored the always-optional,
223
+ # warning-only fallback described above for both the Linux and
224
+ # Windows install scripts. Do not reintroduce bypass handling
225
+ # here based on #384 alone -- check the extension's current HEAD
226
+ # first, since that requirement no longer exists as of #409.
227
+ #
228
+ # Auto-forwarding here when a key IS available
229
+ # (rather than requiring --chef-license-key on every single
230
+ # invocation) mirrors exactly what upstream
231
+ # Chef::Knife::Core::BootstrapContext/WindowsBootstrapContext do for
232
+ # the stock SSH-based bootstrap that knife-ec2/knife-google rely on:
233
+ # prefer an explicitly passed --chef-license-key, else fall back to
234
+ # config[:license_id] (the already-persisted/validated local
235
+ # license that Chef::Knife::Bootstrap#run populates via
236
+ # fetch_license before any plugin_* hook runs). Unlike the stock
237
+ # SSH-based context classes, there is no knife-azure-specific
238
+ # handling of --disable-license-activation: that upstream option
239
+ # isn't honored by this VM-extension delivery path, matching
240
+ # knife-ec2/knife-google, which also have no special-case code for
241
+ # it (their stock bootstrap context handles it generically for
242
+ # SSH/WinRM delivery only, which doesn't apply here either).
243
+ license_key = config[:chef_license_key] || config[:license_id]
244
+ if license_key
245
+ reject_pinned_extension_without_protected_license_support!(license_key)
246
+ pri_config[:chef_license_key] = license_key
247
+ end
248
+
166
249
  pri_config
167
250
  end
168
251
 
252
+ # chef-partners/azure-chef-extension release that first decrypts
253
+ # protectedSettings for chef_license_key (see the comment above the
254
+ # pri_config[:chef_license_key] assignment); builds older than this
255
+ # only read the deprecated, plaintext publicSettings location.
256
+ MIN_LICENSE_CAPABLE_EXTENSION_VERSION = "1210.15.11.1".freeze
257
+
258
+ # --azure-chef-extension-version lets a user pin an older extension
259
+ # build via get_chef_extension_version. If that pinned build predates
260
+ # MIN_LICENSE_CAPABLE_EXTENSION_VERSION, it cannot read the
261
+ # protectedSettings-only chef_license_key we now forward, so the VM
262
+ # would silently receive no usable license and the licensed Chef
263
+ # Infra Client install would fail. Fail fast here instead, before the
264
+ # ARM deployment is even built.
265
+ def reject_pinned_extension_without_protected_license_support!(license_key)
266
+ return unless license_key
267
+
268
+ pinned_version = config[:azure_chef_extension_version]
269
+ # Not pinned (resolved via get_latest_chef_extension_version) or a
270
+ # "<major>.*" family selector: Azure resolves either to a current
271
+ # build that supports protectedSettings, so there's nothing to
272
+ # reject here.
273
+ return if pinned_version.nil? || pinned_version.include?("*")
274
+
275
+ begin
276
+ pinned = Gem::Version.new(pinned_version)
277
+ rescue ArgumentError
278
+ # Not a comparable version string; let the Azure API validate (or
279
+ # reject) it instead of guessing here.
280
+ return
281
+ end
282
+
283
+ return if pinned >= Gem::Version.new(MIN_LICENSE_CAPABLE_EXTENSION_VERSION)
284
+
285
+ ui.error(
286
+ "--azure-chef-extension-version #{pinned_version} is older than " \
287
+ "#{MIN_LICENSE_CAPABLE_EXTENSION_VERSION}, the chef-partners/azure-chef-extension " \
288
+ "release that added protectedSettings support for chef_license_key. That version " \
289
+ "only reads the deprecated, plaintext publicSettings location, so the forwarded " \
290
+ "license key would be silently ignored and the licensed Chef Infra Client install " \
291
+ "would fail. Pin a version >= #{MIN_LICENSE_CAPABLE_EXTENSION_VERSION}, or omit " \
292
+ "--azure-chef-extension-version to use the latest extension build."
293
+ )
294
+ exit 1
295
+ end
296
+
169
297
  end
170
298
  end
171
299
  end
@@ -1,6 +1,6 @@
1
1
  #
2
2
  # Author:: Aliasgar Batterywala (aliasgar.batterywala@clogeny.com)
3
- # Copyright:: Copyright (c) Chef Software Inc.
3
+ # Copyright:: Copyright (c) 2012-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
4
4
  # License:: Apache License, Version 2.0
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -51,11 +51,9 @@ class Chef
51
51
  option :azure_storage_account,
52
52
  short: "-a NAME",
53
53
  long: "--azure-storage-account NAME",
54
- description: "Required for advanced server-create option.
55
- A name for the storage account that is unique within Windows Azure. Storage account names must be
56
- between 3 and 24 characters in length and use numbers and lower-case letters only.
57
- This name is the DNS prefix name and can be used to access blobs, queues, and tables in the storage account.
58
- For example: http://ServiceName.blob.core.windows.net/mycontainer/"
54
+ description: "Deprecated. No longer has any effect: VM OS disks now use Azure managed
55
+ disks instead of an unmanaged VHD in a storage account. Kept only for
56
+ backward compatibility with existing command invocations."
59
57
 
60
58
  option :azure_vm_name,
61
59
  long: "--azure-vm-name NAME",
@@ -1,6 +1,6 @@
1
1
  #
2
2
  # Author:: Nimisha Sharad (nimisha.sharad@clogeny.com)
3
- # Copyright:: Copyright (c) Chef Software Inc.
3
+ # Copyright:: Copyright (c) 2012-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
4
4
  # License:: Apache License, Version 2.0
5
5
  #
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -1,7 +1,7 @@
1
1
  #
2
2
  # Author:: Aliasgar Batterywala (aliasgar.batterywala@clogeny.com)
3
3
  #
4
- # Copyright:: Copyright (c) Chef Software Inc.
4
+ # Copyright:: Copyright (c) 2012-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
5
5
  # License:: Apache License, Version 2.0
6
6
  #
7
7
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -39,7 +39,7 @@ class Chef
39
39
  require "json" unless defined?(JSON)
40
40
 
41
41
  if Chef::Platform.windows?
42
- require_relative "../../azure/resource_management/windows_credentials"
42
+ require_relative "../../../azure/resource_management/windows_credentials"
43
43
  include Azure::ARM::WindowsCredentials
44
44
  end
45
45
  end
@@ -48,12 +48,18 @@ class Chef
48
48
  short: "-r RESOURCE_GROUP_NAME",
49
49
  long: "--azure-resource-group-name RESOURCE_GROUP_NAME",
50
50
  description: "The Resource Group name."
51
+
52
+ option :azure_environment,
53
+ long: "--azure-environment ENVIRONMENT",
54
+ description: "Optional. The Azure cloud to target: AzureCloud (default), " \
55
+ "AzureUSGovernment, AzureChinaCloud, or AzureGermanCloud.",
56
+ default: "AzureCloud"
51
57
  end
52
58
  end
53
59
 
54
60
  def service
55
61
  details = authentication_details
56
- details.update(azure_subscription_id: config[:azure_subscription_id])
62
+ details.update(azure_subscription_id: config[:azure_subscription_id], azure_environment: config[:azure_environment])
57
63
  @service ||= begin
58
64
  require_relative "../../../azure/resource_management/ARM_interface"
59
65
  service = Azure::ResourceManagement::ARMInterface.new(details)
@@ -76,7 +82,7 @@ class Chef
76
82
  errors = []
77
83
  keys.each do |k|
78
84
  if config[k].nil?
79
- errors << "You did not provide a valid '#{pretty_key(k)}' value. Please set knife[:#{k}] in your knife.rb."
85
+ errors << "You did not provide a valid '#{pretty_key(k)}' value. Please set knife[:#{k}] in your config.rb (knife.rb)."
80
86
  end
81
87
  end
82
88
  if errors.each { |e| ui.error(e) }.any?
@@ -98,13 +104,38 @@ class Chef
98
104
 
99
105
  def get_azure_cli_version
100
106
  if @azure_version != ""
101
- get_version = shell_out!("azure -v || az -v | grep azure-cli", returns: [0]).stdout
107
+ get_version = azure_cli_raw_version_output
102
108
  @azure_version = get_version.gsub(/[^0-9.]/, "")
103
109
  end
104
110
  @azure_prefix = @azure_version.to_i < 2 ? "azure" : "az"
105
111
  @azure_version
106
112
  end
107
113
 
114
+ # Detects whether the deprecated xplat "azure" CLI or the modern "az"
115
+ # CLI is installed and returns its raw version output. Filtering for
116
+ # the "azure-cli" line is done in Ruby (rather than via `grep`, and
117
+ # without relying on unix-style shell chaining like `||`/`|`) so this
118
+ # works identically on Windows, where `grep` isn't available and
119
+ # Mixlib::ShellOut runs commands through cmd.exe.
120
+ def azure_cli_raw_version_output
121
+ begin
122
+ result = shell_out("azure -v")
123
+ return result.stdout if result.exitstatus.zero?
124
+ rescue Errno::ENOENT
125
+ # The deprecated "azure" xplat CLI isn't installed on this system's PATH,
126
+ # which is expected/fine for the vast majority of users today. Fall
127
+ # through and try the modern "az" CLI instead.
128
+ end
129
+
130
+ begin
131
+ result = shell_out!("az -v")
132
+ rescue Errno::ENOENT
133
+ raise "Could not find either the 'azure' (deprecated) or the 'az' Azure CLI on this system's PATH. " \
134
+ "Please install the Azure CLI: https://learn.microsoft.com/cli/azure/install-azure-cli"
135
+ end
136
+ result.stdout.each_line.find { |line| line.include?("azure-cli") } || result.stdout
137
+ end
138
+
108
139
  def token_details_for_windows
109
140
  if is_old_xplat?
110
141
  token_details_from_WCM
@@ -176,8 +207,13 @@ class Chef
176
207
  require "base64" unless defined?(Base64)
177
208
  require "openssl" unless defined?(OpenSSL)
178
209
  require "uri" unless defined?(URI)
210
+ retried_with_legacy_provider = false
179
211
  begin
180
- doc = Nokogiri::XML(File.open(find_file(filename)))
212
+ # Use the block form of File.open so the file handle is always closed
213
+ # after parsing, rather than left open for the GC to close later. This
214
+ # matters on Windows, where an open handle prevents a Tempfile-based
215
+ # fixture (used in specs) from being unlinked.
216
+ doc = File.open(find_file(filename)) { |file| Nokogiri::XML(file) }
181
217
  profile = doc.at_css("PublishProfile")
182
218
  subscription = profile.at_css("Subscription")
183
219
  # check given PublishSettings XML file format.Currently PublishSettings file have two different XML format
@@ -192,12 +228,105 @@ class Chef
192
228
  end
193
229
  config[:azure_mgmt_cert] = management_cert.certificate.to_pem + management_cert.key.to_pem
194
230
  config[:azure_subscription_id] = doc.at_css("Subscription").attribute("Id").value
231
+ rescue OpenSSL::PKCS12::PKCS12Error => error
232
+ # Older Azure publish settings files use PKCS12 certificates encrypted with
233
+ # the legacy RC2-40-CBC cipher, which OpenSSL 3.x disables by default. On
234
+ # OpenSSL 3.x the raised error usually doesn't mention "RC2-40-CBC" at all -
235
+ # it's typically the generic "PKCS12_parse: unsupported" - so treat any
236
+ # "unsupported"-style PKCS12Error as a candidate for the legacy-cipher retry,
237
+ # not just messages that explicitly say RC2-40-CBC. Only load OpenSSL's
238
+ # "legacy" provider (widening the process-wide crypto surface) if we
239
+ # actually hit one of these failures, and only retry once.
240
+ if !retried_with_legacy_provider && legacy_cipher_error?(error) && load_openssl_legacy_provider
241
+ retried_with_legacy_provider = true
242
+ retry
243
+ end
244
+
245
+ if legacy_cipher_error?(error)
246
+ ui.error("Cannot parse certificate: #{error.message}")
247
+ if retried_with_legacy_provider
248
+ # We already successfully loaded OpenSSL's legacy provider and retried,
249
+ # but parsing still failed with what looks like a legacy-cipher error.
250
+ # The cipher isn't "unavailable" in this case -- something else about
251
+ # the certificate is unparseable -- so don't tell the user to
252
+ # regenerate it with a "more recent cipher" as if the provider were
253
+ # still missing.
254
+ ui.error("The PKCS12 certificate could not be parsed even after enabling OpenSSL's " \
255
+ "legacy provider (used for ciphers such as RC2-40-CBC). The file may be corrupt " \
256
+ "or use a cipher that isn't supported even by the legacy provider.")
257
+ else
258
+ ui.error("The PKCS12 certificate may use the legacy RC2-40-CBC cipher, which is unavailable " \
259
+ "in the current OpenSSL configuration. Please regenerate the publish settings file " \
260
+ "with a more recent cipher.")
261
+ end
262
+ else
263
+ ui.error("Error parsing PKCS12 certificate: #{error.message}")
264
+ end
265
+ exit 1
195
266
  rescue => error
196
267
  puts "#{error.class} and #{error.message}"
197
268
  exit 1
198
269
  end
199
270
  end
200
271
 
272
+ # Returns true if the given OpenSSL::PKCS12::PKCS12Error looks like it was
273
+ # caused by a legacy/deprecated cipher (such as RC2-40-CBC) being disabled by
274
+ # default on OpenSSL 3.x. On OpenSSL 3.x this commonly surfaces as a generic
275
+ # "unsupported" error rather than one that names RC2-40-CBC explicitly.
276
+ def legacy_cipher_error?(error)
277
+ error.message =~ /RC2-40-CBC/i || error.message =~ /unsupported/i
278
+ end
279
+
280
+ # Attempts to load OpenSSL's "legacy" provider (needed to decrypt PKCS12 files
281
+ # using deprecated ciphers such as RC2-40-CBC). Returns true if the provider was
282
+ # loaded successfully, false otherwise (e.g. it isn't available on this system).
283
+ def load_openssl_legacy_provider
284
+ if defined?(OpenSSL::Provider)
285
+ OpenSSL::Provider.load("legacy")
286
+ OpenSSL::Provider.load("default")
287
+ return true
288
+ end
289
+
290
+ # `OpenSSL::Provider` was only added to the "openssl" Ruby gem in version 3.0,
291
+ # which ships as a default gem starting with Ruby 3.2. On supported Ruby 3.1
292
+ # installations (bundled openssl gem < 3.0) there's no Ruby API to load an
293
+ # OpenSSL 3.x provider, even though the underlying libcrypto may itself be
294
+ # OpenSSL 3.x and support providers. Fall back to calling libcrypto's
295
+ # OSSL_PROVIDER_load directly via FFI (already a runtime dependency of this
296
+ # gem) so legacy PKCS12 files can still be decrypted on Ruby 3.1.
297
+ load_openssl_legacy_provider_via_ffi
298
+ rescue StandardError, LoadError
299
+ false
300
+ end
301
+
302
+ # Loads the OpenSSL "legacy" and "default" providers by calling libcrypto's
303
+ # OSSL_PROVIDER_load function directly through FFI. This only works when the
304
+ # linked libcrypto is OpenSSL 3.x (the function doesn't exist on OpenSSL 1.1.1
305
+ # or LibreSSL); any failure to locate the library/symbol is treated as the
306
+ # provider simply being unavailable.
307
+ def load_openssl_legacy_provider_via_ffi
308
+ require "ffi" unless defined?(FFI)
309
+
310
+ # Bind to OSSL_PROVIDER_load in the *current process's* already-loaded
311
+ # symbol table (FFI::Library::CURRENT_PROCESS) rather than dlopen-ing "ssl"/"crypto"
312
+ # again. Re-loading libcrypto as a second, separate mapping alongside the
313
+ # one Ruby's own "openssl" extension already loaded can make OpenSSL 3.x
314
+ # detect what it considers an unsafe double-load and abort the whole
315
+ # process (observed as "libcrypto in an unsafe way" on macOS); attaching to
316
+ # the existing in-process symbols avoids that entirely.
317
+ provider_loader = Module.new do
318
+ extend FFI::Library
319
+ ffi_lib FFI::Library::CURRENT_PROCESS
320
+ attach_function :OSSL_PROVIDER_load, %i{pointer string}, :pointer
321
+ end
322
+
323
+ legacy = provider_loader.OSSL_PROVIDER_load(nil, "legacy")
324
+ default = provider_loader.OSSL_PROVIDER_load(nil, "default")
325
+ !legacy.null? && !default.null?
326
+ rescue StandardError, LoadError
327
+ false
328
+ end
329
+
201
330
  def find_file(name)
202
331
  name = ::File.expand_path(name)
203
332
  config_dir = Chef::Knife.chef_config_dir
@@ -275,10 +404,10 @@ class Chef
275
404
  end
276
405
 
277
406
  if !is_image_windows?
278
- if (config[:azure_vm_name].match /^(?=.*[a-zA-Z-])([a-zA-z0-9-]{1,64})$/).nil?
407
+ if (config[:azure_vm_name].match(/^(?=.*[a-zA-Z-])([a-zA-Z0-9-]{1,64})$/)).nil?
279
408
  raise ArgumentError, "VM name can only contain alphanumeric and hyphen(-) characters and maximum length cannot exceed 64 characters."
280
409
  end
281
- elsif (config[:azure_vm_name].match /^(?=.*[a-zA-Z-])([a-zA-z0-9-]{1,15})$/).nil?
410
+ elsif (config[:azure_vm_name].match(/^(?=.*[a-zA-Z-])([a-zA-Z0-9-]{1,15})$/)).nil?
282
411
  raise ArgumentError, "VM name can only contain alphanumeric and hyphen(-) characters and maximum length cannot exceed 15 characters."
283
412
  end
284
413
 
@@ -302,6 +431,38 @@ class Chef
302
431
  end
303
432
  end
304
433
 
434
+ if config[:azure_storage_account_type]
435
+ # --azure-storage-account-type now sets the managed OS disk's SKU rather than a
436
+ # storage account's replication type; the old storage-account-only replication
437
+ # values (Standard_ZRS, Standard_GRS, Standard_RAGRS) are not valid managed disk
438
+ # SKUs and would otherwise be sent through to Azure and fail remotely.
439
+ legacy_storage_account_types = %w{Standard_ZRS Standard_GRS Standard_RAGRS}
440
+ valid_managed_disk_types = %w{Standard_LRS StandardSSD_LRS Premium_LRS StandardSSD_ZRS Premium_ZRS}
441
+ if legacy_storage_account_types.include?(config[:azure_storage_account_type])
442
+ raise ArgumentError, "'#{config[:azure_storage_account_type]}' is a storage-account replication type " \
443
+ "that is no longer valid for --azure-storage-account-type, since VMs now use managed disks. " \
444
+ "Please use one of the managed disk SKUs instead: #{valid_managed_disk_types.join(", ")}."
445
+ elsif !valid_managed_disk_types.include?(config[:azure_storage_account_type])
446
+ raise ArgumentError, "Invalid value '#{config[:azure_storage_account_type]}' for --azure-storage-account-type. " \
447
+ "Use one of the following managed disk SKUs: #{valid_managed_disk_types.join(", ")}."
448
+ end
449
+ end
450
+
451
+ if config[:azure_availability_set]
452
+ # Azure availability set SKUs are immutable once created. The deployment
453
+ # template always creates/updates the set as "Aligned" (required for VMs with
454
+ # managed disks), so reusing the name of a pre-existing legacy "Classic" set
455
+ # (e.g. from an older unmanaged-disk deployment) would fail remotely with a
456
+ # cryptic ARM error. Detect that case here and fail fast with guidance instead.
457
+ existing_sku = service.existing_availability_set_sku(config[:azure_resource_group_name], config[:azure_availability_set])
458
+ if existing_sku != :not_found && existing_sku != "Aligned"
459
+ raise ArgumentError, "The availability set '#{config[:azure_availability_set]}' already exists as a " \
460
+ "legacy 'Classic' (unmanaged-disk) availability set. Azure availability set SKUs are immutable, so " \
461
+ "it cannot be converted to 'Aligned' for use with managed disks. Please choose a different, unused " \
462
+ "name for --azure-availability-set, or delete and recreate the existing availability set before reusing it."
463
+ end
464
+ end
465
+
305
466
  config[:ohai_hints] = format_ohai_hints(config[:ohai_hints])
306
467
  validate_ohai_hints unless config[:ohai_hints].casecmp("default").zero?
307
468
  end
@@ -349,7 +510,7 @@ class Chef
349
510
  ## in Windows Credentails Manager (WCM).
350
511
  ## Newer versions use the same pattern across platforms where credentials gets
351
512
  ## stored in ~/.azure/accessTokens.json file.
352
- "Please run XPLAT's '#{@azure_prefix} login' command OR specify azure_tenant_id, azure_subscription_id, azure_client_id, azure_client_secret in your knife.rb"
513
+ "Please run XPLAT's '#{@azure_prefix} login' command OR specify azure_tenant_id, azure_subscription_id, azure_client_id, azure_client_secret in your config.rb (knife.rb)."
353
514
  end
354
515
  end
355
516
  end