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