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
@@ -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