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,3 @@
1
+ require "chef/provisioning/google_driver/driver.rb"
2
+
3
+ Chef::Provisioning.register_driver_class("google", Chef::Provisioning::GoogleDriver::Driver)
@@ -0,0 +1,5 @@
1
+ require "chef/provisioning"
2
+ require "chef/provisioning/google_driver/driver"
3
+
4
+ require "chef/resource/google_key_pair"
5
+ require "chef/provider/google_key_pair"
@@ -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 GlobalOperations service of the GCE API.
8
+ class GlobalOperations < OperationsBase
9
+
10
+ def operations_service
11
+ compute.global_operations
12
+ end
13
+
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,72 @@
1
+ require "ffi_yajl"
2
+ require_relative "google_compute_error"
3
+ require_relative "operation"
4
+
5
+ class Chef
6
+ module Provisioning
7
+ module GoogleDriver
8
+ module Client
9
+ class GoogleBase
10
+ # TODO make these configurable and find a good place where to put them.
11
+ TRIES = 30
12
+ SLEEP_SECONDS = 5
13
+
14
+ NOT_FOUND = 404
15
+ OK = 200
16
+
17
+ attr_reader :google, :project, :zone
18
+
19
+ def initialize(google, project, zone)
20
+ @google = google
21
+ @project = project
22
+ @zone = zone
23
+ end
24
+
25
+ def make_request(method, parameters = nil, body = nil)
26
+ response = google.execute(
27
+ api_method: method,
28
+ parameters: default_parameters.merge(parameters || {}),
29
+ body_object: body
30
+ )
31
+ {
32
+ status: response.response.status,
33
+ body: FFI_Yajl::Parser.parse(response.response.body,
34
+ symbolize_keys: true),
35
+ }
36
+ end
37
+
38
+ def compute
39
+ @compute ||= google.discovered_api("compute")
40
+ end
41
+
42
+ def default_parameters
43
+ { project: project, zone: zone }
44
+ end
45
+
46
+ # Takes the response of an API call and if the response contains an error,
47
+ # it raises an error. If the response was successful, it returns an
48
+ # operation.
49
+ def operation_response(response)
50
+ raise_if_error(response)
51
+ Operation.new(response)
52
+ end
53
+
54
+ # Takes a response of an API call and returns true iff this has status not
55
+ # found.
56
+ def is_not_found?(response)
57
+ response[:status] == NOT_FOUND
58
+ end
59
+
60
+ # Takes a response of an API call and raises an error if the call was
61
+ # unsuccessful.
62
+ def raise_if_error(response)
63
+ # TODO Display warnings in some way.
64
+ if response[:status] != OK || response[:body][:error]
65
+ raise GoogleComputeError, response
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,16 @@
1
+ class Chef
2
+ module Provisioning
3
+ module GoogleDriver
4
+ module Client
5
+ class GoogleComputeError < StandardError
6
+
7
+ def initialize(result)
8
+ #TODO better error formatting
9
+ super(result)
10
+ end
11
+
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,64 @@
1
+ require_relative "google_compute_error"
2
+
3
+ class Chef
4
+ module Provisioning
5
+ module GoogleDriver
6
+ module Client
7
+
8
+ # Wraps a response of an instances.get request to the GCE API and provides
9
+ # access to information about the instance.
10
+ class Instance
11
+ def initialize(response)
12
+ @response = response
13
+ end
14
+
15
+ def name
16
+ @response[:body][:name]
17
+ end
18
+
19
+ def id
20
+ @response[:body][:id]
21
+ end
22
+
23
+ def status
24
+ @response[:body][:status]
25
+ end
26
+
27
+ def terminated?
28
+ status == "TERMINATED"
29
+ end
30
+
31
+ def running?
32
+ status == "RUNNING"
33
+ end
34
+
35
+ def stopping?
36
+ status == "STOPPING"
37
+ end
38
+
39
+ def stopped?
40
+ status == "STOPPED"
41
+ end
42
+
43
+ # TODO right now we assume the host has a accessConfig which is public
44
+ # https://cloud.google.com/compute/docs/reference/latest/instances#resource
45
+ def determine_remote_host
46
+ interfaces = @response[:body][:networkInterfaces]
47
+ interfaces.each do |i|
48
+ if i.key?(:accessConfigs)
49
+ i[:accessConfigs].each do |a|
50
+ if a.key?(:natIP)
51
+ return a[:natIP]
52
+ end
53
+ end
54
+ end
55
+ end
56
+ raise GoogleComputeError, "Server #{name} has no private or public IP address!"
57
+ end
58
+
59
+ end
60
+
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,98 @@
1
+ require_relative "google_base"
2
+ require_relative "instance"
3
+ require_relative "google_compute_error"
4
+ require "retryable"
5
+
6
+ class Chef
7
+ module Provisioning
8
+ module GoogleDriver
9
+ module Client
10
+ # Wraps an Instances service of the GCE API.
11
+ class Instances < GoogleBase
12
+
13
+ # Retrieves the instance with the given name.
14
+ # If the instance doesn't exist, returns nil.
15
+ def get(name)
16
+ response = make_request(
17
+ compute.instances.get,
18
+ { instance: name }
19
+ )
20
+ return nil if is_not_found?(response)
21
+ raise_if_error(response)
22
+ Instance.new(response)
23
+ end
24
+
25
+ def create(options = {})
26
+ response = make_request(
27
+ compute.instances.insert,
28
+ nil,
29
+ options
30
+ )
31
+ operation_response(response)
32
+ end
33
+
34
+ def delete(name)
35
+ response = make_request(
36
+ compute.instances.delete,
37
+ { instance: name }
38
+ )
39
+ operation_response(response)
40
+ end
41
+
42
+ def start(name)
43
+ response = make_request(
44
+ compute.instances.start,
45
+ { instance: name }
46
+ )
47
+ operation_response(response)
48
+ end
49
+
50
+ def stop(name)
51
+ response = make_request(
52
+ compute.instances.stop,
53
+ { instance: name }
54
+ )
55
+ operation_response(response)
56
+ end
57
+
58
+ # This returns the minimum set of options needed to create a Google
59
+ # instance. It adds required options (like name) to the object.
60
+ # https://cloud.google.com/compute/docs/instances#startinstanceapi
61
+ def default_create_options(name)
62
+ {
63
+ machineType: "zones/#{zone}/machineTypes/f1-micro",
64
+ name: name,
65
+ disks: [{
66
+ deviceName: name,
67
+ autoDelete: true,
68
+ boot: true,
69
+ initializeParams: {
70
+ sourceImage: "projects/ubuntu-os-cloud/global/images/ubuntu-1404-trusty-v20150316",
71
+ },
72
+ type: "PERSISTENT",
73
+ }],
74
+ networkInterfaces: [{
75
+ network: "global/networks/default",
76
+ name: "nic0",
77
+ accessConfigs: [{
78
+ type: "ONE_TO_ONE_NAT",
79
+ name: "External NAT",
80
+ }],
81
+ }],
82
+ }
83
+ end
84
+
85
+ def wait_for_status(action_handler, instance, status)
86
+ Retryable.retryable(tries: TRIES, sleep: SLEEP_SECONDS, matching: /reach status/) do |retries, exception|
87
+ action_handler.report_progress(" waited #{retries * SLEEP_SECONDS}/#{TRIES * SLEEP_SECONDS}s for instance #{instance.name} to become #{status}")
88
+ instance = get(instance.name)
89
+ raise GoogleComputeError, "Instance #{instance.name} didn't reach status #{status} yet." unless instance.status == status
90
+ instance
91
+ end
92
+ end
93
+
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,112 @@
1
+ class Chef
2
+ module Provisioning
3
+ module GoogleDriver
4
+ module Client
5
+
6
+ # Wraps a response of a projects.get request to the GCE API and provides
7
+ # access to metadata like SSH keys. It can also be modified and passed
8
+ # back to the projects client which will set the new metadata by sending
9
+ # a projects.set_common_instance_metadata request to the GCE API.
10
+ class Metadata
11
+ def initialize(response)
12
+ metadata = response[:body][:commonInstanceMetadata]
13
+ @fingerprint = metadata[:fingerprint]
14
+ @items = metadata[:items].dup || []
15
+ sort_items!
16
+ @changed = false
17
+ end
18
+
19
+ attr_reader :fingerprint, :items
20
+
21
+ # Returns true iff the metadata has been changed since it has been retrieved
22
+ # from GCE.
23
+ def changed?
24
+ @changed
25
+ end
26
+
27
+ # Note that the returned object should not be modified directly.
28
+ # Instead, set_ssh_key or delete_ssh_key should be used.
29
+ def ssh_keys
30
+ @ssh_keys ||= parse_keys(get_metadata_item(SSH_KEYS))
31
+ end
32
+
33
+ # Ensure the list of keys stored in GCE contains the provided key. If
34
+ # the provided key already exists it will be updated
35
+ # TODO: This will need to be updated later with user accounts:
36
+ # see https://cloud.google.com/compute/docs/access/user-accounts/
37
+ def ensure_key(username, local_key)
38
+ ssh_keys << [username, local_key]
39
+ set_metadata_item(SSH_KEYS, serialize_keys(ssh_keys))
40
+ end
41
+
42
+ # Deletes an ssh key by value, i.e. by the SSH key.
43
+ def delete_ssh_key(value)
44
+ ssh_keys.reject! { |k, v| v == value }
45
+ set_metadata_item(SSH_KEYS, serialize_keys(ssh_keys))
46
+ end
47
+
48
+ # We store the mapping of resource name to key value in GCE so we can tell
49
+ # if a key has changed.
50
+ # Note that the returned object should not be modified directly.
51
+ # Instead, set_ssh_mapping or delete_ssh_mapping should be used.
52
+ def ssh_mappings
53
+ @ssh_mappings ||= Hash[parse_keys(get_metadata_item(SSH_MAPPINGS))]
54
+ end
55
+
56
+ def set_ssh_mapping(name, local_key)
57
+ # TODO what if users change the name of the resource but not the key
58
+ # should we remove the old entry from both mappings?
59
+ ssh_mappings[name] = local_key
60
+ set_metadata_item(SSH_MAPPINGS, serialize_keys(ssh_mappings))
61
+ end
62
+
63
+ def delete_ssh_mapping(name)
64
+ ssh_mappings.delete(name)
65
+ set_metadata_item(SSH_MAPPINGS, serialize_keys(ssh_mappings))
66
+ end
67
+
68
+ private
69
+
70
+ def set_metadata_item(key, value)
71
+ @items.delete_if { |m| m[:key] == key }
72
+ @items << { key: key, value: value }
73
+ sort_items!
74
+ @changed = true
75
+ end
76
+
77
+ def sort_items!
78
+ @items.sort_by! { |a| a[:key] }
79
+ end
80
+
81
+ # Gets one metadata item with the given key.
82
+ def get_metadata_item(key)
83
+ @items.find { |item| item[:key] == key }[:value]
84
+ end
85
+
86
+ SSH_KEYS = "sshKeys"
87
+ SSH_MAPPINGS = "chef-provisioning-google_ssh-mappings"
88
+ KEY_VALUE_SEPARATOR = ":"
89
+ ITEM_SEPARATOR = "\n"
90
+
91
+ # Serializes SSH keys or SSH key mappings into a string.
92
+ # This works both for a hash map or an array of pairs.
93
+ def serialize_keys(keys)
94
+ keys.map { |e| e.join(KEY_VALUE_SEPARATOR) }.join(ITEM_SEPARATOR)
95
+ end
96
+
97
+ # Parses SSH keys or SSH key mappings from a string.
98
+ # This returns an array of pairs. If a hash_map is needed, Hash[] can be
99
+ # called on the result.
100
+ def parse_keys(keys_string)
101
+ return [] if keys_string.nil?
102
+ keys_string.split(ITEM_SEPARATOR).map do |line|
103
+ line.split(KEY_VALUE_SEPARATOR, 2)
104
+ end
105
+ end
106
+
107
+ end
108
+
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,23 @@
1
+ class Chef
2
+ module Provisioning
3
+ module GoogleDriver
4
+ module Client
5
+ class Operation
6
+
7
+ def initialize(response)
8
+ @response = response
9
+ end
10
+
11
+ def name
12
+ @response[:body][:name]
13
+ end
14
+
15
+ def done?
16
+ @response[:body][:status] == "DONE"
17
+ end
18
+
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,44 @@
1
+ require_relative "google_base"
2
+ require_relative "operation"
3
+ require_relative "google_compute_error"
4
+ require "retryable"
5
+
6
+ class Chef
7
+ module Provisioning
8
+ module GoogleDriver
9
+ module Client
10
+ class OperationsBase < GoogleBase
11
+
12
+ # The operations service that should be used,
13
+ # i.e. global_operations or zone_operations.
14
+ def operations_service
15
+ raise NotImplementedError
16
+ end
17
+
18
+ def get(operation)
19
+ response = make_request(
20
+ operations_service.get,
21
+ { operation: operation.name }
22
+ )
23
+ operation_response(response)
24
+ end
25
+
26
+ def wait_for_done(action_handler, operation)
27
+ Retryable.retryable(:tries => TRIES, :sleep => SLEEP_SECONDS, :matching => /not be completed/) do |retries, exception|
28
+ # TODO the operation name isn't useful output
29
+ action_handler.report_progress(" waited #{retries * SLEEP_SECONDS}/#{TRIES * SLEEP_SECONDS}s for operation #{operation.name} to complete")
30
+ # TODO it is really awesome that there are 3 types of operations, and the only way of telling
31
+ # which is which is to parse the full `:selfLink` from the response body. Update this method
32
+ # to take the full operation response from Google so it can tell which operation endpoint
33
+ # to hit
34
+ response_operation = get(operation)
35
+ raise GoogleComputeError, "Operation #{operation.name} could not be completed." unless response_operation.done?
36
+ response_operation
37
+ end
38
+ end
39
+
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end