ec2_hosts 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []