cloudfinder-ec2 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.
- data/.gitattributes +29 -0
- data/.gitignore +22 -0
- data/.rspec +3 -0
- data/.travis.yml +4 -0
- data/CONTRIBUTING.md +43 -0
- data/Gemfile +8 -0
- data/Guardfile +5 -0
- data/LICENSE.txt +29 -0
- data/README.md +138 -0
- data/Rakefile +18 -0
- data/bin/cloudfinder +63 -0
- data/cloudfinder-ec2.gemspec +28 -0
- data/lib/cloudfinder-ec2.rb +8 -0
- data/lib/cloudfinder-ec2/cluster.rb +88 -0
- data/lib/cloudfinder-ec2/clusterfinder.rb +97 -0
- data/lib/cloudfinder-ec2/command/list.rb +72 -0
- data/lib/cloudfinder-ec2/consts.rb +6 -0
- data/lib/cloudfinder-ec2/detector.rb +69 -0
- data/lib/cloudfinder-ec2/instance.rb +34 -0
- data/lib/cloudfinder-ec2/version.rb +5 -0
- data/spec/cloudfinder-ec2/cluster_spec.rb +164 -0
- data/spec/cloudfinder-ec2/clusterfinder_spec.rb +265 -0
- data/spec/cloudfinder-ec2/command/list_spec.rb +131 -0
- data/spec/cloudfinder-ec2/detector_spec.rb +152 -0
- data/spec/cloudfinder-ec2/instance_spec.rb +30 -0
- data/spec/cloudfinder-ec2_spec.rb +5 -0
- data/spec/spec_helper.rb +15 -0
- metadata +149 -0
@@ -0,0 +1,97 @@
|
|
1
|
+
module Cloudfinder
|
2
|
+
module EC2
|
3
|
+
class Clusterfinder
|
4
|
+
|
5
|
+
# Find all the running instances for a named cluster and build them into a cluster object,
|
6
|
+
# grouped by role.
|
7
|
+
#
|
8
|
+
# @param [Hash] query
|
9
|
+
# @option query [string] :cluster_name find instances tagged with this cluster name
|
10
|
+
# @option query [string] :region EC2 region to search
|
11
|
+
# @return [Cloudfinder::EC2::Cluster]
|
12
|
+
def find(query)
|
13
|
+
validate_arguments(query)
|
14
|
+
@cluster_name = query[:cluster_name]
|
15
|
+
instances = []
|
16
|
+
|
17
|
+
each_running_instance(query[:region]) do |instance_data|
|
18
|
+
if in_cluster?(instance_data) && has_role?(instance_data)
|
19
|
+
instances << new_instance(instance_data)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
Cloudfinder::EC2::Cluster.new(
|
24
|
+
cluster_name: @cluster_name,
|
25
|
+
instances: instances
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
ERR_REGION_REQUIRED = 'You must provide a :region argument to Instancefinder::find'
|
31
|
+
ERR_CLUSTER_NAME_REQUIRED = 'You must provide a cluster_name argument to Instancefinder::find'
|
32
|
+
|
33
|
+
# @param [Hash] args
|
34
|
+
# @return [nil]
|
35
|
+
def validate_arguments(args)
|
36
|
+
raise(ArgumentError, ERR_REGION_REQUIRED) unless (args[:region])
|
37
|
+
raise(ArgumentError, ERR_CLUSTER_NAME_REQUIRED) unless (args[:cluster_name])
|
38
|
+
end
|
39
|
+
|
40
|
+
# Find and iterate over all instances running in the given region
|
41
|
+
#
|
42
|
+
# @param [string] region
|
43
|
+
# @return [Struct] instance data provided by AWS
|
44
|
+
def each_running_instance(region)
|
45
|
+
ec2 = Aws::EC2::Client.new(region: region)
|
46
|
+
result = ec2.describe_instances(
|
47
|
+
filters: [{ name: 'instance-state-name', values: ['running'] }]
|
48
|
+
)
|
49
|
+
|
50
|
+
result[:reservations].each do |reservation|
|
51
|
+
reservation[:instances].each do |instance_data|
|
52
|
+
yield instance_data
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# @param [Struct] instance
|
58
|
+
# @return [bool]
|
59
|
+
def in_cluster?(instance)
|
60
|
+
find_tag_value(instance, CLUSTER_TAG_NAME) === @cluster_name
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
# @param [Struct] instance
|
65
|
+
# @return [bool]
|
66
|
+
def has_role?(instance)
|
67
|
+
instance.tags.any? { |tag| tag[:key] === ROLE_TAG_NAME }
|
68
|
+
end
|
69
|
+
|
70
|
+
# @param [Struct] instance
|
71
|
+
# @return [Cloudfinder::EC2::Instance]
|
72
|
+
def new_instance(instance)
|
73
|
+
Cloudfinder::EC2::Instance.new(
|
74
|
+
instance_id: instance[:instance_id],
|
75
|
+
role: find_tag_value(instance, ROLE_TAG_NAME).to_sym,
|
76
|
+
public_ip: instance[:public_ip_address],
|
77
|
+
private_ip: instance[:private_ip_address],
|
78
|
+
public_dns: instance[:public_dns_name],
|
79
|
+
private_dns: instance[:private_dns_name],
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
# @param [Struct] instance
|
84
|
+
# @param [string] tag_key
|
85
|
+
# @return [string]
|
86
|
+
def find_tag_value(instance, tag_key)
|
87
|
+
found_tag = instance.tags.select { |tag| tag[:key] === tag_key }
|
88
|
+
if found_tag.empty?
|
89
|
+
nil
|
90
|
+
else
|
91
|
+
found_tag.first[:value]
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'json'
|
2
|
+
module Cloudfinder
|
3
|
+
module EC2
|
4
|
+
module Command
|
5
|
+
class List
|
6
|
+
|
7
|
+
# Factory an instance of the list command with concrete dependencies
|
8
|
+
#
|
9
|
+
# @return [Object]
|
10
|
+
def self.factory
|
11
|
+
self.new(
|
12
|
+
Cloudfinder::EC2::Clusterfinder.new,
|
13
|
+
Cloudfinder::EC2::Detector.new,
|
14
|
+
STDOUT,
|
15
|
+
STDERR
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param [Cloudfinder::EC2::Clusterfinder] finder
|
20
|
+
# @param [Cloudfinder::EC2::Detector] detector
|
21
|
+
# @param [IO] stdout
|
22
|
+
# @param [IO] stderr
|
23
|
+
def initialize(finder, detector, stdout, stderr)
|
24
|
+
@finder = finder
|
25
|
+
@detector = detector
|
26
|
+
@stdout = stdout
|
27
|
+
@stderr = stderr
|
28
|
+
end
|
29
|
+
|
30
|
+
# Locates the roles and instances that make up a cluster, and prints the result to STDOUT
|
31
|
+
# as JSON for consumption by other processes.
|
32
|
+
#
|
33
|
+
# If the cluster_name or region are not provided, it will attempt to detect the cluster
|
34
|
+
# that the current instance belongs to (using the EC2 metadata service) and find other
|
35
|
+
# instances in the same cluster.
|
36
|
+
#
|
37
|
+
# If cluster detection fails, an exception will be thrown
|
38
|
+
#
|
39
|
+
# @param [Hash] query
|
40
|
+
# @option query [string] :cluster_name optionally specify cluster name to find
|
41
|
+
# @option query [string] :region optionally specify EC2 region to search
|
42
|
+
# @return void
|
43
|
+
def execute(query = {})
|
44
|
+
unless query[:region] && query[:cluster_name]
|
45
|
+
query = autodetect_cluster_or_throw.merge(query)
|
46
|
+
end
|
47
|
+
|
48
|
+
cluster = @finder.find(query)
|
49
|
+
@stdout.puts(JSON.pretty_generate(cluster.to_hash))
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def autodetect_cluster_or_throw
|
55
|
+
begin
|
56
|
+
return @detector.detect_cluster
|
57
|
+
rescue StandardError => e
|
58
|
+
@stderr.puts('------------------------------------------------------------')
|
59
|
+
@stderr.puts('| Automatic cluster detection error |')
|
60
|
+
@stderr.puts('|----------------------------------------------------------|')
|
61
|
+
@stderr.puts('| This instance may not be running on EC2, or there may be |')
|
62
|
+
@stderr.puts('| a temporary issue with the EC2 metadata service. See the |')
|
63
|
+
@stderr.puts('| exception message below for details. |')
|
64
|
+
@stderr.puts('------------------------------------------------------------')
|
65
|
+
raise e
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
module Cloudfinder
|
3
|
+
module EC2
|
4
|
+
class Detector
|
5
|
+
EC2_METADATA_TIMEOUT = 5
|
6
|
+
EC2_METADATA_BASE_URL = 'http://169.254.169.254/latest/meta-data'
|
7
|
+
|
8
|
+
# Detects the cluster for the current instance using the instance metadata service
|
9
|
+
# and the AWS API to fetch tags.
|
10
|
+
#
|
11
|
+
# @return [Hash] with the cluster_name, cluster_role, instance_id and region
|
12
|
+
def detect_cluster
|
13
|
+
initialize_metadata
|
14
|
+
result = {
|
15
|
+
region: @region,
|
16
|
+
instance_id: @instance_id,
|
17
|
+
cluster_name: nil,
|
18
|
+
cluster_role: nil
|
19
|
+
}
|
20
|
+
|
21
|
+
find_instance_tags.each do |tag|
|
22
|
+
if tag[:key] === CLUSTER_TAG_NAME
|
23
|
+
result[:cluster_name] = tag[:value]
|
24
|
+
elsif tag[:key] === ROLE_TAG_NAME
|
25
|
+
result[:cluster_role] = tag[:value].to_sym
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
if result[:cluster_name].nil? || result[:cluster_role].nil?
|
30
|
+
result[:cluster_name] = nil
|
31
|
+
result[:cluster_role] = nil
|
32
|
+
end
|
33
|
+
|
34
|
+
result
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def initialize_metadata
|
40
|
+
@instance_id = find_instance_id
|
41
|
+
@region = find_instance_region
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [Array<Hash>]
|
45
|
+
def find_instance_tags
|
46
|
+
ec2 = Aws::EC2::Client.new(region: @region)
|
47
|
+
result = ec2.describe_tags(filters: [{ name: 'resource-id', values: [@instance_id] }])
|
48
|
+
result[:tags]
|
49
|
+
end
|
50
|
+
|
51
|
+
# @return [string]
|
52
|
+
def find_instance_region
|
53
|
+
zone = get_metadata('/placement/availability-zone')
|
54
|
+
zone[0, zone.length - 1]
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [string]
|
58
|
+
def find_instance_id
|
59
|
+
get_metadata('/instance-id')
|
60
|
+
end
|
61
|
+
|
62
|
+
# @param [string] path
|
63
|
+
# @return [string]
|
64
|
+
def get_metadata(path)
|
65
|
+
open("#{EC2_METADATA_BASE_URL}#{path}", { read_timeout: EC2_METADATA_TIMEOUT }).read
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Cloudfinder
|
2
|
+
module EC2
|
3
|
+
class Instance
|
4
|
+
attr_reader(:instance_id)
|
5
|
+
attr_reader(:public_ip)
|
6
|
+
attr_reader(:public_dns)
|
7
|
+
attr_reader(:private_ip)
|
8
|
+
attr_reader(:private_dns)
|
9
|
+
attr_reader(:role)
|
10
|
+
|
11
|
+
# @param [Hash<string>] instance attributes
|
12
|
+
def initialize(data)
|
13
|
+
@instance_id = data[:instance_id].freeze
|
14
|
+
@public_ip = data[:public_ip].freeze
|
15
|
+
@public_dns = data[:public_dns].freeze
|
16
|
+
@private_ip = data[:private_ip].freeze
|
17
|
+
@private_dns = data[:private_dns].freeze
|
18
|
+
@role = data[:role]
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [Hash<string>]
|
22
|
+
def to_hash
|
23
|
+
{
|
24
|
+
instance_id: @instance_id,
|
25
|
+
public_ip: @public_ip,
|
26
|
+
public_dns: @public_dns,
|
27
|
+
private_dns: @private_dns,
|
28
|
+
private_ip: @private_ip,
|
29
|
+
role: @role
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
describe Cloudfinder::EC2::Cluster do
|
2
|
+
let (:cluster_name) { 'production' }
|
3
|
+
let (:instances) { [] }
|
4
|
+
subject { Cloudfinder::EC2::Cluster.new(cluster_name: cluster_name, instances: instances) }
|
5
|
+
|
6
|
+
shared_examples_for 'cluster' do
|
7
|
+
it 'should have cluster name' do
|
8
|
+
expect(subject.cluster_name).to eq(cluster_name)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'should include cluster name in hash representation' do
|
12
|
+
expect(subject.to_hash[:cluster_name]).to eq cluster_name
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
shared_examples_for 'undefined role' do |rolename|
|
17
|
+
it "should not have #{rolename} role" do
|
18
|
+
expect(subject).not_to have_role(rolename)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should not have #{rolename} in hash representation" do
|
22
|
+
expect(subject.to_hash[:roles]).not_to have_key rolename
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should have empty instances for #{rolename} role" do
|
26
|
+
expect(subject.list_role_instances(rolename)).to be_empty
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
shared_examples_for 'undefined instance' do |unknown_instance_id|
|
31
|
+
it 'should not have unknown instance' do
|
32
|
+
expect(subject).not_to have_instance(unknown_instance_id)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should throw when getting unknown instance' do
|
36
|
+
expect {subject.get_instance(unknown_instance_id)}.to raise_error(RangeError)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
shared_examples_for 'defined role with instances' do |role_name, instance_count|
|
41
|
+
it "should have #{role_name} role" do
|
42
|
+
expect(subject).to have_role(role_name)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should have #{role_name} in hash representation" do
|
46
|
+
expect(subject.to_hash[:roles]).to have_key role_name
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should have #{instance_count} elements for #{role_name} in hash representation" do
|
50
|
+
expect(subject.to_hash[:roles][role_name].count).to eq instance_count
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should list #{instance_count} instances for the #{role_name} role" do
|
54
|
+
expect(subject.list_role_instances(role_name).count).to eq(instance_count)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
shared_examples_for 'running instance in role' do |role_name, instance_index, instance_id|
|
59
|
+
it "should have the #{role_name}:##{instance_index} instance" do
|
60
|
+
expect(subject).to have_instance(instance_id)
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should get instance object for instance id #{instance_id}" do
|
64
|
+
instance = subject.get_instance(instance_id)
|
65
|
+
expect(instance).to be_a Cloudfinder::EC2::Instance
|
66
|
+
expect(instance.instance_id).to eq instance_id
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should return instance object for #{role_name}:##{instance_index}" do
|
70
|
+
instance = subject.list_role_instances(role_name)[instance_index]
|
71
|
+
expect(instance).to be_a Cloudfinder::EC2::Instance
|
72
|
+
expect(instance.instance_id).to eq instance_id
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should list the #{role_name}:##{instance_index} instance for the correct role" do
|
76
|
+
listed_instance = subject.list_role_instances(role_name)[instance_index]
|
77
|
+
expect(listed_instance.instance_id).to eq(instance_id)
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should include #{role_name}:##{instance_index} in hash representation" do
|
81
|
+
instance = subject.get_instance(instance_id)
|
82
|
+
expect(subject.to_hash[:roles][role_name][instance_index]).to eq instance.to_hash
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context 'when created with no instances' do
|
87
|
+
include_examples 'cluster'
|
88
|
+
it { should be_empty }
|
89
|
+
it { should_not be_running }
|
90
|
+
|
91
|
+
include_examples('undefined role', :any)
|
92
|
+
include_examples('undefined instance', 'i-0000001')
|
93
|
+
|
94
|
+
it 'should have empty roles list' do
|
95
|
+
expect(subject.list_roles).to be_empty
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
context 'when created with single db instance' do
|
100
|
+
let (:instances) { [
|
101
|
+
stub_instance(:instance_id => 'i-0000001', :role => :db)
|
102
|
+
] }
|
103
|
+
|
104
|
+
it { should_not be_empty }
|
105
|
+
it { should be_running }
|
106
|
+
include_examples 'cluster'
|
107
|
+
include_examples('undefined role', :'any other')
|
108
|
+
include_examples('undefined instance', 'i-0000002')
|
109
|
+
include_examples('defined role with instances', :db, 1)
|
110
|
+
include_examples('running instance in role', :db, 0, 'i-0000001')
|
111
|
+
|
112
|
+
it 'should list the db role only' do
|
113
|
+
expect(subject.list_roles).to eq([:db])
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context 'when created with multiple db instances' do
|
118
|
+
let (:instances) { [
|
119
|
+
stub_instance(:instance_id => 'i-0000001', :role => :db),
|
120
|
+
stub_instance(:instance_id => 'i-0000002', :role => :db)
|
121
|
+
] }
|
122
|
+
|
123
|
+
it { should_not be_empty }
|
124
|
+
it { should be_running }
|
125
|
+
include_examples 'cluster'
|
126
|
+
include_examples('undefined role', :'any other')
|
127
|
+
include_examples('undefined instance', 'i-0000003')
|
128
|
+
include_examples('defined role with instances', :db, 2)
|
129
|
+
include_examples('running instance in role', :db, 0, 'i-0000001')
|
130
|
+
include_examples('running instance in role', :db, 1, 'i-0000002')
|
131
|
+
|
132
|
+
it 'should list the db role only' do
|
133
|
+
expect(subject.list_roles).to eq([:db])
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
context 'when created with multiple db and app instances' do
|
138
|
+
let (:instances) { [
|
139
|
+
stub_instance(:instance_id => 'i-0000001', :role => :db),
|
140
|
+
stub_instance(:instance_id => 'i-0000002', :role => :app),
|
141
|
+
stub_instance(:instance_id => 'i-0000003', :role => :app)
|
142
|
+
] }
|
143
|
+
|
144
|
+
it { should_not be_empty }
|
145
|
+
it { should be_running }
|
146
|
+
include_examples 'cluster'
|
147
|
+
include_examples('undefined role', :'any other')
|
148
|
+
include_examples('undefined instance', 'i-0000004')
|
149
|
+
include_examples('defined role with instances', :db, 1)
|
150
|
+
include_examples('defined role with instances', :app, 2)
|
151
|
+
include_examples('running instance in role', :db, 0, 'i-0000001')
|
152
|
+
include_examples('running instance in role', :app, 0, 'i-0000002')
|
153
|
+
include_examples('running instance in role', :app, 1, 'i-0000003')
|
154
|
+
|
155
|
+
it 'should list the db and app roles only' do
|
156
|
+
expect(subject.list_roles).to eq([:db, :app])
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def stub_instance(data)
|
161
|
+
Cloudfinder::EC2::Instance.new(data)
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|