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
@@ -1,158 +0,0 @@
1
- require 'socket'
2
-
3
- module Ghost
4
- class Host
5
-
6
- attr_reader :host, :ip
7
-
8
- def initialize(host, ip)
9
- @host = host
10
- @ip = ip
11
- end
12
-
13
- def ==(other)
14
- @host == other.host && @ip = other.ip
15
- end
16
-
17
- alias :to_s :host
18
- alias :name :host
19
- alias :hostname :host
20
-
21
- @@hosts_file = '/etc/hosts'
22
- @@start_token, @@end_token = '# ghost start', '# ghost end'
23
-
24
- class << self
25
- protected :new
26
-
27
- def list
28
- with_exclusive_file_access(true) do |file|
29
- entries = []
30
- in_ghost_area = false
31
- file.pos = 0
32
-
33
- file.each do |line|
34
- if !in_ghost_area and line =~ /^#{@@start_token}/
35
- in_ghost_area = true
36
- elsif in_ghost_area
37
- if line =~ /^#{@@end_token}/o
38
- in_ghost_area = false
39
- elsif line =~ /^(\d+\.\d+\.\d+\.\d+)\s+(.*)$/
40
- ip = $1
41
- hosts = $2.split(/\s+/)
42
- hosts.each { |host| entries << Host.new(host, ip) }
43
- end
44
- end
45
- end
46
-
47
- entries
48
- end
49
- end
50
-
51
- def add(host, ip = "127.0.0.1", force = false)
52
- with_exclusive_file_access do
53
- if find_by_host(host).nil? || force
54
- hosts = list
55
- hosts = hosts.delete_if { |h| h.name == host }
56
-
57
- unless ip[/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/]
58
- ip = Socket.gethostbyname(ip)[3].bytes.to_a.join('.')
59
- end
60
-
61
- new_host = Host.new(host, ip)
62
-
63
- hosts << new_host
64
- write_out!(hosts)
65
-
66
- new_host
67
- else
68
- raise Ghost::RecordExists, "Can not overwrite existing record"
69
- end
70
- end
71
- end
72
-
73
- def find_by_host(hostname)
74
- list.find { |host| host.hostname == hostname }
75
- end
76
-
77
- def find_by_ip(ip)
78
- list.find_all{ |host| host.ip == ip }
79
- end
80
-
81
- def empty!
82
- write_out!([])
83
- end
84
-
85
- def delete(name)
86
- with_exclusive_file_access do
87
- hosts = list
88
- hosts = hosts.delete_if { |host| host.name == name }
89
- write_out!(hosts)
90
- end
91
- end
92
-
93
- def delete_matching(pattern)
94
- with_exclusive_file_access do
95
- pattern = Regexp.escape(pattern)
96
- hosts = list.select { |host| host.name.match(/#{pattern}/) }
97
- hosts.each { |host| delete(host.name) }
98
- hosts
99
- end
100
- end
101
-
102
- protected
103
-
104
- def with_exclusive_file_access(read_only = false)
105
- return_val = nil
106
- flag = read_only ? 'r' : 'r+'
107
-
108
- if @_file
109
- return_val = yield @_file
110
- else
111
- File.open(@@hosts_file, flag) do |f|
112
- f.flock File::LOCK_EX
113
- begin
114
- @_file = f
115
- return_val = yield f
116
- ensure
117
- @_file = nil
118
- end
119
- end
120
- end
121
-
122
- return_val
123
- end
124
-
125
- def write_out!(hosts)
126
- with_exclusive_file_access do |f|
127
- new_ghosts = hosts.inject("") {|s, h| s + "#{h.ip} #{h.hostname}\n" }
128
-
129
- output = ""
130
- in_ghost_area, seen_tokens = false,false
131
- f.pos = 0
132
-
133
- f.each do |line|
134
- if line =~ /^#{@@start_token}/o
135
- in_ghost_area, seen_tokens = true,true
136
- output << line << new_ghosts
137
- elsif line =~ /^#{@@end_token}/o
138
- in_ghost_area = false
139
- end
140
- output << line unless in_ghost_area
141
- end
142
- if !seen_tokens
143
- output << surround_with_tokens( new_ghosts )
144
- end
145
-
146
- f.pos = 0
147
- f.print output
148
- f.truncate(f.pos)
149
- end
150
- end
151
-
152
-
153
- def surround_with_tokens(str)
154
- "\n#{@@start_token}\n#{str}#{@@end_token}\n"
155
- end
156
- end
157
- end
158
- end
@@ -1,116 +0,0 @@
1
- require 'socket'
2
-
3
- module Ghost
4
- class Host
5
- ListCmd = "dscl localhost -list /Local/Default/Hosts 2>&1"
6
- ReadCmd = "dscl localhost -read /Local/Default/Hosts/%s 2>&1"
7
- CreateCmd = "dscl localhost -create /Local/Default/Hosts/%s IPAddress %s 2>&1"
8
- DeleteCmd = "dscl localhost -delete /Local/Default/Hosts/%s 2>&1"
9
-
10
- class << self
11
- protected :new
12
-
13
- def list
14
- list = `#{ListCmd}`
15
- list = list.split("\n")
16
- list.collect { |host| Host.new(host.chomp) }
17
- end
18
-
19
- def add(host, ip = "127.0.0.1", force = false)
20
- if find_by_host(host).nil? || force
21
- unless ip[/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/]
22
- ip = Socket.gethostbyname(ip)[3].bytes.to_a.join('.')
23
- end
24
-
25
- `#{CreateCmd % [host, ip]}`
26
- flush!
27
- find_by_host(host)
28
- else
29
- raise Ghost::RecordExists, "Can not overwrite existing record"
30
- end
31
- end
32
-
33
- def find_by_host(host)
34
- @hosts ||= {}
35
- @hosts[host] ||= begin
36
- output = `#{ReadCmd % host}`
37
-
38
- if output =~ /eDSRecordNotFound/
39
- nil
40
- else
41
- host = parse_host(output)
42
- ip = parse_ip(output)
43
-
44
- Host.new(host, ip)
45
- end
46
- end
47
- end
48
-
49
- def find_by_ip(ip)
50
- nil
51
- end
52
-
53
- def empty!
54
- list.each { |h| delete(h) }
55
- nil
56
- end
57
-
58
- def delete(host)
59
- `#{DeleteCmd % host.to_s}`
60
- flush!
61
- end
62
-
63
- def delete_matching(pattern)
64
- pattern = Regexp.escape(pattern)
65
- hosts = list.select { |h| h.to_s.match(/#{pattern}/) }
66
- hosts.each do |h|
67
- delete(h)
68
- end
69
- flush! unless hosts.empty?
70
- hosts
71
- end
72
-
73
- # Flushes the DNS Cache
74
- def flush!
75
- `dscacheutil -flushcache`
76
- @hosts = {}
77
- true
78
- end
79
-
80
- protected
81
- def parse_host(output)
82
- parse_value(output, 'RecordName')
83
- end
84
-
85
- def parse_ip(output)
86
- parse_value(output, 'IPAddress')
87
- end
88
-
89
- def parse_value(output, key)
90
- match = output.match(Regexp.new("^#{key}: (.*)$"))
91
- match[1] unless match.nil?
92
- end
93
- end
94
-
95
- def initialize(host, ip=nil)
96
- @host = host
97
- @ip = ip
98
- end
99
-
100
- def hostname
101
- @host
102
- end
103
- alias :to_s :hostname
104
- alias :host :hostname
105
- alias :name :hostname
106
-
107
- def ip
108
- @ip ||= self.class.send(:parse_ip, dump)
109
- end
110
-
111
- private
112
- def dump
113
- @dump ||= `#{ReadCmd % hostname}`
114
- end
115
- end
116
- end
@@ -1,110 +0,0 @@
1
- module Ghost
2
- class SshConfigDuplicateError < StandardError; end
3
-
4
- class SshConfig
5
- @@ssh_config = "#{ENV['HOME']}/.ssh/config"
6
-
7
- attr_accessor :host, :hostname, :user, :port
8
- alias :to_s :host
9
-
10
- def initialize(host, hostname, user, port)
11
- @host = host
12
- @hostname = hostname
13
- @user = user
14
- @port = port
15
- end
16
-
17
- class << self
18
- protected :new
19
-
20
- def list
21
- lines = File.read(@@ssh_config).split("\n")
22
-
23
- entries = []
24
- current_entry = []
25
-
26
- lines << ""
27
- lines.each do |line|
28
- if line.strip.empty?
29
- entries << parse(current_entry) if current_entry.any?
30
- current_entry = []
31
- else
32
- current_entry << line
33
- end
34
- end
35
-
36
- entries
37
- end
38
-
39
- def add(args)
40
- host = args[:host]
41
- hostname = args[:hostname]
42
- user = args[:user] || "root"
43
- port = args[:port] || "22"
44
-
45
- force = args[:force]
46
-
47
- if find_by_host(host).nil? || force
48
- delete(host)
49
-
50
- new_config = SshConfig.new(host, hostname, user, port)
51
-
52
- configs = list
53
- configs << new_config
54
-
55
- write(configs)
56
-
57
- new_config
58
- else
59
- raise SshConfigDuplicateError, "Can not overwrite existing record"
60
- end
61
- end
62
-
63
- def find_by_host(host)
64
- list.find { |c| c.host == host }
65
- end
66
-
67
- def empty!
68
- write([])
69
- end
70
-
71
- def delete(host)
72
- configs = list
73
- configs = configs.delete_if { |c| c.host == host }
74
- write(configs)
75
- end
76
-
77
- def write(configs)
78
- hosts = []
79
-
80
- configs.sort! { |a,b| a.host <=> b.host }
81
- configs.each do |c|
82
- hosts << "Host #{c.host}"
83
- hosts << " HostName #{c.hostname}"
84
- hosts << " User #{c.user}" if c.user
85
- hosts << " Port #{c.port}" if c.port
86
- hosts << ""
87
- end
88
-
89
- File.open(@@ssh_config, 'w') {|f| f.print hosts.join("\n") }
90
- end
91
-
92
- def parse(config)
93
- host = config.first[/Host (.*)/, 1]
94
- config_hash = {}
95
-
96
- config[1..-1].each do |entry|
97
- entry.strip!
98
- next if entry.empty?
99
- key, value = entry.strip.split(" ")
100
- config_hash[key.downcase] = value
101
- end
102
-
103
- SshConfig.new(host,
104
- config_hash['hostname'], config_hash['user'],
105
- config_hash['port'])
106
- end
107
-
108
- end
109
- end
110
- end
@@ -1,190 +0,0 @@
1
- require 'ghost/linux-host'
2
-
3
- $hosts_file = File.expand_path(File.join(File.dirname(__FILE__), "etc_hosts"))
4
-
5
- describe Ghost::Host do
6
- before(:all) do
7
- class Ghost::Host
8
- @@hosts_file = $hosts_file
9
- end
10
- end
11
- before { `touch #{$hosts_file.inspect}` }
12
- after { `rm -f #{$hosts_file.inspect}` }
13
-
14
- it "has an IP" do
15
- hostname = 'ghost-test-hostname.local'
16
-
17
- Ghost::Host.add(hostname)
18
- host = Ghost::Host.list.first
19
- host.ip.should eql('127.0.0.1')
20
-
21
- Ghost::Host.empty!
22
-
23
- ip = '169.254.23.121'
24
- host = Ghost::Host.add(hostname, ip)
25
- host.ip.should eql(ip)
26
- end
27
-
28
- it "has a hostname" do
29
- hostname = 'ghost-test-hostname.local'
30
-
31
- Ghost::Host.add(hostname)
32
- host = Ghost::Host.list.first
33
- host.hostname.should eql(hostname)
34
-
35
- Ghost::Host.empty!
36
-
37
- ip = '169.254.23.121'
38
- Ghost::Host.add(hostname, ip)
39
- host.hostname.should eql(hostname)
40
- end
41
-
42
- describe ".list" do
43
- it "returns an array" do
44
- Ghost::Host.list.should be_instance_of(Array)
45
- end
46
-
47
- it "contains instances of Ghost::Host" do
48
- Ghost::Host.add('ghost-test-hostname.local')
49
- Ghost::Host.list.first.should be_instance_of(Ghost::Host)
50
- end
51
-
52
- it "gets all hosts on a single /etc/hosts line" do
53
- example = <<-EoEx
54
- 127.0.0.1 localhost.localdomain
55
- # ghost start
56
- 123.123.123.1 project_a.local\t\tproject_b.local project_c.local
57
- 127.0.0.1 local.bb
58
- # ghost end
59
- EoEx
60
- File.open($hosts_file, 'w') {|f| f << example.gsub!(/^\s*/, '')}
61
- hosts = Ghost::Host.list
62
- hosts.should have(4).items
63
- hosts.map{|h|h.ip}.uniq.sort.should == ['123.123.123.1', '127.0.0.1']
64
- hosts.map{|h|h.host}.sort.should == %w[local.bb project_a.local project_b.local project_c.local]
65
-
66
- Ghost::Host.add("project_d.local")
67
- Ghost::Host.list.should have(5).items
68
- end
69
- end
70
-
71
-
72
- it "does not change hosts files outside of control tokens" do
73
- f = File.open($hosts_file, 'w')
74
- f.write "10.0.0.23 adserver.example.com\n"
75
- f.close
76
- hosts = Ghost::Host.list
77
- hosts.should have(0).items
78
-
79
- hostname = "ghost-test-hostname-b.local"
80
- Ghost::Host.add(hostname)
81
- Ghost::Host.list.should have(1).items
82
-
83
- hostsfile = File.read($hosts_file)
84
- hostsfile.should =~ /^10.0.0.23 adserver.example.com\n/
85
- end
86
-
87
- it "adds control tokens to untouched hosts files" do
88
- f = File.open($hosts_file, 'w')
89
- f.write "10.0.0.23 adserver.example.com\n"
90
- f.close
91
- hostname = "ghost-test-hostname-b.local"
92
- ip = '123.45.67.89'
93
- Ghost::Host.add(hostname, ip)
94
-
95
- hostsfile = File.read($hosts_file)
96
- hostsfile.should =~ /# ghost start\n#{ip}\s+#{hostname}\n# ghost end/
97
- end
98
-
99
- describe "#to_s" do
100
- it "returns hostname" do
101
- hostname = 'ghost-test-hostname.local'
102
-
103
- Ghost::Host.add(hostname)
104
- host = Ghost::Host.list.first
105
- host.to_s.should eql(hostname)
106
- end
107
- end
108
-
109
- describe "finder methods" do
110
- before do
111
- Ghost::Host.add('abc.local')
112
- Ghost::Host.add('def.local')
113
- Ghost::Host.add('efg.local', '10.2.2.4')
114
- end
115
-
116
- it "returns valid Ghost::Host when searching for host name" do
117
- Ghost::Host.find_by_host('abc.local').should be_instance_of(Ghost::Host)
118
- end
119
- end
120
-
121
- describe ".add" do
122
- it "returns Ghost::Host object when passed hostname" do
123
- Ghost::Host.add('ghost-test-hostname.local').should be_instance_of(Ghost::Host)
124
- end
125
-
126
- it "returns Ghost::Host object when passed hostname" do
127
- Ghost::Host.add('ghost-test-hostname.local', '10.0.0.2').should be_instance_of(Ghost::Host)
128
- end
129
-
130
- it "raises error if hostname already exists and not add a duplicate" do
131
- Ghost::Host.empty!
132
- Ghost::Host.add('ghost-test-hostname.local')
133
- lambda { Ghost::Host.add('ghost-test-hostname.local') }.should raise_error
134
- Ghost::Host.list.should have(1).thing
135
- end
136
-
137
- it "overwrites existing hostname if forced" do
138
- hostname = 'ghost-test-hostname.local'
139
-
140
- Ghost::Host.empty!
141
- Ghost::Host.add(hostname)
142
-
143
- Ghost::Host.list.first.hostname.should eql(hostname)
144
- Ghost::Host.list.first.ip.should eql('127.0.0.1')
145
-
146
- Ghost::Host.add(hostname, '10.0.0.1', true)
147
- Ghost::Host.list.first.hostname.should eql(hostname)
148
- Ghost::Host.list.first.ip.should eql('10.0.0.1')
149
-
150
- Ghost::Host.list.should have(1).thing
151
- end
152
-
153
- it "should add a hostname using second hostname's ip" do
154
- hostname = 'ghost-test-hostname.local'
155
- alias_hostname = 'ghost-test-alias-hostname.local'
156
-
157
- Ghost::Host.empty!
158
-
159
- Ghost::Host.add(hostname)
160
- Ghost::Host.add(alias_hostname, hostname)
161
-
162
- Ghost::Host.list.last.ip.should eql(Ghost::Host.list.first.ip)
163
- end
164
-
165
- it "should raise SocketError if it can't find hostname's ip" do
166
- Ghost::Host.empty!
167
- lambda { Ghost::Host.add('ghost-test-alias-hostname.google', 'google') }.should raise_error(SocketError)
168
- end
169
- end
170
-
171
- describe ".empty!" do
172
- it "empties the hostnames" do
173
- Ghost::Host.add('ghost-test-hostname.local') # add a hostname to be sure
174
- Ghost::Host.empty!
175
- Ghost::Host.list.should have(0).things
176
- end
177
- end
178
-
179
- describe ".delete_matching" do
180
- it "deletes matching hostnames" do
181
- keep = 'ghost-test-hostname-keep.local'
182
- Ghost::Host.add(keep)
183
- Ghost::Host.add('ghost-test-hostname-match1.local')
184
- Ghost::Host.add('ghost-test-hostname-match2.local')
185
- Ghost::Host.delete_matching('match')
186
- Ghost::Host.list.should have(1).thing
187
- Ghost::Host.list.first.hostname.should eql(keep)
188
- end
189
- end
190
- end