gce-host 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 (53) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.yardopts +6 -0
  4. data/CHANGELOG.md +3 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE +22 -0
  7. data/README.md +210 -0
  8. data/Rakefile +30 -0
  9. data/bin/gce-host +4 -0
  10. data/docs/GCE.html +132 -0
  11. data/docs/GCE/Host.html +901 -0
  12. data/docs/GCE/Host/CLI.html +610 -0
  13. data/docs/GCE/Host/Config.html +1195 -0
  14. data/docs/GCE/Host/GCEClient.html +215 -0
  15. data/docs/GCE/Host/GCEClient/Error.html +127 -0
  16. data/docs/GCE/Host/GCEClient/NotFound.html +131 -0
  17. data/docs/GCE/Host/HashUtil.html +178 -0
  18. data/docs/GCE/Host/HostData.html +1658 -0
  19. data/docs/GCE/Host/RoleData.html +932 -0
  20. data/docs/GCE/Host/StringUtil.html +359 -0
  21. data/docs/_index.html +231 -0
  22. data/docs/class_list.html +58 -0
  23. data/docs/css/common.css +1 -0
  24. data/docs/css/full_list.css +57 -0
  25. data/docs/css/style.css +339 -0
  26. data/docs/file.LICENSE.html +95 -0
  27. data/docs/file.README.html +312 -0
  28. data/docs/file_list.html +63 -0
  29. data/docs/frames.html +26 -0
  30. data/docs/index.html +312 -0
  31. data/docs/js/app.js +219 -0
  32. data/docs/js/full_list.js +181 -0
  33. data/docs/js/jquery.js +4 -0
  34. data/docs/method_list.html +477 -0
  35. data/docs/top-level-namespace.html +112 -0
  36. data/example/example.conf +8 -0
  37. data/example/example.rb +6 -0
  38. data/gce-host.gemspec +26 -0
  39. data/lib/gce-host.rb +7 -0
  40. data/lib/gce/host.rb +120 -0
  41. data/lib/gce/host/cli.rb +151 -0
  42. data/lib/gce/host/config.rb +109 -0
  43. data/lib/gce/host/gce_client.rb +69 -0
  44. data/lib/gce/host/hash_util.rb +11 -0
  45. data/lib/gce/host/host_data.rb +227 -0
  46. data/lib/gce/host/role_data.rb +64 -0
  47. data/lib/gce/host/string_util.rb +31 -0
  48. data/lib/gce/host/version.rb +5 -0
  49. data/spec/gce_client_spec.rb +29 -0
  50. data/spec/host_spec.rb +199 -0
  51. data/spec/spec_helper.rb +22 -0
  52. data/terraform.tf +52 -0
  53. metadata +226 -0
@@ -0,0 +1,69 @@
1
+ require_relative 'version'
2
+ require 'google/apis/compute_v1'
3
+
4
+ class GCE
5
+ class Host
6
+ class GCEClient
7
+ class Error < ::StandardError; end
8
+ class NotFound < Error; end
9
+
10
+ def instances(condition = {})
11
+ filter = build_filter(condition)
12
+ instances = []
13
+ res = client.list_aggregated_instances(Config.project, filter: filter)
14
+ instances.concat(res.items.values.map(&:instances).compact.flatten(1))
15
+ while res.next_page_token
16
+ res = client.list_aggregated_instances(Config.project, filter: filter, page_token: res.next_page_token)
17
+ instances.concat(res.items.values.map(&:instances).compact.flatten(1))
18
+ end
19
+ instances
20
+ end
21
+
22
+ private
23
+
24
+ def client
25
+ return @client if @client && @client_expiration > Time.now
26
+
27
+ scope = "https://www.googleapis.com/auth/compute.readonly"
28
+ client = Google::Apis::ComputeV1::ComputeService.new
29
+ client.client_options.application_name = 'gce-host'
30
+ client.client_options.application_name = GCE::Host::VERSION
31
+ client.request_options.retries = Config.retries
32
+ client.request_options.timeout_sec = Config.timeout_sec
33
+ client.request_options.open_timeout_sec = Config.open_timeout_sec
34
+
35
+ case Config.auth_method
36
+ when 'compute_engine'
37
+ auth = Google::Auth::GCECredentials.new
38
+
39
+ when 'json_key'
40
+ credential_file = Config.credential_file
41
+ auth = File.open(credential_file) do |f|
42
+ Google::Auth::ServiceAccountCredentials.make_creds(json_key_io: f, scope: scope)
43
+ end
44
+
45
+ when 'application_default'
46
+ auth = Google::Auth.get_application_default([scope])
47
+
48
+ else
49
+ raise ConfigError, "Unknown auth method: #{Config.auth_method}"
50
+ end
51
+
52
+ client.authorization = auth
53
+
54
+ @client_expiration = Time.now + 1800
55
+ @client = client
56
+ end
57
+
58
+ # MEMO: OR did not work
59
+ # MEMO: filter for metadata and tags did not work (metadata.items[0].value eq role)
60
+ def build_filter(condition)
61
+ if names = (condition[:name] || condition[:hostname]) and Array(names).size == 1
62
+ "name eq #{Array(names).first}"
63
+ else
64
+ nil
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,11 @@
1
+ class GCE
2
+ class Host
3
+ module HashUtil
4
+ def self.except(hash, *keys)
5
+ hash = hash.dup
6
+ keys.each {|key| hash.delete(key) }
7
+ hash
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,227 @@
1
+ require 'json'
2
+
3
+ class GCE
4
+ class Host
5
+ # Represents each host
6
+ class HostData
7
+ attr_reader :instance
8
+
9
+ # :hostname, # hostname
10
+ # :roles, # roles.split(',') such as web:app1,db:app1
11
+ # :instance, # Aws::GCE::Types::Instance itself
12
+ #
13
+ # and OPTIONAL_ARRAY_KEYS, OPTIONAL_STRING_KEYS
14
+ def initialize(instance)
15
+ @instance = instance
16
+ end
17
+
18
+ def hostname
19
+ instance.name
20
+ end
21
+
22
+ def roles
23
+ return @roles if @roles
24
+ roles = find_array_key(Config.roles_key)
25
+ @roles = roles.map {|role| GCE::Host::RoleData.build(role) }
26
+ end
27
+
28
+ Config.optional_string_keys.each do |key|
29
+ field = StringUtil.underscore(key)
30
+ define_method(field) do
31
+ instance_variable_get("@#{field}") || instance_variable_set("@#{field}", find_string_key(key))
32
+ end
33
+ end
34
+
35
+ Config.optional_array_keys.each do |key|
36
+ field = StringUtil.underscore(key)
37
+ define_method(field) do
38
+ instance_variable_get("@#{field}") || instance_variable_set("@#{field}", find_array_key(key))
39
+ end
40
+ end
41
+
42
+ define_method(Config.status) do
43
+ instance.status
44
+ end
45
+
46
+ def instance_id
47
+ instance.id
48
+ end
49
+
50
+ def zone
51
+ instance.zone.split('/').last
52
+ end
53
+
54
+ def private_ip_address
55
+ instance.network_interfaces.first.network_ip
56
+ end
57
+
58
+ def private_ip_addresses
59
+ instance.network_interfaces.map(&:network_ip)
60
+ end
61
+
62
+ def public_ip_address
63
+ instance.network_interfaces.first.access_configs.first.nat_ip
64
+ end
65
+
66
+ def public_ip_addresses
67
+ instance.network_interfaces.map {|i| i.access_configs.map(&:nat_ip) }.flatten(1)
68
+ end
69
+
70
+ def creation_timestamp
71
+ instance.creation_timestamp
72
+ end
73
+
74
+ # compatibility with dino-host
75
+ def ip
76
+ private_ip_address
77
+ end
78
+
79
+ # compatibility with dino-host
80
+ def start_date
81
+ creation_timestamp
82
+ end
83
+
84
+ # compatibility with dino-host
85
+ def usages
86
+ roles
87
+ end
88
+
89
+ def terminated?
90
+ instance.status == "TERMINATED"
91
+ end
92
+
93
+ def stopping?
94
+ instance.status == "STOPPING"
95
+ end
96
+
97
+ def running?
98
+ instance.status == "RUNNING"
99
+ end
100
+
101
+ def staging?
102
+ instance.status == "STAGING"
103
+ end
104
+
105
+ def provisioning?
106
+ instance.status == "PROVISIONING"
107
+ end
108
+
109
+ # match with condition or not
110
+ #
111
+ # @param [Hash] condition search parameters
112
+ def match?(condition)
113
+ return false if !condition[Config.status.to_sym] and (terminated? or stopping?)
114
+ return false unless role_match?(condition)
115
+ return false unless status_match?(condition)
116
+ return false unless instance_match?(condition)
117
+ true
118
+ end
119
+
120
+ def to_hash
121
+ params = {
122
+ "hostname" => hostname,
123
+ "roles" => roles,
124
+ "zone" => zone,
125
+ }
126
+ Config.optional_string_keys.each do |key|
127
+ field = StringUtil.underscore(key)
128
+ params[field] = send(field)
129
+ end
130
+ Config.optional_array_keys.each do |key|
131
+ field = StringUtil.underscore(key)
132
+ params[field] = send(field)
133
+ end
134
+ params.merge!(
135
+ "instance_id" => instance_id,
136
+ "private_ip_address" => private_ip_address,
137
+ "public_ip_address" => public_ip_address,
138
+ "creation_timestamp" => creation_timestamp,
139
+ Config.status => send(Config.status),
140
+ )
141
+ end
142
+
143
+ def info
144
+ if self.class.display_short_info?
145
+ info = "#{hostname}:#{status}"
146
+ info << "(#{roles.join(',')})" unless roles.empty?
147
+ info << "[#{tags.join(',')}]" unless tags.empty?
148
+ info << "{#{service}}" unless service.empty?
149
+ info
150
+ else
151
+ to_hash.to_s
152
+ end
153
+ end
154
+
155
+ def inspect
156
+ sprintf "#<GCE::Host::HostData %s>", info
157
+ end
158
+
159
+ private
160
+
161
+ def find_string_key(key)
162
+ item = instance.metadata.items.find {|item| item.key == key } if instance.metadata.items
163
+ item ? item.value : ''
164
+ end
165
+
166
+ def find_array_key(key)
167
+ item = instance.metadata.items.find {|item| item.key == key } if instance.metadata.items
168
+ item ? item.value.split(Config.array_value_delimiter) : []
169
+ end
170
+
171
+ def role_match?(condition)
172
+ # usage is an alias of role
173
+ if role = (condition[:role] || condition[:usage])
174
+ role1, role2, role3 = role.first.split(':')
175
+ else
176
+ role1 = (condition[:role1] || condition[:usage1] || []).first
177
+ role2 = (condition[:role2] || condition[:usage2] || []).first
178
+ role3 = (condition[:role3] || condition[:usage3] || []).first
179
+ end
180
+ if role1
181
+ return false unless roles.find {|role| role.match?(role1, role2, role3) }
182
+ end
183
+ true
184
+ end
185
+
186
+ def status_match?(condition)
187
+ if values = condition[Config.status.to_sym]
188
+ return false unless values.map(&:downcase).include?(send(Config.status).downcase)
189
+ end
190
+ true
191
+ end
192
+
193
+ def instance_match?(condition)
194
+ condition = HashUtil.except(condition, :role, :role1, :role2, :role3, :usage, :usage1, :usage2, :usage3, Config.status.to_sym)
195
+ condition.each do |key, values|
196
+ v = instance_variable_recursive_get(key)
197
+ if v.is_a?(Array)
198
+ return false unless v.find {|_| values.include?(_) }
199
+ else
200
+ return false unless values.include?(v)
201
+ end
202
+ end
203
+ true
204
+ end
205
+
206
+ # "instance.instance_id" => self.instance.instance_id
207
+ def instance_variable_recursive_get(key)
208
+ v = self
209
+ key.to_s.split('.').each {|k| v = v.send(k) }
210
+ v
211
+ end
212
+
213
+ # compatibility with dono-host
214
+ #
215
+ # If service,status,tags keys are defined
216
+ #
217
+ # OPTIONAL_STRING_KEYS=service,status
218
+ # OPTIONAL_ARRAY_KEYS=tags
219
+ #
220
+ # show in short format, otherwise, same with to_hash.to_s
221
+ def self.display_short_info?
222
+ return @display_short_info unless @display_short_info.nil?
223
+ @display_short_info = method_defined?(:service) and method_defined?(:status) and method_defined?(:tags)
224
+ end
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,64 @@
1
+ class GCE
2
+ class Host
3
+ # Represents each role
4
+ class RoleData
5
+ attr_reader :role1, :role2, :role3
6
+
7
+ def initialize(role1, role2 = nil, role3 = nil)
8
+ @role1 = role1
9
+ @role2 = role2
10
+ @role3 = role3
11
+ end
12
+
13
+ def self.build(role)
14
+ role1, role2, role3 = role.split(Config.role_value_delimiter, 3)
15
+ new(role1, role2, role3)
16
+ end
17
+
18
+ # @return [String] something like "admin:jenkins:slave"
19
+ def role
20
+ @role ||= [role1, role2, role3].compact.reject(&:empty?).join(Config.role_value_delimiter)
21
+ end
22
+ alias :to_s :role
23
+
24
+ # @return [Array] something like ["admin", "admin:jenkins", "admin:jenkins:slave"]
25
+ def uppers
26
+ uppers = [RoleData.new(role1)]
27
+ uppers << RoleData.new(role1, role2) if role2 and !role2.empty?
28
+ uppers << RoleData.new(role1, role2, role3) if role3 and !role3.empty?
29
+ uppers
30
+ end
31
+
32
+ def match?(role1, role2 = nil, role3 = nil)
33
+ if role3
34
+ role1 == self.role1 and role2 == self.role2 and role3 == self.role3
35
+ elsif role2
36
+ role1 == self.role1 and role2 == self.role2
37
+ else
38
+ role1 == self.role1
39
+ end
40
+ end
41
+
42
+ # Equality
43
+ #
44
+ # Role::Data.new('admin') == Role::Data.new('admin') #=> true
45
+ # Role::Data.new('admin', 'jenkin') == "admin:jenkins" #=> true
46
+ #
47
+ # @param [Object] other
48
+ def ==(other)
49
+ case other
50
+ when String
51
+ self.role == other
52
+ when GCE::Host::RoleData
53
+ super(other)
54
+ else
55
+ false
56
+ end
57
+ end
58
+
59
+ def inspect
60
+ "\"#{to_s}\""
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,31 @@
1
+ class GCE
2
+ class Host
3
+ # If want sophisticated utility, better to use ActiveSupport
4
+ module StringUtil
5
+ def self.camelize(string)
6
+ string = string.sub(/^[a-z\d]*/) { $&.capitalize }
7
+ string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { $2.capitalize }
8
+ string.gsub!(/\//, '::')
9
+ string
10
+ end
11
+
12
+ def self.underscore(camel_cased_word)
13
+ return camel_cased_word unless camel_cased_word =~ /[A-Z-]|::/
14
+ word = camel_cased_word.to_s.gsub(/::/, '/')
15
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
16
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
17
+ word.tr!("-", "_")
18
+ word.downcase!
19
+ word
20
+ end
21
+
22
+ def self.pluralize(string)
23
+ "#{string.chomp('s')}s"
24
+ end
25
+
26
+ def self.singularize(string)
27
+ string.chomp('s')
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,5 @@
1
+ class GCE
2
+ class Host
3
+ VERSION = '0.1.0'
4
+ end
5
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ describe GCE::Host::GCEClient do
4
+ let(:client) { GCE::Host::GCEClient.new }
5
+ let(:instances) { client.instances }
6
+ let(:instance) { instances.first }
7
+ let(:name) { instance.name }
8
+
9
+ describe '#instances' do
10
+ it do
11
+ expect(instances).not_to be_empty
12
+ end
13
+
14
+ it 'filter with name' do
15
+ instances = client.instances(name: name)
16
+ expect(instances).not_to be_empty
17
+ expect(instances.first.name).to eq(name)
18
+
19
+ instances = client.instances(name: 'something_not_exist')
20
+ expect(instances).to be_empty
21
+ end
22
+
23
+ it 'filter with hostname (alias of name)' do
24
+ instances = client.instances(hostname: name)
25
+ expect(instances).not_to be_empty
26
+ expect(instances.first.name).to eq(name)
27
+ end
28
+ end
29
+ end
data/spec/host_spec.rb ADDED
@@ -0,0 +1,199 @@
1
+ require 'spec_helper'
2
+
3
+ shared_examples_for 'host' do
4
+ it 'should respond_to' do
5
+ [ :hostname,
6
+ :roles,
7
+ :zone,
8
+ GCE::Host::Config.status.to_sym,
9
+ :service,
10
+ :status,
11
+ :tags,
12
+ :instance,
13
+ :instance_id,
14
+ :private_ip_address,
15
+ :private_ip_addresses,
16
+ :public_ip_address,
17
+ :public_ip_addresses,
18
+ :creation_timestamp,
19
+ :ip,
20
+ :start_date,
21
+ :usages,
22
+ ].each do |k|
23
+ expect(subject.respond_to?(k)).to be_truthy
24
+ end
25
+ end
26
+ end
27
+
28
+ describe GCE::Host do
29
+ describe 'options' do
30
+ context do
31
+ let(:subject) { GCE::Host.new(hostname: 'gce-host-web', options: {foo:'bar'}) }
32
+ it { expect(subject.options).to eq({foo:'bar'}) }
33
+ it { expect(subject.conditions).to eq([{hostname: ['gce-host-web']}]) }
34
+ end
35
+
36
+ context do
37
+ let(:subject) { GCE::Host.new({hostname: 'gce-host-web'}, {hostname: 'gce-host-db'}, options: {foo:'bar'}) }
38
+ it { expect(subject.options).to eq({foo:'bar'}) }
39
+ it { expect(subject.conditions).to eq([{hostname: ['gce-host-web']}, {hostname: ['gce-host-db']}]) }
40
+ end
41
+ end
42
+
43
+ context '#to_hash' do
44
+ let(:subject) { GCE::Host.new(service: 'gce-host').first.to_hash }
45
+
46
+ it 'keys' do
47
+ expect(subject.keys).to eq([
48
+ 'hostname',
49
+ 'roles',
50
+ 'zone',
51
+ 'service',
52
+ 'status',
53
+ 'tags',
54
+ 'instance_id',
55
+ 'private_ip_address',
56
+ 'public_ip_address',
57
+ 'creation_timestamp',
58
+ GCE::Host::Config.status,
59
+ ])
60
+ end
61
+
62
+ it 'values are not empty' do
63
+ expect(subject.values.any? {|v| v.nil? or (v.respond_to?(:empty?) and v.empty?) }).to be_falsey
64
+ end
65
+ end
66
+
67
+ context 'by hostname' do
68
+ let(:hosts) { GCE::Host.new(hostname: 'gce-host-web').to_a }
69
+ let(:subject) { hosts.first }
70
+ it_should_behave_like 'host'
71
+ it { expect(hosts.size).to eq(1) }
72
+ it { expect(subject.hostname).to eq('gce-host-web') }
73
+ end
74
+
75
+ context 'by instance_id' do
76
+ let(:instance_id) { GCE::Host.new(service: 'gce-host').first.instance_id }
77
+
78
+ context 'by instance_id' do
79
+ let(:hosts) { GCE::Host.new(instance_id: instance_id).to_a }
80
+ let(:subject) { hosts.first }
81
+ it_should_behave_like 'host'
82
+ it { expect(hosts.size).to eq(1) }
83
+ it { expect(subject.instance_id).to eq(instance_id) }
84
+ end
85
+ end
86
+
87
+ context 'by role' do
88
+ context 'by a role' do
89
+ let(:subject) { GCE::Host.new(role: 'web:test').first }
90
+ it_should_behave_like 'host'
91
+ end
92
+
93
+ context 'by a role1' do
94
+ let(:subject) { GCE::Host.new(role1: 'web').first }
95
+ it_should_behave_like 'host'
96
+ end
97
+
98
+ context 'by multiple roles (or)' do
99
+ let(:hosts) {
100
+ GCE::Host.new(
101
+ {
102
+ role1: 'web',
103
+ role2: 'test',
104
+ },
105
+ {
106
+ role1: 'db',
107
+ role2: 'test',
108
+ },
109
+ ).to_a
110
+ }
111
+ let(:subject) { hosts.first }
112
+ it { expect(hosts.size).to be >= 2 }
113
+ it_should_behave_like 'host'
114
+ end
115
+ end
116
+
117
+ # for compatibility with dino-host
118
+ context 'by usage' do
119
+ context 'by a usage' do
120
+ let(:subject) { GCE::Host.new(usage: 'web:test').first }
121
+ it_should_behave_like 'host'
122
+ end
123
+
124
+ context 'by a usage1' do
125
+ let(:subject) { GCE::Host.new(usage1: 'web').first }
126
+ it_should_behave_like 'host'
127
+ end
128
+
129
+ context 'by multiple usages (or)' do
130
+ let(:hosts) {
131
+ GCE::Host.new(
132
+ {
133
+ usage1: 'web',
134
+ usage2: 'test',
135
+ },
136
+ {
137
+ usage1: 'db',
138
+ usage2: 'test',
139
+ },
140
+ ).to_a
141
+ }
142
+ let(:subject) { hosts.first }
143
+ it { expect(hosts.size).to be >= 2 }
144
+ it_should_behave_like 'host'
145
+ end
146
+ end
147
+
148
+ context 'by status (optional array tags)' do
149
+ context 'by a status' do
150
+ let(:subject) { GCE::Host.new(status: :active).first }
151
+ it_should_behave_like 'host'
152
+ end
153
+
154
+ context 'by multiple status (or)' do
155
+ let(:hosts) { GCE::Host.new(status: [:reserve, :active]).to_a }
156
+ let(:subject) { hosts.first }
157
+ it_should_behave_like 'host'
158
+ it { expect(hosts.size).to be >= 2 }
159
+ end
160
+
161
+ context 'by a string status' do
162
+ let(:subject) { GCE::Host.new(status: 'active').first }
163
+ it_should_behave_like 'host'
164
+ end
165
+
166
+ context 'by multiple string status (or)' do
167
+ let(:hosts) { GCE::Host.new(status: ['reserve', 'active']).to_a }
168
+ let(:subject) { hosts.first }
169
+ it_should_behave_like 'host'
170
+ it { expect(hosts.size).to be >= 2 }
171
+ end
172
+ end
173
+
174
+ context 'by service (optional string tags)' do
175
+ context 'by a service' do
176
+ let(:subject) { GCE::Host.new(service: 'gce-host').first }
177
+ it_should_behave_like 'host'
178
+ end
179
+
180
+ context 'by multiple services (or)' do
181
+ let(:hosts) { GCE::Host.new(service: ['test', 'gce-host']) }
182
+ let(:subject) { hosts.first }
183
+ it_should_behave_like 'host'
184
+ end
185
+ end
186
+
187
+ context 'by tags (optional array tags)' do
188
+ context 'by a tag' do
189
+ let(:subject) { GCE::Host.new(tags: 'master').first }
190
+ it_should_behave_like 'host'
191
+ end
192
+
193
+ context 'by multiple tags (or)' do
194
+ let(:hosts) { GCE::Host.new(tags: ['standby', 'master']) }
195
+ let(:subject) { hosts.first }
196
+ it_should_behave_like 'host'
197
+ end
198
+ end
199
+ end