chef-provisioning-google 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|