blob: 3072c68a21a2eac660ba1ab8f9343c7824284662 [file] [log] [blame]
Alex0989ecf2022-03-29 13:43:21 -05001# Author: Alex Savatieiev (osavatieiev@mirantis.com; a.savex@gmail.com)
2# Copyright 2019-2022 Mirantis, Inc.
Alexe0c5b9e2019-04-23 18:51:23 -05003import ipaddress
4import json
Alex6b633ec2019-06-06 19:44:34 -05005from copy import deepcopy
Alexe0c5b9e2019-04-23 18:51:23 -05006
7from cfg_checker.common import logger_cli
8from cfg_checker.common.exception import InvalidReturnException
Alex1f90e7b2021-09-03 15:31:28 -05009from cfg_checker.common.exception import ConfigException
10from cfg_checker.common.exception import KubeException
Alexe0c5b9e2019-04-23 18:51:23 -050011from cfg_checker.modules.network.network_errors import NetworkErrors
Alex205546c2020-12-30 19:22:30 -060012from cfg_checker.nodes import SaltNodes, KubeNodes
Alexe0c5b9e2019-04-23 18:51:23 -050013
14# TODO: use templated approach
15# net interface structure should be the same
16_if_item = {
17 "name": "unnamed interface",
18 "mac": "",
19 "routes": {},
Alex6b633ec2019-06-06 19:44:34 -050020 "proto": "",
Alexe0c5b9e2019-04-23 18:51:23 -050021 "ip": [],
22 "parameters": {}
23}
24
25# collection of configurations
26_network_item = {
27 "runtime": {},
28 "config": {},
29 "reclass": {}
30}
31
32
33class NetworkMapper(object):
34 RECLASS = "reclass"
35 CONFIG = "config"
36 RUNTIME = "runtime"
37
Alexe9908f72020-05-19 16:04:53 -050038 def __init__(
39 self,
Alex9a4ad212020-10-01 18:04:25 -050040 config,
Alexe9908f72020-05-19 16:04:53 -050041 errors_class=None,
42 skip_list=None,
43 skip_list_file=None
44 ):
Alexe0c5b9e2019-04-23 18:51:23 -050045 logger_cli.info("# Initializing mapper")
Alex205546c2020-12-30 19:22:30 -060046 self.env_config = config
Alex6b633ec2019-06-06 19:44:34 -050047 # init networks and nodes
Alexe0c5b9e2019-04-23 18:51:23 -050048 self.networks = {}
Alex205546c2020-12-30 19:22:30 -060049 self.nodes = self.master.get_nodes(
Alexe9908f72020-05-19 16:04:53 -050050 skip_list=skip_list,
51 skip_list_file=skip_list_file
52 )
Alex205546c2020-12-30 19:22:30 -060053 self.cluster = self.master.get_info()
54 self.domain = self.master.domain
Alex6b633ec2019-06-06 19:44:34 -050055 # init and pre-populate interfaces
56 self.interfaces = {k: {} for k in self.nodes}
57 # Init errors class
Alexe0c5b9e2019-04-23 18:51:23 -050058 if errors_class:
59 self.errors = errors_class
60 else:
61 logger_cli.debug("... init error logs folder")
62 self.errors = NetworkErrors()
63
64 # adding net data to tree
65 def _add_data(self, _list, _n, _h, _d):
66 if _n not in _list:
67 _list[_n] = {}
68 _list[_n][_h] = [_d]
69 elif _h not in _list[_n]:
70 # there is no such host, just create it
71 _list[_n][_h] = [_d]
72 else:
73 # there is such host... this is an error
74 self.errors.add_error(
75 self.errors.NET_DUPLICATE_IF,
76 host=_h,
77 dup_if=_d['name']
78 )
79 _list[_n][_h].append(_d)
80
81 # TODO: refactor map creation. Build one map instead of two separate
82 def _map_network_for_host(self, host, if_class, net_list, data):
83 # filter networks for this IF IP
84 _nets = [n for n in net_list.keys() if if_class.ip in n]
85 _masks = [n.netmask for n in _nets]
86 if len(_nets) > 1:
87 # There a multiple network found for this IP, Error
88 self.errors.add_error(
89 self.errors.NET_SUBNET_INTERSECT,
90 host=host,
91 ip=str(if_class.exploded),
92 networks="; ".join([str(_n) for _n in _nets])
93 )
94 # check mask match
95 if len(_nets) > 0 and if_class.netmask not in _masks:
96 self.errors.add_error(
97 self.errors.NET_MASK_MISMATCH,
98 host=host,
99 if_name=data['name'],
100 if_cidr=if_class.exploded,
101 if_mapped_networks=", ".join([str(_n) for _n in _nets])
102 )
103
104 if len(_nets) < 1:
105 self._add_data(net_list, if_class.network, host, data)
106 else:
107 # add all data
108 for net in _nets:
109 self._add_data(net_list, net, host, data)
110
111 return net_list
112
113 def _map_reclass_networks(self):
114 # class uses nodes from self.nodes dict
115 _reclass = {}
116 # Get required pillars
Alex205546c2020-12-30 19:22:30 -0600117 self.master.get_specific_pillar_for_nodes("linux:network")
118 for node in self.master.nodes.keys():
Alexe0c5b9e2019-04-23 18:51:23 -0500119 # check if this node
Alex205546c2020-12-30 19:22:30 -0600120 if not self.master.is_node_available(node):
Alexe0c5b9e2019-04-23 18:51:23 -0500121 continue
122 # get the reclass value
Alex9a4ad212020-10-01 18:04:25 -0500123 _pillar = \
Alex205546c2020-12-30 19:22:30 -0600124 self.master.nodes[node]['pillars']['linux']['network']
Alexe0c5b9e2019-04-23 18:51:23 -0500125 # we should be ready if there is no interface in reclass for a node
Alex92e07ce2019-05-31 16:00:03 -0500126 # for example on APT node
Alexe0c5b9e2019-04-23 18:51:23 -0500127 if 'interface' in _pillar:
128 _pillar = _pillar['interface']
129 else:
130 logger_cli.info(
131 "... node '{}' skipped, no IF section in reclass".format(
132 node
133 )
134 )
135 continue
Alex92e07ce2019-05-31 16:00:03 -0500136
Alex6b633ec2019-06-06 19:44:34 -0500137 # build map based on IPs and save info too
Alex3bc95f62020-03-05 17:00:04 -0600138 for if_name, _dat in _pillar.items():
Alexb3dc8592019-06-11 13:20:36 -0500139 # get proper IF name
140 _if_name = if_name if 'name' not in _dat else _dat['name']
141 # place it
Alex6b633ec2019-06-06 19:44:34 -0500142 if _if_name not in self.interfaces[node]:
143 self.interfaces[node][_if_name] = deepcopy(_network_item)
Alexb3dc8592019-06-11 13:20:36 -0500144 self.interfaces[node][_if_name]['reclass'] = deepcopy(_dat)
Alex6b633ec2019-06-06 19:44:34 -0500145 # map network if any
Alexb3dc8592019-06-11 13:20:36 -0500146 if 'address' in _dat:
Alexe0c5b9e2019-04-23 18:51:23 -0500147 _if = ipaddress.IPv4Interface(
Alexb3dc8592019-06-11 13:20:36 -0500148 _dat['address'] + '/' + _dat['netmask']
Alexe0c5b9e2019-04-23 18:51:23 -0500149 )
Alexb3dc8592019-06-11 13:20:36 -0500150 _dat['name'] = _if_name
151 _dat['ifs'] = [_if]
Alexe0c5b9e2019-04-23 18:51:23 -0500152
153 _reclass = self._map_network_for_host(
154 node,
155 _if,
156 _reclass,
Alexb3dc8592019-06-11 13:20:36 -0500157 _dat
Alexe0c5b9e2019-04-23 18:51:23 -0500158 )
159
160 return _reclass
161
162 def _map_configured_networks(self):
163 # class uses nodes from self.nodes dict
164 _confs = {}
165
Alex92e07ce2019-05-31 16:00:03 -0500166 # TODO: parse /etc/network/interfaces
167
Alexe0c5b9e2019-04-23 18:51:23 -0500168 return _confs
169
Alex1f90e7b2021-09-03 15:31:28 -0500170 def _map_runtime_networks(self, result):
Alexe0c5b9e2019-04-23 18:51:23 -0500171 # class uses nodes from self.nodes dict
172 _runtime = {}
Alex205546c2020-12-30 19:22:30 -0600173 for key in self.master.nodes.keys():
Alexe0c5b9e2019-04-23 18:51:23 -0500174 # check if we are to work with this node
Alex205546c2020-12-30 19:22:30 -0600175 if not self.master.is_node_available(key):
Alexe0c5b9e2019-04-23 18:51:23 -0500176 continue
Alex205546c2020-12-30 19:22:30 -0600177 # due to much data to be passed from master,
Alexe0c5b9e2019-04-23 18:51:23 -0500178 # it is happening in order
Alex1f90e7b2021-09-03 15:31:28 -0500179 if key in result:
180 _text = result[key]
Alexe0c5b9e2019-04-23 18:51:23 -0500181 if '{' in _text and '}' in _text:
182 _text = _text[_text.find('{'):]
183 else:
184 raise InvalidReturnException(
185 "Non-json object returned: '{}'".format(
186 _text
187 )
188 )
189 _dict = json.loads(_text[_text.find('{'):])
Alex205546c2020-12-30 19:22:30 -0600190 self.master.nodes[key]['routes'] = _dict.pop("routes")
191 self.master.nodes[key]['networks'] = _dict
Alexe0c5b9e2019-04-23 18:51:23 -0500192 else:
Alex205546c2020-12-30 19:22:30 -0600193 self.master.nodes[key]['networks'] = {}
194 self.master.nodes[key]['routes'] = {}
Alexe0c5b9e2019-04-23 18:51:23 -0500195 logger_cli.debug("... {} has {} networks".format(
196 key,
Alex205546c2020-12-30 19:22:30 -0600197 len(self.master.nodes[key]['networks'].keys())
Alexe0c5b9e2019-04-23 18:51:23 -0500198 ))
199 logger_cli.info("-> done collecting networks data")
Alex0bcf31b2022-03-29 17:38:58 -0500200 # Save number for future recoursion depth cutting
201 _total_networks = len(self.master.nodes[key]['networks'])
Alexe0c5b9e2019-04-23 18:51:23 -0500202
Alex3cdb1bd2021-09-10 15:51:11 -0500203 logger_cli.info("-> mapping runtime network IPs")
Alexe0c5b9e2019-04-23 18:51:23 -0500204 # match interfaces by IP subnets
Alex205546c2020-12-30 19:22:30 -0600205 for host, node_data in self.master.nodes.items():
206 if not self.master.is_node_available(host):
Alexe0c5b9e2019-04-23 18:51:23 -0500207 continue
208
Alex3bc95f62020-03-05 17:00:04 -0600209 for net_name, net_data in node_data['networks'].items():
Alexb3dc8592019-06-11 13:20:36 -0500210 # cut net name
211 _i = net_name.find('@')
212 _name = net_name if _i < 0 else net_name[:_i]
Alexe0c5b9e2019-04-23 18:51:23 -0500213 # get ips and calculate subnets
Alexb3dc8592019-06-11 13:20:36 -0500214 if _name in ['lo']:
Alexe0c5b9e2019-04-23 18:51:23 -0500215 # skip the localhost
216 continue
Alex6b633ec2019-06-06 19:44:34 -0500217 else:
218 # add collected data to interface storage
Alexb3dc8592019-06-11 13:20:36 -0500219 if _name not in self.interfaces[host]:
220 self.interfaces[host][_name] = \
Alex6b633ec2019-06-06 19:44:34 -0500221 deepcopy(_network_item)
Alexb3dc8592019-06-11 13:20:36 -0500222 self.interfaces[host][_name]['runtime'] = \
Alex6b633ec2019-06-06 19:44:34 -0500223 deepcopy(net_data)
224
Alexe0c5b9e2019-04-23 18:51:23 -0500225 # get data and make sure that wide mask goes first
226 _ip4s = sorted(
227 net_data['ipv4'],
228 key=lambda s: s[s.index('/'):]
229 )
230 for _ip_str in _ip4s:
231 # create interface class
232 _if = ipaddress.IPv4Interface(_ip_str)
233 # check if this is a VIP
234 # ...all those will have /32 mask
235 net_data['vip'] = None
236 if _if.network.prefixlen == 32:
237 net_data['vip'] = str(_if.exploded)
238 if 'name' not in net_data:
Alexb3dc8592019-06-11 13:20:36 -0500239 net_data['name'] = _name
Alexe0c5b9e2019-04-23 18:51:23 -0500240 if 'ifs' not in net_data:
241 net_data['ifs'] = [_if]
242 # map it
243 _runtime = self._map_network_for_host(
244 host,
245 _if,
246 _runtime,
247 net_data
248 )
249 else:
250 # data is already there, just add VIP
251 net_data['ifs'].append(_if)
252
Alex1839bbf2019-08-22 17:17:21 -0500253 def process_interface(lvl, interface, tree, res):
Alex0bcf31b2022-03-29 17:38:58 -0500254 if abs(lvl) > _total_networks:
255 logger_cli.warn(
256 "WARNING: Probable cyclic dependency, "
257 "tree path discovery was cut down to {}".format(
258 _total_networks
259 )
260 )
261 return
262
Alex1839bbf2019-08-22 17:17:21 -0500263 # get childs for each root
264 # tree row item (<if_name>, [<parents>], [<childs>])
Alex163aa042022-12-01 11:58:32 -0600265 if lvl > 50 or lvl < -50:
266 logger_cli.warning(
267 "WARNING: Recoursion depth limit ({}) reached "
268 "for {}".format(
269 lvl,
270 interface
271 )
272 )
273 return
Alex1839bbf2019-08-22 17:17:21 -0500274 if lvl not in tree:
275 # - no level - add it
276 tree[lvl] = {}
277 # there is such interface in this level?
278 if interface not in tree[lvl]:
279 # - IF not present
Alexf3dbe862019-10-07 15:17:04 -0500280 _n = ''
281 if interface not in res:
282 _n = 'unknown IF'
283 _p = None
284 _c = None
285 else:
286 # -- get parents, add
287 _p = res[interface]['lower']
288 # -- get childs, add
289 _c = res[interface]['upper']
290
Alex1839bbf2019-08-22 17:17:21 -0500291 # if None, put empty list
292 _p = _p if _p else []
Alex1839bbf2019-08-22 17:17:21 -0500293 # if None, put empty list
294 _c = _c if _c else []
295 tree[lvl].update({
296 interface: {
Alexf3dbe862019-10-07 15:17:04 -0500297 "note": _n,
Alex1839bbf2019-08-22 17:17:21 -0500298 "parents": _p,
299 "children": _c,
300 "size": len(_p) if len(_p) > len(_c) else len(_c)
301 }
302 })
303 for p_if in tree[lvl][interface]["parents"]:
304 # -- cycle: execute process for next parent, lvl-1
305 process_interface(lvl-1, p_if, tree, res)
306 for c_if in tree[lvl][interface]["children"]:
307 # -- cycle: execute process for next child, lvl+1
308 process_interface(lvl+1, c_if, tree, res)
309 else:
310 # - IF present - exit (been here already)
311 return
312
313 def _put(cNet, cIndex, _list):
Alexf3dbe862019-10-07 15:17:04 -0500314 _added = False
315 _actual_index = -1
316 # Check list len
317 _len = len(_list)
318 if cIndex >= _len:
319 # grow list to meet index
320 _list = _list + [''] * (cIndex - _len + 1)
321 _len = len(_list)
322
323 for _cI in range(cIndex, _len):
Alex1839bbf2019-08-22 17:17:21 -0500324 # add child per index
325 # if space is free
326 if not _list[_cI]:
327 _list[_cI] = cNet
Alexf3dbe862019-10-07 15:17:04 -0500328 _added = True
329 _actual_index = _cI
Alex1839bbf2019-08-22 17:17:21 -0500330 break
Alexf3dbe862019-10-07 15:17:04 -0500331 if not _added:
332 # grow list by one entry
333 _list = _list + [cNet]
334 _actual_index = len(_list) - 1
335 return _actual_index, _list
Alex1839bbf2019-08-22 17:17:21 -0500336
337 # build network hierachy
338 nr = node_data['networks']
339 # walk interface tree
340 for _ifname in node_data['networks']:
341 _tree = {}
342 _level = 0
343 process_interface(_level, _ifname, _tree, nr)
344 # save tree for node/if
345 node_data['networks'][_ifname]['tree'] = _tree
346
347 # debug, print built tree
348 # logger_cli.debug("# '{}'".format(_ifname))
Alex3bc95f62020-03-05 17:00:04 -0600349 lvls = list(_tree.keys())
Alex1839bbf2019-08-22 17:17:21 -0500350 lvls.sort()
351 n = len(lvls)
352 m = max([len(_tree[k].keys()) for k in _tree.keys()])
353 matrix = [["" for i in range(m)] for j in range(n)]
354 x = 0
355 while True:
356 _lv = lvls.pop(0)
357 # get all interfaces on this level
Alex3bc95f62020-03-05 17:00:04 -0600358 nets = iter(_tree[_lv].keys())
Alex1839bbf2019-08-22 17:17:21 -0500359 while True:
360 y = 0
361 # get next interface
Alex3bc95f62020-03-05 17:00:04 -0600362 try:
363 _net = next(nets)
364 except StopIteration:
365 break
Alex1839bbf2019-08-22 17:17:21 -0500366 # all nets
367 _a = [_net]
368 # put current interface if this is only one left
369 if not _tree[_lv][_net]['children']:
370 if _net not in matrix[x]:
Alexf3dbe862019-10-07 15:17:04 -0500371 _, matrix[x] = _put(
372 _net,
373 y,
374 matrix[x]
375 )
Alex1839bbf2019-08-22 17:17:21 -0500376 y += 1
377 else:
378 # get all nets with same child
379 for _c in _tree[_lv][_net]['children']:
380 for _o_net in nets:
381 if _c in _tree[_lv][_o_net]['children']:
382 _a.append(_o_net)
383 # flush collected nets
384 for idx in range(len(_a)):
385 if _a[idx] in matrix[x]:
386 # there is such interface on this level
387 # get index
388 _nI = matrix[x].index(_a[idx])
Alexf3dbe862019-10-07 15:17:04 -0500389 _, matrix[x+1] = _put(
390 _c,
391 _nI,
392 matrix[x+1]
393 )
Alex1839bbf2019-08-22 17:17:21 -0500394 else:
395 # there is no such interface
396 # add it
Alexf3dbe862019-10-07 15:17:04 -0500397 _t, matrix[x] = _put(
398 _a[idx],
399 0,
400 matrix[x]
401 )
402 # also, put child
403 _, matrix[x+1] = _put(
404 _c,
405 _t,
406 matrix[x+1]
407 )
Alex1839bbf2019-08-22 17:17:21 -0500408 # remove collected nets from processing
409 if _a[idx] in nets:
410 nets.remove(_a[idx])
411 y += len(_a)
412 if not nets:
413 x += 1
414 break
415 if not lvls:
416 break
417
418 lines = []
419 _columns = [len(max([i for i in li])) for li in matrix]
420 for idx_y in range(m):
421 line = ""
422 for idx_x in range(n):
Alex9b2c1d12020-03-19 09:32:35 -0500423 _len = _columns[idx_x] if _columns[idx_x] else 1
424 _fmt = "{" + ":{}".format(_len) + "} "
Alex1839bbf2019-08-22 17:17:21 -0500425 line += _fmt.format(matrix[idx_x][idx_y])
426 lines.append(line)
427 node_data['networks'][_ifname]['matrix'] = matrix
428 node_data['networks'][_ifname]['lines'] = lines
Alexe0c5b9e2019-04-23 18:51:23 -0500429 return _runtime
430
Alex1f90e7b2021-09-03 15:31:28 -0500431
432class SaltNetworkMapper(NetworkMapper):
433 def __init__(
434 self,
435 config,
436 errors_class=None,
437 skip_list=None,
438 skip_list_file=None
439 ):
440 self.master = SaltNodes(config)
441 super(SaltNetworkMapper, self).__init__(
442 config,
443 errors_class=errors_class,
444 skip_list=skip_list,
445 skip_list_file=skip_list_file
446 )
447
448 def get_script_output(self):
449 """
450 Get runtime networks by executing script on nodes
451 """
452 logger_cli.info("# Mapping node runtime network data")
453 self.master.prepare_script_on_active_nodes("ifs_data.py")
454 _result = self.master.execute_script_on_active_nodes(
455 "ifs_data.py",
456 args="json"
457 )
458
459 return _result
460
461 def map_networks(self):
Alex3cdb1bd2021-09-10 15:51:11 -0500462 logger_cli.info("-> Mapping reclass networks")
Alex1f90e7b2021-09-03 15:31:28 -0500463 self.map_network(self.RECLASS)
Alex3cdb1bd2021-09-10 15:51:11 -0500464 logger_cli.info("-> Mapping runtime networks")
Alex1f90e7b2021-09-03 15:31:28 -0500465 self.map_network(self.RUNTIME)
466
Alexe0c5b9e2019-04-23 18:51:23 -0500467 def map_network(self, source):
468 # maps target network using given source
469 _networks = None
470
471 if source == self.RECLASS:
472 _networks = self._map_reclass_networks()
473 elif source == self.CONFIG:
474 _networks = self._map_configured_networks()
475 elif source == self.RUNTIME:
Alex1f90e7b2021-09-03 15:31:28 -0500476 _r = self.get_script_output()
477 _networks = self._map_runtime_networks(_r)
Alexe0c5b9e2019-04-23 18:51:23 -0500478
479 self.networks[source] = _networks
480 return _networks
481
Alex3cdb1bd2021-09-10 15:51:11 -0500482 def create_map(self, skip_keywords=None):
Alex836fac82019-08-22 13:36:16 -0500483 """Create all needed elements for map output
Alexe0c5b9e2019-04-23 18:51:23 -0500484
485 :return: none
486 """
487 _runtime = self.networks[self.RUNTIME]
488 _reclass = self.networks[self.RECLASS]
Alex836fac82019-08-22 13:36:16 -0500489
490 # main networks, target vars
491 _map = {}
Alex6b633ec2019-06-06 19:44:34 -0500492 # No matter of proto, at least one IP will be present for the network
Alex836fac82019-08-22 13:36:16 -0500493 # we interested in, since we are to make sure that L3 level
494 # is configured according to reclass model
Alexe0c5b9e2019-04-23 18:51:23 -0500495 for network in _reclass:
496 # shortcuts
497 _net = str(network)
Alex836fac82019-08-22 13:36:16 -0500498 _map[_net] = {}
Alexe0c5b9e2019-04-23 18:51:23 -0500499 if network not in _runtime:
500 # reclass has network that not found in runtime
501 self.errors.add_error(
502 self.errors.NET_NO_RUNTIME_NETWORK,
503 reclass_net=str(network)
504 )
Alex1839bbf2019-08-22 17:17:21 -0500505 logger_cli.warn(
506 "WARN: {}: {}".format(
507 " No runtime network ", str(network)
508 )
509 )
Alexe0c5b9e2019-04-23 18:51:23 -0500510 continue
Alex6b633ec2019-06-06 19:44:34 -0500511 # hostnames
Alexe0c5b9e2019-04-23 18:51:23 -0500512 names = sorted(_runtime[network].keys())
513 for hostname in names:
Alex836fac82019-08-22 13:36:16 -0500514 _notes = []
Alex6b633ec2019-06-06 19:44:34 -0500515 node = hostname.split('.')[0]
Alex205546c2020-12-30 19:22:30 -0600516 if not self.master.is_node_available(hostname, log=False):
Alexe0c5b9e2019-04-23 18:51:23 -0500517 logger_cli.info(
Alex6b633ec2019-06-06 19:44:34 -0500518 " {0:8} {1}".format(node, "node not available")
Alexe0c5b9e2019-04-23 18:51:23 -0500519 )
520 # add non-responsive node erorr
521 self.errors.add_error(
522 self.errors.NET_NODE_NON_RESPONSIVE,
523 host=hostname
524 )
Alex836fac82019-08-22 13:36:16 -0500525 _notes.append(
526 self.errors.get_error_type_text(
527 self.errors.NET_NODE_NON_RESPONSIVE
528 )
529 )
Alexe0c5b9e2019-04-23 18:51:23 -0500530 continue
Alex6b633ec2019-06-06 19:44:34 -0500531 # lookup interface name on node using network CIDR
532 _if_name = _runtime[network][hostname][0]["name"]
Alex836fac82019-08-22 13:36:16 -0500533 _raw = self.interfaces[hostname][_if_name]['runtime']
Alex6b633ec2019-06-06 19:44:34 -0500534 # get proper reclass
535 _r = self.interfaces[hostname][_if_name]['reclass']
Alex6b633ec2019-06-06 19:44:34 -0500536 _if_name_suffix = ""
537 # get the proto value
Alex3b8e5432019-06-11 15:21:59 -0500538 if _r:
539 _if_rc = ""
540 else:
541 self.errors.add_error(
542 self.errors.NET_NODE_UNEXPECTED_IF,
543 host=hostname,
544 if_name=_if_name
545 )
Alex836fac82019-08-22 13:36:16 -0500546 _notes.append(
547 self.errors.get_error_type_text(
548 self.errors.NET_NODE_UNEXPECTED_IF
549 )
550 )
Alex3b8e5432019-06-11 15:21:59 -0500551 _if_rc = "*"
552
Alex6b633ec2019-06-06 19:44:34 -0500553 if "proto" in _r:
554 _proto = _r['proto']
Alexe0c5b9e2019-04-23 18:51:23 -0500555 else:
Alex6b633ec2019-06-06 19:44:34 -0500556 _proto = "-"
Alexe0c5b9e2019-04-23 18:51:23 -0500557
Alex6b633ec2019-06-06 19:44:34 -0500558 if "type" in _r:
559 _if_name_suffix += _r["type"]
560 if "use_interfaces" in _r:
561 _if_name_suffix += "->" + ",".join(_r["use_interfaces"])
562
563 if _if_name_suffix:
564 _if_name_suffix = "({})".format(_if_name_suffix)
565
Alex6b633ec2019-06-06 19:44:34 -0500566 # get gate and routes if proto is static
567 if _proto == 'static':
568 # get the gateway for current net
Alex205546c2020-12-30 19:22:30 -0600569 _routes = self.master.nodes[hostname]['routes']
Alex6b633ec2019-06-06 19:44:34 -0500570 _route = _routes[_net] if _net in _routes else None
Alex6b633ec2019-06-06 19:44:34 -0500571 # get the default gateway
572 if 'default' in _routes:
573 _d_gate = ipaddress.IPv4Address(
574 _routes['default']['gateway']
575 )
576 else:
577 _d_gate = None
Alexb3dc8592019-06-11 13:20:36 -0500578 _d_gate_str = str(_d_gate) if _d_gate else "No default!"
579 # match route with default
580 if not _route:
581 _gate = "?"
582 else:
583 _gate = _route['gateway'] if _route['gateway'] else "-"
Alex6b633ec2019-06-06 19:44:34 -0500584 else:
585 # in case of manual and dhcp, no check possible
586 _gate = "-"
587 _d_gate = "-"
Alex4067f002019-06-11 10:47:16 -0500588 _d_gate_str = "-"
Alex6b633ec2019-06-06 19:44:34 -0500589 # iterate through interfaces
Alexe0c5b9e2019-04-23 18:51:23 -0500590 _a = _runtime[network][hostname]
591 for _host in _a:
592 for _if in _host['ifs']:
Alexe0c5b9e2019-04-23 18:51:23 -0500593 _ip_str = str(_if.exploded)
Alexab232e42019-06-06 19:44:34 -0500594 _gate_error = ""
595 _up_error = ""
596 _mtu_error = ""
Alexe0c5b9e2019-04-23 18:51:23 -0500597
Alexb3dc8592019-06-11 13:20:36 -0500598 # Match gateway
Alexab232e42019-06-06 19:44:34 -0500599 if _proto == 'static':
Alexb3dc8592019-06-11 13:20:36 -0500600 # default reclass gate
Alex6b633ec2019-06-06 19:44:34 -0500601 _r_gate = "-"
602 if "gateway" in _r:
603 _r_gate = _r["gateway"]
Alexb3dc8592019-06-11 13:20:36 -0500604
Alexab232e42019-06-06 19:44:34 -0500605 # if values not match, put *
Alexb3dc8592019-06-11 13:20:36 -0500606 if _gate != _r_gate and _d_gate_str != _r_gate:
607 # if values not match, check if default match
Alex3b8e5432019-06-11 15:21:59 -0500608 self.errors.add_error(
609 self.errors.NET_UNEXPECTED_GATEWAY,
610 host=hostname,
611 if_name=_if_name,
612 ip=_ip_str,
613 gateway=_gate
614 )
Alex836fac82019-08-22 13:36:16 -0500615 _notes.append(
616 self.errors.get_error_type_text(
617 self.errors.NET_UNEXPECTED_GATEWAY
618 )
619 )
Alexab232e42019-06-06 19:44:34 -0500620 _gate_error = "*"
Alexe0c5b9e2019-04-23 18:51:23 -0500621
622 # IF status in reclass
Alex6b633ec2019-06-06 19:44:34 -0500623 _e = "enabled"
Alexab232e42019-06-06 19:44:34 -0500624 if _e not in _r:
Alex3b8e5432019-06-11 15:21:59 -0500625 self.errors.add_error(
626 self.errors.NET_NO_RC_IF_STATUS,
627 host=hostname,
628 if_name=_if_name
629 )
Alex836fac82019-08-22 13:36:16 -0500630 _notes.append(
631 self.errors.get_error_type_text(
632 self.errors.NET_NO_RC_IF_STATUS
633 )
634 )
Alexab232e42019-06-06 19:44:34 -0500635 _up_error = "*"
Alexe0c5b9e2019-04-23 18:51:23 -0500636
Alexe0c5b9e2019-04-23 18:51:23 -0500637 _rc_mtu = _r['mtu'] if 'mtu' in _r else None
Alexab232e42019-06-06 19:44:34 -0500638 _rc_mtu_s = ""
Alexe0c5b9e2019-04-23 18:51:23 -0500639 # check if this is a VIP address
640 # no checks needed if yes.
641 if _host['vip'] != _ip_str:
642 if _rc_mtu:
Alex3b8e5432019-06-11 15:21:59 -0500643 _rc_mtu_s = str(_rc_mtu)
Alexe0c5b9e2019-04-23 18:51:23 -0500644 # if there is an MTU value, match it
645 if _host['mtu'] != _rc_mtu_s:
646 self.errors.add_error(
647 self.errors.NET_MTU_MISMATCH,
648 host=hostname,
Alex6b633ec2019-06-06 19:44:34 -0500649 if_name=_if_name,
Alexe0c5b9e2019-04-23 18:51:23 -0500650 if_cidr=_ip_str,
651 reclass_mtu=_rc_mtu,
652 runtime_mtu=_host['mtu']
653 )
Alex836fac82019-08-22 13:36:16 -0500654 _notes.append(
655 self.errors.get_error_type_text(
656 self.errors.NET_MTU_MISMATCH
657 )
658 )
Alexb3dc8592019-06-11 13:20:36 -0500659 _rc_mtu_s = "/" + _rc_mtu_s
Alexab232e42019-06-06 19:44:34 -0500660 _mtu_error = "*"
661 else:
662 # empty the matched value
663 _rc_mtu_s = ""
Alex3b8e5432019-06-11 15:21:59 -0500664 elif _host['mtu'] != '1500' and \
665 _proto not in ["-", "dhcp"]:
Alexe0c5b9e2019-04-23 18:51:23 -0500666 # there is no MTU value in reclass
667 # and runtime value is not default
668 self.errors.add_error(
669 self.errors.NET_MTU_EMPTY,
670 host=hostname,
Alex6b633ec2019-06-06 19:44:34 -0500671 if_name=_if_name,
Alexe0c5b9e2019-04-23 18:51:23 -0500672 if_cidr=_ip_str,
673 if_mtu=_host['mtu']
674 )
Alex836fac82019-08-22 13:36:16 -0500675 _notes.append(
676 self.errors.get_error_type_text(
677 self.errors.NET_MTU_EMPTY
678 )
679 )
Alexab232e42019-06-06 19:44:34 -0500680 _mtu_error = "*"
Alexe0c5b9e2019-04-23 18:51:23 -0500681 else:
682 # this is a VIP
Alex6b633ec2019-06-06 19:44:34 -0500683 _if_name = " "*7
Alex6b633ec2019-06-06 19:44:34 -0500684 _if_name_suffix = ""
Alexe0c5b9e2019-04-23 18:51:23 -0500685 _ip_str += " VIP"
Alex836fac82019-08-22 13:36:16 -0500686 # Save all data
687 _values = {
688 "interface": _if_name,
689 "interface_error": _if_rc,
690 "interface_note": _if_name_suffix,
Alex1839bbf2019-08-22 17:17:21 -0500691 "interface_map": "\n".join(_host['lines']),
692 "interface_matrix": _host['matrix'],
Alex836fac82019-08-22 13:36:16 -0500693 "ip_address": _ip_str,
694 "address_type": _proto,
695 "rt_mtu": _host['mtu'],
696 "rc_mtu": _rc_mtu_s,
697 "mtu_error": _mtu_error,
698 "status": _host['state'],
699 "status_error": _up_error,
700 "subnet_gateway": _gate,
701 "subnet_gateway_error": _gate_error,
702 "default_gateway": _d_gate_str,
703 "raw_data": _raw,
704 "error_note": " and ".join(_notes)
705 }
706 if node in _map[_net]:
707 # add if to host
708 _map[_net][node].append(_values)
709 else:
710 _map[_net][node] = [_values]
711 _notes = []
712
713 # save map
714 self.map = _map
Alex836fac82019-08-22 13:36:16 -0500715 return
716
717 def print_map(self):
718 """
719 Create text report for CLI
720
721 :return: none
722 """
723 logger_cli.info("# Networks")
724 logger_cli.info(
725 " {0:8} {1:25} {2:25} {3:6} {4:10} {5:10} {6}/{7}".format(
726 "Host",
727 "IF",
728 "IP",
729 "Proto",
730 "MTU",
731 "State",
732 "Gate",
733 "Def.Gate"
734 )
735 )
736 for network in self.map.keys():
737 logger_cli.info("-> {}".format(network))
738 for hostname in self.map[network].keys():
739 node = hostname.split('.')[0]
740 _n = self.map[network][hostname]
741 for _i in _n:
742 # Host IF IP Proto MTU State Gate Def.Gate
743 _text = "{:7} {:17} {:25} {:6} {:10} " \
744 "{:10} {} / {}".format(
745 _i['interface'] + _i['interface_error'],
746 _i['interface_note'],
747 _i['ip_address'],
748 _i['address_type'],
749 _i['rt_mtu'] + _i['rc_mtu'] + _i['mtu_error'],
750 _i['status'] + _i['status_error'],
751 _i['subnet_gateway'] +
752 _i['subnet_gateway_error'],
753 _i['default_gateway']
Alexe0c5b9e2019-04-23 18:51:23 -0500754 )
Alexe0c5b9e2019-04-23 18:51:23 -0500755 logger_cli.info(
Alex836fac82019-08-22 13:36:16 -0500756 " {0:8} {1}".format(
757 node,
758 _text
759 )
Alexe0c5b9e2019-04-23 18:51:23 -0500760 )
Alex836fac82019-08-22 13:36:16 -0500761
762 # logger_cli.info("\n# Other networks")
763 # _other = [n for n in _runtime if n not in _reclass]
764 # for network in _other:
765 # logger_cli.info("-> {}".format(str(network)))
766 # names = sorted(_runtime[network].keys())
767
768 # for hostname in names:
769 # for _n in _runtime[network][hostname]:
770 # _ifs = [str(ifs.ip) for ifs in _n['ifs']]
771 # _text = "{:25} {:25} {:6} {:10} {}".format(
772 # _n['name'],
773 # ", ".join(_ifs),
774 # "-",
775 # _n['mtu'],
776 # _n['state']
777 # )
778 # logger_cli.info(
779 # " {0:8} {1}".format(hostname.split('.')[0], _text)
780 # )
781 # logger_cli.info("\n")
Alex1f90e7b2021-09-03 15:31:28 -0500782 return
Alex205546c2020-12-30 19:22:30 -0600783
784
785class KubeNetworkMapper(NetworkMapper):
786 def __init__(
787 self,
788 config,
789 errors_class=None,
790 skip_list=None,
791 skip_list_file=None
792 ):
793 self.master = KubeNodes(config)
Alex7b0ee9a2021-09-21 17:16:17 -0500794 self.daemonset = None
Alex205546c2020-12-30 19:22:30 -0600795 super(KubeNetworkMapper, self).__init__(
796 config,
797 errors_class=errors_class,
798 skip_list=skip_list,
799 skip_list_file=skip_list_file
800 )
Alex1f90e7b2021-09-03 15:31:28 -0500801
Alex7b0ee9a2021-09-21 17:16:17 -0500802 def get_daemonset(self):
803 if not self.daemonset:
804 _d = self.master.prepare_daemonset("daemonset_template.yaml")
805
806 # wait for daemonset, normally less than 60 sec for all
807 # but still, let us give it 10 second per pod
808 _timeout = self.master.nodes.__len__() * 10
809 if not self.master.wait_for_daemonset(_d, timeout=_timeout):
810 raise KubeException("Daemonset deployment fail")
811 self.daemonset = _d
812 return self.daemonset
813
Alexb2129542021-11-23 15:49:42 -0600814 def get_script_output(self, script, _args=None):
Alex1f90e7b2021-09-03 15:31:28 -0500815 """
816 Get runtime network by creating DaemonSet with Host network parameter
817 """
818 # prepare daemonset
819 logger_cli.info("-> Preparing daemonset to get node info")
Alex7b0ee9a2021-09-21 17:16:17 -0500820 _daemonset = self.get_daemonset()
Alex1f90e7b2021-09-03 15:31:28 -0500821 logger_cli.info("-> Running script on daemonset")
822 # exec script on all pods in daemonset
Alexb78191f2021-11-02 16:35:46 -0500823 _result = self.master.execute_cmd_on_daemon_set(
Alex1f90e7b2021-09-03 15:31:28 -0500824 _daemonset,
825 script,
Alexb2129542021-11-23 15:49:42 -0600826 _args=_args,
Alexb78191f2021-11-02 16:35:46 -0500827 is_script=True
Alex1f90e7b2021-09-03 15:31:28 -0500828 )
829
830 # delete daemonset
Alex7b0ee9a2021-09-21 17:16:17 -0500831 # TODO: handle daemonset delete properly
832 # self.master.delete_daemonset(_daemonset)
Alex1f90e7b2021-09-03 15:31:28 -0500833
834 return _result
835
836 def map_networks(self):
Alex3cdb1bd2021-09-10 15:51:11 -0500837 logger_cli.info("-> Mapping runtime networks")
Alex1f90e7b2021-09-03 15:31:28 -0500838 self.map_network(self.RUNTIME)
839
840 def map_network(self, source):
Alex7b0ee9a2021-09-21 17:16:17 -0500841 # if network type is mapped - just return it
842 if source in self.networks:
843 return self.networks[source]
Alex1f90e7b2021-09-03 15:31:28 -0500844 # maps target network using given source
845 _networks = None
Alex1f90e7b2021-09-03 15:31:28 -0500846 if source == self.RUNTIME:
847 logger_cli.info("# Mapping node runtime network data")
Alexb2129542021-11-23 15:49:42 -0600848 _r = self.get_script_output("ifs_data.py", _args="json")
Alex1f90e7b2021-09-03 15:31:28 -0500849 _networks = self._map_runtime_networks(_r)
850 else:
851 raise ConfigException(
852 "Network type not supported in 'Kube': '{}'".format(source)
853 )
854
855 self.networks[source] = _networks
856 return _networks
857
Alex3cdb1bd2021-09-10 15:51:11 -0500858 def create_map(self, skip_keywords=None):
Alex1f90e7b2021-09-03 15:31:28 -0500859 """Create all needed elements for map output
860
861 :return: none
862 """
Alex3cdb1bd2021-09-10 15:51:11 -0500863 # shortcut
Alex1f90e7b2021-09-03 15:31:28 -0500864 _runtime = self.networks[self.RUNTIME]
Alex3cdb1bd2021-09-10 15:51:11 -0500865 # networks to skip
866 _net_skip_list = []
Alex1f90e7b2021-09-03 15:31:28 -0500867 # main networks, target vars
868 _map = {}
869 # No matter of proto, at least one IP will be present for the network
870 # we interested in, since we are to make sure that L3 level
871 # is configured according to reclass model
872 for network in _runtime:
873 # shortcuts
874 _net = str(network)
875 _map[_net] = {}
876 # hostnames
877 names = sorted(_runtime[network].keys())
878 for hostname in names:
879 _notes = []
880 node = hostname.split('.')[0]
881 if not self.master.is_node_available(hostname, log=False):
882 logger_cli.info(
883 " {0:8} {1}".format(node, "node not available")
884 )
885 # add non-responsive node erorr
886 self.errors.add_error(
887 self.errors.NET_NODE_NON_RESPONSIVE,
888 host=hostname
889 )
890 _notes.append(
891 self.errors.get_error_type_text(
892 self.errors.NET_NODE_NON_RESPONSIVE
893 )
894 )
895 continue
896 # lookup interface name on node using network CIDR
897 _if_name = _runtime[network][hostname][0]["name"]
898 _raw = self.interfaces[hostname][_if_name]['runtime']
899 _if_name_suffix = ""
900 _a = _runtime[network][hostname]
901 for _host in _a:
902 for _if in _host['ifs']:
903 _ip_str = str(_if.exploded)
Alex3cdb1bd2021-09-10 15:51:11 -0500904 # Make sure we print VIP properly
905 if _host['vip'] == _ip_str:
906 _if_name = " "*7
907 _if_name_suffix = ""
908 _ip_str += " VIP"
Alex1f90e7b2021-09-03 15:31:28 -0500909
910 # Save all data
911 _values = {
912 "interface": _if_name,
913 "interface_note": _if_name_suffix,
914 "interface_map": "\n".join(_host['lines']),
915 "interface_matrix": _host['matrix'],
916 "ip_address": _ip_str,
917 "rt_mtu": _host['mtu'],
918 "status": _host['state'],
Alex3cdb1bd2021-09-10 15:51:11 -0500919 "type": _host['type'],
Alex1f90e7b2021-09-03 15:31:28 -0500920 "raw_data": _raw,
921 }
922 if node in _map[_net]:
923 # add if to host
924 _map[_net][node].append(_values)
925 else:
926 _map[_net][node] = [_values]
927 _notes = []
Alex3cdb1bd2021-09-10 15:51:11 -0500928 # process skips: if key substring found in interface name
929 # then skip the whole network.
930 if any([True for _w in skip_keywords if _w in _if_name]):
931 _net_skip_list.append(_net)
Alex1f90e7b2021-09-03 15:31:28 -0500932
Alex3cdb1bd2021-09-10 15:51:11 -0500933 # remove skipped networks from list
934 _net_skip_list = list(set(_net_skip_list))
935 for _net in _net_skip_list:
936 _map.pop(_net)
Alex1f90e7b2021-09-03 15:31:28 -0500937 # save map
938 self.map = _map
939 return
940
941 def print_map(self):
942 """
943 Create text report for CLI
944
945 :return: none
946 """
947 logger_cli.info("# Networks")
948 logger_cli.info(
Alex3cdb1bd2021-09-10 15:51:11 -0500949 " {0:47} {1:12} {2:25} {3:5} {4:4}".format(
Alex1f90e7b2021-09-03 15:31:28 -0500950 "Host",
951 "IF",
952 "IP",
953 "MTU",
954 "State"
955 )
956 )
957 for network in self.map.keys():
958 logger_cli.info("-> {}".format(network))
959 for hostname in self.map[network].keys():
960 node = hostname.split('.')[0]
961 _n = self.map[network][hostname]
962 for _i in _n:
963 # Host IF IP Proto MTU State Gate Def.Gate
Alex3cdb1bd2021-09-10 15:51:11 -0500964 _text = "{:10} {:2} {:25} {:5} {:4}".format(
Alex1f90e7b2021-09-03 15:31:28 -0500965 _i['interface'],
966 _i['interface_note'],
967 _i['ip_address'],
968 _i['rt_mtu'],
969 _i['status']
970 )
971 logger_cli.info(
Alex3cdb1bd2021-09-10 15:51:11 -0500972 " {0:47} {1}".format(
Alex1f90e7b2021-09-03 15:31:28 -0500973 node,
974 _text
975 )
976 )
977 return