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.
@@ -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