chef-provisioning-google 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (28) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/README.md +121 -0
  4. data/Rakefile +22 -0
  5. data/lib/chef/provider/google_key_pair.rb +172 -0
  6. data/lib/chef/provisioning/driver_init/google.rb +3 -0
  7. data/lib/chef/provisioning/google_driver.rb +5 -0
  8. data/lib/chef/provisioning/google_driver/client/global_operations.rb +18 -0
  9. data/lib/chef/provisioning/google_driver/client/google_base.rb +72 -0
  10. data/lib/chef/provisioning/google_driver/client/google_compute_error.rb +16 -0
  11. data/lib/chef/provisioning/google_driver/client/instance.rb +64 -0
  12. data/lib/chef/provisioning/google_driver/client/instances.rb +98 -0
  13. data/lib/chef/provisioning/google_driver/client/metadata.rb +112 -0
  14. data/lib/chef/provisioning/google_driver/client/operation.rb +23 -0
  15. data/lib/chef/provisioning/google_driver/client/operations_base.rb +44 -0
  16. data/lib/chef/provisioning/google_driver/client/projects.rb +39 -0
  17. data/lib/chef/provisioning/google_driver/client/zone_operations.rb +18 -0
  18. data/lib/chef/provisioning/google_driver/credentials.rb +63 -0
  19. data/lib/chef/provisioning/google_driver/driver.rb +313 -0
  20. data/lib/chef/provisioning/google_driver/resources.rb +0 -0
  21. data/lib/chef/provisioning/google_driver/version.rb +7 -0
  22. data/lib/chef/resource/google_key_pair.rb +50 -0
  23. data/spec/chef/provisioning/google_driver/client/instances_spec.rb +205 -0
  24. data/spec/chef/provisioning/google_driver/client/operations_spec.rb +62 -0
  25. data/spec/chef/provisioning/google_driver/client/projects_spec.rb +162 -0
  26. data/spec/chef/provisioning/google_driver/client/services_helper.rb +33 -0
  27. data/spec/spec_helper.rb +0 -0
  28. metadata +222 -0
@@ -0,0 +1,39 @@
1
+ require_relative "google_base"
2
+ require_relative "metadata"
3
+
4
+ class Chef
5
+ module Provisioning
6
+ module GoogleDriver
7
+ module Client
8
+ # Wraps a Projects service of the GCE API.
9
+ class Projects < GoogleBase
10
+
11
+ def get
12
+ # The default arguments are already enough for this call.
13
+ response = make_request(compute.projects.get)
14
+ raise_if_error(response)
15
+ Metadata.new(response)
16
+ end
17
+
18
+ # Takes a metadata object retrieved via #get and updates the metadata on GCE
19
+ # using the projects.set_common_instance_metadata API call.
20
+ # This fails if the metadata on GCE has been updated since the passed
21
+ # metadata object has been retrieved via #get.
22
+ # Note that this omits the API call if the metadata object hasn't changed
23
+ # locally since it was retrieved via #get.
24
+ def set_common_instance_metadata(metadata)
25
+ return nil unless metadata.changed?
26
+ response = make_request(
27
+ compute.projects.set_common_instance_metadata,
28
+ # Default paremeters are sufficient.
29
+ {},
30
+ { items: metadata.items, fingerprint: metadata.fingerprint }
31
+ )
32
+ operation_response(response)
33
+ end
34
+
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,18 @@
1
+ require_relative "operations_base"
2
+
3
+ class Chef
4
+ module Provisioning
5
+ module GoogleDriver
6
+ module Client
7
+ # Wraps a ZoneOperations service of the GCE API.
8
+ class ZoneOperations < OperationsBase
9
+
10
+ def operations_service
11
+ compute.zone_operations
12
+ end
13
+
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,63 @@
1
+ class Chef
2
+ module Provisioning
3
+ module GoogleDriver
4
+ # Access various forms of Google credentials
5
+ # TODO: load credentials from metadata server when provisioning from a GCE machine
6
+ class Credentials
7
+
8
+ def initialize
9
+ @credentials = {}
10
+ end
11
+
12
+ def [](name)
13
+ @credentials[name]
14
+ end
15
+
16
+ def []=(name, value)
17
+ @credentials[name] = value
18
+ end
19
+
20
+ def self.from_hash(h)
21
+ credentials = self.new
22
+ h.each do |k, v|
23
+ credentials[k] = v
24
+ end
25
+ credentials.validate!
26
+ credentials
27
+ end
28
+
29
+ # Validates whether the key settings are present in the credential object and keys are in the correct format.
30
+ # If no client_email is specified, method will try to load the client_email from the json key.
31
+ def validate!
32
+ unless self[:p12_key_path] || self[:json_key_path]
33
+ raise "Google key path is missing. Options provided: #{self.inspect}"
34
+ end
35
+ if self[:json_key_path]
36
+ json_key_hash = JSON.load(File.open(self[:json_key_path]))
37
+
38
+ unless self[:google_client_email]
39
+ # Try to load client_email from json if not present
40
+ if json_key_hash["client_email"]
41
+ self[:google_client_email] = json_key_hash["client_email"]
42
+ else
43
+ raise "google_client_email must be specified"
44
+ end
45
+ end
46
+
47
+ raise "Invalid Google JSON key, no private key" unless json_key_hash.include?("private_key")
48
+ elsif self[:p12_key_path]
49
+ raise "p12 key doesn't exist in the path specified" unless File.exist?(self[:p12_key_path])
50
+ raise "google_client_email must be specified" unless self[:google_client_email]
51
+ else
52
+ raise "json_key_path or p12_key_path is missing. Options provided: #{self}"
53
+ end
54
+ end
55
+
56
+ def load_defaults
57
+ raise NotImplementedError
58
+ end
59
+
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,313 @@
1
+ require "chef/provisioning/driver"
2
+ require "chef/provisioning/google_driver/credentials"
3
+ require "chef/provisioning/google_driver/version"
4
+ require "chef/mixin/deep_merge"
5
+ require "chef/provisioning/convergence_strategy/install_cached"
6
+ require "chef/provisioning/convergence_strategy/install_sh"
7
+ require "chef/provisioning/convergence_strategy/install_msi"
8
+ require "chef/provisioning/convergence_strategy/no_converge"
9
+ require "chef/provisioning/transport/ssh"
10
+ require "chef/provisioning/transport/winrm"
11
+ require "chef/provisioning/machine/windows_machine"
12
+ require "chef/provisioning/machine/unix_machine"
13
+ require "chef/provisioning/machine_spec"
14
+
15
+ require_relative "client/instances"
16
+ require_relative "client/global_operations"
17
+ require_relative "client/projects"
18
+ require_relative "client/zone_operations"
19
+
20
+ require "google/api_client"
21
+ require "retryable"
22
+ require "etc"
23
+
24
+ class Chef
25
+ module Provisioning
26
+ module GoogleDriver
27
+ # Provisions machines using the Google SDK
28
+ # TODO look at the superclass comments for further explanation of the overridden methods in this class
29
+ class Driver < Chef::Provisioning::Driver
30
+
31
+ include Chef::Mixin::DeepMerge
32
+
33
+ attr_reader :google, :zone, :project, :instance_client, :global_operations_client, :zone_operations_client, :project_client
34
+ URL_REGEX = /^google:(.+?):(.+)$/
35
+
36
+ # URL scheme:
37
+ # google:zone
38
+ def self.from_url(driver_url, config)
39
+ self.new(driver_url, config)
40
+ end
41
+
42
+ def initialize(driver_url, config)
43
+ super
44
+
45
+ m = URL_REGEX.match(driver_url)
46
+ if m.nil?
47
+ raise "Driver URL [#{driver_url}] must match #{URL_REGEX.inspect}"
48
+ end
49
+ # TODO: Move zone into bootstrap_options
50
+ @zone = m[1]
51
+ @project = m[2]
52
+
53
+ @google = Google::APIClient.new(
54
+ :application_name => "chef-provisioning-google",
55
+ :application_version => Chef::Provisioning::GoogleDriver::VERSION
56
+ )
57
+ if google_credentials[:p12_key_path]
58
+ signing_key = Google::APIClient::KeyUtils.load_from_pkcs12(google_credentials[:p12_key_path], "notasecret")
59
+ elsif google_credentials[:json_key_path]
60
+ json_private_key = JSON.load(File.open(google_credentials[:json_key_path]))["private_key"]
61
+ signing_key = Google::APIClient::KeyUtils.load_from_pem(json_private_key, "notasecret")
62
+ end
63
+ google.authorization = Signet::OAuth2::Client.new(
64
+ :token_credential_uri => "https://accounts.google.com/o/oauth2/token",
65
+ :audience => "https://accounts.google.com/o/oauth2/token",
66
+ :scope => ["https://www.googleapis.com/auth/compute", "https://www.googleapis.com/auth/compute.readonly"],
67
+ :issuer => google_credentials[:google_client_email],
68
+ :signing_key => signing_key
69
+ )
70
+ google.authorization.fetch_access_token!
71
+
72
+ @instance_client = Client::Instances.new(google, project, zone)
73
+ @project_client = Client::Projects.new(google, project, zone)
74
+ @global_operations_client =
75
+ Client::GlobalOperations.new(google, project, zone)
76
+ @zone_operations_client =
77
+ Client::ZoneOperations.new(google, project, zone)
78
+ end
79
+
80
+ def self.canonicalize_url(driver_url, config)
81
+ [ driver_url, config ]
82
+ end
83
+
84
+ def allocate_machine(action_handler, machine_spec, machine_options)
85
+ # TODO how do we handle running `allocate` and there is already a machine in GCE
86
+ # but no node in chef? We should just start tracking it.
87
+ if instance_for(machine_spec).nil?
88
+ name = machine_spec.name
89
+ operation = nil
90
+ action_handler.perform_action "creating instance named #{name} in zone #{zone}" do
91
+ default_options = instance_client.default_create_options(name)
92
+ options = hash_only_merge(default_options, machine_options[:insert_options])
93
+ operation = instance_client.create(options)
94
+ end
95
+ zone_operations_client.wait_for_done(action_handler, operation)
96
+ machine_spec.reference = {
97
+ "driver_version" => Chef::Provisioning::GoogleDriver::VERSION,
98
+ "allocated_at" => Time.now.utc.to_s,
99
+ "host_node" => action_handler.host_node,
100
+ }
101
+ machine_spec.driver_url = driver_url
102
+ # %w(is_windows ssh_username sudo use_private_ip_for_ssh ssh_gateway).each do |key|
103
+ %w{ssh_username sudo ssh_gateway key_name}.each do |key|
104
+ machine_spec.reference[key] = machine_options[key.to_sym] if machine_options[key.to_sym]
105
+ end
106
+ end
107
+ end
108
+
109
+ def ready_machine(action_handler, machine_spec, machine_options)
110
+ name = machine_spec.name
111
+ instance = instance_for(machine_spec)
112
+
113
+ if instance.nil?
114
+ raise "Machine #{name} does not have an instance associated with it, or instance does not exist."
115
+ end
116
+
117
+ if !instance.running?
118
+ # could be PROVISIONING, STAGING, STOPPING, TERMINATED
119
+ if %w{STOPPING TERMINATED}.include?(instance.status)
120
+ action_handler.perform_action "instance named #{name} in zone #{zone} was stopped - starting it" do
121
+ instance_client.start(name)
122
+ end
123
+ end
124
+ instance_client.wait_for_status(action_handler, instance, "RUNNING")
125
+ end
126
+
127
+ # Refresh instance object so we get the new ip address and status
128
+ instance = instance_for(machine_spec)
129
+
130
+ wait_for_transport(action_handler, machine_spec, machine_options, instance)
131
+ machine_for(machine_spec, machine_options, instance)
132
+ end
133
+
134
+ def destroy_machine(action_handler, machine_spec, machine_options)
135
+ name = machine_spec.name
136
+ instance = instance_for(machine_spec)
137
+ # https://cloud.google.com/compute/docs/instances#checkmachinestatus
138
+ # TODO Shouldn't we also delete stopped machines?
139
+ if instance && !%w{STOPPING TERMINATED}.include?(instance.status)
140
+ operation = nil
141
+ action_handler.perform_action "destroying instance named #{name} in zone #{zone}" do
142
+ operation = instance_client.delete(name)
143
+ end
144
+ zone_operations_client.wait_for_done(action_handler, operation)
145
+ end
146
+
147
+ strategy = convergence_strategy_for(machine_spec, machine_options)
148
+ strategy.cleanup_convergence(action_handler, machine_spec)
149
+ # TODO clean up known_hosts entry
150
+ end
151
+
152
+ def stop_machine(action_handler, machine_spec, machine_options)
153
+ name = machine_spec.name
154
+ instance = instance_for(machine_spec)
155
+
156
+ if instance.nil?
157
+ raise "Machine #{name} does not have an instance associated with it, or instance does not exist."
158
+ end
159
+
160
+ unless instance.terminated?
161
+ unless instance.stopping?
162
+ action_handler.perform_action "stopping instance named #{name} in zone #{zone}" do
163
+ instance_client.stop(name)
164
+ end
165
+ end
166
+ instance_client.wait_for_status(action_handler, instance, "TERMINATED")
167
+ end
168
+
169
+ if instance.terminated?
170
+ Chef::Log.info "Instance #{instance.name} already stopped, nothing to do."
171
+ end
172
+ end
173
+
174
+ # TODO make these configurable and find a good place where to put them.
175
+ def tries
176
+ Client::GoogleBase::TRIES
177
+ end
178
+
179
+ def sleep
180
+ Client::GoogleBase::SLEEP_SECONDS
181
+ end
182
+
183
+ def wait_for_transport(action_handler, machine_spec, machine_options, instance)
184
+ transport = transport_for(machine_spec, machine_options, instance)
185
+ unless transport.available?
186
+ if action_handler.should_perform_actions
187
+ Retryable.retryable(:tries => tries, :sleep => sleep, :matching => /Not done/) do |retries, exception|
188
+ action_handler.report_progress(" waited #{retries * sleep}/#{tries * sleep}s for instance #{instance.name} to be connectable (transport up and running) ...")
189
+ raise "Not done" unless transport.available?
190
+ end
191
+ action_handler.report_progress "#{machine_spec.name} is now connectable"
192
+ end
193
+ end
194
+ end
195
+
196
+ def transport_for(machine_spec, machine_options, instance)
197
+ # TODO winrm
198
+ # if machine_spec.reference['is_windows']
199
+ # create_winrm_transport(machine_spec, machine_options, instance)
200
+ # else
201
+ create_ssh_transport(machine_spec, machine_options, instance)
202
+ # end
203
+ end
204
+
205
+ def create_ssh_transport(machine_spec, machine_options, instance)
206
+ ssh_options = ssh_options_for(machine_spec, machine_options, instance)
207
+ username = machine_spec.reference["ssh_username"] || machine_options[:ssh_username] || Etc.getlogin
208
+ if machine_options.has_key?(:ssh_username) && machine_options[:ssh_username] != machine_spec.reference["ssh_username"]
209
+ Chef::Log.warn("Server #{machine_spec.name} was created with SSH username #{machine_spec.reference['ssh_username']} and machine_options specifies username #{machine_options[:ssh_username]}. Using #{machine_spec.reference['ssh_username']}. Please edit the node and change the chef_provisioning.reference.ssh_username attribute if you want to change it.")
210
+ end
211
+ options = {}
212
+ if machine_spec.reference[:sudo] || (!machine_spec.reference.has_key?(:sudo) && username != "root")
213
+ options[:prefix] = "sudo "
214
+ end
215
+
216
+ remote_host = instance.determine_remote_host
217
+
218
+ #Enable pty by default
219
+ options[:ssh_pty_enable] = true
220
+ options[:ssh_gateway] = machine_spec.reference["ssh_gateway"] if machine_spec.reference.has_key?("ssh_gateway")
221
+
222
+ Chef::Provisioning::Transport::SSH.new(remote_host, username, ssh_options, options, config)
223
+ end
224
+
225
+ def ssh_options_for(machine_spec, machine_options, instance)
226
+ result = {
227
+ :auth_methods => [ "publickey" ],
228
+ :keys_only => true,
229
+ :host_key_alias => "#{instance.id}.GOOGLE",
230
+ }.merge(machine_options[:ssh_options] || {})
231
+ # TODO right now we only allow keys created for the whole project and specified in the
232
+ # bootstrap options - look at AWS for other options
233
+ if machine_options[:key_name]
234
+ # TODO how do I add keys to config[:private_keys] ?
235
+ # result[:key_data] = [ get_private_key(machine_options[:key_name]) ]
236
+ # TODO: what to do if we find multiple valid keys in config[:private_key_paths] ?
237
+ config[:private_key_paths].each do |path|
238
+ result[:key_data] = IO.read("#{path}/#{machine_options[:key_name]}") if File.exist?("#{path}/#{machine_options[:key_name]}")
239
+ end
240
+ unless result[:key_data]
241
+ raise "#{machine_options[:key_name]} doesn't exist in private_key_paths:#{config[:private_key_paths]}"
242
+ end
243
+ else
244
+ raise "No key found to connect to #{machine_spec.name} (#{machine_spec.reference.inspect})!"
245
+ end
246
+ result
247
+ end
248
+
249
+ def machine_for(machine_spec, machine_options, instance = nil)
250
+ instance ||= instance_for(machine_spec)
251
+
252
+ unless instance
253
+ raise "Instance for node #{machine_spec.name} has not been created!"
254
+ end
255
+
256
+ # TODO winrm
257
+ # if machine_spec.reference['is_windows']
258
+ # Chef::Provisioning::Machine::WindowsMachine.new(machine_spec, transport_for(machine_spec, machine_options, instance), convergence_strategy_for(machine_spec, machine_options))
259
+ # else
260
+ Chef::Provisioning::Machine::UnixMachine.new(machine_spec, transport_for(machine_spec, machine_options, instance), convergence_strategy_for(machine_spec, machine_options))
261
+ # end
262
+ end
263
+
264
+ def convergence_strategy_for(machine_spec, machine_options)
265
+ # Tell Ohai that this is an EC2 instance so that it runs the EC2 plugin
266
+ convergence_options = Cheffish::MergedConfig.new(
267
+ machine_options[:convergence_options] || {},
268
+ # TODO what is the right ohai hints file?
269
+ ohai_hints: { "google" => "" })
270
+
271
+ # Defaults
272
+ unless machine_spec.reference
273
+ return Chef::Provisioning::ConvergenceStrategy::NoConverge.new(convergence_options, config)
274
+ end
275
+
276
+ # TODO winrm
277
+ # if machine_spec.reference['is_windows']
278
+ # Chef::Provisioning::ConvergenceStrategy::InstallMsi.new(convergence_options, config)
279
+ if machine_options[:cached_installer] == true
280
+ Chef::Provisioning::ConvergenceStrategy::InstallCached.new(convergence_options, config)
281
+ else
282
+ Chef::Provisioning::ConvergenceStrategy::InstallSh.new(convergence_options, config)
283
+ end
284
+ end
285
+
286
+ def instance_for(machine_spec)
287
+ if machine_spec.reference
288
+ if machine_spec.driver_url != driver_url
289
+ raise "Switching a machine's driver from #{machine_spec.driver_url} to #{driver_url} is not currently supported! Use machine :destroy and then re-create the machine on the new driver."
290
+ end
291
+ instance_client.get(machine_spec.name)
292
+ end
293
+ end
294
+
295
+ private
296
+
297
+ # TODO load from file or env variables using common google method
298
+ # https://cloud.google.com/sdk/gcloud/#gcloud.auth
299
+ def google_credentials
300
+ # Grab the list of possible credentials
301
+ @google_credentials ||= if driver_options[:google_credentials]
302
+ Credentials.from_hash(driver_options[:google_credentials])
303
+ else
304
+ credentials = Credentials.new
305
+ credentials.load_defaults
306
+ credentials
307
+ end
308
+ end
309
+
310
+ end
311
+ end
312
+ end
313
+ end
@@ -0,0 +1,7 @@
1
+ class Chef
2
+ module Provisioning
3
+ module GoogleDriver
4
+ VERSION = "0.1.0"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,50 @@
1
+ require "chef/resource/lwrp_base"
2
+
3
+ class Chef::Resource::GoogleKeyPair < Chef::Resource::LWRPBase
4
+ self.resource_name = self.dsl_name
5
+
6
+ # TODO instance specific or project wide?
7
+ # Right now we only have project wide keys
8
+
9
+ provides :google_key_pair
10
+
11
+ actions :create, :destroy
12
+ default_action :create
13
+
14
+ # Private key to use as input (will be generated if it does not exist)
15
+ attribute :private_key_path, :kind_of => String
16
+ # Public key to use as input (will be generated if it does not exist)
17
+ attribute :public_key_path, :kind_of => String
18
+ # List of parameters to the private_key resource used for generation of the key
19
+ attribute :private_key_options, :kind_of => Hash
20
+
21
+ # This applies to both the local keys and the remote key
22
+ attribute :allow_overwrite, :kind_of => [TrueClass, FalseClass], :default => false
23
+
24
+ # TODO: add a `user` attribute which sets the user to login with the key
25
+ # GCE uses the key user to create a user on the instance, which may duplicate
26
+ # some logic the user is trying to do with their chef recipes
27
+
28
+ def after_created
29
+ # We default these here so load_current_resource can diff
30
+ if private_key_path.nil?
31
+ private_key_path ::File.join(driver.config[:private_key_write_path], "google_default")
32
+ elsif Pathname.new(private_key_path).relative?
33
+ private_key_path ::File.join(driver.config[:private_key_write_path], private_key_path)
34
+ end
35
+ # TODO you don't actually need to write the private key to disc if it isn't provided
36
+ # it can be read from the private key, but this code update needs testing
37
+ if public_key_path.nil?
38
+ public_key_path ::File.join(driver.config[:private_key_write_path], "google_default.pub")
39
+ elsif Pathname.new(public_key_path).relative?
40
+ public_key_path ::File.join(driver.config[:private_key_write_path], public_key_path)
41
+ end
42
+ end
43
+
44
+ # TODO introduce base class and add this as attribute, like AWS does
45
+ # Ideally, we won't be creating lots of copies of the same driver object, but it is okay
46
+ # if we do - they aren't singletons
47
+ def driver
48
+ run_context.chef_provisioning.driver_for(run_context.chef_provisioning.current_driver)
49
+ end
50
+ end