gcloud_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: 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: