blob: 02c5fa6dffa51327a85ea75b479a64f97fcb14ec [file] [log] [blame]
Alexe0c5b9e2019-04-23 18:51:23 -05001import ipaddress
2import json
Alex6b633ec2019-06-06 19:44:34 -05003from copy import deepcopy
Alexe0c5b9e2019-04-23 18:51:23 -05004
5from cfg_checker.common import logger_cli
6from cfg_checker.common.exception import InvalidReturnException
7from cfg_checker.modules.network.network_errors import NetworkErrors
8from cfg_checker.nodes import salt_master
9
10# TODO: use templated approach
11# net interface structure should be the same
12_if_item = {
13 "name": "unnamed interface",
14 "mac": "",
15 "routes": {},
Alex6b633ec2019-06-06 19:44:34 -050016 "proto": "",
Alexe0c5b9e2019-04-23 18:51:23 -050017 "ip": [],
18 "parameters": {}
19}
20
21# collection of configurations
22_network_item = {
23 "runtime": {},
24 "config": {},
25 "reclass": {}
26}
27
28
29class NetworkMapper(object):
30 RECLASS = "reclass"
31 CONFIG = "config"
32 RUNTIME = "runtime"
33
34 def __init__(self, errors_class=None):
35 logger_cli.info("# Initializing mapper")
Alex6b633ec2019-06-06 19:44:34 -050036 # init networks and nodes
Alexe0c5b9e2019-04-23 18:51:23 -050037 self.networks = {}
38 self.nodes = salt_master.get_nodes()
Alex6b633ec2019-06-06 19:44:34 -050039 # init and pre-populate interfaces
40 self.interfaces = {k: {} for k in self.nodes}
41 # Init errors class
Alexe0c5b9e2019-04-23 18:51:23 -050042 if errors_class:
43 self.errors = errors_class
44 else:
45 logger_cli.debug("... init error logs folder")
46 self.errors = NetworkErrors()
47
48 # adding net data to tree
49 def _add_data(self, _list, _n, _h, _d):
50 if _n not in _list:
51 _list[_n] = {}
52 _list[_n][_h] = [_d]
53 elif _h not in _list[_n]:
54 # there is no such host, just create it
55 _list[_n][_h] = [_d]
56 else:
57 # there is such host... this is an error
58 self.errors.add_error(
59 self.errors.NET_DUPLICATE_IF,
60 host=_h,
61 dup_if=_d['name']
62 )
63 _list[_n][_h].append(_d)
64
65 # TODO: refactor map creation. Build one map instead of two separate
66 def _map_network_for_host(self, host, if_class, net_list, data):
67 # filter networks for this IF IP
68 _nets = [n for n in net_list.keys() if if_class.ip in n]
69 _masks = [n.netmask for n in _nets]
70 if len(_nets) > 1:
71 # There a multiple network found for this IP, Error
72 self.errors.add_error(
73 self.errors.NET_SUBNET_INTERSECT,
74 host=host,
75 ip=str(if_class.exploded),
76 networks="; ".join([str(_n) for _n in _nets])
77 )
78 # check mask match
79 if len(_nets) > 0 and if_class.netmask not in _masks:
80 self.errors.add_error(
81 self.errors.NET_MASK_MISMATCH,
82 host=host,
83 if_name=data['name'],
84 if_cidr=if_class.exploded,
85 if_mapped_networks=", ".join([str(_n) for _n in _nets])
86 )
87
88 if len(_nets) < 1:
89 self._add_data(net_list, if_class.network, host, data)
90 else:
91 # add all data
92 for net in _nets:
93 self._add_data(net_list, net, host, data)
94
95 return net_list
96
97 def _map_reclass_networks(self):
98 # class uses nodes from self.nodes dict
99 _reclass = {}
100 # Get required pillars
101 salt_master.get_specific_pillar_for_nodes("linux:network")
102 for node in salt_master.nodes.keys():
103 # check if this node
104 if not salt_master.is_node_available(node):
105 continue
106 # get the reclass value
107 _pillar = salt_master.nodes[node]['pillars']['linux']['network']
108 # we should be ready if there is no interface in reclass for a node
Alex92e07ce2019-05-31 16:00:03 -0500109 # for example on APT node
Alexe0c5b9e2019-04-23 18:51:23 -0500110 if 'interface' in _pillar:
111 _pillar = _pillar['interface']
112 else:
113 logger_cli.info(
114 "... node '{}' skipped, no IF section in reclass".format(
115 node
116 )
117 )
118 continue
Alex92e07ce2019-05-31 16:00:03 -0500119
Alex6b633ec2019-06-06 19:44:34 -0500120 # build map based on IPs and save info too
Alexe0c5b9e2019-04-23 18:51:23 -0500121 for _if_name, _if_data in _pillar.iteritems():
Alex6b633ec2019-06-06 19:44:34 -0500122 if _if_name not in self.interfaces[node]:
123 self.interfaces[node][_if_name] = deepcopy(_network_item)
124 self.interfaces[node][_if_name]['reclass'] = deepcopy(_if_data)
125 # map network if any
Alexe0c5b9e2019-04-23 18:51:23 -0500126 if 'address' in _if_data:
127 _if = ipaddress.IPv4Interface(
128 _if_data['address'] + '/' + _if_data['netmask']
129 )
130 _if_data['name'] = _if_name
131 _if_data['ifs'] = [_if]
132
133 _reclass = self._map_network_for_host(
134 node,
135 _if,
136 _reclass,
137 _if_data
138 )
139
140 return _reclass
141
142 def _map_configured_networks(self):
143 # class uses nodes from self.nodes dict
144 _confs = {}
145
Alex92e07ce2019-05-31 16:00:03 -0500146 # TODO: parse /etc/network/interfaces
147
Alexe0c5b9e2019-04-23 18:51:23 -0500148 return _confs
149
150 def _map_runtime_networks(self):
151 # class uses nodes from self.nodes dict
152 _runtime = {}
153 logger_cli.info("# Mapping node runtime network data")
154 salt_master.prepare_script_on_active_nodes("ifs_data.py")
155 _result = salt_master.execute_script_on_active_nodes(
156 "ifs_data.py",
157 args=["json"]
158 )
159 for key in salt_master.nodes.keys():
160 # check if we are to work with this node
161 if not salt_master.is_node_available(key):
162 continue
163 # due to much data to be passed from salt_master,
164 # it is happening in order
165 if key in _result:
166 _text = _result[key]
167 if '{' in _text and '}' in _text:
168 _text = _text[_text.find('{'):]
169 else:
170 raise InvalidReturnException(
171 "Non-json object returned: '{}'".format(
172 _text
173 )
174 )
175 _dict = json.loads(_text[_text.find('{'):])
176 salt_master.nodes[key]['routes'] = _dict.pop("routes")
177 salt_master.nodes[key]['networks'] = _dict
178 else:
179 salt_master.nodes[key]['networks'] = {}
180 salt_master.nodes[key]['routes'] = {}
181 logger_cli.debug("... {} has {} networks".format(
182 key,
183 len(salt_master.nodes[key]['networks'].keys())
184 ))
185 logger_cli.info("-> done collecting networks data")
186
187 logger_cli.info("-> mapping IPs")
188 # match interfaces by IP subnets
189 for host, node_data in salt_master.nodes.iteritems():
190 if not salt_master.is_node_available(host):
191 continue
192
193 for net_name, net_data in node_data['networks'].iteritems():
194 # get ips and calculate subnets
195 if net_name in ['lo']:
196 # skip the localhost
197 continue
Alex6b633ec2019-06-06 19:44:34 -0500198 else:
199 # add collected data to interface storage
200 if net_name not in self.interfaces[host]:
201 self.interfaces[host][net_name] = \
202 deepcopy(_network_item)
203 self.interfaces[host][net_name]['runtime'] = \
204 deepcopy(net_data)
205
Alexe0c5b9e2019-04-23 18:51:23 -0500206 # get data and make sure that wide mask goes first
207 _ip4s = sorted(
208 net_data['ipv4'],
209 key=lambda s: s[s.index('/'):]
210 )
211 for _ip_str in _ip4s:
212 # create interface class
213 _if = ipaddress.IPv4Interface(_ip_str)
214 # check if this is a VIP
215 # ...all those will have /32 mask
216 net_data['vip'] = None
217 if _if.network.prefixlen == 32:
218 net_data['vip'] = str(_if.exploded)
219 if 'name' not in net_data:
220 net_data['name'] = net_name
221 if 'ifs' not in net_data:
222 net_data['ifs'] = [_if]
223 # map it
224 _runtime = self._map_network_for_host(
225 host,
226 _if,
227 _runtime,
228 net_data
229 )
230 else:
231 # data is already there, just add VIP
232 net_data['ifs'].append(_if)
233
234 return _runtime
235
236 def map_network(self, source):
237 # maps target network using given source
238 _networks = None
239
240 if source == self.RECLASS:
241 _networks = self._map_reclass_networks()
242 elif source == self.CONFIG:
243 _networks = self._map_configured_networks()
244 elif source == self.RUNTIME:
245 _networks = self._map_runtime_networks()
246
247 self.networks[source] = _networks
248 return _networks
249
250 def print_map(self):
251 """
252 Create text report for CLI
253
254 :return: none
255 """
256 _runtime = self.networks[self.RUNTIME]
257 _reclass = self.networks[self.RECLASS]
Alex6b633ec2019-06-06 19:44:34 -0500258 logger_cli.info("# Networks")
Alexe0c5b9e2019-04-23 18:51:23 -0500259 logger_cli.info(
Alex6b633ec2019-06-06 19:44:34 -0500260 " {0:8} {1:25} {2:25} {3:6} {4:10} {5:10} {6}/{7}".format(
261 "Host",
Alexe0c5b9e2019-04-23 18:51:23 -0500262 "IF",
263 "IP",
Alex6b633ec2019-06-06 19:44:34 -0500264 "Proto",
265 "MTU",
266 "State",
267 "Gate",
268 "Def.Gate"
Alexe0c5b9e2019-04-23 18:51:23 -0500269 )
270 )
Alex6b633ec2019-06-06 19:44:34 -0500271 # No matter of proto, at least one IP will be present for the network
Alexe0c5b9e2019-04-23 18:51:23 -0500272 for network in _reclass:
273 # shortcuts
274 _net = str(network)
275 logger_cli.info("-> {}".format(_net))
276 if network not in _runtime:
277 # reclass has network that not found in runtime
278 self.errors.add_error(
279 self.errors.NET_NO_RUNTIME_NETWORK,
280 reclass_net=str(network)
281 )
282 logger_cli.info(" {:-^50}".format(" No runtime network "))
283 continue
Alex6b633ec2019-06-06 19:44:34 -0500284 # hostnames
Alexe0c5b9e2019-04-23 18:51:23 -0500285 names = sorted(_runtime[network].keys())
286 for hostname in names:
Alex6b633ec2019-06-06 19:44:34 -0500287 node = hostname.split('.')[0]
Alexe0c5b9e2019-04-23 18:51:23 -0500288 if not salt_master.is_node_available(hostname, log=False):
289 logger_cli.info(
Alex6b633ec2019-06-06 19:44:34 -0500290 " {0:8} {1}".format(node, "node not available")
Alexe0c5b9e2019-04-23 18:51:23 -0500291 )
292 # add non-responsive node erorr
293 self.errors.add_error(
294 self.errors.NET_NODE_NON_RESPONSIVE,
295 host=hostname
296 )
Alexe0c5b9e2019-04-23 18:51:23 -0500297 continue
298
Alex6b633ec2019-06-06 19:44:34 -0500299 # lookup interface name on node using network CIDR
300 _if_name = _runtime[network][hostname][0]["name"]
301 # get proper reclass
302 _r = self.interfaces[hostname][_if_name]['reclass']
Alexab232e42019-06-06 19:44:34 -0500303 _if_rc = "" if _r else "*"
Alex6b633ec2019-06-06 19:44:34 -0500304 _if_name_suffix = ""
305 # get the proto value
306 if "proto" in _r:
307 _proto = _r['proto']
Alexe0c5b9e2019-04-23 18:51:23 -0500308 else:
Alex6b633ec2019-06-06 19:44:34 -0500309 _proto = "-"
Alexe0c5b9e2019-04-23 18:51:23 -0500310
Alex6b633ec2019-06-06 19:44:34 -0500311 if "type" in _r:
312 _if_name_suffix += _r["type"]
313 if "use_interfaces" in _r:
314 _if_name_suffix += "->" + ",".join(_r["use_interfaces"])
315
316 if _if_name_suffix:
317 _if_name_suffix = "({})".format(_if_name_suffix)
318
319 # check reclass has this interface
320 if _proto != "-" and not _r:
321 self.errors.add_error(
322 self.errors.NET_NODE_UNEXPECTED_IF,
323 host=hostname,
324 if_name=_if_name
Alexe0c5b9e2019-04-23 18:51:23 -0500325 )
Alexe0c5b9e2019-04-23 18:51:23 -0500326
Alex6b633ec2019-06-06 19:44:34 -0500327 # get gate and routes if proto is static
328 if _proto == 'static':
329 # get the gateway for current net
330 _routes = salt_master.nodes[hostname]['routes']
331 _route = _routes[_net] if _net in _routes else None
332 if not _route:
333 _gate = "no route!"
334 else:
335 _gate = _route['gateway'] if _route['gateway'] else "-"
336
337 # get the default gateway
338 if 'default' in _routes:
339 _d_gate = ipaddress.IPv4Address(
340 _routes['default']['gateway']
341 )
342 else:
343 _d_gate = None
344 _d_gate_str = _d_gate if _d_gate else "No default gateway!"
345 else:
346 # in case of manual and dhcp, no check possible
347 _gate = "-"
348 _d_gate = "-"
Alex4067f002019-06-11 10:47:16 -0500349 _d_gate_str = "-"
Alex6b633ec2019-06-06 19:44:34 -0500350 # iterate through interfaces
Alexe0c5b9e2019-04-23 18:51:23 -0500351 _a = _runtime[network][hostname]
352 for _host in _a:
353 for _if in _host['ifs']:
Alexe0c5b9e2019-04-23 18:51:23 -0500354 _ip_str = str(_if.exploded)
Alexab232e42019-06-06 19:44:34 -0500355 _gate_error = ""
356 _up_error = ""
357 _mtu_error = ""
Alexe0c5b9e2019-04-23 18:51:23 -0500358
359 # check if node is UP
Alexe0c5b9e2019-04-23 18:51:23 -0500360 # get proper network from reclass
Alexab232e42019-06-06 19:44:34 -0500361 if _proto == 'static':
Alexe0c5b9e2019-04-23 18:51:23 -0500362 # Lookup match for the ip
Alex6b633ec2019-06-06 19:44:34 -0500363 _r_gate = "-"
364 if "gateway" in _r:
365 _r_gate = _r["gateway"]
Alexab232e42019-06-06 19:44:34 -0500366 # if values not match, put *
367 if _gate != _r_gate:
368 _gate_error = "*"
Alexe0c5b9e2019-04-23 18:51:23 -0500369
370 # IF status in reclass
Alex6b633ec2019-06-06 19:44:34 -0500371 _e = "enabled"
Alexab232e42019-06-06 19:44:34 -0500372 if _e not in _r:
373 _up_error = "*"
Alexe0c5b9e2019-04-23 18:51:23 -0500374
Alexe0c5b9e2019-04-23 18:51:23 -0500375 _rc_mtu = _r['mtu'] if 'mtu' in _r else None
Alexab232e42019-06-06 19:44:34 -0500376 _rc_mtu_s = ""
Alex6b633ec2019-06-06 19:44:34 -0500377 if _rc_mtu:
378 # there is a reclass MTU, save it
Alexab232e42019-06-06 19:44:34 -0500379 _rc_mtu_s = "/" + str(_rc_mtu)
Alex6b633ec2019-06-06 19:44:34 -0500380 elif _host['mtu'] == 1500 \
381 or _proto == "-" \
382 or _proto == "dhcp":
383 # 1500 is a default value => reclass not have it
384 # or this is a fancy network
Alexab232e42019-06-06 19:44:34 -0500385 pass
Alex6b633ec2019-06-06 19:44:34 -0500386 else:
387 # this is an error
Alexab232e42019-06-06 19:44:34 -0500388 _mtu_error = "*"
Alex6b633ec2019-06-06 19:44:34 -0500389
Alexe0c5b9e2019-04-23 18:51:23 -0500390 # check if this is a VIP address
391 # no checks needed if yes.
392 if _host['vip'] != _ip_str:
393 if _rc_mtu:
394 # if there is an MTU value, match it
395 if _host['mtu'] != _rc_mtu_s:
396 self.errors.add_error(
397 self.errors.NET_MTU_MISMATCH,
398 host=hostname,
Alex6b633ec2019-06-06 19:44:34 -0500399 if_name=_if_name,
Alexe0c5b9e2019-04-23 18:51:23 -0500400 if_cidr=_ip_str,
401 reclass_mtu=_rc_mtu,
402 runtime_mtu=_host['mtu']
403 )
Alexab232e42019-06-06 19:44:34 -0500404 _mtu_error = "*"
405 else:
406 # empty the matched value
407 _rc_mtu_s = ""
Alexe0c5b9e2019-04-23 18:51:23 -0500408 elif _host['mtu'] != '1500':
409 # there is no MTU value in reclass
410 # and runtime value is not default
411 self.errors.add_error(
412 self.errors.NET_MTU_EMPTY,
413 host=hostname,
Alex6b633ec2019-06-06 19:44:34 -0500414 if_name=_if_name,
Alexe0c5b9e2019-04-23 18:51:23 -0500415 if_cidr=_ip_str,
416 if_mtu=_host['mtu']
417 )
Alexab232e42019-06-06 19:44:34 -0500418 _mtu_error = "*"
Alexe0c5b9e2019-04-23 18:51:23 -0500419 else:
420 # this is a VIP
Alex6b633ec2019-06-06 19:44:34 -0500421 _if_name = " "*7
Alex6b633ec2019-06-06 19:44:34 -0500422 _if_name_suffix = ""
Alexe0c5b9e2019-04-23 18:51:23 -0500423 _ip_str += " VIP"
Alex6b633ec2019-06-06 19:44:34 -0500424 # Host IF IP Proto MTU State Gate Def.Gate
Alexab232e42019-06-06 19:44:34 -0500425 _text = "{:7} {:17} {:25} {:6} {:10} " \
Alex6b633ec2019-06-06 19:44:34 -0500426 "{:10} {}/{}".format(
427 _if_name + _if_rc,
428 _if_name_suffix,
Alexe0c5b9e2019-04-23 18:51:23 -0500429 _ip_str,
Alex6b633ec2019-06-06 19:44:34 -0500430 _proto,
Alexab232e42019-06-06 19:44:34 -0500431 _host['mtu'] + _rc_mtu_s + _mtu_error,
432 _host['state'] + _up_error,
433 _gate + _gate_error,
Alex6b633ec2019-06-06 19:44:34 -0500434 _d_gate_str
Alexe0c5b9e2019-04-23 18:51:23 -0500435 )
436 logger_cli.info(
Alex6b633ec2019-06-06 19:44:34 -0500437 " {0:8} {1}".format(
438 node,
Alexe0c5b9e2019-04-23 18:51:23 -0500439 _text
440 )
441 )
442
443 logger_cli.info("\n# Other networks")
444 _other = [n for n in _runtime if n not in _reclass]
445 for network in _other:
446 logger_cli.info("-> {}".format(str(network)))
447 names = sorted(_runtime[network].keys())
448
449 for hostname in names:
450 for _n in _runtime[network][hostname]:
451 _ifs = [str(ifs.ip) for ifs in _n['ifs']]
Alexab232e42019-06-06 19:44:34 -0500452 _text = "{:25} {:25} {:6} {:10} {}".format(
Alexe0c5b9e2019-04-23 18:51:23 -0500453 _n['name'],
454 ", ".join(_ifs),
Alexab232e42019-06-06 19:44:34 -0500455 "-",
Alexe0c5b9e2019-04-23 18:51:23 -0500456 _n['mtu'],
457 _n['state']
458 )
459 logger_cli.info(
Alexab232e42019-06-06 19:44:34 -0500460 " {0:8} {1}".format(hostname.split('.')[0], _text)
Alexe0c5b9e2019-04-23 18:51:23 -0500461 )
Alex6b633ec2019-06-06 19:44:34 -0500462 logger_cli.info("\n")