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.
@@ -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