blob: 03237a639c886ae7ebbe57b8ca09ceab71d70f5b [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
Alexb3dc8592019-06-11 13:20:36 -0500121 for if_name, _dat in _pillar.iteritems():
122 # get proper IF name
123 _if_name = if_name if 'name' not in _dat else _dat['name']
124 # place it
Alex6b633ec2019-06-06 19:44:34 -0500125 if _if_name not in self.interfaces[node]:
126 self.interfaces[node][_if_name] = deepcopy(_network_item)
Alexb3dc8592019-06-11 13:20:36 -0500127 self.interfaces[node][_if_name]['reclass'] = deepcopy(_dat)
Alex6b633ec2019-06-06 19:44:34 -0500128 # map network if any
Alexb3dc8592019-06-11 13:20:36 -0500129 if 'address' in _dat:
Alexe0c5b9e2019-04-23 18:51:23 -0500130 _if = ipaddress.IPv4Interface(
Alexb3dc8592019-06-11 13:20:36 -0500131 _dat['address'] + '/' + _dat['netmask']
Alexe0c5b9e2019-04-23 18:51:23 -0500132 )
Alexb3dc8592019-06-11 13:20:36 -0500133 _dat['name'] = _if_name
134 _dat['ifs'] = [_if]
Alexe0c5b9e2019-04-23 18:51:23 -0500135
136 _reclass = self._map_network_for_host(
137 node,
138 _if,
139 _reclass,
Alexb3dc8592019-06-11 13:20:36 -0500140 _dat
Alexe0c5b9e2019-04-23 18:51:23 -0500141 )
142
143 return _reclass
144
145 def _map_configured_networks(self):
146 # class uses nodes from self.nodes dict
147 _confs = {}
148
Alex92e07ce2019-05-31 16:00:03 -0500149 # TODO: parse /etc/network/interfaces
150
Alexe0c5b9e2019-04-23 18:51:23 -0500151 return _confs
152
153 def _map_runtime_networks(self):
154 # class uses nodes from self.nodes dict
155 _runtime = {}
156 logger_cli.info("# Mapping node runtime network data")
157 salt_master.prepare_script_on_active_nodes("ifs_data.py")
158 _result = salt_master.execute_script_on_active_nodes(
159 "ifs_data.py",
160 args=["json"]
161 )
162 for key in salt_master.nodes.keys():
163 # check if we are to work with this node
164 if not salt_master.is_node_available(key):
165 continue
166 # due to much data to be passed from salt_master,
167 # it is happening in order
168 if key in _result:
169 _text = _result[key]
170 if '{' in _text and '}' in _text:
171 _text = _text[_text.find('{'):]
172 else:
173 raise InvalidReturnException(
174 "Non-json object returned: '{}'".format(
175 _text
176 )
177 )
178 _dict = json.loads(_text[_text.find('{'):])
179 salt_master.nodes[key]['routes'] = _dict.pop("routes")
180 salt_master.nodes[key]['networks'] = _dict
181 else:
182 salt_master.nodes[key]['networks'] = {}
183 salt_master.nodes[key]['routes'] = {}
184 logger_cli.debug("... {} has {} networks".format(
185 key,
186 len(salt_master.nodes[key]['networks'].keys())
187 ))
188 logger_cli.info("-> done collecting networks data")
189
190 logger_cli.info("-> mapping IPs")
191 # match interfaces by IP subnets
192 for host, node_data in salt_master.nodes.iteritems():
193 if not salt_master.is_node_available(host):
194 continue
195
196 for net_name, net_data in node_data['networks'].iteritems():
Alexb3dc8592019-06-11 13:20:36 -0500197 # cut net name
198 _i = net_name.find('@')
199 _name = net_name if _i < 0 else net_name[:_i]
Alexe0c5b9e2019-04-23 18:51:23 -0500200 # get ips and calculate subnets
Alexb3dc8592019-06-11 13:20:36 -0500201 if _name in ['lo']:
Alexe0c5b9e2019-04-23 18:51:23 -0500202 # skip the localhost
203 continue
Alex6b633ec2019-06-06 19:44:34 -0500204 else:
205 # add collected data to interface storage
Alexb3dc8592019-06-11 13:20:36 -0500206 if _name not in self.interfaces[host]:
207 self.interfaces[host][_name] = \
Alex6b633ec2019-06-06 19:44:34 -0500208 deepcopy(_network_item)
Alexb3dc8592019-06-11 13:20:36 -0500209 self.interfaces[host][_name]['runtime'] = \
Alex6b633ec2019-06-06 19:44:34 -0500210 deepcopy(net_data)
211
Alexe0c5b9e2019-04-23 18:51:23 -0500212 # get data and make sure that wide mask goes first
213 _ip4s = sorted(
214 net_data['ipv4'],
215 key=lambda s: s[s.index('/'):]
216 )
217 for _ip_str in _ip4s:
218 # create interface class
219 _if = ipaddress.IPv4Interface(_ip_str)
220 # check if this is a VIP
221 # ...all those will have /32 mask
222 net_data['vip'] = None
223 if _if.network.prefixlen == 32:
224 net_data['vip'] = str(_if.exploded)
225 if 'name' not in net_data:
Alexb3dc8592019-06-11 13:20:36 -0500226 net_data['name'] = _name
Alexe0c5b9e2019-04-23 18:51:23 -0500227 if 'ifs' not in net_data:
228 net_data['ifs'] = [_if]
229 # map it
230 _runtime = self._map_network_for_host(
231 host,
232 _if,
233 _runtime,
234 net_data
235 )
236 else:
237 # data is already there, just add VIP
238 net_data['ifs'].append(_if)
239
240 return _runtime
241
242 def map_network(self, source):
243 # maps target network using given source
244 _networks = None
245
246 if source == self.RECLASS:
247 _networks = self._map_reclass_networks()
248 elif source == self.CONFIG:
249 _networks = self._map_configured_networks()
250 elif source == self.RUNTIME:
251 _networks = self._map_runtime_networks()
252
253 self.networks[source] = _networks
254 return _networks
255
256 def print_map(self):
257 """
258 Create text report for CLI
259
260 :return: none
261 """
262 _runtime = self.networks[self.RUNTIME]
263 _reclass = self.networks[self.RECLASS]
Alex6b633ec2019-06-06 19:44:34 -0500264 logger_cli.info("# Networks")
Alexe0c5b9e2019-04-23 18:51:23 -0500265 logger_cli.info(
Alex6b633ec2019-06-06 19:44:34 -0500266 " {0:8} {1:25} {2:25} {3:6} {4:10} {5:10} {6}/{7}".format(
267 "Host",
Alexe0c5b9e2019-04-23 18:51:23 -0500268 "IF",
269 "IP",
Alex6b633ec2019-06-06 19:44:34 -0500270 "Proto",
271 "MTU",
272 "State",
273 "Gate",
274 "Def.Gate"
Alexe0c5b9e2019-04-23 18:51:23 -0500275 )
276 )
Alex6b633ec2019-06-06 19:44:34 -0500277 # No matter of proto, at least one IP will be present for the network
Alexe0c5b9e2019-04-23 18:51:23 -0500278 for network in _reclass:
279 # shortcuts
280 _net = str(network)
281 logger_cli.info("-> {}".format(_net))
282 if network not in _runtime:
283 # reclass has network that not found in runtime
284 self.errors.add_error(
285 self.errors.NET_NO_RUNTIME_NETWORK,
286 reclass_net=str(network)
287 )
288 logger_cli.info(" {:-^50}".format(" No runtime network "))
289 continue
Alex6b633ec2019-06-06 19:44:34 -0500290 # hostnames
Alexe0c5b9e2019-04-23 18:51:23 -0500291 names = sorted(_runtime[network].keys())
292 for hostname in names:
Alex6b633ec2019-06-06 19:44:34 -0500293 node = hostname.split('.')[0]
Alexe0c5b9e2019-04-23 18:51:23 -0500294 if not salt_master.is_node_available(hostname, log=False):
295 logger_cli.info(
Alex6b633ec2019-06-06 19:44:34 -0500296 " {0:8} {1}".format(node, "node not available")
Alexe0c5b9e2019-04-23 18:51:23 -0500297 )
298 # add non-responsive node erorr
299 self.errors.add_error(
300 self.errors.NET_NODE_NON_RESPONSIVE,
301 host=hostname
302 )
Alexe0c5b9e2019-04-23 18:51:23 -0500303 continue
Alex6b633ec2019-06-06 19:44:34 -0500304 # lookup interface name on node using network CIDR
305 _if_name = _runtime[network][hostname][0]["name"]
306 # get proper reclass
307 _r = self.interfaces[hostname][_if_name]['reclass']
Alexab232e42019-06-06 19:44:34 -0500308 _if_rc = "" if _r else "*"
Alex6b633ec2019-06-06 19:44:34 -0500309 _if_name_suffix = ""
310 # get the proto value
311 if "proto" in _r:
312 _proto = _r['proto']
Alexe0c5b9e2019-04-23 18:51:23 -0500313 else:
Alex6b633ec2019-06-06 19:44:34 -0500314 _proto = "-"
Alexe0c5b9e2019-04-23 18:51:23 -0500315
Alex6b633ec2019-06-06 19:44:34 -0500316 if "type" in _r:
317 _if_name_suffix += _r["type"]
318 if "use_interfaces" in _r:
319 _if_name_suffix += "->" + ",".join(_r["use_interfaces"])
320
321 if _if_name_suffix:
322 _if_name_suffix = "({})".format(_if_name_suffix)
323
324 # check reclass has this interface
325 if _proto != "-" and not _r:
326 self.errors.add_error(
327 self.errors.NET_NODE_UNEXPECTED_IF,
328 host=hostname,
329 if_name=_if_name
Alexe0c5b9e2019-04-23 18:51:23 -0500330 )
Alexe0c5b9e2019-04-23 18:51:23 -0500331
Alex6b633ec2019-06-06 19:44:34 -0500332 # get gate and routes if proto is static
333 if _proto == 'static':
334 # get the gateway for current net
335 _routes = salt_master.nodes[hostname]['routes']
336 _route = _routes[_net] if _net in _routes else None
Alex6b633ec2019-06-06 19:44:34 -0500337 # 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
Alexb3dc8592019-06-11 13:20:36 -0500344 _d_gate_str = str(_d_gate) if _d_gate else "No default!"
345 # match route with default
346 if not _route:
347 _gate = "?"
348 else:
349 _gate = _route['gateway'] if _route['gateway'] else "-"
Alex6b633ec2019-06-06 19:44:34 -0500350 else:
351 # in case of manual and dhcp, no check possible
352 _gate = "-"
353 _d_gate = "-"
Alex4067f002019-06-11 10:47:16 -0500354 _d_gate_str = "-"
Alex6b633ec2019-06-06 19:44:34 -0500355 # iterate through interfaces
Alexe0c5b9e2019-04-23 18:51:23 -0500356 _a = _runtime[network][hostname]
357 for _host in _a:
358 for _if in _host['ifs']:
Alexe0c5b9e2019-04-23 18:51:23 -0500359 _ip_str = str(_if.exploded)
Alexab232e42019-06-06 19:44:34 -0500360 _gate_error = ""
361 _up_error = ""
362 _mtu_error = ""
Alexe0c5b9e2019-04-23 18:51:23 -0500363
Alexb3dc8592019-06-11 13:20:36 -0500364 # Match gateway
Alexab232e42019-06-06 19:44:34 -0500365 if _proto == 'static':
Alexb3dc8592019-06-11 13:20:36 -0500366 # default reclass gate
Alex6b633ec2019-06-06 19:44:34 -0500367 _r_gate = "-"
368 if "gateway" in _r:
369 _r_gate = _r["gateway"]
Alexb3dc8592019-06-11 13:20:36 -0500370
Alexab232e42019-06-06 19:44:34 -0500371 # if values not match, put *
Alexb3dc8592019-06-11 13:20:36 -0500372 if _gate != _r_gate and _d_gate_str != _r_gate:
373 # if values not match, check if default match
Alexab232e42019-06-06 19:44:34 -0500374 _gate_error = "*"
Alexe0c5b9e2019-04-23 18:51:23 -0500375
376 # IF status in reclass
Alex6b633ec2019-06-06 19:44:34 -0500377 _e = "enabled"
Alexab232e42019-06-06 19:44:34 -0500378 if _e not in _r:
379 _up_error = "*"
Alexe0c5b9e2019-04-23 18:51:23 -0500380
Alexe0c5b9e2019-04-23 18:51:23 -0500381 _rc_mtu = _r['mtu'] if 'mtu' in _r else None
Alexab232e42019-06-06 19:44:34 -0500382 _rc_mtu_s = ""
Alex6b633ec2019-06-06 19:44:34 -0500383 if _rc_mtu:
384 # there is a reclass MTU, save it
Alexb3dc8592019-06-11 13:20:36 -0500385 _rc_mtu_s = str(_rc_mtu)
386 elif _host['mtu'] == "1500" \
Alex6b633ec2019-06-06 19:44:34 -0500387 or _proto == "-" \
388 or _proto == "dhcp":
389 # 1500 is a default value => reclass not have it
390 # or this is a fancy network
Alexab232e42019-06-06 19:44:34 -0500391 pass
Alex6b633ec2019-06-06 19:44:34 -0500392 else:
393 # this is an error
Alexab232e42019-06-06 19:44:34 -0500394 _mtu_error = "*"
Alex6b633ec2019-06-06 19:44:34 -0500395
Alexe0c5b9e2019-04-23 18:51:23 -0500396 # check if this is a VIP address
397 # no checks needed if yes.
398 if _host['vip'] != _ip_str:
399 if _rc_mtu:
400 # if there is an MTU value, match it
401 if _host['mtu'] != _rc_mtu_s:
402 self.errors.add_error(
403 self.errors.NET_MTU_MISMATCH,
404 host=hostname,
Alex6b633ec2019-06-06 19:44:34 -0500405 if_name=_if_name,
Alexe0c5b9e2019-04-23 18:51:23 -0500406 if_cidr=_ip_str,
407 reclass_mtu=_rc_mtu,
408 runtime_mtu=_host['mtu']
409 )
Alexb3dc8592019-06-11 13:20:36 -0500410 _rc_mtu_s = "/" + _rc_mtu_s
Alexab232e42019-06-06 19:44:34 -0500411 _mtu_error = "*"
412 else:
413 # empty the matched value
414 _rc_mtu_s = ""
Alexe0c5b9e2019-04-23 18:51:23 -0500415 elif _host['mtu'] != '1500':
416 # there is no MTU value in reclass
417 # and runtime value is not default
418 self.errors.add_error(
419 self.errors.NET_MTU_EMPTY,
420 host=hostname,
Alex6b633ec2019-06-06 19:44:34 -0500421 if_name=_if_name,
Alexe0c5b9e2019-04-23 18:51:23 -0500422 if_cidr=_ip_str,
423 if_mtu=_host['mtu']
424 )
Alexab232e42019-06-06 19:44:34 -0500425 _mtu_error = "*"
Alexe0c5b9e2019-04-23 18:51:23 -0500426 else:
427 # this is a VIP
Alex6b633ec2019-06-06 19:44:34 -0500428 _if_name = " "*7
Alex6b633ec2019-06-06 19:44:34 -0500429 _if_name_suffix = ""
Alexe0c5b9e2019-04-23 18:51:23 -0500430 _ip_str += " VIP"
Alex6b633ec2019-06-06 19:44:34 -0500431 # Host IF IP Proto MTU State Gate Def.Gate
Alexab232e42019-06-06 19:44:34 -0500432 _text = "{:7} {:17} {:25} {:6} {:10} " \
Alexb3dc8592019-06-11 13:20:36 -0500433 "{:10} {} / {}".format(
Alex6b633ec2019-06-06 19:44:34 -0500434 _if_name + _if_rc,
435 _if_name_suffix,
Alexe0c5b9e2019-04-23 18:51:23 -0500436 _ip_str,
Alex6b633ec2019-06-06 19:44:34 -0500437 _proto,
Alexab232e42019-06-06 19:44:34 -0500438 _host['mtu'] + _rc_mtu_s + _mtu_error,
439 _host['state'] + _up_error,
440 _gate + _gate_error,
Alex6b633ec2019-06-06 19:44:34 -0500441 _d_gate_str
Alexe0c5b9e2019-04-23 18:51:23 -0500442 )
443 logger_cli.info(
Alex6b633ec2019-06-06 19:44:34 -0500444 " {0:8} {1}".format(
445 node,
Alexe0c5b9e2019-04-23 18:51:23 -0500446 _text
447 )
448 )
449
450 logger_cli.info("\n# Other networks")
451 _other = [n for n in _runtime if n not in _reclass]
452 for network in _other:
453 logger_cli.info("-> {}".format(str(network)))
454 names = sorted(_runtime[network].keys())
455
456 for hostname in names:
457 for _n in _runtime[network][hostname]:
458 _ifs = [str(ifs.ip) for ifs in _n['ifs']]
Alexab232e42019-06-06 19:44:34 -0500459 _text = "{:25} {:25} {:6} {:10} {}".format(
Alexe0c5b9e2019-04-23 18:51:23 -0500460 _n['name'],
461 ", ".join(_ifs),
Alexab232e42019-06-06 19:44:34 -0500462 "-",
Alexe0c5b9e2019-04-23 18:51:23 -0500463 _n['mtu'],
464 _n['state']
465 )
466 logger_cli.info(
Alexab232e42019-06-06 19:44:34 -0500467 " {0:8} {1}".format(hostname.split('.')[0], _text)
Alexe0c5b9e2019-04-23 18:51:23 -0500468 )
Alex6b633ec2019-06-06 19:44:34 -0500469 logger_cli.info("\n")