ghost 0.3.0 → 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
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