blob: 29ccb41fe1688578a076a59d6ce380de097a4c71 [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 )
214
Alex Savatieievd79dde12019-03-13 19:07:46 -0500215 # get the gateway for current net
216 _routes = self.nodes[hostname]['routes']
217 _route = _routes[_net] if _net in _routes else None
Alex Savatieievefa79c42019-03-14 19:14:04 -0500218 if not _route:
219 _gate = "no route!"
220 else:
Alex3ebc5632019-04-18 16:47:18 -0500221 _gate = _route['gateway'] if _route['gateway'] else "-"
222
Alex Savatieievd79dde12019-03-13 19:07:46 -0500223 # get the default gateway
224 if 'default' in _routes:
225 _d_gate = ipaddress.IPv4Address(
226 _routes['default']['gateway']
227 )
228 else:
229 _d_gate = None
230 _d_gate_str = _d_gate if _d_gate else "No default gateway!"
231
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600232 _a = self.all_nets[network][hostname]
Alex3ebc5632019-04-18 16:47:18 -0500233 for _host in _a:
234 for _if in _host['ifs']:
235 # get proper reclass
236 _ip_str = str(_if.exploded)
237 _r = {}
238 for _item in self.reclass_nets[network][hostname]:
239 for _item_ifs in _item['ifs']:
240 if _ip_str == str(_item_ifs.exploded):
241 _r = _item
Alex Savatieievd79dde12019-03-13 19:07:46 -0500242
Alex3ebc5632019-04-18 16:47:18 -0500243 # check if node is UP
244 if not self.is_node_available(hostname):
245 _r_gate = "-"
246 # get proper network from reclass
247 else:
248 # Lookup match for the ip
249 _r_gate = "no IF in reclass!"
250 # get all networks with this hostname
251 _rn = self.reclass_nets
252 _nets = filter(
253 lambda n: hostname in _rn[n].keys(),
254 self.reclass_nets
255 )
256 _rd = None
257 for _item in _nets:
258 # match ip
259 _r_dat = self.reclass_nets[_item][hostname]
260 for _r_ifs in _r_dat:
261 for _r_if in _r_ifs['ifs']:
262 if _if.ip == _r_if.ip:
263 _rd = _r_ifs
264 break
265 if _rd:
266 _gs = 'gateway'
267 _e = "empty"
268 _r_gate = _rd[_gs] if _gs in _rd else _e
269 break
270
271 # IF status in reclass
272 if 'enabled' not in _r:
273 _enabled = "no record!"
274 else:
275 _e = "enabled"
276 _d = "disabled"
277 _enabled = "("+_e+")" if _r[_e] else "("+_d+")"
278
279 _name = _host['name']
280 _rc_mtu = _r['mtu'] if 'mtu' in _r else None
281
282 # Check if this is a VIP
283 if _if.network.prefixlen == 32:
284 _name = " "*20
285 _ip_str += " VIP"
286 _rc_mtu = "(-)"
287 _enabled = "(-)"
288 _r_gate = "-"
289
290 # Check if this is a default MTU
291 elif _host['mtu'] == '1500':
292 # reclass is empty if MTU is untended to be 1500
293 _rc_mtu = "(-)"
294 elif _rc_mtu:
295 # if there is an MTU value, match it
296 if _host['mtu'] != str(_rc_mtu):
297 self.errors.add_error(
298 self.errors.NET_MTU_MISMATCH,
299 host=hostname,
300 if_name=_name,
301 if_cidr=_ip_str,
302 reclass_mtu=_rc_mtu,
303 runtime_mtu=_host['mtu']
304 )
305 else:
306 # there is no MTU value in reclass
307 self.errors.add_error(
308 self.errors.NET_MTU_EMPTY,
309 host=hostname,
310 if_name=_name,
311 if_cidr=_ip_str,
312 if_mtu=_host['mtu']
313 )
314
Alexa05f74e2019-04-22 10:57:25 -0500315 _text = "{0:25} {1:19} {2:5}{3:10} {4:4}{5:10} " \
316 "{6} / {7} / {8}".format(
317 _name,
318 _ip_str,
319 _host['mtu'],
320 str(_rc_mtu) if _rc_mtu else "(No!)",
321 _host['state'],
322 _enabled,
323 _gate,
324 _d_gate_str,
325 _r_gate
326 )
Alex3ebc5632019-04-18 16:47:18 -0500327 logger_cli.info(
328 " {0:17} {1}".format(
329 hostname.split('.')[0],
330 _text
331 )
332 )
333
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600334 logger_cli.info("\n# Other networks")
335 _other = [n for n in _all_nets if n not in self.reclass_nets]
336 for network in _other:
337 logger_cli.info("-> {}".format(str(network)))
338 names = sorted(self.all_nets[network].keys())
339
340 for hostname in names:
Alex3ebc5632019-04-18 16:47:18 -0500341 for _n in self.all_nets[network][hostname]:
342 _ifs = [str(ifs.ip) for ifs in _n['ifs']]
343 _text = "{0:25}: {1:19} {2:5} {3:4}".format(
344 _n['name'],
345 ", ".join(_ifs),
346 _n['mtu'],
347 _n['state']
348 )
349 logger_cli.info(
350 " {0:17} {1}".format(hostname.split('.')[0], _text)
351 )
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600352
Alex3ebc5632019-04-18 16:47:18 -0500353 def print_summary(self):
354 _total_errors = self.errors.get_errors_total()
355 # Summary
356 logger_cli.info(
357 "\n{:=^8s}\n{:^8s}\n{:=^8s}".format(
358 "=",
359 "Totals",
360 "="
361 )
362 )
363 logger_cli.info(self.errors.get_summary(print_zeros=False))
364 logger_cli.info('-'*20)
365 logger_cli.info("{:5d} total errors found\n".format(_total_errors))
366
367 def print_error_details(self):
368 # Detailed errors
369 if self.errors.get_errors_total() > 0:
370 logger_cli.info("\n# Errors")
371 for _msg in self.errors.get_errors_as_list():
372 logger_cli.info("{}\n".format(_msg))
373 else:
374 logger_cli.info("-> No errors\n")
375
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600376 def create_html_report(self, filename):
377 """
378 Create static html showing network schema-like report
379
380 :return: none
381 """
382 logger_cli.info("### Generating report to '{}'".format(filename))
383 _report = reporter.ReportToFile(
384 reporter.HTMLNetworkReport(),
385 filename
386 )
387 _report({
388 "nodes": self.nodes,
Alex41485522019-04-12 17:26:18 -0500389 "network": {},
390 "mcp_release": self.mcp_release,
391 "openstack_release": self.openstack_release
392
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600393 })
394 logger_cli.info("-> Done")