chef-provisioning-azure 0.3.2 → 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -1,6 +1,6 @@
1
- require 'bundler'
2
- require 'bundler/gem_tasks'
3
-
4
- task :spec do
5
- require File.expand_path('spec/run')
6
- end
1
+ require 'bundler'
2
+ require 'bundler/gem_tasks'
3
+
4
+ task :spec do
5
+ require File.expand_path('spec/run')
6
+ end
@@ -0,0 +1,21 @@
1
+ require 'chef/provisioning/azure_driver/azure_provider'
2
+
3
+ class Chef
4
+ class Provider
5
+ class AzureCloudService < Chef::Provisioning::AzureDriver::AzureProvider
6
+ action :create do
7
+ Chef::Log.info("Creating AzureCloudService: #{new_resource.name}")
8
+ csms = Azure::CloudServiceManagementService.new
9
+ csms.create_cloud_service(new_resource.name, new_resource.options)
10
+ properties = csms.get_cloud_service_properties(new_resource.name)
11
+ Chef::Log.debug("Properties of #{new_resource.name}: #{properties.inspect}")
12
+ end
13
+
14
+ action :destroy do
15
+ Chef::Log.info("Destroying AzureCloudService: #{new_resource.name}")
16
+ csms = Azure::CloudServiceManagementService.new
17
+ csms.delete_cloud_service(new_resource.name)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,36 @@
1
+ require 'chef/provisioning/azure_driver/azure_provider'
2
+
3
+ class Chef
4
+ class Provider
5
+ class AzureSqlServer < Chef::Provisioning::AzureDriver::AzureProvider
6
+ action :create do
7
+ restore = Azure.config.management_endpoint
8
+ Azure.config.management_endpoint = azure_sql_management_endpoint
9
+ Chef::Log.info("Creating AzureSqlServer: #{new_resource.name}")
10
+ csql = Azure::SqlDatabaseManagementService.new
11
+ Chef::Log.info("#{new_resource.options.inspect}")
12
+ properties = csql.create_server("#{new_resource.options[:login]}", \
13
+ "#{new_resource.options[:password]}", \
14
+ "#{new_resource.options[:location]}")
15
+ server = properties.name
16
+
17
+ new_resource.options[:firewall_rules].each do | rule |
18
+ rule_name = URI::encode(rule[:name])
19
+ range = {
20
+ :start_ip_address => rule[:start_ip_address],
21
+ :end_ip_address => rule[:end_ip_address]
22
+ }
23
+ csql.set_sql_server_firewall_rule(server, rule_name, range)
24
+ end
25
+
26
+ Chef::Log.info("Properties of #{new_resource.name}: #{properties.inspect}")
27
+ Azure.config.management_endpoint = restore
28
+ end
29
+
30
+ action :destroy do
31
+ # not supported
32
+ fail "Destroy not yet implemented on Azure SQL Server using ASM."
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,21 @@
1
+ require 'chef/provisioning/azure_driver/azure_provider'
2
+
3
+ class Chef
4
+ class Provider
5
+ class AzureStorageAccount < Chef::Provisioning::AzureDriver::AzureProvider
6
+ action :create do
7
+ Chef::Log.info("Creating AzureStorageAccount: #{new_resource.name}")
8
+ sms = Azure::StorageManagementService.new
9
+ sms.create_storage_account(new_resource.name, new_resource.options)
10
+ properties = sms.get_storage_account_properties(new_resource.name)
11
+ Chef::Log.debug("Properties of #{new_resource.name}: #{properties.inspect}")
12
+ end
13
+
14
+ action :destroy do
15
+ Chef::Log.info("Destroying AzureStorageAccount: #{new_resource.name}")
16
+ sms = Azure::StorageManagementService.new
17
+ sms.delete_storage_account(new_resource.name)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,3 +1,3 @@
1
- require 'chef/provisioning'
2
- require 'chef/provisioning/azure_driver/driver'
3
- require 'chef/provisioning/azure_driver/resources'
1
+ require 'chef/provisioning'
2
+ require 'chef/provisioning/azure_driver/driver'
3
+ require 'chef/provisioning/azure_driver/resources'
@@ -0,0 +1,33 @@
1
+ require 'chef/provider/lwrp_base'
2
+ require 'chef/provisioning/azure_driver/azure_resource'
3
+ require 'chef/provisioning/chef_provider_action_handler'
4
+ require 'azure'
5
+
6
+ class Chef
7
+ module Provisioning
8
+ module AzureDriver
9
+ class AzureProvider < Chef::Provider::LWRPBase
10
+ use_inline_resources
11
+
12
+ AzureResource = Chef::Provisioning::AzureDriver::AzureResource
13
+
14
+ def azure_sql_management_endpoint
15
+ 'https://management.database.windows.net:8443'
16
+ end
17
+
18
+ def action_handler
19
+ @action_handler ||= Chef::Provisioning::ChefProviderActionHandler.new(self)
20
+ end
21
+
22
+ # All these need to implement whyrun
23
+ def whyrun_supported?
24
+ true
25
+ end
26
+
27
+ def driver
28
+ new_resource.driver
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,51 @@
1
+ require 'chef/resource/lwrp_base'
2
+ require 'chef/provisioning/azure_driver/subscriptions'
3
+
4
+ class Chef
5
+ module Provisioning
6
+ module AzureDriver
7
+ class AzureResource < Chef::Resource::LWRPBase
8
+ def initialize(*args)
9
+ super
10
+ if run_context
11
+ @chef_environment = run_context.cheffish.current_environment
12
+ @chef_server = run_context.cheffish.current_chef_server
13
+ @driver = run_context.chef_provisioning.current_driver
14
+ end
15
+
16
+ config = run_context.chef_provisioning.config
17
+ scheme, account_id = driver.split(':', 2)
18
+
19
+ if account_id.nil? || account_id.empty?
20
+ subscription = Subscriptions.default_subscription(config)
21
+ config = Cheffish::MergedConfig.new({ azure_subscriptions: subscription }, config)
22
+ if !subscription
23
+ raise "Driver #{driver} 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."
24
+ end
25
+ else
26
+ subscription_id = account_id || subscription[:subscription_id]
27
+ subscription = Subscriptions.get_subscription(config, subscription_id)
28
+ end
29
+
30
+ if !subscription
31
+ raise "Driver #{driver} has a subscription ID (#{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."
32
+ else
33
+ Chef::Log.debug("Using subscription: #{subscription.inspect}")
34
+ end
35
+
36
+ Azure.configure do |azure|
37
+ azure.management_certificate = subscription[:management_certificate]
38
+ azure.subscription_id = subscription[:subscription_id]
39
+ azure.management_endpoint = subscription[:management_endpoint]
40
+ end
41
+ end
42
+
43
+ attribute :driver
44
+ attribute :chef_server, kind_of: Hash
45
+ attribute :managed_entry_store, kind_of: Chef::Provisioning::ManagedEntryStore,
46
+ lazy_default: proc { Chef::Provisioning::ChefManagedEntryStore.new(chef_server) }
47
+ attribute :subscription
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,25 +1,25 @@
1
- class Chef
2
- module Provisioning
3
- module AzureDriver
4
- # Represents available options when bootstrapping a host on Azure
5
- # These are used to tell Azure some initial pieces of information
6
- # for building a new VM.
7
- class BootstrapOptions < Chef::Provisioning::BootstrapOptions
8
- # @return [String] The name of the VM
9
- attr_accessor :vm_name
10
-
11
- # @return [String] The VM user
12
- attr_accessor :vm_user
13
-
14
- # @return [String] The identifier for the VM image to use
15
- attr_accessor :image
16
-
17
- # @return [String] the password to use
18
- attr_accessor :password
19
-
20
- # @return [String] The Azure location to store this in
21
- attr_accessor :location
22
- end
23
- end
24
- end
25
- end
1
+ class Chef
2
+ module Provisioning
3
+ module AzureDriver
4
+ # Represents available options when bootstrapping a host on Azure
5
+ # These are used to tell Azure some initial pieces of information
6
+ # for building a new VM.
7
+ class BootstrapOptions < Chef::Provisioning::BootstrapOptions
8
+ # @return [String] The name of the VM
9
+ attr_accessor :vm_name
10
+
11
+ # @return [String] The VM user
12
+ attr_accessor :vm_user
13
+
14
+ # @return [String] The identifier for the VM image to use
15
+ attr_accessor :image
16
+
17
+ # @return [String] the password to use
18
+ attr_accessor :password
19
+
20
+ # @return [String] The Azure location to store this in
21
+ attr_accessor :location
22
+ end
23
+ end
24
+ end
25
+ end
@@ -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,359 +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
- username = machine_spec.location['ssh_username'] || default_ssh_username
232
- tcp_endpoint = vm.tcp_endpoints.select { |tcp| tcp[:name] == 'SSH' }.first
233
- remote_host = tcp_endpoint[:vip]
234
-
235
- # TODO: not this... replace with SSH key ASAP, only for getting this thing going...
236
- ssh_options = {
237
- password: machine_options[:password],
238
- port: tcp_endpoint[:public_port] # use public port from Cloud Service endpoint
239
- }
240
-
241
- options = {}
242
- options[:prefix] = 'sudo ' if machine_spec.location[:sudo] || username != 'root'
243
-
244
- # Enable pty by default
245
- # TODO: why?
246
- options[:ssh_pty_enable] = true
247
- options[:ssh_gateway] ||= machine_spec.location['ssh_gateway']
248
-
249
- Chef::Provisioning::Transport::SSH.new(remote_host, username, ssh_options, options, config)
250
- end
251
-
252
- def create_winrm_transport(machine_spec, machine_options, instance)
253
- winrm_transport_options = machine_options[:bootstrap_options][:winrm_transport]
254
- shared_winrm_options = {
255
- :user => machine_options[:vm_user] || 'localadmin',
256
- :pass => machine_options[:password] # TODO: Replace with encryption
257
- }
258
-
259
- if(winrm_transport_options['https'])
260
- tcp_endpoint = instance.tcp_endpoints.select { |tcp| tcp[:name] == 'PowerShell' }.first
261
- remote_host = tcp_endpoint[:vip]
262
- port = tcp_endpoint[:public_port] || default_winrm_https_port
263
- endpoint = "https://#{remote_host}:#{port}/wsman"
264
- type = :ssl
265
- winrm_options = {
266
- :disable_sspi => winrm_transport_options['https'][:disable_sspi] || false, # default to Negotiate
267
- :basic_auth_only => winrm_transport_options['https'][:basic_auth_only] || false, # disallow Basic auth by default
268
- :no_ssl_peer_verification => winrm_transport_options['https'][:no_ssl_peer_verification] || false #disallow MITM potential by default
269
- }
270
- end
271
-
272
- if(winrm_transport_options['http'])
273
- tcp_endpoint = instance.tcp_endpoints.select { |tcp| tcp[:name] == 'WinRm-Http' }.first
274
- remote_host = tcp_endpoint[:vip]
275
- port = tcp_endpoint[:public_port] || default_winrm_http_port
276
- endpoint = "http://#{remote_host}:#{port}/wsman"
277
- type = :plaintext
278
- winrm_options = {
279
- :disable_sspi => winrm_transport_options['http']['disable_sspi'] || false, # default to Negotiate
280
- :basic_auth_only => winrm_transport_options['http']['basic_auth_only'] || false # disallow Basic auth by default
281
- }
282
- end
283
-
284
- merged_winrm_options = winrm_options.merge(shared_winrm_options)
285
- Chef::Provisioning::Transport::WinRM.new("#{endpoint}", type, merged_winrm_options, {})
286
- end
287
-
288
- def default_winrm_http_port
289
- 5985
290
- end
291
-
292
- def default_winrm_https_port
293
- 5986
294
- end
295
-
296
- def convergence_strategy_for(machine_spec, machine_options)
297
- convergence_options = machine_options[:convergence_options]
298
- # Defaults
299
- unless machine_spec.location
300
- return Chef::Provisioning::ConvergenceStrategy::NoConverge.new(convergence_options, config)
301
- end
302
-
303
- if machine_spec.location['is_windows']
304
- Chef::Provisioning::ConvergenceStrategy::InstallMsi.new(convergence_options, config)
305
- elsif machine_options[:cached_installer]
306
- Chef::Provisioning::ConvergenceStrategy::InstallCached.new(convergence_options, config)
307
- else
308
- Chef::Provisioning::ConvergenceStrategy::InstallSh.new(convergence_options, config)
309
- end
310
- end
311
-
312
- def wait_until_ready(action_handler, machine_spec)
313
- vm = vm_for(machine_spec)
314
-
315
- # If the machine is ready, nothing to do
316
- return if vm.status == 'ReadyRole'
317
-
318
- # Skip if we don't actually need to do anything
319
- return unless action_handler.should_perform_actions
320
-
321
- time_elapsed = 0
322
- sleep_time = 10
323
- max_wait_time = 120
324
-
325
- action_handler.report_progress "waiting for #{machine_spec.name} to be ready ..."
326
- while time_elapsed < 120 && vm.status != 'ReadyRole'
327
- action_handler.report_progress "#{time_elapsed}/#{max_wait_time}s..."
328
- sleep(sleep_time)
329
- time_elapsed += sleep_time
330
- # Azure caches results
331
- vm = vm_for(machine_spec)
332
- end
333
- action_handler.report_progress "#{machine_spec.name} is now ready"
334
- end
335
-
336
- def wait_for_transport(action_handler, machine_spec, machine_options)
337
- vm = vm_for(machine_spec)
338
- transport = transport_for(machine_spec, machine_options, vm)
339
-
340
- return if transport.available?
341
- return unless action_handler.should_perform_actions
342
-
343
- time_elapsed = 0
344
- sleep_time = 10
345
- max_wait_time = 120
346
-
347
- action_handler.report_progress "Waiting for transport on #{machine_spec.name} ..."
348
- while time_elapsed < 120 && !transport.available?
349
- action_handler.report_progress "#{time_elapsed}/#{max_wait_time}s..."
350
- sleep(sleep_time)
351
- time_elapsed += sleep_time
352
- end
353
- action_handler.report_progress "Transport to #{machine_spec.name} is now up!"
354
- end
355
-
356
- end
357
- end
358
- end
359
- 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