blob: 08cc99fcb43f2ad0f1a9525fa281d7aad91ecc36 [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
Alex92e07ce2019-05-31 16:00:03 -0500103 # for example on APT node
Alexe0c5b9e2019-04-23 18:51:23 -0500104 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
Alex92e07ce2019-05-31 16:00:03 -0500113
114 # build map based on IPs
Alexe0c5b9e2019-04-23 18:51:23 -0500115 for _if_name, _if_data in _pillar.iteritems():
116 if 'address' in _if_data:
117 _if = ipaddress.IPv4Interface(
118 _if_data['address'] + '/' + _if_data['netmask']
119 )
120 _if_data['name'] = _if_name
121 _if_data['ifs'] = [_if]
122
123 _reclass = self._map_network_for_host(
124 node,
125 _if,
126 _reclass,
127 _if_data
128 )
129
130 return _reclass
131
132 def _map_configured_networks(self):
133 # class uses nodes from self.nodes dict
134 _confs = {}
135
Alex92e07ce2019-05-31 16:00:03 -0500136 # TODO: parse /etc/network/interfaces
137
Alexe0c5b9e2019-04-23 18:51:23 -0500138 return _confs
139
140 def _map_runtime_networks(self):
141 # class uses nodes from self.nodes dict
142 _runtime = {}
143 logger_cli.info("# Mapping node runtime network data")
144 salt_master.prepare_script_on_active_nodes("ifs_data.py")
145 _result = salt_master.execute_script_on_active_nodes(
146 "ifs_data.py",
147 args=["json"]
148 )
149 for key in salt_master.nodes.keys():
150 # check if we are to work with this node
151 if not salt_master.is_node_available(key):
152 continue
153 # due to much data to be passed from salt_master,
154 # it is happening in order
155 if key in _result:
156 _text = _result[key]
157 if '{' in _text and '}' in _text:
158 _text = _text[_text.find('{'):]
159 else:
160 raise InvalidReturnException(
161 "Non-json object returned: '{}'".format(
162 _text
163 )
164 )
165 _dict = json.loads(_text[_text.find('{'):])
166 salt_master.nodes[key]['routes'] = _dict.pop("routes")
167 salt_master.nodes[key]['networks'] = _dict
168 else:
169 salt_master.nodes[key]['networks'] = {}
170 salt_master.nodes[key]['routes'] = {}
171 logger_cli.debug("... {} has {} networks".format(
172 key,
173 len(salt_master.nodes[key]['networks'].keys())
174 ))
175 logger_cli.info("-> done collecting networks data")
176
177 logger_cli.info("-> mapping IPs")
178 # match interfaces by IP subnets
179 for host, node_data in salt_master.nodes.iteritems():
180 if not salt_master.is_node_available(host):
181 continue
182
183 for net_name, net_data in node_data['networks'].iteritems():
184 # get ips and calculate subnets
185 if net_name in ['lo']:
186 # skip the localhost
187 continue
188 # get data and make sure that wide mask goes first
189 _ip4s = sorted(
190 net_data['ipv4'],
191 key=lambda s: s[s.index('/'):]
192 )
193 for _ip_str in _ip4s:
194 # create interface class
195 _if = ipaddress.IPv4Interface(_ip_str)
196 # check if this is a VIP
197 # ...all those will have /32 mask
198 net_data['vip'] = None
199 if _if.network.prefixlen == 32:
200 net_data['vip'] = str(_if.exploded)
201 if 'name' not in net_data:
202 net_data['name'] = net_name
203 if 'ifs' not in net_data:
204 net_data['ifs'] = [_if]
205 # map it
206 _runtime = self._map_network_for_host(
207 host,
208 _if,
209 _runtime,
210 net_data
211 )
212 else:
213 # data is already there, just add VIP
214 net_data['ifs'].append(_if)
215
216 return _runtime
217
218 def map_network(self, source):
219 # maps target network using given source
220 _networks = None
221
222 if source == self.RECLASS:
223 _networks = self._map_reclass_networks()
224 elif source == self.CONFIG:
225 _networks = self._map_configured_networks()
226 elif source == self.RUNTIME:
227 _networks = self._map_runtime_networks()
228
229 self.networks[source] = _networks
230 return _networks
231
232 def print_map(self):
233 """
234 Create text report for CLI
235
236 :return: none
237 """
238 _runtime = self.networks[self.RUNTIME]
239 _reclass = self.networks[self.RECLASS]
240 logger_cli.info("# Reclass networks")
241 logger_cli.info(
242 " {0:17} {1:25}: "
243 "{2:19} {3:5}{4:10} {5}{6} {7} / {8} / {9}".format(
244 "Hostname",
245 "IF",
246 "IP",
247 "rtMTU",
248 "rcMTU",
249 "rtState",
250 "rcState",
251 "rtGate",
252 "rtDef.Gate",
253 "rcGate"
254 )
255 )
256 for network in _reclass:
257 # shortcuts
258 _net = str(network)
259 logger_cli.info("-> {}".format(_net))
260 if network not in _runtime:
261 # reclass has network that not found in runtime
262 self.errors.add_error(
263 self.errors.NET_NO_RUNTIME_NETWORK,
264 reclass_net=str(network)
265 )
266 logger_cli.info(" {:-^50}".format(" No runtime network "))
267 continue
268 names = sorted(_runtime[network].keys())
269 for hostname in names:
270 if not salt_master.is_node_available(hostname, log=False):
271 logger_cli.info(
272 " {0:17} {1}".format(
273 hostname.split('.')[0],
274 "... no data for the node"
275 )
276 )
277 # add non-responsive node erorr
278 self.errors.add_error(
279 self.errors.NET_NODE_NON_RESPONSIVE,
280 host=hostname
281 )
282
283 # print empty row
284 _text = " # node non-responsive"
285 logger_cli.info(
286 " {0:17} {1}".format(
287 hostname.split('.')[0],
288 _text
289 )
290 )
291 continue
292
293 # get the gateway for current net
294 _routes = salt_master.nodes[hostname]['routes']
295 _route = _routes[_net] if _net in _routes else None
296 if not _route:
297 _gate = "no route!"
298 else:
299 _gate = _route['gateway'] if _route['gateway'] else "-"
300
301 # get the default gateway
302 if 'default' in _routes:
303 _d_gate = ipaddress.IPv4Address(
304 _routes['default']['gateway']
305 )
306 else:
307 _d_gate = None
308 _d_gate_str = _d_gate if _d_gate else "No default gateway!"
309
310 _a = _runtime[network][hostname]
311 for _host in _a:
312 for _if in _host['ifs']:
313 # get proper reclass
314 _ip_str = str(_if.exploded)
315 _r = {}
316 if hostname in _reclass[network]:
317 for _item in _reclass[network][hostname]:
318 for _item_ifs in _item['ifs']:
319 if _ip_str == str(_item_ifs.exploded):
320 _r = _item
321 else:
322 self.errors.add_error(
323 self.errors.NET_NODE_UNEXPECTED_IF,
324 host=hostname,
325 if_name=_host['name'],
326 if_ip=_ip_str
327 )
328
329 # check if node is UP
330 if not salt_master.is_node_available(hostname):
331 _r_gate = "-"
332 # get proper network from reclass
333 else:
334 # Lookup match for the ip
335 _r_gate = "no IF in reclass!"
336 # get all networks with this hostname
337 _nets = filter(
338 lambda n: hostname in _reclass[n].keys(),
339 _reclass
340 )
341 _rd = None
342 for _item in _nets:
343 # match ip
344 _r_dat = _reclass[_item][hostname]
345 for _r_ifs in _r_dat:
346 for _r_if in _r_ifs['ifs']:
347 if _if.ip == _r_if.ip:
348 _rd = _r_ifs
349 break
350 if _rd:
351 _gs = 'gateway'
352 _e = "empty"
353 _r_gate = _rd[_gs] if _gs in _rd else _e
354 break
355
356 # IF status in reclass
357 if 'enabled' not in _r:
358 _enabled = "(no record!)"
359 else:
360 _e = "enabled"
361 _d = "disabled"
362 _enabled = "("+_e+")" if _r[_e] else "("+_d+")"
363
364 _name = _host['name']
365 _rc_mtu = _r['mtu'] if 'mtu' in _r else None
366 _rc_mtu_s = str(_rc_mtu) if _rc_mtu else '(-)'
367 # check if this is a VIP address
368 # no checks needed if yes.
369 if _host['vip'] != _ip_str:
370 if _rc_mtu:
371 # if there is an MTU value, match it
372 if _host['mtu'] != _rc_mtu_s:
373 self.errors.add_error(
374 self.errors.NET_MTU_MISMATCH,
375 host=hostname,
376 if_name=_name,
377 if_cidr=_ip_str,
378 reclass_mtu=_rc_mtu,
379 runtime_mtu=_host['mtu']
380 )
381 elif _host['mtu'] != '1500':
382 # there is no MTU value in reclass
383 # and runtime value is not default
384 self.errors.add_error(
385 self.errors.NET_MTU_EMPTY,
386 host=hostname,
387 if_name=_name,
388 if_cidr=_ip_str,
389 if_mtu=_host['mtu']
390 )
391 else:
392 # this is a VIP
393 _name = " "*20
394 _ip_str += " VIP"
395 _enabled = "(-)"
396 _r_gate = "-"
397
398 _text = "{0:25} {1:19} {2:5}{3:10} {4:4}{5:10} " \
399 "{6} / {7} / {8}".format(
400 _name,
401 _ip_str,
402 _host['mtu'],
403 _rc_mtu_s,
404 _host['state'],
405 _enabled,
406 _gate,
407 _d_gate_str,
408 _r_gate
409 )
410 logger_cli.info(
411 " {0:17} {1}".format(
412 hostname.split('.')[0],
413 _text
414 )
415 )
416
417 logger_cli.info("\n# Other networks")
418 _other = [n for n in _runtime if n not in _reclass]
419 for network in _other:
420 logger_cli.info("-> {}".format(str(network)))
421 names = sorted(_runtime[network].keys())
422
423 for hostname in names:
424 for _n in _runtime[network][hostname]:
425 _ifs = [str(ifs.ip) for ifs in _n['ifs']]
426 _text = "{0:25}: {1:19} {2:5} {3:4}".format(
427 _n['name'],
428 ", ".join(_ifs),
429 _n['mtu'],
430 _n['state']
431 )
432 logger_cli.info(
433 " {0:17} {1}".format(hostname.split('.')[0], _text)
434 )