blob: 5c1814848f99c067e6d102e244a443b0485b8c4b [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")
200
Alex3cdb1bd2021-09-10 15:51:11 -0500201 logger_cli.info("-> mapping runtime network IPs")
Alexe0c5b9e2019-04-23 18:51:23 -0500202 # match interfaces by IP subnets
Alex205546c2020-12-30 19:22:30 -0600203 for host, node_data in self.master.nodes.items():
204 if not self.master.is_node_available(host):
Alexe0c5b9e2019-04-23 18:51:23 -0500205 continue
206
Alex3bc95f62020-03-05 17:00:04 -0600207 for net_name, net_data in node_data['networks'].items():
Alexb3dc8592019-06-11 13:20:36 -0500208 # cut net name
209 _i = net_name.find('@')
210 _name = net_name if _i < 0 else net_name[:_i]
Alexe0c5b9e2019-04-23 18:51:23 -0500211 # get ips and calculate subnets
Alexb3dc8592019-06-11 13:20:36 -0500212 if _name in ['lo']:
Alexe0c5b9e2019-04-23 18:51:23 -0500213 # skip the localhost
214 continue
Alex6b633ec2019-06-06 19:44:34 -0500215 else:
216 # add collected data to interface storage
Alexb3dc8592019-06-11 13:20:36 -0500217 if _name not in self.interfaces[host]:
218 self.interfaces[host][_name] = \
Alex6b633ec2019-06-06 19:44:34 -0500219 deepcopy(_network_item)
Alexb3dc8592019-06-11 13:20:36 -0500220 self.interfaces[host][_name]['runtime'] = \
Alex6b633ec2019-06-06 19:44:34 -0500221 deepcopy(net_data)
222
Alexe0c5b9e2019-04-23 18:51:23 -0500223 # get data and make sure that wide mask goes first
224 _ip4s = sorted(
225 net_data['ipv4'],
226 key=lambda s: s[s.index('/'):]
227 )
228 for _ip_str in _ip4s:
229 # create interface class
230 _if = ipaddress.IPv4Interface(_ip_str)
231 # check if this is a VIP
232 # ...all those will have /32 mask
233 net_data['vip'] = None
234 if _if.network.prefixlen == 32:
235 net_data['vip'] = str(_if.exploded)
236 if 'name' not in net_data:
Alexb3dc8592019-06-11 13:20:36 -0500237 net_data['name'] = _name
Alexe0c5b9e2019-04-23 18:51:23 -0500238 if 'ifs' not in net_data:
239 net_data['ifs'] = [_if]
240 # map it
241 _runtime = self._map_network_for_host(
242 host,
243 _if,
244 _runtime,
245 net_data
246 )
247 else:
248 # data is already there, just add VIP
249 net_data['ifs'].append(_if)
250
Alex1839bbf2019-08-22 17:17:21 -0500251 def process_interface(lvl, interface, tree, res):
252 # get childs for each root
253 # tree row item (<if_name>, [<parents>], [<childs>])
Alex163aa042022-12-01 11:58:32 -0600254 if lvl > 50 or lvl < -50:
255 logger_cli.warning(
256 "WARNING: Recoursion depth limit ({}) reached "
257 "for {}".format(
258 lvl,
259 interface
260 )
261 )
262 return
Alex1839bbf2019-08-22 17:17:21 -0500263 if lvl not in tree:
264 # - no level - add it
265 tree[lvl] = {}
266 # there is such interface in this level?
267 if interface not in tree[lvl]:
268 # - IF not present
Alexf3dbe862019-10-07 15:17:04 -0500269 _n = ''
270 if interface not in res:
271 _n = 'unknown IF'
272 _p = None
273 _c = None
274 else:
275 # -- get parents, add
276 _p = res[interface]['lower']
277 # -- get childs, add
278 _c = res[interface]['upper']
279
Alex1839bbf2019-08-22 17:17:21 -0500280 # if None, put empty list
281 _p = _p if _p else []
Alex1839bbf2019-08-22 17:17:21 -0500282 # if None, put empty list
283 _c = _c if _c else []
284 tree[lvl].update({
285 interface: {
Alexf3dbe862019-10-07 15:17:04 -0500286 "note": _n,
Alex1839bbf2019-08-22 17:17:21 -0500287 "parents": _p,
288 "children": _c,
289 "size": len(_p) if len(_p) > len(_c) else len(_c)
290 }
291 })
292 for p_if in tree[lvl][interface]["parents"]:
293 # -- cycle: execute process for next parent, lvl-1
294 process_interface(lvl-1, p_if, tree, res)
295 for c_if in tree[lvl][interface]["children"]:
296 # -- cycle: execute process for next child, lvl+1
297 process_interface(lvl+1, c_if, tree, res)
298 else:
299 # - IF present - exit (been here already)
300 return
301
302 def _put(cNet, cIndex, _list):
Alexf3dbe862019-10-07 15:17:04 -0500303 _added = False
304 _actual_index = -1
305 # Check list len
306 _len = len(_list)
307 if cIndex >= _len:
308 # grow list to meet index
309 _list = _list + [''] * (cIndex - _len + 1)
310 _len = len(_list)
311
312 for _cI in range(cIndex, _len):
Alex1839bbf2019-08-22 17:17:21 -0500313 # add child per index
314 # if space is free
315 if not _list[_cI]:
316 _list[_cI] = cNet
Alexf3dbe862019-10-07 15:17:04 -0500317 _added = True
318 _actual_index = _cI
Alex1839bbf2019-08-22 17:17:21 -0500319 break
Alexf3dbe862019-10-07 15:17:04 -0500320 if not _added:
321 # grow list by one entry
322 _list = _list + [cNet]
323 _actual_index = len(_list) - 1
324 return _actual_index, _list
Alex1839bbf2019-08-22 17:17:21 -0500325
326 # build network hierachy
327 nr = node_data['networks']
328 # walk interface tree
329 for _ifname in node_data['networks']:
330 _tree = {}
331 _level = 0
332 process_interface(_level, _ifname, _tree, nr)
333 # save tree for node/if
334 node_data['networks'][_ifname]['tree'] = _tree
335
336 # debug, print built tree
337 # logger_cli.debug("# '{}'".format(_ifname))
Alex3bc95f62020-03-05 17:00:04 -0600338 lvls = list(_tree.keys())
Alex1839bbf2019-08-22 17:17:21 -0500339 lvls.sort()
340 n = len(lvls)
341 m = max([len(_tree[k].keys()) for k in _tree.keys()])
342 matrix = [["" for i in range(m)] for j in range(n)]
343 x = 0
344 while True:
345 _lv = lvls.pop(0)
346 # get all interfaces on this level
Alex3bc95f62020-03-05 17:00:04 -0600347 nets = iter(_tree[_lv].keys())
Alex1839bbf2019-08-22 17:17:21 -0500348 while True:
349 y = 0
350 # get next interface
Alex3bc95f62020-03-05 17:00:04 -0600351 try:
352 _net = next(nets)
353 except StopIteration:
354 break
Alex1839bbf2019-08-22 17:17:21 -0500355 # all nets
356 _a = [_net]
357 # put current interface if this is only one left
358 if not _tree[_lv][_net]['children']:
359 if _net not in matrix[x]:
Alexf3dbe862019-10-07 15:17:04 -0500360 _, matrix[x] = _put(
361 _net,
362 y,
363 matrix[x]
364 )
Alex1839bbf2019-08-22 17:17:21 -0500365 y += 1
366 else:
367 # get all nets with same child
368 for _c in _tree[_lv][_net]['children']:
369 for _o_net in nets:
370 if _c in _tree[_lv][_o_net]['children']:
371 _a.append(_o_net)
372 # flush collected nets
373 for idx in range(len(_a)):
374 if _a[idx] in matrix[x]:
375 # there is such interface on this level
376 # get index
377 _nI = matrix[x].index(_a[idx])
Alexf3dbe862019-10-07 15:17:04 -0500378 _, matrix[x+1] = _put(
379 _c,
380 _nI,
381 matrix[x+1]
382 )
Alex1839bbf2019-08-22 17:17:21 -0500383 else:
384 # there is no such interface
385 # add it
Alexf3dbe862019-10-07 15:17:04 -0500386 _t, matrix[x] = _put(
387 _a[idx],
388 0,
389 matrix[x]
390 )
391 # also, put child
392 _, matrix[x+1] = _put(
393 _c,
394 _t,
395 matrix[x+1]
396 )
Alex1839bbf2019-08-22 17:17:21 -0500397 # remove collected nets from processing
398 if _a[idx] in nets:
399 nets.remove(_a[idx])
400 y += len(_a)
401 if not nets:
402 x += 1
403 break
404 if not lvls:
405 break
406
407 lines = []
408 _columns = [len(max([i for i in li])) for li in matrix]
409 for idx_y in range(m):
410 line = ""
411 for idx_x in range(n):
Alex9b2c1d12020-03-19 09:32:35 -0500412 _len = _columns[idx_x] if _columns[idx_x] else 1
413 _fmt = "{" + ":{}".format(_len) + "} "
Alex1839bbf2019-08-22 17:17:21 -0500414 line += _fmt.format(matrix[idx_x][idx_y])
415 lines.append(line)
416 node_data['networks'][_ifname]['matrix'] = matrix
417 node_data['networks'][_ifname]['lines'] = lines
Alexe0c5b9e2019-04-23 18:51:23 -0500418 return _runtime
419
Alex1f90e7b2021-09-03 15:31:28 -0500420
421class SaltNetworkMapper(NetworkMapper):
422 def __init__(
423 self,
424 config,
425 errors_class=None,
426 skip_list=None,
427 skip_list_file=None
428 ):
429 self.master = SaltNodes(config)
430 super(SaltNetworkMapper, self).__init__(
431 config,
432 errors_class=errors_class,
433 skip_list=skip_list,
434 skip_list_file=skip_list_file
435 )
436
437 def get_script_output(self):
438 """
439 Get runtime networks by executing script on nodes
440 """
441 logger_cli.info("# Mapping node runtime network data")
442 self.master.prepare_script_on_active_nodes("ifs_data.py")
443 _result = self.master.execute_script_on_active_nodes(
444 "ifs_data.py",
445 args="json"
446 )
447
448 return _result
449
450 def map_networks(self):
Alex3cdb1bd2021-09-10 15:51:11 -0500451 logger_cli.info("-> Mapping reclass networks")
Alex1f90e7b2021-09-03 15:31:28 -0500452 self.map_network(self.RECLASS)
Alex3cdb1bd2021-09-10 15:51:11 -0500453 logger_cli.info("-> Mapping runtime networks")
Alex1f90e7b2021-09-03 15:31:28 -0500454 self.map_network(self.RUNTIME)
455
Alexe0c5b9e2019-04-23 18:51:23 -0500456 def map_network(self, source):
457 # maps target network using given source
458 _networks = None
459
460 if source == self.RECLASS:
461 _networks = self._map_reclass_networks()
462 elif source == self.CONFIG:
463 _networks = self._map_configured_networks()
464 elif source == self.RUNTIME:
Alex1f90e7b2021-09-03 15:31:28 -0500465 _r = self.get_script_output()
466 _networks = self._map_runtime_networks(_r)
Alexe0c5b9e2019-04-23 18:51:23 -0500467
468 self.networks[source] = _networks
469 return _networks
470
Alex3cdb1bd2021-09-10 15:51:11 -0500471 def create_map(self, skip_keywords=None):
Alex836fac82019-08-22 13:36:16 -0500472 """Create all needed elements for map output
Alexe0c5b9e2019-04-23 18:51:23 -0500473
474 :return: none
475 """
476 _runtime = self.networks[self.RUNTIME]
477 _reclass = self.networks[self.RECLASS]
Alex836fac82019-08-22 13:36:16 -0500478
479 # main networks, target vars
480 _map = {}
Alex6b633ec2019-06-06 19:44:34 -0500481 # No matter of proto, at least one IP will be present for the network
Alex836fac82019-08-22 13:36:16 -0500482 # we interested in, since we are to make sure that L3 level
483 # is configured according to reclass model
Alexe0c5b9e2019-04-23 18:51:23 -0500484 for network in _reclass:
485 # shortcuts
486 _net = str(network)
Alex836fac82019-08-22 13:36:16 -0500487 _map[_net] = {}
Alexe0c5b9e2019-04-23 18:51:23 -0500488 if network not in _runtime:
489 # reclass has network that not found in runtime
490 self.errors.add_error(
491 self.errors.NET_NO_RUNTIME_NETWORK,
492 reclass_net=str(network)
493 )
Alex1839bbf2019-08-22 17:17:21 -0500494 logger_cli.warn(
495 "WARN: {}: {}".format(
496 " No runtime network ", str(network)
497 )
498 )
Alexe0c5b9e2019-04-23 18:51:23 -0500499 continue
Alex6b633ec2019-06-06 19:44:34 -0500500 # hostnames
Alexe0c5b9e2019-04-23 18:51:23 -0500501 names = sorted(_runtime[network].keys())
502 for hostname in names:
Alex836fac82019-08-22 13:36:16 -0500503 _notes = []
Alex6b633ec2019-06-06 19:44:34 -0500504 node = hostname.split('.')[0]
Alex205546c2020-12-30 19:22:30 -0600505 if not self.master.is_node_available(hostname, log=False):
Alexe0c5b9e2019-04-23 18:51:23 -0500506 logger_cli.info(
Alex6b633ec2019-06-06 19:44:34 -0500507 " {0:8} {1}".format(node, "node not available")
Alexe0c5b9e2019-04-23 18:51:23 -0500508 )
509 # add non-responsive node erorr
510 self.errors.add_error(
511 self.errors.NET_NODE_NON_RESPONSIVE,
512 host=hostname
513 )
Alex836fac82019-08-22 13:36:16 -0500514 _notes.append(
515 self.errors.get_error_type_text(
516 self.errors.NET_NODE_NON_RESPONSIVE
517 )
518 )
Alexe0c5b9e2019-04-23 18:51:23 -0500519 continue
Alex6b633ec2019-06-06 19:44:34 -0500520 # lookup interface name on node using network CIDR
521 _if_name = _runtime[network][hostname][0]["name"]
Alex836fac82019-08-22 13:36:16 -0500522 _raw = self.interfaces[hostname][_if_name]['runtime']
Alex6b633ec2019-06-06 19:44:34 -0500523 # get proper reclass
524 _r = self.interfaces[hostname][_if_name]['reclass']
Alex6b633ec2019-06-06 19:44:34 -0500525 _if_name_suffix = ""
526 # get the proto value
Alex3b8e5432019-06-11 15:21:59 -0500527 if _r:
528 _if_rc = ""
529 else:
530 self.errors.add_error(
531 self.errors.NET_NODE_UNEXPECTED_IF,
532 host=hostname,
533 if_name=_if_name
534 )
Alex836fac82019-08-22 13:36:16 -0500535 _notes.append(
536 self.errors.get_error_type_text(
537 self.errors.NET_NODE_UNEXPECTED_IF
538 )
539 )
Alex3b8e5432019-06-11 15:21:59 -0500540 _if_rc = "*"
541
Alex6b633ec2019-06-06 19:44:34 -0500542 if "proto" in _r:
543 _proto = _r['proto']
Alexe0c5b9e2019-04-23 18:51:23 -0500544 else:
Alex6b633ec2019-06-06 19:44:34 -0500545 _proto = "-"
Alexe0c5b9e2019-04-23 18:51:23 -0500546
Alex6b633ec2019-06-06 19:44:34 -0500547 if "type" in _r:
548 _if_name_suffix += _r["type"]
549 if "use_interfaces" in _r:
550 _if_name_suffix += "->" + ",".join(_r["use_interfaces"])
551
552 if _if_name_suffix:
553 _if_name_suffix = "({})".format(_if_name_suffix)
554
Alex6b633ec2019-06-06 19:44:34 -0500555 # get gate and routes if proto is static
556 if _proto == 'static':
557 # get the gateway for current net
Alex205546c2020-12-30 19:22:30 -0600558 _routes = self.master.nodes[hostname]['routes']
Alex6b633ec2019-06-06 19:44:34 -0500559 _route = _routes[_net] if _net in _routes else None
Alex6b633ec2019-06-06 19:44:34 -0500560 # get the default gateway
561 if 'default' in _routes:
562 _d_gate = ipaddress.IPv4Address(
563 _routes['default']['gateway']
564 )
565 else:
566 _d_gate = None
Alexb3dc8592019-06-11 13:20:36 -0500567 _d_gate_str = str(_d_gate) if _d_gate else "No default!"
568 # match route with default
569 if not _route:
570 _gate = "?"
571 else:
572 _gate = _route['gateway'] if _route['gateway'] else "-"
Alex6b633ec2019-06-06 19:44:34 -0500573 else:
574 # in case of manual and dhcp, no check possible
575 _gate = "-"
576 _d_gate = "-"
Alex4067f002019-06-11 10:47:16 -0500577 _d_gate_str = "-"
Alex6b633ec2019-06-06 19:44:34 -0500578 # iterate through interfaces
Alexe0c5b9e2019-04-23 18:51:23 -0500579 _a = _runtime[network][hostname]
580 for _host in _a:
581 for _if in _host['ifs']:
Alexe0c5b9e2019-04-23 18:51:23 -0500582 _ip_str = str(_if.exploded)
Alexab232e42019-06-06 19:44:34 -0500583 _gate_error = ""
584 _up_error = ""
585 _mtu_error = ""
Alexe0c5b9e2019-04-23 18:51:23 -0500586
Alexb3dc8592019-06-11 13:20:36 -0500587 # Match gateway
Alexab232e42019-06-06 19:44:34 -0500588 if _proto == 'static':
Alexb3dc8592019-06-11 13:20:36 -0500589 # default reclass gate
Alex6b633ec2019-06-06 19:44:34 -0500590 _r_gate = "-"
591 if "gateway" in _r:
592 _r_gate = _r["gateway"]
Alexb3dc8592019-06-11 13:20:36 -0500593
Alexab232e42019-06-06 19:44:34 -0500594 # if values not match, put *
Alexb3dc8592019-06-11 13:20:36 -0500595 if _gate != _r_gate and _d_gate_str != _r_gate:
596 # if values not match, check if default match
Alex3b8e5432019-06-11 15:21:59 -0500597 self.errors.add_error(
598 self.errors.NET_UNEXPECTED_GATEWAY,
599 host=hostname,
600 if_name=_if_name,
601 ip=_ip_str,
602 gateway=_gate
603 )
Alex836fac82019-08-22 13:36:16 -0500604 _notes.append(
605 self.errors.get_error_type_text(
606 self.errors.NET_UNEXPECTED_GATEWAY
607 )
608 )
Alexab232e42019-06-06 19:44:34 -0500609 _gate_error = "*"
Alexe0c5b9e2019-04-23 18:51:23 -0500610
611 # IF status in reclass
Alex6b633ec2019-06-06 19:44:34 -0500612 _e = "enabled"
Alexab232e42019-06-06 19:44:34 -0500613 if _e not in _r:
Alex3b8e5432019-06-11 15:21:59 -0500614 self.errors.add_error(
615 self.errors.NET_NO_RC_IF_STATUS,
616 host=hostname,
617 if_name=_if_name
618 )
Alex836fac82019-08-22 13:36:16 -0500619 _notes.append(
620 self.errors.get_error_type_text(
621 self.errors.NET_NO_RC_IF_STATUS
622 )
623 )
Alexab232e42019-06-06 19:44:34 -0500624 _up_error = "*"
Alexe0c5b9e2019-04-23 18:51:23 -0500625
Alexe0c5b9e2019-04-23 18:51:23 -0500626 _rc_mtu = _r['mtu'] if 'mtu' in _r else None
Alexab232e42019-06-06 19:44:34 -0500627 _rc_mtu_s = ""
Alexe0c5b9e2019-04-23 18:51:23 -0500628 # check if this is a VIP address
629 # no checks needed if yes.
630 if _host['vip'] != _ip_str:
631 if _rc_mtu:
Alex3b8e5432019-06-11 15:21:59 -0500632 _rc_mtu_s = str(_rc_mtu)
Alexe0c5b9e2019-04-23 18:51:23 -0500633 # if there is an MTU value, match it
634 if _host['mtu'] != _rc_mtu_s:
635 self.errors.add_error(
636 self.errors.NET_MTU_MISMATCH,
637 host=hostname,
Alex6b633ec2019-06-06 19:44:34 -0500638 if_name=_if_name,
Alexe0c5b9e2019-04-23 18:51:23 -0500639 if_cidr=_ip_str,
640 reclass_mtu=_rc_mtu,
641 runtime_mtu=_host['mtu']
642 )
Alex836fac82019-08-22 13:36:16 -0500643 _notes.append(
644 self.errors.get_error_type_text(
645 self.errors.NET_MTU_MISMATCH
646 )
647 )
Alexb3dc8592019-06-11 13:20:36 -0500648 _rc_mtu_s = "/" + _rc_mtu_s
Alexab232e42019-06-06 19:44:34 -0500649 _mtu_error = "*"
650 else:
651 # empty the matched value
652 _rc_mtu_s = ""
Alex3b8e5432019-06-11 15:21:59 -0500653 elif _host['mtu'] != '1500' and \
654 _proto not in ["-", "dhcp"]:
Alexe0c5b9e2019-04-23 18:51:23 -0500655 # there is no MTU value in reclass
656 # and runtime value is not default
657 self.errors.add_error(
658 self.errors.NET_MTU_EMPTY,
659 host=hostname,
Alex6b633ec2019-06-06 19:44:34 -0500660 if_name=_if_name,
Alexe0c5b9e2019-04-23 18:51:23 -0500661 if_cidr=_ip_str,
662 if_mtu=_host['mtu']
663 )
Alex836fac82019-08-22 13:36:16 -0500664 _notes.append(
665 self.errors.get_error_type_text(
666 self.errors.NET_MTU_EMPTY
667 )
668 )
Alexab232e42019-06-06 19:44:34 -0500669 _mtu_error = "*"
Alexe0c5b9e2019-04-23 18:51:23 -0500670 else:
671 # this is a VIP
Alex6b633ec2019-06-06 19:44:34 -0500672 _if_name = " "*7
Alex6b633ec2019-06-06 19:44:34 -0500673 _if_name_suffix = ""
Alexe0c5b9e2019-04-23 18:51:23 -0500674 _ip_str += " VIP"
Alex836fac82019-08-22 13:36:16 -0500675 # Save all data
676 _values = {
677 "interface": _if_name,
678 "interface_error": _if_rc,
679 "interface_note": _if_name_suffix,
Alex1839bbf2019-08-22 17:17:21 -0500680 "interface_map": "\n".join(_host['lines']),
681 "interface_matrix": _host['matrix'],
Alex836fac82019-08-22 13:36:16 -0500682 "ip_address": _ip_str,
683 "address_type": _proto,
684 "rt_mtu": _host['mtu'],
685 "rc_mtu": _rc_mtu_s,
686 "mtu_error": _mtu_error,
687 "status": _host['state'],
688 "status_error": _up_error,
689 "subnet_gateway": _gate,
690 "subnet_gateway_error": _gate_error,
691 "default_gateway": _d_gate_str,
692 "raw_data": _raw,
693 "error_note": " and ".join(_notes)
694 }
695 if node in _map[_net]:
696 # add if to host
697 _map[_net][node].append(_values)
698 else:
699 _map[_net][node] = [_values]
700 _notes = []
701
702 # save map
703 self.map = _map
Alex836fac82019-08-22 13:36:16 -0500704 return
705
706 def print_map(self):
707 """
708 Create text report for CLI
709
710 :return: none
711 """
712 logger_cli.info("# Networks")
713 logger_cli.info(
714 " {0:8} {1:25} {2:25} {3:6} {4:10} {5:10} {6}/{7}".format(
715 "Host",
716 "IF",
717 "IP",
718 "Proto",
719 "MTU",
720 "State",
721 "Gate",
722 "Def.Gate"
723 )
724 )
725 for network in self.map.keys():
726 logger_cli.info("-> {}".format(network))
727 for hostname in self.map[network].keys():
728 node = hostname.split('.')[0]
729 _n = self.map[network][hostname]
730 for _i in _n:
731 # Host IF IP Proto MTU State Gate Def.Gate
732 _text = "{:7} {:17} {:25} {:6} {:10} " \
733 "{:10} {} / {}".format(
734 _i['interface'] + _i['interface_error'],
735 _i['interface_note'],
736 _i['ip_address'],
737 _i['address_type'],
738 _i['rt_mtu'] + _i['rc_mtu'] + _i['mtu_error'],
739 _i['status'] + _i['status_error'],
740 _i['subnet_gateway'] +
741 _i['subnet_gateway_error'],
742 _i['default_gateway']
Alexe0c5b9e2019-04-23 18:51:23 -0500743 )
Alexe0c5b9e2019-04-23 18:51:23 -0500744 logger_cli.info(
Alex836fac82019-08-22 13:36:16 -0500745 " {0:8} {1}".format(
746 node,
747 _text
748 )
Alexe0c5b9e2019-04-23 18:51:23 -0500749 )
Alex836fac82019-08-22 13:36:16 -0500750
751 # logger_cli.info("\n# Other networks")
752 # _other = [n for n in _runtime if n not in _reclass]
753 # for network in _other:
754 # logger_cli.info("-> {}".format(str(network)))
755 # names = sorted(_runtime[network].keys())
756
757 # for hostname in names:
758 # for _n in _runtime[network][hostname]:
759 # _ifs = [str(ifs.ip) for ifs in _n['ifs']]
760 # _text = "{:25} {:25} {:6} {:10} {}".format(
761 # _n['name'],
762 # ", ".join(_ifs),
763 # "-",
764 # _n['mtu'],
765 # _n['state']
766 # )
767 # logger_cli.info(
768 # " {0:8} {1}".format(hostname.split('.')[0], _text)
769 # )
770 # logger_cli.info("\n")
Alex1f90e7b2021-09-03 15:31:28 -0500771 return
Alex205546c2020-12-30 19:22:30 -0600772
773
774class KubeNetworkMapper(NetworkMapper):
775 def __init__(
776 self,
777 config,
778 errors_class=None,
779 skip_list=None,
780 skip_list_file=None
781 ):
782 self.master = KubeNodes(config)
Alex7b0ee9a2021-09-21 17:16:17 -0500783 self.daemonset = None
Alex205546c2020-12-30 19:22:30 -0600784 super(KubeNetworkMapper, self).__init__(
785 config,
786 errors_class=errors_class,
787 skip_list=skip_list,
788 skip_list_file=skip_list_file
789 )
Alex1f90e7b2021-09-03 15:31:28 -0500790
Alex7b0ee9a2021-09-21 17:16:17 -0500791 def get_daemonset(self):
792 if not self.daemonset:
793 _d = self.master.prepare_daemonset("daemonset_template.yaml")
794
795 # wait for daemonset, normally less than 60 sec for all
796 # but still, let us give it 10 second per pod
797 _timeout = self.master.nodes.__len__() * 10
798 if not self.master.wait_for_daemonset(_d, timeout=_timeout):
799 raise KubeException("Daemonset deployment fail")
800 self.daemonset = _d
801 return self.daemonset
802
Alexb2129542021-11-23 15:49:42 -0600803 def get_script_output(self, script, _args=None):
Alex1f90e7b2021-09-03 15:31:28 -0500804 """
805 Get runtime network by creating DaemonSet with Host network parameter
806 """
807 # prepare daemonset
808 logger_cli.info("-> Preparing daemonset to get node info")
Alex7b0ee9a2021-09-21 17:16:17 -0500809 _daemonset = self.get_daemonset()
Alex1f90e7b2021-09-03 15:31:28 -0500810 logger_cli.info("-> Running script on daemonset")
811 # exec script on all pods in daemonset
Alexb78191f2021-11-02 16:35:46 -0500812 _result = self.master.execute_cmd_on_daemon_set(
Alex1f90e7b2021-09-03 15:31:28 -0500813 _daemonset,
814 script,
Alexb2129542021-11-23 15:49:42 -0600815 _args=_args,
Alexb78191f2021-11-02 16:35:46 -0500816 is_script=True
Alex1f90e7b2021-09-03 15:31:28 -0500817 )
818
819 # delete daemonset
Alex7b0ee9a2021-09-21 17:16:17 -0500820 # TODO: handle daemonset delete properly
821 # self.master.delete_daemonset(_daemonset)
Alex1f90e7b2021-09-03 15:31:28 -0500822
823 return _result
824
825 def map_networks(self):
Alex3cdb1bd2021-09-10 15:51:11 -0500826 logger_cli.info("-> Mapping runtime networks")
Alex1f90e7b2021-09-03 15:31:28 -0500827 self.map_network(self.RUNTIME)
828
829 def map_network(self, source):
Alex7b0ee9a2021-09-21 17:16:17 -0500830 # if network type is mapped - just return it
831 if source in self.networks:
832 return self.networks[source]
Alex1f90e7b2021-09-03 15:31:28 -0500833 # maps target network using given source
834 _networks = None
Alex1f90e7b2021-09-03 15:31:28 -0500835 if source == self.RUNTIME:
836 logger_cli.info("# Mapping node runtime network data")
Alexb2129542021-11-23 15:49:42 -0600837 _r = self.get_script_output("ifs_data.py", _args="json")
Alex1f90e7b2021-09-03 15:31:28 -0500838 _networks = self._map_runtime_networks(_r)
839 else:
840 raise ConfigException(
841 "Network type not supported in 'Kube': '{}'".format(source)
842 )
843
844 self.networks[source] = _networks
845 return _networks
846
Alex3cdb1bd2021-09-10 15:51:11 -0500847 def create_map(self, skip_keywords=None):
Alex1f90e7b2021-09-03 15:31:28 -0500848 """Create all needed elements for map output
849
850 :return: none
851 """
Alex3cdb1bd2021-09-10 15:51:11 -0500852 # shortcut
Alex1f90e7b2021-09-03 15:31:28 -0500853 _runtime = self.networks[self.RUNTIME]
Alex3cdb1bd2021-09-10 15:51:11 -0500854 # networks to skip
855 _net_skip_list = []
Alex1f90e7b2021-09-03 15:31:28 -0500856 # main networks, target vars
857 _map = {}
858 # No matter of proto, at least one IP will be present for the network
859 # we interested in, since we are to make sure that L3 level
860 # is configured according to reclass model
861 for network in _runtime:
862 # shortcuts
863 _net = str(network)
864 _map[_net] = {}
865 # hostnames
866 names = sorted(_runtime[network].keys())
867 for hostname in names:
868 _notes = []
869 node = hostname.split('.')[0]
870 if not self.master.is_node_available(hostname, log=False):
871 logger_cli.info(
872 " {0:8} {1}".format(node, "node not available")
873 )
874 # add non-responsive node erorr
875 self.errors.add_error(
876 self.errors.NET_NODE_NON_RESPONSIVE,
877 host=hostname
878 )
879 _notes.append(
880 self.errors.get_error_type_text(
881 self.errors.NET_NODE_NON_RESPONSIVE
882 )
883 )
884 continue
885 # lookup interface name on node using network CIDR
886 _if_name = _runtime[network][hostname][0]["name"]
887 _raw = self.interfaces[hostname][_if_name]['runtime']
888 _if_name_suffix = ""
889 _a = _runtime[network][hostname]
890 for _host in _a:
891 for _if in _host['ifs']:
892 _ip_str = str(_if.exploded)
Alex3cdb1bd2021-09-10 15:51:11 -0500893 # Make sure we print VIP properly
894 if _host['vip'] == _ip_str:
895 _if_name = " "*7
896 _if_name_suffix = ""
897 _ip_str += " VIP"
Alex1f90e7b2021-09-03 15:31:28 -0500898
899 # Save all data
900 _values = {
901 "interface": _if_name,
902 "interface_note": _if_name_suffix,
903 "interface_map": "\n".join(_host['lines']),
904 "interface_matrix": _host['matrix'],
905 "ip_address": _ip_str,
906 "rt_mtu": _host['mtu'],
907 "status": _host['state'],
Alex3cdb1bd2021-09-10 15:51:11 -0500908 "type": _host['type'],
Alex1f90e7b2021-09-03 15:31:28 -0500909 "raw_data": _raw,
910 }
911 if node in _map[_net]:
912 # add if to host
913 _map[_net][node].append(_values)
914 else:
915 _map[_net][node] = [_values]
916 _notes = []
Alex3cdb1bd2021-09-10 15:51:11 -0500917 # process skips: if key substring found in interface name
918 # then skip the whole network.
919 if any([True for _w in skip_keywords if _w in _if_name]):
920 _net_skip_list.append(_net)
Alex1f90e7b2021-09-03 15:31:28 -0500921
Alex3cdb1bd2021-09-10 15:51:11 -0500922 # remove skipped networks from list
923 _net_skip_list = list(set(_net_skip_list))
924 for _net in _net_skip_list:
925 _map.pop(_net)
Alex1f90e7b2021-09-03 15:31:28 -0500926 # save map
927 self.map = _map
928 return
929
930 def print_map(self):
931 """
932 Create text report for CLI
933
934 :return: none
935 """
936 logger_cli.info("# Networks")
937 logger_cli.info(
Alex3cdb1bd2021-09-10 15:51:11 -0500938 " {0:47} {1:12} {2:25} {3:5} {4:4}".format(
Alex1f90e7b2021-09-03 15:31:28 -0500939 "Host",
940 "IF",
941 "IP",
942 "MTU",
943 "State"
944 )
945 )
946 for network in self.map.keys():
947 logger_cli.info("-> {}".format(network))
948 for hostname in self.map[network].keys():
949 node = hostname.split('.')[0]
950 _n = self.map[network][hostname]
951 for _i in _n:
952 # Host IF IP Proto MTU State Gate Def.Gate
Alex3cdb1bd2021-09-10 15:51:11 -0500953 _text = "{:10} {:2} {:25} {:5} {:4}".format(
Alex1f90e7b2021-09-03 15:31:28 -0500954 _i['interface'],
955 _i['interface_note'],
956 _i['ip_address'],
957 _i['rt_mtu'],
958 _i['status']
959 )
960 logger_cli.info(
Alex3cdb1bd2021-09-10 15:51:11 -0500961 " {0:47} {1}".format(
Alex1f90e7b2021-09-03 15:31:28 -0500962 node,
963 _text
964 )
965 )
966 return