chef-provisioning-google 0.1.0

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 (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