Alex | 0989ecf | 2022-03-29 13:43:21 -0500 | [diff] [blame^] | 1 | # Author: Alex Savatieiev (osavatieiev@mirantis.com; a.savex@gmail.com) |
| 2 | # Copyright 2019-2022 Mirantis, Inc. |
Alex | e0c5b9e | 2019-04-23 18:51:23 -0500 | [diff] [blame] | 3 | import ipaddress |
| 4 | import json |
| 5 | |
Alex | 92e07ce | 2019-05-31 16:00:03 -0500 | [diff] [blame] | 6 | from cfg_checker.common import logger_cli |
Alex | e0c5b9e | 2019-04-23 18:51:23 -0500 | [diff] [blame] | 7 | from cfg_checker.helpers.console_utils import Progress |
Alex | e0c5b9e | 2019-04-23 18:51:23 -0500 | [diff] [blame] | 8 | |
| 9 | |
Alex | e0c5b9e | 2019-04-23 18:51:23 -0500 | [diff] [blame] | 10 | # This is independent class with a salt.nodes input |
| 11 | class NetworkPinger(object): |
Alex | e9908f7 | 2020-05-19 16:04:53 -0500 | [diff] [blame] | 12 | def __init__( |
| 13 | self, |
Alex | aae5804 | 2020-12-31 11:48:28 -0600 | [diff] [blame] | 14 | mapper, |
Alex | e9908f7 | 2020-05-19 16:04:53 -0500 | [diff] [blame] | 15 | detailed=False, |
Alex | e9908f7 | 2020-05-19 16:04:53 -0500 | [diff] [blame] | 16 | skip_list=None, |
| 17 | skip_list_file=None |
| 18 | ): |
Alex | 9a4ad21 | 2020-10-01 18:04:25 -0500 | [diff] [blame] | 19 | logger_cli.info("# Initializing Pinger") |
Alex | aae5804 | 2020-12-31 11:48:28 -0600 | [diff] [blame] | 20 | self.mapper = mapper |
Alex | e0c5b9e | 2019-04-23 18:51:23 -0500 | [diff] [blame] | 21 | # only data |
Alex | e0c5b9e | 2019-04-23 18:51:23 -0500 | [diff] [blame] | 22 | self.detailed_summary = detailed |
Alex | 7b0ee9a | 2021-09-21 17:16:17 -0500 | [diff] [blame] | 23 | self.results = {} |
Alex | e0c5b9e | 2019-04-23 18:51:23 -0500 | [diff] [blame] | 24 | |
| 25 | def _collect_node_addresses(self, target_net): |
| 26 | # use reclass model and standard methods |
| 27 | # to create list of nodes with target network |
Alex | 7b0ee9a | 2021-09-21 17:16:17 -0500 | [diff] [blame] | 28 | _networks = self.mapper.map_network(self.mapper.RUNTIME) |
| 29 | if target_net in _networks: |
| 30 | return _networks[target_net] |
Alex | e0c5b9e | 2019-04-23 18:51:23 -0500 | [diff] [blame] | 31 | else: |
| 32 | logger_cli.info( |
Alex | 7b0ee9a | 2021-09-21 17:16:17 -0500 | [diff] [blame] | 33 | "# Target network of {} not found after mapping".format( |
Alex | e0c5b9e | 2019-04-23 18:51:23 -0500 | [diff] [blame] | 34 | target_net.exploded |
| 35 | ) |
| 36 | ) |
| 37 | return None |
| 38 | |
Alex | 7b0ee9a | 2021-09-21 17:16:17 -0500 | [diff] [blame] | 39 | def _get_packets_data(self, network_cidr_str, mtu): |
Alex | e0c5b9e | 2019-04-23 18:51:23 -0500 | [diff] [blame] | 40 | # Conduct actual ping using network CIDR |
Alex | 7b0ee9a | 2021-09-21 17:16:17 -0500 | [diff] [blame] | 41 | logger_cli.debug("... collecting node pairs") |
Alex | 3bc95f6 | 2020-03-05 17:00:04 -0600 | [diff] [blame] | 42 | _fake_if = ipaddress.IPv4Interface(str(network_cidr_str)) |
Alex | e0c5b9e | 2019-04-23 18:51:23 -0500 | [diff] [blame] | 43 | _net = _fake_if.network |
| 44 | # collect nodes and ips from reclass |
| 45 | nodes = self._collect_node_addresses(_net) |
| 46 | # build list of packets to be sent |
| 47 | # source -> target |
| 48 | _count = 0 |
| 49 | _packets = {} |
| 50 | _nodes = sorted(nodes.keys()) |
| 51 | _nodes_total = len(_nodes) |
| 52 | logger_cli.info("-> {} nodes found within subnet of '{}'".format( |
| 53 | _nodes_total, |
| 54 | network_cidr_str |
| 55 | )) |
| 56 | while len(_nodes) > 0: |
| 57 | src_host = _nodes.pop() |
| 58 | src_data = nodes[src_host] |
| 59 | src_if_name = src_data[0]['name'] |
| 60 | src_ips = [str(_if.ip) for _if in src_data[0]['ifs']] |
| 61 | _packets[src_host] = { |
| 62 | "ip": src_ips[0], |
| 63 | "if_name": src_if_name, |
| 64 | "targets": {} |
| 65 | } |
| 66 | |
Alex | 3bc95f6 | 2020-03-05 17:00:04 -0600 | [diff] [blame] | 67 | for tgt_host, tgt_data in nodes.items(): |
Alex | e9547d8 | 2019-06-03 15:22:50 -0500 | [diff] [blame] | 68 | _t = _packets[src_host]["targets"] |
Alex | e0c5b9e | 2019-04-23 18:51:23 -0500 | [diff] [blame] | 69 | for tgt_if in tgt_data: |
| 70 | tgt_if_name = tgt_if['name'] |
| 71 | _ip_index = 0 |
| 72 | for tgt_ip in tgt_if['ifs']: |
| 73 | _ip = str(tgt_ip.ip) |
| 74 | if _ip not in src_ips: |
Alex | e9547d8 | 2019-06-03 15:22:50 -0500 | [diff] [blame] | 75 | if tgt_host not in _t: |
| 76 | _t[tgt_host] = [] |
Alex | 7b0ee9a | 2021-09-21 17:16:17 -0500 | [diff] [blame] | 77 | # Handling mtu and packet size |
| 78 | if mtu == 0: |
| 79 | # Detect MTU |
| 80 | _mtu = int(tgt_if['mtu']) |
| 81 | else: |
| 82 | _mtu = mtu |
| 83 | _packet_size = int(_mtu)-20-8 if _mtu != 0 else 0 |
Alex | e0c5b9e | 2019-04-23 18:51:23 -0500 | [diff] [blame] | 84 | _tgt = { |
| 85 | "ip": _ip, |
| 86 | "tgt_host": tgt_host, |
| 87 | "ip_index": _ip_index, |
| 88 | "if_name": tgt_if_name, |
Alex | 7b0ee9a | 2021-09-21 17:16:17 -0500 | [diff] [blame] | 89 | "mtu": _mtu, |
| 90 | "size": _packet_size |
Alex | e0c5b9e | 2019-04-23 18:51:23 -0500 | [diff] [blame] | 91 | } |
Alex | e9547d8 | 2019-06-03 15:22:50 -0500 | [diff] [blame] | 92 | _t[tgt_host].append( |
Alex | e0c5b9e | 2019-04-23 18:51:23 -0500 | [diff] [blame] | 93 | _tgt |
| 94 | ) |
| 95 | _count += 1 |
| 96 | _ip_index += 1 |
| 97 | else: |
| 98 | pass |
| 99 | logger_cli.info("-> {} packets to send".format(_count)) |
| 100 | |
Alex | 92e07ce | 2019-05-31 16:00:03 -0500 | [diff] [blame] | 101 | if not _count: |
| 102 | logger_cli.warning( |
| 103 | "\n# WARNING: No packets to send for '{}', " |
| 104 | "check network configuration\n".format(network_cidr_str) |
| 105 | ) |
| 106 | |
Alex | 7b0ee9a | 2021-09-21 17:16:17 -0500 | [diff] [blame] | 107 | return None, _packets, _nodes_total |
| 108 | else: |
| 109 | return _count, _packets, _nodes_total |
| 110 | |
| 111 | def _process_result(self, src, src_data, result): |
| 112 | # Parse output |
| 113 | try: |
| 114 | _result = json.loads(result) |
| 115 | except (ValueError, TypeError): |
| 116 | self.mapper.errors.add_error( |
| 117 | self.mapper.errors.NET_NODE_NON_RESPONSIVE, |
| 118 | node=src, |
| 119 | response=result |
| 120 | ) |
| 121 | |
| 122 | _msg = "# ERROR: Unexpected return for '{}': '{}'\n".format( |
| 123 | src, |
| 124 | result |
| 125 | ) |
| 126 | |
| 127 | return False, _msg |
| 128 | |
| 129 | # Handle return codes |
| 130 | for tgt_node, _tgt_ips in _result.items(): |
| 131 | for _params in _tgt_ips: |
| 132 | _body = "{}({}) --{}--> {}({}@{})\n".format( |
| 133 | src, |
| 134 | src_data["if_name"], |
| 135 | _params["returncode"], |
| 136 | tgt_node, |
| 137 | _params["if_name"], |
| 138 | _params["ip"] |
| 139 | ) |
| 140 | _stdout = "" |
| 141 | _stderr = "" |
| 142 | if len(_params["stdout"]) > 0: |
| 143 | _stdout = "stdout:\n{}\n".format(_params["stdout"]) |
| 144 | if len(_params["stderr"]) > 0: |
| 145 | _stderr = "stderr:\n{}\n".format(_params["stderr"]) |
| 146 | |
| 147 | if not _params["returncode"]: |
| 148 | # 0 |
| 149 | self.mapper.errors.add_error( |
| 150 | self.mapper.errors.NET_PING_SUCCESS, |
| 151 | ping_path=_body, |
| 152 | stdout=_stdout, |
| 153 | stderr=_stderr |
| 154 | ) |
| 155 | elif _params["returncode"] == 68: |
| 156 | # 68 is a 'can't resove host error' |
| 157 | self.mapper.errors.add_error( |
| 158 | self.mapper.errors.NET_PING_NOT_RESOLVED, |
| 159 | ping_path=_body, |
| 160 | stdout=_stdout, |
| 161 | stderr=_stderr |
| 162 | ) |
| 163 | elif _params["returncode"] > 1: |
| 164 | # >1 is when no actial (any) response |
| 165 | self.mapper.errors.add_error( |
| 166 | self.mapper.errors.NET_PING_ERROR, |
| 167 | ping_path=_body, |
| 168 | stdout=_stdout, |
| 169 | stderr=_stderr |
| 170 | ) |
| 171 | else: |
| 172 | # 1 is for timeouts amd/or packet lost |
| 173 | self.mapper.errors.add_error( |
| 174 | self.mapper.errors.NET_PING_TIMEOUT, |
| 175 | ping_path=_body, |
| 176 | stdout=_stdout, |
| 177 | stderr=_stderr |
| 178 | ) |
| 179 | return True, _result |
| 180 | |
| 181 | def print_summary(self, cidr, data): |
| 182 | # Print summary of ping activity in node-node perspective |
| 183 | logger_cli.info("\n-> {}, {} nodes".format(cidr, len(data))) |
| 184 | # iterate nodes |
| 185 | for _n, _d in data.items(): |
| 186 | # targets |
| 187 | _total = len(_d['targets']) |
| 188 | _fail = [] |
| 189 | _pass = [] |
| 190 | _mtus = [] |
| 191 | for _f, _t in _d['targets'].items(): |
| 192 | # filter data |
| 193 | _fail += [[_f, _l] for _l in _t if _l['returncode']] |
| 194 | _pass += [[_f, _l] for _l in _t if not _l['returncode']] |
| 195 | _mtus += [str(_l['mtu']) for _l in _t] |
| 196 | # summary of passed |
| 197 | # source node |
| 198 | logger_cli.info( |
| 199 | " PASS: {}/{} nodes from {} ({}:{}) with MTU {}".format( |
| 200 | len(_pass), |
| 201 | _total, |
| 202 | _n, |
| 203 | _d['if_name'], |
| 204 | _d['ip'], |
| 205 | ",".join(list(set(_mtus))) |
| 206 | ) |
| 207 | ) |
| 208 | # print fails |
| 209 | for node, _dict in _fail: |
| 210 | logger_cli.info( |
| 211 | "\tFAIL: {} ({}:{}) with {}/{}".format( |
| 212 | node, |
| 213 | _dict['if_name'], |
| 214 | _dict['ip'], |
| 215 | _dict['size'], |
| 216 | _dict['mtu'] |
| 217 | ) |
| 218 | ) |
| 219 | |
| 220 | # logger_cli.info(self.mapper.errors.get_summary(print_zeros=False)) |
| 221 | return |
| 222 | |
| 223 | def print_details(self, cidr, data): |
| 224 | def _print_stds(stdout, stderr): |
| 225 | logger_cli.debug(" stdout:\n") |
| 226 | for line in stdout.splitlines(): |
| 227 | logger_cli.debug(" {}".format(line)) |
| 228 | if not stderr: |
| 229 | logger_cli.debug(" stderr: <empty>") |
| 230 | else: |
| 231 | logger_cli.debug(" stderr:\n") |
| 232 | for line in stderr.splitlines(): |
| 233 | logger_cli.debug(" {}".format(line)) |
| 234 | |
| 235 | logger_cli.info("\n---> {}, {} nodes".format(cidr, len(data))) |
| 236 | # iterate nodes |
| 237 | for _n, _d in data.items(): |
| 238 | # targets |
| 239 | _fail = [] |
| 240 | _pass = [] |
| 241 | for _f, _t in _d['targets'].items(): |
| 242 | # filter data |
| 243 | _fail += [[_f, _l] for _l in _t if _l['returncode']] |
| 244 | _pass += [[_f, _l] for _l in _t if not _l['returncode']] |
| 245 | # summary of passed |
| 246 | # source node |
| 247 | logger_cli.info( |
| 248 | "======= from {} ({}:{})".format( |
| 249 | _n, |
| 250 | _d['if_name'], |
| 251 | _d['ip'] |
| 252 | ) |
| 253 | ) |
| 254 | for node, _dict in _pass: |
| 255 | logger_cli.info( |
| 256 | " + PASS: to {} ({}:{}) with {}/{}".format( |
| 257 | node, |
| 258 | _dict['if_name'], |
| 259 | _dict['ip'], |
| 260 | _dict['size'], |
| 261 | _dict['mtu'] |
| 262 | ) |
| 263 | ) |
| 264 | _print_stds(_dict['stdout'], _dict['stderr']) |
| 265 | # print fails |
| 266 | for node, _dict in _fail: |
| 267 | logger_cli.info( |
| 268 | " - FAIL: to {} ({}:{}) with {}/{}".format( |
| 269 | node, |
| 270 | _dict['if_name'], |
| 271 | _dict['ip'], |
| 272 | _dict['size'], |
| 273 | _dict['mtu'] |
| 274 | ) |
| 275 | ) |
| 276 | _print_stds(_dict['stdout'], _dict['stderr']) |
| 277 | |
| 278 | # Detailed errors |
| 279 | # logger_cli.info( |
| 280 | # "\n{}\n".format( |
| 281 | # self.mapper.errors.get_errors() |
| 282 | # ) |
| 283 | # ) |
| 284 | return |
| 285 | |
| 286 | |
| 287 | class SaltNetworkPinger(NetworkPinger): |
| 288 | def __init__( |
| 289 | self, |
| 290 | mapper, |
| 291 | detailed=False, |
| 292 | skip_list=None, |
| 293 | skip_list_file=None |
| 294 | ): |
| 295 | super(SaltNetworkPinger, self).__init__( |
| 296 | mapper, |
| 297 | detailed=detailed, |
| 298 | skip_list=skip_list, |
| 299 | skip_list_file=skip_list_file |
| 300 | ) |
| 301 | |
| 302 | def ping_nodes(self, network_cidr_str, mtu): |
| 303 | # get packets |
| 304 | _count, _packets, _nodes_total = self._get_packets_data( |
| 305 | network_cidr_str, |
| 306 | mtu |
| 307 | ) |
| 308 | if not _count: |
| 309 | return None |
Alex | 92e07ce | 2019-05-31 16:00:03 -0500 | [diff] [blame] | 310 | |
Alex | e0c5b9e | 2019-04-23 18:51:23 -0500 | [diff] [blame] | 311 | # do ping of packets |
Alex | 7b0ee9a | 2021-09-21 17:16:17 -0500 | [diff] [blame] | 312 | logger_cli.info( |
| 313 | "-> Pinging nodes in {}".format(network_cidr_str)) |
Alex | aae5804 | 2020-12-31 11:48:28 -0600 | [diff] [blame] | 314 | self.mapper.master.prepare_script_on_active_nodes("ping.py") |
Alex | e0c5b9e | 2019-04-23 18:51:23 -0500 | [diff] [blame] | 315 | _progress = Progress(_count) |
| 316 | _progress_index = 0 |
| 317 | _node_index = 0 |
Alex | 3bc95f6 | 2020-03-05 17:00:04 -0600 | [diff] [blame] | 318 | for src, src_data in _packets.items(): |
Alex | e0c5b9e | 2019-04-23 18:51:23 -0500 | [diff] [blame] | 319 | _targets = src_data["targets"] |
| 320 | _node_index += 1 |
| 321 | # create 'targets.json' on source host |
Alex | aae5804 | 2020-12-31 11:48:28 -0600 | [diff] [blame] | 322 | _path = self.mapper.master.prepare_json_on_node( |
Alex | e0c5b9e | 2019-04-23 18:51:23 -0500 | [diff] [blame] | 323 | src, |
| 324 | _targets, |
| 325 | "targets.json" |
| 326 | ) |
| 327 | # execute ping.py |
Alex | aae5804 | 2020-12-31 11:48:28 -0600 | [diff] [blame] | 328 | _results = self.mapper.master.execute_script_on_node( |
Alex | e0c5b9e | 2019-04-23 18:51:23 -0500 | [diff] [blame] | 329 | src, |
| 330 | "ping.py", |
| 331 | args=[_path] |
| 332 | ) |
| 333 | _progress_index += len(_targets) |
| 334 | # print progress |
| 335 | _progress.write_progress( |
| 336 | _progress_index, |
| 337 | note='/ {}/{} nodes / current {}'.format( |
| 338 | _node_index, |
| 339 | _nodes_total, |
| 340 | src |
| 341 | ) |
| 342 | ) |
Alex | e0c5b9e | 2019-04-23 18:51:23 -0500 | [diff] [blame] | 343 | # Parse results back in place |
Alex | 7b0ee9a | 2021-09-21 17:16:17 -0500 | [diff] [blame] | 344 | _ret_code, _data = self._process_result( |
| 345 | src, src_data, |
| 346 | _results[src] |
| 347 | ) |
| 348 | if not _ret_code: |
| 349 | _progress.clearline() |
| 350 | logger_cli.error(_data) |
| 351 | continue |
| 352 | else: |
| 353 | src_data["targets"] = _data |
Alex | e0c5b9e | 2019-04-23 18:51:23 -0500 | [diff] [blame] | 354 | |
Alex | d9fd85e | 2019-05-16 16:58:24 -0500 | [diff] [blame] | 355 | _progress.end() |
Alex | e0c5b9e | 2019-04-23 18:51:23 -0500 | [diff] [blame] | 356 | |
Alex | 7b0ee9a | 2021-09-21 17:16:17 -0500 | [diff] [blame] | 357 | return _packets |
Alex | e0c5b9e | 2019-04-23 18:51:23 -0500 | [diff] [blame] | 358 | |
Alex | 92e07ce | 2019-05-31 16:00:03 -0500 | [diff] [blame] | 359 | |
Alex | 7b0ee9a | 2021-09-21 17:16:17 -0500 | [diff] [blame] | 360 | class KubeNetworkPinger(NetworkPinger): |
| 361 | def __init__( |
| 362 | self, |
| 363 | mapper, |
| 364 | detailed=False, |
| 365 | skip_list=None, |
| 366 | skip_list_file=None |
| 367 | ): |
| 368 | super(KubeNetworkPinger, self).__init__( |
| 369 | mapper, |
| 370 | detailed=detailed, |
| 371 | skip_list=skip_list, |
| 372 | skip_list_file=skip_list_file |
Alex | e0c5b9e | 2019-04-23 18:51:23 -0500 | [diff] [blame] | 373 | ) |
Alex | 7b0ee9a | 2021-09-21 17:16:17 -0500 | [diff] [blame] | 374 | |
| 375 | def ping_nodes(self, network_cidr_str, mtu): |
| 376 | # get packets |
| 377 | _count, _packets, _nodes_total = self._get_packets_data( |
| 378 | network_cidr_str, |
| 379 | mtu |
| 380 | ) |
| 381 | if not _count: |
| 382 | return None |
| 383 | |
| 384 | # do ping of packets |
| 385 | logger_cli.info( |
| 386 | "-> Pinging nodes in {}".format(network_cidr_str)) |
| 387 | _progress = Progress(_count) |
| 388 | _progress_index = 0 |
| 389 | _node_index = 0 |
| 390 | for src, src_data in _packets.items(): |
| 391 | _targets = src_data["targets"] |
| 392 | _node_index += 1 |
| 393 | # create 'targets.json' on source host |
| 394 | _ds = self.mapper.get_daemonset() |
| 395 | _pname = self.mapper.master.get_pod_name_in_daemonset_by_node( |
| 396 | src, |
| 397 | _ds |
| 398 | ) |
| 399 | _path = self.mapper.master.prepare_json_in_pod( |
| 400 | _pname, |
| 401 | _ds.metadata.namespace, |
| 402 | _targets, |
| 403 | "targets.json" |
| 404 | ) |
| 405 | # execute ping.py |
Alex | dcb792f | 2021-10-04 14:24:21 -0500 | [diff] [blame] | 406 | _result = self.mapper.master.exec_script_on_target_pod( |
Alex | 7b0ee9a | 2021-09-21 17:16:17 -0500 | [diff] [blame] | 407 | _pname, |
| 408 | "ping.py", |
| 409 | args=[_path] |
| 410 | ) |
| 411 | _progress_index += len(_targets) |
| 412 | # print progress |
| 413 | _progress.write_progress( |
| 414 | _progress_index, |
| 415 | note='/ {}/{} nodes / current {}'.format( |
| 416 | _node_index, |
| 417 | _nodes_total, |
| 418 | src |
| 419 | ) |
| 420 | ) |
| 421 | # Parse results back in place |
| 422 | _ret_code, _data = self._process_result( |
| 423 | src, src_data, |
| 424 | _result |
| 425 | ) |
| 426 | if not _ret_code: |
| 427 | _progress.clearline() |
| 428 | logger_cli.error(_data) |
| 429 | continue |
| 430 | else: |
| 431 | src_data["targets"] = _data |
| 432 | |
| 433 | _progress.end() |
| 434 | |
| 435 | return _packets |