ghost 0.3.0 → 1.0.0.pre
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.
- data/LICENSE +1 -1
- data/bin/ghost +2 -130
- data/lib/ghost.rb +3 -16
- data/lib/ghost/cli.rb +60 -0
- data/lib/ghost/cli/task.rb +71 -0
- data/lib/ghost/cli/task/add.rb +25 -0
- data/lib/ghost/cli/task/delete.rb +30 -0
- data/lib/ghost/cli/task/empty.rb +18 -0
- data/lib/ghost/cli/task/export.rb +19 -0
- data/lib/ghost/cli/task/help.rb +41 -0
- data/lib/ghost/cli/task/import.rb +25 -0
- data/lib/ghost/cli/task/list.rb +40 -0
- data/lib/ghost/host.rb +34 -0
- data/lib/ghost/store.rb +12 -0
- data/lib/ghost/store/dscl_store.rb +71 -0
- data/lib/ghost/store/hosts_file_store.rb +123 -0
- data/lib/ghost/tokenized_file.rb +65 -0
- data/lib/ghost/version.rb +3 -0
- data/spec/ghost/cli/task/add_spec.rb +80 -0
- data/spec/ghost/cli/task/delete_spec.rb +20 -0
- data/spec/ghost/cli/task/empty_spec.rb +19 -0
- data/spec/ghost/cli/task/export_spec.rb +16 -0
- data/spec/ghost/cli/task/help_spec.rb +36 -0
- data/spec/ghost/cli/task/import_spec.rb +56 -0
- data/spec/ghost/cli/task/list_spec.rb +50 -0
- data/spec/ghost/cli_spec.rb +22 -0
- data/spec/ghost/host_spec.rb +36 -0
- data/spec/ghost/store/dscl_store_spec.rb +153 -0
- data/spec/ghost/store/hosts_file_store_spec.rb +316 -0
- data/spec/ghost/store_spec.rb +2 -0
- data/spec/ghost/tokenized_file_spec.rb +131 -0
- data/spec/spec_helper.rb +4 -2
- data/spec/support/cli.rb +29 -0
- data/spec/support/resolv.rb +15 -0
- metadata +91 -27
- data/Rakefile +0 -28
- data/TODO +0 -0
- data/bin/ghost-ssh +0 -132
- data/lib/ghost/linux-host.rb +0 -158
- data/lib/ghost/mac-host.rb +0 -116
- data/lib/ghost/ssh_config.rb +0 -110
- data/spec/etc_hosts_spec.rb +0 -190
- data/spec/ghost_spec.rb +0 -151
- data/spec/spec.opts +0 -1
- data/spec/ssh_config_spec.rb +0 -80
- data/spec/ssh_config_template +0 -11
data/LICENSE
CHANGED
data/bin/ghost
CHANGED
@@ -1,133 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
#
|
3
|
-
# Created by Bodaniel Jeanes on 2008-8-19.
|
4
|
-
# Copyright (c) 2008. All rights reserved.
|
5
2
|
|
6
|
-
require
|
7
|
-
require 'open-uri'
|
3
|
+
require 'ghost/cli'
|
8
4
|
|
9
|
-
|
10
|
-
script_name = File.basename $0
|
11
|
-
puts """USAGE: #{script_name} add <hostname> [<ip=127.0.1.1>]
|
12
|
-
#{script_name} modify <hostname> <ip> OR <hostname> (will lookup ip)
|
13
|
-
#{script_name} delete <hostname>
|
14
|
-
#{script_name} delete_matching <pattern>
|
15
|
-
#{script_name} list [<filter>]
|
16
|
-
#{script_name} empty
|
17
|
-
#{script_name} export
|
18
|
-
#{script_name} import <file>
|
19
|
-
"""
|
20
|
-
exit(exit_code)
|
21
|
-
end
|
22
|
-
|
23
|
-
begin
|
24
|
-
if ARGV.size.zero? || ['-h', '--help', 'help'].include?(ARGV.first)
|
25
|
-
help_text
|
26
|
-
else
|
27
|
-
case ARGV[0]
|
28
|
-
when 'add'
|
29
|
-
if [2,3].include?(ARGV.size)
|
30
|
-
begin
|
31
|
-
ARGV.shift
|
32
|
-
host = Ghost::Host.add(*ARGV)
|
33
|
-
puts " [Adding] #{host.name} -> #{host.ip}"
|
34
|
-
exit 0
|
35
|
-
rescue SocketError
|
36
|
-
$stderr.puts "Cannot find IP for host"
|
37
|
-
exit 3
|
38
|
-
rescue Ghost::RecordExists
|
39
|
-
$stderr.puts "Cannot overwrite an existing entry. Use the modify subcommand"
|
40
|
-
exit 3
|
41
|
-
end
|
42
|
-
else
|
43
|
-
$stderr.puts "The add subcommand requires at least a hostname.\n\n"
|
44
|
-
help_text 2
|
45
|
-
end
|
46
|
-
when 'modify'
|
47
|
-
if ARGV.size == 3
|
48
|
-
ARGV.shift
|
49
|
-
ARGV << true
|
50
|
-
host = Ghost::Host.add(*ARGV)
|
51
|
-
puts " [Modifying] #{host.name} -> #{host.ip}"
|
52
|
-
exit 0
|
53
|
-
else
|
54
|
-
$stderr.puts "The modify subcommand requires a hostname and an IP.\n\n"
|
55
|
-
help_text 4
|
56
|
-
end
|
57
|
-
when 'delete', 'del', 'remove', 'rm'
|
58
|
-
if ARGV.size == 2
|
59
|
-
Ghost::Host.delete(ARGV[1])
|
60
|
-
puts " [Deleting] #{ARGV[1]}"
|
61
|
-
exit 0
|
62
|
-
else
|
63
|
-
$stderr.puts "The delete subcommand requires a hostname.\n\n"
|
64
|
-
help_text 2
|
65
|
-
end
|
66
|
-
when 'delete_matching'
|
67
|
-
if ARGV.size == 2
|
68
|
-
matched = Ghost::Host.delete_matching(ARGV[1])
|
69
|
-
if matched.empty?
|
70
|
-
puts " No matching entries found"
|
71
|
-
else
|
72
|
-
matched.each { |h| puts " [Deleting] #{h}" }
|
73
|
-
end
|
74
|
-
exit 0
|
75
|
-
else
|
76
|
-
$stderr.puts "The delete_matching subcommand requires a hostname pattern.\n\n"
|
77
|
-
help_text 2
|
78
|
-
end
|
79
|
-
when 'list'
|
80
|
-
filter = %r|#{ARGV[1]}| if ARGV.size == 2
|
81
|
-
|
82
|
-
hosts = Ghost::Host.list
|
83
|
-
|
84
|
-
if filter
|
85
|
-
hosts.reject! { |host| host.name !~ filter && host.ip !~ filter }
|
86
|
-
end
|
87
|
-
|
88
|
-
pad = hosts.max{|a,b| a.to_s.length <=> b.to_s.length }.to_s.length
|
89
|
-
|
90
|
-
puts "Listing #{hosts.size} host(s):"
|
91
|
-
|
92
|
-
hosts.each do |host|
|
93
|
-
puts "#{host.name.rjust(pad+2)} -> #{host.ip}"
|
94
|
-
end
|
95
|
-
exit 0
|
96
|
-
when 'empty'
|
97
|
-
print " [Emptying] "
|
98
|
-
Ghost::Host.empty!
|
99
|
-
puts "Done."
|
100
|
-
exit 0
|
101
|
-
when 'export'
|
102
|
-
hosts = Ghost::Host.list
|
103
|
-
hosts.each do |host|
|
104
|
-
puts "#{host.name} #{host.ip}"
|
105
|
-
end
|
106
|
-
exit 0
|
107
|
-
when 'import'
|
108
|
-
if ARGV.size == 2
|
109
|
-
begin
|
110
|
-
import_file = open(ARGV[1])
|
111
|
-
import_file.each_line do |line|
|
112
|
-
host_infos = line.strip.split(' ', 2)
|
113
|
-
host = Ghost::Host.add(*host_infos)
|
114
|
-
puts " [Adding] #{host.name} -> #{host.ip}"
|
115
|
-
end
|
116
|
-
exit 0
|
117
|
-
rescue
|
118
|
-
$stderr.puts "Cannot import. A problem occured while opening the import file."
|
119
|
-
exit 5
|
120
|
-
end
|
121
|
-
else
|
122
|
-
$stderr.puts "The import command requires an input file.\n\n"
|
123
|
-
help_text 6
|
124
|
-
end
|
125
|
-
else
|
126
|
-
$stderr.puts "Invalid option: #{ARGV[0]}"
|
127
|
-
help_text 1
|
128
|
-
end
|
129
|
-
end
|
130
|
-
rescue Errno::EACCES
|
131
|
-
$stderr.puts "Please re-run as root or using sudo"
|
132
|
-
exit 3
|
133
|
-
end
|
5
|
+
Ghost::Cli.new.parse
|
data/lib/ghost.rb
CHANGED
@@ -1,22 +1,9 @@
|
|
1
1
|
$: << File.dirname(__FILE__)
|
2
2
|
|
3
3
|
module Ghost
|
4
|
-
class RecordExists < StandardError
|
5
|
-
end
|
6
4
|
end
|
7
5
|
|
8
|
-
require '
|
6
|
+
require 'ghost/version'
|
7
|
+
require 'ghost/host'
|
8
|
+
require 'ghost/store'
|
9
9
|
|
10
|
-
case RbConfig::CONFIG['host_os']
|
11
|
-
when /darwin/
|
12
|
-
productVersion = `/usr/bin/sw_vers -productVersion`.strip
|
13
|
-
if productVersion =~ /^10\.7\.[2-9]{1}$/
|
14
|
-
require 'ghost/linux-host'
|
15
|
-
else
|
16
|
-
require 'ghost/mac-host'
|
17
|
-
end
|
18
|
-
when /linux/
|
19
|
-
require 'ghost/linux-host'
|
20
|
-
end
|
21
|
-
|
22
|
-
require 'ghost/ssh_config'
|
data/lib/ghost/cli.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'ghost'
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'optparse/version'
|
5
|
+
|
6
|
+
module Ghost
|
7
|
+
class Cli
|
8
|
+
attr_accessor :out, :parser
|
9
|
+
|
10
|
+
def initialize(out = STDOUT)
|
11
|
+
self.out = out
|
12
|
+
|
13
|
+
setup_parser
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse(args = ARGV)
|
17
|
+
parser.parse! args
|
18
|
+
|
19
|
+
arg = args.shift.to_s
|
20
|
+
|
21
|
+
if (task = tasks[arg])
|
22
|
+
task.perform(*args)
|
23
|
+
else
|
24
|
+
abort "No such task"
|
25
|
+
end
|
26
|
+
rescue Errno::EACCES
|
27
|
+
abort "Insufficient privileges. Try using `sudo` or running as root."
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def setup_parser
|
33
|
+
self.parser = OptionParser.new do |o|
|
34
|
+
o.on_tail '-v', '--version' do
|
35
|
+
puts parser.ver
|
36
|
+
exit
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
parser.program_name = "ghost"
|
41
|
+
parser.version = Ghost::VERSION
|
42
|
+
end
|
43
|
+
|
44
|
+
def print(*args)
|
45
|
+
out.print(*args)
|
46
|
+
end
|
47
|
+
|
48
|
+
def puts(*args)
|
49
|
+
out.puts(*args)
|
50
|
+
end
|
51
|
+
|
52
|
+
# TODO: should output to STDERR
|
53
|
+
def abort(*args)
|
54
|
+
puts *args
|
55
|
+
exit 1
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
require 'ghost/cli/task'
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'unindent'
|
2
|
+
|
3
|
+
module Ghost
|
4
|
+
class Cli
|
5
|
+
class Task
|
6
|
+
attr_accessor :out
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_accessor :name
|
10
|
+
|
11
|
+
def desc(str = nil)
|
12
|
+
if str
|
13
|
+
@desc = str
|
14
|
+
else
|
15
|
+
@desc
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def help
|
20
|
+
if block_given?
|
21
|
+
@help = yield.unindent
|
22
|
+
else
|
23
|
+
@help
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(out)
|
29
|
+
self.out = out
|
30
|
+
end
|
31
|
+
|
32
|
+
def perform(*); end
|
33
|
+
def description; end
|
34
|
+
def help; end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def puts(*args)
|
39
|
+
out.puts(*args)
|
40
|
+
end
|
41
|
+
|
42
|
+
def print(*args)
|
43
|
+
out.print(*args)
|
44
|
+
end
|
45
|
+
|
46
|
+
def abort(*args)
|
47
|
+
out.puts(*args)
|
48
|
+
exit 1
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def tasks
|
53
|
+
@tasks ||= Hash[self.class.tasks.map { |name, task| [name, task.new(out)] }]
|
54
|
+
end
|
55
|
+
|
56
|
+
class << self
|
57
|
+
def task(*names, &block)
|
58
|
+
task = Class.new(Task, &block)
|
59
|
+
names.map!(&:to_s)
|
60
|
+
task.name = names.first
|
61
|
+
names.each { |name| tasks[name] = task }
|
62
|
+
end
|
63
|
+
|
64
|
+
def tasks
|
65
|
+
@tasks ||= {}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
Dir[File.dirname(__FILE__) + "/task/*.rb"].each { |f| require(f) }
|
@@ -0,0 +1,25 @@
|
|
1
|
+
Ghost::Cli.task :add do
|
2
|
+
desc "Add a host"
|
3
|
+
def perform(host, ip = nil)
|
4
|
+
host = Ghost::Host.new(*[host, ip].compact)
|
5
|
+
Ghost.store.add(host)
|
6
|
+
puts "[Adding] #{host.name} -> #{host.ip}"
|
7
|
+
rescue Ghost::Host::NotResolvable
|
8
|
+
abort "Unable to resolve IP address for target host #{ip.inspect}."
|
9
|
+
end
|
10
|
+
|
11
|
+
help do
|
12
|
+
<<-EOF.unindent
|
13
|
+
Usage: ghost add <local host name> [<remote host name>|<IP address>]
|
14
|
+
|
15
|
+
#{desc}.
|
16
|
+
|
17
|
+
If a second parameter is not provided, it defaults to 127.0.0.1
|
18
|
+
|
19
|
+
Examples:
|
20
|
+
ghost add my-localhost # points to 127.0.0.1
|
21
|
+
ghost add google.dev google.com # points to the IP of google.com
|
22
|
+
ghost add router 192.168.1.1 # points to 192.168.1.1
|
23
|
+
EOF
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
Ghost::Cli.task :delete, :rm, :del, :remove do
|
2
|
+
desc "Remove a ghost-managed host"
|
3
|
+
def perform(host)
|
4
|
+
host = /#{$1}/i if %r[^/(.+)/$] =~ host
|
5
|
+
Ghost.store.delete(host)
|
6
|
+
end
|
7
|
+
|
8
|
+
help do
|
9
|
+
<<-EOF.unindent
|
10
|
+
Usage: ghost delete <host name|regex>
|
11
|
+
|
12
|
+
#{desc}.
|
13
|
+
|
14
|
+
The delete task accepts either a literal host name, or a regular
|
15
|
+
expression, notated with surrounding forward slashes (/). If a
|
16
|
+
host name is provided, it will delete only the entries whose host
|
17
|
+
names are equal to the provided host name. If a regular expression
|
18
|
+
is provided, all entries whose host names match the regular
|
19
|
+
expression will be deleted.
|
20
|
+
|
21
|
+
Depending on your shell, you may need to escape or quote the
|
22
|
+
regular expression to ensure that the shell doesn't perform
|
23
|
+
expansion on the expression.
|
24
|
+
|
25
|
+
Examples:
|
26
|
+
ghost delete foo.com # will delete foo.com
|
27
|
+
ghost delete /^fo+\\.com$/ # will delete fo.com, fooooo.com, etc.
|
28
|
+
EOF
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
Ghost::Cli.task :empty do
|
2
|
+
desc "Clear all ghost-managed hosts"
|
3
|
+
|
4
|
+
def perform
|
5
|
+
print "[Emptying] "
|
6
|
+
Ghost.store.empty
|
7
|
+
puts "Done."
|
8
|
+
end
|
9
|
+
|
10
|
+
help do
|
11
|
+
<<-EOF.unindent
|
12
|
+
Usage: ghost empty
|
13
|
+
|
14
|
+
The empty task will delete all ghost-managed hosts from
|
15
|
+
the host store.
|
16
|
+
EOF
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
Ghost::Cli.task :export do
|
2
|
+
desc "Export all hosts in /etc/hosts format"
|
3
|
+
def perform
|
4
|
+
Ghost.store.all.each do |host|
|
5
|
+
puts "#{host.ip} #{host.name}"
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
help do
|
10
|
+
<<-EOF.unindent
|
11
|
+
Usage: ghost export
|
12
|
+
|
13
|
+
#{desc}.
|
14
|
+
|
15
|
+
The export will be printed to standard out.
|
16
|
+
EOF
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
@@ -0,0 +1,41 @@
|
|
1
|
+
Ghost::Cli.task :help, nil do
|
2
|
+
def perform(name=nil)
|
3
|
+
return overview unless name
|
4
|
+
|
5
|
+
task = tasks_by_name[name]
|
6
|
+
abort "No help for task '#{name}'" unless task && task.help
|
7
|
+
|
8
|
+
puts task.help
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def overview
|
14
|
+
puts "USAGE: ghost <task> [<args>]"
|
15
|
+
puts ""
|
16
|
+
puts "The ghost tasks are:"
|
17
|
+
|
18
|
+
tasks_to_show do |name, desc|
|
19
|
+
puts " #{name} #{desc}"
|
20
|
+
end
|
21
|
+
|
22
|
+
puts ""
|
23
|
+
puts "See 'ghost help <task>' for more information on a specific task."
|
24
|
+
end
|
25
|
+
|
26
|
+
def tasks_by_name
|
27
|
+
Ghost::Cli.tasks
|
28
|
+
end
|
29
|
+
|
30
|
+
def tasks
|
31
|
+
tasks_by_name.values.uniq
|
32
|
+
end
|
33
|
+
|
34
|
+
def tasks_to_show
|
35
|
+
size = tasks.map { |t| t.name.length }.max
|
36
|
+
tasks.sort_by(&:name).each do |task|
|
37
|
+
next unless task.desc
|
38
|
+
yield task.name.ljust(size), task.desc
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
Ghost::Cli.task :import do
|
2
|
+
desc "Import hosts in /etc/hosts format"
|
3
|
+
def perform(*files)
|
4
|
+
files.each do |file|
|
5
|
+
File.readlines(file).each do |line|
|
6
|
+
ip, name = *line.split(/\s+/)
|
7
|
+
host = Ghost::Host.new(name, ip)
|
8
|
+
Ghost.store.add(host)
|
9
|
+
puts "[Adding] #{host.name} -> #{host.ip}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
help do
|
15
|
+
<<-EOF.unindent
|
16
|
+
Usage: ghost import file1 [file2 [file3 ...]]
|
17
|
+
|
18
|
+
#{desc}.
|
19
|
+
|
20
|
+
Each file provided will be read in as an import file. The
|
21
|
+
behavior for duplicate or conflicting entries is not currently
|
22
|
+
defined.
|
23
|
+
EOF
|
24
|
+
end
|
25
|
+
end
|