ec2_hosts 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f3d3a81641bffc23b95c18e6979f3847387c9f65
4
+ data.tar.gz: 4798f749b1729aae57c0fc72bba3a0a67a4ffa27
5
+ SHA512:
6
+ metadata.gz: 0628070a21cf3c3cb8d4cf0429b77e68077d822cfa97978201b650d9a92abd316cc8f33056b398e4e86a980ed774ccf9b5de08ce3c72b9dba597c46c945062ea
7
+ data.tar.gz: c8a91a6e75de1ab71968ec13c62933471ec902cb1c6d2723681e0d66d118fe7b420c66f69ea9b2728c97f02e82159b237379c1194832a95bb6551967e3526448
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.3
4
+ before_install: gem install bundler -v 1.11.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ec2_hosts.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # ec2_hosts
2
+
3
+ Update your hosts file based on ec2 compute instances.
4
+
5
+ This is handy when used in conjunction with something like [sshuttle](https://github.com/sshuttle/sshuttle),
6
+ allowing you to have a "poor man's vpn".
7
+
8
+ ## Installation
9
+
10
+ ```shell
11
+ $ gem install ec2_hosts
12
+ ```
13
+
14
+ ## Requirements
15
+
16
+ ## Usage
17
+
18
+ ## Example
19
+
20
+ Update your hosts file using ec2_hosts:
21
+
22
+ ```shell
23
+ $ sudo ec2_hosts -p my-cool-project --public bastion
24
+
25
+ ```
26
+ Start sshuttle session:
27
+
28
+ ```shell
29
+ $ sshuttle --remote=bastion01 --daemon --pidfile=/tmp/sshuttle.pid 192.168.1.0/24
30
+ ```
31
+
32
+ Now your hosts file will contain entries for all compute instances in the project,
33
+ and you can ssh directly to them from your local machine.
34
+
35
+ Hosts matching the pattern passed in with the `--public` flag will have their public
36
+ IP address added to your host file instead of the their private internal IP address.
37
+
38
+ ## Contributing
39
+
40
+ Bug reports and pull requests are welcome on GitHub at [https://github.com/atongen/ec2_hosts](https://github.com/atongen/ec2_hosts).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/ec2_hosts ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path('../../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
5
+ require 'ec2_hosts'
6
+ Ec2Hosts::Runner.new(ARGV.dup).run!
data/ec2_hosts.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ec2_hosts/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ec2_hosts"
8
+ spec.version = Ec2Hosts::VERSION
9
+ spec.authors = ["Andrew Tongen"]
10
+ spec.email = ["atongen@gmail.com"]
11
+
12
+ spec.summary = %q{Update your hosts file based on aws ec2 compute instances}
13
+ spec.description = %q{Update your hosts file based on aws ec2 compute instances}
14
+ spec.homepage = "https://github.com/atongen/ec2_hosts"
15
+
16
+ if spec.respond_to?(:metadata)
17
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
18
+ else
19
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
20
+ end
21
+
22
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
23
+ spec.bindir = "bin"
24
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
25
+ spec.require_paths = ["lib"]
26
+
27
+ spec.add_dependency "aws-sdk-v1", "~> 1.60"
28
+
29
+ spec.add_development_dependency "bundler", "~> 1.11"
30
+ spec.add_development_dependency "rake", "~> 10.0"
31
+ spec.add_development_dependency "rspec", "~> 3.0"
32
+ end
@@ -0,0 +1,132 @@
1
+ module Ec2Hosts
2
+ class Hosts
3
+
4
+ attr_reader :ec2,
5
+ :options
6
+
7
+ def initialize(options = {})
8
+ # Creds must be present in environment
9
+ @ec2 = AWS::EC2.new
10
+ @options = options
11
+ if @options[:tags]
12
+ @processed_tags = process_tags(@options[:tags])
13
+ elsif @options[:template]
14
+ @processed_template = process_template(@options[:template])
15
+ end
16
+ end
17
+
18
+ def vpc
19
+ return @vpc if instance_variable_defined?(:@vpc)
20
+
21
+ vpcs = ec2.vpcs.filter("tag:Name", options[:vpc]).map {|v|v}
22
+
23
+ if vpcs.length > 1
24
+ raise ArgumentError.new("Multiple VPCs with name '#{options[:vpc]}' found.")
25
+ elsif vpcs.length == 0
26
+ raise ArgumentError.new("VPC '#{options[:vpc]}' not found.")
27
+ end
28
+
29
+ @vpc = vpcs.first
30
+ end
31
+
32
+ def instances
33
+ return @instances if instance_variable_defined?(:@instances)
34
+
35
+ if options[:only_running]
36
+ @instances = vpc.instances.filter("instance-state-name", "running")
37
+ else
38
+ @instances = vpc.instances
39
+ end
40
+ end
41
+
42
+ def to_a
43
+ raw = instances.inject({}) do |memo, inst|
44
+ hostname = ""
45
+ if @processed_tags
46
+ hostname = parse_tags_hostname(@processed_tags, inst.tags.map.to_a.to_h)
47
+ elsif @processed_template
48
+ hostname = parse_template_hostname(@processed_template, inst.tags.map.to_a.to_h)
49
+ else
50
+ hostname = inst.tags["Name"]
51
+ end
52
+
53
+ hostname = "" unless hostname_valid?(hostname)
54
+
55
+ if hostname == "" && !options[:ignore_missing]
56
+ hostname = inst.private_dns_name.split('.').first
57
+ end
58
+
59
+ if hostname != ""
60
+ memo[hostname] = inst
61
+ end
62
+
63
+ memo
64
+ end.sort.to_h
65
+
66
+ list = []
67
+
68
+ raw.each do |hostname, inst|
69
+ begin
70
+ if !options[:public].nil? && hostname.downcase.include?(options[:public])
71
+ if !options[:exclude_public]
72
+ # get public ip address
73
+ if ip = inst.public_ip_address
74
+ list << "#{ip} #{hostname}"
75
+ raise HostError.new
76
+ end
77
+ end
78
+ else
79
+ # get private ip address
80
+ if ip = inst.private_ip_address
81
+ list << "#{ip} #{hostname}"
82
+ raise HostError.new
83
+ end
84
+ end
85
+ rescue HostError; end
86
+ end
87
+
88
+ list
89
+ end
90
+
91
+ private
92
+
93
+ def process_template(template)
94
+ template.split(/({\w+})/).select { |s| s != "" }.map do |snip|
95
+ if m = snip.match(/\A{(\w+)}\Z/)
96
+ ->(tags) { tags[m[1]] }
97
+ else
98
+ snip
99
+ end
100
+ end
101
+ end
102
+
103
+ def parse_template_hostname(template, instance_tags)
104
+ result = template.map do |snip|
105
+ if snip.is_a?(Proc)
106
+ snip.call(instance_tags)
107
+ else
108
+ snip
109
+ end
110
+ end
111
+ if result.any? { |r| r.nil? }
112
+ ""
113
+ else
114
+ result.join.gsub('_', '-')
115
+ end
116
+ end
117
+
118
+ def process_tags(tags)
119
+ tags.split(",").select { |s| s != "" }
120
+ end
121
+
122
+ def parse_tags_hostname(tags, instance_tags)
123
+ tags.map do |tag|
124
+ instance_tags[tag]
125
+ end.compact.join("-").gsub('_', '-')
126
+ end
127
+
128
+ def hostname_valid?(hostname)
129
+ !!hostname.to_s.match(/\A[A-Za-z]+[0-9A-Za-z-]*\Z/)
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,84 @@
1
+ require 'optparse'
2
+
3
+ module Ec2Hosts
4
+ class Options
5
+
6
+ attr_reader :options
7
+
8
+ def initialize(args)
9
+ @options = {
10
+ vpc: nil,
11
+ tags: nil,
12
+ template: nil,
13
+ ignore_missing: true,
14
+ only_running: true,
15
+ public: nil,
16
+ exclude_public: false,
17
+ file: '/etc/hosts',
18
+ backup: nil,
19
+ dry_run: false,
20
+ delete: false,
21
+ clear: false
22
+ }
23
+ parser.parse!(args)
24
+
25
+ if @options[:backup].nil?
26
+ @options[:backup] = "#{@options[:file]}.bak"
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def parser
33
+ @parser ||= begin
34
+ OptionParser.new do |opts|
35
+ opts.banner = "Usage: $ ec2_hosts [options]"
36
+ opts.on('-v', '--vpc VPC_NAME', "Name of VPC to use. Defaults to nil.") do |opt|
37
+ @options[:vpc] = opt
38
+ end
39
+ opts.on('--tags TAGS', "CSV of tag names used to build host name. Defaults to nil.") do |opt|
40
+ @options[:tags] = opt
41
+ end
42
+ opts.on('--template TEMPLATE', "Template string to build hostname from instance tags. Defaults to nil.") do |opt|
43
+ @options[:template] = opt
44
+ end
45
+ opts.on('-i', '--[no-]ignore-missing', "Ignore hosts with no matching tags, or invalid template. Defaults to true.") do |opt|
46
+ @options[:ignore_missing] = opt
47
+ end
48
+ opts.on('-r', '--[no-]only-running', "Only list running instances. Defaults to true.") do |opt|
49
+ @options[:only_running] = opt
50
+ end
51
+ opts.on('-p', '--public PUBLIC', "Pattern to match for public/bastion hosts. Use public IP for these. Defaults to nil") do |opt|
52
+ @options[:public] = opt
53
+ end
54
+ opts.on('--[no-]exclude-public', "Exclude public hosts from list when updating hosts file. Allows them to be managed manually. Defaults to false") do |opt|
55
+ @options[:exclude_public] = opt
56
+ end
57
+ opts.on('-f', '--file FILE', "Hosts file to update. Defaults to /etc/hosts") do |opt|
58
+ @options[:file] = opt
59
+ end
60
+ opts.on('-b', '--backup BACKUP', "Path to backup original hosts file to. Defaults to FILE with '.bak' extension appended.") do |opt|
61
+ @options[:file] = opt
62
+ end
63
+ opts.on('--[no-]dry-run', "Dry run, do not modify hosts file. Defaults to false") do |opt|
64
+ @options[:dry_run] = opt
65
+ end
66
+ opts.on('--[no-]delete', "Delete the VPC from hosts file. Defaults to false") do |opt|
67
+ @options[:delete] = opt
68
+ end
69
+ opts.on('--[no-]clear', "Clear all ec2 host entries from hosts file. Defaults to false") do |opt|
70
+ @options[:clear] = opt
71
+ end
72
+ opts.on_tail("--help", "Show this message") do
73
+ puts opts
74
+ exit
75
+ end
76
+ opts.on_tail("--version", "Show version") do
77
+ puts ::Ec2Hosts::VERSION
78
+ exit
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,37 @@
1
+ module Ec2Hosts
2
+ class Runner
3
+
4
+ def initialize(args)
5
+ @options = Options.new(args).options
6
+ end
7
+
8
+ def run!
9
+ if @options[:vpc] && @options[:clear]
10
+ raise ArgumentError.new("Cannot specify 'clear' and 'vpc' at the same time.")
11
+ end
12
+
13
+ if @options[:tags] && @options[:template]
14
+ raise ArgumentError.new("Cannot specify 'tags' and 'template' at the same time.")
15
+ end
16
+
17
+ updater = Updater.new(@options)
18
+
19
+ if @options[:clear]
20
+ updater.clear
21
+ else
22
+ if @options[:vpc].nil?
23
+ raise ArgumentError.new("No ec2 vpc specified.")
24
+ end
25
+
26
+ if @options[:delete]
27
+ new_hosts_list = []
28
+ else
29
+ hosts = Hosts.new(@options)
30
+ new_hosts_list = hosts.to_a
31
+ end
32
+
33
+ updater.update(new_hosts_list)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,176 @@
1
+ module Ec2Hosts
2
+ # Updater implements a simple state machine which is used to update
3
+ # content between zero or more blocks of content which start and end
4
+ # with pre-defined "marker" lines.
5
+ class Updater
6
+
7
+ module Marker
8
+ BEFORE = 0
9
+ INSIDE = 1
10
+ AFTER = 2
11
+ end
12
+
13
+ attr_reader :options
14
+
15
+ def initialize(options = {})
16
+ @options = options
17
+ end
18
+
19
+ def update(new_hosts)
20
+ old_hosts = File.read(options[:file])
21
+
22
+ if old_hosts.include?(start_marker) && old_hosts.include?(end_marker)
23
+ # valid markers exists
24
+ if options[:delete]
25
+ new_content = delete_vpc_hosts(old_hosts)
26
+ else
27
+ new_content = gen_new_hosts(old_hosts, new_hosts)
28
+ end
29
+
30
+ # remove zero or more white space characters at end of file with
31
+ # a single new-line
32
+ new_content.gsub!(/\s+$/, "\n")
33
+
34
+ if options[:dry_run]
35
+ puts new_content
36
+ elsif new_content != old_hosts
37
+ # backup old host file
38
+ File.open(options[:backup], 'w') { |f| f << old_hosts }
39
+ # write new content
40
+ File.open(options[:file], 'w') { |f| f << new_content }
41
+ end
42
+ elsif old_hosts.include?(start_marker) || old_hosts.include?(end_marker)
43
+ raise UpdaterError.new("Invalid marker present in existing hosts content")
44
+ else
45
+ # marker doesn't exist
46
+ if options[:delete]
47
+ new_content = old_hosts
48
+ else
49
+ new_content = [old_hosts, start_marker, new_hosts, end_marker].join("\n")
50
+ end
51
+ # remove one or more white space characters at end of file with
52
+ # a single new-line
53
+ new_content.gsub!(/\s+$/, "\n")
54
+
55
+ if options[:dry_run]
56
+ puts new_content
57
+ elsif new_content != old_hosts
58
+ # backup old host file
59
+ File.open(options[:backup], 'w') { |f| f << old_hosts }
60
+ # write new content
61
+ File.open(options[:file], 'w') { |f| f << new_content }
62
+ end
63
+ end
64
+
65
+ true
66
+ end
67
+
68
+ def clear
69
+ old_hosts = File.read(options[:file])
70
+ new_content = old_hosts.dup
71
+
72
+ markers = old_hosts.each_line.map do |line|
73
+ regex = "\# (START|END) EC2 HOSTS - (.+) \#"
74
+ if m = line.match(/^#{regex}$/)
75
+ m[2]
76
+ end
77
+ end.compact.uniq
78
+
79
+ markers.each do |project|
80
+ new_content = delete_vpc_hosts(new_content)
81
+ end
82
+ new_content.gsub!(/\s+$/, "\n")
83
+
84
+ if options[:dry_run]
85
+ puts new_content
86
+ elsif new_content != old_hosts
87
+ # backup old host file
88
+ File.open(options[:file], 'w') { |f| f << old_hosts }
89
+ # write new content
90
+ File.open(options[:file], 'w') { |f| f << new_content }
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ def gen_new_hosts(hosts, new_hosts)
97
+ new_content = ''
98
+ marker_state = Marker::BEFORE
99
+ hosts.split("\n").each do |line|
100
+ if line == start_marker
101
+ if marker_state == Marker::BEFORE
102
+ # transition to inside the marker
103
+ new_content << start_marker + "\n"
104
+ marker_state = Marker::INSIDE
105
+ # add new host content
106
+ new_hosts.split("\n").each do |host|
107
+ new_content << host + "\n"
108
+ end
109
+ else
110
+ raise UpdaterError.new("Invalid marker state")
111
+ end
112
+ elsif line == end_marker
113
+ if marker_state == Marker::INSIDE
114
+ # transition to after the marker
115
+ new_content << end_marker + "\n"
116
+ marker_state = Marker::AFTER
117
+ else
118
+ raise UpdaterError.new("Invalid marker state")
119
+ end
120
+ else
121
+ case marker_state
122
+ when Marker::BEFORE, Marker::AFTER
123
+ new_content << line + "\n"
124
+ when Marker::INSIDE
125
+ # skip everything between old markers
126
+ next
127
+ end
128
+ end
129
+ end
130
+ new_content
131
+ end
132
+
133
+ def delete_vpc_hosts(hosts)
134
+ new_content = ''
135
+ marker_state = Marker::BEFORE
136
+ hosts.split("\n").each do |line|
137
+ if line == start_marker
138
+ if marker_state == Marker::BEFORE
139
+ marker_state = Marker::INSIDE
140
+ # don't add any content, we're deleting this block
141
+ else
142
+ raise UpdaterError.new("Invalid marker state")
143
+ end
144
+ elsif line == end_marker
145
+ if marker_state == Marker::INSIDE
146
+ marker_state = Marker::AFTER
147
+ else
148
+ raise UpdaterError.new("Invalid marker state")
149
+ end
150
+ else
151
+ case marker_state
152
+ when Marker::BEFORE, Marker::AFTER
153
+ new_content << line + "\n"
154
+ when Marker::INSIDE
155
+ # skip everything between old markers
156
+ next
157
+ end
158
+ end
159
+ end
160
+ new_content
161
+ end
162
+
163
+ def start_marker
164
+ @start_marker ||= begin
165
+ "# START EC2 HOSTS - #{options[:vpc]} #"
166
+ end
167
+ end
168
+
169
+ def end_marker
170
+ @end_marker ||= begin
171
+ "# END EC2 HOSTS - #{options[:vpc]} #"
172
+ end
173
+ end
174
+
175
+ end
176
+ end
@@ -0,0 +1,3 @@
1
+ module Ec2Hosts
2
+ VERSION = "0.1.0"
3
+ end
data/lib/ec2_hosts.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'aws-sdk-v1'
2
+
3
+ require 'ec2_hosts/version'
4
+ require 'ec2_hosts/options'
5
+ require 'ec2_hosts/hosts'
6
+ require 'ec2_hosts/updater'
7
+ require 'ec2_hosts/runner'
8
+
9
+ module Ec2Hosts
10
+ class HostError < StandardError; end
11
+ class UpdaterError < StandardError; end
12
+ end
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ec2_hosts
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Andrew Tongen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-01-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk-v1
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.60'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.60'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.11'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.11'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ description: Update your hosts file based on aws ec2 compute instances
70
+ email:
71
+ - atongen@gmail.com
72
+ executables:
73
+ - ec2_hosts
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".gitignore"
78
+ - ".rspec"
79
+ - ".travis.yml"
80
+ - Gemfile
81
+ - README.md
82
+ - Rakefile
83
+ - bin/ec2_hosts
84
+ - ec2_hosts.gemspec
85
+ - lib/ec2_hosts.rb
86
+ - lib/ec2_hosts/hosts.rb
87
+ - lib/ec2_hosts/options.rb
88
+ - lib/ec2_hosts/runner.rb
89
+ - lib/ec2_hosts/updater.rb
90
+ - lib/ec2_hosts/version.rb
91
+ homepage: https://github.com/atongen/ec2_hosts
92
+ licenses: []
93
+ metadata:
94
+ allowed_push_host: https://rubygems.org
95
+ post_install_message:
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubyforge_project:
111
+ rubygems_version: 2.5.1
112
+ signing_key:
113
+ specification_version: 4
114
+ summary: Update your hosts file based on aws ec2 compute instances
115
+ test_files: []