chef-metal-fogsphere 0.1.0.alpha.3 → 0.1.0.alpha.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bded9aeef55e520b6ba1327ca33e5eb587e6a7e3
4
+ data.tar.gz: fa4cb785cbea11310642c04dced35248a907a27e
5
+ SHA512:
6
+ metadata.gz: c7090eb59674a2d1d143ad954d5768db3b560a59a5c5fff298d13f5608a8f37b89139561a3f612a574e5ed267239c5e28fc8d08d1677c8ad38849923301c22be
7
+ data.tar.gz: e27dd63106852b395ca304b5e43b0eba66f2de0cbcb83d71eed7592cc27f2d47af725f3ec713503059a85dc0ed5046f23580f442bd6d05d6a9648ef5efd38cac
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |gem|
6
6
  gem.name = "chef-metal-fogsphere"
7
- gem.version = '0.1.0.alpha.3'
7
+ gem.version = '0.1.0.alpha.4'
8
8
  gem.license = 'Apache 2.0'
9
9
  gem.authors = ["Matt Wrock"]
10
10
  gem.email = ["matt@centurylinkcloud.com"]
@@ -16,5 +16,5 @@ Gem::Specification.new do |gem|
16
16
  gem.require_paths = ["lib"]
17
17
 
18
18
  gem.add_dependency 'rbvmomi', '~> 1.5.1'
19
- gem.add_dependency 'chef-metal'
19
+ gem.add_dependency 'chef-metal', '0.11.beta.2'
20
20
  end
@@ -0,0 +1,3 @@
1
+ require 'clc_driver'
2
+
3
+ ChefMetal.register_driver_class("vsphere", ChefMetalVsphere::VsphereDriver)
@@ -1,12 +1,15 @@
1
- require 'chef_metal'
2
- require 'clc_provisioner'
3
-
4
- class Chef
5
- module DSL
6
- module Recipe
7
- def with_vsphere_provisioner(options = {}, &block)
8
- run_context.chef_metal.with_provisioner(ChefMetalVsphere::VsphereProvisioner.new({ :provider => 'vsphere' }.merge(options)), &block)
9
- end
10
- end
11
- end
1
+ require 'clc_driver'
2
+ require 'chef_metal'
3
+
4
+ class Chef
5
+ module DSL
6
+ module Recipe
7
+ def with_vsphere_driver(driver_options = nil, &block)
8
+ config = Cheffish::MergedConfig.new({ :driver_options => driver_options }, run_context.config)
9
+ config, id = ChefMetalVsphere::VsphereDriver.compute_options_for(nil, config)
10
+ config = ChefMetal.config_for_url("vsphere:#{id}", config)
11
+ run_context.chef_metal.with_driver(ChefMetalVsphere::VsphereDriver.new("vsphere:#{id}", config), &block)
12
+ end
13
+ end
14
+ end
12
15
  end
data/lib/clc_driver.rb ADDED
@@ -0,0 +1,390 @@
1
+ require 'chef_metal/driver'
2
+ require 'chef_metal/machine/windows_machine'
3
+ require 'chef_metal/machine/unix_machine'
4
+ require 'chef_metal/convergence_strategy/install_msi'
5
+ require 'chef_metal/convergence_strategy/install_cached'
6
+ require 'chef_metal/transport/ssh'
7
+ require 'fog'
8
+ require 'fog/core'
9
+ require 'fog/compute'
10
+ require 'fog/aws'
11
+
12
+ module ChefMetalVsphere
13
+ # Provisions machines in vagrant.
14
+ class VsphereDriver < ChefMetal::Driver
15
+ include Chef::Mixin::ShellOut
16
+
17
+ DEFAULT_OPTIONS = {
18
+ :create_timeout => 600,
19
+ :start_timeout => 600,
20
+ :ssh_timeout => 20
21
+ } unless defined? DEFAULT_OPTIONS
22
+
23
+ def self.from_url(driver_url, config)
24
+ id = driver_url.split(':')[2]
25
+ config, id = compute_options_for(id, config)
26
+ VsphereDriver.new("vsphere:#{id}", config)
27
+ end
28
+
29
+ def self.compute_options_for(id, config)
30
+ compute_options = config[:driver_options] || {}
31
+ compute_options[:provider] = 'vsphere'
32
+ new_config = { :driver_options => { :compute_options => compute_options }}
33
+
34
+ # Set the identifier from the URL
35
+ if id && id != ''
36
+ compute_options[:vsphere_server] = id
37
+ end
38
+
39
+ config = Cheffish::MergedConfig.new(new_config, config)
40
+
41
+ id = compute_options[:vsphere_server]
42
+
43
+ [ config, id ]
44
+ end
45
+
46
+ # Create a new fog provisioner.
47
+ #
48
+ # ## Parameters
49
+ # compute_options - hash of options to be passed to Fog::Compute.new
50
+ # Special options:
51
+ # - :base_bootstrap_options is merged with bootstrap_options in acquire_machine
52
+ # to present the full set of bootstrap options. Write down any bootstrap_options
53
+ # you intend to apply universally here.
54
+ # - :aws_credentials is an AWS CSV file (created with Download Credentials)
55
+ # containing your aws key information. If you don not specify aws_access_key_id
56
+ # and aws_secret_access_key explicitly, the first line from this file
57
+ # will be used. You may pass a Cheffish::AWSCredentials object.
58
+ # - :create_timeout - the time to wait for the instance to boot to ssh (defaults to 600)
59
+ # - :start_timeout - the time to wait for the instance to start (defaults to 600)
60
+ def initialize(driver_url, config)
61
+ super(driver_url, config)
62
+ end
63
+
64
+ def compute_options
65
+ driver_options[:compute_options] || {}
66
+ end
67
+
68
+ def provider
69
+ compute_options[:provider]
70
+ end
71
+
72
+ # Inflate a provisioner from node_json information; we don't want to force the
73
+ # driver to figure out what the provisioner really needs, since it varies
74
+ # from provisioner to provisioner.
75
+ #
76
+ # ## Parameters
77
+ # node_json - node_json to inflate the provisioner for
78
+ #
79
+ # returns a FogProvisioner
80
+ # TODO: def self.inflate(node_json)
81
+ # right now, not implemented, will raise error from base class until overridden
82
+
83
+ # Acquire a machine, generally by provisioning it. Returns a Machine
84
+ # object pointing at the machine, allowing useful actions like setup,
85
+ # converge, execute, file and directory. The Machine object will have a
86
+ # "node_json" property which must be saved to the server (if it is any
87
+ # different from the original node_json object).
88
+ #
89
+ # ## Parameters
90
+ # action_handler - the action_handler object that is calling this method; this
91
+ # is generally a action_handler, but could be anything that can support the
92
+ # ChefMetal::ActionHandler interface (i.e., in the case of the test
93
+ # kitchen metal driver for acquiring and destroying VMs; see the base
94
+ # class for what needs providing).
95
+ # node_json - node_json object (deserialized json) representing this machine. If
96
+ # the node_json has a provisioner_options hash in it, these will be used
97
+ # instead of options provided by the provisioner. TODO compare and
98
+ # fail if different?
99
+ # node_json will have node_json['normal']['provisioner_options'] in it with any options.
100
+ # It is a hash with this format:
101
+ #
102
+ # -- provisioner_url: fog:<relevant_fog_options>
103
+ # -- bootstrap_options: hash of options to pass to compute.servers.create
104
+ # -- is_windows: true if windows. TODO detect this from ami?
105
+ # -- create_timeout - the time to wait for the instance to boot to ssh (defaults to 600)
106
+ # -- start_timeout - the time to wait for the instance to start (defaults to 600)
107
+ # -- ssh_timeout - the time to wait for ssh to be available if the instance is detected as up (defaults to 20)
108
+ #
109
+ # Example bootstrap_options for ec2:
110
+ # 'bootstrap_options' => {
111
+ # 'image_id' =>'ami-311f2b45',
112
+ # 'flavor_id' =>'t1.micro',
113
+ # 'key_name' => 'key-pair-name'
114
+ # }
115
+ #
116
+ # node_json['normal']['provisioner_output'] will be populated with information
117
+ # about the created machine. For vagrant, it is a hash with this
118
+ # format:
119
+ #
120
+ # -- provisioner_url: fog:<relevant_fog_options>
121
+ # -- server_id: the ID of the server so it can be found again
122
+ #
123
+ def allocate_machine(action_handler, machine_spec, machine_options)
124
+ if machine_spec.location
125
+ if machine_spec.location['driver_url'] != driver_url
126
+ raise "Switching a machine's driver from #{machine_spec.location['driver_url']} to #{driver_url} for is not currently supported! Use machine :destroy and then re-create the machine on the new driver."
127
+ end
128
+
129
+ server = server_for(machine_spec)
130
+ if server
131
+ return server
132
+ else
133
+ Chef::Log.warn "Machine #{machine_spec.name} (#{machine_spec.location['server_id']} on #{driver_url}) no longer exists. Recreating ..."
134
+ end
135
+ end
136
+
137
+ bootstrap_options = bootstrap_options_for(machine_spec, machine_options)
138
+
139
+ description = [ "creating machine #{machine_spec.name} on #{driver_url}" ]
140
+ bootstrap_options.each_pair { |key,value| description << " #{key}: #{value.inspect}" }
141
+ server = nil
142
+ action_handler.report_progress description
143
+
144
+ clone_results = compute.vm_clone(convert_to_strings(bootstrap_options))
145
+ server = compute.servers.get(clone_results['new_vm']['id'])
146
+ if(bootstrap_options[:additional_disk_size_gb] && bootstrap_options[:additional_disk_size_gb].is_a?(Integer) && bootstrap_options[:additional_disk_size_gb] > 0)
147
+ volume_options = {
148
+ 'server_id' => server.id,
149
+ 'datastore' => bootstrap_options[:datastore],
150
+ 'size_gb' => bootstrap_options[:additional_disk_size_gb]
151
+ }
152
+ volume = compute.volumes.create(volume_options)
153
+ end
154
+
155
+ machine_spec.location = {
156
+ 'driver_url' => driver_url,
157
+ 'driver_version' => '0.0.1',
158
+ 'server_id' => server.id,
159
+ 'allocated_at' => Time.now.utc.to_s
160
+ }
161
+ machine_spec.location['key_name'] = bootstrap_options[:key_name] if bootstrap_options[:key_name]
162
+ %w(is_windows ssh_username sudo use_private_ip_for_ssh ssh_gateway).each do |key|
163
+ machine_spec.location[key] = machine_options[key.to_sym] if machine_options[key.to_sym]
164
+ end
165
+
166
+ action_handler.performed_action "machine #{machine_spec.name} created as #{server.id} on #{driver_url}"
167
+ server
168
+ end
169
+
170
+ def ready_machine(action_handler, machine_spec, machine_options)
171
+ server = server_for(machine_spec)
172
+ if server.nil?
173
+ raise "Machine #{machine_spec.name} does not have a server associated with it, or server does not exist."
174
+ end
175
+
176
+ wait_until_ready(action_handler, machine_spec, machine_options, server)
177
+
178
+ is_static = false
179
+ bootstrap_options = bootstrap_options_for(machine_spec, machine_options)
180
+ if has_static_ip(bootstrap_options)
181
+ Chef::Log.info "waiting for customizations to complete"
182
+ sleep(30)
183
+ Chef::Log.info "rebooting..."
184
+ server.reboot
185
+ is_static = true
186
+ end
187
+
188
+ begin
189
+ wait_for_transport(action_handler, machine_spec, machine_options, server)
190
+ rescue Fog::Errors::TimeoutError
191
+ # Only ever reboot once, and only if it's been less than 10 minutes since we stopped waiting
192
+ if machine_spec.location['started_at'] || remaining_wait_time(machine_spec, machine_options) < -(10*60)
193
+ raise
194
+ else
195
+ # Sometimes (on EC2) the machine comes up but gets stuck or has
196
+ # some other problem. If this is the case, we restart the server
197
+ # to unstick it. Reboot covers a multitude of sins.
198
+ Chef::Log.warn "Machine #{machine_spec.name} (#{server.id} on #{driver_url}) was started but SSH did not come up. Rebooting machine in an attempt to unstick it ..."
199
+ restart_server(action_handler, machine_spec, server)
200
+ wait_until_ready(action_handler, machine_spec, machine_options, server)
201
+ wait_for_transport(action_handler, machine_spec, machine_options, server)
202
+ end
203
+ end
204
+
205
+ machine = machine_for(machine_spec, machine_options, server)
206
+
207
+ if is_static
208
+ interfaces_file = "/etc/network/interfaces"
209
+ nameservers = "#{bootstrap_options[:primary_dns]} #{bootstrap_options[:secondary_dns]}"
210
+ machine.execute(action_handler, "if ! cat #{interfaces_file} | grep -q dns-search ; then echo 'dns-search #{machine_spec.name}' >> #{interfaces_file} ; fi")
211
+ machine.execute(action_handler, "if ! cat #{interfaces_file} | grep -q dns-nameservers ; then echo 'dns-nameservers #{nameservers}' >> #{interfaces_file} ; fi")
212
+ machine.execute(action_handler, 'echo "ACTION=="add", SUBSYSTEM=="cpu", ATTR{online}="1"" > /etc/udev/rules.d/99-vmware-cpuhotplug-udev.rules')
213
+ machine.execute(action_handler, '/etc/init.d/networking restart')
214
+ end
215
+
216
+ machine
217
+ end
218
+
219
+ def restart_server(action_handler, machine_spec, server)
220
+ action_handler.perform_action "restart machine #{machine_spec.name} (#{server.id} on #{driver_url})" do
221
+ server.reboot
222
+ machine_spec.location['started_at'] = Time.now.utc.to_s
223
+ end
224
+ end
225
+
226
+ def has_static_ip(bootstrap_options)
227
+ if bootstrap_options.has_key?(:customization_spec)
228
+ bootstrap_options = bootstrap_options[:customization_spec]
229
+ if bootstrap_options.has_key?('ipsettings')
230
+ bootstrap_options = bootstrap_options['ipsettings']
231
+ if bootstrap_options.has_key?('ip')
232
+ return true
233
+ end
234
+ end
235
+ end
236
+ false
237
+ end
238
+
239
+ def convert_to_strings(objay)
240
+ if objay.kind_of?(Array)
241
+ objay.map { |v| convert_to_strings(v) }
242
+ elsif objay.kind_of?(Hash)
243
+ Hash[objay.map { |(k, v)| [k.to_s, convert_to_strings(v)] }]
244
+ else
245
+ objay
246
+ end
247
+ end
248
+
249
+ # Connect to machine without acquiring it
250
+ def connect_to_machine(machine_spec, machine_options)
251
+ machine_for(machine_spec, machine_options)
252
+ end
253
+
254
+
255
+ def destroy_machine(action_handler, machine_spec, machine_options)
256
+ server = server_for(machine_spec)
257
+ if server
258
+ action_handler.perform_action "destroy machine #{machine_spec.name} (#{machine_spec.location['server_id']} at #{driver_url})" do
259
+ server.destroy
260
+ machine_spec.location = nil
261
+ end
262
+ end
263
+ strategy = convergence_strategy_for(machine_spec, machine_options)
264
+ strategy.cleanup_convergence(action_handler, machine_spec)
265
+ end
266
+
267
+ def stop_machine(action_handler, machine_spec, machine_options)
268
+ server = server_for(machine_spec)
269
+ if server
270
+ action_handler.perform_action "stop machine #{machine_spec.name} (#{server.id} at #{driver_url})" do
271
+ server.stop
272
+ end
273
+ end
274
+ end
275
+
276
+ def compute
277
+ @compute ||= Fog::Compute.new(compute_options)
278
+ end
279
+
280
+ # Not meant to be part of public interface
281
+ def transport_for(machine_spec, machine_options, server)
282
+ # TODO winrm
283
+ create_ssh_transport(machine_spec, machine_options, server)
284
+ end
285
+
286
+ protected
287
+
288
+ def option_for(machine_options, key)
289
+ machine_options[key] || DEFAULT_OPTIONS[key]
290
+ end
291
+
292
+ def symbolize_keys(options)
293
+ options.inject({}) { |result,(key,value)| result[key.to_sym] = value; result }
294
+ end
295
+
296
+ def server_for(machine_spec)
297
+ if machine_spec.location
298
+ compute.servers.get(machine_spec.location['server_id'])
299
+ else
300
+ nil
301
+ end
302
+ end
303
+
304
+ def bootstrap_options_for(machine_spec, machine_options)
305
+ bootstrap_options = symbolize_keys(machine_options[:bootstrap_options] || {})
306
+ if !bootstrap_options[:key_name]
307
+ bootstrap_options[:key_name] = 'metal_default'
308
+ end
309
+ tags = {
310
+ 'Name' => machine_spec.name,
311
+ 'BootstrapId' => machine_spec.id,
312
+ 'BootstrapHost' => Socket.gethostname,
313
+ 'BootstrapUser' => Etc.getlogin
314
+ }
315
+ # User-defined tags override the ones we set
316
+ tags.merge!(bootstrap_options[:tags]) if bootstrap_options[:tags]
317
+ bootstrap_options.merge!({ :tags => tags })
318
+ bootstrap_options[:name] ||= machine_spec.name
319
+
320
+ bootstrap_options
321
+ end
322
+
323
+ def machine_for(machine_spec, machine_options, server = nil)
324
+ server ||= server_for(machine_spec)
325
+ if !server
326
+ raise "Server for node #{machine_spec.name} has not been created!"
327
+ end
328
+
329
+ if machine_spec.location['is_windows']
330
+ ChefMetal::Machine::WindowsMachine.new(machine_spec, transport_for(machine_spec, machine_options, server), convergence_strategy_for(machine_spec, machine_options))
331
+ else
332
+ ChefMetal::Machine::UnixMachine.new(machine_spec, transport_for(machine_spec, machine_options, server), convergence_strategy_for(machine_spec, machine_options))
333
+ end
334
+ end
335
+
336
+ def convergence_strategy_for(machine_spec, machine_options)
337
+ # Defaults
338
+ if !machine_spec.location
339
+ return ChefMetal::ConvergenceStrategy::NoConverge.new(machine_options[:convergence_options], config)
340
+ end
341
+
342
+ if machine_spec.location['is_windows']
343
+ ChefMetal::ConvergenceStrategy::InstallMsi.new(machine_options[:convergence_options], config)
344
+ else
345
+ ChefMetal::ConvergenceStrategy::InstallCached.new(machine_options[:convergence_options], config)
346
+ end
347
+ end
348
+
349
+ def create_ssh_transport(machine_spec, machine_options, server)
350
+ username = compute_options[:ssh_username] || 'root'
351
+ bootstrap_options = bootstrap_options_for(machine_spec, machine_options)
352
+ remote_host = has_static_ip(bootstrap_options) ? bootstrap_options[:customization_spec]['ipsettings']['ip'] : server.ipaddress
353
+ ChefMetal::Transport::SSH.new(remote_host, username, {:password => compute_options[:ssh_password], :paranoid => false}, {}, config)
354
+ end
355
+
356
+ def wait_for_transport(action_handler, machine_spec, machine_options, server)
357
+ transport = transport_for(machine_spec, machine_options, server)
358
+ if !transport.available?
359
+ if action_handler.should_perform_actions
360
+ action_handler.report_progress "waiting for #{machine_spec.name} (#{server.id} on #{driver_url}) to be connectable (transport up and running) ..."
361
+
362
+ _self = self
363
+
364
+ server.wait_for(remaining_wait_time(machine_spec, machine_options)) do
365
+ transport.available?
366
+ end
367
+ action_handler.report_progress "#{machine_spec.name} is now connectable"
368
+ end
369
+ end
370
+ end
371
+
372
+ def remaining_wait_time(machine_spec, machine_options)
373
+ if machine_spec.location['started_at']
374
+ timeout = option_for(machine_options, :start_timeout) - (Time.now.utc - Time.parse(machine_spec.location['started_at']))
375
+ else
376
+ timeout = option_for(machine_options, :create_timeout) - (Time.now.utc - Time.parse(machine_spec.location['allocated_at']))
377
+ end
378
+ end
379
+
380
+ def wait_until_ready(action_handler, machine_spec, machine_options, server)
381
+ if server.tools_state == 'toolsNotRunning'
382
+ if action_handler.should_perform_actions
383
+ action_handler.report_progress "waiting for #{machine_spec.name} (#{server.id} on #{driver_url}) to be ready ..."
384
+ server.wait_for(remaining_wait_time(machine_spec, machine_options)) { tools_state != 'toolsNotRunning' }
385
+ action_handler.report_progress "#{machine_spec.name} is now ready"
386
+ end
387
+ end
388
+ end
389
+ end
390
+ end
metadata CHANGED
@@ -1,48 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chef-metal-fogsphere
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.alpha.3
5
- prerelease: 6
4
+ version: 0.1.0.alpha.4
6
5
  platform: ruby
7
6
  authors:
8
7
  - Matt Wrock
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2014-05-18 00:00:00.000000000 Z
11
+ date: 2014-05-27 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: rbvmomi
16
15
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
16
  requirements:
19
- - - ~>
17
+ - - "~>"
20
18
  - !ruby/object:Gem::Version
21
19
  version: 1.5.1
22
20
  type: :runtime
23
21
  prerelease: false
24
22
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
23
  requirements:
27
- - - ~>
24
+ - - "~>"
28
25
  - !ruby/object:Gem::Version
29
26
  version: 1.5.1
30
27
  - !ruby/object:Gem::Dependency
31
28
  name: chef-metal
32
29
  requirement: !ruby/object:Gem::Requirement
33
- none: false
34
30
  requirements:
35
- - - ! '>='
31
+ - - '='
36
32
  - !ruby/object:Gem::Version
37
- version: '0'
33
+ version: 0.11.beta.2
38
34
  type: :runtime
39
35
  prerelease: false
40
36
  version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
37
  requirements:
43
- - - ! '>='
38
+ - - '='
44
39
  - !ruby/object:Gem::Version
45
- version: '0'
40
+ version: 0.11.beta.2
46
41
  description: A fork of Chef-Metal-fog for vsphere
47
42
  email:
48
43
  - matt@centurylinkcloud.com
@@ -50,36 +45,35 @@ executables: []
50
45
  extensions: []
51
46
  extra_rdoc_files: []
52
47
  files:
53
- - .gitignore
48
+ - ".gitignore"
54
49
  - LICENSE
55
- - chef-metal-fogsphere-0.1.0.alpha.1.zip
56
50
  - chef-metal-fogsphere.gemspec
51
+ - lib/chef_metal/driver_init/vsphere.rb
57
52
  - lib/chef_metal_clc.rb
58
- - lib/clc_provisioner.rb
53
+ - lib/clc_driver.rb
59
54
  homepage:
60
55
  licenses:
61
56
  - Apache 2.0
57
+ metadata: {}
62
58
  post_install_message:
63
59
  rdoc_options: []
64
60
  require_paths:
65
61
  - lib
66
62
  required_ruby_version: !ruby/object:Gem::Requirement
67
- none: false
68
63
  requirements:
69
- - - ! '>='
64
+ - - ">="
70
65
  - !ruby/object:Gem::Version
71
66
  version: '0'
72
67
  required_rubygems_version: !ruby/object:Gem::Requirement
73
- none: false
74
68
  requirements:
75
- - - ! '>'
69
+ - - ">"
76
70
  - !ruby/object:Gem::Version
77
71
  version: 1.3.1
78
72
  requirements: []
79
73
  rubyforge_project:
80
- rubygems_version: 1.8.28
74
+ rubygems_version: 2.2.1
81
75
  signing_key:
82
- specification_version: 3
76
+ specification_version: 4
83
77
  summary: A fork of Chef-Metal-fog for vsphere
84
78
  test_files: []
85
79
  has_rdoc:
Binary file
@@ -1,419 +0,0 @@
1
- require 'chef_metal/provisioner'
2
- require 'chef_metal/aws_credentials'
3
- require 'chef_metal/openstack_credentials'
4
- require 'chef_metal/machine/windows_machine'
5
- require 'chef_metal/machine/unix_machine'
6
- require 'chef_metal/convergence_strategy/install_msi'
7
- require 'chef_metal/convergence_strategy/install_cached'
8
- require 'chef_metal/transport/ssh'
9
- require 'fog'
10
- require 'fog/core'
11
- require 'fog/compute'
12
- require 'fog/aws'
13
-
14
- module ChefMetalVsphere
15
- # Provisions machines in vagrant.
16
- class VsphereProvisioner < ChefMetal::Provisioner
17
- include Chef::Mixin::ShellOut
18
-
19
- DEFAULT_OPTIONS = {
20
- :create_timeout => 600,
21
- :start_timeout => 600,
22
- :ssh_timeout => 20
23
- } unless defined? DEFAULT_OPTIONS
24
-
25
- def self.inflate(node_json)
26
- url = node_json['normal']['provisioner_output']['provisioner_url']
27
- scheme, provider, id = url.split(':', 3)
28
- VsphereProvisioner.new({ :provider => provider }, id)
29
- end
30
-
31
- # Create a new fog provisioner.
32
- #
33
- # ## Parameters
34
- # compute_options - hash of options to be passed to Fog::Compute.new
35
- # Special options:
36
- # - :base_bootstrap_options is merged with bootstrap_options in acquire_machine
37
- # to present the full set of bootstrap options. Write down any bootstrap_options
38
- # you intend to apply universally here.
39
- # - :aws_credentials is an AWS CSV file (created with Download Credentials)
40
- # containing your aws key information. If you do not specify aws_access_key_id
41
- # and aws_secret_access_key explicitly, the first line from this file
42
- # will be used. You may pass a Cheffish::AWSCredentials object.
43
- # - :create_timeout - the time to wait for the instance to boot to ssh (defaults to 600)
44
- # - :start_timeout - the time to wait for the instance to start (defaults to 600)
45
- # - :ssh_timeout - the time to wait for ssh to be available if the instance is detected as up (defaults to 20)
46
- # id - the ID in the provisioner_url (fog:PROVIDER:ID)
47
- def initialize(compute_options, id=nil)
48
- @compute_options = compute_options
49
- @base_bootstrap_options = compute_options.delete(:base_bootstrap_options) || {}
50
- @base_bootstrap_options_for = {}
51
- end
52
-
53
- attr_reader :compute_options
54
-
55
- def current_base_bootstrap_options
56
- result = @base_bootstrap_options.dup
57
- result
58
- end
59
-
60
- # Inflate a provisioner from node_json information; we don't want to force the
61
- # driver to figure out what the provisioner really needs, since it varies
62
- # from provisioner to provisioner.
63
- #
64
- # ## Parameters
65
- # node_json - node_json to inflate the provisioner for
66
- #
67
- # returns a FogProvisioner
68
- # TODO: def self.inflate(node_json)
69
- # right now, not implemented, will raise error from base class until overridden
70
-
71
- # Acquire a machine, generally by provisioning it. Returns a Machine
72
- # object pointing at the machine, allowing useful actions like setup,
73
- # converge, execute, file and directory. The Machine object will have a
74
- # "node_json" property which must be saved to the server (if it is any
75
- # different from the original node_json object).
76
- #
77
- # ## Parameters
78
- # action_handler - the action_handler object that is calling this method; this
79
- # is generally a action_handler, but could be anything that can support the
80
- # ChefMetal::ActionHandler interface (i.e., in the case of the test
81
- # kitchen metal driver for acquiring and destroying VMs; see the base
82
- # class for what needs providing).
83
- # node_json - node_json object (deserialized json) representing this machine. If
84
- # the node_json has a provisioner_options hash in it, these will be used
85
- # instead of options provided by the provisioner. TODO compare and
86
- # fail if different?
87
- # node_json will have node_json['normal']['provisioner_options'] in it with any options.
88
- # It is a hash with this format:
89
- #
90
- # -- provisioner_url: fog:<relevant_fog_options>
91
- # -- bootstrap_options: hash of options to pass to compute.servers.create
92
- # -- is_windows: true if windows. TODO detect this from ami?
93
- # -- create_timeout - the time to wait for the instance to boot to ssh (defaults to 600)
94
- # -- start_timeout - the time to wait for the instance to start (defaults to 600)
95
- # -- ssh_timeout - the time to wait for ssh to be available if the instance is detected as up (defaults to 20)
96
- #
97
- # Example bootstrap_options for ec2:
98
- # 'bootstrap_options' => {
99
- # 'image_id' =>'ami-311f2b45',
100
- # 'flavor_id' =>'t1.micro',
101
- # 'key_name' => 'key-pair-name'
102
- # }
103
- #
104
- # node_json['normal']['provisioner_output'] will be populated with information
105
- # about the created machine. For vagrant, it is a hash with this
106
- # format:
107
- #
108
- # -- provisioner_url: fog:<relevant_fog_options>
109
- # -- server_id: the ID of the server so it can be found again
110
- #
111
- def acquire_machine(action_handler, node_json)
112
- # Set up the modified node_json data
113
- provisioner_output = node_json['normal']['provisioner_output'] || {
114
- 'provisioner_url' => provisioner_url,
115
- 'provisioner_version' => '0.0.1'
116
- }
117
-
118
- if provisioner_output['provisioner_url'] != provisioner_url
119
- raise "Switching a machine's provider from #{provisioner_output['provisioner_url']} to #{provisioner_url} for is not currently supported! Use machine :destroy and then re-create the machine on the new provisioner."
120
- end
121
-
122
- node_json['normal']['provisioner_output'] = provisioner_output
123
-
124
- if provisioner_output['server_id']
125
-
126
- # If the server already exists, make sure it is up
127
-
128
- # TODO verify that the server info matches the specification (ami, etc.)\
129
- server = server_for(node_json)
130
- if !server
131
- Chef::Log.warn "Machine #{node_json['name']} (#{provisioner_output['server_id']} on #{provisioner_url}) is not associated with the ec2 account. Recreating ..."
132
- need_to_create = true
133
- elsif server.respond_to?(:state) && %w(terminated archive).include?(server.state) # Can't come back from that
134
- Chef::Log.warn "Machine #{node_json['name']} (#{server.id} on #{provisioner_url}) is terminated. Recreating ..."
135
- need_to_create = true
136
- else
137
- need_to_create = false
138
- if !server.ready?
139
- action_handler.perform_action "start machine #{node_json['name']} (#{server.id} on #{provisioner_url})" do
140
- server.start
141
- end
142
- action_handler.perform_action "wait for machine #{node_json['name']} (#{server.id} on #{provisioner_url}) to be ready" do
143
- wait_until_ready(node_json, server, option_for(node_json, :start_timeout))
144
- end
145
- else
146
- wait_until_ready(node_json, server, option_for(node_json, :ssh_timeout))
147
- end
148
- end
149
- else
150
- need_to_create = true
151
- end
152
-
153
- if need_to_create
154
- # If the server does not exist, create it
155
- bootstrap_options = bootstrap_options_for(action_handler.new_resource, node_json)
156
- bootstrap_options = bootstrap_options.merge(:name => node_json['name'])
157
-
158
- start_time = Time.now
159
- timeout = option_for(node_json, :create_timeout)
160
-
161
- description = [ "create machine #{node_json['name']} on #{provisioner_url}" ]
162
- bootstrap_options.each_pair { |key,value| description << " #{key}: #{value.inspect}" }
163
- server = nil
164
- action_handler.perform_action description do
165
- clone_results = compute.vm_clone(convert_to_strings(bootstrap_options))
166
- server = compute.servers.get(clone_results['new_vm']['id'])
167
- if(bootstrap_options[:additional_disk_size_gb] && bootstrap_options[:additional_disk_size_gb].is_a?(Integer) && bootstrap_options[:additional_disk_size_gb] > 0)
168
- volume_options = {
169
- 'server_id' => server.id,
170
- 'datastore' => bootstrap_options[:datastore],
171
- 'size_gb' => bootstrap_options[:additional_disk_size_gb]
172
- }
173
- volume = compute.volumes.create(volume_options)
174
- end
175
- provisioner_output['server_id'] = server.id
176
- # Save quickly in case something goes wrong
177
- save_node(action_handler, node_json, action_handler.new_resource.chef_server)
178
- end
179
-
180
- if server
181
- @@ip_pool_lock = Mutex.new
182
- # Re-retrieve the server in a more malleable form and wait for it to be ready
183
- server = compute.servers.get(server.id)
184
- action_handler.perform_action "machine #{node_json['name']} created as #{server.id} on #{provisioner_url}" do
185
- end
186
- # Wait for the machine to come up
187
- Chef::Log.info "waiting for machine to come up"
188
- server.wait_for(timeout - (Time.now - start_time)) { tools_state != 'toolsNotRunning' }
189
-
190
- if has_static_ip(node_json)
191
- Chef::Log.info "waiting for customizations to complete"
192
- sleep(30)
193
- Chef::Log.info "rebooting..."
194
- server.reboot
195
- end
196
-
197
- # wait for ssh to start listening
198
- transport = nil
199
- _self = self
200
- action_handler.perform_action "wait for machine #{node_json['name']} to boot" do
201
- server.wait_for(timeout - (Time.now - start_time)) do
202
- if tools_state != 'toolsNotRunning'
203
- transport ||= _self.transport_for(server, node_json)
204
- begin
205
- transport.execute('pwd')
206
- Chef::Log.info "ssh succeeded"
207
- true
208
- rescue ChefMetal::Transport::SSH::InitialConnectTimeout, Errno::ECONNREFUSED, Net::SSH::Disconnect, Errno::EHOSTUNREACH => err
209
- Chef::Log.info "ssh failed1: #{err}"
210
- false
211
- rescue Exception => e
212
- Chef::Log.info "ssh failed2: #{e}"
213
- true
214
- end
215
- else
216
- false
217
- end
218
- end
219
- end
220
-
221
- # If there is some other error, we just wait patiently for SSH
222
- Chef::Log.info "entering next block"
223
- begin
224
- server.wait_for(option_for(node_json, :ssh_timeout)) { transport.available? }
225
- rescue Fog::Errors::TimeoutError
226
- # Sometimes (on EC2) the machine comes up but gets stuck or has
227
- # some other problem. If this is the case, we restart the server
228
- # to unstick it. Reboot covers a multitude of sins.
229
- Chef::Log.warn "Machine #{node_json['name']} (#{server.id} on #{provisioner_url}) was started but SSH did not come up. Rebooting machine in an attempt to unstick it ..."
230
- action_handler.perform_action "reboot machine #{node_json['name']} to try to unstick it" do
231
- server.reboot
232
- end
233
- Chef::Log.info "rebooted"
234
- action_handler.perform_action "wait for machine #{node_json['name']} to be ready after reboot" do
235
- wait_until_ready(node_json, server, option_for(node_json, :start_timeout))
236
- end
237
- Chef::Log.info "reported ready"
238
- end
239
- end
240
- end
241
-
242
- # Create machine object for callers to use
243
- machine_for(node_json, server)
244
- end
245
-
246
- def has_static_ip(node_json)
247
- bootstrap_options = node_json['normal']['provisioner_options']['bootstrap_options']
248
- if bootstrap_options.has_key?('customization_spec')
249
- bootstrap_options = bootstrap_options['customization_spec']
250
- if bootstrap_options.has_key?('ipsettings')
251
- bootstrap_options = bootstrap_options['ipsettings']
252
- if bootstrap_options.has_key?('ip')
253
- return true
254
- end
255
- end
256
- end
257
- false
258
- end
259
-
260
- def convert_to_strings(objay)
261
- if objay.kind_of?(Array)
262
- objay.map { |v| convert_to_strings(v) }
263
- elsif objay.kind_of?(Hash)
264
- Hash[objay.map { |(k, v)| [k.to_s, convert_to_strings(v)] }]
265
- else
266
- objay
267
- end
268
- end
269
-
270
- # Connect to machine without acquiring it
271
- def connect_to_machine(node_json)
272
- machine_for(node_json)
273
- end
274
-
275
- def delete_machine(action_handler, node_json)
276
- if node_json['normal']['provisioner_output'] && node_json['normal']['provisioner_output']['server_id']
277
- server = compute.servers.get(node_json['normal']['provisioner_output']['server_id'])
278
- if server
279
- action_handler.perform_action "destroy machine #{node_json['name']} (#{node_json['normal']['provisioner_output']['server_id']} at #{provisioner_url})" do
280
- server.destroy
281
- end
282
- end
283
- convergence_strategy_for(node_json).cleanup_convergence(action_handler, node_json)
284
- end
285
- end
286
-
287
- def stop_machine(action_handler, node_json)
288
- # If the machine doesn't exist, we silently do nothing
289
- if node_json['normal']['provisioner_output'] && node_json['normal']['provisioner_output']['server_id']
290
- server = compute.servers.get(node_json['normal']['provisioner_output']['server_id'])
291
- action_handler.perform_action "stop machine #{node_json['name']} (#{server.id} at #{provisioner_url})" do
292
- server.stop
293
- end
294
- end
295
- end
296
-
297
- def resource_created(machine)
298
- @base_bootstrap_options_for[machine] = current_base_bootstrap_options
299
- end
300
-
301
- def compute
302
- @compute ||= Fog::Compute.new(compute_options)
303
- end
304
-
305
- def provisioner_url
306
- provider_identifier = compute_options[:vsphere_server]
307
- "fog:#{compute_options[:provider]}:#{provider_identifier}"
308
- end
309
-
310
- # Not meant to be part of public interface
311
- def transport_for(server, node_json = nil)
312
- # TODO winrm
313
- create_ssh_transport(server, node_json)
314
- end
315
-
316
- protected
317
-
318
- def option_for(node_json, key)
319
- if node_json['normal']['provisioner_options'] && node_json['normal']['provisioner_options'][key.to_s]
320
- node_json['normal']['provisioner_options'][key.to_s]
321
- elsif compute_options[key]
322
- compute_options[key]
323
- else
324
- DEFAULT_OPTIONS[key]
325
- end
326
- end
327
-
328
- def symbolize_keys(options)
329
- options.inject({}) { |result,(key,value)| result[key.to_sym] = value; result }
330
- end
331
-
332
- def server_for(node_json)
333
- if node_json['normal']['provisioner_output'] && node_json['normal']['provisioner_output']['server_id']
334
- compute.servers.get(node_json['normal']['provisioner_output']['server_id'])
335
- else
336
- nil
337
- end
338
- end
339
-
340
- def bootstrap_options_for(machine, node_json)
341
- provisioner_options = node_json['normal']['provisioner_options'] || {}
342
- bootstrap_options = @base_bootstrap_options_for[machine] || current_base_bootstrap_options
343
- bootstrap_options = bootstrap_options.merge(symbolize_keys(provisioner_options['bootstrap_options'] || {}))
344
- require 'socket'
345
- require 'etc'
346
- tags = {
347
- 'Name' => node_json['name'],
348
- 'BootstrapChefServer' => machine.chef_server[:chef_server_url],
349
- 'BootstrapHost' => Socket.gethostname,
350
- 'BootstrapUser' => Etc.getlogin,
351
- 'Bootstrapnode_jsonName' => node_json['name']
352
- }
353
- if machine.chef_server[:options] && machine.chef_server[:options][:data_store]
354
- tags['ChefLocalRepository'] = machine.chef_server[:options][:data_store].chef_fs.fs_description
355
- end
356
- # User-defined tags override the ones we set
357
- tags.merge!(bootstrap_options[:tags]) if bootstrap_options[:tags]
358
- bootstrap_options.merge!({ :tags => tags })
359
-
360
- bootstrap_options
361
- end
362
-
363
- def machine_for(node_json, server = nil)
364
- server ||= server_for(node_json)
365
- if !server
366
- raise "Server for node_json #{node_json['name']} has not been created!"
367
- end
368
-
369
- if node_json['normal']['provisioner_options'] && node_json['normal']['provisioner_options']['is_windows']
370
- ChefMetal::Machine::WindowsMachine.new(node_json, transport_for(server, node_json), convergence_strategy_for(node_json))
371
- else
372
- ChefMetal::Machine::UnixMachine.new(node_json, transport_for(server, node_json), convergence_strategy_for(node_json))
373
- end
374
- end
375
-
376
- def convergence_strategy_for(node_json)
377
- if node_json['normal']['provisioner_options'] && node_json['normal']['provisioner_options']['is_windows']
378
- @windows_convergence_strategy ||= begin
379
- options = {}
380
- provisioner_options = node_json['normal']['provisioner_options'] || {}
381
- options[:chef_client_timeout] = provisioner_options['chef_client_timeout'] if provisioner_options.has_key?('chef_client_timeout')
382
- ChefMetal::ConvergenceStrategy::InstallMsi.new(options)
383
- end
384
- else
385
- @unix_convergence_strategy ||= begin
386
- options = {}
387
- provisioner_options = node_json['normal']['provisioner_options'] || {}
388
- options[:chef_client_timeout] = provisioner_options['chef_client_timeout'] if provisioner_options.has_key?('chef_client_timeout')
389
- ChefMetal::ConvergenceStrategy::InstallCached.new(options)
390
- end
391
- end
392
- end
393
-
394
- def create_ssh_transport(server, node_json)
395
- username = compute_options[:ssh_username] || 'root'
396
-
397
- bootstrap_options = node_json['normal']['provisioner_options']['bootstrap_options']
398
- remote_host = has_static_ip(node_json) ? bootstrap_options['customization_spec']['ipsettings']['ip'] : server.ipaddress
399
-
400
- ChefMetal::Transport::SSH.new(remote_host, username, {:password => compute_options[:ssh_password], :paranoid => false}, {})
401
- end
402
-
403
- def wait_until_ready(node_json, server, timeout)
404
- transport = nil
405
- _self = self
406
- server.wait_for(timeout) do
407
- if transport
408
- transport.available?
409
- elsif tools_state != 'toolsNotRunning'
410
- # Don't create the transport until the machine is ready (we won't have the host till then)
411
- transport = _self.transport_for(server, node_json)
412
- transport.available?
413
- else
414
- false
415
- end
416
- end
417
- end
418
- end
419
- end