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