blob: 7e6a23957edce55f6933bcd405cfb444c0ed7201 [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)
Alexbab1efe2019-04-23 18:51:23 -0500118 # check if this is a VIP
119 # ...all those will have /32 mask
120 net_data['vip'] = None
121 if _if.network.prefixlen == 32:
122 net_data['vip'] = str(_if.exploded)
Alex3ebc5632019-04-18 16:47:18 -0500123 if 'name' not in net_data:
124 net_data['name'] = net_name
125 if 'ifs' not in net_data:
126 net_data['ifs'] = [_if]
127 # map it
128 _all_nets = self._map_network_for_host(
129 host,
130 _if,
131 _all_nets,
132 net_data
133 )
134 else:
135 # data is already there, just add VIP
136 net_data['ifs'].append(_if)
Alex Savatieieve9613992019-02-21 18:20:35 -0600137
138 # save collected info
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600139 self.all_nets = _all_nets
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600140
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600141 def collect_reclass_networks(self):
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600142 logger_cli.info("# Mapping reclass networks")
Alex3ebc5632019-04-18 16:47:18 -0500143 self.stage = "Reclass"
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600144 # Get networks from reclass and mark them
145 _reclass_nets = {}
146 # Get required pillars
147 self.get_specific_pillar_for_nodes("linux:network")
148 for node in self.nodes.keys():
Alex Savatieievefa79c42019-03-14 19:14:04 -0500149 # check if this node
150 if not self.is_node_available(node):
151 continue
152 # get the reclass value
153 _pillar = self.nodes[node]['pillars']['linux']['network']
Alex Savatieieva1f6f8c2019-03-18 17:13:55 -0500154 # we should be ready if there is no interface in reclass for a node
155 # for example on APT node
156 if 'interface' in _pillar:
157 _pillar = _pillar['interface']
158 else:
Alex3ebc5632019-04-18 16:47:18 -0500159 logger_cli.info(
160 "... node '{}' skipped, no IF section in reclass".format(
161 node
162 )
163 )
Alex Savatieieva1f6f8c2019-03-18 17:13:55 -0500164 continue
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600165 for _if_name, _if_data in _pillar.iteritems():
166 if 'address' in _if_data:
167 _if = ipaddress.IPv4Interface(
168 _if_data['address'] + '/' + _if_data['netmask']
169 )
170 _if_data['name'] = _if_name
Alex3ebc5632019-04-18 16:47:18 -0500171 _if_data['ifs'] = [_if]
Alex Savatieiev01f0d7f2019-03-07 17:53:29 -0600172
173 _reclass_nets = self._map_network_for_host(
174 node,
175 _if,
176 _reclass_nets,
177 _if_data
178 )
179
180 self.reclass_nets = _reclass_nets
181
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600182 def print_network_report(self):
183 """
184 Create text report for CLI
185
186 :return: none
187 """
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600188 _all_nets = self.all_nets.keys()
189 logger_cli.info("# Reclass networks")
Alex Savatieievefa79c42019-03-14 19:14:04 -0500190 logger_cli.info(
Alex3ebc5632019-04-18 16:47:18 -0500191 " {0:17} {1:25}: "
192 "{2:19} {3:5}{4:10} {5}{6} {7} / {8} / {9}".format(
Alex Savatieievefa79c42019-03-14 19:14:04 -0500193 "Hostname",
194 "IF",
195 "IP",
196 "rtMTU",
197 "rcMTU",
198 "rtState",
199 "rcState",
200 "rtGate",
201 "rtDef.Gate",
202 "rcGate"
203 )
Alex Savatieievd79dde12019-03-13 19:07:46 -0500204 )
Alex3ebc5632019-04-18 16:47:18 -0500205 # TODO: Move matching to separate function
206 self.stage = "Matching"
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600207 _reclass = [n for n in _all_nets if n in self.reclass_nets]
208 for network in _reclass:
209 # shortcuts
Alex Savatieievd79dde12019-03-13 19:07:46 -0500210 _net = str(network)
211 logger_cli.info("-> {}".format(_net))
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600212 names = sorted(self.all_nets[network].keys())
Alex Savatieieve9613992019-02-21 18:20:35 -0600213 for hostname in names:
Alex Savatieievefa79c42019-03-14 19:14:04 -0500214 if not self.is_node_available(hostname, log=False):
Alex3ebc5632019-04-18 16:47:18 -0500215 logger_cli.info(
Alex Savatieievefa79c42019-03-14 19:14:04 -0500216 " {0:17} {1}".format(
217 hostname.split('.')[0],
218 "... no data for the node"
219 )
220 )
Alexec688e02019-04-22 12:05:06 -0500221 # add non-responsive node erorr
222 self.errors.add_error(
223 self.errors.NET_NODE_NON_RESPONSIVE,
224 host=hostname
225 )
226
227 # print empty row
228 _text = " # node non-responsive"
229 logger_cli.info(
230 " {0:17} {1}".format(
231 hostname.split('.')[0],
232 _text
233 )
234 )
235 continue
Alex Savatieievefa79c42019-03-14 19:14:04 -0500236
Alex Savatieievd79dde12019-03-13 19:07:46 -0500237 # get the gateway for current net
238 _routes = self.nodes[hostname]['routes']
239 _route = _routes[_net] if _net in _routes else None
Alex Savatieievefa79c42019-03-14 19:14:04 -0500240 if not _route:
241 _gate = "no route!"
242 else:
Alex3ebc5632019-04-18 16:47:18 -0500243 _gate = _route['gateway'] if _route['gateway'] else "-"
244
Alex Savatieievd79dde12019-03-13 19:07:46 -0500245 # get the default gateway
246 if 'default' in _routes:
247 _d_gate = ipaddress.IPv4Address(
248 _routes['default']['gateway']
249 )
250 else:
251 _d_gate = None
252 _d_gate_str = _d_gate if _d_gate else "No default gateway!"
253
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600254 _a = self.all_nets[network][hostname]
Alex3ebc5632019-04-18 16:47:18 -0500255 for _host in _a:
256 for _if in _host['ifs']:
257 # get proper reclass
258 _ip_str = str(_if.exploded)
259 _r = {}
Alexec688e02019-04-22 12:05:06 -0500260 if hostname in self.reclass_nets[network]:
261 for _item in self.reclass_nets[network][hostname]:
262 for _item_ifs in _item['ifs']:
263 if _ip_str == str(_item_ifs.exploded):
264 _r = _item
265 else:
266 self.errors.add_error(
267 self.errors.NET_NODE_UNEXPECTED_IF,
268 host=hostname,
269 if_name=_host['name'],
270 if_ip=_ip_str
271 )
Alex Savatieievd79dde12019-03-13 19:07:46 -0500272
Alex3ebc5632019-04-18 16:47:18 -0500273 # check if node is UP
274 if not self.is_node_available(hostname):
275 _r_gate = "-"
276 # get proper network from reclass
277 else:
278 # Lookup match for the ip
279 _r_gate = "no IF in reclass!"
280 # get all networks with this hostname
281 _rn = self.reclass_nets
282 _nets = filter(
283 lambda n: hostname in _rn[n].keys(),
284 self.reclass_nets
285 )
286 _rd = None
287 for _item in _nets:
288 # match ip
289 _r_dat = self.reclass_nets[_item][hostname]
290 for _r_ifs in _r_dat:
291 for _r_if in _r_ifs['ifs']:
292 if _if.ip == _r_if.ip:
293 _rd = _r_ifs
294 break
295 if _rd:
296 _gs = 'gateway'
297 _e = "empty"
298 _r_gate = _rd[_gs] if _gs in _rd else _e
299 break
300
301 # IF status in reclass
302 if 'enabled' not in _r:
Alexec688e02019-04-22 12:05:06 -0500303 _enabled = "(no record!)"
Alex3ebc5632019-04-18 16:47:18 -0500304 else:
305 _e = "enabled"
306 _d = "disabled"
307 _enabled = "("+_e+")" if _r[_e] else "("+_d+")"
308
309 _name = _host['name']
310 _rc_mtu = _r['mtu'] if 'mtu' in _r else None
Alexbab1efe2019-04-23 18:51:23 -0500311 _rc_mtu_s = str(_rc_mtu) if _rc_mtu else '(-)'
312 # check if this is a VIP address
313 # no checks needed if yes.
314 if _host['vip'] != _ip_str:
315 if _rc_mtu:
316 # if there is an MTU value, match it
317 if _host['mtu'] != _rc_mtu_s:
318 self.errors.add_error(
319 self.errors.NET_MTU_MISMATCH,
320 host=hostname,
321 if_name=_name,
322 if_cidr=_ip_str,
323 reclass_mtu=_rc_mtu,
324 runtime_mtu=_host['mtu']
325 )
326 elif _host['mtu'] != '1500':
327 # there is no MTU value in reclass
328 # and runtime value is not default
Alex3ebc5632019-04-18 16:47:18 -0500329 self.errors.add_error(
Alexbab1efe2019-04-23 18:51:23 -0500330 self.errors.NET_MTU_EMPTY,
Alex3ebc5632019-04-18 16:47:18 -0500331 host=hostname,
332 if_name=_name,
333 if_cidr=_ip_str,
Alexbab1efe2019-04-23 18:51:23 -0500334 if_mtu=_host['mtu']
Alex3ebc5632019-04-18 16:47:18 -0500335 )
336 else:
Alexbab1efe2019-04-23 18:51:23 -0500337 # this is a VIP
338 _name = " "*20
339 _ip_str += " VIP"
340 _enabled = "(-)"
341 _r_gate = "-"
Alex3ebc5632019-04-18 16:47:18 -0500342
Alexa05f74e2019-04-22 10:57:25 -0500343 _text = "{0:25} {1:19} {2:5}{3:10} {4:4}{5:10} " \
344 "{6} / {7} / {8}".format(
345 _name,
346 _ip_str,
347 _host['mtu'],
Alexbab1efe2019-04-23 18:51:23 -0500348 _rc_mtu_s,
Alexa05f74e2019-04-22 10:57:25 -0500349 _host['state'],
350 _enabled,
351 _gate,
352 _d_gate_str,
353 _r_gate
354 )
Alex3ebc5632019-04-18 16:47:18 -0500355 logger_cli.info(
356 " {0:17} {1}".format(
357 hostname.split('.')[0],
358 _text
359 )
360 )
361
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600362 logger_cli.info("\n# Other networks")
363 _other = [n for n in _all_nets if n not in self.reclass_nets]
364 for network in _other:
365 logger_cli.info("-> {}".format(str(network)))
366 names = sorted(self.all_nets[network].keys())
367
368 for hostname in names:
Alex3ebc5632019-04-18 16:47:18 -0500369 for _n in self.all_nets[network][hostname]:
370 _ifs = [str(ifs.ip) for ifs in _n['ifs']]
371 _text = "{0:25}: {1:19} {2:5} {3:4}".format(
372 _n['name'],
373 ", ".join(_ifs),
374 _n['mtu'],
375 _n['state']
376 )
377 logger_cli.info(
378 " {0:17} {1}".format(hostname.split('.')[0], _text)
379 )
Alex Savatieiev42b89fa2019-03-07 18:45:26 -0600380
Alex3ebc5632019-04-18 16:47:18 -0500381 def print_summary(self):
Alex3ebc5632019-04-18 16:47:18 -0500382 logger_cli.info(self.errors.get_summary(print_zeros=False))
Alex3ebc5632019-04-18 16:47:18 -0500383
384 def print_error_details(self):
385 # Detailed errors
Alexb151fbe2019-04-22 16:53:30 -0500386 logger_cli.info(
387 "\n{}\n".format(
388 self.errors.get_errors()
389 )
390 )
Alex3ebc5632019-04-18 16:47:18 -0500391
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600392 def create_html_report(self, filename):
393 """
394 Create static html showing network schema-like report
395
396 :return: none
397 """
398 logger_cli.info("### Generating report to '{}'".format(filename))
399 _report = reporter.ReportToFile(
400 reporter.HTMLNetworkReport(),
401 filename
402 )
403 _report({
404 "nodes": self.nodes,
Alex41485522019-04-12 17:26:18 -0500405 "network": {},
406 "mcp_release": self.mcp_release,
407 "openstack_release": self.openstack_release
408
Alex Savatieiev9b2f6512019-02-20 18:05:00 -0600409 })
410 logger_cli.info("-> Done")