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.
Files changed (46) hide show
  1. data/LICENSE +1 -1
  2. data/bin/ghost +2 -130
  3. data/lib/ghost.rb +3 -16
  4. data/lib/ghost/cli.rb +60 -0
  5. data/lib/ghost/cli/task.rb +71 -0
  6. data/lib/ghost/cli/task/add.rb +25 -0
  7. data/lib/ghost/cli/task/delete.rb +30 -0
  8. data/lib/ghost/cli/task/empty.rb +18 -0
  9. data/lib/ghost/cli/task/export.rb +19 -0
  10. data/lib/ghost/cli/task/help.rb +41 -0
  11. data/lib/ghost/cli/task/import.rb +25 -0
  12. data/lib/ghost/cli/task/list.rb +40 -0
  13. data/lib/ghost/host.rb +34 -0
  14. data/lib/ghost/store.rb +12 -0
  15. data/lib/ghost/store/dscl_store.rb +71 -0
  16. data/lib/ghost/store/hosts_file_store.rb +123 -0
  17. data/lib/ghost/tokenized_file.rb +65 -0
  18. data/lib/ghost/version.rb +3 -0
  19. data/spec/ghost/cli/task/add_spec.rb +80 -0
  20. data/spec/ghost/cli/task/delete_spec.rb +20 -0
  21. data/spec/ghost/cli/task/empty_spec.rb +19 -0
  22. data/spec/ghost/cli/task/export_spec.rb +16 -0
  23. data/spec/ghost/cli/task/help_spec.rb +36 -0
  24. data/spec/ghost/cli/task/import_spec.rb +56 -0
  25. data/spec/ghost/cli/task/list_spec.rb +50 -0
  26. data/spec/ghost/cli_spec.rb +22 -0
  27. data/spec/ghost/host_spec.rb +36 -0
  28. data/spec/ghost/store/dscl_store_spec.rb +153 -0
  29. data/spec/ghost/store/hosts_file_store_spec.rb +316 -0
  30. data/spec/ghost/store_spec.rb +2 -0
  31. data/spec/ghost/tokenized_file_spec.rb +131 -0
  32. data/spec/spec_helper.rb +4 -2
  33. data/spec/support/cli.rb +29 -0
  34. data/spec/support/resolv.rb +15 -0
  35. metadata +91 -27
  36. data/Rakefile +0 -28
  37. data/TODO +0 -0
  38. data/bin/ghost-ssh +0 -132
  39. data/lib/ghost/linux-host.rb +0 -158
  40. data/lib/ghost/mac-host.rb +0 -116
  41. data/lib/ghost/ssh_config.rb +0 -110
  42. data/spec/etc_hosts_spec.rb +0 -190
  43. data/spec/ghost_spec.rb +0 -151
  44. data/spec/spec.opts +0 -1
  45. data/spec/ssh_config_spec.rb +0 -80
  46. data/spec/ssh_config_template +0 -11
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2008 Bodaniel Jeanes
1
+ Copyright (c) 2008-2012 Bodaniel Jeanes
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
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 File.expand_path(File.dirname(__FILE__) + '/../lib/ghost')
7
- require 'open-uri'
3
+ require 'ghost/cli'
8
4
 
9
- def help_text(exit_code = 0)
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
@@ -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 'rbconfig'
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'
@@ -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