chef-metal-crowbar 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0425f5008268f505bb329da8705c9ab3720d44c7
4
+ data.tar.gz: aabb37caae2cf94343380b0cdb1c6ab30292775d
5
+ SHA512:
6
+ metadata.gz: 8fc75b0411a5d4f785c582fba068371751448ae714f35611292ca1c96f3cedcdf4e60a7b5600e898dd937668831d1888c7440ea33c86cac6a31f63d12320fa08
7
+ data.tar.gz: 3ea6801e6e7e8d5bf91d75fa735329f79087b5b145292172960e8905ea4b6aae1b346ff790e15e10542e2b620b88101d252268230f74250d271ae05ea8828f5a
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ # Apache 2
2
+
3
+ This submodule of OpenCrowbar is Apache 2 licensed.
4
+
5
+ ## Copyright 2014, Rob Hirschfeld
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
18
+
19
+
20
+ ## For details
21
+
22
+ See [[doc/licenses/README.md]]
@@ -0,0 +1,3 @@
1
+ # OpenCrowbar for Chef-Metal
2
+
3
+ This repo contains the interface between Chef Metal (https://github.com/opscode/chef-metal/) and OpenCrowbar.
@@ -0,0 +1,22 @@
1
+ #!/bin/bash
2
+ # Copyright 2014, Rob Hirschfeld
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ require 'bundler'
17
+ require 'bundler'
18
+ require 'bundler/gem_tasks'
19
+
20
+ task :spec do
21
+ require File.expand_path('spec/run')
22
+ end
@@ -0,0 +1,217 @@
1
+ # Copyright 2014, Rob Hirschfeld
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/provider/lwrp_base'
16
+ require 'chef_metal_crowbar/crowbar_driver'
17
+
18
+ class Chef::Provider::CrowbarKeyPair < Chef::Provider::LWRPBase
19
+
20
+ use_inline_resources
21
+
22
+ def whyrun_supported?
23
+ true
24
+ end
25
+
26
+ action :create do
27
+ create_key(:create)
28
+ end
29
+
30
+ action :delete do
31
+ if current_resource_exists?
32
+ converge_by "delete #{key_description}" do
33
+ case new_driver.compute_options[:provider]
34
+ when 'DigitalOcean'
35
+ compute.destroy_key_pair(@current_id)
36
+ when 'OpenStack', 'Rackspace'
37
+ compute.key_pairs.destroy(@current_id)
38
+ else
39
+ compute.key_pairs.delete(new_resource.name)
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ def key_description
46
+ "#{new_resource.name} on #{new_driver.driver_url}"
47
+ end
48
+
49
+ @@use_pkcs8 = nil # For Ruby 1.9 and below, PKCS can be run
50
+
51
+ def create_key(action)
52
+ if @should_create_directory
53
+ Cheffish.inline_resource(self, action) do
54
+ directory run_context.config[:private_key_write_path]
55
+ end
56
+ end
57
+
58
+ if current_resource_exists?
59
+ # If the public keys are different, update the server public key
60
+ if !current_resource.private_key_path
61
+ if new_resource.allow_overwrite
62
+ ensure_keys(action)
63
+ else
64
+ raise "#{key_description} already exists on the server, but the private key #{new_private_key_path} does not exist!"
65
+ end
66
+ else
67
+ ensure_keys(action)
68
+ end
69
+
70
+ case new_driver.compute_options[:provider]
71
+ when 'DigitalOcean'
72
+ new_fingerprints = [Cheffish::KeyFormatter.encode(desired_key, :format => :openssh)]
73
+ when 'OpenStack', 'Rackspace'
74
+ new_fingerprints = [Cheffish::KeyFormatter.encode(desired_key, :format => :openssh)]
75
+ else
76
+ # “The nice thing about standards is that you have so many to
77
+ # choose from.” - Andrew S. Tanenbaum
78
+ #
79
+ # The AWS EC2 API uses a PKCS#1 MD5 fingerprint for keys that you
80
+ # import into EC2, but a PKCS#8 SHA1 fingerprint for keys that you
81
+ # generate using its web console. Both fingerprints are different
82
+ # from the familiar RFC4716 MD5 fingerprint that OpenSSH displays
83
+ # for host keys.
84
+ #
85
+ # So compute both possible AWS fingerprints and check if either of
86
+ # them matches.
87
+ new_fingerprints = [Cheffish::KeyFormatter.encode(desired_key, :format => :fingerprint)]
88
+ if RUBY_VERSION.to_f < 2.0
89
+ if @@use_pkcs8.nil?
90
+ begin
91
+ require 'openssl_pkcs8'
92
+ @@use_pkcs8 = true
93
+ rescue LoadError
94
+ Chef::Log.warn("The openssl_pkcs8 gem is not loaded: you may not be able to read key fingerprints created by some cloud providers. gem install openssl_pkcs8 to fix!")
95
+ @@use_pkcs8 = false
96
+ end
97
+ end
98
+ if @@use_pkcs8
99
+ new_fingerprints << Cheffish::KeyFormatter.encode(desired_private_key,
100
+ :format => :pkcs8sha1fingerprint)
101
+ end
102
+ end
103
+ end
104
+
105
+ if !new_fingerprints.any? { |f| compare_public_key f }
106
+ if new_resource.allow_overwrite
107
+ converge_by "update #{key_description} to match local key at #{new_resource.private_key_path}" do
108
+ compute.key_pairs.get(new_resource.name).destroy
109
+ compute.import_key_pair(new_resource.name, Cheffish::KeyFormatter.encode(desired_key, :format => :openssh))
110
+ end
111
+ else
112
+ raise "#{key_description} with fingerprint #{@current_fingerprint} does not match local key fingerprint(s) #{new_fingerprints}, and allow_overwrite is false!"
113
+ end
114
+ end
115
+ else
116
+ # Generate the private and/or public keys if they do not exist
117
+ ensure_keys(action)
118
+
119
+ # Create key
120
+ converge_by "create #{key_description} from local key at #{new_resource.private_key_path}" do
121
+ compute.import_key_pair(new_resource.name, Cheffish::KeyFormatter.encode(desired_key, :format => :openssh))
122
+ end
123
+ end
124
+ end
125
+
126
+ def new_driver
127
+ run_context.chef_metal.driver_for(new_resource.driver)
128
+ end
129
+
130
+ def ensure_keys(action)
131
+ resource = new_resource
132
+ private_key_path = new_private_key_path
133
+ Cheffish.inline_resource(self, action) do
134
+ private_key private_key_path do
135
+ public_key_path resource.public_key_path
136
+ if resource.private_key_options
137
+ resource.private_key_options.each_pair do |key,value|
138
+ send(key, value)
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+ def desired_key
146
+ @desired_key ||= begin
147
+ if new_resource.public_key_path
148
+ public_key, format = Cheffish::KeyFormatter.decode(IO.read(new_resource.public_key_path))
149
+ public_key
150
+ else
151
+ desired_private_key.public_key
152
+ end
153
+ end
154
+ end
155
+
156
+ def desired_private_key
157
+ @desired_private_key ||= begin
158
+ private_key, format = Cheffish::KeyFormatter.decode(IO.read(new_private_key_path))
159
+ private_key
160
+ end
161
+ end
162
+
163
+ def current_resource_exists?
164
+ @current_resource.action != [ :delete ]
165
+ end
166
+
167
+ def compare_public_key(new)
168
+ c = @current_fingerprint.split[0,2].join(' ')
169
+ n = new.split[0,2].join(' ')
170
+ c == n
171
+ end
172
+
173
+ def compute
174
+ new_driver.compute
175
+ end
176
+
177
+ def current_public_key
178
+ current_resource.source_key
179
+ end
180
+
181
+ def new_private_key_path
182
+ private_key_path = new_resource.private_key_path || new_resource.name
183
+ if private_key_path.is_a?(Symbol)
184
+ private_key_path
185
+ elsif Pathname.new(private_key_path).relative? && new_driver.config[:private_key_write_path]
186
+ @should_create_directory = true
187
+ ::File.join(new_driver.config[:private_key_write_path], private_key_path)
188
+ else
189
+ private_key_path
190
+ end
191
+ end
192
+
193
+ def new_public_key_path
194
+ new_resource.public_key_path
195
+ end
196
+
197
+ def load_current_resource
198
+ if !new_driver.kind_of?(ChefMetalCrowbar::CrowbarDriver)
199
+ raise 'crowbar_key_pair only works with crowbar_driver'
200
+ end
201
+ @current_resource = Chef::Resource::CrowbarKeyPair.new(new_resource.name, run_context)
202
+ # there is only 1 provider, so there's no need to change it
203
+ current_key_pair = compute.key_pairs.get(new_resource.name)
204
+ if current_key_pair
205
+ @current_fingerprint = current_key_pair ? current_key_pair.fingerprint : nil
206
+ else
207
+ current_resource.action :delete
208
+ end
209
+
210
+ if new_private_key_path && ::File.exist?(new_private_key_path)
211
+ current_resource.private_key_path new_private_key_path
212
+ end
213
+ if new_public_key_path && ::File.exist?(new_public_key_path)
214
+ current_resource.public_key_path new_public_key_path
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,43 @@
1
+ # Copyright 2014, Rob Hirschfeld
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_metal'
16
+
17
+ class Chef::Resource::CrowbarKeyPair < Chef::Resource::LWRPBase
18
+ self.resource_name = 'crowbar_key_pair'
19
+
20
+ def initialize(*args)
21
+ super
22
+ @driver = run_context.chef_metal.current_driver
23
+ end
24
+
25
+ actions :create, :delete, :nothing
26
+ default_action :create
27
+
28
+ attribute :driver
29
+ # Private key to use as input (will be generated if it does not exist)
30
+ attribute :private_key_path, :kind_of => String
31
+ # Public key to use as input (will be generated if it does not exist)
32
+ attribute :public_key_path, :kind_of => String
33
+ # List of parameters to the private_key resource used for generation of the key
34
+ attribute :private_key_options, :kind_of => Hash
35
+
36
+ # TODO what is the right default for this?
37
+ attribute :allow_overwrite, :kind_of => [TrueClass, FalseClass], :default => false
38
+
39
+ # Proc that runs after the resource completes. Called with (resource, private_key, public_key)
40
+ def after(&block)
41
+ block ? @after = block : @after
42
+ end
43
+ end
@@ -0,0 +1,17 @@
1
+ # Copyright 2014, Rob Hirschfeld
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_metal_crowbar/crowbar_driver'
16
+
17
+ ChefMetal.register_driver_class("crowbar", ChefMetalCrowbar::CrowbarDriver)
@@ -0,0 +1,17 @@
1
+ # Copyright 2014, Rob Hirschfeld
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_metal'
16
+ require 'chef_metal_crowbar/crowbar_driver'
17
+ require 'chef_metal_crowbar/recipe_dsl'
@@ -0,0 +1,696 @@
1
+ # Copyright 2014, Rob Hirschfeld
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_metal/driver'
16
+ require 'chef_metal/machine/windows_machine'
17
+ require 'chef_metal/machine/unix_machine'
18
+ require 'chef_metal/machine_spec'
19
+ require 'chef_metal/convergence_strategy/install_msi'
20
+ require 'chef_metal/convergence_strategy/install_sh'
21
+ require 'chef_metal/convergence_strategy/install_cached'
22
+ require 'chef_metal/convergence_strategy/no_converge'
23
+ require 'chef_metal/transport/ssh'
24
+ require 'chef_metal_crowbar/version'
25
+ require 'etc'
26
+ require 'time'
27
+ require 'cheffish/merged_config'
28
+ require 'chef_metal_crowbar/recipe_dsl'
29
+ require 'crowbar/core'
30
+
31
+ module ChefMetalCrowbar
32
+
33
+ class CrowbarDriver < ChefMetal::Driver
34
+
35
+ AVAILABLE_DEPLOYMENT = 'available'
36
+ RESERVED_DEPLOYMENT = 'reserved'
37
+ TARGET_NODE_ROLE = "crowbar-managed-node"
38
+ KEY_ATTRIB = "chef-server_admin_client_key"
39
+
40
+ # Passed in a driver_url, and a config in the format of Driver.config.
41
+ def self.from_url(driver_url, config)
42
+ CrowbarDriver.new(driver_url, config)
43
+ end
44
+
45
+ def initialize(driver_url, config)
46
+ super(driver_url, config)
47
+ end
48
+
49
+ def crowbar_api
50
+ # relies on url & driver_config from Driver superclass
51
+ scheme, crowbar_url = url.split(':', 2)
52
+ Crowbar::Core.connect crowbar_url, driver_config
53
+ end
54
+
55
+ # Acquire a machine, generally by provisioning it. Returns a Machine
56
+ # object pointing at the machine, allowing useful actions like setup,
57
+ # converge, execute, file and directory.
58
+ def allocate_machine(action_handler, machine_spec, machine_options)
59
+ Core.connect crowbar_url
60
+
61
+ # If the server does not exist, create it
62
+ create_servers(action_handler, { machine_spec => machine_options }, Chef::ChefFS::Parallelizer.new(0))
63
+ machine_spec
64
+ end
65
+
66
+ def allocate_machine(action_handler, machine_spec, machine_options)
67
+ if !crowbar_api.node_exists?(machine_spec.location['server_id'])
68
+ # It doesn't really exist
69
+ action_handler.perform_action "Machine #{machine_spec.location['server_id']} does not really exist. Recreating ..." do
70
+ machine_spec.location = nil
71
+ end
72
+ end
73
+ if !machine_spec.location
74
+ action_handler.perform_action "Creating server #{machine_spec.name} with options #{machine_options}" do
75
+ server = crowbar_api.allocate_server(machine_spec.name, machine_options)
76
+ server_id = server["id"]
77
+ machine_spec.location = {
78
+ 'driver_url' => driver_url,
79
+ 'driver_version' => ChefMetalCrowbar::VERSION,
80
+ 'server_id' => server_id,
81
+ 'bootstrap_key' => crowbar_api.ssh_private_key(server_id)
82
+ }
83
+ end
84
+ end
85
+ end
86
+
87
+ def ready_machine(action_handler, machine_spec, machine_options)
88
+ server_id = machine_spec.location['server_id']
89
+ server = crowbar_api.node(server_id)
90
+ if server["alive"] == 'false'
91
+ action_handler.perform_action "Powering up machine #{server_id}" do
92
+ crowbar_api.power(server_id, "on")
93
+ end
94
+ end
95
+
96
+ if server["state"] != 0
97
+ action_handler.perform_action "wait for machine #{server_id}" do
98
+ crowbar_api.wait_for_machine_to_have_status(server_id, 0)
99
+ end
100
+ end
101
+
102
+ # Return the Machine object
103
+ machine_for(machine_spec, machine_options)
104
+ end
105
+
106
+ def machine_for(machine_spec, machine_options)
107
+ server_id = machine_spec.location['server_id']
108
+ ssh_options = {
109
+ :auth_methods => ['publickey'],
110
+ :keys => [ get_key('bootstrapkey') ],
111
+ }
112
+ transport = ChefMetal::Transport::SSHTransport.new(server_id, ssh_options, {}, config)
113
+ convergence_strategy = ChefMetal::ConvergenceStrategy::InstallCached.new(machine_options[:convergence_options])
114
+ ChefMetal::Machine::UnixMachine.new(machine_spec, transport, convergence_strategy)
115
+ end
116
+
117
+ private
118
+
119
+
120
+ # hit node API to see if node exists (code 200 only)
121
+ def node_exists?(name)
122
+ exists?("node", name)
123
+ end
124
+
125
+ # hit deployment API to see if deployment exists (code 200 only)
126
+ def deployment_exists?(name)
127
+ exists?("deployment", name)
128
+ end
129
+
130
+ # using the attibutes, get the key
131
+ def ssh_private_key(name)
132
+ get(driver_url + API_BASE + "nodes/#{name}/attribs/#{KEY_ATTRIB}")
133
+ end
134
+
135
+ # follow getready process to allocate nodes
136
+ def allocate_node(name, machine_options)
137
+
138
+ # get available nodes
139
+ from_deployment = AVAILABLE_DEPLOYMENT
140
+ raise "Available Pool '#{from_deployment} does not exist" unless deployment_exists?(from_deployment)
141
+ pool = get(driver_url + API_BASE + "deployments/#{from_deployment}/nodes")
142
+ raise "No available nodes in pool #{from_deployment}" if pool.size == 0
143
+
144
+ # assign node from pool
145
+ node = pool[0]
146
+
147
+ # prepare for moving by moving the deployment to proposed
148
+ to_deployment = RESERVED_DEPLOYMENT
149
+ put(driver_url + API_BASE + "deployments/#{to_deployment}/propose")
150
+
151
+ # set alias (name) and reserve
152
+ node["alias"] = name
153
+ node["deployment"] = to_deployment
154
+ put(driver_url + API_BASE + "nodes/#{node["id"]}", node)
155
+
156
+ # bind the OS NodeRole if missing (eventually set the OS property)
157
+ bind = {:node=>node["id"], :role=>TARGET_NODE_ROLE, :deployment=>to_deployment}
158
+ # blindly add node role > we need to make this smarter and skip if unneeded
159
+ post(driver_url + API_BASE + "node_roles", bind)
160
+
161
+ # commit the deployment
162
+ put(driver_url + API_BASE + "deployments/#{to_deployment}/commit")
163
+
164
+ # at this point Crowbar will bring up the node in the background
165
+ # we can return the node handle to the user
166
+ node["name"]
167
+
168
+ end
169
+
170
+ def node(name)
171
+ get(driver_url + API_BASE + "nodes/#{name}")
172
+ end
173
+
174
+ def power(name, action="on")
175
+ put(driver_url + API_BASE + "nodes/#{name}/power?poweraction=#{action}")
176
+ end
177
+
178
+ def wait_for_machine_to_have_status(name, target_state)
179
+ node(name)["state"] == target_state
180
+ end
181
+
182
+ # debug messages
183
+ def debug(msg)
184
+ Chef::Log.debug msg
185
+ end
186
+
187
+
188
+ # include Chef::Mixin::ShellOut
189
+
190
+ # DEFAULT_OPTIONS = {
191
+ # :create_timeout => 180,
192
+ # :start_timeout => 180,
193
+ # :ssh_timeout => 20
194
+ # }
195
+
196
+ # class << self
197
+ # alias :__new__ :new
198
+
199
+ # def inherited(klass)
200
+ # class << klass
201
+ # alias :new :__new__
202
+ # end
203
+ # end
204
+ # end
205
+
206
+ # @@registered_provider_classes = {}
207
+ # def self.register_provider_class(name, driver)
208
+ # @@registered_provider_classes[name] = driver
209
+ # end
210
+
211
+ # def self.provider_class_for(provider)
212
+ # require "chef_metal_crowbar/providers/#{provider.downcase}"
213
+ # @@registered_provider_classes[provider]
214
+ # end
215
+
216
+ # def self.new(driver_url, config)
217
+ # provider = driver_url.split(':')[1]
218
+ # provider_class_for(provider).new(driver_url, config)
219
+ # end
220
+
221
+ # def self.canonicalize_url(driver_url, config)
222
+ # _, provider, id = driver_url.split(':', 3)
223
+ # config, id = provider_class_for(provider).compute_options_for(provider, id, config)
224
+ # [ "crowbar:#{provider}:#{id}", config ]
225
+ # end
226
+
227
+ # # Passed in a config which is *not* merged with driver_url (because we don't
228
+ # # know what it is yet) but which has the same keys
229
+ # def self.from_provider(provider, config)
230
+ # provider ||= "core"
231
+ # # Figure out the options and merge them into the config
232
+ # config, id = provider_class_for(provider).compute_options_for(provider, nil, config)
233
+
234
+ # driver_url = "crowbar:#{provider}:#{id}"
235
+
236
+ # ChefMetal.driver_for_url(driver_url, config)
237
+ # end
238
+
239
+ # def compute_options
240
+ # driver_options[:compute_options].to_hash || {}
241
+ # end
242
+
243
+ # def provider
244
+ # compute_options[:provider]
245
+ # end
246
+
247
+ # # Acquire a machine, generally by provisioning it. Returns a Machine
248
+ # # object pointing at the machine, allowing useful actions like setup,
249
+ # # converge, execute, file and directory.
250
+ # def allocate_machine(action_handler, machine_spec, machine_options)
251
+ # # If the server does not exist, create it
252
+ # create_servers(action_handler, { machine_spec => machine_options }, Chef::ChefFS::Parallelizer.new(0))
253
+ # machine_spec
254
+ # end
255
+
256
+ # def allocate_machines(action_handler, specs_and_options, parallelizer)
257
+ # create_servers(action_handler, specs_and_options, parallelizer) do |machine_spec, server|
258
+ # yield machine_spec
259
+ # end
260
+ # specs_and_options.keys
261
+ # end
262
+
263
+ # def ready_machine(action_handler, machine_spec, machine_options)
264
+ # server = server_for(machine_spec)
265
+ # if server.nil?
266
+ # raise "Machine #{machine_spec.name} does not have a server associated with it, or server does not exist."
267
+ # end
268
+
269
+ # # Attach floating IPs if necessary
270
+ # attach_floating_ips(action_handler, machine_spec, machine_options, server)
271
+
272
+ # # Start the server if needed, and wait for it to start
273
+ # start_server(action_handler, machine_spec, server)
274
+ # wait_until_ready(action_handler, machine_spec, machine_options, server)
275
+ # begin
276
+ # wait_for_transport(action_handler, machine_spec, machine_options, server)
277
+ # rescue Crowbar::Errors::TimeoutError
278
+ # # Only ever reboot once, and only if it's been less than 10 minutes since we stopped waiting
279
+ # if machine_spec.location['started_at'] || remaining_wait_time(machine_spec, machine_options) < -(10*60)
280
+ # raise
281
+ # else
282
+ # # Sometimes (on EC2) the machine comes up but gets stuck or has
283
+ # # some other problem. If this is the case, we restart the server
284
+ # # to unstick it. Reboot covers a multitude of sins.
285
+ # 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 ..."
286
+ # restart_server(action_handler, machine_spec, server)
287
+ # wait_until_ready(action_handler, machine_spec, machine_options, server)
288
+ # wait_for_transport(action_handler, machine_spec, machine_options, server)
289
+ # end
290
+ # end
291
+
292
+ # machine_for(machine_spec, machine_options, server)
293
+ # end
294
+
295
+ # # Connect to machine without acquiring it
296
+ # def connect_to_machine(machine_spec, machine_options)
297
+ # machine_for(machine_spec, machine_options)
298
+ # end
299
+
300
+ # def destroy_machine(action_handler, machine_spec, machine_options)
301
+ # server = server_for(machine_spec)
302
+ # if server
303
+ # action_handler.perform_action "destroy machine #{machine_spec.name} (#{machine_spec.location['server_id']} at #{driver_url})" do
304
+ # server.destroy
305
+ # machine_spec.location = nil
306
+ # end
307
+ # end
308
+ # strategy = convergence_strategy_for(machine_spec, machine_options)
309
+ # strategy.cleanup_convergence(action_handler, machine_spec)
310
+ # end
311
+
312
+ # def stop_machine(action_handler, machine_spec, machine_options)
313
+ # server = server_for(machine_spec)
314
+ # if server
315
+ # action_handler.perform_action "stop machine #{machine_spec.name} (#{server.id} at #{driver_url})" do
316
+ # server.stop
317
+ # end
318
+ # end
319
+ # end
320
+
321
+ # def compute
322
+ # @compute ||= Crowbar::Compute.new(compute_options)
323
+ # end
324
+
325
+ # # Not meant to be part of public interface
326
+ # def transport_for(machine_spec, machine_options, server)
327
+ # # TODO winrm
328
+ # create_ssh_transport(machine_spec, machine_options, server)
329
+ # end
330
+
331
+ # protected
332
+
333
+ # def option_for(machine_options, key)
334
+ # machine_options[key] || DEFAULT_OPTIONS[key]
335
+ # end
336
+
337
+ # def creator
338
+ # raise "unsupported provider #{provider} (please implement #creator)"
339
+ # end
340
+
341
+ # def create_servers(action_handler, specs_and_options, parallelizer, &block)
342
+ # specs_and_servers = servers_for(specs_and_options.keys)
343
+
344
+ # # Get the list of servers which exist, segmented by their bootstrap options
345
+ # # (we will try to create a set of servers for each set of bootstrap options
346
+ # # with create_many)
347
+ # by_bootstrap_options = {}
348
+ # specs_and_options.each do |machine_spec, machine_options|
349
+ # server = specs_and_servers[machine_spec]
350
+ # if server
351
+ # if %w(terminated archive).include?(server.state) # Can't come back from that
352
+ # Chef::Log.warn "Machine #{machine_spec.name} (#{server.id} on #{driver_url}) is terminated. Recreating ..."
353
+ # else
354
+ # yield machine_spec, server if block_given?
355
+ # next
356
+ # end
357
+ # elsif machine_spec.location
358
+ # Chef::Log.warn "Machine #{machine_spec.name} (#{machine_spec.location['server_id']} on #{driver_url}) no longer exists. Recreating ..."
359
+ # end
360
+
361
+ # bootstrap_options = bootstrap_options_for(action_handler, machine_spec, machine_options)
362
+ # by_bootstrap_options[bootstrap_options] ||= []
363
+ # by_bootstrap_options[bootstrap_options] << machine_spec
364
+ # end
365
+
366
+ # # Create the servers in parallel
367
+ # parallelizer.parallelize(by_bootstrap_options) do |bootstrap_options, machine_specs|
368
+ # machine_description = if machine_specs.size == 1
369
+ # "machine #{machine_specs.first.name}"
370
+ # else
371
+ # "machines #{machine_specs.map { |s| s.name }.join(", ")}"
372
+ # end
373
+ # description = [ "creating #{machine_description} on #{driver_url}" ]
374
+ # bootstrap_options.each_pair { |key,value| description << " #{key}: #{value.inspect}" }
375
+ # action_handler.report_progress description
376
+
377
+ # if action_handler.should_perform_actions
378
+ # # Actually create the servers
379
+ # create_many_servers(machine_specs.size, bootstrap_options, parallelizer) do |server|
380
+
381
+ # # Assign each one to a machine spec
382
+ # machine_spec = machine_specs.pop
383
+ # machine_options = specs_and_options[machine_spec]
384
+ # machine_spec.location = {
385
+ # 'driver_url' => driver_url,
386
+ # 'driver_version' => ChefMetalCrowbar::VERSION,
387
+ # 'server_id' => server.id,
388
+ # 'creator' => creator,
389
+ # 'allocated_at' => Time.now.to_i
390
+ # }
391
+ # machine_spec.location['key_name'] = bootstrap_options[:key_name] if bootstrap_options[:key_name]
392
+ # %w(is_windows ssh_username sudo use_private_ip_for_ssh ssh_gateway).each do |key|
393
+ # machine_spec.location[key] = machine_options[key.to_sym] if machine_options[key.to_sym]
394
+ # end
395
+ # action_handler.performed_action "machine #{machine_spec.name} created as #{server.id} on #{driver_url}"
396
+
397
+ # yield machine_spec, server if block_given?
398
+ # end
399
+
400
+ # if machine_specs.size > 0
401
+ # raise "Not all machines were created by create_many_machines!"
402
+ # end
403
+ # end
404
+ # end.to_a
405
+ # end
406
+
407
+ # def create_many_servers(num_servers, bootstrap_options, parallelizer)
408
+ # parallelizer.parallelize(1.upto(num_servers)) do |i|
409
+ # server = compute.servers.create(bootstrap_options)
410
+ # yield server if block_given?
411
+ # server
412
+ # end.to_a
413
+ # end
414
+
415
+ # def start_server(action_handler, machine_spec, server)
416
+ # # If it is stopping, wait for it to get out of "stopping" transition state before starting
417
+ # if server.state == 'stopping'
418
+ # action_handler.report_progress "wait for #{machine_spec.name} (#{server.id} on #{driver_url}) to finish stopping ..."
419
+ # server.wait_for { server.state != 'stopping' }
420
+ # action_handler.report_progress "#{machine_spec.name} is now stopped"
421
+ # end
422
+
423
+ # if server.state == 'stopped'
424
+ # action_handler.perform_action "start machine #{machine_spec.name} (#{server.id} on #{driver_url})" do
425
+ # server.start
426
+ # machine_spec.location['started_at'] = Time.now.to_i
427
+ # end
428
+ # machine_spec.save(action_handler)
429
+ # end
430
+ # end
431
+
432
+ # def restart_server(action_handler, machine_spec, server)
433
+ # action_handler.perform_action "restart machine #{machine_spec.name} (#{server.id} on #{driver_url})" do
434
+ # server.reboot
435
+ # machine_spec.location['started_at'] = Time.now.to_i
436
+ # end
437
+ # machine_spec.save(action_handler)
438
+ # end
439
+
440
+ # def remaining_wait_time(machine_spec, machine_options)
441
+ # if machine_spec.location['started_at']
442
+ # timeout = option_for(machine_options, :start_timeout) - (Time.now.utc - parse_time(machine_spec.location['started_at']))
443
+ # else
444
+ # timeout = option_for(machine_options, :create_timeout) - (Time.now.utc - parse_time(machine_spec.location['allocated_at']))
445
+ # end
446
+ # timeout > 0 ? timeout : 0.01
447
+ # end
448
+
449
+ # def parse_time(value)
450
+ # if value.is_a?(String)
451
+ # Time.parse(value)
452
+ # else
453
+ # Time.at(value)
454
+ # end
455
+ # end
456
+
457
+ # def wait_until_ready(action_handler, machine_spec, machine_options, server)
458
+ # if !server.ready?
459
+ # if action_handler.should_perform_actions
460
+ # action_handler.report_progress "waiting for #{machine_spec.name} (#{server.id} on #{driver_url}) to be ready ..."
461
+ # server.wait_for(remaining_wait_time(machine_spec, machine_options)) { ready? }
462
+ # action_handler.report_progress "#{machine_spec.name} is now ready"
463
+ # end
464
+ # end
465
+ # end
466
+
467
+ # def wait_for_transport(action_handler, machine_spec, machine_options, server)
468
+ # transport = transport_for(machine_spec, machine_options, server)
469
+ # if !transport.available?
470
+ # if action_handler.should_perform_actions
471
+ # action_handler.report_progress "waiting for #{machine_spec.name} (#{server.id} on #{driver_url}) to be connectable (transport up and running) ..."
472
+
473
+ # _self = self
474
+
475
+ # server.wait_for(remaining_wait_time(machine_spec, machine_options)) do
476
+ # transport.available?
477
+ # end
478
+ # action_handler.report_progress "#{machine_spec.name} is now connectable"
479
+ # end
480
+ # end
481
+ # end
482
+
483
+ # def attach_floating_ips(action_handler, machine_spec, machine_options, server)
484
+ # # TODO this is not particularly idempotent. OK, it is not idempotent AT ALL. Fix.
485
+ # if option_for(machine_options, :floating_ip_pool)
486
+ # Chef::Log.info 'Attaching IP from pool'
487
+ # action_handler.perform_action "attach floating IP from #{option_for(machine_options, :floating_ip_pool)} pool" do
488
+ # attach_ip_from_pool(server, option_for(machine_options, :floating_ip_pool))
489
+ # end
490
+ # elsif option_for(machine_options, :floating_ip)
491
+ # Chef::Log.info 'Attaching given IP'
492
+ # action_handler.perform_action "attach floating IP #{option_for(machine_options, :floating_ip)}" do
493
+ # attach_ip(server, option_for(machine_options, :allocation_id), option_for(machine_options, :floating_ip))
494
+ # end
495
+ # end
496
+ # end
497
+
498
+ # # Attach IP to machine from IP pool
499
+ # # Code taken from kitchen-openstack driver
500
+ # # https://github.com/test-kitchen/kitchen-openstack/blob/master/lib/kitchen/driver/openstack.rb#L196-L207
501
+ # def attach_ip_from_pool(server, pool)
502
+ # @ip_pool_lock ||= Mutex.new
503
+ # @ip_pool_lock.synchronize do
504
+ # Chef::Log.info "Attaching floating IP from <#{pool}> pool"
505
+ # free_addrs = compute.addresses.collect do |i|
506
+ # i.ip if i.fixed_ip.nil? and i.instance_id.nil? and i.pool == pool
507
+ # end.compact
508
+ # if free_addrs.empty?
509
+ # raise ActionFailed, "No available IPs in pool <#{pool}>"
510
+ # end
511
+ # attach_ip(server, free_addrs[0])
512
+ # end
513
+ # end
514
+
515
+ # # Attach given IP to machine
516
+ # # Code taken from kitchen-openstack driver
517
+ # # https://github.com/test-kitchen/kitchen-openstack/blob/master/lib/kitchen/driver/openstack.rb#L209-L213
518
+ # def attach_ip(server, allocation_id, ip)
519
+ # Chef::Log.info "Attaching floating IP <#{ip}>"
520
+ # compute.associate_address(:instance_id => server.id,
521
+ # :allocation_id => allocation_id,
522
+ # :public_ip => ip)
523
+ # end
524
+
525
+ # def symbolize_keys(options)
526
+ # options.inject({}) do |result,(key,value)|
527
+ # result[key.to_sym] = value
528
+ # result
529
+ # end
530
+ # end
531
+
532
+ # def server_for(machine_spec)
533
+ # if machine_spec.location
534
+ # compute.servers.get(machine_spec.location['server_id'])
535
+ # else
536
+ # nil
537
+ # end
538
+ # end
539
+
540
+ # def servers_for(machine_specs)
541
+ # result = {}
542
+ # machine_specs.each do |machine_spec|
543
+ # if machine_spec.location
544
+ # if machine_spec.location['driver_url'] != driver_url
545
+ # 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."
546
+ # end
547
+ # result[machine_spec] = compute.servers.get(machine_spec.location['server_id'])
548
+ # else
549
+ # result[machine_spec] = nil
550
+ # end
551
+ # end
552
+ # result
553
+ # end
554
+
555
+ # @@metal_default_lock = Mutex.new
556
+
557
+ # def overwrite_default_key_willy_nilly(action_handler)
558
+ # driver = self
559
+ # updated = @@metal_default_lock.synchronize do
560
+ # ChefMetal.inline_resource(action_handler) do
561
+ # crowbar_key_pair 'metal_default' do
562
+ # driver driver
563
+ # allow_overwrite true
564
+ # end
565
+ # end
566
+ # end
567
+ # if updated
568
+ # # Only warn the first time
569
+ # Chef::Log.warn("Using metal_default key, which is not shared between machines! It is recommended to create an AWS key pair with the crowbar_key_pair resource, and set :bootstrap_options => { :key_name => <key name> }")
570
+ # end
571
+ # 'metal_default'
572
+ # end
573
+
574
+ # def bootstrap_options_for(action_handler, machine_spec, machine_options)
575
+ # bootstrap_options = symbolize_keys(machine_options[:bootstrap_options] || {})
576
+
577
+ # bootstrap_options[:tags] = default_tags(machine_spec, bootstrap_options[:tags] || {})
578
+
579
+ # bootstrap_options[:name] ||= machine_spec.name
580
+
581
+ # bootstrap_options
582
+ # end
583
+
584
+ # def default_tags(machine_spec, bootstrap_tags = {})
585
+ # tags = {
586
+ # 'Name' => machine_spec.name,
587
+ # 'BootstrapId' => machine_spec.id,
588
+ # 'BootstrapHost' => Socket.gethostname,
589
+ # 'BootstrapUser' => Etc.getlogin
590
+ # }
591
+ # # User-defined tags override the ones we set
592
+ # tags.merge(bootstrap_tags)
593
+ # end
594
+
595
+ # def machine_for(machine_spec, machine_options, server = nil)
596
+ # server ||= server_for(machine_spec)
597
+ # if !server
598
+ # raise "Server for node #{machine_spec.name} has not been created!"
599
+ # end
600
+
601
+ # if machine_spec.location['is_windows']
602
+ # ChefMetal::Machine::WindowsMachine.new(machine_spec, transport_for(machine_spec, machine_options, server), convergence_strategy_for(machine_spec, machine_options))
603
+ # else
604
+ # ChefMetal::Machine::UnixMachine.new(machine_spec, transport_for(machine_spec, machine_options, server), convergence_strategy_for(machine_spec, machine_options))
605
+ # end
606
+ # end
607
+
608
+ # def convergence_strategy_for(machine_spec, machine_options)
609
+ # # Defaults
610
+ # if !machine_spec.location
611
+ # return ChefMetal::ConvergenceStrategy::NoConverge.new(machine_options[:convergence_options], config)
612
+ # end
613
+
614
+ # if machine_spec.location['is_windows']
615
+ # ChefMetal::ConvergenceStrategy::InstallMsi.new(machine_options[:convergence_options], config)
616
+ # elsif machine_options[:cached_installer] == true
617
+ # ChefMetal::ConvergenceStrategy::InstallCached.new(machine_options[:convergence_options], config)
618
+ # else
619
+ # ChefMetal::ConvergenceStrategy::InstallSh.new(machine_options[:convergence_options], config)
620
+ # end
621
+ # end
622
+
623
+ # def ssh_options_for(machine_spec, machine_options, server)
624
+ # result = {
625
+ # # TODO create a user known hosts file
626
+ # # :user_known_hosts_file => vagrant_ssh_config['UserKnownHostsFile'],
627
+ # # :paranoid => true,
628
+ # :auth_methods => [ 'publickey' ],
629
+ # :keys_only => true,
630
+ # :host_key_alias => "#{server.id}.#{provider}"
631
+ # }.merge(machine_options[:ssh_options] || {})
632
+ # if server.respond_to?(:private_key) && server.private_key
633
+ # result[:key_data] = [ server.private_key ]
634
+ # elsif server.respond_to?(:key_name) && server.key_name
635
+ # key = get_private_key(server.key_name)
636
+ # if !key
637
+ # raise "Server has key name '#{server.key_name}', but the corresponding private key was not found locally. Check if the key is in Chef::Config.private_key_paths: #{Chef::Config.private_key_paths.join(', ')}"
638
+ # end
639
+ # result[:key_data] = [ key ]
640
+ # elsif machine_spec.location['key_name']
641
+ # key = get_private_key(machine_spec.location['key_name'])
642
+ # if !key
643
+ # raise "Server was created with key name '#{machine_spec.location['key_name']}', but the corresponding private key was not found locally. Check if the key is in Chef::Config.private_key_paths: #{Chef::Config.private_key_paths.join(', ')}"
644
+ # end
645
+ # result[:key_data] = [ key ]
646
+ # elsif machine_options[:bootstrap_options][:key_path]
647
+ # result[:key_data] = [ IO.read(machine_options[:bootstrap_options][:key_path]) ]
648
+ # elsif machine_options[:bootstrap_options][:key_name]
649
+ # result[:key_data] = [ get_private_key(machine_options[:bootstrap_options][:key_name]) ]
650
+ # else
651
+ # # TODO make a way to suggest other keys to try ...
652
+ # raise "No key found to connect to #{machine_spec.name} (#{machine_spec.location.inspect})!"
653
+ # end
654
+ # result
655
+ # end
656
+
657
+ # def default_ssh_username
658
+ # 'root'
659
+ # end
660
+
661
+ # def create_ssh_transport(machine_spec, machine_options, server)
662
+ # ssh_options = ssh_options_for(machine_spec, machine_options, server)
663
+ # username = machine_spec.location['ssh_username'] || default_ssh_username
664
+ # if machine_options.has_key?(:ssh_username) && machine_options[:ssh_username] != machine_spec.location['ssh_username']
665
+ # Chef::Log.warn("Server #{machine_spec.name} was created with SSH username #{machine_spec.location['ssh_username']} and machine_options specifies username #{machine_options[:ssh_username]}. Using #{machine_spec.location['ssh_username']}. Please edit the node and change the metal.location.ssh_username attribute if you want to change it.")
666
+ # end
667
+ # options = {}
668
+ # if machine_spec.location[:sudo] || (!machine_spec.location.has_key?(:sudo) && username != 'root')
669
+ # options[:prefix] = 'sudo '
670
+ # end
671
+
672
+ # remote_host = nil
673
+ # if machine_spec.location['use_private_ip_for_ssh']
674
+ # remote_host = server.private_ip_address
675
+ # elsif !server.public_ip_address
676
+ # Chef::Log.warn("Server #{machine_spec.name} has no public ip address. Using private ip '#{server.private_ip_address}'. Set driver option 'use_private_ip_for_ssh' => true if this will always be the case ...")
677
+ # remote_host = server.private_ip_address
678
+ # elsif server.public_ip_address
679
+ # remote_host = server.public_ip_address
680
+ # else
681
+ # raise "Server #{server.id} has no private or public IP address!"
682
+ # end
683
+
684
+ # #Enable pty by default
685
+ # options[:ssh_pty_enable] = true
686
+ # options[:ssh_gateway] = machine_spec.location['ssh_gateway'] if machine_spec.location.has_key?('ssh_gateway')
687
+
688
+ # ChefMetal::Transport::SSH.new(remote_host, username, ssh_options, options, config)
689
+ # end
690
+
691
+ # def self.compute_options_for(provider, id, config)
692
+ # raise "unsupported crowbar provider #{provider}"
693
+ # end
694
+
695
+ end
696
+ end