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 +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/README.md +58 -0
- data/Rakefile +6 -0
- data/bin/gcloud_hosts +6 -0
- data/gcloud_hosts.gemspec +30 -0
- data/lib/gcloud_hosts/hosts.rb +49 -0
- data/lib/gcloud_hosts/options.rb +68 -0
- data/lib/gcloud_hosts/runner.rb +52 -0
- data/lib/gcloud_hosts/updater.rb +132 -0
- data/lib/gcloud_hosts/version.rb +3 -0
- data/lib/gcloud_hosts.rb +11 -0
- metadata +102 -0
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
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
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
data/bin/gcloud_hosts
ADDED
@@ -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
|
data/lib/gcloud_hosts.rb
ADDED
@@ -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:
|