knife-azure 1.8.0 → 1.8.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/LICENSE +201 -201
- data/lib/azure/resource_management/ARM_deployment_template.rb +1 -1
- data/lib/azure/service_management/certificate.rb +0 -0
- data/lib/azure/service_management/connection.rb +0 -0
- data/lib/azure/service_management/deploy.rb +0 -0
- data/lib/azure/service_management/disk.rb +0 -0
- data/lib/azure/service_management/host.rb +0 -0
- data/lib/azure/service_management/image.rb +0 -0
- data/lib/azure/service_management/rest.rb +0 -0
- data/lib/azure/service_management/role.rb +0 -0
- data/lib/azure/service_management/utility.rb +0 -0
- data/lib/chef/knife/azure_ag_list.rb +0 -2
- data/lib/chef/knife/azure_base.rb +70 -0
- data/lib/chef/knife/azure_image_list.rb +0 -0
- data/lib/chef/knife/azure_internal-lb_list.rb +0 -2
- data/lib/chef/knife/azure_server_create.rb +2 -83
- data/lib/chef/knife/azure_server_delete.rb +0 -0
- data/lib/chef/knife/azure_server_list.rb +0 -0
- data/lib/chef/knife/azure_server_show.rb +0 -0
- data/lib/chef/knife/azure_vnet_list.rb +0 -2
- data/lib/chef/knife/azurerm_base.rb +71 -52
- data/lib/chef/knife/azurerm_server_create.rb +38 -45
- data/lib/chef/knife/azurerm_server_delete.rb +35 -27
- data/lib/chef/knife/azurerm_server_list.rb +20 -0
- data/lib/chef/knife/azurerm_server_show.rb +3 -1
- data/lib/chef/knife/bootstrap/bootstrapper.rb +11 -6
- data/lib/knife-azure/version.rb +1 -1
- metadata +3 -17
@@ -124,7 +124,77 @@ class Chef
|
|
124
124
|
end
|
125
125
|
|
126
126
|
# validate command pre-requisites (cli options)
|
127
|
+
# (locate_config_value(:winrm_password).length <= 6 && locate_config_value(:winrm_password).length >= 72)
|
127
128
|
def validate_params!
|
129
|
+
if locate_config_value(:winrm_password) && !locate_config_value(:winrm_password).strip.size.between?(6, 72)
|
130
|
+
ui.error("The supplied password must be 6-72 characters long and meet password complexity requirements")
|
131
|
+
exit 1
|
132
|
+
end
|
133
|
+
|
134
|
+
if locate_config_value(:ssh_password) && !locate_config_value(:ssh_password).empty? && !locate_config_value(:ssh_password).strip.size.between?(6, 72)
|
135
|
+
ui.error("The supplied ssh password must be 6-72 characters long and meet password complexity requirements")
|
136
|
+
exit 1
|
137
|
+
end
|
138
|
+
|
139
|
+
if locate_config_value(:azure_connect_to_existing_dns) && locate_config_value(:azure_vm_name).nil?
|
140
|
+
ui.error("Specify the VM name using --azure-vm-name option, since you are connecting to existing dns")
|
141
|
+
exit 1
|
142
|
+
end
|
143
|
+
|
144
|
+
if locate_config_value(:azure_service_location) && locate_config_value(:azure_affinity_group)
|
145
|
+
ui.error("Cannot specify both --azure-service-location and --azure-affinity-group, use one or the other.")
|
146
|
+
exit 1
|
147
|
+
elsif locate_config_value(:azure_service_location).nil? && locate_config_value(:azure_affinity_group).nil?
|
148
|
+
ui.error("Must specify either --azure-service-location or --azure-affinity-group.")
|
149
|
+
exit 1
|
150
|
+
end
|
151
|
+
|
152
|
+
if locate_config_value(:winrm_authentication_protocol) && ! %w{basic negotiate kerberos}.include?(locate_config_value(:winrm_authentication_protocol).downcase)
|
153
|
+
ui.error("Invalid value for --winrm-authentication-protocol option. Use valid protocol values i.e [basic, negotiate, kerberos]")
|
154
|
+
exit 1
|
155
|
+
end
|
156
|
+
|
157
|
+
if !(service.valid_image?(locate_config_value(:azure_source_image)))
|
158
|
+
ui.error("Image '#{locate_config_value(:azure_source_image)}' is invalid")
|
159
|
+
exit 1
|
160
|
+
end
|
161
|
+
|
162
|
+
# Validate join domain requirements.
|
163
|
+
if locate_config_value(:azure_domain_name) || locate_config_value(:azure_domain_user)
|
164
|
+
if locate_config_value(:azure_domain_user).nil? || locate_config_value(:azure_domain_passwd).nil?
|
165
|
+
ui.error("Must specify both --azure-domain-user and --azure-domain-passwd.")
|
166
|
+
exit 1
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
if locate_config_value(:winrm_transport) == "ssl" && locate_config_value(:thumbprint).nil? && ( locate_config_value(:winrm_ssl_verify_mode).nil? || locate_config_value(:winrm_ssl_verify_mode) == :verify_peer )
|
171
|
+
ui.error("The SSL transport was specified without the --thumbprint option. Specify a thumbprint, or alternatively set the --winrm-ssl-verify-mode option to 'verify_none' to skip verification.")
|
172
|
+
exit 1
|
173
|
+
end
|
174
|
+
|
175
|
+
if locate_config_value(:extended_logs) && locate_config_value(:bootstrap_protocol) != 'cloud-api'
|
176
|
+
ui.error("--extended-logs option only works with --bootstrap-protocol cloud-api")
|
177
|
+
exit 1
|
178
|
+
end
|
179
|
+
|
180
|
+
if locate_config_value(:bootstrap_protocol) == 'cloud-api' && locate_config_value(:azure_vm_name).nil? && locate_config_value(:azure_dns_name).nil?
|
181
|
+
ui.error("Specifying the DNS name using --azure-dns-name or VM name using --azure-vm-name option is required with --bootstrap-protocol cloud-api")
|
182
|
+
exit 1
|
183
|
+
end
|
184
|
+
|
185
|
+
if locate_config_value(:daemon)
|
186
|
+
unless is_image_windows?
|
187
|
+
raise ArgumentError, "The daemon option is only supported for Windows nodes."
|
188
|
+
end
|
189
|
+
|
190
|
+
unless locate_config_value(:bootstrap_protocol) == 'cloud-api'
|
191
|
+
raise ArgumentError, "The --daemon option requires the use of --bootstrap-protocol cloud-api"
|
192
|
+
end
|
193
|
+
|
194
|
+
unless %w{none service task}.include?(locate_config_value(:daemon).downcase)
|
195
|
+
raise ArgumentError, "Invalid value for --daemon option. Valid values are 'none', 'service' and 'task'."
|
196
|
+
end
|
197
|
+
end
|
128
198
|
end
|
129
199
|
|
130
200
|
# validates keys
|
File without changes
|
@@ -232,7 +232,6 @@ class Chef
|
|
232
232
|
|
233
233
|
def wait_until_virtual_machine_ready(retry_interval_in_seconds = 30)
|
234
234
|
vm_status = nil
|
235
|
-
|
236
235
|
begin
|
237
236
|
azure_vm_startup_timeout = locate_config_value(:azure_vm_startup_timeout).to_i
|
238
237
|
azure_vm_ready_timeout = locate_config_value(:azure_vm_ready_timeout).to_i
|
@@ -355,12 +354,9 @@ class Chef
|
|
355
354
|
if role.at_css("RoleName").text == locate_config_value(:azure_vm_name)
|
356
355
|
lnx_waagent_fail_msg = "Failed to deserialize the status reported by the Guest Agent"
|
357
356
|
waagent_status_msg = role.at_css("GuestAgentStatus FormattedMessage Message").text
|
358
|
-
|
359
357
|
if role.at_css("GuestAgentStatus Status").text == "Ready"
|
360
358
|
extn_status = role.at_css("ResourceExtensionStatusList Status").text
|
361
|
-
|
362
359
|
Chef::Log.debug("Resource extension status is #{extn_status}")
|
363
|
-
|
364
360
|
if extn_status == "Installing"
|
365
361
|
extension_status[:status] = :extension_installing
|
366
362
|
extension_status[:message] = role.at_css("ResourceExtensionStatusList FormattedMessage Message").text
|
@@ -387,30 +383,20 @@ class Chef
|
|
387
383
|
else
|
388
384
|
extension_status[:status] = :extension_status_not_detected
|
389
385
|
end
|
390
|
-
|
391
386
|
return extension_status
|
392
387
|
end
|
393
388
|
|
394
389
|
def run
|
395
390
|
$stdout.sync = true
|
396
|
-
|
397
391
|
storage = nil
|
398
|
-
|
399
392
|
Chef::Log.info("validating...")
|
400
393
|
validate_asm_keys!(:azure_source_image)
|
401
|
-
|
402
394
|
validate_params!
|
403
|
-
|
404
395
|
ssh_override_winrm if !is_image_windows?
|
405
|
-
|
406
396
|
Chef::Log.info("creating...")
|
407
|
-
|
408
397
|
config[:azure_dns_name] = get_dns_name(locate_config_value(:azure_dns_name))
|
409
|
-
|
410
|
-
|
411
|
-
config[:azure_vm_name] = locate_config_value(:azure_dns_name)
|
412
|
-
end
|
413
|
-
|
398
|
+
config[:azure_vm_name] = locate_config_value(:azure_dns_name) unless locate_config_value(:azure_vm_name)
|
399
|
+
config[:chef_node_name] = locate_config_value(:azure_vm_name) unless locate_config_value(:chef_node_name)
|
414
400
|
service.create_server(create_server_def)
|
415
401
|
wait_until_virtual_machine_ready()
|
416
402
|
if locate_config_value(:bootstrap_protocol) == 'cloud-api' && locate_config_value(:extended_logs)
|
@@ -423,73 +409,6 @@ class Chef
|
|
423
409
|
bootstrap_exec(server) unless locate_config_value(:bootstrap_protocol) == 'cloud-api'
|
424
410
|
end
|
425
411
|
|
426
|
-
def validate_params!
|
427
|
-
if locate_config_value(:winrm_password) && (locate_config_value(:winrm_password).length <= 6 && locate_config_value(:winrm_password).length >= 72)
|
428
|
-
ui.error("The supplied password must be 6-72 characters long and meet password complexity requirements")
|
429
|
-
exit 1
|
430
|
-
end
|
431
|
-
|
432
|
-
if locate_config_value(:ssh_password) && (locate_config_value(:ssh_password).length <= 6 && locate_config_value(:ssh_password).length >= 72)
|
433
|
-
ui.error("The supplied password must be 6-72 characters long and meet password complexity requirements")
|
434
|
-
exit 1
|
435
|
-
end
|
436
|
-
|
437
|
-
if locate_config_value(:azure_connect_to_existing_dns) && locate_config_value(:azure_vm_name).nil?
|
438
|
-
ui.error("Specify the VM name using --azure-vm-name option, since you are connecting to existing dns")
|
439
|
-
exit 1
|
440
|
-
end
|
441
|
-
|
442
|
-
if locate_config_value(:azure_service_location) && locate_config_value(:azure_affinity_group)
|
443
|
-
ui.error("Cannot specify both --azure-service-location and --azure-affinity-group, use one or the other.")
|
444
|
-
exit 1
|
445
|
-
elsif locate_config_value(:azure_service_location).nil? && locate_config_value(:azure_affinity_group).nil?
|
446
|
-
ui.error("Must specify either --azure-service-location or --azure-affinity-group.")
|
447
|
-
exit 1
|
448
|
-
end
|
449
|
-
|
450
|
-
if locate_config_value(:winrm_authentication_protocol) && ! %w{basic negotiate kerberos}.include?(locate_config_value(:winrm_authentication_protocol))
|
451
|
-
ui.error("Invalid value for --winrm-authentication-protocol option. Use valid protocol values i.e [basic, negotiate, kerberos]")
|
452
|
-
exit 1
|
453
|
-
end
|
454
|
-
|
455
|
-
if !(service.valid_image?(locate_config_value(:azure_source_image)))
|
456
|
-
ui.error("Image provided is invalid")
|
457
|
-
exit 1
|
458
|
-
end
|
459
|
-
|
460
|
-
# Validate join domain requirements.
|
461
|
-
if locate_config_value(:azure_domain_name) || locate_config_value(:azure_domain_user)
|
462
|
-
if locate_config_value(:azure_domain_user).nil? || locate_config_value(:azure_domain_passwd).nil?
|
463
|
-
ui.error("Must specify both --azure-domain-user and --azure-domain-passwd.")
|
464
|
-
exit 1
|
465
|
-
end
|
466
|
-
end
|
467
|
-
|
468
|
-
if locate_config_value(:winrm_transport) == "ssl" && locate_config_value(:thumbprint).nil? && ( locate_config_value(:winrm_ssl_verify_mode).nil? || locate_config_value(:winrm_ssl_verify_mode) == :verify_peer )
|
469
|
-
ui.error("The SSL transport was specified without the --thumbprint option. Specify a thumbprint, or alternatively set the --winrm-ssl-verify-mode option to 'verify_none' to skip verification.")
|
470
|
-
exit 1
|
471
|
-
end
|
472
|
-
|
473
|
-
if locate_config_value(:extended_logs) && locate_config_value(:bootstrap_protocol) != 'cloud-api'
|
474
|
-
ui.error("--extended-logs option works with --bootstrap-protocol cloud-api")
|
475
|
-
exit 1
|
476
|
-
end
|
477
|
-
|
478
|
-
if locate_config_value(:daemon)
|
479
|
-
unless is_image_windows?
|
480
|
-
raise ArgumentError, "The daemon option is only support for Windows nodes."
|
481
|
-
end
|
482
|
-
|
483
|
-
unless locate_config_value(:bootstrap_protocol) == 'cloud-api'
|
484
|
-
raise ArgumentError, "--daemon option works with --bootstrap-protocol cloud-api"
|
485
|
-
end
|
486
|
-
|
487
|
-
unless %w{none service task}.include?(locate_config_value(:daemon))
|
488
|
-
raise ArgumentError, "Invalid value for --daemon option. Use valid daemon values i.e 'none', 'service' and 'task'."
|
489
|
-
end
|
490
|
-
end
|
491
|
-
end
|
492
|
-
|
493
412
|
def create_server_def
|
494
413
|
server_def = {
|
495
414
|
:azure_storage_account => locate_config_value(:azure_storage_account),
|
File without changes
|
File without changes
|
File without changes
|
@@ -1,7 +1,7 @@
|
|
1
1
|
|
2
2
|
# Author:: Aliasgar Batterywala (aliasgar.batterywala@clogeny.com)
|
3
3
|
#
|
4
|
-
# Copyright:: Copyright
|
4
|
+
# Copyright:: Copyright 2009-2018, Chef Software Inc.
|
5
5
|
# License:: Apache License, Version 2.0
|
6
6
|
#
|
7
7
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
@@ -40,7 +40,6 @@ class Chef
|
|
40
40
|
|
41
41
|
def self.included(includer)
|
42
42
|
includer.class_eval do
|
43
|
-
|
44
43
|
deps do
|
45
44
|
require 'readline'
|
46
45
|
require 'chef/json_compat'
|
@@ -50,7 +49,6 @@ class Chef
|
|
50
49
|
:short => "-r RESOURCE_GROUP_NAME",
|
51
50
|
:long => "--azure-resource-group-name RESOURCE_GROUP_NAME",
|
52
51
|
:description => "The Resource Group name."
|
53
|
-
|
54
52
|
end
|
55
53
|
end
|
56
54
|
|
@@ -71,13 +69,13 @@ class Chef
|
|
71
69
|
|
72
70
|
# validates ARM mandatory keys
|
73
71
|
def validate_arm_keys!(*keys)
|
74
|
-
parse_publish_settings_file(locate_config_value(:azure_publish_settings_file))
|
72
|
+
parse_publish_settings_file(locate_config_value(:azure_publish_settings_file)) unless locate_config_value(:azure_publish_settings_file).nil?
|
75
73
|
keys.push(:azure_subscription_id)
|
76
74
|
|
77
|
-
if
|
75
|
+
if azure_cred?
|
78
76
|
validate_azure_login
|
79
77
|
else
|
80
|
-
|
78
|
+
keys.concat([:azure_tenant_id, :azure_client_id, :azure_client_secret])
|
81
79
|
end
|
82
80
|
|
83
81
|
errors = []
|
@@ -92,7 +90,7 @@ class Chef
|
|
92
90
|
end
|
93
91
|
|
94
92
|
def authentication_details
|
95
|
-
if
|
93
|
+
if is_azure_cred?
|
96
94
|
return {:azure_tenant_id => locate_config_value(:azure_tenant_id), :azure_client_id => locate_config_value(:azure_client_id), :azure_client_secret => locate_config_value(:azure_client_secret)}
|
97
95
|
elsif Chef::Platform.windows?
|
98
96
|
token_details = token_details_for_windows()
|
@@ -103,16 +101,13 @@ class Chef
|
|
103
101
|
token_details
|
104
102
|
end
|
105
103
|
|
106
|
-
def
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
def is_WCM_env_var_set?
|
115
|
-
ENV['AZURE_USE_SECURE_TOKEN_STORAGE'].nil? ? false : true
|
104
|
+
def get_azure_cli_version
|
105
|
+
if @azure_version != ""
|
106
|
+
get_version = shell_out!("azure -v || az -v | grep azure-cli", { returns: [0] }).stdout
|
107
|
+
@azure_version = get_version.gsub(/[^0-9.]/, '')
|
108
|
+
end
|
109
|
+
@azure_prefix = @azure_version.to_i < 2 ? "azure" : "az"
|
110
|
+
@azure_version
|
116
111
|
end
|
117
112
|
|
118
113
|
def token_details_for_windows
|
@@ -141,56 +136,49 @@ class Chef
|
|
141
136
|
return false
|
142
137
|
elsif time_difference <= 600 # 600sec = 10min
|
143
138
|
# This is required otherwise a long running command may fail inbetween if the token gets expired.
|
144
|
-
raise "Token will expire within 10 minutes. Please run '
|
139
|
+
raise "Token will expire within 10 minutes. Please run '#{@azure_prefix} login' command"
|
145
140
|
else
|
146
141
|
return true
|
147
142
|
end
|
148
143
|
end
|
149
144
|
|
150
145
|
def refresh_token
|
146
|
+
azure_authentication
|
147
|
+
token_details = Chef::Platform.windows? ? token_details_for_windows() : token_details_for_linux()
|
148
|
+
end
|
149
|
+
|
150
|
+
def azure_authentication
|
151
151
|
begin
|
152
152
|
ui.log("Authenticating...")
|
153
|
-
Mixlib::ShellOut.new("
|
153
|
+
Mixlib::ShellOut.new("#{@azure_prefix} vm show 'knifetest@resourcegroup' testvm", :timeout => 30).run_command
|
154
154
|
rescue Mixlib::ShellOut::CommandTimeout
|
155
155
|
rescue Exception
|
156
|
-
|
156
|
+
raise_azure_status
|
157
157
|
end
|
158
|
-
if Chef::Platform.windows?
|
159
|
-
token_details = token_details_for_windows()
|
160
|
-
else
|
161
|
-
token_details = token_details_for_linux()
|
162
|
-
end
|
163
|
-
token_details
|
164
158
|
end
|
165
159
|
|
166
160
|
def check_token_validity(token_details)
|
167
|
-
|
168
|
-
token_details = refresh_token
|
169
|
-
|
170
|
-
|
161
|
+
unless is_token_valid?(token_details)
|
162
|
+
token_details = refresh_token
|
163
|
+
unless is_token_valid?(token_details)
|
164
|
+
raise_azure_status
|
171
165
|
end
|
172
166
|
end
|
173
167
|
token_details
|
174
168
|
end
|
175
169
|
|
176
170
|
def validate_azure_login
|
177
|
-
err_string = "Please run XPLAT's 'azure login' command OR specify azure_tenant_id, azure_subscription_id, azure_client_id, azure_client_secret in your knife.rb"
|
178
|
-
|
179
|
-
## Older versions of the Azure CLI on Windows stored credentials in a unique way
|
180
|
-
## in Windows Credentails Manager (WCM).
|
181
|
-
## Newer versions use the same pattern across platforms where credentials gets
|
182
|
-
## stored in ~/.azure/accessTokens.json file.
|
183
171
|
if Chef::Platform.windows? && (is_old_xplat? || is_WCM_env_var_set?)
|
184
172
|
# cmdkey command is used for accessing windows credential manager
|
185
173
|
xplat_creds_cmd = Mixlib::ShellOut.new("cmdkey /list | findstr AzureXplatCli")
|
186
174
|
result = xplat_creds_cmd.run_command
|
187
175
|
if result.stdout.nil? || result.stdout.empty?
|
188
|
-
raise
|
176
|
+
raise login_message
|
189
177
|
end
|
190
178
|
else
|
191
179
|
home_dir = File.expand_path('~')
|
192
180
|
if !File.exists?(home_dir + "/.azure/accessTokens.json") || File.size?(home_dir + '/.azure/accessTokens.json') <= 2
|
193
|
-
raise
|
181
|
+
raise login_message
|
194
182
|
end
|
195
183
|
end
|
196
184
|
end
|
@@ -238,20 +226,6 @@ class Chef
|
|
238
226
|
file
|
239
227
|
end
|
240
228
|
|
241
|
-
def pretty_key(key)
|
242
|
-
key.to_s.gsub(/_/, ' ').gsub(/\w+/){ |w| (w =~ /(ssh)|(aws)/i) ? w.upcase : w.capitalize }
|
243
|
-
end
|
244
|
-
|
245
|
-
def is_image_windows?
|
246
|
-
locate_config_value(:azure_image_reference_offer) =~ /WindowsServer.*/
|
247
|
-
end
|
248
|
-
|
249
|
-
def msg_pair(label, value, color=:cyan)
|
250
|
-
if value && !value.to_s.empty?
|
251
|
-
puts "#{ui.color(label, color)}: #{value}"
|
252
|
-
end
|
253
|
-
end
|
254
|
-
|
255
229
|
def msg_server_summary(server)
|
256
230
|
puts "\n\n"
|
257
231
|
if server.provisioningstate == 'Succeeded'
|
@@ -335,6 +309,51 @@ class Chef
|
|
335
309
|
config[:ohai_hints] = format_ohai_hints(locate_config_value(:ohai_hints))
|
336
310
|
validate_ohai_hints if ! locate_config_value(:ohai_hints).casecmp('default').zero?
|
337
311
|
end
|
312
|
+
|
313
|
+
private
|
314
|
+
|
315
|
+
def msg_pair(label, value, color=:cyan)
|
316
|
+
if value && !value.to_s.empty?
|
317
|
+
puts "#{ui.color(label, color)}: #{value}"
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
def pretty_key(key)
|
322
|
+
key.to_s.gsub(/_/, ' ').gsub(/\w+/){ |w| (w =~ /(ssh)|(aws)/i) ? w.upcase : w.capitalize }
|
323
|
+
end
|
324
|
+
|
325
|
+
def is_image_windows?
|
326
|
+
locate_config_value(:azure_image_reference_offer) =~ /WindowsServer.*/
|
327
|
+
end
|
328
|
+
|
329
|
+
def is_azure_cred?
|
330
|
+
locate_config_value(:azure_tenant_id) && locate_config_value(:azure_client_id) && locate_config_value(:azure_client_secret)
|
331
|
+
end
|
332
|
+
|
333
|
+
def azure_cred?
|
334
|
+
locate_config_value(:azure_tenant_id).nil? || locate_config_value(:azure_client_id).nil? || locate_config_value(:azure_client_secret).nil?
|
335
|
+
end
|
336
|
+
|
337
|
+
def is_old_xplat?
|
338
|
+
return true unless @azure_version
|
339
|
+
Gem::Version.new(@azure_version) < Gem::Version.new(XPLAT_VERSION_WITH_WCM_DEPRECATED)
|
340
|
+
end
|
341
|
+
|
342
|
+
def is_WCM_env_var_set?
|
343
|
+
ENV['AZURE_USE_SECURE_TOKEN_STORAGE'].nil? ? false : true
|
344
|
+
end
|
345
|
+
|
346
|
+
def raise_azure_status
|
347
|
+
raise "Token has expired. Please run '#{@azure_prefix} login' command"
|
348
|
+
end
|
349
|
+
|
350
|
+
def login_message
|
351
|
+
## Older versions of the Azure CLI on Windows stored credentials in a unique way
|
352
|
+
## in Windows Credentails Manager (WCM).
|
353
|
+
## Newer versions use the same pattern across platforms where credentials gets
|
354
|
+
## stored in ~/.azure/accessTokens.json file.
|
355
|
+
"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"
|
356
|
+
end
|
338
357
|
end
|
339
358
|
end
|
340
359
|
end
|