chef-provisioning-azure 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|