blob: c44775f7270e149eab31796ce1226d8ecedee18e [file] [log] [blame]
Alexe0c5b9e2019-04-23 18:51:23 -05001import ipaddress
2import json
3
4from cfg_checker.common import logger_cli
5from cfg_checker.common.exception import InvalidReturnException
6from cfg_checker.modules.network.network_errors import NetworkErrors
7from cfg_checker.nodes import salt_master
8
9# TODO: use templated approach
10# net interface structure should be the same
11_if_item = {
12 "name": "unnamed interface",
13 "mac": "",
14 "routes": {},
15 "ip": [],
16 "parameters": {}
17}
18
19# collection of configurations
20_network_item = {
21 "runtime": {},
22 "config": {},
23 "reclass": {}
24}
25
26
27class NetworkMapper(object):
28 RECLASS = "reclass"
29 CONFIG = "config"
30 RUNTIME = "runtime"
31
32 def __init__(self, errors_class=None):
33 logger_cli.info("# Initializing mapper")
34 self.networks = {}
35 self.nodes = salt_master.get_nodes()
36 if errors_class:
37 self.errors = errors_class
38 else:
39 logger_cli.debug("... init error logs folder")
40 self.errors = NetworkErrors()
41
42 # adding net data to tree
43 def _add_data(self, _list, _n, _h, _d):
44 if _n not in _list:
45 _list[_n] = {}
46 _list[_n][_h] = [_d]
47 elif _h not in _list[_n]:
48 # there is no such host, just create it
49 _list[_n][_h] = [_d]
50 else:
51 # there is such host... this is an error
52 self.errors.add_error(
53 self.errors.NET_DUPLICATE_IF,
54 host=_h,
55 dup_if=_d['name']
56 )
57 _list[_n][_h].append(_d)
58
59 # TODO: refactor map creation. Build one map instead of two separate
60 def _map_network_for_host(self, host, if_class, net_list, data):
61 # filter networks for this IF IP
62 _nets = [n for n in net_list.keys() if if_class.ip in n]
63 _masks = [n.netmask for n in _nets]
64 if len(_nets) > 1:
65 # There a multiple network found for this IP, Error
66 self.errors.add_error(
67 self.errors.NET_SUBNET_INTERSECT,
68 host=host,
69 ip=str(if_class.exploded),
70 networks="; ".join([str(_n) for _n in _nets])
71 )
72 # check mask match
73 if len(_nets) > 0 and if_class.netmask not in _masks:
74 self.errors.add_error(
75 self.errors.NET_MASK_MISMATCH,
76 host=host,
77 if_name=data['name'],
78 if_cidr=if_class.exploded,
79 if_mapped_networks=", ".join([str(_n) for _n in _nets])
80 )
81
82 if len(_nets) < 1:
83 self._add_data(net_list, if_class.network, host, data)
84 else:
85 # add all data
86 for net in _nets:
87 self._add_data(net_list, net, host, data)
88
89 return net_list
90
91 def _map_reclass_networks(self):
92 # class uses nodes from self.nodes dict
93 _reclass = {}
94 # Get required pillars
95 salt_master.get_specific_pillar_for_nodes("linux:network")
96 for node in salt_master.nodes.keys():
97 # check if this node
98 if not salt_master.is_node_available(node):
99 continue
100 # get the reclass value
101 _pillar = salt_master.nodes[node]['pillars']['linux']['network']
102 # we should be ready if there is no interface in reclass for a node
103 # for example on APT nohde
104 if 'interface' in _pillar:
105 _pillar = _pillar['interface']
106 else:
107 logger_cli.info(
108 "... node '{}' skipped, no IF section in reclass".format(
109 node
110 )
111 )
112 continue
113 for _if_name, _if_data in _pillar.iteritems():
114 if 'address' in _if_data:
115 _if = ipaddress.IPv4Interface(
116 _if_data['address'] + '/' + _if_data['netmask']
117 )
118 _if_data['name'] = _if_name
119 _if_data['ifs'] = [_if]
120
121 _reclass = self._map_network_for_host(
122 node,
123 _if,
124 _reclass,
125 _if_data
126 )
127
128 return _reclass
129
130 def _map_configured_networks(self):
131 # class uses nodes from self.nodes dict
132 _confs = {}
133
134 return _confs
135
136 def _map_runtime_networks(self):
137 # class uses nodes from self.nodes dict
138 _runtime = {}
139 logger_cli.info("# Mapping node runtime network data")
140 salt_master.prepare_script_on_active_nodes("ifs_data.py")
141 _result = salt_master.execute_script_on_active_nodes(
142 "ifs_data.py",
143 args=["json"]
144 )
145 for key in salt_master.nodes.keys():
146 # check if we are to work with this node
147 if not salt_master.is_node_available(key):
148 continue
149 # due to much data to be passed from salt_master,
150 # it is happening in order
151 if key in _result:
152 _text = _result[key]
153 if '{' in _text and '}' in _text:
154 _text = _text[_text.find('{'):]
155 else:
156 raise InvalidReturnException(
157 "Non-json object returned: '{}'".format(
158 _text
159 )
160 )
161 _dict = json.loads(_text[_text.find('{'):])
162 salt_master.nodes[key]['routes'] = _dict.pop("routes")
163 salt_master.nodes[key]['networks'] = _dict
164 else:
165 salt_master.nodes[key]['networks'] = {}
166 salt_master.nodes[key]['routes'] = {}
167 logger_cli.debug("... {} has {} networks".format(
168 key,
169 len(salt_master.nodes[key]['networks'].keys())
170 ))
171 logger_cli.info("-> done collecting networks data")
172
173 logger_cli.info("-> mapping IPs")
174 # match interfaces by IP subnets
175 for host, node_data in salt_master.nodes.iteritems():
176 if not salt_master.is_node_available(host):
177 continue
178
179 for net_name, net_data in node_data['networks'].iteritems():
180 # get ips and calculate subnets
181 if net_name in ['lo']:
182 # skip the localhost
183 continue
184 # get data and make sure that wide mask goes first
185 _ip4s = sorted(
186 net_data['ipv4'],
187 key=lambda s: s[s.index('/'):]
188 )
189 for _ip_str in _ip4s:
190 # create interface class
191 _if = ipaddress.IPv4Interface(_ip_str)
192 # check if this is a VIP
193 # ...all those will have /32 mask
194 net_data['vip'] = None
195 if _if.network.prefixlen == 32:
196 net_data['vip'] = str(_if.exploded)
197 if 'name' not in net_data:
198 net_data['name'] = net_name
199 if 'ifs' not in net_data:
200 net_data['ifs'] = [_if]
201 # map it
202 _runtime = self._map_network_for_host(
203 host,
204 _if,
205 _runtime,
206 net_data
207 )
208 else:
209 # data is already there, just add VIP
210 net_data['ifs'].append(_if)
211
212 return _runtime
213
214 def map_network(self, source):
215 # maps target network using given source
216 _networks = None
217
218 if source == self.RECLASS:
219 _networks = self._map_reclass_networks()
220 elif source == self.CONFIG:
221 _networks = self._map_configured_networks()
222 elif source == self.RUNTIME:
223 _networks = self._map_runtime_networks()
224
225 self.networks[source] = _networks
226 return _networks
227
228 def print_map(self):
229 """
230 Create text report for CLI
231
232 :return: none
233 """
234 _runtime = self.networks[self.RUNTIME]
235 _reclass = self.networks[self.RECLASS]
236 logger_cli.info("# Reclass networks")
237 logger_cli.info(
238 " {0:17} {1:25}: "
239 "{2:19} {3:5}{4:10} {5}{6} {7} / {8} / {9}".format(
240 "Hostname",
241 "IF",
242 "IP",
243 "rtMTU",
244 "rcMTU",
245 "rtState",
246 "rcState",
247 "rtGate",
248 "rtDef.Gate",
249 "rcGate"
250 )
251 )
252 for network in _reclass:
253 # shortcuts
254 _net = str(network)
255 logger_cli.info("-> {}".format(_net))
256 if network not in _runtime:
257 # reclass has network that not found in runtime
258 self.errors.add_error(
259 self.errors.NET_NO_RUNTIME_NETWORK,
260 reclass_net=str(network)
261 )
262 logger_cli.info(" {:-^50}".format(" No runtime network "))
263 continue
264 names = sorted(_runtime[network].keys())
265 for hostname in names:
266 if not salt_master.is_node_available(hostname, log=False):
267 logger_cli.info(
268 " {0:17} {1}".format(
269 hostname.split('.')[0],
270 "... no data for the node"
271 )
272 )
273 # add non-responsive node erorr
274 self.errors.add_error(
275 self.errors.NET_NODE_NON_RESPONSIVE,
276 host=hostname
277 )
278
279 # print empty row
280 _text = " # node non-responsive"
281 logger_cli.info(
282 " {0:17} {1}".format(
283 hostname.split('.')[0],
284 _text
285 )
286 )
287 continue
288
289 # get the gateway for current net
290 _routes = salt_master.nodes[hostname]['routes']
291 _route = _routes[_net] if _net in _routes else None
292 if not _route:
293 _gate = "no route!"
294 else:
295 _gate = _route['gateway'] if _route['gateway'] else "-"
296
297 # get the default gateway
298 if 'default' in _routes:
299 _d_gate = ipaddress.IPv4Address(
300 _routes['default']['gateway']
301 )
302 else:
303 _d_gate = None
304 _d_gate_str = _d_gate if _d_gate else "No default gateway!"
305
306 _a = _runtime[network][hostname]
307 for _host in _a:
308 for _if in _host['ifs']:
309 # get proper reclass
310 _ip_str = str(_if.exploded)
311 _r = {}
312 if hostname in _reclass[network]:
313 for _item in _reclass[network][hostname]:
314 for _item_ifs in _item['ifs']:
315 if _ip_str == str(_item_ifs.exploded):
316 _r = _item
317 else:
318 self.errors.add_error(
319 self.errors.NET_NODE_UNEXPECTED_IF,
320 host=hostname,
321 if_name=_host['name'],
322 if_ip=_ip_str
323 )
324
325 # check if node is UP
326 if not salt_master.is_node_available(hostname):
327 _r_gate = "-"
328 # get proper network from reclass
329 else:
330 # Lookup match for the ip
331 _r_gate = "no IF in reclass!"
332 # get all networks with this hostname
333 _nets = filter(
334 lambda n: hostname in _reclass[n].keys(),
335 _reclass
336 )
337 _rd = None
338 for _item in _nets:
339 # match ip
340 _r_dat = _reclass[_item][hostname]
341 for _r_ifs in _r_dat:
342 for _r_if in _r_ifs['ifs']:
343 if _if.ip == _r_if.ip:
344 _rd = _r_ifs
345 break
346 if _rd:
347 _gs = 'gateway'
348 _e = "empty"
349 _r_gate = _rd[_gs] if _gs in _rd else _e
350 break
351
352 # IF status in reclass
353 if 'enabled' not in _r:
354 _enabled = "(no record!)"
355 else:
356 _e = "enabled"
357 _d = "disabled"
358 _enabled = "("+_e+")" if _r[_e] else "("+_d+")"
359
360 _name = _host['name']
361 _rc_mtu = _r['mtu'] if 'mtu' in _r else None
362 _rc_mtu_s = str(_rc_mtu) if _rc_mtu else '(-)'
363 # check if this is a VIP address
364 # no checks needed if yes.
365 if _host['vip'] != _ip_str:
366 if _rc_mtu:
367 # if there is an MTU value, match it
368 if _host['mtu'] != _rc_mtu_s:
369 self.errors.add_error(
370 self.errors.NET_MTU_MISMATCH,
371 host=hostname,
372 if_name=_name,
373 if_cidr=_ip_str,
374 reclass_mtu=_rc_mtu,
375 runtime_mtu=_host['mtu']
376 )
377 elif _host['mtu'] != '1500':
378 # there is no MTU value in reclass
379 # and runtime value is not default
380 self.errors.add_error(
381 self.errors.NET_MTU_EMPTY,
382 host=hostname,
383 if_name=_name,
384 if_cidr=_ip_str,
385 if_mtu=_host['mtu']
386 )
387 else:
388 # this is a VIP
389 _name = " "*20
390 _ip_str += " VIP"
391 _enabled = "(-)"
392 _r_gate = "-"
393
394 _text = "{0:25} {1:19} {2:5}{3:10} {4:4}{5:10} " \
395 "{6} / {7} / {8}".format(
396 _name,
397 _ip_str,
398 _host['mtu'],
399 _rc_mtu_s,
400 _host['state'],
401 _enabled,
402 _gate,
403 _d_gate_str,
404 _r_gate
405 )
406 logger_cli.info(
407 " {0:17} {1}".format(
408 hostname.split('.')[0],
409 _text
410 )
411 )
412
413 logger_cli.info("\n# Other networks")
414 _other = [n for n in _runtime if n not in _reclass]
415 for network in _other:
416 logger_cli.info("-> {}".format(str(network)))
417 names = sorted(_runtime[network].keys())
418
419 for hostname in names:
420 for _n in _runtime[network][hostname]:
421 _ifs = [str(ifs.ip) for ifs in _n['ifs']]
422 _text = "{0:25}: {1:19} {2:5} {3:4}".format(
423 _n['name'],
424 ", ".join(_ifs),
425 _n['mtu'],
426 _n['state']
427 )
428 logger_cli.info(
429 " {0:17} {1}".format(hostname.split('.')[0], _text)
430 )