chef-provisioning-azure 0.4.0 → 0.5.0
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/Gemfile +5 -0
- data/LICENSE +201 -201
- data/README.md +108 -108
- data/Rakefile +15 -6
- data/chef-provisioning-azure.gemspec +33 -0
- data/lib/chef/provider/azure_cloud_service.rb +21 -21
- data/lib/chef/provider/azure_sql_server.rb +36 -36
- data/lib/chef/provider/azure_storage_account.rb +21 -21
- data/lib/chef/provisioning/azure_driver.rb +3 -3
- data/lib/chef/provisioning/azure_driver/azure_provider.rb +33 -33
- data/lib/chef/provisioning/azure_driver/azure_resource.rb +51 -51
- data/lib/chef/provisioning/azure_driver/bootstrap_options.rb +25 -25
- data/lib/chef/provisioning/azure_driver/constants.rb +34 -34
- data/lib/chef/provisioning/azure_driver/driver.rb +360 -360
- data/lib/chef/provisioning/azure_driver/machine_options.rb +62 -62
- data/lib/chef/provisioning/azure_driver/resources.rb +7 -7
- data/lib/chef/provisioning/azure_driver/subscriptions.rb +222 -222
- data/lib/chef/provisioning/azure_driver/version.rb +8 -8
- data/lib/chef/provisioning/driver_init/azure.rb +3 -3
- data/lib/chef/resource/azure_cloud_service.rb +13 -13
- data/lib/chef/resource/azure_sql_server.rb +13 -13
- data/lib/chef/resource/azure_storage_account.rb +13 -13
- metadata +19 -3
@@ -1,34 +1,34 @@
|
|
1
|
-
class Chef
|
2
|
-
module Provisioning
|
3
|
-
module AzureDriver
|
4
|
-
# A collection of useful Azure-specific constants
|
5
|
-
class Constants
|
6
|
-
# Constants around transport mechanisms available
|
7
|
-
class Transport
|
8
|
-
HTTP = 'http'
|
9
|
-
HTTPS = 'https'
|
10
|
-
end
|
11
|
-
|
12
|
-
# Constants for describing VM sizes in Azure
|
13
|
-
class MachineSize
|
14
|
-
# Put in machine specs here...
|
15
|
-
EXTRASMALL = 'ExtraSmall'
|
16
|
-
# What is this?
|
17
|
-
SMALL = 'Small'
|
18
|
-
# Are these now A2?
|
19
|
-
MEDIUM = 'Medium'
|
20
|
-
LARGE = 'Large'
|
21
|
-
XLARGE = 'ExtraLarge'
|
22
|
-
A5 = 'A5'
|
23
|
-
A6 = 'A6'
|
24
|
-
A7 = 'A7'
|
25
|
-
BASIC_A0 = 'Basic_A0'
|
26
|
-
BASIC_A1 = 'Basic_A1'
|
27
|
-
BASIC_A2 = 'Basic_A2'
|
28
|
-
BASIC_A3 = 'Basic_A3'
|
29
|
-
BASIC_A4 = 'Basic_A4'
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
1
|
+
class Chef
|
2
|
+
module Provisioning
|
3
|
+
module AzureDriver
|
4
|
+
# A collection of useful Azure-specific constants
|
5
|
+
class Constants
|
6
|
+
# Constants around transport mechanisms available
|
7
|
+
class Transport
|
8
|
+
HTTP = 'http'
|
9
|
+
HTTPS = 'https'
|
10
|
+
end
|
11
|
+
|
12
|
+
# Constants for describing VM sizes in Azure
|
13
|
+
class MachineSize
|
14
|
+
# Put in machine specs here...
|
15
|
+
EXTRASMALL = 'ExtraSmall'
|
16
|
+
# What is this?
|
17
|
+
SMALL = 'Small'
|
18
|
+
# Are these now A2?
|
19
|
+
MEDIUM = 'Medium'
|
20
|
+
LARGE = 'Large'
|
21
|
+
XLARGE = 'ExtraLarge'
|
22
|
+
A5 = 'A5'
|
23
|
+
A6 = 'A6'
|
24
|
+
A7 = 'A7'
|
25
|
+
BASIC_A0 = 'Basic_A0'
|
26
|
+
BASIC_A1 = 'Basic_A1'
|
27
|
+
BASIC_A2 = 'Basic_A2'
|
28
|
+
BASIC_A3 = 'Basic_A3'
|
29
|
+
BASIC_A4 = 'Basic_A4'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -1,360 +1,360 @@
|
|
1
|
-
require 'chef/mixin/shell_out'
|
2
|
-
require 'chef/provisioning/driver'
|
3
|
-
require 'chef/provisioning/convergence_strategy/install_cached'
|
4
|
-
require 'chef/provisioning/convergence_strategy/install_sh'
|
5
|
-
require 'chef/provisioning/convergence_strategy/install_msi'
|
6
|
-
require 'chef/provisioning/convergence_strategy/no_converge'
|
7
|
-
require 'chef/provisioning/transport/ssh'
|
8
|
-
require 'chef/provisioning/transport/winrm'
|
9
|
-
require 'chef/provisioning/machine/windows_machine'
|
10
|
-
require 'chef/provisioning/machine/unix_machine'
|
11
|
-
require 'chef/provisioning/machine_spec'
|
12
|
-
|
13
|
-
require 'chef/provisioning/azure_driver/version'
|
14
|
-
require 'chef/provisioning/azure_driver/subscriptions'
|
15
|
-
|
16
|
-
require 'yaml'
|
17
|
-
require 'azure'
|
18
|
-
|
19
|
-
class Chef
|
20
|
-
module Provisioning
|
21
|
-
module AzureDriver
|
22
|
-
# Provisions machines using the Azure SDK
|
23
|
-
class Driver < Chef::Provisioning::Driver
|
24
|
-
attr_reader :region
|
25
|
-
|
26
|
-
# Construct an AzureDriver object from a URL - used to parse existing URL
|
27
|
-
# data to hydrate a driver object.
|
28
|
-
# URL scheme:
|
29
|
-
# azure:subscription_id
|
30
|
-
# @return [AzureDriver] A chef-provisioning Azure driver object for the given URL
|
31
|
-
def self.from_url(driver_url, config)
|
32
|
-
Driver.new(driver_url, config)
|
33
|
-
end
|
34
|
-
|
35
|
-
def self.canonicalize_url(driver_url, config)
|
36
|
-
scheme, account_id = driver_url.split(':', 2)
|
37
|
-
if account_id.nil? || account_id.empty?
|
38
|
-
subscription = Subscriptions.default_subscription(config)
|
39
|
-
if !subscription
|
40
|
-
raise "Driver #{driver_url} did not specify a subscription ID, and no default subscription was found. Have you downloaded the Azure CLI and used `azure account download` and `azure account import` to set up Azure? Alternately, you can set azure_subscriptions to [ { subscription_id: '...', management_credentials: ... }] in your Chef configuration."
|
41
|
-
end
|
42
|
-
config = Cheffish::MergedConfig.new({ azure_subscriptions: subscription }, config)
|
43
|
-
end
|
44
|
-
if subscription
|
45
|
-
[ "#{scheme}:#{subscription[:subscription_id]}", config ]
|
46
|
-
else
|
47
|
-
[ driver_url, config]
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
def initialize(driver_url, config)
|
52
|
-
super
|
53
|
-
scheme, subscription_id = driver_url.split(':', 2)
|
54
|
-
@subscription = Subscriptions.get_subscription(config, subscription_id)
|
55
|
-
if !subscription
|
56
|
-
raise "Driver #{driver_url} has a subscription ID, but the system has no credentials configured for it! If you have access to this subscription, you can use `azure account download` and `azure account import` in the Azure CLI to get the credentials, or set azure_subscriptions to [ { subscription_id: '...', management_credentials: ... }] in your Chef configuration."
|
57
|
-
end
|
58
|
-
|
59
|
-
# TODO make this instantiable so we can have multiple drivers ......
|
60
|
-
Azure.configure do |azure|
|
61
|
-
# Configure these 3 properties to use Storage
|
62
|
-
azure.management_certificate = subscription[:management_certificate]
|
63
|
-
azure.subscription_id = subscription[:subscription_id]
|
64
|
-
azure.management_endpoint = subscription[:management_endpoint]
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
attr_reader :subscription
|
69
|
-
|
70
|
-
# -- Machine methods --
|
71
|
-
|
72
|
-
# Allocate a new machine with the Azure API and start it up, without
|
73
|
-
# blocking to wait for it. Creates any needed resources to get a machine
|
74
|
-
# up and running.
|
75
|
-
# @param (see Chef::Provisioning::Driver#allocate_machine)
|
76
|
-
def allocate_machine(action_handler, machine_spec, machine_options)
|
77
|
-
existing_vm = vm_for(machine_spec)
|
78
|
-
|
79
|
-
# We don't need to do anything if the existing VM is found
|
80
|
-
return if existing_vm
|
81
|
-
|
82
|
-
bootstrap_options = machine_options[:bootstrap_options] || {}
|
83
|
-
bootstrap_options[:vm_size] ||= 'Small'
|
84
|
-
bootstrap_options[:cloud_service_name] ||= 'chefprovisioning'
|
85
|
-
bootstrap_options[:storage_account_name] ||= 'chefprovisioning'
|
86
|
-
bootstrap_options[:location] ||= 'West US'
|
87
|
-
|
88
|
-
location = bootstrap_options[:location]
|
89
|
-
|
90
|
-
machine_spec.location = {
|
91
|
-
'driver_url' => driver_url,
|
92
|
-
'driver_version' => Chef::Provisioning::AzureDriver::VERSION,
|
93
|
-
'allocated_at' => Time.now.utc.to_s,
|
94
|
-
'host_node' => action_handler.host_node,
|
95
|
-
'image_id' => machine_options[:image_id],
|
96
|
-
'location' => location,
|
97
|
-
'cloud_service' => bootstrap_options[:cloud_service_name]
|
98
|
-
}
|
99
|
-
|
100
|
-
image_id = machine_options[:image_id] || default_image_for_location(location)
|
101
|
-
|
102
|
-
Chef::Log.debug "Azure bootstrap options: #{bootstrap_options.inspect}"
|
103
|
-
|
104
|
-
params = {
|
105
|
-
vm_name: machine_spec.name,
|
106
|
-
vm_user: bootstrap_options[:vm_user] || default_ssh_username,
|
107
|
-
image: image_id,
|
108
|
-
# This is only until SSH keys are added
|
109
|
-
password: machine_options[:password],
|
110
|
-
location: location,
|
111
|
-
cloud_service_name: bootstrap_options[:cloud_service_name]
|
112
|
-
}
|
113
|
-
|
114
|
-
# If the cloud service exists already, need to add a role to it - otherwise create virtual machine (including cloud service)
|
115
|
-
cloud_service = azure_cloud_service_service.get_cloud_service(bootstrap_options[:cloud_service_name])
|
116
|
-
existing_deployment = azure_vm_service.list_virtual_machines(bootstrap_options[:cloud_service_name]).any?
|
117
|
-
|
118
|
-
if cloud_service and existing_deployment
|
119
|
-
action_handler.report_progress "Cloud Service #{bootstrap_options[:cloud_service_name]} already exists, adding role."
|
120
|
-
action_handler.report_progress "Creating #{machine_spec.name} with image #{image_id} in #{bootstrap_options[:cloud_service_name]}..."
|
121
|
-
vm = azure_vm_service.add_role(params, bootstrap_options)
|
122
|
-
else
|
123
|
-
action_handler.report_progress "Creating #{machine_spec.name} with image #{image_id} in #{location}..."
|
124
|
-
vm = azure_vm_service.create_virtual_machine(params, bootstrap_options)
|
125
|
-
end
|
126
|
-
|
127
|
-
machine_spec.location['vm_name'] = vm.vm_name
|
128
|
-
machine_spec.location['is_windows'] = (true if vm.os_type == 'Windows') || false
|
129
|
-
action_handler.report_progress "Created #{vm.vm_name} in #{location}..."
|
130
|
-
end
|
131
|
-
|
132
|
-
# (see Chef::Provisioning::Driver#ready_machine)
|
133
|
-
def ready_machine(action_handler, machine_spec, machine_options)
|
134
|
-
vm = vm_for(machine_spec)
|
135
|
-
location = machine_spec.location['location']
|
136
|
-
|
137
|
-
if vm.nil?
|
138
|
-
fail "Machine #{machine_spec.name} does not have a VM associated with it, or the VM does not exist."
|
139
|
-
end
|
140
|
-
|
141
|
-
# TODO: Not sure if this is the right thing to check
|
142
|
-
if vm.status != 'ReadyRole'
|
143
|
-
action_handler.report_progress "Readying #{machine_spec.name} in #{location}..."
|
144
|
-
wait_until_ready(action_handler, machine_spec)
|
145
|
-
wait_for_transport(action_handler, machine_spec, machine_options)
|
146
|
-
else
|
147
|
-
action_handler.report_progress "#{machine_spec.name} already ready in #{location}!"
|
148
|
-
end
|
149
|
-
|
150
|
-
machine_for(machine_spec, machine_options, vm)
|
151
|
-
end
|
152
|
-
|
153
|
-
# (see Chef::Provisioning::Driver#destroy_machine)
|
154
|
-
def destroy_machine(action_handler, machine_spec, machine_options)
|
155
|
-
vm = vm_for(machine_spec)
|
156
|
-
vm_name = machine_spec.name
|
157
|
-
cloud_service = machine_spec.location['cloud_service']
|
158
|
-
|
159
|
-
# Check if we need to proceed
|
160
|
-
return if vm.nil? || vm_name.nil? || cloud_service.nil?
|
161
|
-
|
162
|
-
# Skip if we don't actually need to do anything
|
163
|
-
return unless action_handler.should_perform_actions
|
164
|
-
|
165
|
-
# TODO: action_handler.do |block| ?
|
166
|
-
action_handler.report_progress "Destroying VM #{machine_spec.name}!"
|
167
|
-
azure_vm_service.delete_virtual_machine(vm_name, cloud_service)
|
168
|
-
action_handler.report_progress "Destroyed VM #{machine_spec.name}!"
|
169
|
-
end
|
170
|
-
|
171
|
-
private
|
172
|
-
|
173
|
-
def machine_for(machine_spec, machine_options, vm = nil)
|
174
|
-
vm ||= vm_for(machine_spec)
|
175
|
-
|
176
|
-
fail "VM for node #{machine_spec.name} has not been created!" unless vm
|
177
|
-
|
178
|
-
transport = transport_for(machine_spec, machine_options, vm)
|
179
|
-
convergence_strategy = convergence_strategy_for(machine_spec, machine_options)
|
180
|
-
|
181
|
-
if machine_spec.location['is_windows']
|
182
|
-
Chef::Provisioning::Machine::WindowsMachine.new(machine_spec, transport, convergence_strategy)
|
183
|
-
else
|
184
|
-
Chef::Provisioning::Machine::UnixMachine.new(machine_spec, transport, convergence_strategy)
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
|
-
def azure_vm_service
|
189
|
-
@vm_service ||= Azure::VirtualMachineManagementService.new
|
190
|
-
end
|
191
|
-
|
192
|
-
def azure_cloud_service_service
|
193
|
-
@cloud_service_service ||= Azure::CloudServiceManagementService.new
|
194
|
-
end
|
195
|
-
|
196
|
-
def default_ssh_username
|
197
|
-
'ubuntu'
|
198
|
-
end
|
199
|
-
|
200
|
-
def vm_for(machine_spec)
|
201
|
-
if machine_spec.location && machine_spec.name
|
202
|
-
existing_vms = azure_vm_service.list_virtual_machines
|
203
|
-
existing_vms.select { |vm| vm.vm_name == machine_spec.name }.first
|
204
|
-
else
|
205
|
-
nil
|
206
|
-
end
|
207
|
-
end
|
208
|
-
|
209
|
-
def transport_for(machine_spec, machine_options, vm)
|
210
|
-
if machine_spec.location['is_windows']
|
211
|
-
create_winrm_transport(machine_spec, machine_options, vm)
|
212
|
-
else
|
213
|
-
create_ssh_transport(machine_spec, machine_options, vm)
|
214
|
-
end
|
215
|
-
end
|
216
|
-
|
217
|
-
def default_image_for_location(location)
|
218
|
-
Chef::Log.debug("Choosing default image for region '#{location}'")
|
219
|
-
|
220
|
-
case location
|
221
|
-
when 'East US'
|
222
|
-
when 'Southeast Asia'
|
223
|
-
when 'West US'
|
224
|
-
'b39f27a8b8c64d52b05eac6a62ebad85__Ubuntu-14_04_1-LTS-amd64-server-20140927-en-us-30GB'
|
225
|
-
else
|
226
|
-
raise 'Unsupported location!'
|
227
|
-
end
|
228
|
-
end
|
229
|
-
|
230
|
-
def create_ssh_transport(machine_spec, machine_options, vm)
|
231
|
-
bootstrap_options = machine_options[:bootstrap_options] || {}
|
232
|
-
username = bootstrap_options[:vm_user] || default_ssh_username
|
233
|
-
tcp_endpoint = vm.tcp_endpoints.select { |tcp| tcp[:name] == 'SSH' }.first
|
234
|
-
remote_host = tcp_endpoint[:vip]
|
235
|
-
|
236
|
-
# TODO: not this... replace with SSH key ASAP, only for getting this thing going...
|
237
|
-
ssh_options = {
|
238
|
-
password: machine_options[:password],
|
239
|
-
port: tcp_endpoint[:public_port] # use public port from Cloud Service endpoint
|
240
|
-
}
|
241
|
-
|
242
|
-
options = {}
|
243
|
-
options[:prefix] = 'sudo ' if machine_spec.location[:sudo] || username != 'root'
|
244
|
-
|
245
|
-
# Enable pty by default
|
246
|
-
# TODO: why?
|
247
|
-
options[:ssh_pty_enable] = true
|
248
|
-
options[:ssh_gateway] ||= machine_spec.location['ssh_gateway']
|
249
|
-
|
250
|
-
Chef::Provisioning::Transport::SSH.new(remote_host, username, ssh_options, options, config)
|
251
|
-
end
|
252
|
-
|
253
|
-
def create_winrm_transport(machine_spec, machine_options, instance)
|
254
|
-
winrm_transport_options = machine_options[:bootstrap_options][:winrm_transport]
|
255
|
-
shared_winrm_options = {
|
256
|
-
:user => machine_options[:vm_user] || 'localadmin',
|
257
|
-
:pass => machine_options[:password] # TODO: Replace with encryption
|
258
|
-
}
|
259
|
-
|
260
|
-
if(winrm_transport_options['https'])
|
261
|
-
tcp_endpoint = instance.tcp_endpoints.select { |tcp| tcp[:name] == 'PowerShell' }.first
|
262
|
-
remote_host = tcp_endpoint[:vip]
|
263
|
-
port = tcp_endpoint[:public_port] || default_winrm_https_port
|
264
|
-
endpoint = "https://#{remote_host}:#{port}/wsman"
|
265
|
-
type = :ssl
|
266
|
-
winrm_options = {
|
267
|
-
:disable_sspi => winrm_transport_options['https'][:disable_sspi] || false, # default to Negotiate
|
268
|
-
:basic_auth_only => winrm_transport_options['https'][:basic_auth_only] || false, # disallow Basic auth by default
|
269
|
-
:no_ssl_peer_verification => winrm_transport_options['https'][:no_ssl_peer_verification] || false #disallow MITM potential by default
|
270
|
-
}
|
271
|
-
end
|
272
|
-
|
273
|
-
if(winrm_transport_options['http'])
|
274
|
-
tcp_endpoint = instance.tcp_endpoints.select { |tcp| tcp[:name] == 'WinRm-Http' }.first
|
275
|
-
remote_host = tcp_endpoint[:vip]
|
276
|
-
port = tcp_endpoint[:public_port] || default_winrm_http_port
|
277
|
-
endpoint = "http://#{remote_host}:#{port}/wsman"
|
278
|
-
type = :plaintext
|
279
|
-
winrm_options = {
|
280
|
-
:disable_sspi => winrm_transport_options['http']['disable_sspi'] || false, # default to Negotiate
|
281
|
-
:basic_auth_only => winrm_transport_options['http']['basic_auth_only'] || false # disallow Basic auth by default
|
282
|
-
}
|
283
|
-
end
|
284
|
-
|
285
|
-
merged_winrm_options = winrm_options.merge(shared_winrm_options)
|
286
|
-
Chef::Provisioning::Transport::WinRM.new("#{endpoint}", type, merged_winrm_options, {})
|
287
|
-
end
|
288
|
-
|
289
|
-
def default_winrm_http_port
|
290
|
-
5985
|
291
|
-
end
|
292
|
-
|
293
|
-
def default_winrm_https_port
|
294
|
-
5986
|
295
|
-
end
|
296
|
-
|
297
|
-
def convergence_strategy_for(machine_spec, machine_options)
|
298
|
-
convergence_options = machine_options[:convergence_options]
|
299
|
-
# Defaults
|
300
|
-
unless machine_spec.location
|
301
|
-
return Chef::Provisioning::ConvergenceStrategy::NoConverge.new(convergence_options, config)
|
302
|
-
end
|
303
|
-
|
304
|
-
if machine_spec.location['is_windows']
|
305
|
-
Chef::Provisioning::ConvergenceStrategy::InstallMsi.new(convergence_options, config)
|
306
|
-
elsif machine_options[:cached_installer]
|
307
|
-
Chef::Provisioning::ConvergenceStrategy::InstallCached.new(convergence_options, config)
|
308
|
-
else
|
309
|
-
Chef::Provisioning::ConvergenceStrategy::InstallSh.new(convergence_options, config)
|
310
|
-
end
|
311
|
-
end
|
312
|
-
|
313
|
-
def wait_until_ready(action_handler, machine_spec)
|
314
|
-
vm = vm_for(machine_spec)
|
315
|
-
|
316
|
-
# If the machine is ready, nothing to do
|
317
|
-
return if vm.status == 'ReadyRole'
|
318
|
-
|
319
|
-
# Skip if we don't actually need to do anything
|
320
|
-
return unless action_handler.should_perform_actions
|
321
|
-
|
322
|
-
time_elapsed = 0
|
323
|
-
sleep_time = 10
|
324
|
-
max_wait_time = 120
|
325
|
-
|
326
|
-
action_handler.report_progress "waiting for #{machine_spec.name} to be ready ..."
|
327
|
-
while time_elapsed < 120 && vm.status != 'ReadyRole'
|
328
|
-
action_handler.report_progress "#{time_elapsed}/#{max_wait_time}s..."
|
329
|
-
sleep(sleep_time)
|
330
|
-
time_elapsed += sleep_time
|
331
|
-
# Azure caches results
|
332
|
-
vm = vm_for(machine_spec)
|
333
|
-
end
|
334
|
-
action_handler.report_progress "#{machine_spec.name} is now ready"
|
335
|
-
end
|
336
|
-
|
337
|
-
def wait_for_transport(action_handler, machine_spec, machine_options)
|
338
|
-
vm = vm_for(machine_spec)
|
339
|
-
transport = transport_for(machine_spec, machine_options, vm)
|
340
|
-
|
341
|
-
return if transport.available?
|
342
|
-
return unless action_handler.should_perform_actions
|
343
|
-
|
344
|
-
time_elapsed = 0
|
345
|
-
sleep_time = 10
|
346
|
-
max_wait_time = 120
|
347
|
-
|
348
|
-
action_handler.report_progress "Waiting for transport on #{machine_spec.name} ..."
|
349
|
-
while time_elapsed < 120 && !transport.available?
|
350
|
-
action_handler.report_progress "#{time_elapsed}/#{max_wait_time}s..."
|
351
|
-
sleep(sleep_time)
|
352
|
-
time_elapsed += sleep_time
|
353
|
-
end
|
354
|
-
action_handler.report_progress "Transport to #{machine_spec.name} is now up!"
|
355
|
-
end
|
356
|
-
|
357
|
-
end
|
358
|
-
end
|
359
|
-
end
|
360
|
-
end
|
1
|
+
require 'chef/mixin/shell_out'
|
2
|
+
require 'chef/provisioning/driver'
|
3
|
+
require 'chef/provisioning/convergence_strategy/install_cached'
|
4
|
+
require 'chef/provisioning/convergence_strategy/install_sh'
|
5
|
+
require 'chef/provisioning/convergence_strategy/install_msi'
|
6
|
+
require 'chef/provisioning/convergence_strategy/no_converge'
|
7
|
+
require 'chef/provisioning/transport/ssh'
|
8
|
+
require 'chef/provisioning/transport/winrm'
|
9
|
+
require 'chef/provisioning/machine/windows_machine'
|
10
|
+
require 'chef/provisioning/machine/unix_machine'
|
11
|
+
require 'chef/provisioning/machine_spec'
|
12
|
+
|
13
|
+
require 'chef/provisioning/azure_driver/version'
|
14
|
+
require 'chef/provisioning/azure_driver/subscriptions'
|
15
|
+
|
16
|
+
require 'yaml'
|
17
|
+
require 'azure'
|
18
|
+
|
19
|
+
class Chef
|
20
|
+
module Provisioning
|
21
|
+
module AzureDriver
|
22
|
+
# Provisions machines using the Azure SDK
|
23
|
+
class Driver < Chef::Provisioning::Driver
|
24
|
+
attr_reader :region
|
25
|
+
|
26
|
+
# Construct an AzureDriver object from a URL - used to parse existing URL
|
27
|
+
# data to hydrate a driver object.
|
28
|
+
# URL scheme:
|
29
|
+
# azure:subscription_id
|
30
|
+
# @return [AzureDriver] A chef-provisioning Azure driver object for the given URL
|
31
|
+
def self.from_url(driver_url, config)
|
32
|
+
Driver.new(driver_url, config)
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.canonicalize_url(driver_url, config)
|
36
|
+
scheme, account_id = driver_url.split(':', 2)
|
37
|
+
if account_id.nil? || account_id.empty?
|
38
|
+
subscription = Subscriptions.default_subscription(config)
|
39
|
+
if !subscription
|
40
|
+
raise "Driver #{driver_url} did not specify a subscription ID, and no default subscription was found. Have you downloaded the Azure CLI and used `azure account download` and `azure account import` to set up Azure? Alternately, you can set azure_subscriptions to [ { subscription_id: '...', management_credentials: ... }] in your Chef configuration."
|
41
|
+
end
|
42
|
+
config = Cheffish::MergedConfig.new({ azure_subscriptions: subscription }, config)
|
43
|
+
end
|
44
|
+
if subscription
|
45
|
+
[ "#{scheme}:#{subscription[:subscription_id]}", config ]
|
46
|
+
else
|
47
|
+
[ driver_url, config]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize(driver_url, config)
|
52
|
+
super
|
53
|
+
scheme, subscription_id = driver_url.split(':', 2)
|
54
|
+
@subscription = Subscriptions.get_subscription(config, subscription_id)
|
55
|
+
if !subscription
|
56
|
+
raise "Driver #{driver_url} has a subscription ID, but the system has no credentials configured for it! If you have access to this subscription, you can use `azure account download` and `azure account import` in the Azure CLI to get the credentials, or set azure_subscriptions to [ { subscription_id: '...', management_credentials: ... }] in your Chef configuration."
|
57
|
+
end
|
58
|
+
|
59
|
+
# TODO make this instantiable so we can have multiple drivers ......
|
60
|
+
Azure.configure do |azure|
|
61
|
+
# Configure these 3 properties to use Storage
|
62
|
+
azure.management_certificate = subscription[:management_certificate]
|
63
|
+
azure.subscription_id = subscription[:subscription_id]
|
64
|
+
azure.management_endpoint = subscription[:management_endpoint]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
attr_reader :subscription
|
69
|
+
|
70
|
+
# -- Machine methods --
|
71
|
+
|
72
|
+
# Allocate a new machine with the Azure API and start it up, without
|
73
|
+
# blocking to wait for it. Creates any needed resources to get a machine
|
74
|
+
# up and running.
|
75
|
+
# @param (see Chef::Provisioning::Driver#allocate_machine)
|
76
|
+
def allocate_machine(action_handler, machine_spec, machine_options)
|
77
|
+
existing_vm = vm_for(machine_spec)
|
78
|
+
|
79
|
+
# We don't need to do anything if the existing VM is found
|
80
|
+
return if existing_vm
|
81
|
+
|
82
|
+
bootstrap_options = machine_options[:bootstrap_options] || {}
|
83
|
+
bootstrap_options[:vm_size] ||= 'Small'
|
84
|
+
bootstrap_options[:cloud_service_name] ||= 'chefprovisioning'
|
85
|
+
bootstrap_options[:storage_account_name] ||= 'chefprovisioning'
|
86
|
+
bootstrap_options[:location] ||= 'West US'
|
87
|
+
|
88
|
+
location = bootstrap_options[:location]
|
89
|
+
|
90
|
+
machine_spec.location = {
|
91
|
+
'driver_url' => driver_url,
|
92
|
+
'driver_version' => Chef::Provisioning::AzureDriver::VERSION,
|
93
|
+
'allocated_at' => Time.now.utc.to_s,
|
94
|
+
'host_node' => action_handler.host_node,
|
95
|
+
'image_id' => machine_options[:image_id],
|
96
|
+
'location' => location,
|
97
|
+
'cloud_service' => bootstrap_options[:cloud_service_name]
|
98
|
+
}
|
99
|
+
|
100
|
+
image_id = machine_options[:image_id] || default_image_for_location(location)
|
101
|
+
|
102
|
+
Chef::Log.debug "Azure bootstrap options: #{bootstrap_options.inspect}"
|
103
|
+
|
104
|
+
params = {
|
105
|
+
vm_name: machine_spec.name,
|
106
|
+
vm_user: bootstrap_options[:vm_user] || default_ssh_username,
|
107
|
+
image: image_id,
|
108
|
+
# This is only until SSH keys are added
|
109
|
+
password: machine_options[:password],
|
110
|
+
location: location,
|
111
|
+
cloud_service_name: bootstrap_options[:cloud_service_name]
|
112
|
+
}
|
113
|
+
|
114
|
+
# If the cloud service exists already, need to add a role to it - otherwise create virtual machine (including cloud service)
|
115
|
+
cloud_service = azure_cloud_service_service.get_cloud_service(bootstrap_options[:cloud_service_name])
|
116
|
+
existing_deployment = azure_vm_service.list_virtual_machines(bootstrap_options[:cloud_service_name]).any?
|
117
|
+
|
118
|
+
if cloud_service and existing_deployment
|
119
|
+
action_handler.report_progress "Cloud Service #{bootstrap_options[:cloud_service_name]} already exists, adding role."
|
120
|
+
action_handler.report_progress "Creating #{machine_spec.name} with image #{image_id} in #{bootstrap_options[:cloud_service_name]}..."
|
121
|
+
vm = azure_vm_service.add_role(params, bootstrap_options)
|
122
|
+
else
|
123
|
+
action_handler.report_progress "Creating #{machine_spec.name} with image #{image_id} in #{location}..."
|
124
|
+
vm = azure_vm_service.create_virtual_machine(params, bootstrap_options)
|
125
|
+
end
|
126
|
+
|
127
|
+
machine_spec.location['vm_name'] = vm.vm_name
|
128
|
+
machine_spec.location['is_windows'] = (true if vm.os_type == 'Windows') || false
|
129
|
+
action_handler.report_progress "Created #{vm.vm_name} in #{location}..."
|
130
|
+
end
|
131
|
+
|
132
|
+
# (see Chef::Provisioning::Driver#ready_machine)
|
133
|
+
def ready_machine(action_handler, machine_spec, machine_options)
|
134
|
+
vm = vm_for(machine_spec)
|
135
|
+
location = machine_spec.location['location']
|
136
|
+
|
137
|
+
if vm.nil?
|
138
|
+
fail "Machine #{machine_spec.name} does not have a VM associated with it, or the VM does not exist."
|
139
|
+
end
|
140
|
+
|
141
|
+
# TODO: Not sure if this is the right thing to check
|
142
|
+
if vm.status != 'ReadyRole'
|
143
|
+
action_handler.report_progress "Readying #{machine_spec.name} in #{location}..."
|
144
|
+
wait_until_ready(action_handler, machine_spec)
|
145
|
+
wait_for_transport(action_handler, machine_spec, machine_options)
|
146
|
+
else
|
147
|
+
action_handler.report_progress "#{machine_spec.name} already ready in #{location}!"
|
148
|
+
end
|
149
|
+
|
150
|
+
machine_for(machine_spec, machine_options, vm)
|
151
|
+
end
|
152
|
+
|
153
|
+
# (see Chef::Provisioning::Driver#destroy_machine)
|
154
|
+
def destroy_machine(action_handler, machine_spec, machine_options)
|
155
|
+
vm = vm_for(machine_spec)
|
156
|
+
vm_name = machine_spec.name
|
157
|
+
cloud_service = machine_spec.location['cloud_service']
|
158
|
+
|
159
|
+
# Check if we need to proceed
|
160
|
+
return if vm.nil? || vm_name.nil? || cloud_service.nil?
|
161
|
+
|
162
|
+
# Skip if we don't actually need to do anything
|
163
|
+
return unless action_handler.should_perform_actions
|
164
|
+
|
165
|
+
# TODO: action_handler.do |block| ?
|
166
|
+
action_handler.report_progress "Destroying VM #{machine_spec.name}!"
|
167
|
+
azure_vm_service.delete_virtual_machine(vm_name, cloud_service)
|
168
|
+
action_handler.report_progress "Destroyed VM #{machine_spec.name}!"
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
def machine_for(machine_spec, machine_options, vm = nil)
|
174
|
+
vm ||= vm_for(machine_spec)
|
175
|
+
|
176
|
+
fail "VM for node #{machine_spec.name} has not been created!" unless vm
|
177
|
+
|
178
|
+
transport = transport_for(machine_spec, machine_options, vm)
|
179
|
+
convergence_strategy = convergence_strategy_for(machine_spec, machine_options)
|
180
|
+
|
181
|
+
if machine_spec.location['is_windows']
|
182
|
+
Chef::Provisioning::Machine::WindowsMachine.new(machine_spec, transport, convergence_strategy)
|
183
|
+
else
|
184
|
+
Chef::Provisioning::Machine::UnixMachine.new(machine_spec, transport, convergence_strategy)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def azure_vm_service
|
189
|
+
@vm_service ||= Azure::VirtualMachineManagementService.new
|
190
|
+
end
|
191
|
+
|
192
|
+
def azure_cloud_service_service
|
193
|
+
@cloud_service_service ||= Azure::CloudServiceManagementService.new
|
194
|
+
end
|
195
|
+
|
196
|
+
def default_ssh_username
|
197
|
+
'ubuntu'
|
198
|
+
end
|
199
|
+
|
200
|
+
def vm_for(machine_spec)
|
201
|
+
if machine_spec.location && machine_spec.name
|
202
|
+
existing_vms = azure_vm_service.list_virtual_machines
|
203
|
+
existing_vms.select { |vm| vm.vm_name == machine_spec.name }.first
|
204
|
+
else
|
205
|
+
nil
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def transport_for(machine_spec, machine_options, vm)
|
210
|
+
if machine_spec.location['is_windows']
|
211
|
+
create_winrm_transport(machine_spec, machine_options, vm)
|
212
|
+
else
|
213
|
+
create_ssh_transport(machine_spec, machine_options, vm)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def default_image_for_location(location)
|
218
|
+
Chef::Log.debug("Choosing default image for region '#{location}'")
|
219
|
+
|
220
|
+
case location
|
221
|
+
when 'East US'
|
222
|
+
when 'Southeast Asia'
|
223
|
+
when 'West US'
|
224
|
+
'b39f27a8b8c64d52b05eac6a62ebad85__Ubuntu-14_04_1-LTS-amd64-server-20140927-en-us-30GB'
|
225
|
+
else
|
226
|
+
raise 'Unsupported location!'
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def create_ssh_transport(machine_spec, machine_options, vm)
|
231
|
+
bootstrap_options = machine_options[:bootstrap_options] || {}
|
232
|
+
username = bootstrap_options[:vm_user] || default_ssh_username
|
233
|
+
tcp_endpoint = vm.tcp_endpoints.select { |tcp| tcp[:name] == 'SSH' }.first
|
234
|
+
remote_host = tcp_endpoint[:vip]
|
235
|
+
|
236
|
+
# TODO: not this... replace with SSH key ASAP, only for getting this thing going...
|
237
|
+
ssh_options = {
|
238
|
+
password: machine_options[:password],
|
239
|
+
port: tcp_endpoint[:public_port] # use public port from Cloud Service endpoint
|
240
|
+
}
|
241
|
+
|
242
|
+
options = {}
|
243
|
+
options[:prefix] = 'sudo ' if machine_spec.location[:sudo] || username != 'root'
|
244
|
+
|
245
|
+
# Enable pty by default
|
246
|
+
# TODO: why?
|
247
|
+
options[:ssh_pty_enable] = true
|
248
|
+
options[:ssh_gateway] ||= machine_spec.location['ssh_gateway']
|
249
|
+
|
250
|
+
Chef::Provisioning::Transport::SSH.new(remote_host, username, ssh_options, options, config)
|
251
|
+
end
|
252
|
+
|
253
|
+
def create_winrm_transport(machine_spec, machine_options, instance)
|
254
|
+
winrm_transport_options = machine_options[:bootstrap_options][:winrm_transport]
|
255
|
+
shared_winrm_options = {
|
256
|
+
:user => machine_options[:vm_user] || 'localadmin',
|
257
|
+
:pass => machine_options[:password] # TODO: Replace with encryption
|
258
|
+
}
|
259
|
+
|
260
|
+
if(winrm_transport_options['https'])
|
261
|
+
tcp_endpoint = instance.tcp_endpoints.select { |tcp| tcp[:name] == 'PowerShell' }.first
|
262
|
+
remote_host = tcp_endpoint[:vip]
|
263
|
+
port = tcp_endpoint[:public_port] || default_winrm_https_port
|
264
|
+
endpoint = "https://#{remote_host}:#{port}/wsman"
|
265
|
+
type = :ssl
|
266
|
+
winrm_options = {
|
267
|
+
:disable_sspi => winrm_transport_options['https'][:disable_sspi] || false, # default to Negotiate
|
268
|
+
:basic_auth_only => winrm_transport_options['https'][:basic_auth_only] || false, # disallow Basic auth by default
|
269
|
+
:no_ssl_peer_verification => winrm_transport_options['https'][:no_ssl_peer_verification] || false #disallow MITM potential by default
|
270
|
+
}
|
271
|
+
end
|
272
|
+
|
273
|
+
if(winrm_transport_options['http'])
|
274
|
+
tcp_endpoint = instance.tcp_endpoints.select { |tcp| tcp[:name] == 'WinRm-Http' }.first
|
275
|
+
remote_host = tcp_endpoint[:vip]
|
276
|
+
port = tcp_endpoint[:public_port] || default_winrm_http_port
|
277
|
+
endpoint = "http://#{remote_host}:#{port}/wsman"
|
278
|
+
type = :plaintext
|
279
|
+
winrm_options = {
|
280
|
+
:disable_sspi => winrm_transport_options['http']['disable_sspi'] || false, # default to Negotiate
|
281
|
+
:basic_auth_only => winrm_transport_options['http']['basic_auth_only'] || false # disallow Basic auth by default
|
282
|
+
}
|
283
|
+
end
|
284
|
+
|
285
|
+
merged_winrm_options = winrm_options.merge(shared_winrm_options)
|
286
|
+
Chef::Provisioning::Transport::WinRM.new("#{endpoint}", type, merged_winrm_options, {})
|
287
|
+
end
|
288
|
+
|
289
|
+
def default_winrm_http_port
|
290
|
+
5985
|
291
|
+
end
|
292
|
+
|
293
|
+
def default_winrm_https_port
|
294
|
+
5986
|
295
|
+
end
|
296
|
+
|
297
|
+
def convergence_strategy_for(machine_spec, machine_options)
|
298
|
+
convergence_options = machine_options[:convergence_options]
|
299
|
+
# Defaults
|
300
|
+
unless machine_spec.location
|
301
|
+
return Chef::Provisioning::ConvergenceStrategy::NoConverge.new(convergence_options, config)
|
302
|
+
end
|
303
|
+
|
304
|
+
if machine_spec.location['is_windows']
|
305
|
+
Chef::Provisioning::ConvergenceStrategy::InstallMsi.new(convergence_options, config)
|
306
|
+
elsif machine_options[:cached_installer]
|
307
|
+
Chef::Provisioning::ConvergenceStrategy::InstallCached.new(convergence_options, config)
|
308
|
+
else
|
309
|
+
Chef::Provisioning::ConvergenceStrategy::InstallSh.new(convergence_options, config)
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
def wait_until_ready(action_handler, machine_spec)
|
314
|
+
vm = vm_for(machine_spec)
|
315
|
+
|
316
|
+
# If the machine is ready, nothing to do
|
317
|
+
return if vm.status == 'ReadyRole'
|
318
|
+
|
319
|
+
# Skip if we don't actually need to do anything
|
320
|
+
return unless action_handler.should_perform_actions
|
321
|
+
|
322
|
+
time_elapsed = 0
|
323
|
+
sleep_time = 10
|
324
|
+
max_wait_time = 120
|
325
|
+
|
326
|
+
action_handler.report_progress "waiting for #{machine_spec.name} to be ready ..."
|
327
|
+
while time_elapsed < 120 && vm.status != 'ReadyRole'
|
328
|
+
action_handler.report_progress "#{time_elapsed}/#{max_wait_time}s..."
|
329
|
+
sleep(sleep_time)
|
330
|
+
time_elapsed += sleep_time
|
331
|
+
# Azure caches results
|
332
|
+
vm = vm_for(machine_spec)
|
333
|
+
end
|
334
|
+
action_handler.report_progress "#{machine_spec.name} is now ready"
|
335
|
+
end
|
336
|
+
|
337
|
+
def wait_for_transport(action_handler, machine_spec, machine_options)
|
338
|
+
vm = vm_for(machine_spec)
|
339
|
+
transport = transport_for(machine_spec, machine_options, vm)
|
340
|
+
|
341
|
+
return if transport.available?
|
342
|
+
return unless action_handler.should_perform_actions
|
343
|
+
|
344
|
+
time_elapsed = 0
|
345
|
+
sleep_time = 10
|
346
|
+
max_wait_time = 120
|
347
|
+
|
348
|
+
action_handler.report_progress "Waiting for transport on #{machine_spec.name} ..."
|
349
|
+
while time_elapsed < 120 && !transport.available?
|
350
|
+
action_handler.report_progress "#{time_elapsed}/#{max_wait_time}s..."
|
351
|
+
sleep(sleep_time)
|
352
|
+
time_elapsed += sleep_time
|
353
|
+
end
|
354
|
+
action_handler.report_progress "Transport to #{machine_spec.name} is now up!"
|
355
|
+
end
|
356
|
+
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|