chef-provisioning-opennebula 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/README.md +532 -0
  4. data/Rakefile +6 -0
  5. data/lib/chef/provider/one_image.rb +244 -0
  6. data/lib/chef/provider/one_template.rb +95 -0
  7. data/lib/chef/provider/one_user.rb +105 -0
  8. data/lib/chef/provider/one_vnet.rb +122 -0
  9. data/lib/chef/provider/one_vnet_lease.rb +133 -0
  10. data/lib/chef/provisioning/driver_init/opennebula.rb +17 -0
  11. data/lib/chef/provisioning/opennebula_driver.rb +18 -0
  12. data/lib/chef/provisioning/opennebula_driver/credentials.rb +105 -0
  13. data/lib/chef/provisioning/opennebula_driver/driver.rb +572 -0
  14. data/lib/chef/provisioning/opennebula_driver/one_lib.rb +352 -0
  15. data/lib/chef/provisioning/opennebula_driver/resources.rb +20 -0
  16. data/lib/chef/provisioning/opennebula_driver/version.rb +30 -0
  17. data/lib/chef/resource/one_image.rb +65 -0
  18. data/lib/chef/resource/one_template.rb +46 -0
  19. data/lib/chef/resource/one_user.rb +50 -0
  20. data/lib/chef/resource/one_vnet.rb +51 -0
  21. data/lib/chef/resource/one_vnet_lease.rb +46 -0
  22. data/spec/integration/test_all_integration_spec.rb +102 -0
  23. data/spec/recipes/attach_back_one_vm_spec.rb +20 -0
  24. data/spec/recipes/attach_back_two_vm_spec.rb +20 -0
  25. data/spec/recipes/attach_one_image_spec.rb +20 -0
  26. data/spec/recipes/converge_back_one_vm_spec.rb +19 -0
  27. data/spec/recipes/converge_back_two_vm_spec.rb +19 -0
  28. data/spec/recipes/converge_bootstrap_vm_spec.rb +34 -0
  29. data/spec/recipes/create_back_one_vm_spec.rb +20 -0
  30. data/spec/recipes/create_back_two_vm_spec.rb +20 -0
  31. data/spec/recipes/create_bootstrap_vm_spec.rb +34 -0
  32. data/spec/recipes/create_one_image_spec.rb +21 -0
  33. data/spec/recipes/create_one_template_spec.rb +52 -0
  34. data/spec/recipes/delete_all_spec.rb +47 -0
  35. data/spec/recipes/driver_options_spec.rb +70 -0
  36. data/spec/recipes/instantiate_one_template_spec.rb +35 -0
  37. data/spec/recipes/snapshot_one_image_spec.rb +21 -0
  38. data/spec/recipes/snapshot_two_image_spec.rb +21 -0
  39. data/spec/spec_helper.rb +35 -0
  40. data/spec/support/opennebula_support.rb +64 -0
  41. metadata +168 -0
@@ -0,0 +1,122 @@
1
+ # Copyright 2015, BlackBerry, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ #
16
+ # Implementation of Provider class.
17
+ #
18
+ class Chef
19
+ #
20
+ # Implementation of Provider class.
21
+ #
22
+ class Provider
23
+ #
24
+ # Implementation of Provider class.
25
+ #
26
+ class OneVnet < Chef::Provider::LWRPBase
27
+ use_inline_resources
28
+
29
+ provides :one_vnet
30
+
31
+ attr_reader :current_vnet
32
+
33
+ def action_handler
34
+ @action_handler ||= Chef::Provisioning::ChefProviderActionHandler.new(self)
35
+ end
36
+
37
+ def exists?(filter)
38
+ new_driver = driver
39
+ @current_vnet = new_driver.one.get_resource('vnet', filter)
40
+ Chef::Log.debug("VNET '#{filter}' exists: #{!@current_vnet.nil?}")
41
+ !@current_vnet.nil?
42
+ end
43
+
44
+ action :create do
45
+ fail "Missing attribute 'template_file'" unless @new_resource.template_file
46
+ fail "Missing attribute 'cluster_id'" unless @new_resource.cluster_id
47
+
48
+ if exists?(:name => @new_resource.name)
49
+ action_handler.report_progress "vnet '#{@new_resource.name}' already exists - nothing to do"
50
+ else
51
+ action_handler.perform_action "created vnet '#{@new_resource.name}' from '#{@new_resource.template_file}'" do
52
+ template_str = ::File.read(@new_resource.template_file) + "\nNAME=\"#{@new_resource.name}\""
53
+ vnet = new_driver.one.allocate_vnet(template_str, @new_resource.cluster_id)
54
+ Chef::Log.debug(template_str)
55
+ fail "failed to create vnet '#{@new_resource.name}': #{vnet.message}" if OpenNebula.is_error?(vnet)
56
+ @new_resource.updated_by_last_action(true)
57
+ end
58
+ end
59
+ end
60
+
61
+ action :delete do
62
+ if exists?(:id => @new_resource.vnet_id, :name => @new_resource.name)
63
+ action_handler.perform_action "deleted vnet '#{new_resource.name}' (#{@current_vnet.id})" do
64
+ rc = @current_vnet.delete
65
+ fail "failed to delete vnet '#{@new_resource.name}': #{rc.message}" if OpenNebula.is_error?(rc)
66
+ @new_resource.updated_by_last_action(true)
67
+ end
68
+ else
69
+ action_handler.report_progress "vnet '#{new_resource.name}' does not exists - nothing to do"
70
+ end
71
+ end
72
+
73
+ action :reserve do
74
+ fail "Missing attribute 'network'" unless @new_resource.network
75
+
76
+ if exists?(:name => @new_resource.name)
77
+ hash = @current_vnet.to_hash
78
+ ar_pool = [hash['VNET']['AR_POOL']].flatten
79
+ Chef::Log.debug(@current_vnet.to_hash)
80
+ same = false
81
+ if @new_resource.ar_id && @new_resource.ar_id > -1
82
+ ar_pool.each do |ar|
83
+ same = true if ar['AR']['AR_ID'] == @new_resource.ar_id.to_s && ar['AR']['SIZE'].to_i == @new_resource.size
84
+ end
85
+ else
86
+ same = ar_pool[0]['AR']['SIZE'].to_i == @new_resource.size
87
+ end
88
+ if same
89
+ action_handler.report_progress "vnet '#{@new_resource.name}' already exists - nothing to do"
90
+ else
91
+ fail "vnet '#{@new_resource.name}' exists with different configuration"
92
+ end
93
+ else
94
+ fail "parent network '#{@new_resource.network}' does not exist" unless exists?(:id => @new_resource.network)
95
+ action_handler.perform_action "reserved vnet '#{@new_resource.name}'" do
96
+ rc = @current_vnet.reserve(@new_resource.name, @new_resource.size.to_s, @new_resource.ar_id.to_s, @new_resource.mac_ip, nil)
97
+ fail "Failed to reserve new vnet in network (#{@new_resource.network}): #{rc.message}" if OpenNebula.is_error?(rc)
98
+ @new_resource.updated_by_last_action(true)
99
+ end
100
+ end
101
+ end
102
+
103
+ protected
104
+
105
+ def driver
106
+ if current_driver && current_driver.driver_url != new_driver.driver_url
107
+ fail "Cannot move '#{machine_spec.name}' from #{current_driver.driver_url} to #{new_driver.driver_url}: machine moving is not supported. Destroy and recreate."
108
+ end
109
+ fail "Driver not specified for one_image #{new_resource.name}" unless new_driver
110
+ new_driver
111
+ end
112
+
113
+ def new_driver
114
+ run_context.chef_provisioning.driver_for(new_resource.driver)
115
+ end
116
+
117
+ def current_driver
118
+ run_context.chef_provisioning.driver_for(run_context.chef_provisioning.current_driver) if run_context.chef_provisioning.current_driver
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,133 @@
1
+ # Copyright 2015, BlackBerry, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ #
16
+ # Implementation of Provider class.
17
+ #
18
+ class Chef
19
+ #
20
+ # Implementation of Provider class.
21
+ #
22
+ class Provider
23
+ #
24
+ # Implementation of Provider class.
25
+ #
26
+ class OneVnetLease < Chef::Provider::LWRPBase
27
+ use_inline_resources
28
+
29
+ provides :one_vnet_lease
30
+
31
+ attr_reader :current_vnet
32
+
33
+ def action_handler
34
+ @action_handler ||= Chef::Provisioning::ChefProviderActionHandler.new(self)
35
+ end
36
+
37
+ def exists?
38
+ new_driver = driver
39
+ filter = { @new_resource.vnet.is_a?(Integer) ? :id : :name => @new_resource.vnet }
40
+ @current_vnet = new_driver.one.get_resource('vnet', filter)
41
+ fail "vnet '#{@new_resource.vnet}' does not exist" if @current_vnet.nil?
42
+ @current_vnet.info!
43
+ hash = @current_vnet.to_hash
44
+
45
+ lookup = @new_resource.name.include?(':') ? 'MAC' : 'IP'
46
+ ar_pool = [hash['VNET']['AR_POOL']].flatten
47
+
48
+ if @new_resource.ar_id && @new_resource.ar_id > -1
49
+ ar_pool = get_ar_pool(ar_pool, @new_resource.ar_id.to_s)
50
+ fail "ar_id not found '#{@new_resource.ar_id}'" if ar_pool.nil?
51
+ end
52
+ available = lease_available?(ar_pool, lookup)
53
+ fail "'#{name}' is already allocated to a VM (ID: #{vm})" unless available
54
+ available
55
+ # ar_pool.each do |a|
56
+ # if a['AR']['LEASES']['LEASE']
57
+ # [a['AR']['LEASES']['LEASE']].flatten.each do |l|
58
+ # if l[lookup] && l[lookup] == @new_resource.name
59
+ # exists = true
60
+ # vm = l['VM'].to_i
61
+ # break
62
+ # end
63
+ # end
64
+ # end
65
+ # end
66
+ # fail "'#{name}' is already allocated to a VM (ID: #{vm})" if exists && vm > -1
67
+ # (exists && vm == -1)
68
+ end
69
+
70
+ action :hold do
71
+ if exists?
72
+ action_handler.report_progress("#{@new_resource.name} is already on hold and not used")
73
+ else
74
+ action_handler.perform_action "hold '#{@new_resource.name}'" do
75
+ rc = @current_vnet.hold(@new_resource.name, @new_resource.ar_id || -1)
76
+ fail "Failed to put a hold on '#{@new_resource.name}': #{rc.message}" if OpenNebula.is_error?(rc)
77
+ @new_resource.updated_by_last_action(true)
78
+ end
79
+ end
80
+ end
81
+
82
+ action :release do
83
+ if exists?
84
+ action_handler.perform_action "released '#{@new_resource.name}'" do
85
+ rc = @current_vnet.release(@new_resource.name, @new_resource.ar_id || -1)
86
+ fail "Failed to release '#{@new_resource.name}': #{rc.message}" if OpenNebula.is_error?(rc)
87
+ @new_resource.updated_by_last_action(true)
88
+ end
89
+ else
90
+ action_handler.report_progress("#{@new_resource.name} is not present - nothing do to")
91
+ end
92
+ end
93
+
94
+ protected
95
+
96
+ def get_ar_pool(ar_pool, ar_id)
97
+ ar_pool.each { |a| return [a] if a['AR']['AR_ID'] == ar_id }
98
+ nil
99
+ end
100
+
101
+ def lease_available?(ar_pool, lookup)
102
+ exists = false
103
+ vm = -2
104
+ ar_pool.each do |a|
105
+ next unless a['AR']['LEASES']['LEASE']
106
+ [a['AR']['LEASES']['LEASE']].flatten.each do |l|
107
+ next unless l[lookup] && l[lookup] == @new_resource.name
108
+ exists = true
109
+ vm = l['VM'].to_i
110
+ break
111
+ end
112
+ end
113
+ (exists && vm == -1)
114
+ end
115
+
116
+ def driver
117
+ if current_driver && current_driver.driver_url != new_driver.driver_url
118
+ fail "Cannot move '#{machine_spec.name}' from #{current_driver.driver_url} to #{new_driver.driver_url}: machine moving is not supported. Destroy and recreate."
119
+ end
120
+ fail "Driver not specified for one_image #{new_resource.name}" unless new_driver
121
+ new_driver
122
+ end
123
+
124
+ def new_driver
125
+ run_context.chef_provisioning.driver_for(new_resource.driver)
126
+ end
127
+
128
+ def current_driver
129
+ run_context.chef_provisioning.driver_for(run_context.chef_provisioning.current_driver) if run_context.chef_provisioning.current_driver
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,17 @@
1
+ # Copyright 2015, BlackBerry, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'chef/provisioning/opennebula_driver/driver'
16
+
17
+ Chef::Provisioning.register_driver_class("opennebula", Chef::Provisioning::OpenNebulaDriver::Driver)
@@ -0,0 +1,18 @@
1
+ # Copyright 2015, BlackBerry, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'chef/provisioning'
16
+ require 'chef/provisioning/opennebula_driver/driver'
17
+ require 'chef/provisioning/opennebula_driver/resources'
18
+ require 'chef/provisioning/opennebula_driver/credentials'
@@ -0,0 +1,105 @@
1
+ # Copyright 2015, BlackBerry, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'json'
16
+
17
+ #
18
+ # Implementation of Provider class.
19
+ #
20
+ class Chef
21
+ #
22
+ # Extending module.
23
+ #
24
+ module Provisioning
25
+ #
26
+ # Extending module.
27
+ #
28
+ module OpenNebulaDriver
29
+ #
30
+ # Implementation of Provider class.
31
+ #
32
+ class Credentials
33
+ def initialize(options = {})
34
+ @credentials = {}
35
+ load_default(options)
36
+ load_profiles
37
+ end
38
+
39
+ def default
40
+ fail 'No credentials loaded! Do you have a ~/.one/one_auth file?' if @credentials.size == 0
41
+ @credentials[ENV['ONE_DEFAULT_PROFILE'] || 'default'] || @credentials.first[1]
42
+ end
43
+
44
+ def [](name)
45
+ fail "Profile '#{name}' does not exist" unless @credentials[name]
46
+ @credentials[name]
47
+ end
48
+
49
+ def load_default(options = {})
50
+ oneauth_file = ENV['ONE_AUTH'] || File.expand_path('~/.one/one_auth')
51
+ begin
52
+ creds = File.read(oneauth_file).strip
53
+ @credentials['default'] = { :credentials => creds, :options => options }
54
+ end if File.file?(oneauth_file)
55
+ end
56
+
57
+ def load_profiles
58
+ file = nil
59
+ if ENV['ONE_CONFIG'] && !ENV['ONE_CONFIG'].empty? && File.file?(ENV['ONE_CONFIG'])
60
+ file = ENV['ONE_CONFIG']
61
+ elsif ENV['HOME'] && File.file?("#{ENV['HOME']}/.one/one_config")
62
+ file = "#{ENV['HOME']}/.one/one_config"
63
+ elsif File.file?("/var/lib/one/.one/one_config")
64
+ file = "/var/lib/one/.one/one_config"
65
+ else
66
+ Chef::Log.info("No ONE_CONFIG file found, will use default profile")
67
+ end
68
+ json = {}
69
+ begin
70
+ content_hash = JSON.parse(File.read(file), :symbolize_names => true)
71
+ content_hash.each { |k, v| json[k.to_s] = v }
72
+ rescue Exception => e
73
+ Chef::Log.warn("Failed to read and parse config file #{file}: #{e.message}")
74
+ end
75
+ @credentials.merge!(json)
76
+ end
77
+
78
+ def load_plain(creds, options = {})
79
+ @credentials['default'] = {
80
+ :credentials => creds,
81
+ :options => options
82
+ } unless creds.nil?
83
+ @credentials
84
+ end
85
+
86
+ def load_file(filename, options = {})
87
+ creds = File.read(filename).strip if File.file?(filename)
88
+ @credentials['default'] = {
89
+ :credentials => creds,
90
+ :options => options
91
+ } unless creds.nil?
92
+ @credentials
93
+ end
94
+
95
+ def self.method_missing(name, *args, &block)
96
+ singleton.send(name, *args, &block)
97
+ end
98
+
99
+ def self.singleton
100
+ @one_credentials ||= Credentials.new
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,572 @@
1
+ # Copyright 2015, BlackBerry, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'chef/provisioning/driver'
16
+ require 'chef/provisioning/machine/unix_machine'
17
+ require 'chef/provisioning/convergence_strategy/install_cached'
18
+ require 'chef/provisioning/convergence_strategy/install_sh'
19
+ require 'chef/provisioning/convergence_strategy/no_converge'
20
+ require 'chef/provisioning/transport/ssh'
21
+ require 'chef/provisioning/opennebula_driver/version'
22
+ require 'chef/provisioning/opennebula_driver/one_lib'
23
+ require 'chef/provisioning/opennebula_driver/credentials'
24
+
25
+ class Chef
26
+ module Provisioning
27
+ module OpenNebulaDriver
28
+ #
29
+ # A Driver instance represents a place where machines can be created
30
+ # and found, and contains methods to create, delete, start, stop, and
31
+ # find them.
32
+ #
33
+ # For AWS, a Driver instance corresponds to a single account.
34
+ # For Vagrant, it is a directory where VM files are found.
35
+ #
36
+ # = How to Make a Driver
37
+ #
38
+ # To implement a Driver, you must implement the following methods:
39
+ #
40
+ # * initialize(driver_url) - create a new driver with the given URL
41
+ # * driver_url - a URL representing everything unique about your
42
+ # driver. (NOT credentials)
43
+ # * allocate_machine - ask the driver to allocate a machine to you.
44
+ # * ready_machine - get the machine "ready" - wait for it to be booted
45
+ # and accessible (for example, accessible via SSH
46
+ # transport).
47
+ # * stop_machine - stop the machine.
48
+ # * destroy_machine - delete the machine.
49
+ # * connect_to_machine - connect to the given machine.
50
+ #
51
+ # Optionally, you can also implement:
52
+ # * allocate_machines - allocate an entire group of machines.
53
+ # * ready_machines - get a group of machines warm and booted.
54
+ # * stop_machines - stop a group of machines.
55
+ # * destroy_machines - delete a group of machines.
56
+ #
57
+ # Additionally, you must create a file named
58
+ # `chef/provisioning/driver_init/<scheme>.rb`,
59
+ # where <scheme> is the name of the scheme you chose for your driver_url.
60
+ # This file, when required, must call
61
+ # Chef::Provisioning.add_registered_driver(<scheme>, <class>).
62
+ # The given <class>.from_url(url, config) will be called with a
63
+ # driver_url and configuration.
64
+ #
65
+ # All of these methods must be idempotent - if the work is already done,
66
+ # they just don't do anything.
67
+ #
68
+ class Driver < Chef::Provisioning::Driver
69
+ attr_reader :one
70
+
71
+ #
72
+ # OpenNebula by default reads the following information:
73
+ #
74
+ # username:password from ENV['ONE_AUTH'] or ENV['HOME']/.one/one_auth
75
+ # endpoint from ENV['ONE_XMLRPC']
76
+ #
77
+ # driver_url format:
78
+ # opennebula:<endpoint>:<profile>
79
+ # where <profile> points to a one_auth file in ENV['HOME']/.one/<profile>
80
+ #
81
+ # driver_options:
82
+ # credentials: bbsl-auto:text_pass credentials has precedence over secret_file
83
+ # secret_file: local_path_to_one_auth_file
84
+ #
85
+ def initialize(driver_url, config)
86
+ super
87
+ scan = driver_url.match(%r/(opennebula):(https?:\/\/[^:\/]+ (?::[0-9]{2,5})? (?:\/[^:\s]+) ) :?([^:\s]+)?/x)
88
+ endpoint = scan[2]
89
+ profile_name = scan[3]
90
+ fail "OpenNebula endpoint must be specified in 'driver_url': #{driver_url}" if endpoint.nil?
91
+
92
+ profile = profile_name ? one_credentials[profile_name] : one_credentials.default
93
+ Chef::Log.warn("':credentials' and ':secret_file' will be deprecated in next version in favour of 'opennebula:<endpoint>:<profile_name>'") if profile_name.nil?
94
+ @one = OneLib.new(profile[:credentials], endpoint, profile[:options] || {})
95
+ end
96
+
97
+ def self.from_url(driver_url, config)
98
+ Driver.new(driver_url, config)
99
+ end
100
+
101
+ # URL _must_ have an endpoint to prevent machine moving, which
102
+ # is not possible today between different endpoints.
103
+ def self.canonicalize_url(driver_url, config)
104
+ [driver_url, config]
105
+ end
106
+
107
+ # Allocate a machine from the underlying service. This method
108
+ # does not need to wait for the machine to boot or have an IP, but
109
+ # it must store enough information in machine_spec.location to find
110
+ # the machine later in ready_machine.
111
+ #
112
+ # If a machine is powered off or otherwise unusable, this method may
113
+ # start it, but does not need to wait until it is started. The
114
+ # idea is to get the gears moving, but the job doesn't need to be
115
+ # done :)
116
+ #
117
+ # @param [Chef::Provisioning::ActionHandler] action_handler
118
+ # The action_handler object that is calling this method
119
+ # @param [Chef::Provisioning::MachineSpec] machine_spec
120
+ # A machine specification representing this machine.
121
+ # @param [Hash] machine_options
122
+ # A set of options representing the desired options when
123
+ # constructing the machine
124
+ #
125
+ # @return [Chef::Provisioning::MachineSpec]
126
+ # Modifies the passed-in machine_spec. Anything in here will be
127
+ # saved back after allocate_machine completes.
128
+ #
129
+ def allocate_machine(action_handler, machine_spec, machine_options)
130
+ fqdn = begin
131
+ machine_options.bootstrap_options[:enforce_chef_fqdn]
132
+ rescue NoMethodError
133
+ false
134
+ end
135
+ fail "Machine 'name' must be a FQDN" if fqdn && machine_spec.name.scan(/([a-z0-9-]+\.)/i).length == 0
136
+ instance = instance_for(machine_spec)
137
+
138
+ if instance.nil?
139
+ fail "'bootstrap_options' must be specified" unless machine_options.bootstrap_options
140
+ check_unique_names(machine_options.bootstrap_options, machine_spec)
141
+ action_handler.perform_action "created vm '#{machine_spec.name}'" do
142
+ Chef::Log.debug(machine_options)
143
+ tpl = @one.get_template(machine_spec.name, machine_options.bootstrap_options)
144
+ vm = @one.allocate_vm(tpl)
145
+ populate_node_object(machine_spec, machine_options, vm)
146
+ end
147
+ Chef::Log.debug(machine_spec.reference)
148
+ else
149
+ Chef::Log.info("vm '#{machine_spec.name}' already exists - nothing to do")
150
+ end
151
+ machine_spec
152
+ end
153
+
154
+ # Ready a machine, to the point where it is running and accessible
155
+ # via a transport. This will NOT allocate a machine, but may kick
156
+ # it if it is down. This method waits for the machine to be usable,
157
+ # returning a Machine object pointing at the machine, allowing useful
158
+ # actions like setup, converge, execute, file and directory.
159
+ #
160
+ #
161
+ # @param [Chef::Provisioning::ActionHandler] action_handler
162
+ # The action_handler object that is calling this method
163
+ # @param [Chef::Provisioning::MachineSpec] machine_spec
164
+ # A machine specification representing this machine.
165
+ # @param [Hash] machine_options
166
+ # A set of options representing the desired state of the machine
167
+ #
168
+ # @return [Machine] A machine object pointing at the machine, allowing
169
+ # useful actions like setup, converge, execute, file and directory.
170
+ #
171
+ def ready_machine(action_handler, machine_spec, machine_options)
172
+ instance = instance_for(machine_spec)
173
+ fail "Machine '#{machine_spec.name}' does not have an instance associated with it, or instance does not exist." if instance.nil?
174
+
175
+ # TODO: Currently it does not start stopped VMs, it only waits for new VMs to be in RUNNING state
176
+ machine = nil
177
+ action_handler.perform_action "vm '#{machine_spec.name}' is ready" do
178
+ deployed = @one.wait_for_vm(instance.id)
179
+ machine_spec.reference['name'] = deployed.name
180
+ machine_spec.reference['state'] = deployed.state_str
181
+ nic_hash = deployed.to_hash
182
+ ip = [nic_hash['VM']['TEMPLATE']['NIC']].flatten[0]['IP']
183
+ fail "Could not get IP from VM '#{deployed.name}'" if ip.nil? || ip.to_s.empty?
184
+ machine_spec.reference['ip'] = ip
185
+ machine = machine_for(machine_spec, machine_options)
186
+ end
187
+ machine
188
+ end
189
+
190
+ # Connect to a machine without allocating or readying it. This method will
191
+ # NOT make any changes to anything, or attempt to wait.
192
+ #
193
+ # @param [Chef::Provisioning::MachineSpec] machine_spec
194
+ # MachineSpec representing this machine.
195
+ # @param [Hash] machine_options
196
+ # @return [Machine] A machine object pointing at the machine, allowing
197
+ # useful actions like setup, converge, execute, file and directory.
198
+ #
199
+ def connect_to_machine(machine_spec, machine_options)
200
+ machine_for(machine_spec, machine_options)
201
+ end
202
+
203
+ # Delete the given machine -- destroy the machine,
204
+ # returning things to the state before allocate_machine was called.
205
+ #
206
+ # @param [Chef::Provisioning::ActionHandler] action_handler
207
+ # The action_handler object that is calling this method
208
+ # @param [Chef::Provisioning::MachineSpec] machine_spec
209
+ # A machine specification representing this machine.
210
+ # @param [Hash] machine_options
211
+ # A set of options representing the desired state of the machine
212
+ def destroy_machine(action_handler, machine_spec, machine_options)
213
+ instance = instance_for(machine_spec)
214
+ if !instance.nil?
215
+ action_handler.perform_action "destroyed machine #{machine_spec.name} (#{machine_spec.reference['instance_id']})" do
216
+ instance.delete
217
+ 1.upto(10) do
218
+ instance.info
219
+ break if instance.state_str == 'DONE'
220
+ Chef::Log.debug("Waiting for VM '#{instance.id}' to be in 'DONE' state: '#{instance.state_str}'")
221
+ sleep(2)
222
+ end
223
+ fail "Failed to destroy '#{instance.name}'. Current state: #{instance.state_str}" if instance.state_str != 'DONE'
224
+ end
225
+ else
226
+ Chef::Log.info("vm #{machine_spec.name} (#{machine_spec.reference['instance_id']}) does not exist - nothing to do")
227
+ end
228
+ strategy = convergence_strategy_for(machine_spec, machine_options)
229
+ strategy.cleanup_convergence(action_handler, machine_spec)
230
+ end
231
+
232
+ # Stop the given machine.
233
+ #
234
+ # @param [Chef::Provisioning::ActionHandler] action_handler
235
+ # The action_handler object that is calling this method
236
+ # @param [Chef::Provisioning::MachineSpec] machine_spec
237
+ # A machine specification representing this machine.
238
+ # @param [Hash] machine_options
239
+ # A set of options representing the desired state of the machine
240
+ def stop_machine(action_handler, machine_spec, machine_options)
241
+ instance = instance_for(machine_spec)
242
+ if !instance.nil?
243
+ action_handler.perform_action "powered off machine #{machine_spec.name} (#{machine_spec.reference['instance_id']})" do
244
+ if machine_spec.reference[:is_shutdown] || machine_options.bootstrap_options[:is_shutdown]
245
+ hard = machine_spec.reference[:shutdown_hard] || machine_options.bootstrap_options[:shutdown_hard] || false
246
+ instance.shutdown(hard)
247
+ else
248
+ instance.stop
249
+ end
250
+ end
251
+ else
252
+ Chef::Log.info("vm #{machine_spec.name} (#{machine_spec.reference['instance_id']}) does not exist - nothing to do")
253
+ end
254
+ end
255
+
256
+ # Allocate an image. Returns quickly with an ID that tracks the image.
257
+ #
258
+ # @param [Chef::Provisioning::ActionHandler] action_handler
259
+ # The action_handler object that is calling this method
260
+ # @param [Chef::Provisioning::ImageSpec] image_spec
261
+ # A machine specification representing this machine.
262
+ # @param [Hash] image_options
263
+ # A set of options representing the desired state of the machine
264
+ def allocate_image(action_handler, image_spec, image_options, machine_spec)
265
+ if image_spec.reference
266
+ # check if image already exists
267
+ image = @one.get_resource('img', :id => image_spec.reference['image_id'].to_i)
268
+ action_handler.report_progress "image #{image_spec.name} (ID: #{image_spec.reference['image_id']}) already exists" unless image.nil?
269
+ else
270
+ action_handler.perform_action "create image #{image_spec.name} from machine ID #{machine_spec.reference['instance_id']} with options #{image_options.inspect}" do
271
+ vm = @one.get_resource('vm', :id => machine_spec.reference['instance_id'])
272
+ fail "allocate_image: VM does not exist" if vm.nil?
273
+ # set default disk ID
274
+ disk_id = 1
275
+ if image_options.disk_id
276
+ disk_id = image_options.disk_id.is_a?(Integer) ? image_options.disk_id : @one.get_disk_id(vm, new_resource.disk_id)
277
+ end
278
+
279
+ new_img = vm.disk_snapshot(disk_id, image_spec.name, "", true)
280
+ fail "Failed to create snapshot '#{new_resource.name}': #{new_img.message}" if OpenNebula.is_error?(new_img)
281
+ populate_img_object(image_spec, new_image)
282
+ end
283
+ end
284
+ end
285
+
286
+ # Ready an image, waiting till the point where it is ready to be used.
287
+ #
288
+ # @param [Chef::Provisioning::ActionHandler] action_handler
289
+ # The action_handler object that is calling this method
290
+ # @param [Chef::Provisioning::ImageSpec] image_spec
291
+ # A machine specification representing this machine.
292
+ # @param [Hash] image_options
293
+ # A set of options representing the desired state of the machine
294
+ def ready_image(action_handler, image_spec, _image_options)
295
+ img = @one.get_resource('img', :id => image_spec.reference['image_id'].to_i)
296
+ fail "Image #{image_spec.name} (#{image_spec.reference['image_id']}) does not exist" if img.nil?
297
+ action_handler.perform_action "image #{image_spec.name} is ready" do
298
+ deployed = @one.wait_for_img(img.name, img.id)
299
+ image_spec.reference['state'] = deployed.state_str
300
+ end
301
+ img
302
+ end
303
+
304
+ # Destroy an image using this service.
305
+ #
306
+ # @param [Chef::Provisioning::ActionHandler] action_handler
307
+ # The action_handler object that is calling this method
308
+ # @param [Chef::Provisioning::ImageSpec] image_spec
309
+ # A machine specification representing this machine.
310
+ # @param [Hash] image_options
311
+ # A set of options representing the desired state of the machine
312
+ def destroy_image(action_handler, image_spec, _image_options)
313
+ img = @one.get_resource('img', :id => image_spec.location['image_id'].to_i)
314
+ if img.nil?
315
+ action_handler.report_progress "image #{image_spec.name} (#{image_spec.location['image_id']}) does not exist - nothing to do"
316
+ else
317
+ action_handler.perform_action "deleted image #{image_spec.name} (#{image_spec.location['image_id']})" do
318
+ rc = img.delete
319
+ fail "Failed to delete image '#{image_spec.name}' : #{rc.message}" if OpenNebula.is_error?(rc)
320
+ end
321
+ end
322
+ end
323
+
324
+ #
325
+ # Optional interface methods
326
+ #
327
+
328
+ #
329
+ # Allocate a set of machines. This should have the same effect as
330
+ # running allocate_machine on all machine_specs.
331
+ #
332
+ # Drivers do not need to implement this; the default implementation
333
+ # calls acquire_machine in parallel.
334
+ #
335
+ # == Parallelizing
336
+ #
337
+ # The parallelizer must implement #parallelize
338
+ # @example Example parallelizer
339
+ # parallelizer.parallelize(specs_and_options) do |machine_spec|
340
+ # allocate_machine(action_handler, machine_spec)
341
+ # end.to_a
342
+ # # The to_a at the end causes you to wait until the
343
+ # parallelization is done
344
+ #
345
+ # This object is shared among other chef-provisioning actions, ensuring
346
+ # that you do not go over parallelization limits set by the user. Use
347
+ # of the parallelizer to parallelizer machines is not required.
348
+ #
349
+ # == Passing a block
350
+ #
351
+ # If you pass a block to this function, each machine will be yielded
352
+ # to you as it completes, and then the function will return when
353
+ # all machines are yielded.
354
+ #
355
+ # @example Passing a block
356
+ # allocate_machines(
357
+ # action_handler,
358
+ # specs_and_options,
359
+ # parallelizer) do |machine_spec|
360
+ # ...
361
+ # end
362
+ #
363
+ # @param [Chef::Provisioning::ActionHandler] action_handler
364
+ # The action_handler object that is calling this method; this
365
+ # is generally a driver, but could be anything that can support
366
+ # the interface (i.e., in the case of the test kitchen
367
+ # provisioning driver for acquiring and destroying VMs).
368
+ # @param [Hash] specs_and_options
369
+ # A hash of machine_spec -> machine_options representing the
370
+ # machines to allocate.
371
+ # @param [Parallelizer] parallelizer an object with a
372
+ # parallelize() method that works like this:
373
+ # @return [Array<Machine>] An array of machine objects created
374
+ def allocate_machines(action_handler, specs_and_options, parallelizer)
375
+ parallelizer.parallelize(specs_and_options) do |machine_spec, machine_options|
376
+ allocate_machine(add_prefix(machine_spec, action_handler), machine_spec, machine_options)
377
+ yield machine_spec if block_given?
378
+ machine_spec
379
+ end.to_a
380
+ end
381
+
382
+ # Ready machines in batch, in parallel if possible.
383
+ def ready_machines(action_handler, specs_and_options, parallelizer)
384
+ parallelizer.parallelize(specs_and_options) do |machine_spec, machine_options|
385
+ machine = ready_machine(add_prefix(machine_spec, action_handler), machine_spec, machine_options)
386
+ yield machine if block_given?
387
+ machine
388
+ end.to_a
389
+ end
390
+
391
+ # Stop machines in batch, in parallel if possible.
392
+ def stop_machines(action_handler, specs_and_options, parallelizer)
393
+ parallelizer.parallelize(specs_and_options) do |machine_spec, machine_options|
394
+ stop_machine(add_prefix(machine_spec, action_handler), machine_spec, machine_options)
395
+ yield machine_spec if block_given?
396
+ end.to_a
397
+ end
398
+
399
+ # Delete machines in batch, in parallel if possible.
400
+ def destroy_machines(action_handler, specs_and_options, parallelizer)
401
+ parallelizer.parallelize(specs_and_options) do |machine_spec, machine_options|
402
+ destroy_machine(add_prefix(machine_spec, action_handler), machine_spec, machine_options)
403
+ yield machine_spec if block_given?
404
+ end.to_a
405
+ end
406
+
407
+ # Allocate a load balancer
408
+ # @param [ChefMetal::ActionHandler] action_handler The action handler
409
+ # @param [ChefMetal::LoadBalancerSpec] lb_spec Frozen LB specification
410
+ # @param [Hash] lb_options A hash of options to pass the LB
411
+ # @param [Array[ChefMetal::MachineSpec]] machine_specs
412
+ # An array of machine specs the load balancer should have
413
+ def allocate_load_balancer(_action_handler, _lb_spec, _lb_options, _machine_specs)
414
+ end
415
+
416
+ # Make the load balancer ready
417
+ # @param [ChefMetal::ActionHandler] action_handler The action handler
418
+ # @param [ChefMetal::LoadBalancerSpec] lb_spec Frozen LB specification
419
+ # @param [Hash] lb_options A hash of options to pass the LB
420
+ def ready_load_balancer(_action_handler, _lb_spec, _lb_options, _machine_specs)
421
+ end
422
+
423
+ # Destroy the load balancer
424
+ # @param [ChefMetal::ActionHandler] action_handler The action handler
425
+ # @param [ChefMetal::LoadBalancerSpec] lb_spec Frozen LB specification
426
+ # @param [Hash] lb_options A hash of options to pass the LB
427
+ def destroy_load_balancer(_action_handler, _lb_spec, _lb_options)
428
+ end
429
+
430
+ protected
431
+
432
+ def one_credentials
433
+ @one_credentials ||= begin
434
+ credentials = Credentials.new(driver_options[:one_options] || {})
435
+ if driver_options[:credentials]
436
+ credentials.load_plain(driver_options[:credentials], driver_options[:one_options] || {})
437
+ elsif driver_options[:secret_file]
438
+ credentials.load_file(driver_options[:secret_file], driver_options[:one_options] || {})
439
+ end
440
+ credentials
441
+ end
442
+ end
443
+
444
+ def check_unique_names(bootstrap_options, machine_spec)
445
+ fail "VM with name '#{machine_spec.name}' already exists" unless @one.get_resource('vm', :name => machine_spec.name).nil? if bootstrap_options[:unique_names]
446
+ end
447
+
448
+ def populate_node_object(machine_spec, machine_options, vm)
449
+ machine_spec.driver_url = driver_url
450
+ machine_spec.reference = {
451
+ 'driver_version' => Chef::Provisioning::OpenNebulaDriver::VERSION,
452
+ 'allocated_at' => Time.now.utc.to_s,
453
+ 'image_id' => machine_options.bootstrap_options[:image_id] || nil,
454
+ 'is_shutdown' => machine_options.bootstrap_options[:is_shutdown] || false,
455
+ 'shutdown_hard' => machine_options.bootstrap_options[:shutdown_hard] || false,
456
+ 'instance_id' => vm.id,
457
+ 'name' => vm.name,
458
+ 'state' => vm.state_str
459
+ }
460
+ # handle ssh_user and ssh_username for backward compatibility
461
+ Chef::Log.warn("':ssh_user' will be deprecated in next version in favour of ':ssh_username'") if machine_options.key?(:ssh_user)
462
+ machine_spec.reference['ssh_username'] = get_ssh_user(machine_spec, machine_options)
463
+ %w(is_windows sudo transport_address_location ssh_gateway).each do |key|
464
+ machine_spec.reference[key] = machine_options[key.to_sym] if machine_options[key.to_sym]
465
+ end
466
+ end
467
+
468
+ def populate_img_object(image_spec, new_image)
469
+ image_spec.driver_url = driver_url
470
+ image_spec.reference = {
471
+ 'driver_version' => Chef::Provisioning::OpenNebulaDriver::VERSION,
472
+ 'image_id' => new_image,
473
+ 'allocated_at' => Time.now.to_i,
474
+ 'state' => 'none'
475
+ }
476
+ image_spec.machine_options ||= {}
477
+ image_spec.machine_options.merge!(:bootstrap_options => { :image_id => new_image })
478
+ end
479
+
480
+ def instance_for(machine_spec)
481
+ instance = nil
482
+ if machine_spec.reference
483
+ fail "Switching a machine's driver from #{machine_spec.driver_url} to #{driver_url} is not supported!" \
484
+ " Use machine :destroy and then :create the machine on the new driver." if machine_spec.driver_url != driver_url
485
+ instance = @one.get_resource('vm', :id => machine_spec.reference['instance_id'].to_i)
486
+ elsif machine_spec.location
487
+ fail "Switching a machine's driver from #{machine_spec.driver_url} to #{driver_url} is not supported!" \
488
+ " Use machine :destroy and then :create the machine on the new driver." if machine_spec.driver_url != driver_url
489
+ instance = @one.get_resource('vm', :id => machine_spec.location['server_id'].to_i)
490
+ unless instance.nil?
491
+ # Convert from previous driver
492
+ machine_spec.reference = {
493
+ 'driver_version' => machine_spec.location['driver_version'],
494
+ 'allocated_at' => machine_spec.location['allocated_at'],
495
+ 'image_id' => machine_spec.location['image_id'],
496
+ 'instance_id' => machine_spec.location['server_id'],
497
+ 'name' => machine_spec.location['name'],
498
+ 'state' => machine_spec.location['state']
499
+ }
500
+ end
501
+ end
502
+ instance
503
+ end
504
+
505
+ def machine_for(machine_spec, machine_options)
506
+ instance = instance_for(machine_spec)
507
+ fail "#{machine_spec.name} (#{machine_spec.reference['instance_id']}) does not exist!" if instance.nil?
508
+ # TODO: Support Windoze VMs (see chef-provisioning-vagrant)
509
+ Chef::Provisioning::Machine::UnixMachine.new(machine_spec, transport_for(machine_spec, machine_options, instance), convergence_strategy_for(machine_spec, machine_options))
510
+ end
511
+
512
+ def get_ssh_user(machine_spec, machine_options)
513
+ # handle ssh_user and ssh_username for backward compatibility
514
+ Chef::Log.warn("':ssh_user' will be deprecated in next version in favour of ':ssh_username'") if machine_options.key?(:ssh_user)
515
+ machine_spec.reference['ssh_username'] || machine_options[:ssh_username] || machine_options[:ssh_user] || 'local'
516
+ end
517
+
518
+ def transport_for(machine_spec, machine_options, _instance)
519
+ # TODO: Store ssh_options in machine_spec.reference ???
520
+ ssh_options = {
521
+ :keys_only => false,
522
+ :forward_agent => true,
523
+ :use_agent => true,
524
+ :user_known_hosts_file => '/dev/null'
525
+ }.merge(machine_options[:ssh_options] || {})
526
+ username = get_ssh_user(machine_spec, machine_options)
527
+ conf = machine_options[:ssh_config] || config
528
+ options = {}
529
+ if machine_spec.reference[:sudo] || (!machine_spec.reference.key?(:sudo) && username != 'root')
530
+ options[:prefix] = 'sudo '
531
+ end
532
+
533
+ # Enable pty by default
534
+ options[:ssh_pty_enable] = true
535
+ options[:ssh_gateway] = machine_spec.reference['ssh_gateway'] if machine_spec.reference.key?('ssh_gateway')
536
+
537
+ transport = Chef::Provisioning::Transport::SSH.new(machine_spec.reference['ip'], username, ssh_options, options, conf)
538
+
539
+ # wait up to 5 min to establish SSH connection
540
+ 100.times do
541
+ break if transport.available?
542
+ sleep 3
543
+ Chef::Log.debug("Waiting for SSH server ...")
544
+ end
545
+ fail "Failed to establish SSH connection to '#{machine_spec.name}'" unless transport.available?
546
+ transport
547
+ end
548
+
549
+ def convergence_strategy_for(machine_spec, machine_options)
550
+ # TODO: Support Windoze VMs (see chef-provisioning-vagrant)
551
+ convergence_options = Cheffish::MergedConfig.new(machine_options[:convergence_options] || {})
552
+
553
+ if !machine_spec.reference
554
+ Chef::Provisioning::ConvergenceStrategy::NoConverge.new(convergence_options, config)
555
+ elsif machine_options[:cached_installer] == true
556
+ Chef::Provisioning::ConvergenceStrategy::InstallCached.new(convergence_options, config)
557
+ else
558
+ Chef::Provisioning::ConvergenceStrategy::InstallSh.new(convergence_options, config)
559
+ end
560
+ end
561
+
562
+ def add_prefix(machine_spec, action_handler)
563
+ AddPrefixActionHandler.new(action_handler, "[#{machine_spec.name}] ")
564
+ end
565
+
566
+ def get_private_key(name)
567
+ Cheffish.get_private_key(name, config)
568
+ end
569
+ end
570
+ end
571
+ end
572
+ end