knife-azure 1.1.4 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/README.md +37 -0
- data/lib/azure/ag.rb +97 -0
- data/lib/azure/connection.rb +25 -7
- data/lib/azure/deploy.rb +8 -4
- data/lib/azure/host.rb +7 -4
- data/lib/azure/rest.rb +32 -8
- data/lib/azure/role.rb +85 -55
- data/lib/azure/storageaccount.rb +4 -2
- data/lib/azure/utility.rb +12 -0
- data/lib/azure/vnet.rb +89 -0
- data/lib/chef/knife/azure_ag_create.rb +76 -0
- data/lib/chef/knife/azure_ag_list.rb +51 -0
- data/lib/chef/knife/azure_base.rb +9 -0
- data/lib/chef/knife/azure_server_create.rb +61 -31
- data/lib/chef/knife/azure_server_delete.rb +8 -1
- data/lib/chef/knife/{azure_server_describe.rb → azure_server_show.rb} +22 -14
- data/lib/chef/knife/azure_vnet_create.rb +77 -0
- data/lib/chef/knife/azure_vnet_list.rb +52 -0
- data/lib/knife-azure/version.rb +1 -1
- metadata +28 -43
data/lib/azure/storageaccount.rb
CHANGED
@@ -18,6 +18,7 @@
|
|
18
18
|
|
19
19
|
class Azure
|
20
20
|
class StorageAccounts
|
21
|
+
include AzureUtility
|
21
22
|
def initialize(connection)
|
22
23
|
@connection=connection
|
23
24
|
end
|
@@ -52,8 +53,9 @@ class Azure
|
|
52
53
|
# Look up on cloud and not local cache
|
53
54
|
def exists_on_cloud?(name)
|
54
55
|
ret_val = @connection.query_azure("storageservices/#{name}")
|
55
|
-
|
56
|
-
|
56
|
+
error_code, error_message = error_from_response_xml(ret_val) if ret_val
|
57
|
+
if ret_val.nil? || error_code.length > 0
|
58
|
+
Chef::Log.warn 'Unable to find storage account:' + error_message + ' : ' + error_message if ret_val
|
57
59
|
false
|
58
60
|
else
|
59
61
|
true
|
data/lib/azure/utility.rb
CHANGED
@@ -25,5 +25,17 @@ module AzureUtility
|
|
25
25
|
end
|
26
26
|
content
|
27
27
|
end
|
28
|
+
|
29
|
+
def error_from_response_xml(response_xml)
|
30
|
+
error_code_and_message = ['','']
|
31
|
+
error_node = response_xml.at_css('Error')
|
32
|
+
|
33
|
+
if error_node
|
34
|
+
error_code_and_message[0] = xml_content(error_node, 'Code')
|
35
|
+
error_code_and_message[1] = xml_content(error_node, 'Message')
|
36
|
+
end
|
37
|
+
|
38
|
+
error_code_and_message
|
39
|
+
end
|
28
40
|
end
|
29
41
|
|
data/lib/azure/vnet.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Jeff Mendoza (jeffmendoza@live.com)
|
3
|
+
# Copyright:: Copyright (c) 2013 Opscode, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
class Azure
|
20
|
+
class Vnets
|
21
|
+
def initialize(connection)
|
22
|
+
@connection = connection
|
23
|
+
end
|
24
|
+
|
25
|
+
def load
|
26
|
+
@vnets ||= begin
|
27
|
+
@vnets = {}
|
28
|
+
response = @connection.query_azure('networking/virtualnetwork')
|
29
|
+
response.css('VirtualNetworkSite').each do |vnet|
|
30
|
+
item = Vnet.new(@connection).parse(vnet)
|
31
|
+
@vnets[item.name] = item
|
32
|
+
end
|
33
|
+
@vnets
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def all
|
38
|
+
load.values
|
39
|
+
end
|
40
|
+
|
41
|
+
def exists?(name)
|
42
|
+
load.key?(name)
|
43
|
+
end
|
44
|
+
|
45
|
+
def find(name)
|
46
|
+
load[name]
|
47
|
+
end
|
48
|
+
|
49
|
+
def create(params)
|
50
|
+
ag = Vnet.new(@connection)
|
51
|
+
ag.create(params)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class Azure
|
57
|
+
class Vnet
|
58
|
+
attr_accessor :name, :affinity_group, :state
|
59
|
+
|
60
|
+
def initialize(connection)
|
61
|
+
@connection = connection
|
62
|
+
end
|
63
|
+
|
64
|
+
def parse(image)
|
65
|
+
@name = image.at_css('Name').content
|
66
|
+
@affinity_group = image.at_css('AffinityGroup').content
|
67
|
+
@state = image.at_css('State').content
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
def create(params)
|
72
|
+
response = @connection.query_azure('networking/media')
|
73
|
+
vnets = response.css('VirtualNetworkSite')
|
74
|
+
vnet = nil
|
75
|
+
vnets.each { |vn| vnet = vn if vn['name'] == params[:azure_vnet_name] }
|
76
|
+
add = vnet.nil?
|
77
|
+
vnet = Nokogiri::XML::Node.new('VirtualNetworkSite', response) if add
|
78
|
+
vnet['name'] = params[:azure_vnet_name]
|
79
|
+
vnet['AffinityGroup'] = params[:azure_ag_name]
|
80
|
+
addr_space = Nokogiri::XML::Node.new('AddressSpace', response)
|
81
|
+
addr_prefix = Nokogiri::XML::Node.new('AddressPrefix', response)
|
82
|
+
addr_prefix.content = params[:azure_address_space]
|
83
|
+
addr_space.children = addr_prefix
|
84
|
+
vnet.children = addr_space
|
85
|
+
vnets.last.add_next_sibling(vnet) if add
|
86
|
+
@connection.query_azure('networking/media', 'put', response.to_xml)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Jeff Mendoza (jeffmendoza@live.com)
|
3
|
+
# Copyright:: Copyright (c) 2013 Opscode, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
require File.expand_path('../azure_base', __FILE__)
|
20
|
+
|
21
|
+
class Chef
|
22
|
+
class Knife
|
23
|
+
class AzureAgCreate < Knife
|
24
|
+
include Knife::AzureBase
|
25
|
+
|
26
|
+
banner 'knife azure ag create (options)'
|
27
|
+
|
28
|
+
option :azure_affinity_group,
|
29
|
+
:short => '-a GROUP',
|
30
|
+
:long => '--azure-affinity-group GROUP',
|
31
|
+
:description => 'Specifies new affinity group name.'
|
32
|
+
|
33
|
+
option :azure_ag_desc,
|
34
|
+
:long => '--azure-ag-desc DESC',
|
35
|
+
:description => 'Optional. Description for new affinity group.'
|
36
|
+
|
37
|
+
option :azure_service_location,
|
38
|
+
:short => '-m LOCATION',
|
39
|
+
:long => '--azure-service-location LOCATION',
|
40
|
+
:description => 'Specifies the geographic location - the name of '\
|
41
|
+
'the data center location that is valid for your '\
|
42
|
+
'subscription. Eg: West US, East US, '\
|
43
|
+
'East Asia, Southeast Asia, North Europe, West Europe'
|
44
|
+
|
45
|
+
def run
|
46
|
+
$stdout.sync = true
|
47
|
+
|
48
|
+
Chef::Log.info('validating...')
|
49
|
+
validate!([:azure_subscription_id,
|
50
|
+
:azure_mgmt_cert,
|
51
|
+
:azure_api_host_name,
|
52
|
+
:azure_affinity_group,
|
53
|
+
:azure_service_location])
|
54
|
+
|
55
|
+
params = {
|
56
|
+
azure_ag_name: locate_config_value(:azure_affinity_group),
|
57
|
+
azure_ag_desc: locate_config_value(:azure_ag_desc),
|
58
|
+
azure_location: locate_config_value(:azure_service_location),
|
59
|
+
}
|
60
|
+
|
61
|
+
rsp = connection.ags.create(params)
|
62
|
+
print "\n"
|
63
|
+
if rsp.at_css('Status').nil?
|
64
|
+
if rsp.at_css('Code').nil? || rsp.at_css('Message').nil?
|
65
|
+
puts 'Unknown Error. try -VV'
|
66
|
+
else
|
67
|
+
puts "#{rsp.at_css('Code').content}: "\
|
68
|
+
"#{rsp.at_css('Message').content}"
|
69
|
+
end
|
70
|
+
else
|
71
|
+
puts "Creation status: #{rsp.at_css('Status').content}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Jeff Mendoza (jeffmendoza@live.com)
|
3
|
+
# Copyright:: Copyright (c) 2013 Opscode, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
require File.expand_path('../azure_base', __FILE__)
|
20
|
+
|
21
|
+
class Chef
|
22
|
+
class Knife
|
23
|
+
class AzureAgList < Knife
|
24
|
+
include Knife::AzureBase
|
25
|
+
|
26
|
+
deps { require 'highline' }
|
27
|
+
|
28
|
+
banner 'knife azure ag list (options)'
|
29
|
+
|
30
|
+
def hl
|
31
|
+
@highline ||= HighLine.new
|
32
|
+
end
|
33
|
+
|
34
|
+
def run
|
35
|
+
$stdout.sync = true
|
36
|
+
|
37
|
+
validate!
|
38
|
+
|
39
|
+
cols = %w{Name Location Description}
|
40
|
+
|
41
|
+
the_list = cols.map { |col| ui.color(col, :bold) }
|
42
|
+
connection.ags.all.each do |ag|
|
43
|
+
cols.each { |attr| the_list << ag.send(attr.downcase).to_s }
|
44
|
+
end
|
45
|
+
|
46
|
+
puts "\n"
|
47
|
+
puts hl.list(the_list, :uneven_columns_across, cols.size)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -103,7 +103,16 @@ class Chef
|
|
103
103
|
msg_pair('DNS Name', server.hostedservicename + ".cloudapp.net")
|
104
104
|
msg_pair('VM Name', server.name)
|
105
105
|
msg_pair('Size', server.size)
|
106
|
+
msg_pair('Azure Source Image', locate_config_value(:azure_source_image))
|
107
|
+
msg_pair('Azure Service Location', locate_config_value(:azure_service_location))
|
106
108
|
msg_pair('Public Ip Address', server.publicipaddress)
|
109
|
+
msg_pair('Private Ip Address', server.ipaddress)
|
110
|
+
msg_pair('SSH Port', server.sshport) unless server.sshport.nil?
|
111
|
+
msg_pair('WinRM Port', server.winrmport) unless server.winrmport.nil?
|
112
|
+
msg_pair('TCP Ports', server.tcpports) unless server.tcpports.nil? or server.tcpports.empty?
|
113
|
+
msg_pair('UDP Ports', server.udpports) unless server.udpports.nil? or server.udpports.empty?
|
114
|
+
msg_pair('Environment', locate_config_value(:environment) || '_default')
|
115
|
+
msg_pair('Runlist', locate_config_value(:run_list)) unless locate_config_value(:run_list).empty?
|
107
116
|
puts "\n"
|
108
117
|
end
|
109
118
|
|
@@ -215,41 +215,68 @@ class Chef
|
|
215
215
|
(0...len).map{65.+(rand(25)).chr}.join
|
216
216
|
end
|
217
217
|
|
218
|
-
def wait_until_virtual_machine_ready(
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
polling_attempts = 1
|
227
|
-
until vm_ready || polling_attempts >= max_polling_attempts
|
228
|
-
print '.'
|
229
|
-
sleep retry_interval_in_seconds
|
230
|
-
vm_ready = check_if_virtual_machine_ready()
|
231
|
-
polling_attempts += 1
|
218
|
+
def wait_until_virtual_machine_ready(retry_interval_in_seconds = 30)
|
219
|
+
vm_status = nil
|
220
|
+
puts
|
221
|
+
|
222
|
+
begin
|
223
|
+
vm_status = wait_for_virtual_machine_state(:vm_status_provisioning, 5, retry_interval_in_seconds)
|
224
|
+
if vm_status != :vm_status_ready
|
225
|
+
wait_for_virtual_machine_state(:vm_status_ready, 15, retry_interval_in_seconds)
|
232
226
|
end
|
233
|
-
|
234
|
-
|
235
|
-
|
227
|
+
rescue Exception => e
|
228
|
+
Chef::Log.error("#{e.to_s}")
|
229
|
+
raise 'Verify connectivity to Azure and subscription resource limit compliance (e.g. maximum CPU core limits) and try again.'
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def wait_for_virtual_machine_state(vm_status_goal, total_wait_time_in_minutes, retry_interval_in_seconds)
|
234
|
+
vm_status_ordering = {:vm_status_not_detected => 0, :vm_status_provisioning => 1, :vm_status_ready => 2}
|
235
|
+
vm_status_description = {:vm_status_not_detected => 'any', :vm_status_provisioning => 'provisioning', :vm_status_ready => 'ready'}
|
236
|
+
|
237
|
+
print ui.color("Waiting for virtual machine to reach status '#{vm_status_description[vm_status_goal]}'", :magenta)
|
238
|
+
|
239
|
+
total_wait_time_in_seconds = total_wait_time_in_minutes * 60
|
240
|
+
max_polling_attempts = total_wait_time_in_seconds / retry_interval_in_seconds
|
241
|
+
polling_attempts = 0
|
242
|
+
|
243
|
+
wait_start_time = Time.now
|
244
|
+
|
245
|
+
begin
|
246
|
+
vm_status = get_virtual_machine_status()
|
247
|
+
vm_ready = vm_status_ordering[vm_status] >= vm_status_ordering[vm_status_goal]
|
248
|
+
print '.'
|
249
|
+
sleep retry_interval_in_seconds if !vm_ready
|
250
|
+
polling_attempts += 1
|
251
|
+
end until vm_ready || polling_attempts >= max_polling_attempts
|
252
|
+
|
253
|
+
if ! vm_ready
|
254
|
+
raise Chef::Exceptions::CommandTimeout, "Virtual machine state '#{vm_status_description[vm_status_goal]}' not reached after #{total_wait_time_in_minutes} minutes."
|
255
|
+
end
|
256
|
+
|
257
|
+
elapsed_time_in_minutes = ((Time.now - wait_start_time) / 60).round(2)
|
258
|
+
print ui.color("vm state '#{vm_status_description[vm_status_goal]}' reached after #{elapsed_time_in_minutes} minutes.\n", :cyan)
|
259
|
+
vm_status
|
260
|
+
end
|
261
|
+
|
262
|
+
def get_virtual_machine_status()
|
263
|
+
role = get_role_server()
|
264
|
+
unless role.nil?
|
265
|
+
Chef::Log.debug("Role status is #{role.status.to_s}")
|
266
|
+
if "ReadyRole".eql? role.status.to_s
|
267
|
+
return :vm_status_ready
|
268
|
+
elsif "Provisioning".eql? role.status.to_s
|
269
|
+
return :vm_status_provisioning
|
236
270
|
else
|
237
|
-
|
271
|
+
return :vm_status_not_detected
|
238
272
|
end
|
273
|
+
end
|
274
|
+
return :vm_status_not_detected
|
239
275
|
end
|
240
276
|
|
241
|
-
def
|
277
|
+
def get_role_server()
|
242
278
|
deploy = connection.deploys.queryDeploy(locate_config_value(:azure_dns_name))
|
243
|
-
|
244
|
-
if role.nil?
|
245
|
-
raise "Could not find role - status unknown."
|
246
|
-
end
|
247
|
-
Chef::Log.debug("Role status is #{role.status.to_s}")
|
248
|
-
if "ReadyRole".eql? role.status.to_s
|
249
|
-
return true
|
250
|
-
else
|
251
|
-
return false
|
252
|
-
end
|
279
|
+
deploy.find_role(locate_config_value(:azure_vm_name))
|
253
280
|
end
|
254
281
|
|
255
282
|
def tcp_test_winrm(ip_addr, port)
|
@@ -335,11 +362,14 @@ class Chef
|
|
335
362
|
end
|
336
363
|
|
337
364
|
begin
|
338
|
-
|
339
|
-
fqdn = server.publicipaddress
|
365
|
+
connection.deploys.create(create_server_def)
|
340
366
|
wait_until_virtual_machine_ready()
|
367
|
+
server = get_role_server()
|
368
|
+
fqdn = server.publicipaddress
|
341
369
|
rescue Exception => e
|
342
370
|
Chef::Log.error("Failed to create the server -- exception being rescued: #{e.to_s}")
|
371
|
+
backtrace_message = "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
|
372
|
+
Chef::Log.debug("#{backtrace_message}")
|
343
373
|
cleanup_and_exit(remove_hosted_service_on_failure, remove_storage_service_on_failure)
|
344
374
|
end
|
345
375
|
|
@@ -71,6 +71,12 @@ class Chef
|
|
71
71
|
option :azure_dns_name,
|
72
72
|
:long => "--azure-dns-name NAME",
|
73
73
|
:description => "specifies the DNS name (also known as hosted service name)"
|
74
|
+
|
75
|
+
option :wait,
|
76
|
+
:long => "--wait",
|
77
|
+
:boolean => true,
|
78
|
+
:default => false,
|
79
|
+
:description => "Wait for server deletion. Default is false"
|
74
80
|
|
75
81
|
# Extracted from Chef::Knife.delete_object, because it has a
|
76
82
|
# confirmation step built in... By specifying the '--purge'
|
@@ -127,7 +133,8 @@ class Chef
|
|
127
133
|
:preserve_azure_vhd => locate_config_value(:preserve_azure_vhd),
|
128
134
|
:preserve_azure_dns_name => locate_config_value(:preserve_azure_dns_name),
|
129
135
|
:azure_dns_name => server.hostedservicename,
|
130
|
-
:delete_azure_storage_account => locate_config_value(:delete_azure_storage_account)
|
136
|
+
:delete_azure_storage_account => locate_config_value(:delete_azure_storage_account),
|
137
|
+
:wait => locate_config_value(:wait) })
|
131
138
|
|
132
139
|
puts "\n"
|
133
140
|
ui.warn("Deleted server #{server.name}")
|
@@ -22,11 +22,11 @@ require File.expand_path('../azure_base', __FILE__)
|
|
22
22
|
|
23
23
|
class Chef
|
24
24
|
class Knife
|
25
|
-
class
|
25
|
+
class AzureServerShow < Knife
|
26
26
|
|
27
27
|
include Knife::AzureBase
|
28
28
|
|
29
|
-
banner "knife azure server
|
29
|
+
banner "knife azure server show SERVER [SERVER]"
|
30
30
|
|
31
31
|
def run
|
32
32
|
$stdout.sync = true
|
@@ -38,27 +38,35 @@ class Chef
|
|
38
38
|
puts ''
|
39
39
|
if (role)
|
40
40
|
details = Array.new
|
41
|
-
details << ui.color('Role name', :bold, :
|
41
|
+
details << ui.color('Role name', :bold, :cyan)
|
42
42
|
details << role.name
|
43
|
-
details << ui.color('Status', :bold, :
|
43
|
+
details << ui.color('Status', :bold, :cyan)
|
44
44
|
details << role.status
|
45
|
-
details << ui.color('Size', :bold, :
|
45
|
+
details << ui.color('Size', :bold, :cyan)
|
46
46
|
details << role.size
|
47
|
-
details << ui.color('Hosted service name', :bold, :
|
47
|
+
details << ui.color('Hosted service name', :bold, :cyan)
|
48
48
|
details << role.hostedservicename
|
49
|
-
details << ui.color('Deployment name', :bold, :
|
49
|
+
details << ui.color('Deployment name', :bold, :cyan)
|
50
50
|
details << role.deployname
|
51
|
-
details << ui.color('Host name', :bold, :
|
51
|
+
details << ui.color('Host name', :bold, :cyan)
|
52
52
|
details << role.hostname
|
53
|
-
|
54
|
-
|
53
|
+
unless role.sshport.nil?
|
54
|
+
details << ui.color('SSH port', :bold, :cyan)
|
55
|
+
details << role.sshport
|
56
|
+
end
|
57
|
+
unless role.winrmport.nil?
|
58
|
+
details << ui.color('WinRM port', :bold, :cyan)
|
59
|
+
details << role.winrmport
|
60
|
+
end
|
61
|
+
details << ui.color('Public IP', :bold, :cyan)
|
62
|
+
details << role.publicipaddress
|
55
63
|
puts ui.list(details, :columns_across, 2)
|
56
64
|
if role.tcpports.length > 0 || role.udpports.length > 0
|
57
65
|
details.clear
|
58
|
-
details << ui.color('Ports open', :bold, :
|
59
|
-
details << ui.color('Local port', :bold, :
|
60
|
-
details << ui.color('IP', :bold, :
|
61
|
-
details << ui.color('Public port', :bold, :
|
66
|
+
details << ui.color('Ports open', :bold, :cyan)
|
67
|
+
details << ui.color('Local port', :bold, :cyan)
|
68
|
+
details << ui.color('IP', :bold, :cyan)
|
69
|
+
details << ui.color('Public port', :bold, :cyan)
|
62
70
|
if role.tcpports.length > 0
|
63
71
|
role.tcpports.each do |port|
|
64
72
|
details << 'tcp'
|