clc-fork-chef-metal 0.11.beta.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +106 -0
  3. data/LICENSE +201 -0
  4. data/README.md +201 -0
  5. data/Rakefile +6 -0
  6. data/bin/metal +276 -0
  7. data/lib/chef/provider/machine.rb +147 -0
  8. data/lib/chef/provider/machine_batch.rb +130 -0
  9. data/lib/chef/provider/machine_execute.rb +30 -0
  10. data/lib/chef/provider/machine_file.rb +49 -0
  11. data/lib/chef/resource/machine.rb +95 -0
  12. data/lib/chef/resource/machine_batch.rb +20 -0
  13. data/lib/chef/resource/machine_execute.rb +22 -0
  14. data/lib/chef/resource/machine_file.rb +28 -0
  15. data/lib/chef_metal.rb +62 -0
  16. data/lib/chef_metal/action_handler.rb +63 -0
  17. data/lib/chef_metal/add_prefix_action_handler.rb +29 -0
  18. data/lib/chef_metal/chef_machine_spec.rb +64 -0
  19. data/lib/chef_metal/chef_provider_action_handler.rb +72 -0
  20. data/lib/chef_metal/chef_run_data.rb +80 -0
  21. data/lib/chef_metal/convergence_strategy.rb +26 -0
  22. data/lib/chef_metal/convergence_strategy/install_cached.rb +157 -0
  23. data/lib/chef_metal/convergence_strategy/install_msi.rb +56 -0
  24. data/lib/chef_metal/convergence_strategy/install_sh.rb +51 -0
  25. data/lib/chef_metal/convergence_strategy/no_converge.rb +38 -0
  26. data/lib/chef_metal/convergence_strategy/precreate_chef_objects.rb +180 -0
  27. data/lib/chef_metal/driver.rb +267 -0
  28. data/lib/chef_metal/machine.rb +110 -0
  29. data/lib/chef_metal/machine/basic_machine.rb +82 -0
  30. data/lib/chef_metal/machine/unix_machine.rb +276 -0
  31. data/lib/chef_metal/machine/windows_machine.rb +102 -0
  32. data/lib/chef_metal/machine_spec.rb +78 -0
  33. data/lib/chef_metal/recipe_dsl.rb +84 -0
  34. data/lib/chef_metal/transport.rb +87 -0
  35. data/lib/chef_metal/transport/ssh.rb +235 -0
  36. data/lib/chef_metal/transport/winrm.rb +109 -0
  37. data/lib/chef_metal/version.rb +3 -0
  38. metadata +223 -0
@@ -0,0 +1,38 @@
1
+ require 'chef_metal/convergence_strategy'
2
+ require 'pathname'
3
+ require 'cheffish'
4
+
5
+ module ChefMetal
6
+ class ConvergenceStrategy
7
+ class NoConverge < ConvergenceStrategy
8
+ def initialize(convergence_options, config)
9
+ super
10
+ end
11
+
12
+ def chef_server
13
+ @chef_server ||= convergence_options[:chef_server] || Cheffish.default_chef_server(config)
14
+ end
15
+
16
+ def setup_convergence(action_handler, machine)
17
+ machine_spec.save(action_handler)
18
+ end
19
+
20
+ def converge(action_handler, machine)
21
+ end
22
+
23
+ def cleanup_convergence(action_handler, machine_spec)
24
+ _self = self
25
+ ChefMetal.inline_resource(action_handler) do
26
+ chef_node machine_spec.name do
27
+ chef_server _self.chef_server
28
+ action :delete
29
+ end
30
+ chef_client machine_spec.name do
31
+ chef_server _self.chef_server
32
+ action :delete
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,180 @@
1
+ require 'chef_metal/convergence_strategy'
2
+ require 'pathname'
3
+ require 'cheffish'
4
+
5
+ module ChefMetal
6
+ class ConvergenceStrategy
7
+ class PrecreateChefObjects < ConvergenceStrategy
8
+ def initialize(convergence_options, config)
9
+ super
10
+ end
11
+
12
+ def chef_server
13
+ @chef_server ||= convergence_options[:chef_server] || Cheffish.default_chef_server(config)
14
+ end
15
+
16
+ def setup_convergence(action_handler, machine)
17
+ # Create keys on machine
18
+ public_key = create_keys(action_handler, machine)
19
+ # Create node and client on chef server
20
+ create_chef_objects(action_handler, machine, public_key)
21
+
22
+ # If the chef server lives on localhost, tunnel the port through to the guest
23
+ # (we need to know what got tunneled!)
24
+ chef_server_url = chef_server[:chef_server_url]
25
+ chef_server_url = machine.make_url_available_to_remote(chef_server_url)
26
+
27
+ # Support for multiple ohai hints, required on some platforms
28
+ create_ohai_files(action_handler, machine)
29
+
30
+ # Create client.rb and client.pem on machine
31
+ content = client_rb_content(chef_server_url, machine.node['name'])
32
+ machine.write_file(action_handler, convergence_options[:client_rb_path], content, :ensure_dir => true)
33
+ end
34
+
35
+ def converge(action_handler, machine)
36
+ machine.make_url_available_to_remote(chef_server[:chef_server_url])
37
+ end
38
+
39
+ def cleanup_convergence(action_handler, machine_spec)
40
+ _self = self
41
+ ChefMetal.inline_resource(action_handler) do
42
+ chef_node machine_spec.name do
43
+ chef_server _self.chef_server
44
+ action :delete
45
+ end
46
+ chef_client machine_spec.name do
47
+ chef_server _self.chef_server
48
+ action :delete
49
+ end
50
+ end
51
+ end
52
+
53
+ protected
54
+
55
+ def create_keys(action_handler, machine)
56
+ server_private_key = machine.read_file(convergence_options[:client_pem_path])
57
+ if server_private_key
58
+ begin
59
+ server_private_key, format = Cheffish::KeyFormatter.decode(server_private_key)
60
+ rescue
61
+ server_private_key = nil
62
+ end
63
+ end
64
+
65
+ if server_private_key
66
+ if source_key && server_private_key.to_pem != source_key.to_pem
67
+ # If the server private key does not match our source key, overwrite it
68
+ server_private_key = source_key
69
+ if convergence_options[:allow_overwrite_keys]
70
+ machine.write_file(action_handler, convergence_options[:client_pem_path], server_private_key.to_pem, :ensure_dir => true)
71
+ else
72
+ raise "Private key on machine #{machine.name} does not match desired input key."
73
+ end
74
+ end
75
+
76
+ else
77
+
78
+ # If the server does not already have keys, create them and upload
79
+ _convergence_options = convergence_options
80
+ ChefMetal.inline_resource(action_handler) do
81
+ private_key 'in_memory' do
82
+ path :none
83
+ if _convergence_options[:private_key_options]
84
+ _convergence_options[:private_key_options].each_pair do |key,value|
85
+ send(key, value)
86
+ end
87
+ end
88
+ after { |resource, private_key| server_private_key = private_key }
89
+ end
90
+ end
91
+
92
+ machine.write_file(action_handler, convergence_options[:client_pem_path], server_private_key.to_pem, :ensure_dir => true)
93
+ end
94
+
95
+ server_private_key.public_key
96
+ end
97
+
98
+ def is_localhost(host)
99
+ host == '127.0.0.1' || host == 'localhost' || host == '[::1]'
100
+ end
101
+
102
+ def source_key
103
+ if convergence_options[:source_key].is_a?(String)
104
+ key, format = Cheffish::KeyFormatter.decode(convergence_options[:source_key], convergence_options[:source_key_pass_phrase])
105
+ key
106
+ elsif convergence_options[:source_key]
107
+ convergence_options[:source_key]
108
+ elsif convergence_options[:source_key_path]
109
+ key, format = Cheffish::KeyFormatter.decode(IO.read(convergence_options[:source_key_path]), convergence_options[:source_key_pass_phrase], convergence_options[:source_key_path])
110
+ key
111
+ else
112
+ nil
113
+ end
114
+ end
115
+
116
+ # Create the ohai file(s)
117
+ def create_ohai_files(action_handler, machine)
118
+ if convergence_options[:ohai_hints]
119
+ convergence_options[:ohai_hints].each_pair do |hint, data|
120
+ # The location of the ohai hint
121
+ ohai_hint = "/etc/chef/ohai/hints/#{hint}.json"
122
+ machine.write_file(action_handler, ohai_hint, data.to_json, :ensure_dir => true)
123
+ end
124
+ end
125
+ end
126
+
127
+ def create_chef_objects(action_handler, machine, public_key)
128
+ _convergence_options = convergence_options
129
+ _chef_server = chef_server
130
+ # Save the node and create the client keys and client.
131
+ ChefMetal.inline_resource(action_handler) do
132
+ # Create client
133
+ chef_client machine.name do
134
+ chef_server _chef_server
135
+ source_key public_key
136
+ output_key_path _convergence_options[:public_key_path]
137
+ output_key_format _convergence_options[:public_key_format]
138
+ admin _convergence_options[:admin]
139
+ validator _convergence_options[:validator]
140
+ end
141
+
142
+ # Create node
143
+ # TODO strip automatic attributes first so we don't race with "current state"
144
+ chef_node machine.name do
145
+ chef_server _chef_server
146
+ raw_json machine.node
147
+ end
148
+ end
149
+
150
+ # If using enterprise/hosted chef, fix acls
151
+ if chef_server[:chef_server_url] =~ /\/+organizations\/.+/
152
+ grant_client_node_permissions(action_handler, chef_server, machine.name, ["read", "update"])
153
+ end
154
+ end
155
+
156
+ # Grant the client permissions to the node
157
+ # This procedure assumes that the client name and node name are the same
158
+ def grant_client_node_permissions(action_handler, chef_server, node_name, perms)
159
+ api = Cheffish.chef_server_api(chef_server)
160
+ node_perms = api.get("/nodes/#{node_name}/_acl")
161
+ perms.each do |p|
162
+ if !node_perms[p]['actors'].include?(node_name)
163
+ action_handler.perform_action "Add #{node_name} to client #{p} ACLs" do
164
+ node_perms[p]['actors'] << node_name
165
+ api.put("/nodes/#{node_name}/_acl/#{p}", p => node_perms[p])
166
+ end
167
+ end
168
+ end
169
+ end
170
+
171
+ def client_rb_content(chef_server_url, node_name)
172
+ <<EOM
173
+ chef_server_url #{chef_server_url.inspect}
174
+ node_name #{node_name.inspect}
175
+ client_key #{convergence_options[:client_pem_path].inspect}
176
+ EOM
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,267 @@
1
+ module ChefMetal
2
+ #
3
+ # A Driver instance represents a place where machines can be created and found,
4
+ # and contains methods to create, delete, start, stop, and find them.
5
+ #
6
+ # For AWS, a Driver instance corresponds to a single account.
7
+ # For Vagrant, it is a directory where VM files are found.
8
+ #
9
+ # == How to Make a Driver
10
+ #
11
+ # To implement a Driver, you must implement the following methods:
12
+ #
13
+ # - initialize(driver_url) - create a new driver with the given URL
14
+ # - driver_url - a URL representing everything unique about your driver.
15
+ # But NOT credentials.
16
+ # - allocate_machine - ask the driver to allocate a machine to you.
17
+ # - ready_machine - get the machine "ready" - wait for it to be booted and
18
+ # accessible (for example, accessible via SSH transport).
19
+ # - stop_machine - stop the machine.
20
+ # - destroy_machine - delete the machine.
21
+ # - connect_to_machine - connect to the given machine.
22
+ #
23
+ # Optionally, you can also implement:
24
+ # - allocate_machines - allocate an entire group of machines.
25
+ # - ready_machines - get a group of machines warm and booted.
26
+ # - stop_machines - stop a group of machines.
27
+ # - destroy_machines - delete a group of machines.
28
+ #
29
+ # Additionally, you must create a file named `chef_metal/driver_init/<scheme>.rb`,
30
+ # where <scheme> is the name of the scheme you chose for your driver_url. This
31
+ # file, when required, must call ChefMetal.add_registered_driver(<scheme>, <class>).
32
+ # The given <class>.from_url(url, config) will be called with a driver_url and
33
+ # configuration.
34
+ #
35
+ # All of these methods must be idempotent - if the work is already done, they
36
+ # just don't do anything.
37
+ #
38
+ class Driver
39
+ #
40
+ # Inflate a driver from a driver URL.
41
+ #
42
+ # == Parameters
43
+ # driver_url - the URL to inflate the driver
44
+ # config - a configuration hash. See "config" for a list of known keys.
45
+ #
46
+ # == Returns
47
+ # A Driver representing the given driver_url.
48
+ #
49
+ def initialize(driver_url, config)
50
+ @driver_url = driver_url
51
+ @config = config
52
+ end
53
+
54
+ #
55
+ # Override this on specific driver classes
56
+ #
57
+ def self.from_url(driver_url, config)
58
+ ChefMetal.from_url(driver_url, config)
59
+ end
60
+
61
+ #
62
+ # A URL representing the driver and the place where machines come from.
63
+ # This will be stuffed in machine_spec.location['driver_url'] so that the
64
+ # machine can be reinflated. URLs must have a unique scheme identifying the
65
+ # driver class, and enough information to identify the place where created
66
+ # machines can be found. For AWS, this is the account number; for lxc and
67
+ # vagrant, it is the directory in which VMs and containers are.
68
+ #
69
+ # For example:
70
+ # - fog:AWS:123456789012
71
+ # - vagrant:/var/vms
72
+ # - lxc:
73
+ # - docker:
74
+ #
75
+ attr_reader :driver_url
76
+
77
+ # A configuration hash. These keys may be present:
78
+ # - :driver_options: a driver-defined object containing driver config.
79
+ # - :private_keys: a hash of private keys, with a "name" and a "value". Values are either strings (paths) or PrivateKey objects.
80
+ # - :private_key_paths: a list of paths to directories containing private keys.
81
+ # - :write_private_key_path: the path to which we write new keys by default.
82
+ # - :log_level: :debug/:info/:warn/:error/:fatal
83
+ # - :chef_server_url: url to chef server
84
+ # - :node_name: username to talk to chef server
85
+ # - :client_key: path to key used to talk to chef server
86
+ attr_reader :config
87
+
88
+ #
89
+ # Driver configuration. Equivalent to config[:driver_options] || {}
90
+ #
91
+ def driver_options
92
+ config[:driver_options] || {}
93
+ end
94
+
95
+ #
96
+ # Allocate a machine from the PXE/cloud/VM/container driver. This method
97
+ # does not need to wait for the machine to boot or have an IP, but it must
98
+ # store enough information in machine_spec.location to find the machine
99
+ # later in ready_machine.
100
+ #
101
+ # If a machine is powered off or otherwise unusable, this method may start
102
+ # it, but does not need to wait until it is started. The idea is to get the
103
+ # gears moving, but the job doesn't need to be done :)
104
+ #
105
+ # ## Parameters
106
+ # action_handler - the action_handler object that is calling this method; this
107
+ # is generally a driver, but could be anything that can support the
108
+ # interface (i.e., in the case of the test kitchen metal driver for
109
+ # acquiring and destroying VMs).
110
+ #
111
+ # existing_machine - a MachineSpec representing the existing machine (if any).
112
+ #
113
+ # machine_options - a set of options representing the desired provisioning
114
+ # state of the machine (image name, bootstrap ssh credentials,
115
+ # etc.). This will NOT be stored in the machine_spec, and is
116
+ # ephemeral.
117
+ #
118
+ # ## Returns
119
+ #
120
+ # Modifies the passed-in machine_spec. Anything in here will be saved
121
+ # back after allocate_machine completes.
122
+ #
123
+ def allocate_machine(action_handler, machine_spec, machine_options)
124
+ raise "#{self.class} does not implement allocate_machine"
125
+ end
126
+
127
+ #
128
+ # Ready a machine, to the point where it is running and accessible via a
129
+ # transport. This will NOT allocate a machine, but may kick it if it is down.
130
+ # This method waits for the machine to be usable, returning a Machine object
131
+ # pointing at the machine, allowing useful actions like setup, converge,
132
+ # execute, file and directory.
133
+ #
134
+ # ## Parameters
135
+ # action_handler - the action_handler object that is calling this method; this
136
+ # is generally a driver, but could be anything that can support the
137
+ # interface (i.e., in the case of the test kitchen metal driver for
138
+ # acquiring and destroying VMs).
139
+ # machine_spec - MachineSpec representing this machine.
140
+ # machine_options - a set of options representing the desired provisioning
141
+ # state of the machine (image name, bootstrap ssh credentials,
142
+ # etc.). This will NOT be stored in the machine_spec, and is
143
+ # ephemeral.
144
+ #
145
+ # ## Returns
146
+ #
147
+ # Machine object pointing at the machine, allowing useful actions like setup,
148
+ # converge, execute, file and directory.
149
+ #
150
+ def ready_machine(action_handler, machine_spec, machine_options)
151
+ raise "#{self.class} does not implement ready_machine"
152
+ end
153
+
154
+ #
155
+ # Connect to a machine without allocating or readying it. This method will
156
+ # NOT make any changes to anything, or attempt to wait.
157
+ #
158
+ # ## Parameters
159
+ # machine_spec - MachineSpec representing this machine.
160
+ #
161
+ # ## Returns
162
+ #
163
+ # Machine object pointing at the machine, allowing useful actions like setup,
164
+ # converge, execute, file and directory.
165
+ #
166
+ def connect_to_machine(machine_spec, machine_options)
167
+ raise "#{self.class} does not implement connect_to_machine"
168
+ end
169
+
170
+ #
171
+ # Delete the given machine (idempotent). Should destroy the machine,
172
+ # returning things to the state before allocate_machine was called.
173
+ #
174
+ def destroy_machine(action_handler, machine_spec, machine_options)
175
+ raise "#{self.class} does not implement destroy_machine"
176
+ end
177
+
178
+ #
179
+ # Stop the given machine.
180
+ #
181
+ def stop_machine(action_handler, machine_spec, machine_options)
182
+ raise "#{self.class} does not implement stop_machine"
183
+ end
184
+
185
+ #
186
+ # Optional interface methods
187
+ #
188
+
189
+ #
190
+ # Allocate a set of machines. This should have the same effect as running
191
+ # allocate_machine on all machine_specs.
192
+ #
193
+ # Drivers do not need to implement this; the default implementation
194
+ # calls acquire_machine in parallel.
195
+ #
196
+ # ## Parameters
197
+ # action_handler - the action_handler object that is calling this method; this
198
+ # is generally a driver, but could be anything that can support the
199
+ # interface (i.e., in the case of the test kitchen metal driver for
200
+ # acquiring and destroying VMs).
201
+ # specs_and_options - a hash of machine_spec -> machine_options representing the
202
+ # machines to allocate.
203
+ # parallelizer - an object with a parallelize() method that works like this:
204
+ #
205
+ # parallelizer.parallelize(specs_and_options) do |machine_spec|
206
+ # allocate_machine(action_handler, machine_spec)
207
+ # end.to_a
208
+ # # The to_a at the end causes you to wait until the parallelization is done
209
+ #
210
+ # This object is shared among other chef-metal actions, ensuring that you do
211
+ # not go over parallelization limits set by the user. Use of the parallelizer
212
+ # to parallelizer machines is not required.
213
+ #
214
+ # ## Block
215
+ #
216
+ # If you pass a block to this function, each machine will be yielded to you
217
+ # as it completes, and then the function will return when all machines are
218
+ # yielded.
219
+ #
220
+ # allocate_machines(action_handler, specs_and_options, parallelizer) do |machine_spec|
221
+ # ...
222
+ # end
223
+ #
224
+ def allocate_machines(action_handler, specs_and_options, parallelizer)
225
+ parallelizer.parallelize(specs_and_options) do |machine_spec, machine_options|
226
+ allocate_machine(add_prefix(machine_spec, action_handler), machine_spec, machine_options)
227
+ yield machine_spec if block_given?
228
+ machine_spec
229
+ end.to_a
230
+ end
231
+
232
+ # Ready machines in batch, in parallel if possible.
233
+ def ready_machines(action_handler, specs_and_options, parallelizer)
234
+ parallelizer.parallelize(specs_and_options) do |machine_spec, machine_options|
235
+ machine = ready_machine(add_prefix(machine_spec, action_handler), machine_spec, machine_options)
236
+ yield machine if block_given?
237
+ machine
238
+ end.to_a
239
+ end
240
+
241
+ # Stop machines in batch, in parallel if possible.
242
+ def stop_machines(action_handler, specs_and_options, parallelizer)
243
+ parallelizer.parallelize(specs_and_options) do |machine_spec, machine_options|
244
+ stop_machine(add_prefix(machine_spec, action_handler), machine_spec, machine_options)
245
+ yield machine_spec if block_given?
246
+ end.to_a
247
+ end
248
+
249
+ # Delete machines in batch, in parallel if possible.
250
+ def destroy_machines(action_handler, specs_and_options, parallelizer)
251
+ parallelizer.parallelize(specs_and_options) do |machine_spec, machine_options|
252
+ destroy_machine(add_prefix(machine_spec, action_handler), machine_spec, machine_options)
253
+ yield machine_spec if block_given?
254
+ end.to_a
255
+ end
256
+
257
+ protected
258
+
259
+ def add_prefix(machine_spec, action_handler)
260
+ AddPrefixActionHandler.new(action_handler, "[#{machine_spec.name}] ")
261
+ end
262
+
263
+ def get_private_key(name)
264
+ Cheffish.get_private_key(name, config)
265
+ end
266
+ end
267
+ end