From 2a1fa5367aeae7bb5ebed5b2750735923fce841c Mon Sep 17 00:00:00 2001 From: Gabor <82544733+phoenixszg@users.noreply.github.com> Date: Mon, 19 Jun 2023 11:53:18 +0200 Subject: [PATCH 1/5] CLI prompts (#1) * update telnet.py * update telnet.py * update ssh.py * update telnet.py * update telnet.py * update telnet.py * update telnet.py * update telnet.py --------- --- mb_netmgmt/ssh.py | 17 +++++++++++------ mb_netmgmt/telnet.py | 13 +++++++++++-- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/mb_netmgmt/ssh.py b/mb_netmgmt/ssh.py index 08c6e4e..e9f47a2 100644 --- a/mb_netmgmt/ssh.py +++ b/mb_netmgmt/ssh.py @@ -17,6 +17,7 @@ # You should have received a copy of the GNU General Public License # along with mb-netmgmt. If not, see #] ?$") # IOS + patterns.append(b"[\r\n\x00\x1b\[K]RP/\d+/(?:RS?P)?\d+\/CPU\d+:[^#]+(?:\([^\)]+\))?#$") # IOS XR + patterns.append(b"[\r\n\x00\x1b\[K](?P[\w/ .:,\(\)\-\?]*)(?P\[[\w/.:\-]*\])?(?(default)(?P(?:\?|: ?|)$)|(?P: $))") # Interactive prompt + patterns.append(b"[\r\n\x00\x1b\[K] --More-- $") # Terminal paging + message = self.read_message(self.channel.upstream, patterns) return {"response": message.decode()} - def read_message(self, channel, terminators): + def read_message(self, channel, patterns): message = b"" end_of_message = False while not end_of_message and not stopped: message += channel.recv(1024) - for terminator in terminators: - if terminator in message: + for pattern in patterns: + if re.findall(pattern, message): end_of_message = True + break return message diff --git a/mb_netmgmt/telnet.py b/mb_netmgmt/telnet.py index ba41205..ed57a0f 100644 --- a/mb_netmgmt/telnet.py +++ b/mb_netmgmt/telnet.py @@ -42,7 +42,8 @@ def handle(self): pass self.wfile.write(self.command_prompt) request = None - while request != b"exit\r\n": + self.stopped = False + while not self.stopped: request, request_id = self.read_request() if not request: return @@ -59,9 +60,17 @@ def read_request(self): def respond(self, response, request_id): self.wfile.write(response["response"].encode()) + if response["response"].encode() in [b"exit\r\n\r", b"quit\r\n\r"]: + self.stopped = True def read_proxy_response(self): - return {"response": self.telnet.read_until(self.command_prompt).decode()} + prompt_patterns = [] + prompt_patterns.append(b"[\r\n][\-\w+\.:/]+(?:\([^\)]+\))?[>#] ?$") # IOS + prompt_patterns.append(b"[\r\n\x00]RP/\d+/(?:RS?P)?\d+\/CPU\d+:[^#]+(?:\([^\)]+\))?#$") # IOS XR + prompt_patterns.append(b"[\r\n\x00](?P[\w/ .:,\(\)\-\?]*)(?P\[[\w/.:\-]*\])?(?(default)(?P(?:\?|: ?|)$)|(?P: $))") # Interactive prompt + prompt_patterns.append(b"[\r\n\x00] --More-- $") # Terminal paging + _, _, response = self.telnet.expect(prompt_patterns) + return {"response": response.decode()} def handle_username_prompt(self): username_prompt = b"Username: " From 95cb9b848504fa66aa9e94cf046ab2b7376ac07d Mon Sep 17 00:00:00 2001 From: Gabor <82544733+phoenixszg@users.noreply.github.com> Date: Mon, 19 Jun 2023 14:36:48 +0200 Subject: [PATCH 2/5] add initial command prompt to patterns (#2) * update telnet.py * update telnet.py * update ssh.py * update telnet.py * update telnet.py * update telnet.py * update telnet.py * update telnet.py * match for initial command as well --------- --- mb_netmgmt/ssh.py | 1 + mb_netmgmt/telnet.py | 1 + 2 files changed, 2 insertions(+) diff --git a/mb_netmgmt/ssh.py b/mb_netmgmt/ssh.py index e9f47a2..f347b58 100644 --- a/mb_netmgmt/ssh.py +++ b/mb_netmgmt/ssh.py @@ -90,6 +90,7 @@ def respond(self, response, request_id): def read_proxy_response(self): patterns = [] + patterns.append(self.channel.command_prompt) # Initial command prompt patterns.append(b"[\r\n\x1b\[K][\-\w+\.:/]+(?:\([^\)]+\))?[>#] ?$") # IOS patterns.append(b"[\r\n\x00\x1b\[K]RP/\d+/(?:RS?P)?\d+\/CPU\d+:[^#]+(?:\([^\)]+\))?#$") # IOS XR patterns.append(b"[\r\n\x00\x1b\[K](?P[\w/ .:,\(\)\-\?]*)(?P\[[\w/.:\-]*\])?(?(default)(?P(?:\?|: ?|)$)|(?P: $))") # Interactive prompt diff --git a/mb_netmgmt/telnet.py b/mb_netmgmt/telnet.py index ed57a0f..ceb82bf 100644 --- a/mb_netmgmt/telnet.py +++ b/mb_netmgmt/telnet.py @@ -65,6 +65,7 @@ def respond(self, response, request_id): def read_proxy_response(self): prompt_patterns = [] + prompt_patterns.append(self.command_prompt) # Initial command prompt prompt_patterns.append(b"[\r\n][\-\w+\.:/]+(?:\([^\)]+\))?[>#] ?$") # IOS prompt_patterns.append(b"[\r\n\x00]RP/\d+/(?:RS?P)?\d+\/CPU\d+:[^#]+(?:\([^\)]+\))?#$") # IOS XR prompt_patterns.append(b"[\r\n\x00](?P[\w/ .:,\(\)\-\?]*)(?P\[[\w/.:\-]*\])?(?(default)(?P(?:\?|: ?|)$)|(?P: $))") # Interactive prompt From 3b140d30292e631a3f70a4c651040c4841bb2876 Mon Sep 17 00:00:00 2001 From: Gabor <82544733+phoenixszg@users.noreply.github.com> Date: Thu, 22 Jun 2023 10:48:35 +0200 Subject: [PATCH 3/5] Define CLI patterns centrally (#3) * update telnet.py * update telnet.py * update ssh.py * update telnet.py * update telnet.py * update telnet.py * update telnet.py * update telnet.py * match for initial command as well * define cli patterns centrally * comment --------- --- mb_netmgmt/__main__.py | 7 +++++++ mb_netmgmt/ssh.py | 10 +++------- mb_netmgmt/telnet.py | 8 ++------ 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/mb_netmgmt/__main__.py b/mb_netmgmt/__main__.py index a6f8734..4f8fdf6 100755 --- a/mb_netmgmt/__main__.py +++ b/mb_netmgmt/__main__.py @@ -120,6 +120,13 @@ def save_key(self, proxy): def get_proxy(self, stub): return stub["responses"][0].get("proxy") + def get_cli_patterns(self): + patterns = [] + patterns.append(b"[\r\n\x1b\[K][\-\w+\.:/]+(?:\([^\)]+\))?[>#] ?$") # based on IOS driver of Exscript + patterns.append(b"[\r\n\x00\x1b\[K]RP/\d+/(?:RS?P)?\d+\/CPU\d+:[^#]+(?:\([^\)]+\))?#$") # based on IOS XR driver of Exscript + patterns.append(b"[\r\n\x00\x1b\[K](?P[\w/ .:,\(\)\-\?]*)(?P\[[\w/.:\-]*\])?(?(default)(?P(?:\?|: ?|)$)|(?P: $))") # Interactive prompt + patterns.append(b"[\r\n\x00\x1b\[K] --More-- $") # Terminal paging + return patterns def disable_algorithms(disabled_algorithms): # https://github.com/ncclient/ncclient/issues/526#issuecomment-1096563028 diff --git a/mb_netmgmt/ssh.py b/mb_netmgmt/ssh.py index f347b58..8be0ed1 100644 --- a/mb_netmgmt/ssh.py +++ b/mb_netmgmt/ssh.py @@ -89,13 +89,9 @@ def respond(self, response, request_id): return response def read_proxy_response(self): - patterns = [] - patterns.append(self.channel.command_prompt) # Initial command prompt - patterns.append(b"[\r\n\x1b\[K][\-\w+\.:/]+(?:\([^\)]+\))?[>#] ?$") # IOS - patterns.append(b"[\r\n\x00\x1b\[K]RP/\d+/(?:RS?P)?\d+\/CPU\d+:[^#]+(?:\([^\)]+\))?#$") # IOS XR - patterns.append(b"[\r\n\x00\x1b\[K](?P[\w/ .:,\(\)\-\?]*)(?P\[[\w/.:\-]*\])?(?(default)(?P(?:\?|: ?|)$)|(?P: $))") # Interactive prompt - patterns.append(b"[\r\n\x00\x1b\[K] --More-- $") # Terminal paging - message = self.read_message(self.channel.upstream, patterns) + prompt_patterns = [self.channel.command_prompt] + prompt_patterns += self.get_cli_patterns() + message = self.read_message(self.channel.upstream, prompt_patterns) return {"response": message.decode()} def read_message(self, channel, patterns): diff --git a/mb_netmgmt/telnet.py b/mb_netmgmt/telnet.py index ceb82bf..0648956 100644 --- a/mb_netmgmt/telnet.py +++ b/mb_netmgmt/telnet.py @@ -64,12 +64,8 @@ def respond(self, response, request_id): self.stopped = True def read_proxy_response(self): - prompt_patterns = [] - prompt_patterns.append(self.command_prompt) # Initial command prompt - prompt_patterns.append(b"[\r\n][\-\w+\.:/]+(?:\([^\)]+\))?[>#] ?$") # IOS - prompt_patterns.append(b"[\r\n\x00]RP/\d+/(?:RS?P)?\d+\/CPU\d+:[^#]+(?:\([^\)]+\))?#$") # IOS XR - prompt_patterns.append(b"[\r\n\x00](?P[\w/ .:,\(\)\-\?]*)(?P\[[\w/.:\-]*\])?(?(default)(?P(?:\?|: ?|)$)|(?P: $))") # Interactive prompt - prompt_patterns.append(b"[\r\n\x00] --More-- $") # Terminal paging + prompt_patterns = [self.command_prompt] + prompt_patterns += self.get_cli_patterns() _, _, response = self.telnet.expect(prompt_patterns) return {"response": response.decode()} From d290b6ca3a7fcaa71fc0d36fea97fb669d601052 Mon Sep 17 00:00:00 2001 From: Gabor <82544733+phoenixszg@users.noreply.github.com> Date: Thu, 22 Jun 2023 11:08:33 +0200 Subject: [PATCH 4/5] Black fromatting (#4) * update telnet.py * update telnet.py * update ssh.py * update telnet.py * update telnet.py * update telnet.py * update telnet.py * update telnet.py * match for initial command as well * define cli patterns centrally * comment * black --------- --- mb_netmgmt/__main__.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/mb_netmgmt/__main__.py b/mb_netmgmt/__main__.py index 4f8fdf6..17bcc79 100755 --- a/mb_netmgmt/__main__.py +++ b/mb_netmgmt/__main__.py @@ -122,12 +122,19 @@ def get_proxy(self, stub): def get_cli_patterns(self): patterns = [] - patterns.append(b"[\r\n\x1b\[K][\-\w+\.:/]+(?:\([^\)]+\))?[>#] ?$") # based on IOS driver of Exscript - patterns.append(b"[\r\n\x00\x1b\[K]RP/\d+/(?:RS?P)?\d+\/CPU\d+:[^#]+(?:\([^\)]+\))?#$") # based on IOS XR driver of Exscript - patterns.append(b"[\r\n\x00\x1b\[K](?P[\w/ .:,\(\)\-\?]*)(?P\[[\w/.:\-]*\])?(?(default)(?P(?:\?|: ?|)$)|(?P: $))") # Interactive prompt + patterns.append( + b"[\r\n\x1b\[K][\-\w+\.:/]+(?:\([^\)]+\))?[>#] ?$" + ) # based on IOS driver of Exscript + patterns.append( + b"[\r\n\x00\x1b\[K]RP/\d+/(?:RS?P)?\d+\/CPU\d+:[^#]+(?:\([^\)]+\))?#$" + ) # based on IOS XR driver of Exscript + patterns.append( + b"[\r\n\x00\x1b\[K](?P[\w/ .:,\(\)\-\?]*)(?P\[[\w/.:\-]*\])?(?(default)(?P(?:\?|: ?|)$)|(?P: $))" + ) # Interactive prompt patterns.append(b"[\r\n\x00\x1b\[K] --More-- $") # Terminal paging return patterns + def disable_algorithms(disabled_algorithms): # https://github.com/ncclient/ncclient/issues/526#issuecomment-1096563028 class MonkeyPatchedTransport(paramiko.Transport): From 25043a8253ae6d80b20c9d90879b44f5c51f105a Mon Sep 17 00:00:00 2001 From: Gabor <82544733+phoenixszg@users.noreply.github.com> Date: Thu, 22 Jun 2023 15:05:17 +0200 Subject: [PATCH 5/5] Test for prompt patterns (#5) * update telnet.py * update telnet.py * update ssh.py * update telnet.py * update telnet.py * update telnet.py * update telnet.py * update telnet.py * match for initial command as well * define cli patterns centrally * comment * black * extend interactive pattern * move get_cli_patterns * test_cli_patterns --------- --- mb_netmgmt/__main__.py | 27 ++++++++++++++------------- mb_netmgmt/ssh.py | 4 ++-- mb_netmgmt/telnet.py | 4 ++-- test/test_mb_netmgmt.py | 37 ++++++++++++++++++++++++++++++++++++- 4 files changed, 54 insertions(+), 18 deletions(-) diff --git a/mb_netmgmt/__main__.py b/mb_netmgmt/__main__.py index 17bcc79..db0d78e 100755 --- a/mb_netmgmt/__main__.py +++ b/mb_netmgmt/__main__.py @@ -120,19 +120,20 @@ def save_key(self, proxy): def get_proxy(self, stub): return stub["responses"][0].get("proxy") - def get_cli_patterns(self): - patterns = [] - patterns.append( - b"[\r\n\x1b\[K][\-\w+\.:/]+(?:\([^\)]+\))?[>#] ?$" - ) # based on IOS driver of Exscript - patterns.append( - b"[\r\n\x00\x1b\[K]RP/\d+/(?:RS?P)?\d+\/CPU\d+:[^#]+(?:\([^\)]+\))?#$" - ) # based on IOS XR driver of Exscript - patterns.append( - b"[\r\n\x00\x1b\[K](?P[\w/ .:,\(\)\-\?]*)(?P\[[\w/.:\-]*\])?(?(default)(?P(?:\?|: ?|)$)|(?P: $))" - ) # Interactive prompt - patterns.append(b"[\r\n\x00\x1b\[K] --More-- $") # Terminal paging - return patterns + +def get_cli_patterns(): + patterns = [] + patterns.append( + b"[\r\n\x1b\[K][\-\w+\.:/]+(?:\([^\)]+\))?[>#] ?$" + ) # based on IOS driver of Exscript + patterns.append( + b"[\r\n\x00\x1b\[K]RP/\d+/(?:RS?P)?\d+\/CPU\d+:[^#]+(?:\([^\)]+\))?#$" + ) # based on IOS XR driver of Exscript + patterns.append( + b"[\r\n\x00\x1b\[K](?P[\w/ .:,\(\)\-\?]*)(?P\[[\w/.,():\-]*\])?(?(default)(?P(?:\?|: ?| |)$)|(?P: $))" + ) # Interactive prompt + patterns.append(b"[\r\n\x00\x1b\[K] --More-- $") # Terminal paging + return patterns def disable_algorithms(disabled_algorithms): diff --git a/mb_netmgmt/ssh.py b/mb_netmgmt/ssh.py index 8be0ed1..1cb3e8c 100644 --- a/mb_netmgmt/ssh.py +++ b/mb_netmgmt/ssh.py @@ -23,7 +23,7 @@ import paramiko -from mb_netmgmt.__main__ import Protocol +from mb_netmgmt.__main__ import Protocol, get_cli_patterns stopped = False @@ -90,7 +90,7 @@ def respond(self, response, request_id): def read_proxy_response(self): prompt_patterns = [self.channel.command_prompt] - prompt_patterns += self.get_cli_patterns() + prompt_patterns += get_cli_patterns() message = self.read_message(self.channel.upstream, prompt_patterns) return {"response": message.decode()} diff --git a/mb_netmgmt/telnet.py b/mb_netmgmt/telnet.py index 0648956..eef8665 100644 --- a/mb_netmgmt/telnet.py +++ b/mb_netmgmt/telnet.py @@ -22,7 +22,7 @@ import time from socketserver import StreamRequestHandler, TCPServer -from mb_netmgmt.__main__ import Protocol +from mb_netmgmt.__main__ import Protocol, get_cli_patterns Server = TCPServer @@ -65,7 +65,7 @@ def respond(self, response, request_id): def read_proxy_response(self): prompt_patterns = [self.command_prompt] - prompt_patterns += self.get_cli_patterns() + prompt_patterns += get_cli_patterns() _, _, response = self.telnet.expect(prompt_patterns) return {"response": response.decode()} diff --git a/test/test_mb_netmgmt.py b/test/test_mb_netmgmt.py index fba5339..61b2782 100644 --- a/test/test_mb_netmgmt.py +++ b/test/test_mb_netmgmt.py @@ -1,5 +1,6 @@ import io import os +import re from threading import Thread from urllib.parse import urlparse @@ -10,13 +11,37 @@ from ncclient.transport.ssh import MSG_DELIM from mb_netmgmt import mb, netconf, ssh, use_scalar_strings, yaml -from mb_netmgmt.__main__ import create_server +from mb_netmgmt.__main__ import create_server, get_cli_patterns port = 8081 prompt = b"prompt#" mock_response = f""" """ +cli_responses = [ + (b"\rIOS-1>", True), + (b"\rIOS-1#", True), + (b"\rIOS-1(config)#", True), + (b"\rIOS-1(config-if)#", True), + (b"\rRP/0/8/CPU0:IOSXR-2>", True), + (b"\rRP/0/8/CPU0:IOSXR-2#", True), + (b"\rRP/0/8/CPU0:IOSXR-2(config)#", True), + (b"\rRP/0/8/CPU0:IOSXR-2(config-if)#", True), + ( + b"\rKUncommitted changes found, commit them before exiting(yes/no/cancel)? [cancel]:", + True, + ), + (b"\rProtocol [ipv4]: ", True), + (b"\rTarget IP address: ", True), + (b"\rHost name or IP address (control-c to abort): []?", True), + (b"\rDestination file name (control-c to abort): [running-config]?", True), + (b"\rDelete net/node0_8_CPU0/disk0:/c12k-mini.vm-4.3.2[confirm]", True), + (b"\rDestination filename [/net/node0_8_CPU0/disk0:/c12k-mini.vm-4.3.2]?", True), + (b"\rReload hardware module ? [no,yes] ", True), + (b"\rDo you wish to continue?[confirm(y/n)]", True), + (b"\r --More-- ", True), + (b"\rSending 5, 100-byte ICMP Echos to 8.8.8.8, timeout is 2 seconds:", False), +] @pytest.mark.parametrize("protocol", ["http", "snmp", "telnet", "netconf"]) @@ -202,3 +227,13 @@ def test_use_scalar_strings(base, result): yaml.dump(base, s) s.seek(0) assert s.read() == result + + +@pytest.mark.parametrize("cli_response,result", cli_responses) +def test_cli_patterns(cli_response, result): + matched = False + for pattern in get_cli_patterns(): + if re.findall(pattern, cli_response): + matched = True + break + assert matched == result