cloudfinder-ec2 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,6 @@
1
+ module Cloudfinder
2
+ module EC2
3
+ CLUSTER_TAG_NAME = 'cloudfinder-cluster'
4
+ ROLE_TAG_NAME = 'cloudfinder-role'
5
+ end
6
+ 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,5 @@
1
+ module Cloudfinder
2
+ module EC2
3
+ VERSION = "0.1.0"
4
+ end
5
+ 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