blob: aeb1e61eed0baaf25c6d254dc8d9f0307e5cf43d [file] [log] [blame]
Alex Savatieieve9613992019-02-21 18:20:35 -06001import ipaddress
Alex3ebc5632019-04-18 16:47:18 -05002import json
Alex Savatieiev9b2f6512019-02-20 18:05:00 -06003
Alex Savatieiev9b2f6512019-02-20 18:05:00 -06004
Alex3ebc5632019-04-18 16:47:18 -05005from cfg_checker.common import logger_cli
6from cfg_checker.modules.network.network_errors import NetworkErrors
7from cfg_checker.nodes import SaltNodes
Alex Savatieievf526dc02019-03-06 10:11:32 -06008from cfg_checker.reports import reporter
Alex Savatieiev9b2f6512019-02-20 18:05:00 -06009
10
11class NetworkChecker(SaltNodes):
Alex3ebc5632019-04-18 16:47:18 -050012 def __init__(self):
13 super(NetworkChecker, self).__init__()
14 self.errors = NetworkErrors()
15
16 # adding net data to tree
17 def _add_data(self, _list, _n, _h, _d):
18 if _n not in _list:
19 _list[_n] = {}
20 _list[_n][_h] = [_d]
21 elif _h not in _list[_n]:
22 # there is no such host, just create it
23 _list[_n][_h] = [_d]
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -060024 else:
Alex3ebc5632019-04-18 16:47:18 -050025 # there is such host... this is an error
26 self.errors.add_error(
27 self.errors.NET_DUPLICATE_IF,
28 host=_h,
29 dup_if=_d['name']
30 )
31 _list[_n][_h].append(_d)
32
33 # TODO: refactor map creation. Build one map instead of two separate
34 def _map_network_for_host(self, host, if_class, net_list, data):
35 # filter networks for this IF IP
36 _nets = [n for n in net_list.keys() if if_class.ip in n]
37 _masks = [n.netmask for n in _nets]
38 if len(_nets) > 1:
39 # There a multiple network found for this IP, Error
40 self.errors.add_error(
41 self.errors.NET_SUBNET_INTERSECT,
42 host=host,
43 networks="; ".join(_nets)
44 )
45 # check mask match
46 if len(_nets) > 0 and if_class.netmask not in _masks:
47 self.errors.add_error(
48 self.errors.NET_MASK_MISMATCH,
49 host=host,
50 if_name=data['name'],
51 if_cidr=if_class.exploded,
52 if_mapped_networks=", ".join([str(_n) for _n in _nets])
53 )
54
55 if len(_nets) < 1:
56 self._add_data(net_list, if_class.network, host, data)
57 else:
58 # add all data
59 for net in _nets:
60 self._add_data(net_list, net, host, data)
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -060061
62 return net_list
63
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060064 def collect_network_info(self):
65 """
66 Collects info on the network using ifs_data.py script
67
68 :return: none
69 """
Alex Savatieiev42b89fa2019-03-07 18:45:26 -060070 logger_cli.info("# Mapping node runtime network data")
Alex3ebc5632019-04-18 16:47:18 -050071 _result = self.execute_script_on_active_nodes(
72 "ifs_data.py",
73 args=["json"]
74 )
75 self.stage = "Runtime"
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060076 for key in self.nodes.keys():
Alex Savatieievefa79c42019-03-14 19:14:04 -050077 # check if we are to work with this node
78 if not self.is_node_available(key):
79 continue
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060080 # due to much data to be passed from salt, it is happening in order
81 if key in _result:
82 _text = _result[key]
83 _dict = json.loads(_text[_text.find('{'):])
Alex Savatieievd79dde12019-03-13 19:07:46 -050084 self.nodes[key]['routes'] = _dict.pop("routes")
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060085 self.nodes[key]['networks'] = _dict
86 else:
87 self.nodes[key]['networks'] = {}
Alex Savatieievd79dde12019-03-13 19:07:46 -050088 self.nodes[key]['routes'] = {}
Alex Savatieiev42b89fa2019-03-07 18:45:26 -060089 logger_cli.debug("... {} has {} networks".format(
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060090 key,
91 len(self.nodes[key]['networks'].keys())
92 ))
Alex Savatieievf808cd22019-03-01 13:17:59 -060093 logger_cli.info("-> done collecting networks data")
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060094
Alex3ebc5632019-04-18 16:47:18 -050095 # TODO: Mimic reclass structure for easy compare
Alex Savatieieve9613992019-02-21 18:20:35 -060096 logger_cli.info("### Building network tree")
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -060097 # match interfaces by IP subnets
Alex Savatieieve9613992019-02-21 18:20:35 -060098 _all_nets = {}
Alex Savatieieva05921f2019-02-21 18:21:39 -060099 for host, node_data in self.nodes.iteritems():
Alex Savatieievefa79c42019-03-14 19:14:04 -0500100 if not self.is_node_available(host):
101 continue
102
Alex Savatieieve9613992019-02-21 18:20:35 -0600103 for net_name, net_data in node_data['networks'].iteritems():
104 # get ips and calculate subnets
Alex Savatieievd79dde12019-03-13 19:07:46 -0500105 if net_name in ['lo']:
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600106 # skip the localhost
Alex Savatieieve9613992019-02-21 18:20:35 -0600107 continue
Alex3ebc5632019-04-18 16:47:18 -0500108 # get data and make sure that wide mask goes first
109 _ip4s = sorted(
110 net_data['ipv4'],
111 key=lambda s: s[s.index('/'):]
112 )
113 for _ip_str in _ip4s:
114 # create interface class
Alex Savatieieve9613992019-02-21 18:20:35 -0600115 _if = ipaddress.IPv4Interface(_ip_str)
Alex3ebc5632019-04-18 16:47:18 -0500116 if 'name' not in net_data:
117 net_data['name'] = net_name
118 if 'ifs' not in net_data:
119 net_data['ifs'] = [_if]
120 # map it
121 _all_nets = self._map_network_for_host(
122 host,
123 _if,
124 _all_nets,
125 net_data
126 )
127 else:
128 # data is already there, just add VIP
129 net_data['ifs'].append(_if)
Alex Savatieieve9613992019-02-21 18:20:35 -0600130
131 # save collected info
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600132 self.all_nets = _all_nets
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600133
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600134 def collect_reclass_networks(self):
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600135 logger_cli.info("# Mapping reclass networks")
Alex3ebc5632019-04-18 16:47:18 -0500136 self.stage = "Reclass"
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600137 # Get networks from reclass and mark them
138 _reclass_nets = {}
139 # Get required pillars
140 self.get_specific_pillar_for_nodes("linux:network")
141 for node in self.nodes.keys():
Alex Savatieievefa79c42019-03-14 19:14:04 -0500142 # check if this node
143 if not self.is_node_available(node):
144 continue
145 # get the reclass value
146 _pillar = self.nodes[node]['pillars']['linux']['network']
Alex Savatieieva1f6f8c2019-03-18 17:13:55 -0500147 # we should be ready if there is no interface in reclass for a node
148 # for example on APT node
149 if 'interface' in _pillar:
150 _pillar = _pillar['interface']
151 else:
Alex3ebc5632019-04-18 16:47:18 -0500152 logger_cli.info(
153 "... node '{}' skipped, no IF section in reclass".format(
154 node
155 )
156 )
Alex Savatieieva1f6f8c2019-03-18 17:13:55 -0500157 continue
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600158 for _if_name, _if_data in _pillar.iteritems():
159 if 'address' in _if_data:
160 _if = ipaddress.IPv4Interface(
161 _if_data['address'] + '/' + _if_data['netmask']
162 )
163 _if_data['name'] = _if_name
Alex3ebc5632019-04-18 16:47:18 -0500164 _if_data['ifs'] = [_if]
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600165
166 _reclass_nets = self._map_network_for_host(
167 node,
168 _if,
169 _reclass_nets,
170 _if_data
171 )
172
173 self.reclass_nets = _reclass_nets
174
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600175 def print_network_report(self):
176 """
177 Create text report for CLI
178
179 :return: none
180 """
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600181 _all_nets = self.all_nets.keys()
182 logger_cli.info("# Reclass networks")
Alex Savatieievefa79c42019-03-14 19:14:04 -0500183 logger_cli.info(
Alex3ebc5632019-04-18 16:47:18 -0500184 " {0:17} {1:25}: "
185 "{2:19} {3:5}{4:10} {5}{6} {7} / {8} / {9}".format(
Alex Savatieievefa79c42019-03-14 19:14:04 -0500186 "Hostname",
187 "IF",
188 "IP",
189 "rtMTU",
190 "rcMTU",
191 "rtState",
192 "rcState",
193 "rtGate",
194 "rtDef.Gate",
195 "rcGate"
196 )
Alex Savatieievd79dde12019-03-13 19:07:46 -0500197 )
Alex3ebc5632019-04-18 16:47:18 -0500198 # TODO: Move matching to separate function
199 self.stage = "Matching"
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600200 _reclass = [n for n in _all_nets if n in self.reclass_nets]
201 for network in _reclass:
202 # shortcuts
Alex Savatieievd79dde12019-03-13 19:07:46 -0500203 _net = str(network)
204 logger_cli.info("-> {}".format(_net))
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600205 names = sorted(self.all_nets[network].keys())
Alex Savatieieve9613992019-02-21 18:20:35 -0600206 for hostname in names:
Alex Savatieievefa79c42019-03-14 19:14:04 -0500207 if not self.is_node_available(hostname, log=False):
Alex3ebc5632019-04-18 16:47:18 -0500208 logger_cli.info(
Alex Savatieievefa79c42019-03-14 19:14:04 -0500209 " {0:17} {1}".format(
210 hostname.split('.')[0],
211 "... no data for the node"
212 )
213 )
Alexec688e02019-04-22 12:05:06 -0500214 # add non-responsive node erorr
215 self.errors.add_error(
216 self.errors.NET_NODE_NON_RESPONSIVE,
217 host=hostname
218 )
219
220 # print empty row
221 _text = " # node non-responsive"
222 logger_cli.info(
223 " {0:17} {1}".format(
224 hostname.split('.')[0],
225 _text
226 )
227 )
228 continue
Alex Savatieievefa79c42019-03-14 19:14:04 -0500229
Alex Savatieievd79dde12019-03-13 19:07:46 -0500230 # get the gateway for current net
231 _routes = self.nodes[hostname]['routes']
232 _route = _routes[_net] if _net in _routes else None
Alex Savatieievefa79c42019-03-14 19:14:04 -0500233 if not _route:
234 _gate = "no route!"
235 else:
Alex3ebc5632019-04-18 16:47:18 -0500236 _gate = _route['gateway'] if _route['gateway'] else "-"
237
Alex Savatieievd79dde12019-03-13 19:07:46 -0500238 # get the default gateway
239 if 'default' in _routes:
240 _d_gate = ipaddress.IPv4Address(
241 _routes['default']['gateway']
242 )
243 else:
244 _d_gate = None
245 _d_gate_str = _d_gate if _d_gate else "No default gateway!"
246
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600247 _a = self.all_nets[network][hostname]
Alex3ebc5632019-04-18 16:47:18 -0500248 for _host in _a:
249 for _if in _host['ifs']:
250 # get proper reclass
251 _ip_str = str(_if.exploded)
252 _r = {}
Alexec688e02019-04-22 12:05:06 -0500253 if hostname in self.reclass_nets[network]:
254 for _item in self.reclass_nets[network][hostname]:
255 for _item_ifs in _item['ifs']:
256 if _ip_str == str(_item_ifs.exploded):
257 _r = _item
258 else:
259 self.errors.add_error(
260 self.errors.NET_NODE_UNEXPECTED_IF,
261 host=hostname,
262 if_name=_host['name'],
263 if_ip=_ip_str
264 )
Alex Savatieievd79dde12019-03-13 19:07:46 -0500265
Alex3ebc5632019-04-18 16:47:18 -0500266 # check if node is UP
267 if not self.is_node_available(hostname):
268 _r_gate = "-"
269 # get proper network from reclass
270 else:
271 # Lookup match for the ip
272 _r_gate = "no IF in reclass!"
273 # get all networks with this hostname
274 _rn = self.reclass_nets
275 _nets = filter(
276 lambda n: hostname in _rn[n].keys(),
277 self.reclass_nets
278 )
279 _rd = None
280 for _item in _nets:
281 # match ip
282 _r_dat = self.reclass_nets[_item][hostname]
283 for _r_ifs in _r_dat:
284 for _r_if in _r_ifs['ifs']:
285 if _if.ip == _r_if.ip:
286 _rd = _r_ifs
287 break
288 if _rd:
289 _gs = 'gateway'
290 _e = "empty"
291 _r_gate = _rd[_gs] if _gs in _rd else _e
292 break
293
294 # IF status in reclass
295 if 'enabled' not in _r:
Alexec688e02019-04-22 12:05:06 -0500296 _enabled = "(no record!)"
Alex3ebc5632019-04-18 16:47:18 -0500297 else:
298 _e = "enabled"
299 _d = "disabled"
300 _enabled = "("+_e+")" if _r[_e] else "("+_d+")"
301
302 _name = _host['name']
303 _rc_mtu = _r['mtu'] if 'mtu' in _r else None
304
305 # Check if this is a VIP
306 if _if.network.prefixlen == 32:
307 _name = " "*20
308 _ip_str += " VIP"
309 _rc_mtu = "(-)"
310 _enabled = "(-)"
311 _r_gate = "-"
312
313 # Check if this is a default MTU
314 elif _host['mtu'] == '1500':
315 # reclass is empty if MTU is untended to be 1500
316 _rc_mtu = "(-)"
317 elif _rc_mtu:
Alexec688e02019-04-22 12:05:06 -0500318 _rc_mtu_s = str(_rc_mtu)
Alex3ebc5632019-04-18 16:47:18 -0500319 # if there is an MTU value, match it
Alexec688e02019-04-22 12:05:06 -0500320 if _host['mtu'] != _rc_mtu_s:
Alex3ebc5632019-04-18 16:47:18 -0500321 self.errors.add_error(
322 self.errors.NET_MTU_MISMATCH,
323 host=hostname,
324 if_name=_name,
325 if_cidr=_ip_str,
326 reclass_mtu=_rc_mtu,
327 runtime_mtu=_host['mtu']
328 )
329 else:
330 # there is no MTU value in reclass
331 self.errors.add_error(
332 self.errors.NET_MTU_EMPTY,
333 host=hostname,
334 if_name=_name,
335 if_cidr=_ip_str,
336 if_mtu=_host['mtu']
337 )
338
Alexa05f74e2019-04-22 10:57:25 -0500339 _text = "{0:25} {1:19} {2:5}{3:10} {4:4}{5:10} " \
340 "{6} / {7} / {8}".format(
341 _name,
342 _ip_str,
343 _host['mtu'],
Alexec688e02019-04-22 12:05:06 -0500344 "("+_rc_mtu_s+")" if _rc_mtu else "(No!)",
Alexa05f74e2019-04-22 10:57:25 -0500345 _host['state'],
346 _enabled,
347 _gate,
348 _d_gate_str,
349 _r_gate
350 )
Alex3ebc5632019-04-18 16:47:18 -0500351 logger_cli.info(
352 " {0:17} {1}".format(
353 hostname.split('.')[0],
354 _text
355 )
356 )
357
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600358 logger_cli.info("\n# Other networks")
359 _other = [n for n in _all_nets if n not in self.reclass_nets]
360 for network in _other:
361 logger_cli.info("-> {}".format(str(network)))
362 names = sorted(self.all_nets[network].keys())
363
364 for hostname in names:
Alex3ebc5632019-04-18 16:47:18 -0500365 for _n in self.all_nets[network][hostname]:
366 _ifs = [str(ifs.ip) for ifs in _n['ifs']]
367 _text = "{0:25}: {1:19} {2:5} {3:4}".format(
368 _n['name'],
369 ", ".join(_ifs),
370 _n['mtu'],
371 _n['state']
372 )
373 logger_cli.info(
374 " {0:17} {1}".format(hostname.split('.')[0], _text)
375 )
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600376
Alex3ebc5632019-04-18 16:47:18 -0500377 def print_summary(self):
378 _total_errors = self.errors.get_errors_total()
379 # Summary
380 logger_cli.info(
381 "\n{:=^8s}\n{:^8s}\n{:=^8s}".format(
382 "=",
383 "Totals",
384 "="
385 )
386 )
387 logger_cli.info(self.errors.get_summary(print_zeros=False))
388 logger_cli.info('-'*20)
389 logger_cli.info("{:5d} total errors found\n".format(_total_errors))
390
391 def print_error_details(self):
392 # Detailed errors
393 if self.errors.get_errors_total() > 0:
394 logger_cli.info("\n# Errors")
395 for _msg in self.errors.get_errors_as_list():
396 logger_cli.info("{}\n".format(_msg))
397 else:
398 logger_cli.info("-> No errors\n")
399
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600400 def create_html_report(self, filename):
401 """
402 Create static html showing network schema-like report
403
404 :return: none
405 """
406 logger_cli.info("### Generating report to '{}'".format(filename))
407 _report = reporter.ReportToFile(
408 reporter.HTMLNetworkReport(),
409 filename
410 )
411 _report({
412 "nodes": self.nodes,
Alex41485522019-04-12 17:26:18 -0500413 "network": {},
414 "mcp_release": self.mcp_release,
415 "openstack_release": self.openstack_release
416
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600417 })
418 logger_cli.info("-> Done")