ec2-host 0.0.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 94ae5ab54d79d7e269102382f12b40c31f16c349
4
+ data.tar.gz: 14ccb9795b4629b607899c7192ad1567e50c4132
5
+ SHA512:
6
+ metadata.gz: 82e4d30e8df2b2311146a37db0e13e6adac9f70383fb892f4342908401ef37582b4c4ff30defc1f56265f559ba7d7954540b2fc21b0646088b89a94b22aad952
7
+ data.tar.gz: 43d10651c6b326ff1ab898e0807c366926288b3b7fefed673ce04a006eeb7a1b4f1e0523158e4e09cb01f83708b8a844706945c69617c61e9ebfe39966e4adc6
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *~
2
+ .bundle
3
+ .pryrc
4
+ .rvmrc
5
+ .rbenv-version
6
+ .ruby-version
7
+ .yardoc
8
+ .DS_Store
9
+ Gemfile.lock
10
+ coverage
11
+ doc
12
+ log
13
+ tmp
14
+ vendor
15
+ *.swp
16
+ pkg/
17
+ .env
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ # 0.0.1 (2015/08/10)
2
+
3
+ first version
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Naotoshi Seo
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # ec2-host
2
+
3
+ Search hosts on AWS EC2
4
+
5
+ ## Installation
6
+
7
+ ```
8
+ gem install ec2-host
9
+ ```
10
+
11
+ ## Configuration
12
+
13
+ You can write a configuration file located at `/etc/sysconfig/ec2-host` (You can configure this path by `EC2_HOST_CONFIG_FILE` environment variable), or as environment variables:
14
+
15
+ AWS SDK (CLI) parameters:
16
+
17
+ * **AWS_REGION**; AWS SDK (CLI) region such as `ap-northeast-1`, `us-east-1`.
18
+ * **AWS_ACCESS_KEY_ID**: AWS SDK (CLI) crendentials. Default loads a credentials file
19
+ * **AWS_SECRET_ACCESS_KEY**: AWS SDK (CLI) credentials. Default load a credentials file
20
+ * **AWS_PROFILE**: The profile key of the AWS SDK (CLI) credentails file. Default is `default`
21
+ * **AWS_CREDENTIALS_FILE**: Path of the AWS SDK (CLI) credentails file. Default is `$HOME/.aws/credentials`. See [Configuring the AWS Command Line Interface](http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html#cli-config-files) for details.
22
+
23
+ ec2-host parameters:
24
+
25
+ * **HOSTNAME_TAG**: EC2 tag key used to express a hostname. The default is `Name`.
26
+ * **ROLES_TAG**: EC2 tag keys used to express roles. The default is `Roles`
27
+ * You can assign multiple roles seperated by `,` comma
28
+ * Also, you can express levels of roles delimited by `:`.
29
+ * Example: admin:ami, then `EC2::Host.new(role: 'admin:ami')` and also `EC2::Host.new(role1: 'admin')` returns this host
30
+ * **OPTIONAL_STRING_TAGS**: You may add optional non-array tags. You can specify multiple tags like `Service,Status`.
31
+ * **OPTIONAL_ARRAY_TAGS**: You may add optional array tags. Array tags allows multiple values delimited by `,` (comma) as `Roles` tag.
32
+ * **LOG_LEVEL**: Log level such as `info`, `debug`, `error`. The default is `info`.
33
+
34
+ See [sampel.conf](./sample.conf)
35
+
36
+ ## Tag Example
37
+
38
+ * **Name**: hostname
39
+ * **Roles**: app:web,app:db
40
+ * **Service**: sugoi
41
+ * **Status**: setup
42
+
43
+ ## CLI Usage
44
+
45
+ ```
46
+ $ ec2-host --role1 admin
47
+ host1
48
+ ip-XXX-XXX-XXX-XXX # if Name tag is not assigned
49
+ ```
50
+
51
+ See `ec2-host help get-hosts` for details:
52
+
53
+ ```
54
+ Usage:
55
+ ec2-host get-hosts
56
+
57
+ Options:
58
+ -h, [--hostname=one two three] # name or private_dns_name
59
+ --usage, [--role=one two three] # role
60
+ --r1, --usage1, --u1, [--role1=one two three] # role1, the 1st part of role delimited by :
61
+ --r2, --usage2, --u2, [--role2=one two three] # role2, the 2nd part of role delimited by :
62
+ --r3, --usage3, --u3, [--role3=one two three] # role3, the 3rd part of role delimited by :
63
+ -i, [--info], [--no-info] # show host info, not only hostname
64
+ [--debug], [--no-debug] # debug mode
65
+
66
+ Search EC2 hosts
67
+ ```
68
+
69
+ ## Library Usage
70
+
71
+ See http://sonots.github.io/ec2-host/doc/frames.html.
72
+
73
+ ## ChangeLog
74
+
75
+ See [CHANGELOG.md](CHANGELOG.md) for details.
76
+
77
+ ## For Developers
78
+
79
+ ### ToDo
80
+
81
+ * Support assume-roles
82
+ * Use mock/stub to run test (currently, directly accessing to EC2)
83
+ * Should cache a result of describe-instances in like 30 seconds?
84
+
85
+ ### How to Run test
86
+
87
+ See [spec/README.md](spec/README.md)
88
+
89
+ ### How to Release Gem
90
+
91
+ 1. Update gem.version in the gemspec
92
+ 2. Update CHANGELOG.md
93
+ 3. git commit && git push
94
+ 4. Run `bundle exec rake release`
95
+
96
+ ### How to Update doc
97
+
98
+ 1. Run `./doc.sh`
99
+ 2. git commit && git push (to gh-pages branch)
100
+
101
+ ### Licenses
102
+
103
+ See [LICENSE](LICENSE)
104
+
data/Rakefile ADDED
@@ -0,0 +1,30 @@
1
+ # -*- coding: utf-8; -*-
2
+ require "bundler/gem_tasks"
3
+
4
+ begin
5
+ require 'rspec/core/rake_task'
6
+ RSpec::Core::RakeTask.new :spec do |spec|
7
+ spec.pattern = FileList['spec/**/*_spec.rb']
8
+ end
9
+ task default: :spec
10
+ rescue LoadError, NameError
11
+ # OK, they can be absent on non-development mode.
12
+ end
13
+
14
+ desc "irb console"
15
+ task :console do
16
+ require_relative "lib/ec2-host"
17
+ require 'irb'
18
+ require 'irb/completion'
19
+ ARGV.clear
20
+ IRB.start
21
+ end
22
+ task :c => :console
23
+
24
+ desc "pry console"
25
+ task :pry do
26
+ require_relative "lib/ec2-host"
27
+ require 'pry'
28
+ ARGV.clear
29
+ Pry.start
30
+ end
data/bin/ec2-host ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/ec2/host/cli'
4
+ EC2::Host::CLI.start(ARGV)
data/doc.sh ADDED
@@ -0,0 +1,5 @@
1
+ #!/bin/sh
2
+ bundle exec yardoc
3
+ rsync -au --delete doc/ /tmp/doc
4
+ git checkout gh-pages
5
+ rsync -au --delete /tmp/doc/ doc
data/ec2-host.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ Gem::Specification.new do |gem|
2
+ gem.name = "ec2-host"
3
+ gem.version = '0.0.1'
4
+ gem.author = ['Naotoshi Seo']
5
+ gem.email = ['sonots@gmail.com']
6
+ gem.homepage = 'https://github.com/sonots/ec2-host'
7
+ gem.summary = "Search hosts on AWS EC2"
8
+ gem.description = "Search hosts on AWS EC2"
9
+ gem.licenses = ['MIT']
10
+
11
+ gem.files = `git ls-files`.split("\n")
12
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
13
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
14
+ gem.require_paths = ["lib"]
15
+
16
+ gem.add_runtime_dependency 'aws-sdk'
17
+ gem.add_runtime_dependency 'hashie'
18
+ gem.add_runtime_dependency 'thor'
19
+ gem.add_runtime_dependency 'dotenv'
20
+
21
+ gem.add_development_dependency 'yard'
22
+ gem.add_development_dependency 'rdoc'
23
+ gem.add_development_dependency 'rspec'
24
+ gem.add_development_dependency 'simplecov'
25
+ gem.add_development_dependency 'pry'
26
+ gem.add_development_dependency 'pry-nav'
27
+ gem.add_development_dependency 'rake'
28
+ gem.add_development_dependency 'bundler'
29
+ end
@@ -0,0 +1,15 @@
1
+ require 'pp'
2
+ require 'aws-sdk'
3
+ require 'dotenv'
4
+ Dotenv.load
5
+
6
+ # Aws.config.update({
7
+ # region: 'us-west-2',
8
+ # credentials: Aws::Credentials.new('akid', 'secret'),
9
+ # })
10
+
11
+ ec2 = Aws::EC2::Client.new
12
+ pp instances = ec2.describe_instances.reservations.map(&:instances)
13
+
14
+ require 'pry'
15
+ binding.pry
@@ -0,0 +1,6 @@
1
+ require 'ec2-host'
2
+ require 'pp'
3
+
4
+ EC2::Host.new(role1: 'admin').each do |host|
5
+ pp host
6
+ end
data/lib/ec2-host.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'bundler/setup'
2
+
3
+ require_relative 'ec2/host/string_util'
4
+ require_relative 'ec2/host/hash_util'
5
+ require_relative 'ec2/host/config'
6
+ require_relative 'ec2/host/host_data'
7
+ require_relative 'ec2/host/role_data'
8
+ require_relative 'ec2/host/client_util'
9
+ require_relative 'ec2/host'
data/lib/ec2/host.rb ADDED
@@ -0,0 +1,113 @@
1
+ class EC2
2
+ # Search EC2 hosts from tags
3
+ #
4
+ # require 'ec2-host'
5
+ # # Search by `Name` tag
6
+ # EC2::Host.new(hostname: 'test').first # => test
7
+ #
8
+ # # Search by `Roles` tag
9
+ # EC2::Host.new(
10
+ # role: 'admin:haikanko',
11
+ # ).each do |host|
12
+ # # ...
13
+ # end
14
+ #
15
+ # or
16
+ #
17
+ # EC2::Host.new(
18
+ # role1: 'admin',
19
+ # role2: 'haikanko',
20
+ # ).each do |host|
21
+ # # ...
22
+ # end
23
+ #
24
+ # # Or search
25
+ # EC2::Host.new(
26
+ # {
27
+ # role1: 'db',
28
+ # role2: 'master',
29
+ # },
30
+ # {
31
+ # role1: 'web',
32
+ # }
33
+ # ).each do |host|
34
+ # # ...
35
+ # end
36
+ #
37
+ # EC2::Host.me.hostname # => 'test'
38
+ class Host
39
+ include Enumerable
40
+
41
+ # @return [Host::Data] representing myself
42
+ def self.me
43
+ new(instance_id: ClientUtil.get_instance_id).each do |d|
44
+ return d
45
+ end
46
+ raise 'Not Found'
47
+ end
48
+
49
+ # Configure EC2::Host
50
+ #
51
+ # @params [Hash] params see EC2::Host::Config for configurable parameters
52
+ def self.configure(params = {})
53
+ Config.configure(params)
54
+ end
55
+
56
+ ARRAY_TAG_DELIMITER = ','
57
+ ROLE_TAG_DELIMITER = ':'
58
+
59
+ attr_reader :conditions, :options
60
+
61
+ # @param [Array of Hash, or Hash] conditions (and options)
62
+ #
63
+ # EC2::Host.new(
64
+ # hostname: 'test',
65
+ # options: {a: 'b'}
66
+ # )
67
+ #
68
+ # EC2::Host.new(
69
+ # {
70
+ # hostname: 'foo',
71
+ # },
72
+ # {
73
+ # hostname: 'bar',
74
+ # },
75
+ # options: {a: 'b'}
76
+ # )
77
+ def initialize(*conditions)
78
+ conditions = [{}] if conditions.empty?
79
+ conditions = [conditions] if conditions.kind_of?(Hash)
80
+ @options = {}
81
+ if conditions.size == 1
82
+ @options = conditions.first.delete(:options) || {}
83
+ else
84
+ index = conditions.find_index {|condition| condition.has_key?(:options) }
85
+ @options = conditions.delete_at(index)[:options] if index
86
+ end
87
+ raise ArgumentError, "Hash expected (options)" unless @options.is_a?(Hash)
88
+ @conditions = []
89
+ conditions.each do |condition|
90
+ @conditions << Hash[condition.map {|k, v| [k, Array(v).map(&:to_s)]}]
91
+ end
92
+ raise ArgumentError, "Array of Hash, or Hash expected (conditions)" unless @conditions.all? {|h| h.kind_of?(Hash)}
93
+ end
94
+
95
+ # @yieldparam [Host::Data] data entry
96
+ def each(&block)
97
+ @conditions.each do |condition|
98
+ search(ClientUtil.get_instances, condition, &block)
99
+ end
100
+ return self
101
+ end
102
+
103
+ private
104
+
105
+ def search(instances, condition)
106
+ instances.each do |i|
107
+ d = EC2::Host::HostData.initialize(i)
108
+ next unless d.match?(condition)
109
+ yield d
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,74 @@
1
+ require 'thor'
2
+ require 'ec2-host'
3
+
4
+ class EC2
5
+ class Host
6
+ class CLI < Thor
7
+ default_command :get_hosts
8
+
9
+ desc 'get-hosts', 'Search EC2 hosts (default)'
10
+ option :hostname,
11
+ :aliases => '-h',
12
+ :type => :array,
13
+ :desc => "name or private_dns_name"
14
+ option :role,
15
+ :aliases => %w[--usage],
16
+ :type => :array,
17
+ :desc => "role"
18
+ option :role1,
19
+ :aliases => %w[--r1 --usage1 --u1], # hmm, -r1 is not suppored by thor
20
+ :type => :array,
21
+ :desc => "role1, the 1st part of role delimited by #{ROLE_TAG_DELIMITER}"
22
+ option :role2,
23
+ :aliases => %w[--r2 --usage2 --u2],
24
+ :type => :array,
25
+ :desc => "role2, the 2nd part of role delimited by #{ROLE_TAG_DELIMITER}"
26
+ option :role3,
27
+ :aliases => %w[--r3 --usage3 --u3],
28
+ :type => :array,
29
+ :desc => "role3, the 3rd part of role delimited by #{ROLE_TAG_DELIMITER}"
30
+ Config.optional_options.each do |opt, tag|
31
+ option opt, :type => :array, :desc => opt
32
+ end
33
+ option :info,
34
+ :aliases => %w[-i],
35
+ :type => :boolean,
36
+ :desc => "show host info, not only hostname"
37
+ option :debug,
38
+ :type => :boolean,
39
+ :desc => "debug mode"
40
+ def get_hosts
41
+ if options[:info]
42
+ EC2::Host.new(condition).each do |host|
43
+ $stdout.puts host.info
44
+ end
45
+ else
46
+ EC2::Host.new(condition).each do |host|
47
+ $stdout.puts host.hostname
48
+ end
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def condition
55
+ return @condition if @condition
56
+ _condition = HashUtil.except(options, :info, :debug)
57
+ @condition = {}
58
+ _condition.each do |key, val|
59
+ if tag = Config.optional_options[key.to_s]
60
+ field = StringUtil.underscore(tag)
61
+ @condition[field.to_sym] = val
62
+ else
63
+ @condition[key.to_sym] = val
64
+ end
65
+ end
66
+ if options[:debug]
67
+ $stderr.puts(options: options)
68
+ $stderr.puts(condition: @condition)
69
+ end
70
+ @condition
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,27 @@
1
+ require 'net/http'
2
+ require 'aws-sdk'
3
+
4
+ class EC2
5
+ class Host
6
+ class ClientUtil
7
+ def self.get_instances
8
+ # I do not use describe_instances(filter:) because it does not support array tag ..
9
+ return @instances if @instances
10
+ Aws.config.update(region: Config.aws_region, credentials: Config.aws_credentials)
11
+ ec2 = Aws::EC2::Client.new
12
+ @instances = ec2.describe_instances.reservations.map(&:instances).flatten
13
+ end
14
+
15
+ def self.get_instance_id
16
+ return @instance_id if @instance_id
17
+ begin
18
+ http_conn = Net::HTTP.new('169.254.169.254')
19
+ http_conn.open_timeout = 5
20
+ @instance_id = http_conn.start {|http| http.get('/latest/meta-data/instance-id').body }
21
+ rescue Net::OpenTimeout
22
+ raise "HTTP connection to 169.254.169.254 is timeout. Probably, not an EC2 instance?"
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,111 @@
1
+ require 'dotenv'
2
+ Dotenv.load
3
+
4
+ class EC2
5
+ class Host
6
+ class Config
7
+ class << self
8
+ attr_writer :config_file,
9
+ :aws_region,
10
+ :aws_profile,
11
+ :aws_access_key_id,
12
+ :aws_secret_access_key,
13
+ :aws_credentials_file,
14
+ :log_level,
15
+ :hostname_tag,
16
+ :roles_tag,
17
+ :optional_array_tags,
18
+ :optional_string_tags
19
+ end
20
+
21
+ def self.configure(params)
22
+ params.each do |key, val|
23
+ send("#{key}=", val)
24
+ end
25
+ end
26
+
27
+ def self.config_file
28
+ @config_file ||= ENV.fetch('EC2_HOST_CONFIG_FILE', '/etc/sysconfig/ec2-host')
29
+ end
30
+
31
+ def self.aws_region
32
+ @aws_region ||= ENV['AWS_REGION'] || config.fetch('AWS_REGION')
33
+ end
34
+
35
+ def self.aws_profile
36
+ @aws_profile ||= ENV['AWS_PROFILE'] || config.fetch('AWS_PROFILE', 'default')
37
+ end
38
+
39
+ def self.aws_access_key_id
40
+ @aws_access_key_id ||= ENV['AWS_ACCESS_KEY_ID'] || config.fetch('AWS_ACCESS_KEY_ID', nil)
41
+ end
42
+
43
+ def self.aws_secret_access_key
44
+ @aws_secret_access_key ||= ENV['AWS_SECRET_ACCESS_KEY'] || config.fetch('AWS_SECRET_ACCESS_KEY', nil)
45
+ end
46
+
47
+ # this is not an official aws sdk environment variable
48
+ def self.aws_credentials_file
49
+ @aws_credentials_file ||= ENV['AWS_CREDENTIALS_FILE'] || config.fetch('AWS_CREDENTIALS_FILE', nil)
50
+ end
51
+
52
+ def self.log_level
53
+ @log_level ||= ENV['LOG_LEVEL'] || config.fetch('LOG_LEVEL', 'info')
54
+ end
55
+
56
+ def self.hostname_tag
57
+ @hostname_tag ||= ENV['HOSTNAME_TAG'] || config.fetch('HOSTNAME_TAG', 'Name')
58
+ end
59
+
60
+ def self.roles_tag
61
+ @roles_tag ||= ENV['ROLES_TAG'] || config.fetch('ROLES_TAG', 'Roles')
62
+ end
63
+
64
+ def self.optional_array_tags
65
+ @optional_array_tags ||= (ENV['OPTIONAL_ARRAY_TAGS'] || config.fetch('OPTIONAL_ARRAY_TAGS', '')).split(',')
66
+ end
67
+
68
+ def self.optional_string_tags
69
+ @optional_string_tags ||= (ENV['OPTIONAL_STRING_TAGS'] || config.fetch('OPTIONAL_STRING_TAGS', '')).split(',')
70
+ end
71
+
72
+ # private
73
+
74
+ def self.aws_credentials
75
+ @aws_credentials ||=
76
+ if aws_access_key_id and aws_secret_access_key
77
+ Aws::Credentials.new(aws_access_key_id, aws_secret_access_key)
78
+ else
79
+ Aws::SharedCredentials.new(profile_name: aws_profile, path: aws_credentials_file)
80
+ end
81
+ end
82
+
83
+ def self.optional_array_options
84
+ @optional_array_options ||= Hash[optional_array_tags.map {|tag|
85
+ [StringUtil.singularize(StringUtil.underscore(tag)), tag]
86
+ }]
87
+ end
88
+
89
+ def self.optional_string_options
90
+ @optional_string_options ||= Hash[optional_string_tags.map {|tag|
91
+ [StringUtil.underscore(tag), tag]
92
+ }]
93
+ end
94
+
95
+ def self.optional_options
96
+ @optional_options ||= optional_array_options.merge(optional_string_options)
97
+ end
98
+
99
+ def self.config
100
+ return @config if @config
101
+ @config = {}
102
+ File.readlines(config_file).each do |line|
103
+ next if line.start_with?('#')
104
+ key, val = line.chomp.split('=', 2)
105
+ @config[key] = val
106
+ end
107
+ @config
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,11 @@
1
+ class EC2
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,128 @@
1
+ require 'forwardable'
2
+ require 'hashie/mash'
3
+
4
+ class EC2
5
+ class Host
6
+ # Represents each host
7
+ class HostData < Hashie::Mash
8
+ # :hostname, # tag:Name or hostname part of private_dns_name
9
+ # :roles, # tag:Roles.split(',') such as web:app1,db:app1
10
+ # :region, # ENV['AWS_REGION'],
11
+ # :instance, # Aws::EC2::Types::Instance itself
12
+ #
13
+ # and OPTIONAL_ARRAY_TAGS, OPTIONAL_STRING_TAGS
14
+
15
+ extend Forwardable
16
+ def_delegators :instance,
17
+ :instance_id,
18
+ :private_ip_address,
19
+ :public_ip_address,
20
+ :launch_time,
21
+ :state,
22
+ :monitoring
23
+
24
+ alias_method :ip, :private_ip_address
25
+ alias_method :start_date, :launch_time
26
+ def usages; roles; end
27
+
28
+ def self.initialize(instance)
29
+ d = self.new
30
+ d.instance = instance
31
+ d.set_hostname
32
+ d.set_roles
33
+ d.set_region
34
+ d.set_string_tags
35
+ d.set_array_tags
36
+ d
37
+ end
38
+
39
+ # match with condition or not
40
+ #
41
+ # @param [Hash] condition search parameters
42
+ def match?(condition)
43
+ return false unless role_match?(condition)
44
+ condition = HashUtil.except(condition,
45
+ :role, :role1, :role2, :role3,
46
+ :usage, :usage1, :usage2, :usage3
47
+ )
48
+ condition.each do |key, values|
49
+ if self.send(key).is_a?(Array)
50
+ return false unless self.send(key).find {|v| values.include?(v) }
51
+ else
52
+ return false unless values.include?(self.send(key))
53
+ end
54
+ end
55
+ true
56
+ end
57
+
58
+ def inspect
59
+ sprintf "#<Aws::Host::HostData %s>", info
60
+ end
61
+
62
+ def info
63
+ if hostname and status and roles and tags and service
64
+ # special treatment for DeNA ;)
65
+ sprintf "%s:%s(%s)[%s]{%s}", \
66
+ hostname, status, roles.join(' '), tags.join(' '), service
67
+ else
68
+ HashUtil.except(self, :instance).to_s
69
+ end
70
+ end
71
+
72
+ # private
73
+
74
+ def role_match?(condition)
75
+ # usage is an alias of role
76
+ if role = (condition[:role] || condition[:usage])
77
+ role1, role2, role3 = role.first.split(':')
78
+ else
79
+ role1 = (condition[:role1] || condition[:usage1] || []).first
80
+ role2 = (condition[:role2] || condition[:usage2] || []).first
81
+ role3 = (condition[:role3] || condition[:usage3] || []).first
82
+ end
83
+ if role1
84
+ return false unless self.roles.find {|role| role.match?(role1, role2, role3) }
85
+ end
86
+ true
87
+ end
88
+
89
+ def set_hostname
90
+ self.hostname = find_string_tag(Config.hostname_tag)
91
+ self.hostname = instance.private_dns_name.split('.').first if self.hostname.empty?
92
+ end
93
+
94
+ def set_roles
95
+ roles = find_array_tag(Config.roles_tag)
96
+ self.roles = roles.map {|role| EC2::Host::RoleData.initialize(role) }
97
+ end
98
+
99
+ def set_region
100
+ self.region = Config.aws_region
101
+ end
102
+
103
+ def set_string_tags
104
+ Config.optional_string_tags.each do |tag|
105
+ field = StringUtil.underscore(tag)
106
+ self[field] = find_string_tag(tag)
107
+ end
108
+ end
109
+
110
+ def set_array_tags
111
+ Config.optional_array_tags.each do |tag|
112
+ field = StringUtil.underscore(tag)
113
+ self[field] = find_array_tag(tag)
114
+ end
115
+ end
116
+
117
+ def find_string_tag(key)
118
+ v = instance.tags.find {|tag| tag.key == key }
119
+ v ? v.value : ''
120
+ end
121
+
122
+ def find_array_tag(key)
123
+ v = instance.tags.find {|tag| tag.key == key }
124
+ v ? v.value.split(ARRAY_TAG_DELIMITER) : []
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,60 @@
1
+ class EC2
2
+ class Host
3
+ RoleData = Struct.new(
4
+ :role1, :role2, :role3
5
+ )
6
+
7
+ # Represents each role
8
+ class RoleData
9
+ def self.initialize(role)
10
+ role1, role2, role3 = role.split(ROLE_TAG_DELIMITER)
11
+ self.new(role1, role2, role3)
12
+ end
13
+
14
+ # @return [String] something like "admin:jenkins:slave"
15
+ def role
16
+ @role ||= [role1, role2, role3].compact.reject(&:empty?).join(ROLE_TAG_DELIMITER)
17
+ end
18
+ alias :to_s :role
19
+
20
+ # @return [Array] something like ["admin", "admin:jenkins", "admin:jenkins:slave"]
21
+ def uppers
22
+ uppers = [RoleData.new(role1)]
23
+ uppers << RoleData.new(role1, role2) if role2 and !role2.empty?
24
+ uppers << RoleData.new(role1, role2, role3) if role3 and !role3.empty?
25
+ uppers
26
+ end
27
+
28
+ def match?(role1, role2 = nil, role3 = nil)
29
+ if role3
30
+ role1 == self.role1 and role2 == self.role2 and role3 == self.role3
31
+ elsif role2
32
+ role1 == self.role1 and role2 == self.role2
33
+ else
34
+ role1 == self.role1
35
+ end
36
+ end
37
+
38
+ # Equality
39
+ #
40
+ # Role::Data.new('admin') == Role::Data.new('admin') #=> true
41
+ # Role::Data.new('admin', 'jenkin') == "admin:jenkins" #=> true
42
+ #
43
+ # @param [Object] other
44
+ def ==(other)
45
+ case other
46
+ when String
47
+ self.role == other
48
+ when EC2::Host::RoleData
49
+ super(other)
50
+ else
51
+ false
52
+ end
53
+ end
54
+
55
+ def inspect
56
+ "\"#{to_s}\""
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,31 @@
1
+ class EC2
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
data/sample.conf ADDED
@@ -0,0 +1,8 @@
1
+ AWS_ACCESS_KEY_ID=xxxxxxx
2
+ AWS_SECRET_ACCESS_KEY=xxxxxx
3
+ AWS_REGION=ap-northeast-1
4
+ HOSTNAME_TAG=Name
5
+ ROLES_TAG=Roles
6
+ OPTIONAL_ARRAY_TAGS=Tags
7
+ OPTIONAL_STRING_TAGS=Service,Status
8
+ LOG_LEVEL=info
data/spec/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # How to run test
2
+
3
+ NOTE: Currently, mock is not supported yet. So, you have to create your own AWS account, and instances.
4
+
5
+ Configure .env file as
6
+
7
+ ```
8
+ AWS_ACCESS_KEY_ID=
9
+ AWS_SECRET_ACCESS_KEY=
10
+ AWS_REGION=
11
+ EC2_HOST_CONFIG_FILE=.env
12
+ OPTIONAL_ARRAY_TAGS=Tags
13
+ OPTIONAL_STRING_TAGS=Service,Status
14
+ ```
15
+
16
+ EC2 instance tags must be configured as followings:
17
+
18
+ ```
19
+ [
20
+ {
21
+ Name: test
22
+ Roles: admin:admin,test
23
+ Service: test
24
+ Status: reserve
25
+ Tags: standby
26
+ }
27
+ {
28
+ Name: isucon4
29
+ Roles: isucon4:qual
30
+ Service: isucon4
31
+ Status: active
32
+ Tags: master
33
+ }
34
+ ]
35
+ ```
data/spec/host_spec.rb ADDED
@@ -0,0 +1,163 @@
1
+ require 'spec_helper'
2
+
3
+ shared_examples_for 'host' do
4
+ it 'should have parameters' do
5
+ [ :hostname,
6
+ :roles,
7
+ :status,
8
+ :tags,
9
+ :service,
10
+ :region,
11
+ :instance,
12
+ :instance_id,
13
+ :private_ip_address,
14
+ :public_ip_address,
15
+ :launch_time,
16
+ :state,
17
+ :monitoring,
18
+ :ip,
19
+ :start_date,
20
+ :usages,
21
+ ].each do |k|
22
+ expect{ subject.__send__(k) }.not_to raise_error
23
+ end
24
+ end
25
+ end
26
+
27
+ describe EC2::Host do
28
+ describe 'options' do
29
+ context do
30
+ let(:subject) { EC2::Host.new(hostname: 'test', options: {foo:'bar'}) }
31
+ it { expect(subject.options).to eq({foo:'bar'}) }
32
+ it { expect(subject.conditions).to eq([{hostname: ['test']}]) }
33
+ end
34
+
35
+ context do
36
+ let(:subject) { EC2::Host.new({hostname: 'foo'}, {hostname: 'bar'}, options: {foo:'bar'}) }
37
+ it { expect(subject.options).to eq({foo:'bar'}) }
38
+ it { expect(subject.conditions).to eq([{hostname: ['foo']}, {hostname: ['bar']}]) }
39
+ end
40
+ end
41
+
42
+ context 'by hostname' do
43
+ let(:hosts) { EC2::Host.new(hostname: 'test').to_a }
44
+ let(:subject) { hosts.first }
45
+ it_should_behave_like 'host'
46
+ it { expect(hosts.size).to eq(1) }
47
+ it { expect(subject.hostname).to eq('test') }
48
+ end
49
+
50
+ context 'by role' do
51
+ context 'by a role' do
52
+ let(:subject) { EC2::Host.new(role1: 'admin').first }
53
+ it_should_behave_like 'host'
54
+ end
55
+
56
+ context 'by multiple roles (or)' do
57
+ let(:hosts) {
58
+ EC2::Host.new(
59
+ {
60
+ role1: 'admin',
61
+ role2: 'ami',
62
+ },
63
+ {
64
+ role1: 'isucon4',
65
+ }
66
+ ).to_a
67
+ }
68
+ let(:subject) { hosts.first }
69
+ it { expect(hosts.size).to be >= 2 }
70
+ it_should_behave_like 'host'
71
+ end
72
+ end
73
+
74
+ # This is for DeNA use
75
+ context 'by usage (an alias of usage)' do
76
+ context 'by a usage' do
77
+ let(:subject) { EC2::Host.new(usage1: 'admin').first }
78
+ it_should_behave_like 'host'
79
+ end
80
+
81
+ context 'by multiple usages (or)' do
82
+ let(:hosts) {
83
+ EC2::Host.new(
84
+ {
85
+ usage1: 'admin',
86
+ usage2: 'ami',
87
+ },
88
+ {
89
+ usage1: 'isucon4',
90
+ }
91
+ ).to_a
92
+ }
93
+ let(:subject) { hosts.first }
94
+ it { expect(hosts.size).to be >= 2 }
95
+ it_should_behave_like 'host'
96
+ end
97
+ end
98
+
99
+ context 'by status' do
100
+ context 'by a status' do
101
+ let(:subject) { EC2::Host.new(status: :active).first }
102
+ it_should_behave_like 'host'
103
+ end
104
+
105
+ context 'by multiple status (or)' do
106
+ let(:hosts) { EC2::Host.new(status: [:reserve, :active]).to_a }
107
+ let(:subject) { hosts.first }
108
+ it_should_behave_like 'host'
109
+ it { expect(hosts.size).to be >= 2 }
110
+ end
111
+
112
+ context 'by a string status' do
113
+ let(:subject) { EC2::Host.new(status: 'active').first }
114
+ it_should_behave_like 'host'
115
+ end
116
+
117
+ context 'by multiple string status (or)' do
118
+ let(:hosts) { EC2::Host.new(status: ['reserve', 'active']).to_a }
119
+ let(:subject) { hosts.first }
120
+ it_should_behave_like 'host'
121
+ it { expect(hosts.size).to be >= 2 }
122
+ end
123
+ end
124
+
125
+ context 'by service' do
126
+ context 'by a service' do
127
+ let(:subject) { EC2::Host.new(service: 'isucon4').first }
128
+ it_should_behave_like 'host'
129
+ end
130
+
131
+ context 'by multiple services (or)' do
132
+ let(:hosts) { EC2::Host.new(service: ['test', 'isucon4']) }
133
+ let(:subject) { hosts.first }
134
+ it_should_behave_like 'host'
135
+ end
136
+ end
137
+
138
+ context 'by region' do
139
+ context 'by a region' do
140
+ let(:subject) { EC2::Host.new(region: 'ap-northeast-1').first }
141
+ it_should_behave_like 'host'
142
+ end
143
+
144
+ context 'by multiple regions (or)' do
145
+ let(:hosts) { EC2::Host.new(region: ['ap-northeast-1']) }
146
+ let(:subject) { hosts.first }
147
+ it_should_behave_like 'host'
148
+ end
149
+ end
150
+
151
+ context 'by tags' do
152
+ context 'by a tag' do
153
+ let(:subject) { EC2::Host.new(tags: 'master').first }
154
+ it_should_behave_like 'host'
155
+ end
156
+
157
+ context 'by multiple tags (or)' do
158
+ let(:hosts) { EC2::Host.new(tags: ['standby', 'master']) }
159
+ let(:subject) { hosts.first }
160
+ it_should_behave_like 'host'
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,22 @@
1
+ ENV['RACK_ENV'] = 'test'
2
+ Bundler.require :test
3
+
4
+ require 'simplecov'
5
+ SimpleCov.start do
6
+ add_filter 'vendor/'
7
+ add_filter 'spec/'
8
+ add_group 'libs', 'lib'
9
+ end
10
+
11
+ Bundler.require :default # <- need this *after* simplecov
12
+ require 'pry'
13
+ require 'ec2-host'
14
+ require 'dotenv'
15
+ Dotenv.load
16
+
17
+ # Requires supporting files with custom matchers and macros, etc,
18
+ # in ./support/ and its subdirectories.
19
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
20
+
21
+ RSpec.configure do |config|
22
+ end
metadata ADDED
@@ -0,0 +1,241 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ec2-host
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Naotoshi Seo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-08-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: hashie
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: thor
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: dotenv
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: yard
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rdoc
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: simplecov
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: pry
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: pry-nav
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rake
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: bundler
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ description: Search hosts on AWS EC2
182
+ email:
183
+ - sonots@gmail.com
184
+ executables:
185
+ - ec2-host
186
+ extensions: []
187
+ extra_rdoc_files: []
188
+ files:
189
+ - ".gitignore"
190
+ - CHANGELOG.md
191
+ - Gemfile
192
+ - LICENSE
193
+ - README.md
194
+ - Rakefile
195
+ - bin/ec2-host
196
+ - doc.sh
197
+ - ec2-host.gemspec
198
+ - example/aws-sdk.rb
199
+ - example/example.rb
200
+ - lib/ec2-host.rb
201
+ - lib/ec2/host.rb
202
+ - lib/ec2/host/cli.rb
203
+ - lib/ec2/host/client_util.rb
204
+ - lib/ec2/host/config.rb
205
+ - lib/ec2/host/hash_util.rb
206
+ - lib/ec2/host/host_data.rb
207
+ - lib/ec2/host/role_data.rb
208
+ - lib/ec2/host/string_util.rb
209
+ - sample.conf
210
+ - spec/README.md
211
+ - spec/host_spec.rb
212
+ - spec/spec_helper.rb
213
+ homepage: https://github.com/sonots/ec2-host
214
+ licenses:
215
+ - MIT
216
+ metadata: {}
217
+ post_install_message:
218
+ rdoc_options: []
219
+ require_paths:
220
+ - lib
221
+ required_ruby_version: !ruby/object:Gem::Requirement
222
+ requirements:
223
+ - - ">="
224
+ - !ruby/object:Gem::Version
225
+ version: '0'
226
+ required_rubygems_version: !ruby/object:Gem::Requirement
227
+ requirements:
228
+ - - ">="
229
+ - !ruby/object:Gem::Version
230
+ version: '0'
231
+ requirements: []
232
+ rubyforge_project:
233
+ rubygems_version: 2.2.2
234
+ signing_key:
235
+ specification_version: 4
236
+ summary: Search hosts on AWS EC2
237
+ test_files:
238
+ - spec/README.md
239
+ - spec/host_spec.rb
240
+ - spec/spec_helper.rb
241
+ has_rdoc: