aboutsummaryrefslogtreecommitdiffstats
path: root/app/discover/fetchers/cli/cli_access.py
diff options
context:
space:
mode:
Diffstat (limited to 'app/discover/fetchers/cli/cli_access.py')
-rw-r--r--app/discover/fetchers/cli/cli_access.py206
1 files changed, 206 insertions, 0 deletions
diff --git a/app/discover/fetchers/cli/cli_access.py b/app/discover/fetchers/cli/cli_access.py
new file mode 100644
index 0000000..1db84ea
--- /dev/null
+++ b/app/discover/fetchers/cli/cli_access.py
@@ -0,0 +1,206 @@
+###############################################################################
+# Copyright (c) 2017 Koren Lev (Cisco Systems), Yaron Yogev (Cisco Systems) #
+# and others #
+# #
+# All rights reserved. This program and the accompanying materials #
+# are made available under the terms of the Apache License, Version 2.0 #
+# which accompanies this distribution, and is available at #
+# http://www.apache.org/licenses/LICENSE-2.0 #
+###############################################################################
+import re
+import time
+
+from discover.fetcher import Fetcher
+from utils.binary_converter import BinaryConverter
+from utils.logging.console_logger import ConsoleLogger
+from utils.ssh_conn import SshConn
+
+
+class CliAccess(BinaryConverter, Fetcher):
+ connections = {}
+ ssh_cmd = "ssh -o StrictHostKeyChecking=no "
+ call_count_per_con = {}
+ max_call_count_per_con = 100
+ cache_lifetime = 60 # no. of seconds to cache results
+ cached_commands = {}
+
+ def __init__(self):
+ super().__init__()
+ self.log = ConsoleLogger()
+
+ @staticmethod
+ def is_gateway_host(ssh_to_host):
+ ssh_conn = SshConn(ssh_to_host)
+ return ssh_conn.is_gateway_host(ssh_to_host)
+
+ def run_on_gateway(self, cmd, ssh_to_host="", enable_cache=True,
+ use_sudo=True):
+ self.run(cmd, ssh_to_host=ssh_to_host, enable_cache=enable_cache,
+ on_gateway=True, use_sudo=use_sudo)
+
+ def run(self, cmd, ssh_to_host="", enable_cache=True, on_gateway=False,
+ ssh=None, use_sudo=True):
+ ssh_conn = ssh if ssh else SshConn(ssh_to_host)
+ if use_sudo and not cmd.strip().startswith("sudo "):
+ cmd = "sudo " + cmd
+ if not on_gateway and ssh_to_host \
+ and not ssh_conn.is_gateway_host(ssh_to_host):
+ cmd = self.ssh_cmd + ssh_to_host + " " + cmd
+ curr_time = time.time()
+ cmd_path = ssh_to_host + ',' + cmd
+ if enable_cache and cmd_path in self.cached_commands:
+ # try to re-use output from last call
+ cached = self.cached_commands[cmd_path]
+ if cached["timestamp"] + self.cache_lifetime < curr_time:
+ # result expired
+ self.cached_commands.pop(cmd_path, None)
+ else:
+ # result is good to use - skip the SSH call
+ self.log.info('CliAccess: ****** using cached result, ' +
+ 'host: ' + ssh_to_host + ', cmd: %s ******', cmd)
+ return cached["result"]
+
+ self.log.info('CliAccess: host: %s, cmd: %s', ssh_to_host, cmd)
+ ret = ssh_conn.exec(cmd)
+ self.cached_commands[cmd_path] = {"timestamp": curr_time, "result": ret}
+ return ret
+
+ def run_fetch_lines(self, cmd, ssh_to_host="", enable_cache=True):
+ out = self.run(cmd, ssh_to_host, enable_cache)
+ if not out:
+ return []
+ # first try to split lines by whitespace
+ ret = out.splitlines()
+ # if split by whitespace did not work, try splitting by "\\n"
+ if len(ret) == 1:
+ ret = [l for l in out.split("\\n") if l != ""]
+ return ret
+
+ # parse command output columns separated by whitespace
+ # since headers can contain whitespace themselves,
+ # it is the caller's responsibility to provide the headers
+ def parse_cmd_result_with_whitespace(self, lines, headers, remove_first):
+ if remove_first:
+ # remove headers line
+ del lines[:1]
+ results = [self.parse_line_with_ws(line, headers)
+ for line in lines]
+ return results
+
+ # parse command output with "|" column separators and "-" row separators
+ def parse_cmd_result_with_separators(self, lines):
+ headers = self.parse_headers_line_with_separators(lines[1])
+ # remove line with headers and formatting lines above it and below it
+ del lines[:3]
+ # remove formatting line in the end
+ lines.pop()
+ results = [self.parse_content_line_with_separators(line, headers)
+ for line in lines]
+ return results
+
+ # parse a line with columns separated by whitespace
+ def parse_line_with_ws(self, line, headers):
+ s = line if isinstance(line, str) else self.binary2str(line)
+ parts = [word.strip() for word in s.split() if word.strip()]
+ ret = {}
+ for i, p in enumerate(parts):
+ header = headers[i]
+ ret[header] = p
+ return ret
+
+ # parse a line with "|" column separators
+ def parse_line_with_separators(self, line):
+ s = self.binary2str(line)
+ parts = [word.strip() for word in s.split("|") if word.strip()]
+ # remove the ID field
+ del parts[:1]
+ return parts
+
+ def parse_headers_line_with_separators(self, line):
+ return self.parse_line_with_separators(line)
+
+ def parse_content_line_with_separators(self, line, headers):
+ content_parts = self.parse_line_with_separators(line)
+ content = {}
+ for i in range(0, len(content_parts)):
+ content[headers[i]] = content_parts[i]
+ return content
+
+ def merge_ws_spillover_lines(self, lines):
+ # with WS-separated output, extra output sometimes spills to next line
+ # detect that and add to the end of the previous line for our procesing
+ pending_line = None
+ fixed_lines = []
+ # remove headers line
+ for l in lines:
+ if l[0] == '\t':
+ # this is a spill-over line
+ if pending_line:
+ # add this line to the end of the previous line
+ pending_line = pending_line.strip() + "," + l.strip()
+ else:
+ # add the previous pending line to the fixed lines list
+ if pending_line:
+ fixed_lines.append(pending_line)
+ # make current line the pending line
+ pending_line = l
+ if pending_line:
+ fixed_lines.append(pending_line)
+ return fixed_lines
+
+ """
+ given output lines from CLI command like 'ip -d link show',
+ find lines belonging to section describing a specific interface
+ parameters:
+ - lines: list of strings, output of command
+ - header_regexp: regexp marking the start of the section
+ - end_regexp: regexp marking the end of the section
+ """
+ def get_section_lines(self, lines, header_regexp, end_regexp):
+ if not lines:
+ return []
+ header_re = re.compile(header_regexp)
+ start_pos = None
+ # find start_pos of section
+ line_count = len(lines)
+ for line_num in range(0, line_count-1):
+ matches = header_re.match(lines[line_num])
+ if matches:
+ start_pos = line_num
+ break
+ if not start_pos:
+ return []
+ # find end of section
+ end_pos = line_count
+ end_re = re.compile(end_regexp)
+ for line_num in range(start_pos+1, end_pos-1):
+ matches = end_re.match(lines[line_num])
+ if matches:
+ end_pos = line_num
+ break
+ return lines[start_pos:end_pos]
+
+ def get_object_data(self, o, lines, regexps):
+ """
+ find object data in output lines from CLI command
+ parameters:
+ - o: object (dict), to which we'll add attributes with the data found
+ - lines: list of strings
+ - regexps: dict, keys are attribute names, values are regexp to match
+ for finding the value of the attribute
+ """
+ for line in lines:
+ self.find_matching_regexps(o, line, regexps)
+ for regexp_tuple in regexps:
+ name = regexp_tuple['name']
+ if 'name' not in o and 'default' in regexp_tuple:
+ o[name] = regexp_tuple['default']
+
+ def find_matching_regexps(self, o, line, regexps):
+ for regexp_tuple in regexps:
+ name = regexp_tuple['name']
+ regex = regexp_tuple['re']
+ regex = re.compile(regex)
+ matches = regex.search(line)
+ if matches:
+ o[name] = matches.group(1)