gcloud_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: 5ba67d06a8c97f3882fac1ff37cc025d3d64a7f1
4
+ data.tar.gz: 7520d469eec780d23438d6985d92e2cf87c75678
5
+ SHA512:
6
+ metadata.gz: 5a2b239695545db654fe3ad1b01e11e84f2a17da16753f79c9dfea8fee940cfe9b2bf3b1e5c0daf40442df1c2a0817c9783cfb9524902121be993d5aaab24677
7
+ data.tar.gz: ca94497f4fc039bb66504cecbee41223ab61e11952b775f0949d1354dcedb6f1e5704ca1bf3e15a4fe9b20ff3451edf23678ee2a11ded8fae6ac587d07bda0e5
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 gcloud_hosts.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # gcloud_hosts
2
+
3
+ Update your hosts file based on gcloud 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 gcloud_hosts
12
+ ```
13
+
14
+ ## Requirements
15
+
16
+ Requires [gcloud tool](https://cloud.google.com/sdk/gcloud/) installed and authenticated against at least 1 GCP project.
17
+
18
+ ## Usage
19
+
20
+ ```shell
21
+ $ gcloud_hosts -h
22
+ Usage: $ gcloud_hosts [options]
23
+ -g, --gcloud GCLOUD Path to gcloud executable. Defaults to PATH location
24
+ -p, --project PROJECT gcloud project to use. Defaults to default gcloud configuration.
25
+ -n, --network NETWORK gcloud network to filter on. Defaults nil.
26
+ -d, --domain DOMAIN Domain to append to all hosts. Default: "c.[PROJECT].internal"
27
+ --public PUBLIC Pattern to match for public/bastion hosts. Use public IP for these. Defaults to nil
28
+ -f, --file FILE Hosts file to update. Defaults to /etc/hosts
29
+ -b, --backup BACKUP Path to backup original hosts file to. Defaults to FILE with '.bak' extension appended.
30
+ --[no-]dry-run Dry run, do not modify hosts file. Defaults to false
31
+ --[no-]delete Delete the project from hosts file. Defaults to false
32
+ --help Show this message
33
+ --version Show version
34
+ ```
35
+
36
+ ## Example
37
+
38
+ Update your hosts file using gcloud_hosts:
39
+
40
+ ```shell
41
+ $ sudo gcloud_hosts -p my-cool-project --public bastion
42
+
43
+ ```
44
+ Start sshuttle session:
45
+
46
+ ```shell
47
+ $ sshuttle --remote=bastion01 --daemon --pidfile=/tmp/sshuttle.pid 192.168.1.0/24
48
+ ```
49
+
50
+ Now your hosts file will contain entries for all compute instances in the project,
51
+ and you can ssh directly to them from your local machine.
52
+
53
+ Hosts matching the pattern passed in with the `--public` flag will have their public
54
+ IP address added to your host file instead of the their private internal IP address.
55
+
56
+ ## Contributing
57
+
58
+ Bug reports and pull requests are welcome on GitHub at [https://github.com/atongen/gcloud_hosts](https://github.com/atongen/gcloud_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/gcloud_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 'gcloud_hosts'
6
+ GcloudHosts::Runner.new(ARGV.dup).run!
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'gcloud_hosts/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "gcloud_hosts"
8
+ spec.version = GcloudHosts::VERSION
9
+ spec.authors = ["Andrew Tongen"]
10
+ spec.email = ["atongen@gmail.com"]
11
+
12
+ spec.summary = %q{Update your hosts file based on gcloud compute instances}
13
+ spec.description = %q{Update your hosts file based on gcloud compute instances}
14
+ spec.homepage = "https://github.com/atongen/gcloud_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_development_dependency "bundler", "~> 1.11"
28
+ spec.add_development_dependency "rake", "~> 10.0"
29
+ spec.add_development_dependency "rspec", "~> 3.0"
30
+ end
@@ -0,0 +1,49 @@
1
+ require 'json'
2
+
3
+ module GcloudHosts
4
+ module Hosts
5
+
6
+ def self.instances(gcloud_path, project, network)
7
+ JSON.parse(%x{ #{gcloud_path} compute instances list --project #{project} --format json 2>/dev/null })
8
+ .select { |i| i["status"] == "RUNNING" }
9
+ .select do |i|
10
+ network.to_s.strip == "" ||
11
+ i["networkInterfaces"].any? { |ni| ni["network"] == network }
12
+ end.sort { |x,y| x["name"] <=> y["name"] }
13
+ end
14
+
15
+ def self.hosts(gcloud_path, project, network, domain, public_pattern)
16
+ instances(gcloud_path, project, network).inject([]) do |list, i|
17
+ begin
18
+ if public_pattern.to_s.strip != "" && i["name"].downcase.include?(public_pattern)
19
+ # get external ip address
20
+ i["networkInterfaces"].each do |ni|
21
+ ni["accessConfigs"].each do |ac|
22
+ if ac["name"].downcase.include?("nat") && ac["type"].downcase.include?("nat")
23
+ if ip = ac["natIP"]
24
+ str = "#{ip} #{i["name"]}"
25
+ list << str
26
+ raise HostError.new
27
+ end
28
+ end
29
+ end
30
+ end
31
+ else
32
+ # get first internal private network interface
33
+ i["networkInterfaces"].each do |ni|
34
+ if ni["name"] == "nic0"
35
+ if ip = ni["networkIP"]
36
+ str = "#{ip} #{i["name"]}"
37
+ str << " #{i["name"]}.#{domain}" unless domain.to_s.strip == ""
38
+ list << str
39
+ raise HostError.new
40
+ end
41
+ end
42
+ end
43
+ end
44
+ rescue HostError; end
45
+ list
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,68 @@
1
+ require 'optparse'
2
+
3
+ module GcloudHosts
4
+ class Options
5
+
6
+ attr_reader :options
7
+
8
+ def initialize(args)
9
+ @options = {
10
+ gcloud: %x{ which gcloud 2>/dev/null }.to_s.strip,
11
+ project: nil,
12
+ network: nil,
13
+ domain: nil,
14
+ public: nil,
15
+ file: '/etc/hosts',
16
+ backup: nil,
17
+ dry_run: false,
18
+ delete: false
19
+ }
20
+ parser.parse!(args)
21
+ end
22
+
23
+ private
24
+
25
+ def parser
26
+ @parser ||= begin
27
+ OptionParser.new do |opts|
28
+ opts.banner = "Usage: $ gcloud_hosts [options]"
29
+ opts.on('-g', '--gcloud GCLOUD', "Path to gcloud executable. Defaults to PATH location") do |opt|
30
+ @options[:project] = opt
31
+ end
32
+ opts.on('-p', '--project PROJECT', "gcloud project to use. Defaults to default gcloud configuration.") do |opt|
33
+ @options[:project] = opt
34
+ end
35
+ opts.on('-n', '--network NETWORK', "gcloud network to filter on. Defaults nil.") do |opt|
36
+ @options[:network] = opt
37
+ end
38
+ opts.on('-d', '--domain DOMAIN', "Domain to append to all hosts. Default: \"c.[PROJECT].internal\"") do |opt|
39
+ @options[:domain] = opt
40
+ end
41
+ opts.on('--public PUBLIC', "Pattern to match for public/bastion hosts. Use public IP for these. Defaults to nil") do |opt|
42
+ @options[:public] = opt
43
+ end
44
+ opts.on('-f', '--file FILE', "Hosts file to update. Defaults to /etc/hosts") do |opt|
45
+ @options[:file] = opt
46
+ end
47
+ opts.on('-b', '--backup BACKUP', "Path to backup original hosts file to. Defaults to FILE with '.bak' extension appended.") do |opt|
48
+ @options[:file] = opt
49
+ end
50
+ opts.on('--[no-]dry-run', "Dry run, do not modify hosts file. Defaults to false") do |opt|
51
+ @options[:dry_run] = opt
52
+ end
53
+ opts.on('--[no-]delete', "Delete the project from hosts file. Defaults to false") do |opt|
54
+ @options[:delete] = opt
55
+ end
56
+ opts.on_tail("--help", "Show this message") do
57
+ puts opts
58
+ exit
59
+ end
60
+ opts.on_tail("--version", "Show version") do
61
+ puts ::GcloudHosts::VERSION.join('.')
62
+ exit
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,52 @@
1
+ require 'json'
2
+
3
+ module GcloudHosts
4
+ class Runner
5
+
6
+ def initialize(args)
7
+ @options = Options.new(args).options
8
+ end
9
+
10
+ def run!
11
+ project = @options[:project]
12
+ if project.to_s.strip == ""
13
+ project = env["core"]["project"]
14
+ end
15
+ if project.to_s.strip == ""
16
+ raise AuthError.new("No gcloud project specified.")
17
+ end
18
+
19
+ if @options[:domain]
20
+ domain = @options[:domain].to_s.strip
21
+ else
22
+ domain = "c.#{project}.internal"
23
+ end
24
+
25
+ backup = @options[:backup] ||
26
+ @options[:file] + '.bak'
27
+
28
+ if @options[:delete]
29
+ new_hosts_list = []
30
+ else
31
+ new_hosts_list = Hosts.hosts(@options[:gcloud], project, @options[:network], domain, @options[:public])
32
+ end
33
+ Updater.update(new_hosts_list.join("\n"), project, @options[:file], backup, @options[:dry_run], @options[:delete])
34
+ end
35
+
36
+ private
37
+
38
+ def env
39
+ @env ||= begin
40
+ gcloud = @options[:gcloud]
41
+ if gcloud.to_s.strip == ""
42
+ raise AuthError.new("gcloud command not found.")
43
+ end
44
+ env = JSON.parse(%x{ #{gcloud} config list --format json 2>/dev/null })
45
+ if env["core"]["account"].to_s.strip == ""
46
+ raise AuthError.new("Please log into gcloud.")
47
+ end
48
+ env
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,132 @@
1
+ module GcloudHosts
2
+ # Updater implements a very 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
+ module Updater
6
+
7
+ module Marker
8
+ BEFORE = 0
9
+ INSIDE = 1
10
+ AFTER = 2
11
+ end
12
+
13
+ def self.update(new_hosts, project, file, backup_file, dry_run, delete)
14
+ start_marker = "# START GCLOUD HOSTS - #{project} #"
15
+ end_marker = "# END GCLOUD HOSTS - #{project} #"
16
+
17
+ old_hosts = File.read(file)
18
+
19
+ if old_hosts.include?(start_marker) && old_hosts.include?(end_marker)
20
+ # valid markers exists
21
+ if delete
22
+ new_content = delete_project_hosts(old_hosts, start_marker, end_marker)
23
+ else
24
+ new_content = gen_new_hosts(old_hosts, new_hosts, start_marker, end_marker)
25
+ end
26
+ # remove zero or more white space characters at end of file with
27
+ # a single new-line
28
+ new_content.gsub!(/\s+$/, "\n")
29
+
30
+ if dry_run
31
+ puts new_content
32
+ elsif new_content != old_hosts
33
+ # backup old host file
34
+ File.open(backup_file, 'w') { |f| f << old_hosts }
35
+ # write new content
36
+ File.open(file, 'w') { |f| f << new_content }
37
+ end
38
+ elsif old_hosts.include?(start_marker) || old_hosts.include?(end_marker)
39
+ raise UpdaterError.new("Invalid marker present in existing hosts content")
40
+ else
41
+ # marker doesn't exist
42
+ if delete
43
+ new_content = old_hosts
44
+ else
45
+ new_content = [old_hosts, start_marker, new_hosts, end_marker].join("\n")
46
+ end
47
+ # remove one or more white space characters at end of file with
48
+ # a single new-line
49
+ new_content.gsub!(/\s+$/, "\n")
50
+
51
+ if dry_run
52
+ puts new_content
53
+ elsif new_content != old_hosts
54
+ # backup old host file
55
+ File.open(backup_file, 'w') { |f| f << old_hosts }
56
+ # write new content
57
+ File.open(file, 'w') { |f| f << new_content }
58
+ end
59
+ end
60
+
61
+ true
62
+ end
63
+
64
+ def self.gen_new_hosts(hosts, new_hosts, start_marker, end_marker)
65
+ new_content = ''
66
+ marker_state = Marker::BEFORE
67
+ hosts.split("\n").each do |line|
68
+ if line == start_marker
69
+ if marker_state == Marker::BEFORE
70
+ # transition to inside the marker
71
+ new_content << start_marker + "\n"
72
+ marker_state = Marker::INSIDE
73
+ # add new host content
74
+ new_hosts.split("\n").each do |host|
75
+ new_content << host + "\n"
76
+ end
77
+ else
78
+ raise UpdaterError.new("Invalid marker state")
79
+ end
80
+ elsif line == end_marker
81
+ if marker_state == Marker::INSIDE
82
+ # transition to after the marker
83
+ new_content << end_marker + "\n"
84
+ marker_state = Marker::AFTER
85
+ else
86
+ raise UpdaterError.new("Invalid marker state")
87
+ end
88
+ else
89
+ case marker_state
90
+ when Marker::BEFORE, Marker::AFTER
91
+ new_content << line + "\n"
92
+ when Marker::INSIDE
93
+ # skip everything between old markers
94
+ next
95
+ end
96
+ end
97
+ end
98
+ new_content
99
+ end
100
+
101
+ def self.delete_project_hosts(hosts, start_marker, end_marker)
102
+ new_content = ''
103
+ marker_state = Marker::BEFORE
104
+ hosts.split("\n").each do |line|
105
+ if line == start_marker
106
+ if marker_state == Marker::BEFORE
107
+ marker_state = Marker::INSIDE
108
+ # don't add any content, we're deleting this block
109
+ else
110
+ raise UpdaterError.new("Invalid marker state")
111
+ end
112
+ elsif line == end_marker
113
+ if marker_state == Marker::INSIDE
114
+ marker_state = Marker::AFTER
115
+ else
116
+ raise UpdaterError.new("Invalid marker state")
117
+ end
118
+ else
119
+ case marker_state
120
+ when Marker::BEFORE, Marker::AFTER
121
+ new_content << line + "\n"
122
+ when Marker::INSIDE
123
+ # skip everything between old markers
124
+ next
125
+ end
126
+ end
127
+ end
128
+ new_content
129
+ end
130
+
131
+ end
132
+ end
@@ -0,0 +1,3 @@
1
+ module GcloudHosts
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,11 @@
1
+ require 'gcloud_hosts/version'
2
+ require 'gcloud_hosts/options'
3
+ require 'gcloud_hosts/hosts'
4
+ require 'gcloud_hosts/updater'
5
+ require 'gcloud_hosts/runner'
6
+
7
+ module GcloudHosts
8
+ class HostError < StandardError; end
9
+ class UpdaterError < StandardError; end
10
+ class AuthError < StandardError; end
11
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gcloud_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: 2016-02-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.11'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.11'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ description: Update your hosts file based on gcloud compute instances
56
+ email:
57
+ - atongen@gmail.com
58
+ executables:
59
+ - gcloud_hosts
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ".gitignore"
64
+ - ".rspec"
65
+ - ".travis.yml"
66
+ - Gemfile
67
+ - README.md
68
+ - Rakefile
69
+ - bin/gcloud_hosts
70
+ - gcloud_hosts.gemspec
71
+ - lib/gcloud_hosts.rb
72
+ - lib/gcloud_hosts/hosts.rb
73
+ - lib/gcloud_hosts/options.rb
74
+ - lib/gcloud_hosts/runner.rb
75
+ - lib/gcloud_hosts/updater.rb
76
+ - lib/gcloud_hosts/version.rb
77
+ homepage: https://github.com/atongen/gcloud_hosts
78
+ licenses: []
79
+ metadata:
80
+ allowed_push_host: https://rubygems.org
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubyforge_project:
97
+ rubygems_version: 2.4.5.1
98
+ signing_key:
99
+ specification_version: 4
100
+ summary: Update your hosts file based on gcloud compute instances
101
+ test_files: []
102
+ has_rdoc: