chef-provisioning-opennebula 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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