-
Notifications
You must be signed in to change notification settings - Fork 34
/
hosts.rb
200 lines (180 loc) · 5.78 KB
/
hosts.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
require "yast"
require "yast2/target_file"
require "cfa/base_model"
require "cfa/matcher"
require "cfa/augeas_parser"
module CFA
# class representings /etc/hosts file model. It provides helper to manipulate
# with file. It uses CFA framework and Augeas parser.
# @see http://www.rubydoc.info/github/config-files-api/config_files_api/CFA/BaseModel
# @see http://www.rubydoc.info/github/config-files-api/config_files_api/CFA/AugeasParser
class Hosts < BaseModel
PARSER = AugeasParser.new("hosts.lns")
PATH = "/etc/hosts".freeze
include Yast::Logger
def initialize(file_handler: nil)
super(PARSER, PATH, file_handler: file_handler)
end
# The old format used by {Yast::HostClass}.
# @return [Hash{String => Array<String>}] keys are IPs,
# values are lists of lines in /etc/hosts (not names!)
# with whitespace separated hostnames, where the first one is canonical
# and the rest are aliases
#
# For example, the file contents
#
# 1.2.3.4 www.example.org www
# 1.2.3.7 log.example.org log
# 1.2.3.7 sql.example.org sql
#
# is returned as
#
# {
# "1.2.3.4" => "www.example.org www"
# "1.2.3.7" => [
# "log.example.org log",
# "sql.example.org sql"
# ]
# }
def hosts
matcher = Matcher.new { |k, _v| k =~ /^\d*$/ }
data.select(matcher).each_with_object({}) do |host, result|
entry = host[:value]
result[entry["ipaddr"]] ||= []
result[entry["ipaddr"]] << single_host_entry(entry)
end
end
# Returns single entry from hosts for given ip or empty array if not found
# @see #hosts
# @return [Array<String>]
def host(ip)
hosts = data.select(ip_matcher(ip))
hosts.map do |host|
single_host_entry(host[:value])
end
end
# deletes all occurences of given ip in host table
# @return [void]
def delete_by_ip(ip)
entries = data.select(ip_matcher(ip))
if entries.empty?
log.info "no entry to delete for ip #{ip}"
return
end
if entries.size > 1
log.info "delete host with ip '#{ip}' removes more then one entry"
end
entries.each do |e|
log.info "deleting record #{e.inspect}"
data.delete(e[:key])
end
end
# Replaces or adds a new host entry.
# If more than one entry with the given ip exists
# then it replaces the last instance.
# @param [String] ip
# @param [String] canonical
# @param [Array<String>] aliases
# @return [void]
def set_entry(ip, canonical, aliases = [])
entries = data.select(ip_matcher(ip))
if entries.empty?
add_entry(ip, canonical, aliases)
return
end
if entries.size > 1
log.info "more then one entry with ip '#{ip}'. Replacing last one."
end
entry = entries.last[:value]
entry["ipaddr"] = ip
entry["canonical"] = canonical
# clear previous aliases
entry.delete("alias")
entry.delete("alias[]")
aliases_col = entry.collection("alias")
aliases.each do |a|
aliases_col.add(a)
end
end
# Adds new entry, even if it exists
# @param [String] ip
# @param [String] canonical
# @param [Array<String>] aliases
# @return [void]
def add_entry(ip, canonical, aliases = [])
log.info "adding new entry for ip #{ip}"
entry_line = AugeasTree.new
entry_line["ipaddr"] = ip
entry_line["canonical"] = canonical
aliases_col = entry_line.collection("alias")
aliases.each do |a|
aliases_col.add(a)
end
data.add(unique_id, entry_line)
end
# Removes hostname from all entries in hosts table.
# If it is the only hostname for a given ip, the ip is removed
# If it is canonical name, then the first alias becomes the canonical hostname
# @param [String] hostname
# @return [void]
def delete_hostname(hostname)
entries = data.select(hostname_matcher(hostname))
entries.each do |pair|
entry = pair[:value]
if entry["canonical"] == hostname
aliases = aliases_for(entry)
if aliases.empty?
delete_host(entry["ipaddr"])
else
entry["canonical"] = aliases.first
entry.delete("alias")
entry.delete("alias[]")
aliases_col = entry.collection("alias")
aliases[1..-1].each do |a|
aliases_col.add(a)
end
end
else
reduced_aliases = aliases_for(entry)
reduced_aliases.delete(hostname)
entry.delete("alias")
entry.delete("alias[]")
aliases_col = entry.collection("alias")
aliases[1..-1].each do |a|
aliases_col.add(a)
end
end
end
end
private
# returns matcher for cfa to find entries with given ip
def ip_matcher(ip)
Matcher.new { |_k, v| v["ipaddr"] == ip }
end
# returns matcher for cfa to find entries with given hostname
def hostname_matcher(hostname)
Matcher.new do |_k, v|
v["canonical"] == hostname || aliases_for(v).include?(hostname)
end
end
# returns aliases as array even if there is only one
def aliases_for(entry)
entry["alias[]"] ? entry.collection("alias").map { |a| a } : [entry["alias"]].compact
end
# generate old format string with first canonical and then aliases
# all separated by space
def single_host_entry(entry)
result = [entry["canonical"]]
result.concat(aliases_for(entry))
result.join(" ")
end
# helper to generate unique id for cfa entry
def unique_id
id = 1
loop do
return id.to_s unless data[id.to_s]
id += 1
end
end
end
end