blob: 48e7acbe5d79e47e1aa1103e0614619814aa5c03 [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):
Alexb151fbe2019-04-22 16:53:30 -050013 logger_cli.info("# Gathering environment information")
Alex3ebc5632019-04-18 16:47:18 -050014 super(NetworkChecker, self).__init__()
Alexb151fbe2019-04-22 16:53:30 -050015 logger_cli.info("# Initializing error logs folder")
Alex3ebc5632019-04-18 16:47:18 -050016 self.errors = NetworkErrors()
17
18 # adding net data to tree
19 def _add_data(self, _list, _n, _h, _d):
20 if _n not in _list:
21 _list[_n] = {}
22 _list[_n][_h] = [_d]
23 elif _h not in _list[_n]:
24 # there is no such host, just create it
25 _list[_n][_h] = [_d]
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -060026 else:
Alex3ebc5632019-04-18 16:47:18 -050027 # there is such host... this is an error
28 self.errors.add_error(
29 self.errors.NET_DUPLICATE_IF,
30 host=_h,
31 dup_if=_d['name']
32 )
33 _list[_n][_h].append(_d)
34
35 # TODO: refactor map creation. Build one map instead of two separate
36 def _map_network_for_host(self, host, if_class, net_list, data):
37 # filter networks for this IF IP
38 _nets = [n for n in net_list.keys() if if_class.ip in n]
39 _masks = [n.netmask for n in _nets]
40 if len(_nets) > 1:
41 # There a multiple network found for this IP, Error
42 self.errors.add_error(
43 self.errors.NET_SUBNET_INTERSECT,
44 host=host,
45 networks="; ".join(_nets)
46 )
47 # check mask match
48 if len(_nets) > 0 and if_class.netmask not in _masks:
49 self.errors.add_error(
50 self.errors.NET_MASK_MISMATCH,
51 host=host,
52 if_name=data['name'],
53 if_cidr=if_class.exploded,
54 if_mapped_networks=", ".join([str(_n) for _n in _nets])
55 )
56
57 if len(_nets) < 1:
58 self._add_data(net_list, if_class.network, host, data)
59 else:
60 # add all data
61 for net in _nets:
62 self._add_data(net_list, net, host, data)
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -060063
64 return net_list
65
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060066 def collect_network_info(self):
67 """
68 Collects info on the network using ifs_data.py script
69
70 :return: none
71 """
Alex Savatieiev42b89fa2019-03-07 18:45:26 -060072 logger_cli.info("# Mapping node runtime network data")
Alex3ebc5632019-04-18 16:47:18 -050073 _result = self.execute_script_on_active_nodes(
74 "ifs_data.py",
75 args=["json"]
76 )
77 self.stage = "Runtime"
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060078 for key in self.nodes.keys():
Alex Savatieievefa79c42019-03-14 19:14:04 -050079 # check if we are to work with this node
80 if not self.is_node_available(key):
81 continue
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060082 # due to much data to be passed from salt, it is happening in order
83 if key in _result:
84 _text = _result[key]
85 _dict = json.loads(_text[_text.find('{'):])
Alex Savatieievd79dde12019-03-13 19:07:46 -050086 self.nodes[key]['routes'] = _dict.pop("routes")
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060087 self.nodes[key]['networks'] = _dict
88 else:
89 self.nodes[key]['networks'] = {}
Alex Savatieievd79dde12019-03-13 19:07:46 -050090 self.nodes[key]['routes'] = {}
Alex Savatieiev42b89fa2019-03-07 18:45:26 -060091 logger_cli.debug("... {} has {} networks".format(
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060092 key,
93 len(self.nodes[key]['networks'].keys())
94 ))
Alex Savatieievf808cd22019-03-01 13:17:59 -060095 logger_cli.info("-> done collecting networks data")
Alex Savatieiev9b2f6512019-02-20 18:05:00 -060096
Alex3ebc5632019-04-18 16:47:18 -050097 # TODO: Mimic reclass structure for easy compare
Alex Savatieieve9613992019-02-21 18:20:35 -060098 logger_cli.info("### Building network tree")
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -060099 # match interfaces by IP subnets
Alex Savatieieve9613992019-02-21 18:20:35 -0600100 _all_nets = {}
Alex Savatieieva05921f2019-02-21 18:21:39 -0600101 for host, node_data in self.nodes.iteritems():
Alex Savatieievefa79c42019-03-14 19:14:04 -0500102 if not self.is_node_available(host):
103 continue
104
Alex Savatieieve9613992019-02-21 18:20:35 -0600105 for net_name, net_data in node_data['networks'].iteritems():
106 # get ips and calculate subnets
Alex Savatieievd79dde12019-03-13 19:07:46 -0500107 if net_name in ['lo']:
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600108 # skip the localhost
Alex Savatieieve9613992019-02-21 18:20:35 -0600109 continue
Alex3ebc5632019-04-18 16:47:18 -0500110 # get data and make sure that wide mask goes first
111 _ip4s = sorted(
112 net_data['ipv4'],
113 key=lambda s: s[s.index('/'):]
114 )
115 for _ip_str in _ip4s:
116 # create interface class
Alex Savatieieve9613992019-02-21 18:20:35 -0600117 _if = ipaddress.IPv4Interface(_ip_str)
Alex3ebc5632019-04-18 16:47:18 -0500118 if 'name' not in net_data:
119 net_data['name'] = net_name
120 if 'ifs' not in net_data:
121 net_data['ifs'] = [_if]
122 # map it
123 _all_nets = self._map_network_for_host(
124 host,
125 _if,
126 _all_nets,
127 net_data
128 )
129 else:
130 # data is already there, just add VIP
131 net_data['ifs'].append(_if)
Alex Savatieieve9613992019-02-21 18:20:35 -0600132
133 # save collected info
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600134 self.all_nets = _all_nets
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600135
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600136 def collect_reclass_networks(self):
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600137 logger_cli.info("# Mapping reclass networks")
Alex3ebc5632019-04-18 16:47:18 -0500138 self.stage = "Reclass"
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600139 # Get networks from reclass and mark them
140 _reclass_nets = {}
141 # Get required pillars
142 self.get_specific_pillar_for_nodes("linux:network")
143 for node in self.nodes.keys():
Alex Savatieievefa79c42019-03-14 19:14:04 -0500144 # check if this node
145 if not self.is_node_available(node):
146 continue
147 # get the reclass value
148 _pillar = self.nodes[node]['pillars']['linux']['network']
Alex Savatieieva1f6f8c2019-03-18 17:13:55 -0500149 # we should be ready if there is no interface in reclass for a node
150 # for example on APT node
151 if 'interface' in _pillar:
152 _pillar = _pillar['interface']
153 else:
Alex3ebc5632019-04-18 16:47:18 -0500154 logger_cli.info(
155 "... node '{}' skipped, no IF section in reclass".format(
156 node
157 )
158 )
Alex Savatieieva1f6f8c2019-03-18 17:13:55 -0500159 continue
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600160 for _if_name, _if_data in _pillar.iteritems():
161 if 'address' in _if_data:
162 _if = ipaddress.IPv4Interface(
163 _if_data['address'] + '/' + _if_data['netmask']
164 )
165 _if_data['name'] = _if_name
Alex3ebc5632019-04-18 16:47:18 -0500166 _if_data['ifs'] = [_if]
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600167
168 _reclass_nets = self._map_network_for_host(
169 node,
170 _if,
171 _reclass_nets,
172 _if_data
173 )
174
175 self.reclass_nets = _reclass_nets
176
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600177 def print_network_report(self):
178 """
179 Create text report for CLI
180
181 :return: none
182 """
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600183 _all_nets = self.all_nets.keys()
184 logger_cli.info("# Reclass networks")
Alex Savatieievefa79c42019-03-14 19:14:04 -0500185 logger_cli.info(
Alex3ebc5632019-04-18 16:47:18 -0500186 " {0:17} {1:25}: "
187 "{2:19} {3:5}{4:10} {5}{6} {7} / {8} / {9}".format(
Alex Savatieievefa79c42019-03-14 19:14:04 -0500188 "Hostname",
189 "IF",
190 "IP",
191 "rtMTU",
192 "rcMTU",
193 "rtState",
194 "rcState",
195 "rtGate",
196 "rtDef.Gate",
197 "rcGate"
198 )
Alex Savatieievd79dde12019-03-13 19:07:46 -0500199 )
Alex3ebc5632019-04-18 16:47:18 -0500200 # TODO: Move matching to separate function
201 self.stage = "Matching"
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600202 _reclass = [n for n in _all_nets if n in self.reclass_nets]
203 for network in _reclass:
204 # shortcuts
Alex Savatieievd79dde12019-03-13 19:07:46 -0500205 _net = str(network)
206 logger_cli.info("-> {}".format(_net))
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600207 names = sorted(self.all_nets[network].keys())
Alex Savatieieve9613992019-02-21 18:20:35 -0600208 for hostname in names:
Alex Savatieievefa79c42019-03-14 19:14:04 -0500209 if not self.is_node_available(hostname, log=False):
Alex3ebc5632019-04-18 16:47:18 -0500210 logger_cli.info(
Alex Savatieievefa79c42019-03-14 19:14:04 -0500211 " {0:17} {1}".format(
212 hostname.split('.')[0],
213 "... no data for the node"
214 )
215 )
Alexec688e02019-04-22 12:05:06 -0500216 # add non-responsive node erorr
217 self.errors.add_error(
218 self.errors.NET_NODE_NON_RESPONSIVE,
219 host=hostname
220 )
221
222 # print empty row
223 _text = " # node non-responsive"
224 logger_cli.info(
225 " {0:17} {1}".format(
226 hostname.split('.')[0],
227 _text
228 )
229 )
230 continue
Alex Savatieievefa79c42019-03-14 19:14:04 -0500231
Alex Savatieievd79dde12019-03-13 19:07:46 -0500232 # get the gateway for current net
233 _routes = self.nodes[hostname]['routes']
234 _route = _routes[_net] if _net in _routes else None
Alex Savatieievefa79c42019-03-14 19:14:04 -0500235 if not _route:
236 _gate = "no route!"
237 else:
Alex3ebc5632019-04-18 16:47:18 -0500238 _gate = _route['gateway'] if _route['gateway'] else "-"
239
Alex Savatieievd79dde12019-03-13 19:07:46 -0500240 # get the default gateway
241 if 'default' in _routes:
242 _d_gate = ipaddress.IPv4Address(
243 _routes['default']['gateway']
244 )
245 else:
246 _d_gate = None
247 _d_gate_str = _d_gate if _d_gate else "No default gateway!"
248
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600249 _a = self.all_nets[network][hostname]
Alex3ebc5632019-04-18 16:47:18 -0500250 for _host in _a:
251 for _if in _host['ifs']:
252 # get proper reclass
253 _ip_str = str(_if.exploded)
254 _r = {}
Alexec688e02019-04-22 12:05:06 -0500255 if hostname in self.reclass_nets[network]:
256 for _item in self.reclass_nets[network][hostname]:
257 for _item_ifs in _item['ifs']:
258 if _ip_str == str(_item_ifs.exploded):
259 _r = _item
260 else:
261 self.errors.add_error(
262 self.errors.NET_NODE_UNEXPECTED_IF,
263 host=hostname,
264 if_name=_host['name'],
265 if_ip=_ip_str
266 )
Alex Savatieievd79dde12019-03-13 19:07:46 -0500267
Alex3ebc5632019-04-18 16:47:18 -0500268 # check if node is UP
269 if not self.is_node_available(hostname):
270 _r_gate = "-"
271 # get proper network from reclass
272 else:
273 # Lookup match for the ip
274 _r_gate = "no IF in reclass!"
275 # get all networks with this hostname
276 _rn = self.reclass_nets
277 _nets = filter(
278 lambda n: hostname in _rn[n].keys(),
279 self.reclass_nets
280 )
281 _rd = None
282 for _item in _nets:
283 # match ip
284 _r_dat = self.reclass_nets[_item][hostname]
285 for _r_ifs in _r_dat:
286 for _r_if in _r_ifs['ifs']:
287 if _if.ip == _r_if.ip:
288 _rd = _r_ifs
289 break
290 if _rd:
291 _gs = 'gateway'
292 _e = "empty"
293 _r_gate = _rd[_gs] if _gs in _rd else _e
294 break
295
296 # IF status in reclass
297 if 'enabled' not in _r:
Alexec688e02019-04-22 12:05:06 -0500298 _enabled = "(no record!)"
Alex3ebc5632019-04-18 16:47:18 -0500299 else:
300 _e = "enabled"
301 _d = "disabled"
302 _enabled = "("+_e+")" if _r[_e] else "("+_d+")"
303
304 _name = _host['name']
305 _rc_mtu = _r['mtu'] if 'mtu' in _r else None
306
307 # Check if this is a VIP
308 if _if.network.prefixlen == 32:
309 _name = " "*20
310 _ip_str += " VIP"
311 _rc_mtu = "(-)"
312 _enabled = "(-)"
313 _r_gate = "-"
314
315 # Check if this is a default MTU
316 elif _host['mtu'] == '1500':
317 # reclass is empty if MTU is untended to be 1500
318 _rc_mtu = "(-)"
319 elif _rc_mtu:
Alexec688e02019-04-22 12:05:06 -0500320 _rc_mtu_s = str(_rc_mtu)
Alex3ebc5632019-04-18 16:47:18 -0500321 # if there is an MTU value, match it
Alexec688e02019-04-22 12:05:06 -0500322 if _host['mtu'] != _rc_mtu_s:
Alex3ebc5632019-04-18 16:47:18 -0500323 self.errors.add_error(
324 self.errors.NET_MTU_MISMATCH,
325 host=hostname,
326 if_name=_name,
327 if_cidr=_ip_str,
328 reclass_mtu=_rc_mtu,
329 runtime_mtu=_host['mtu']
330 )
331 else:
332 # there is no MTU value in reclass
333 self.errors.add_error(
334 self.errors.NET_MTU_EMPTY,
335 host=hostname,
336 if_name=_name,
337 if_cidr=_ip_str,
338 if_mtu=_host['mtu']
339 )
340
Alexa05f74e2019-04-22 10:57:25 -0500341 _text = "{0:25} {1:19} {2:5}{3:10} {4:4}{5:10} " \
342 "{6} / {7} / {8}".format(
343 _name,
344 _ip_str,
345 _host['mtu'],
Alexec688e02019-04-22 12:05:06 -0500346 "("+_rc_mtu_s+")" if _rc_mtu else "(No!)",
Alexa05f74e2019-04-22 10:57:25 -0500347 _host['state'],
348 _enabled,
349 _gate,
350 _d_gate_str,
351 _r_gate
352 )
Alex3ebc5632019-04-18 16:47:18 -0500353 logger_cli.info(
354 " {0:17} {1}".format(
355 hostname.split('.')[0],
356 _text
357 )
358 )
359
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600360 logger_cli.info("\n# Other networks")
361 _other = [n for n in _all_nets if n not in self.reclass_nets]
362 for network in _other:
363 logger_cli.info("-> {}".format(str(network)))
364 names = sorted(self.all_nets[network].keys())
365
366 for hostname in names:
Alex3ebc5632019-04-18 16:47:18 -0500367 for _n in self.all_nets[network][hostname]:
368 _ifs = [str(ifs.ip) for ifs in _n['ifs']]
369 _text = "{0:25}: {1:19} {2:5} {3:4}".format(
370 _n['name'],
371 ", ".join(_ifs),
372 _n['mtu'],
373 _n['state']
374 )
375 logger_cli.info(
376 " {0:17} {1}".format(hostname.split('.')[0], _text)
377 )
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600378
Alex3ebc5632019-04-18 16:47:18 -0500379 def print_summary(self):
Alex3ebc5632019-04-18 16:47:18 -0500380 logger_cli.info(self.errors.get_summary(print_zeros=False))
Alex3ebc5632019-04-18 16:47:18 -0500381
382 def print_error_details(self):
383 # Detailed errors
Alexb151fbe2019-04-22 16:53:30 -0500384 logger_cli.info(
385 "\n{}\n".format(
386 self.errors.get_errors()
387 )
388 )
Alex3ebc5632019-04-18 16:47:18 -0500389
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600390 def create_html_report(self, filename):
391 """
392 Create static html showing network schema-like report
393
394 :return: none
395 """
396 logger_cli.info("### Generating report to '{}'".format(filename))
397 _report = reporter.ReportToFile(
398 reporter.HTMLNetworkReport(),
399 filename
400 )
401 _report({
402 "nodes": self.nodes,
Alex41485522019-04-12 17:26:18 -0500403 "network": {},
404 "mcp_release": self.mcp_release,
405 "openstack_release": self.openstack_release
406
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600407 })
408 logger_cli.info("-> Done")