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.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/README.md +121 -0
- data/Rakefile +22 -0
- data/lib/chef/provider/google_key_pair.rb +172 -0
- data/lib/chef/provisioning/driver_init/google.rb +3 -0
- data/lib/chef/provisioning/google_driver.rb +5 -0
- data/lib/chef/provisioning/google_driver/client/global_operations.rb +18 -0
- data/lib/chef/provisioning/google_driver/client/google_base.rb +72 -0
- data/lib/chef/provisioning/google_driver/client/google_compute_error.rb +16 -0
- data/lib/chef/provisioning/google_driver/client/instance.rb +64 -0
- data/lib/chef/provisioning/google_driver/client/instances.rb +98 -0
- data/lib/chef/provisioning/google_driver/client/metadata.rb +112 -0
- data/lib/chef/provisioning/google_driver/client/operation.rb +23 -0
- data/lib/chef/provisioning/google_driver/client/operations_base.rb +44 -0
- data/lib/chef/provisioning/google_driver/client/projects.rb +39 -0
- data/lib/chef/provisioning/google_driver/client/zone_operations.rb +18 -0
- data/lib/chef/provisioning/google_driver/credentials.rb +63 -0
- data/lib/chef/provisioning/google_driver/driver.rb +313 -0
- data/lib/chef/provisioning/google_driver/resources.rb +0 -0
- data/lib/chef/provisioning/google_driver/version.rb +7 -0
- data/lib/chef/resource/google_key_pair.rb +50 -0
- data/spec/chef/provisioning/google_driver/client/instances_spec.rb +205 -0
- data/spec/chef/provisioning/google_driver/client/operations_spec.rb +62 -0
- data/spec/chef/provisioning/google_driver/client/projects_spec.rb +162 -0
- data/spec/chef/provisioning/google_driver/client/services_helper.rb +33 -0
- data/spec/spec_helper.rb +0 -0
- metadata +222 -0
@@ -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,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
|