From 0eb8cd635b174e0cc587a819d9de4fde740d9817 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 4 Mar 2026 13:55:03 +0800 Subject: [PATCH 01/19] Add SSH terminal size and server host key algorithms parameters --- config/agent/agent.yml | 10 +++++++ .../src/netdriver_agent/client/channel.py | 28 +++++++++++-------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/config/agent/agent.yml b/config/agent/agent.yml index 3ed7c8e..cc3bbe9 100755 --- a/config/agent/agent.yml +++ b/config/agent/agent.yml @@ -30,6 +30,10 @@ session: # https://asyncssh.readthedocs.io/en/latest/api.html#supported-algorithms # enabled all algorithms by default, add extra algorithms if needed encryption_algs: [] + # Set terminal size, optional item + term_size: + width: 1000 + height: 100 profiles: # global profile, used when no other profile matche global: @@ -38,6 +42,9 @@ session: expiration_time: "16:00" # If the idle time exceeds the maximum, close the session. Unit: Seconds max_idle_time: 600.0 + # https://asyncssh.readthedocs.io/en/stable/api.html#publickeyalgs + # A list of server host key algorithms to allow during the SSH handshake + server_host_key_algs: [] # profile based on vendor/type/version, priority is higher than default vendor: cisco: @@ -46,13 +53,16 @@ session: read_timeout: 60.0 expiration_time: "16:00" max_idle_time: 600.0 + server_host_key_algs: [] # 9.8: # read_timeout: 12.0 # expiration_time: "16:00" # max_idle_time: 600.0 + # server_host_key_algs: [] # profile based on IP address, priority is higher than vendor/type/version # ip: # 192.168.60.198: # read_timeout: 15.0 # expiration_time: "16:00" # max_idle_time: 600.0 + # server_host_key_algs: [] diff --git a/packages/agent/src/netdriver_agent/client/channel.py b/packages/agent/src/netdriver_agent/client/channel.py index 0c8f300..54e6557 100755 --- a/packages/agent/src/netdriver_agent/client/channel.py +++ b/packages/agent/src/netdriver_agent/client/channel.py @@ -7,7 +7,7 @@ from typing import Optional, Tuple, List import asyncssh -from netdriver_core.exception.errors import ChannelError, ChannelReadTimeout +from netdriver_core.exception.errors import ChannelError from netdriver_core.log import logman from netdriver_core.utils.asyncu import async_timeout @@ -95,19 +95,26 @@ _DEFAULT_READ_BUFFER_SIZE = 8192 -def update_ssh_config(kwargs: dict, config: Configuration) -> dict: +def update_ssh_config(kwargs: dict, profile: dict, config: Configuration) -> dict: """ Update SSH configuration with defaults and provided parameters """ - extra_kex_algs = set(config.session.ssh.kex_algs() or []) - extra_encryption_algs = (config.session.ssh.encryption_algs() or []) + ssh = config.session.ssh + extra_kex_algs = set(ssh.kex_algs() or []) + extra_encryption_algs = (ssh.encryption_algs() or []) ssh_config = _DEFAUTL_SSH_CONFIG.copy() ssh_config["kex_algs"] = list(ssh_config["kex_algs"].union(extra_kex_algs)) ssh_config["encryption_algs"] = list(ssh_config["encryption_algs"].union(extra_encryption_algs)) - ssh_config["login_timeout"] = config.session.ssh.login_timeout() or ssh_config["login_timeout"] - ssh_config["connect_timeout"] = config.session.ssh.connect_timeout() or ssh_config["connect_timeout"] - ssh_config["keepalive_interval"] = config.session.ssh.keepalive_interval() or ssh_config["keepalive_interval"] - ssh_config["keepalive_count_max"] = config.session.ssh.keepalive_count_max() or ssh_config["keepalive_count_max"] + ssh_config["login_timeout"] = ssh.login_timeout() or ssh_config["login_timeout"] + ssh_config["connect_timeout"] = ssh.connect_timeout() or ssh_config["connect_timeout"] + ssh_config["keepalive_interval"] = ssh.keepalive_interval() or ssh_config["keepalive_interval"] + ssh_config["keepalive_count_max"] = ssh.keepalive_count_max() or ssh_config["keepalive_count_max"] + server_host_key_algs = profile.get("server_host_key_algs", []) + if server_host_key_algs: + ssh_config["server_host_key_algs"] = server_host_key_algs + term_size = () kwargs.update(ssh_config) - return kwargs + if ssh.term_size() and ssh.term_size.width() and ssh.term_size.height(): + term_size = (ssh.term_size.width(), ssh.term_size.height()) + return kwargs, term_size class Channel: @@ -125,7 +132,6 @@ async def create(cls, username: Optional[str] = None, password: Optional[str] = None, encode: str = "utf-8", - term_size: Tuple = None, logger: object = None, profile: dict = {}, config: Configuration = None, @@ -136,7 +142,7 @@ async def create(cls, cls._read_channel_until_timeout = profile.get("read_timeout", DEFAULT_SESSION_PROFILE.get("read_timeout", 10)) if protocol == "ssh": - kwargs = update_ssh_config(kwargs, config) + kwargs, term_size = update_ssh_config(kwargs, profile, config) conn = await asyncssh.connect( host=str(ip), port=port, username=username, password=password, encoding=encode, **kwargs) From b2723c425899d1843fd5b2021e2cb4bd8f5ae482 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 4 Mar 2026 14:23:51 +0800 Subject: [PATCH 02/19] Remove invalid code --- .../src/netdriver_agent/client/merger.py | 66 -------- .../src/netdriver_agent/client/session.py | 141 +----------------- .../agent/src/netdriver_agent/client/task.py | 44 +----- .../plugins/cisco/cisco_asa.py | 13 +- .../plugins/qianxin/qianxin_nsg.py | 12 +- .../src/netdriver_core/exception/errors.py | 14 -- .../core/src/netdriver_core/plugin/types.py | 12 -- 7 files changed, 5 insertions(+), 297 deletions(-) delete mode 100755 packages/agent/src/netdriver_agent/client/merger.py delete mode 100644 packages/core/src/netdriver_core/plugin/types.py diff --git a/packages/agent/src/netdriver_agent/client/merger.py b/packages/agent/src/netdriver_agent/client/merger.py deleted file mode 100755 index 971f12b..0000000 --- a/packages/agent/src/netdriver_agent/client/merger.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import asyncio -from typing import Dict, List -from netdriver_agent.client.task import PullTask - - -class Merger: - """ To merge same type pull task into one """ - _queue: asyncio.Queue - _lock: asyncio.Lock - _sleep_time: float - _logger: any - - def __init__(self, queue_siz: int = 64): - self._queue = asyncio.Queue(maxsize=queue_siz) - self._lock = asyncio.Lock() - - async def enqueue(self, task: PullTask): - """ Enqueue task - :param task: PullTask - """ - async with self._lock: - task.set_enqueue_timestamp() - self._queue.put_nowait(task) - - async def dequeue(self) -> PullTask: - """ Dequeue task - :return: PullTask - """ - task: PullTask = await self._queue.get() - task.set_dequeue_timestamp() - task.set_exec_start_timestamp() - return task - - async def get_mergable_tasks(self) -> Dict[str, List[PullTask]]: - """ Get merged tasks - Merge same vsys tasks into one, vsys as key - :return: Dict[PullTask] - """ - tasks: Dict[str, List[PullTask]] = {} - async with self._lock: - # read all tasks in queue - task: PullTask = await self.dequeue() - if task.vsys in tasks: - tasks[task.vsys].append(task) - else: - tasks[task.vsys] = [task] - - while not self._queue.empty(): - task = await self.dequeue() - if task.vsys in tasks: - tasks[task.vsys].append(task) - else: - tasks[task.vsys] = [task] - return tasks - - def task_done(self): - try: - if not self._queue.empty(): - self._queue.task_done() - except Exception as e: - pass - - async def join(self): - await self._queue.join() \ No newline at end of file diff --git a/packages/agent/src/netdriver_agent/client/session.py b/packages/agent/src/netdriver_agent/client/session.py index 8236753..e2562f5 100755 --- a/packages/agent/src/netdriver_agent/client/session.py +++ b/packages/agent/src/netdriver_agent/client/session.py @@ -14,13 +14,11 @@ from netdriver_core import utils from netdriver_agent.client.channel import DEFAULT_SESSION_PROFILE, Channel, ReadBuffer -from netdriver_agent.client.merger import Merger from netdriver_core.dev.mode import Mode -from netdriver_agent.client.task import CmdTask, CmdTaskResult, PullTask, PullTaskResult +from netdriver_agent.client.task import CmdTask, CmdTaskResult from netdriver_core.exception.errors import (ConnectTimeout, ExecCmdError, ExecCmdTimeout, ExecError, GetPromptFailed, - LoginFailed, PullConfigFailed, QueueFullError, SessionInitFailed, UnsupportedConfigType) + LoginFailed, QueueFullError, SessionInitFailed) from netdriver_core.log.logman import create_session_logger -from netdriver_core.plugin.types import ConfigType from netdriver_core.utils.asyncu import AsyncTimeoutError, async_timeout @@ -47,12 +45,6 @@ class Session: _config: Configuration _cmd_queue: Queue _cmd_task_consumer: asyncio.Task - _runconf_req_merger: Merger - _runconf_req_consumer: asyncio.Task - _route_req_merger: Merger - _route_req_consumer: asyncio.Task - _hitcount_req_merger: Merger - _route_req_consumer: asyncio.Task _channel: Channel # current mode _mode: Mode @@ -125,22 +117,6 @@ def get_more_pattern(self) -> Tuple[Pattern, str]: """ Get more pattern and more command """ raise NotImplementedError("Method get_more_pattern not implemented") - @abc.abstractmethod - async def pull_running_config(self, vsys: str = None) -> str: - """ Pull running config """ - raise NotImplementedError("Method pull_running_config not implemented") - - @abc.abstractmethod - async def pull_routes(self, vsys: str = None) -> str: - """ Pull routes """ - raise NotImplementedError("Method pull_routes not implemented") - - @abc.abstractmethod - async def pull_hitcounts(self, vsys: str = None) -> str: - """ Pull hit counts """ - raise NotImplementedError("Method pull_hit_counts not implemented") - - @classmethod async def create(cls, *args, **kwargs) -> "Session": """ Create session @@ -197,9 +173,6 @@ def __init__(self, profiles = self._config.session.profiles() if self._config else {} self._session_profile = self.load_session_profile(profiles) self._cmd_queue = Queue(queue_size) - self._runconf_req_merger = Merger() - self._route_req_merger = Merger() - self._hitcount_req_merger = Merger() self._create_time = datetime.now().timestamp() self._last_use = None self._cmd_hooks = {} @@ -269,12 +242,6 @@ async def _async_init(self): await self._init_session() self._cmd_task_consumer = asyncio.create_task(self._consume_cmd_queue(), name=f"{self.session_key}_cmd_consumer") - self._runconf_req_consumer = asyncio.create_task(self._consume_runconf_queue(), - name=f"{self.session_key}_runconf_req_consumer") - self._route_req_consumer = asyncio.create_task(self._consume_routes_queue(), - name=f"{self.session_key}_route_req_consumer") - self._hitcount_req_consumer = asyncio.create_task(self._consume_hitcounts_queue(), - name=f"{self.session_key}_hitcount_req_consumer") except ConnectTimeout as e: raise e except ExecError as e: @@ -409,85 +376,6 @@ async def _consume_cmd_queue(self): except Exception as e: self._logger.warning("Called task_done too many times.") - async def _consume_runconf_queue(self): - self._logger.info(f"Start consuming runconf queue") - output: str = "" - while not self._is_closing: - task_dict = await self._runconf_req_merger.get_mergable_tasks() - if not task_dict: - continue - for vsys, tasks in task_dict.items(): - output = None - exception = None - try: - self._logger.info(f"Get [{len(tasks)}] pull_running_config tasks of {vsys}.") - self._logger.info(f"Pulling running config.") - output = await self.pull_running_config(vsys) - self._logger.info(f"Pulled running config, length: [{len(output)}].") - except asyncio.CancelledError as e: - self._logger.warning(f"_consume_runconf_queue cancelled: {e}", exc_info=True) - break - except ExecError as exec_e: - exec_e.output = output - exception = exception - except BaseException as e: - exception = PullConfigFailed(e, output=output) - finally: - self._runconf_req_merger.set_mergable_tasks_result(tasks=tasks, output=output, exception=exception) - - async def _consume_routes_queue(self): - self._logger.info(f"Start consuming routes queue") - output: str = "" - while not self._is_closing: - task_dict = await self._route_req_merger.get_mergable_tasks() - if not task_dict: - continue - for vsys, tasks in task_dict.items(): - output = None - exception = None - try: - self._logger.info(f"Get [{len(tasks)}] pull_routes tasks of {vsys}.") - self._logger.info(f"Pulling routes.") - output = await self.pull_routes(vsys) - self._logger.info(f"Pulled routes, length: [{len(output)}].") - except asyncio.CancelledError as e: - self._logger.warning(f"_consume_routes_queue cancelled: {e}", exc_info=True) - break - except ExecError as exec_e: - exec_e.output = output - exception = exec_e - except BaseException as e: - exception = PullConfigFailed(e, output=output) - finally: - self._route_req_merger.set_mergable_tasks_result(tasks=tasks, output=output, exception=exception) - - async def _consume_hitcounts_queue(self): - self._logger.info(f"Start cosuming hitcountrs queue") - while not self._is_closing: - task_dict = await self._hitcount_req_merger.get_mergable_tasks() - if not task_dict: - continue - output = None - exception = None - for vsys, tasks in task_dict.items(): - output = None - exception = None - try: - self._logger.info(f"Get [{len(tasks)}] pull_hitcounts tasks of {vsys}.") - self._logger.info(f"Pulling hit_counts.") - output = await self.pull_hitcounts(vsys) - self._logger.info(f"Pulled hit_counts, length: [{len(output)}].") - except asyncio.CancelledError as e: - self._logger.warning(f"_consume_hitcounts_queue cancelled: {e}", exc_info=True) - break - except ExecError as exec_e: - exec_e.output = output - exception = exec_e - except BaseException as e: - exception = PullConfigFailed(e, output=output) - finally: - self._hitcount_req_merger.set_mergable_tasks_result(tasks=tasks, output=output, exception=exception) - async def _get_prompt(self, write_return: bool = True) -> str: """ Get current prompt """ if write_return: @@ -516,12 +404,6 @@ async def close(self) -> None: self._is_closing = True self._cmd_task_consumer.cancel() await self._cmd_queue.join() - self._runconf_req_consumer.cancel() - await self._runconf_req_merger.join() - self._route_req_consumer.cancel() - await self._route_req_merger.join() - self._hitcount_req_consumer.cancel() - await self._hitcount_req_merger.join() except Exception as e: self._logger.error(f"Error closing session {self.session_key}: {e}") finally: @@ -642,25 +524,6 @@ async def send_cmd_only(self, command: str, vsys: str, mode: Mode): task.cacnel() # del task - async def pull(self, type: ConfigType, vsys: str, timeout: float = 10.0) -> PullTaskResult: - """ Send request to pull queue - :param task: PullTask - :return: PullTaskResult - """ - task: PullTask = PullTask(type=type, vsys=vsys, timeout=timeout, - future=get_event_loop().create_future()) - match task.type: - case ConfigType.RUNNING: - await self._runconf_req_merger.enqueue(task) - case ConfigType.ROUTE: - await self._route_req_merger.enqueue(task) - case ConfigType.HIT_COUNT: - await self._hitcount_req_merger.enqueue(task) - case _: - self._logger.warning(f"Unsupported config type: {task.type}") - task.set_result(exception=UnsupportedConfigType()) - return await task.get_result() - async def get_display_info(self) -> List[Any]: """Return session information.""" await self._init_task_done diff --git a/packages/agent/src/netdriver_agent/client/task.py b/packages/agent/src/netdriver_agent/client/task.py index fd08b20..8771bad 100755 --- a/packages/agent/src/netdriver_agent/client/task.py +++ b/packages/agent/src/netdriver_agent/client/task.py @@ -6,7 +6,6 @@ from netdriver_core.dev.mode import Mode from netdriver_core.exception.errors import BaseError -from netdriver_core.plugin.types import ConfigType class TaskResult: @@ -99,45 +98,4 @@ async def get_result(self) -> CmdTaskResult: result.exec_time = self.exec_end_timestamp - self.exec_start_timestamp result.exception = self.exception result.output = output - return result - - -class PullTaskResult(TaskResult): - """ Pull Task Result """ - - -class PullTask(Task): - """ Pull Task """ - type: ConfigType - - def __init__(self, type: ConfigType, vsys: str = None, - timeout: float = 10, catch_error: bool = True, - future: Future = None): - super().__init__(vsys, timeout, catch_error, future) - self.type = type - - def __str__(self): - return f"[{self.type}|{self.vsys}|{self.timeout}]" - - def set_result(self, output: str = None, exception: BaseError = None): - self.set_exec_end_timestamp() - self.exception = exception - if self.future and not self.future.done(): - self.future.set_result(output) - - async def get_result(self) -> PullTaskResult: - ouput = await self.future - result = PullTaskResult() - # If dequeue_timestamp is None, it not dequeued - if not self.dequeue_timestamp: - result.queue_time = 0.0 - else: - result.queue_time = self.dequeue_timestamp - self.enqueue_timestamp - # If exec_start_timestamp is None, it not dequeued - if not self.exec_start_timestamp: - result.exec_time = 0.0 - else: - result.exec_time = self.exec_end_timestamp - self.exec_start_timestamp - result.exception = self.exception - result.output = ouput - return result + return result \ No newline at end of file diff --git a/packages/agent/src/netdriver_agent/plugins/cisco/cisco_asa.py b/packages/agent/src/netdriver_agent/plugins/cisco/cisco_asa.py index b5a7110..806657d 100644 --- a/packages/agent/src/netdriver_agent/plugins/cisco/cisco_asa.py +++ b/packages/agent/src/netdriver_agent/plugins/cisco/cisco_asa.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from netdriver_core.dev.mode import Mode from netdriver_core.plugin.plugin_info import PluginInfo from netdriver_agent.plugins.cisco import CiscoBase @@ -15,14 +14,4 @@ class CiscoASA(CiscoBase): model="asa", version="base", description="Cisco ASA Plugin" - ) - - async def pull_running_config(self, vsys: str = CiscoBase._DEFAULT_VSYS) -> str: - return await self.exec_cmd_in_vsys_and_mode("show running-config\nshow access-list", vsys=vsys, - mode=Mode.ENABLE) - - async def pull_hitcounts(self, vsys: str = CiscoBase._DEFAULT_VSYS) -> str: - return await self.exec_cmd_in_vsys_and_mode("show access-list", vsys=vsys, mode=Mode.ENABLE) - - async def pull_routes(self, vsys: str = CiscoBase._DEFAULT_VSYS) -> str: - return await self.exec_cmd_in_vsys_and_mode("show route", vsys=vsys, mode=Mode.ENABLE) \ No newline at end of file + ) \ No newline at end of file diff --git a/packages/agent/src/netdriver_agent/plugins/qianxin/qianxin_nsg.py b/packages/agent/src/netdriver_agent/plugins/qianxin/qianxin_nsg.py index 4e018a0..5c58e10 100644 --- a/packages/agent/src/netdriver_agent/plugins/qianxin/qianxin_nsg.py +++ b/packages/agent/src/netdriver_agent/plugins/qianxin/qianxin_nsg.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from netdriver_core.dev.mode import Mode from netdriver_core.plugin.plugin_info import PluginInfo from netdriver_agent.plugins.qianxin import QiAnXinBase @@ -14,13 +13,4 @@ class QiAnXinNSG(QiAnXinBase): model="nsg.*", version="base", description="QiAnXin NSG Plugin" - ) - - async def pull_running_config(self, vsys: str = QiAnXinBase._DEFAULT_VSYS) -> str: - return await self.exec_cmd_in_vsys_and_mode("show running config", vsys=vsys, mode=Mode.ENABLE) - - async def pull_hitcounts(self, vsys: str = QiAnXinBase._DEFAULT_VSYS) -> str: - return await self.exec_cmd_in_vsys_and_mode("show security policy", vsys=vsys, mode=Mode.ENABLE) - - async def pull_routes(self, vsys: str = QiAnXinBase._DEFAULT_VSYS) -> str: - return await self.exec_cmd_in_vsys_and_mode("show ip route\nshow ipv6 route", vsys=vsys, mode=Mode.ENABLE) \ No newline at end of file + ) \ No newline at end of file diff --git a/packages/core/src/netdriver_core/exception/errors.py b/packages/core/src/netdriver_core/exception/errors.py index 9f53bcb..c35b9d0 100755 --- a/packages/core/src/netdriver_core/exception/errors.py +++ b/packages/core/src/netdriver_core/exception/errors.py @@ -66,13 +66,6 @@ def __init__(self, self.code = ErrorCode.SESSION_INIT_FAILED -class UnsupportedConfigType(BaseError): - def __init__(self, msg: str = "Config type only supports: running, routes, hit_counts.") -> None: - super().__init__(msg) - self.status_code = 400 - self.code = ErrorCode.UNSUPPORTED_CONFIG_TYPE - - class ExecError(BaseError): """ Base class for execute command error """ output: str @@ -172,13 +165,6 @@ def __init__(self, msg: str, output: str) -> None: self.code = ErrorCode.GET_PROMPT_FAILED -class PullConfigFailed(ExecError): - def __init__(self, msg: str, output: str) -> None: - super().__init__(msg, output) - self.status_code = 500 - self.code = ErrorCode.PULL_CONFIG_ERROR - - class UpdateTimeout(ExecError): def __init__(self, msg: str, output: str = "") -> None: super().__init__(msg, output) diff --git a/packages/core/src/netdriver_core/plugin/types.py b/packages/core/src/netdriver_core/plugin/types.py deleted file mode 100644 index f9adc09..0000000 --- a/packages/core/src/netdriver_core/plugin/types.py +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -from enum import StrEnum - - -class ConfigType(StrEnum): - """ Config Type Enum """ - - RUNNING = "running" - ROUTE = "route" - HIT_COUNT = "hit_count" From 0bc283fb2de0b6b9ce0bb29eaf3e1541ae926c3d Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 4 Mar 2026 14:42:25 +0800 Subject: [PATCH 03/19] Refactoring Leadsec device plugin --- .../plugins/leadsec/__init__.py | 81 +------------------ .../plugins/leadsec/leadsec.py | 79 ++++++++++++++++++ .../plugins/leadsec/leadsec_powerv.py | 33 +------- 3 files changed, 82 insertions(+), 111 deletions(-) create mode 100644 packages/agent/src/netdriver_agent/plugins/leadsec/leadsec.py diff --git a/packages/agent/src/netdriver_agent/plugins/leadsec/__init__.py b/packages/agent/src/netdriver_agent/plugins/leadsec/__init__.py index 0fd2153..f9d1a67 100644 --- a/packages/agent/src/netdriver_agent/plugins/leadsec/__init__.py +++ b/packages/agent/src/netdriver_agent/plugins/leadsec/__init__.py @@ -1,83 +1,4 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import re -from netdriver_core.plugin.plugin_info import PluginInfo -from netdriver_agent.plugins.base import Base - -class LeadsecBase(Base): - """ Leadsec Base Session """ - info = PluginInfo( - vendor="leadsec", - model="base", - version="base", - description="Leadsec Base Plugin" - ) - - _DEFAULT_RETURN = "\n" - _DEFAULT_VSYS = "default" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._term_size = (10000, 100) - - def get_default_return(self) -> str: - """ Implementations, defined by Session """ - return LeadsecBase._DEFAULT_RETURN - - def get_login_prompt_pattern(self) -> re.Pattern: - return LeadsecBase.PatternHelper.get_login_prompt_pattern() - - def get_union_pattern(self): - return LeadsecBase.PatternHelper.get_union_pattern() - - def get_error_patterns(self) -> list[re.Pattern]: - return LeadsecBase.PatternHelper.get_error_patterns() - - def get_ignore_error_patterns(self) -> list[re.Pattern]: - return LeadsecBase.PatternHelper.get_ignore_error_patterns() - - def get_auto_confirm_patterns(self) -> dict[re.Pattern, str]: - return super().get_auto_confirm_patterns() - - def get_more_pattern(self) -> tuple[re.Pattern, str]: - return LeadsecBase.PatternHelper.get_more_pattern() - - class PatternHelper: - """ Inner class for patterns """ - # hostname# or hostname% - _PATRTERN_LOGIN = r"^\r{0,1}[a-zA-Z0-9]+>$" - - @staticmethod - def get_login_prompt_pattern() -> re.Pattern: - """ Get login prompt pattern """ - return re.compile(LeadsecBase.PatternHelper._PATRTERN_LOGIN, re.MULTILINE) - - @staticmethod - def get_union_pattern() -> re.Pattern: - """ Get union pattern """ - return re.compile("(?P{})".format( - LeadsecBase.PatternHelper._PATRTERN_LOGIN - ), re.MULTILINE) - - @staticmethod - def get_error_patterns() -> list[re.Pattern]: - regex_strs = [ - r"^\^\s.*", - r"错误:?:?\s?.*", - r"unknown keyword", - r"\S*存在", - ] - return [re.compile(regex_str, re.MULTILINE) for regex_str in regex_strs] - - @staticmethod - def get_ignore_error_patterns() -> list[re.Pattern]: - return [] - - @staticmethod - def get_auto_confirm_patterns() -> dict[re.Pattern, str]: - return {} - - @staticmethod - def get_more_pattern() -> tuple[re.Pattern, str]: - return (None, ' ') +from .leadsec import LeadsecBase \ No newline at end of file diff --git a/packages/agent/src/netdriver_agent/plugins/leadsec/leadsec.py b/packages/agent/src/netdriver_agent/plugins/leadsec/leadsec.py new file mode 100644 index 0000000..b47fb96 --- /dev/null +++ b/packages/agent/src/netdriver_agent/plugins/leadsec/leadsec.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import re +from netdriver_core.dev.mode import Mode +from netdriver_core.plugin.plugin_info import PluginInfo +from netdriver_agent.plugins.base import Base + + +class LeadsecBase(Base): + """ Leadsec Base Session """ + + info = PluginInfo( + vendor="leadsec", + model="base", + version="base", + description="Leadsec Base Plugin" + ) + + _SUPPORTED_MODES = [Mode.LOGIN] + + def get_union_pattern(self): + return LeadsecBase.PatternHelper.get_union_pattern() + + def get_error_patterns(self) -> list[re.Pattern]: + return LeadsecBase.PatternHelper.get_error_patterns() + + def get_ignore_error_patterns(self) -> list[re.Pattern]: + return LeadsecBase.PatternHelper.get_ignore_error_patterns() + + def get_more_pattern(self) -> tuple[re.Pattern, str]: + return LeadsecBase.PatternHelper.get_more_pattern() + + def get_mode_prompt_patterns(self) -> dict[Mode, re.Pattern]: + return { + Mode.LOGIN: LeadsecBase.PatternHelper.get_login_prompt_pattern() + } + + async def disable_pagging(self): + self._logger.warning("Leadsec not support pagination command") + + class PatternHelper: + """ Inner class for patterns """ + # hostname# or hostname% + _PATRTERN_LOGIN = r"^\r{0,1}[a-zA-Z0-9]+>$" + + @staticmethod + def get_login_prompt_pattern() -> re.Pattern: + """ Get login prompt pattern """ + return re.compile(LeadsecBase.PatternHelper._PATRTERN_LOGIN, re.MULTILINE) + + @staticmethod + def get_union_pattern() -> re.Pattern: + """ Get union pattern """ + return re.compile("(?P{})".format( + LeadsecBase.PatternHelper._PATRTERN_LOGIN + ), re.MULTILINE) + + @staticmethod + def get_error_patterns() -> list[re.Pattern]: + regex_strs = [ + r"^\^\s.*", + r"错误:?:?\s?.*", + r"unknown keyword", + r"\S*存在", + ] + return [re.compile(regex_str, re.MULTILINE) for regex_str in regex_strs] + + @staticmethod + def get_ignore_error_patterns() -> list[re.Pattern]: + return [] + + @staticmethod + def get_auto_confirm_patterns() -> dict[re.Pattern, str]: + return {} + + @staticmethod + def get_more_pattern() -> tuple[re.Pattern, str]: + return (None, ' ') diff --git a/packages/agent/src/netdriver_agent/plugins/leadsec/leadsec_powerv.py b/packages/agent/src/netdriver_agent/plugins/leadsec/leadsec_powerv.py index aea054f..0255461 100644 --- a/packages/agent/src/netdriver_agent/plugins/leadsec/leadsec_powerv.py +++ b/packages/agent/src/netdriver_agent/plugins/leadsec/leadsec_powerv.py @@ -1,45 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from math import log -from netdriver_core.dev.mode import Mode -from netdriver_core.exception.errors import DetectCurrentModeFailed from netdriver_core.plugin.plugin_info import PluginInfo from netdriver_agent.plugins.leadsec import LeadsecBase class LeadsecPowerV(LeadsecBase): """ Leadsec PowerV Session """ + info = PluginInfo( vendor="leadsec", model="powerv", version="base", description="Leadsec PowerV Plugin" - ) - - def decide_current_mode(self, prompt): - self._logger.info(f"Deciding mode with: {prompt}") - mode: Mode = None - login_pattern = self.get_login_prompt_pattern() - - if login_pattern and login_pattern.search(prompt): - mode = Mode.LOGIN - else: - raise DetectCurrentModeFailed(f"Unknown mode, prompt: {prompt}") - - self._logger.info(f"Got mode: {self._mode} with lastline: {prompt}") - - - def decide_current_vsys(self, prompt: str): - # PowerV does not have vsys, return default - self._vsys = self._DEFAULT_VSYS - self._logger.info(f"Set vsys to: {self._vsys}") - - async def switch_vsys(self, vsys: str) -> str: - if vsys != LeadsecBase._DEFAULT_VSYS: - self._logger.warning("Leadsec PowerV not support vsys, ignore") - self._vsys = vsys - return "" - - async def disable_pagging(self): - self._logger.warning("Leadsec PowerV not support pagination command") \ No newline at end of file + ) \ No newline at end of file From a3b473216fb3bb04de09bbd19bd6b82e20027996 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 4 Mar 2026 16:52:08 +0800 Subject: [PATCH 04/19] Improve plugin auto confirm --- .../src/netdriver_agent/client/session.py | 5 ++--- .../src/netdriver_agent/plugins/h3c/h3c.py | 10 +++++----- .../netdriver_agent/plugins/huawei/huawei.py | 9 +++++---- .../plugins/huawei/huawei_usg.py | 1 + .../netdriver_agent/plugins/juniper/juniper.py | 2 +- .../plugins/paloalto/paloalto.py | 2 +- .../agent/tests/plugins/test_h3c_patterns.py | 14 ++++++++++++++ .../tests/plugins/test_hillstone_patterns.py | 18 ++++++++++++++++++ .../tests/plugins/test_huawei_patterns.py | 14 ++++++++++++++ .../tests/plugins/test_juniper_patterns.py | 11 +++++++++++ .../tests/plugins/test_paloalto_pattens.py | 11 +++++++++++ .../core/src/netdriver_core/utils/regex.py | 11 +++++++++++ 12 files changed, 94 insertions(+), 14 deletions(-) diff --git a/packages/agent/src/netdriver_agent/client/session.py b/packages/agent/src/netdriver_agent/client/session.py index e2562f5..6635a28 100755 --- a/packages/agent/src/netdriver_agent/client/session.py +++ b/packages/agent/src/netdriver_agent/client/session.py @@ -444,9 +444,8 @@ async def _switch_vsys_and_mode(self, vsys: str = None, mode: Mode = None) -> st async def exec_cmd_hooks(self, command: str) -> str: """ Execute command hooks """ - if command in self._cmd_hooks: - self._logger.info(f"Exec command hook for command: {command}") - return await self._cmd_hooks[command](command) + self._logger.info(f"Exec command hook for command: {command}") + return await self._cmd_hooks[command](command) async def exec_cmd(self, command: str) -> str: """ diff --git a/packages/agent/src/netdriver_agent/plugins/h3c/h3c.py b/packages/agent/src/netdriver_agent/plugins/h3c/h3c.py index e3a1c29..ead8467 100644 --- a/packages/agent/src/netdriver_agent/plugins/h3c/h3c.py +++ b/packages/agent/src/netdriver_agent/plugins/h3c/h3c.py @@ -33,7 +33,7 @@ def get_error_patterns(self) -> list[re.Pattern]: def get_ignore_error_patterns(self) -> list[re.Pattern]: return H3CBase.PatternHelper.get_ignore_error_patterns() - def get_auto_confirm_patterns(self) -> dict[str, re.Pattern]: + def get_auto_confirm_patterns(self) -> dict[re.Pattern, str]: return H3CBase.PatternHelper.get_auto_confirm_patterns() def get_mode_prompt_patterns(self) -> dict[Mode, re.Pattern]: @@ -98,12 +98,12 @@ def get_ignore_error_patterns() -> list[re.Pattern]: return [re.compile(regex_str, re.MULTILINE) for regex_str in regex_strs] @staticmethod - def get_auto_confirm_patterns() -> dict[str, re.Pattern]: + def get_auto_confirm_patterns() -> dict[re.Pattern, str]: return { - re.compile(r"The current configuration will be written to the device. Are you sure? \[Y\/N\]:", re.MULTILINE): "Y", + re.compile(r"The current configuration will be written to the device\. Are you sure\? \[Y\/N\]:", re.MULTILINE): "Y", re.compile(r"\(To leave the existing filename unchanged, press the enter key\):", re.MULTILINE): "", - re.compile(r"flash:/startup.cfg exists, overwrite? \[Y\/N\]:", re.MULTILINE): "Y", - re.compile(r"Are you sure you want to continue the save operation? \[Y\/N\]:", re.MULTILINE): "Y" + re.compile(r"flash:\/startup\.cfg exists, overwrite\? \[Y\/N\]:", re.MULTILINE): "Y", + re.compile(r"Are you sure you want to continue the save operation\? \[Y\/N\]:", re.MULTILINE): "Y" } @staticmethod diff --git a/packages/agent/src/netdriver_agent/plugins/huawei/huawei.py b/packages/agent/src/netdriver_agent/plugins/huawei/huawei.py index a8482c6..a81727d 100644 --- a/packages/agent/src/netdriver_agent/plugins/huawei/huawei.py +++ b/packages/agent/src/netdriver_agent/plugins/huawei/huawei.py @@ -33,7 +33,7 @@ def get_error_patterns(self) -> list[re.Pattern]: def get_ignore_error_patterns(self) -> list[re.Pattern]: return HuaweiBase.PatternHelper.get_ignore_error_patterns() - def get_auto_confirm_patterns(self) -> dict[str, re.Pattern]: + def get_auto_confirm_patterns(self) -> dict[re.Pattern, str]: return HuaweiBase.PatternHelper.get_auto_confirm_patterns() def get_mode_prompt_patterns(self) -> dict[Mode, re.Pattern]: @@ -120,9 +120,10 @@ def get_ignore_error_patterns() -> list[re.Pattern]: @staticmethod def get_auto_confirm_patterns() -> dict[re.Pattern, str]: return { - re.compile(r"Are you sure to continue\?\[Y\/N\]: ", re.MULTILINE): "Y", - re.compile(r"startup saved-configuration file on peer device\?\[Y\/N\]: ", re.MULTILINE): "Y", - re.compile(r"Warning: The current configuration will be written to the device. Continue? \[Y\/N\]: ", re.MULTILINE): "Y", + re.compile(r"Are you sure to continue\?\[Y\/N\]", re.MULTILINE): "Y", + re.compile(r"startup saved-configuration file on peer device\?\[Y\/N\]", re.MULTILINE): "Y", + re.compile(r"Warning: The current configuration will be written to the device\. Continue\? \[Y\/N\]", re.MULTILINE): "Y", + re.compile(r"Warning: This command will invalidate the rule\. Continue\?\[Y\/N\]", re.MULTILINE): "Y" } @staticmethod diff --git a/packages/agent/src/netdriver_agent/plugins/huawei/huawei_usg.py b/packages/agent/src/netdriver_agent/plugins/huawei/huawei_usg.py index fffeac9..2b3c12a 100644 --- a/packages/agent/src/netdriver_agent/plugins/huawei/huawei_usg.py +++ b/packages/agent/src/netdriver_agent/plugins/huawei/huawei_usg.py @@ -26,6 +26,7 @@ def register_hooks(self): """ Register hooks for specific commands """ self.register_hook("save", self.save) self.register_hook("save all", self.save) + self.register_hook("disable", self.save) def decide_current_vsys(self, prompt: str): """ Decide current vsys diff --git a/packages/agent/src/netdriver_agent/plugins/juniper/juniper.py b/packages/agent/src/netdriver_agent/plugins/juniper/juniper.py index 35a3874..a0f9652 100644 --- a/packages/agent/src/netdriver_agent/plugins/juniper/juniper.py +++ b/packages/agent/src/netdriver_agent/plugins/juniper/juniper.py @@ -98,7 +98,7 @@ def get_ignore_error_patterns() -> list[re.Pattern]: @staticmethod def get_auto_confirm_patterns() -> dict[re.Pattern, str]: return { - re.compile(r"Exit with uncommitted changes? [yes,no] (yes) ", re.MULTILINE): "yes" + re.compile(r"Exit with uncommitted changes\? \[yes,no\] \(yes\) ", re.MULTILINE): "yes" } @staticmethod diff --git a/packages/agent/src/netdriver_agent/plugins/paloalto/paloalto.py b/packages/agent/src/netdriver_agent/plugins/paloalto/paloalto.py index 13e7040..74adeaa 100644 --- a/packages/agent/src/netdriver_agent/plugins/paloalto/paloalto.py +++ b/packages/agent/src/netdriver_agent/plugins/paloalto/paloalto.py @@ -33,7 +33,7 @@ def get_error_patterns(self) -> list[re.Pattern]: def get_ignore_error_patterns(self) -> list[re.Pattern]: return PaloaltoBase.PatternHelper.get_ignore_error_patterns() - def get_auto_confirm_patterns(self) -> dict[str, re.Pattern]: + def get_auto_confirm_patterns(self) -> dict[re.Pattern, str]: return PaloaltoBase.PatternHelper.get_auto_confirm_patterns() def get_mode_prompt_patterns(self) -> dict[Mode, re.Pattern]: diff --git a/packages/agent/tests/plugins/test_h3c_patterns.py b/packages/agent/tests/plugins/test_h3c_patterns.py index 94dddf2..fb01279 100755 --- a/packages/agent/tests/plugins/test_h3c_patterns.py +++ b/packages/agent/tests/plugins/test_h3c_patterns.py @@ -71,3 +71,17 @@ async def test_error_catch(output: str): ignore_patterns = H3CBase.PatternHelper.get_ignore_error_patterns() error_str = regex.catch_error_of_output(output, error_patterns, ignore_patterns) assert error_str + + +@pytest.mark.unit +@pytest.mark.asyncio +@pytest.mark.parametrize("output", [ + ("The current configuration will be written to the device. Are you sure? [Y/N]:"), + ("(To leave the existing filename unchanged, press the enter key):"), + ("flash:/startup.cfg exists, overwrite? [Y/N]:"), + ("Are you sure you want to continue the save operation? [Y/N]:") +]) +async def test_auto_confirm(output: str): + auto_confirm_patterns = H3CBase.PatternHelper.get_auto_confirm_patterns() + confirm_cmd = regex.catch_auto_confirm_of_output(output, auto_confirm_patterns) + assert confirm_cmd != None \ No newline at end of file diff --git a/packages/agent/tests/plugins/test_hillstone_patterns.py b/packages/agent/tests/plugins/test_hillstone_patterns.py index ff307f2..6dd02bb 100755 --- a/packages/agent/tests/plugins/test_hillstone_patterns.py +++ b/packages/agent/tests/plugins/test_hillstone_patterns.py @@ -71,3 +71,21 @@ async def test_error_catch(output: str): ignore_patterns = HillstoneBase.PatternHelper.get_ignore_error_patterns() error_str = regex.catch_error_of_output(output, error_patterns, ignore_patterns) assert error_str + + +@pytest.mark.unit +@pytest.mark.asyncio +@pytest.mark.parametrize("output", [ + ("Save configuration, are you sure? [y]/n: "), + ("Save configuration for all VSYS, are you sure? [y]/n: "), + ("Backup start configuration file, are you sure? y/[n]: "), + ("Backup all start configuration files, are you sure? y/[n]: "), + ("保存配置,请确认 [y]/n: "), + ("备份启动配置文件,请确认 y/[n]: "), + ("保存所有VSYS的配置,请确认 [y]/n: "), + ("备份所有启动配置文件,请确认 y/[n]: ") +]) +async def test_auto_confirm(output: str): + auto_confirm_patterns = HillstoneBase.PatternHelper.get_auto_confirm_patterns() + confirm_cmd = regex.catch_auto_confirm_of_output(output, auto_confirm_patterns) + assert confirm_cmd != None \ No newline at end of file diff --git a/packages/agent/tests/plugins/test_huawei_patterns.py b/packages/agent/tests/plugins/test_huawei_patterns.py index 01e9c78..3cad7a0 100755 --- a/packages/agent/tests/plugins/test_huawei_patterns.py +++ b/packages/agent/tests/plugins/test_huawei_patterns.py @@ -122,3 +122,17 @@ async def test_error_ignore(output: str): ignore_patterns = HuaweiBase.PatternHelper.get_ignore_error_patterns() error_str = regex.catch_error_of_output(output, error_patterns, ignore_patterns) assert not error_str + + +@pytest.mark.unit +@pytest.mark.asyncio +@pytest.mark.parametrize("output", [ + ("Are you sure to continue?[Y/N]"), + ("startup saved-configuration file on peer device?[Y/N]"), + ("Warning: The current configuration will be written to the device. Continue? [Y/N]:"), + ("Warning: This command will invalidate the rule. Continue?[Y/N]") +]) +async def test_auto_confirm(output: str): + auto_confirm_patterns = HuaweiBase.PatternHelper.get_auto_confirm_patterns() + confirm_cmd = regex.catch_auto_confirm_of_output(output, auto_confirm_patterns) + assert confirm_cmd != None \ No newline at end of file diff --git a/packages/agent/tests/plugins/test_juniper_patterns.py b/packages/agent/tests/plugins/test_juniper_patterns.py index 409b71a..753654c 100755 --- a/packages/agent/tests/plugins/test_juniper_patterns.py +++ b/packages/agent/tests/plugins/test_juniper_patterns.py @@ -67,3 +67,14 @@ async def test_error_catch(output: str): ignore_patterns = JuniperBase.PatternHelper.get_ignore_error_patterns() error_str = regex.catch_error_of_output(output, error_patterns, ignore_patterns) assert error_str + + +@pytest.mark.unit +@pytest.mark.asyncio +@pytest.mark.parametrize("output", [ + ("Exit with uncommitted changes? [yes,no] (yes) ") +]) +async def test_auto_confirm(output: str): + auto_confirm_patterns = JuniperBase.PatternHelper.get_auto_confirm_patterns() + confirm_cmd = regex.catch_auto_confirm_of_output(output, auto_confirm_patterns) + assert confirm_cmd != None \ No newline at end of file diff --git a/packages/agent/tests/plugins/test_paloalto_pattens.py b/packages/agent/tests/plugins/test_paloalto_pattens.py index 026ce1a..4fa558a 100755 --- a/packages/agent/tests/plugins/test_paloalto_pattens.py +++ b/packages/agent/tests/plugins/test_paloalto_pattens.py @@ -62,3 +62,14 @@ async def test_error_catch(output: str): ignore_patterns = PaloaltoBase.PatternHelper.get_ignore_error_patterns() error_str = regex.catch_error_of_output(output, error_patterns, ignore_patterns) assert error_str + + +@pytest.mark.unit +@pytest.mark.asyncio +@pytest.mark.parametrize("output", [ + ("Would you like to proceed with commit? (y or n) Please type \"y\" for yes or \"n\" for no.") +]) +async def test_auto_confirm(output: str): + auto_confirm_patterns = PaloaltoBase.PatternHelper.get_auto_confirm_patterns() + confirm_cmd = regex.catch_auto_confirm_of_output(output, auto_confirm_patterns) + assert confirm_cmd != None \ No newline at end of file diff --git a/packages/core/src/netdriver_core/utils/regex.py b/packages/core/src/netdriver_core/utils/regex.py index 738e72c..36e8c62 100755 --- a/packages/core/src/netdriver_core/utils/regex.py +++ b/packages/core/src/netdriver_core/utils/regex.py @@ -38,6 +38,17 @@ def catch_error_of_output(output: str, return None +def catch_auto_confirm_of_output(output: str, + auto_confirm_patterns: dict[re.Pattern, str]) -> str | None: + log.debug("Catching auto confirm in output.") + output = output.replace("\r", "") + for pattern, confirm_cmd in auto_confirm_patterns.items(): + if pattern.search(output): + return confirm_cmd + log.debug("No auto confirm found in output") + return None + + def remove_suffix(text: str, suffix: str) -> str: """ Remove suffix from text :param text: text From a0562543cd12284752eb8369b37f99272d8cc4ff Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 4 Mar 2026 16:58:23 +0800 Subject: [PATCH 05/19] Execute an empty command --- packages/agent/src/netdriver_agent/client/session.py | 4 ++-- packages/agent/src/netdriver_agent/client/task.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/agent/src/netdriver_agent/client/session.py b/packages/agent/src/netdriver_agent/client/session.py index 6635a28..bd5b58c 100755 --- a/packages/agent/src/netdriver_agent/client/session.py +++ b/packages/agent/src/netdriver_agent/client/session.py @@ -322,7 +322,7 @@ async def _exec_cmd_task(self, task: CmdTask) -> str: line_size = len(lines) i = 0 while not has_error and i < line_size: - line = lines[i] + line = lines[i].strip() self._logger.info((f"Exec cmd[{i}]: {line}")) output += await self.exec_cmd(line) if task.catch_error: @@ -471,7 +471,7 @@ async def exec_cmd_in_vsys_and_mode(self, command: str, vsys: str = None, mode: line_size = len(lines) i = 0 while i < line_size: - line = lines[i] + line = lines[i].strip() output += await self.exec_cmd(line) i += 1 return output diff --git a/packages/agent/src/netdriver_agent/client/task.py b/packages/agent/src/netdriver_agent/client/task.py index 8771bad..06ccf78 100755 --- a/packages/agent/src/netdriver_agent/client/task.py +++ b/packages/agent/src/netdriver_agent/client/task.py @@ -70,7 +70,7 @@ def __init__(self, command: str, vsys: str = None, mode: Mode = None, timeout: float = 10, catch_error: bool = True, detail_output: bool = True, future: Future = None): super().__init__(vsys, timeout, catch_error, future) - self.command = command.strip() + self.command = command self.mode = mode self.detail_output = detail_output From d01252ebbc8ead8d0c7651098b298985d8b8a491 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 4 Mar 2026 18:19:54 +0800 Subject: [PATCH 06/19] Cisco ASA enters config mode set max terminal width --- .../plugins/cisco/cisco_asa.py | 47 ++++++++++++++++++- .../plugins/hillstone/hillstone.py | 1 - 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/agent/src/netdriver_agent/plugins/cisco/cisco_asa.py b/packages/agent/src/netdriver_agent/plugins/cisco/cisco_asa.py index 806657d..b89d446 100644 --- a/packages/agent/src/netdriver_agent/plugins/cisco/cisco_asa.py +++ b/packages/agent/src/netdriver_agent/plugins/cisco/cisco_asa.py @@ -1,8 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +from netdriver_agent.client.channel import ReadBuffer +from netdriver_core.dev.mode import Mode +from netdriver_core.exception.errors import ConfigFailed from netdriver_core.plugin.plugin_info import PluginInfo from netdriver_agent.plugins.cisco import CiscoBase +from netdriver_core.utils.asyncu import async_timeout # pylint: disable=abstract-method @@ -14,4 +18,45 @@ class CiscoASA(CiscoBase): model="asa", version="base", description="Cisco ASA Plugin" - ) \ No newline at end of file + ) + + _CMD_CANCEL_MORE = "terminal pager 0" + _CMD_MAX_TERMINAL_WIDTH = "terminal width 0" + _is_set_max_terminal_width = False + + @async_timeout(5) + async def config(self) -> str: + """ Enter config mode + + @raise ConfigFailed: config failed + """ + self._logger.info("Entering config mode") + pattern_modes = self.get_mode_prompt_patterns() + pattern_config = pattern_modes.get(Mode.CONFIG) + pattern_enable = pattern_modes.get(Mode.ENABLE) + + await self.write_channel(self._CMD_CONFIG) + output = ReadBuffer(cmd=self._CMD_CONFIG) + try: + while not self._channel.read_at_eof(): + ret = await self.read_channel() + output.append(ret) + if pattern_config and output.check_pattern(pattern_config, False): + self._mode = Mode.CONFIG + self._logger.info("Entered config mode") + break + if pattern_enable and output.check_pattern(pattern_enable): + self._logger.info("Config failed, got enable prompt") + raise ConfigFailed("Config failed, got enable prompt", output=output.get_data()) + except ConfigFailed as e: + raise e + except Exception as e: + raise ConfigFailed(msg=str(e), output=output.get_data()) from e + if not self._is_set_max_terminal_width: + ret = await self.set_max_terminal_width() + output.append(ret) + self._is_set_max_terminal_width = True + return output.get_data() + + async def set_max_terminal_width(self): + return await self.exec_cmd(self._CMD_MAX_TERMINAL_WIDTH) \ No newline at end of file diff --git a/packages/agent/src/netdriver_agent/plugins/hillstone/hillstone.py b/packages/agent/src/netdriver_agent/plugins/hillstone/hillstone.py index 2c0ad87..dc4f6e0 100644 --- a/packages/agent/src/netdriver_agent/plugins/hillstone/hillstone.py +++ b/packages/agent/src/netdriver_agent/plugins/hillstone/hillstone.py @@ -21,7 +21,6 @@ class HillstoneBase(Base): _CMD_CONFIG = "configure" _CMD_EXIT_CONFIG = "end" - _CMD_CANCEL_MORE = "terminal length 0" _SUPPORTED_MODES = [Mode.CONFIG, Mode.ENABLE] def get_union_pattern(self) -> re.Pattern: From 0211a78d663b4bf471fadcecb5f00e3ed3c003ce Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 4 Mar 2026 18:48:38 +0800 Subject: [PATCH 07/19] Fortinet vdom cannot recognize enable mode --- .../plugins/fortinet/fortinet.py | 8 ++--- .../tests/plugins/test_fortinet_patterns.py | 36 +++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/packages/agent/src/netdriver_agent/plugins/fortinet/fortinet.py b/packages/agent/src/netdriver_agent/plugins/fortinet/fortinet.py index 7fc4170..e2df53f 100644 --- a/packages/agent/src/netdriver_agent/plugins/fortinet/fortinet.py +++ b/packages/agent/src/netdriver_agent/plugins/fortinet/fortinet.py @@ -57,10 +57,10 @@ async def _decide_init_state(self) -> str: class PatternHelper: """ Inner class for patterns """ - # hostname # - _PATTERN_ENABLE = r"^\r{0,1}\S+\s*#\s*$" - # hostname (root) # - _PATTERN_VSYS= r"^\r{0,1}\S+\s*\((\S+)\)\s*#\s*$" + # hostname # or hostname $ + _PATTERN_ENABLE = r"^\r{0,1}\S+\s*(\(\S+\))?\s*(#|\$)\s*$" + # hostname (root) # or hostname (root) $ + _PATTERN_VSYS= r"^\r{0,1}\S+\s*\((\S+)\)\s*(#|\$)\s*$" # --More-- _PATTERN_MORE = r"--More--" diff --git a/packages/agent/tests/plugins/test_fortinet_patterns.py b/packages/agent/tests/plugins/test_fortinet_patterns.py index b544415..fc3ebba 100755 --- a/packages/agent/tests/plugins/test_fortinet_patterns.py +++ b/packages/agent/tests/plugins/test_fortinet_patterns.py @@ -16,6 +16,24 @@ async def test_enable_pattern(): assert enable_pattern.search("hostname # \r\n") assert enable_pattern.search("\nhostname # \n") assert enable_pattern.search("\r\nhostname # \r\n") + assert enable_pattern.search("hostname (root) #") + assert enable_pattern.search("hostname (root) # ") + assert enable_pattern.search("hostname (root) # \n") + assert enable_pattern.search("hostname (root) # \r\n") + assert enable_pattern.search("\nhostname (root) # \n") + assert enable_pattern.search("\r\nhostname (root) # \r\n") + assert enable_pattern.search("hostname $") + assert enable_pattern.search("hostname $ ") + assert enable_pattern.search("hostname $ \n") + assert enable_pattern.search("hostname $ \r\n") + assert enable_pattern.search("\nhostname $ \n") + assert enable_pattern.search("\r\nhostname $ \r\n") + assert enable_pattern.search("hostname (root) $") + assert enable_pattern.search("hostname (root) $ ") + assert enable_pattern.search("hostname (root) $ \n") + assert enable_pattern.search("hostname (root) $ \r\n") + assert enable_pattern.search("\nhostname (root) $ \n") + assert enable_pattern.search("\r\nhostname (root) $ \r\n") @pytest.mark.unit @@ -28,6 +46,12 @@ async def test_vsys_pattern(): assert config_pattern.search("hostname (root) # \r\n") assert config_pattern.search("\nhostname (root) # \n") assert config_pattern.search("\r\nhostname (root) # \r\n") + assert config_pattern.search("hostname (root) $") + assert config_pattern.search("hostname (root) $ ") + assert config_pattern.search("hostname (root) $ \n") + assert config_pattern.search("hostname (root) $ \r\n") + assert config_pattern.search("\nhostname (root) $ \n") + assert config_pattern.search("\r\nhostname (root) $ \r\n") @pytest.mark.unit @@ -46,6 +70,18 @@ async def test_union_pattern(): assert union_pattern.search("hostname (root) # \r\n") assert union_pattern.search("\nhostname (root) # \n") assert union_pattern.search("\r\nhostname (root) # \r\n") + assert union_pattern.search("hostname $") + assert union_pattern.search("hostname $ ") + assert union_pattern.search("hostname $ \n") + assert union_pattern.search("hostname $ \r\n") + assert union_pattern.search("\nhostname $ \n") + assert union_pattern.search("\r\nhostname $ \r\n") + assert union_pattern.search("hostname (root) $") + assert union_pattern.search("hostname (root) $ ") + assert union_pattern.search("hostname (root) $ \n") + assert union_pattern.search("hostname (root) $ \r\n") + assert union_pattern.search("\nhostname (root) $ \n") + assert union_pattern.search("\r\nhostname (root) $ \r\n") @pytest.mark.unit From e19ec334a78f4149f5dc511e098b5eee81b8f44a Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 5 Mar 2026 14:09:55 +0800 Subject: [PATCH 08/19] Check point enable pattern update --- .../src/netdriver_agent/plugins/check_point/check_point.py | 4 ++-- packages/agent/tests/plugins/test_check_point_patterns.py | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/agent/src/netdriver_agent/plugins/check_point/check_point.py b/packages/agent/src/netdriver_agent/plugins/check_point/check_point.py index ce75727..ee6da39 100644 --- a/packages/agent/src/netdriver_agent/plugins/check_point/check_point.py +++ b/packages/agent/src/netdriver_agent/plugins/check_point/check_point.py @@ -41,8 +41,8 @@ def get_more_pattern(self) -> tuple[re.Pattern, str]: class PatternHelper: """ Inner class for patterns """ - # hostname> - _PATTERN_ENABLE = r"^\r{0,1}\S+\s*>\s*$" + # hostname> or [Global] hostname> or [WARNING! Local Member] hostname> + _PATTERN_ENABLE = r"^\r{0,1}(\[.+\])?\s*\S+\s*>\s*$" # -- More -- _PATTERN_MORE = r"-- More --" diff --git a/packages/agent/tests/plugins/test_check_point_patterns.py b/packages/agent/tests/plugins/test_check_point_patterns.py index 7ae57a3..cd4fb57 100755 --- a/packages/agent/tests/plugins/test_check_point_patterns.py +++ b/packages/agent/tests/plugins/test_check_point_patterns.py @@ -15,6 +15,8 @@ async def test_enable_pattern(): assert enable_pattern.search("hostname> \r\n") assert enable_pattern.search("\nhostname> \n") assert enable_pattern.search("\r\nhostname> \r\n") + assert enable_pattern.search("[WARNING! Local Member] hostname> ") + assert enable_pattern.search("[Global] hostname> ") @pytest.mark.unit @@ -27,6 +29,8 @@ async def test_union_pattern(): assert union_pattern.search("hostname> \r\n") assert union_pattern.search("\nhostname> \n") assert union_pattern.search("\r\nhostname> \r\n") + assert union_pattern.search("[WARNING! Local Member] hostname> ") + assert union_pattern.search("[Global] hostname> ") From 2df4b8246a504e1624df1a6100c727f4701f9532 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 5 Mar 2026 14:19:17 +0800 Subject: [PATCH 09/19] H3C pattern update --- .../src/netdriver_agent/plugins/h3c/h3c.py | 4 +- .../agent/tests/plugins/test_h3c_patterns.py | 37 ++++++++++--------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/packages/agent/src/netdriver_agent/plugins/h3c/h3c.py b/packages/agent/src/netdriver_agent/plugins/h3c/h3c.py index ead8467..da49d9f 100644 --- a/packages/agent/src/netdriver_agent/plugins/h3c/h3c.py +++ b/packages/agent/src/netdriver_agent/plugins/h3c/h3c.py @@ -60,9 +60,9 @@ async def _decide_init_state(self) -> str: class PatternHelper: """ Inner class for patterns """ # - _PATTERN_ENABLE = r"^\r{0,1}(RBM_P|RBM_S)?<.+>\s*$" + _PATTERN_ENABLE = r"^\r{0,1}\x00{0,1}(RBM_P|RBM_S)?<.+>\s*$" # [hostname] - _PATTERN_CONFIG = r"^\r{0,1}(RBM_P|RBM_S)?\[.+\]\s*$" + _PATTERN_CONFIG = r"^\r{0,1}(RBM_P|RBM_S)?\[(?![Yy]\/[Nn]).+\]\s*$" # ---- More ---- _PATTERN_MORE = r"---- More ----" diff --git a/packages/agent/tests/plugins/test_h3c_patterns.py b/packages/agent/tests/plugins/test_h3c_patterns.py index fb01279..a08ab34 100755 --- a/packages/agent/tests/plugins/test_h3c_patterns.py +++ b/packages/agent/tests/plugins/test_h3c_patterns.py @@ -9,28 +9,30 @@ @pytest.mark.unit @pytest.mark.asyncio async def test_enable_pattern(): - login_pattern = H3CBase.PatternHelper.get_enable_prompt_pattern() - assert login_pattern.search("\r") - assert login_pattern.search("\r\n") - assert login_pattern.search(" ") - assert login_pattern.search(" \n") - assert login_pattern.search(" \r\n") - assert login_pattern.search("RBM_P") + enable_pattern = H3CBase.PatternHelper.get_enable_prompt_pattern() + assert enable_pattern.search("\r") + assert enable_pattern.search("\r\n") + assert enable_pattern.search("\x00") + assert enable_pattern.search(" ") + assert enable_pattern.search(" \n") + assert enable_pattern.search(" \r\n") + assert enable_pattern.search("RBM_P") @pytest.mark.unit @pytest.mark.asyncio async def test_config_pattern(): - enable_pattern = H3CBase.PatternHelper.get_config_prompt_pattern() - assert enable_pattern.search("[hostname]") - assert enable_pattern.search("[hostname] ") - assert enable_pattern.search("[hostname] \n") - assert enable_pattern.search("[hostname] \r\n") - assert enable_pattern.search("\n[hostname] \n") - assert enable_pattern.search("\r\n[hostname] \r\n") - assert enable_pattern.search("[hostname-vlan1]") - assert enable_pattern.search("RBM_P[hostname]") - + config_pattern = H3CBase.PatternHelper.get_config_prompt_pattern() + assert config_pattern.search("[hostname]") + assert config_pattern.search("[hostname] ") + assert config_pattern.search("[hostname] \n") + assert config_pattern.search("[hostname] \r\n") + assert config_pattern.search("\n[hostname] \n") + assert config_pattern.search("\r\n[hostname] \r\n") + assert config_pattern.search("[hostname-vlan1]") + assert config_pattern.search("RBM_P[hostname]") + assert not config_pattern.search("[Y/N]") + assert not config_pattern.search("[y/n]") @pytest.mark.unit @pytest.mark.asyncio @@ -42,6 +44,7 @@ async def test_union_pattern(): assert union_pattern.search(" \r\n") assert union_pattern.search("\n \n") assert union_pattern.search("\r\n \r\n") + assert union_pattern.search("\x00") assert union_pattern.search("[hostname]") assert union_pattern.search("[hostname] ") assert union_pattern.search("[hostname] \n") From 4183227220b58a73e8eeb2260583ed5cc7cca00f Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 5 Mar 2026 14:23:50 +0800 Subject: [PATCH 10/19] Array pattern update --- .../agent/src/netdriver_agent/plugins/array/array.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/agent/src/netdriver_agent/plugins/array/array.py b/packages/agent/src/netdriver_agent/plugins/array/array.py index ac04cbb..8ef9f3c 100644 --- a/packages/agent/src/netdriver_agent/plugins/array/array.py +++ b/packages/agent/src/netdriver_agent/plugins/array/array.py @@ -46,15 +46,15 @@ def get_more_pattern(self) -> tuple[re.Pattern, str]: class PatternHelper: """ Inner class for patterns """ # hostname> $ - _PATTERN_LOGIN = r"^\r{0,1}[^\s<]+>\s*$" + _PATTERN_LOGIN = r"^\r{0,2}[^\s<]+>\s*$" # hostname# $ - _PATTERN_ENABLE = r"^\r{0,1}[^\s#]+#\s*$" + _PATTERN_ENABLE = r"^\r{0,2}[^\s#]+#\s*$" # hostname(config)# $ - _PATTERN_CONFIG = r"^\r{0,1}\S+\(\S+\)#\s*$" + _PATTERN_CONFIG = r"^\r{0,2}\S+\(\S+\)#\s*$" # vsite_namer$ $ - _PATTERN_VSITE_ENABLE = r"^\r{0,1}\S+\$\s*$" + _PATTERN_VSITE_ENABLE = r"^\r{0,2}\S+\$\s*$" # vsite_name(config)$ $ - _PATTERN_VSITE_CONFIG = r"^\r{0,1}\S+\(\S+\)\$\s*$" + _PATTERN_VSITE_CONFIG = r"^\r{0,2}\S+\(\S+\)\$\s*$" _PATTERN_ENABLE_PASSWORD = r"Enable password:" # --More-- _PATTERN_MORE = r" --More-- " From 64a24014a3935da54b0157d9b4ff320794b906d5 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 5 Mar 2026 14:37:01 +0800 Subject: [PATCH 11/19] Fortinet and Topsec plugin bug fix --- .../agent/src/netdriver_agent/plugins/fortinet/fortinet.py | 5 +++-- packages/agent/src/netdriver_agent/plugins/topsec/topsec.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/agent/src/netdriver_agent/plugins/fortinet/fortinet.py b/packages/agent/src/netdriver_agent/plugins/fortinet/fortinet.py index e2df53f..52dc2e9 100644 --- a/packages/agent/src/netdriver_agent/plugins/fortinet/fortinet.py +++ b/packages/agent/src/netdriver_agent/plugins/fortinet/fortinet.py @@ -20,6 +20,7 @@ class FortinetBase(Base): ) _CMD_CANCEL_MORE = "config system console\nset output standard\nend" + _CMD_END = "end" _SUPPORTED_MODES = [Mode.ENABLE] def get_union_pattern(self) -> re.Pattern: @@ -48,8 +49,8 @@ async def _decide_init_state(self) -> str: if vsys_pattern: vsys_match = vsys_pattern.search(prompt) if vsys_match and vsys_match.group(1) != self._vsys: - self.write_channel("end") - prompt = await self._get_prompt() + await self.write_channel(self._CMD_END) + prompt = await self._get_prompt(write_return=False) # keep decide vsys before decide mode self.decide_current_vsys(prompt) self.decide_current_mode(prompt) diff --git a/packages/agent/src/netdriver_agent/plugins/topsec/topsec.py b/packages/agent/src/netdriver_agent/plugins/topsec/topsec.py index 8de0222..6c92be3 100644 --- a/packages/agent/src/netdriver_agent/plugins/topsec/topsec.py +++ b/packages/agent/src/netdriver_agent/plugins/topsec/topsec.py @@ -19,7 +19,7 @@ class TopSecBase(Base): description="TopSec Base Plugin" ) - SUPPORTED_MODES = [Mode.ENABLE] + _SUPPORTED_MODES = [Mode.ENABLE] def get_union_pattern(self) -> re.Pattern: return TopSecBase.PatternHelper.get_union_pattern() From f6bed81c54964dfeae04a36adb6bddf8d1a99a95 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 5 Mar 2026 14:47:58 +0800 Subject: [PATCH 12/19] Cisco,Huawei,Juniper plugin error pattern update --- packages/agent/src/netdriver_agent/plugins/cisco/cisco.py | 2 +- .../agent/src/netdriver_agent/plugins/huawei/huawei.py | 7 +------ .../agent/src/netdriver_agent/plugins/juniper/juniper.py | 3 +-- packages/agent/tests/plugins/test_huawei_patterns.py | 2 -- .../netdriver_simunet/server/handlers/cisco/cisco_asa.yml | 3 +++ 5 files changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/agent/src/netdriver_agent/plugins/cisco/cisco.py b/packages/agent/src/netdriver_agent/plugins/cisco/cisco.py index c77bd68..ba5ef92 100644 --- a/packages/agent/src/netdriver_agent/plugins/cisco/cisco.py +++ b/packages/agent/src/netdriver_agent/plugins/cisco/cisco.py @@ -126,7 +126,7 @@ def get_error_patterns() -> list[re.Pattern]: r"\^$", r"^%.+", r"^Command authorization failed.*", - r"^Command rejected:.*" + r"^Command rejected:.*", r"ERROR:.+", r"Invalid password", r"Access denied.", diff --git a/packages/agent/src/netdriver_agent/plugins/huawei/huawei.py b/packages/agent/src/netdriver_agent/plugins/huawei/huawei.py index a81727d..a6ca218 100644 --- a/packages/agent/src/netdriver_agent/plugins/huawei/huawei.py +++ b/packages/agent/src/netdriver_agent/plugins/huawei/huawei.py @@ -96,24 +96,19 @@ def get_ignore_error_patterns() -> list[re.Pattern]: regex_strs = [ # Address r"Error: Address item conflicts!", - r"Error: The address item does not exist!", r"Error: The delete configuration does not exist.", r"Error: The address or address set is not created!", # Service r"Error: Cannot add! Service item conflicts or illegal reference!", r"Error: The service item does not exist!", r"Error: Service item conflicts!", - r"Error: The service item does not exist!", r"Error: The service set is not created(.+)!", - # Schedule - r"Error: No such a time-range.", # NAT r"Error: The specified address-group does not exist.", r"Error: The specified rule does not exist yet.", # NetD r"This condition has already been configured", - r"[a-zA-Z]* (item conflicts|Service item exists\.)", - r"Error: Worng parameter found at.*" + r"[a-zA-Z]* (item conflicts|Service item exists\.)" ] return [re.compile(regex_str, re.MULTILINE) for regex_str in regex_strs] diff --git a/packages/agent/src/netdriver_agent/plugins/juniper/juniper.py b/packages/agent/src/netdriver_agent/plugins/juniper/juniper.py index a0f9652..c962b5b 100644 --- a/packages/agent/src/netdriver_agent/plugins/juniper/juniper.py +++ b/packages/agent/src/netdriver_agent/plugins/juniper/juniper.py @@ -90,8 +90,7 @@ def get_error_patterns() -> list[re.Pattern]: @staticmethod def get_ignore_error_patterns() -> list[re.Pattern]: regex_strs = [ - r"warning: statement not found", - r"warning: element \S+ not found" + r"warning:.+" ] return [re.compile(regex_str, re.MULTILINE) for regex_str in regex_strs] diff --git a/packages/agent/tests/plugins/test_huawei_patterns.py b/packages/agent/tests/plugins/test_huawei_patterns.py index 3cad7a0..20553fc 100755 --- a/packages/agent/tests/plugins/test_huawei_patterns.py +++ b/packages/agent/tests/plugins/test_huawei_patterns.py @@ -104,7 +104,6 @@ async def test_error_catch(output: str): @pytest.mark.asyncio @pytest.mark.parametrize("output", [ ("HRP_S[USG6000V2-group-address-set-test]address 1.1.1.1 mask 32\naddress 1.1.1.1 mask 32 (+B)\n Error: Address item conflicts!"), - ("HRP_S[USG6000V2-group-address-set-test]undo address 3\nundo address 3 (+B)\n Error: The address item does not exist!\nHRP_S[USG6000V2-group-address-set-test]"), ("HRP_S[USG6000V2-domain-set-test]undo add domain byntra\nundo add domain byntra (+B)\n Error: The delete configuration does not exist.\nHRP_S[USG6000V2-domain-set-test]"), ("HRP_S[USG6000V2]undo ip address-set 123\nundo ip address-set 123 (+B)\n Error: The address or address set is not created!\nHRP_S[USG6000V2]"), ("HRP_S[USG6000V2-group-service-set-svc_test]service service-set ssh\nservice service-set ssh (+B)\n Error: Cannot add! Service item conflicts or illegal reference!\nHRP_S[USG6000V2-group-service-set-svc_test]"), @@ -112,7 +111,6 @@ async def test_error_catch(output: str): ("HRP_S[USG6000V2-object-service-set-test_1]service protocol 85\nservice protocol 85 (+B)\n Error: Service item conflicts!\nHRP_S[USG6000V2-object-service-set-test_1]"), ("HRP_S[USG6000V2-object-service-set-test_1]undo service 7\nundo service 7 (+B)\n Error: The service item does not exist!\nHRP_S[USG6000V2-object-service-set-test_1]"), ("HRP_S[USG6000V2]undo ip service-set xyz\nundo ip service-set xyz (+B)\n Error: The service set is not created(Please specify service set type when creat it)!\nHRP_S[USG6000V2]"), - ("HRP_S[USG6000V2]undo time-range 123\nundo time-range 123 (+B)\n Error: No such a time-range.\nHRP_S[USG6000V2]"), ("HRP_S[USG6000V2]undo nat address-group test\nundo nat address-group test (+B)\n Error: The specified address-group does not exist."), ("HRP_S[USG6000V2-policy-nat]undo rule name 123\nundo rule name 123 (+B)\n Error: The specified rule does not exist yet."), ("This condition has already been configured"), diff --git a/packages/simunet/src/netdriver_simunet/server/handlers/cisco/cisco_asa.yml b/packages/simunet/src/netdriver_simunet/server/handlers/cisco/cisco_asa.yml index 9c6ea87..a01fd31 100755 --- a/packages/simunet/src/netdriver_simunet/server/handlers/cisco/cisco_asa.yml +++ b/packages/simunet/src/netdriver_simunet/server/handlers/cisco/cisco_asa.yml @@ -813,6 +813,9 @@ modes: - cmd: "hostname cisco-asa" output: "" + - cmd: "terminal width 0" + output: "" + common: - cmd: "show version" output: | From a825efca3d915876dafc367e07c8de119b92babb Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 5 Mar 2026 16:08:18 +0800 Subject: [PATCH 13/19] H3C and Huawei ignore password change --- .../src/netdriver_agent/client/session.py | 70 ++++++++++--------- .../agent/src/netdriver_agent/plugins/base.py | 3 + .../src/netdriver_agent/plugins/h3c/h3c.py | 11 ++- .../netdriver_agent/plugins/huawei/huawei.py | 15 +++- .../agent/tests/plugins/test_h3c_patterns.py | 11 +++ .../tests/plugins/test_huawei_patterns.py | 15 ++++ 6 files changed, 88 insertions(+), 37 deletions(-) diff --git a/packages/agent/src/netdriver_agent/client/session.py b/packages/agent/src/netdriver_agent/client/session.py index bd5b58c..3fd91ab 100755 --- a/packages/agent/src/netdriver_agent/client/session.py +++ b/packages/agent/src/netdriver_agent/client/session.py @@ -109,7 +109,7 @@ def get_ignore_error_patterns(self) -> List[Pattern]: @abc.abstractmethod def get_auto_confirm_patterns(self) -> Dict[Pattern, str]: - """ Get auto confirm patterns and cmds""" + """ Get auto confirm patterns and cmds """ raise NotImplementedError("Method get_auto_confirm_patterns not implemented") @abc.abstractmethod @@ -117,6 +117,11 @@ def get_more_pattern(self) -> Tuple[Pattern, str]: """ Get more pattern and more command """ raise NotImplementedError("Method get_more_pattern not implemented") + @abc.abstractmethod + def get_ignore_password_change_patterns(self) -> dict[Pattern, str]: + """ Get pattern of all mode ignore password change prompts """ + raise NotImplementedError("Method get_ignore_password_change_patterns not implemented") + @classmethod async def create(cls, *args, **kwargs) -> "Session": """ Create session @@ -272,6 +277,7 @@ async def _decide_init_state(self) -> str: async def _init_session(self) -> None: self._logger.info(f"Init session") + await self._ignore_password_change() await self._decide_init_state() await self.disable_pagging() self._logger.info(f"Session init done") @@ -573,38 +579,36 @@ async def _handle_auto_confirms(self, cmd, timeout: float = 10.0, auto_enter: bo return output.get_data() - async def get_runconf_templates(self) -> Dict[str, str]: - """ Get running config tempaltes - using aiofiles to load templates from ./{vendor}_{model} directory: - load all files which ends with .textfsm, and the filename should be runconf_{key}.textfsm, - content as value - :return: Dict[str, str] - """ - directory = utils.files.get_plugin_dir(self) - return await utils.files.load_templates( - directory= f"{directory}/{self.info.vendor}_{self.info.model}", prefix="runconf") - - async def get_hitcounts_templates(self) -> Dict[str, str]: - """ Get hit counts tempaltes - using aiofiles to load templates from ./{vendor}_{model} directory: - load all files which ends with .textfsm, and the filename should be hitcounts_{key}.textfsm, - content as value - :return: Dict[str, str] - """ - directory = utils.files.get_plugin_dir(self) - return await utils.files.load_templates( - directory= f"{directory}/{self.info.vendor}_{self.info.model}", prefix="hitcount") - - async def get_routes_templates(self) -> Dict[str, str]: - """ Get routes tempaltes - using aiofiles to load templates from ./{vendor}_{model} directory: - load all files which ends with .textfsm, and the filename should be routes_{key}.textfsm, - content as value - :return: Dict[str, str] - """ - directory = utils.files.get_plugin_dir(self) - return await utils.files.load_templates( - directory= f"{directory}/{self.info.vendor}_{self.info.model}", prefix="route") + @async_timeout(10) + async def _ignore_password_change(self, timeout: float = 10.0, auto_enter: bool = True) -> str: + """ Ignore password change """ + self._logger.info("Ignore password change") + ignore_password_change_patterns = self.get_ignore_password_change_patterns() + output = ReadBuffer() + if ignore_password_change_patterns: + union_pattern = self.get_union_pattern() + pattern_count = len(ignore_password_change_patterns) + 1 + while not self._channel.read_at_eof(): + chunk = await self.read_channel() + output.append(chunk) + # make sure the last check should update the position + i = 1 + is_update_pos = True if i == pattern_count else False + + i += 1 + if union_pattern and output.check_pattern(union_pattern, is_update_pos): + self._logger.info("Finished ignore password change") + break + + for pattern, cmd in ignore_password_change_patterns.items(): + is_update_pos = True if i == pattern_count else False + i += 1 + if output.check_pattern(pattern, is_update_pos): + self._logger.info(f"Matched ignore password change pattern: {pattern}, send ignore password change cmd: {cmd}") + await self.write_channel(cmd, auto_enter=auto_enter) + else: + self._logger.info(f"Do not ignore password change") + return output.get_data() def check_idle_time(self) -> bool: """Check whether the session needs to be closed due to prolonged idle time""" diff --git a/packages/agent/src/netdriver_agent/plugins/base.py b/packages/agent/src/netdriver_agent/plugins/base.py index 4611393..e5549aa 100644 --- a/packages/agent/src/netdriver_agent/plugins/base.py +++ b/packages/agent/src/netdriver_agent/plugins/base.py @@ -76,6 +76,9 @@ def get_enable_password_prompt_pattern(self) -> re.Pattern: def get_auto_confirm_patterns(self) -> dict[re.Pattern, str]: return {} + def get_ignore_password_change_patterns(self) -> dict[re.Pattern, str]: + return {} + async def switch_mode(self, mode: Mode) -> str: """ Switch to the target mode diff --git a/packages/agent/src/netdriver_agent/plugins/h3c/h3c.py b/packages/agent/src/netdriver_agent/plugins/h3c/h3c.py index da49d9f..2b1ae33 100644 --- a/packages/agent/src/netdriver_agent/plugins/h3c/h3c.py +++ b/packages/agent/src/netdriver_agent/plugins/h3c/h3c.py @@ -44,7 +44,10 @@ def get_mode_prompt_patterns(self) -> dict[Mode, re.Pattern]: def get_more_pattern(self) -> tuple[re.Pattern, str]: return (H3CBase.PatternHelper.get_more_pattern(), self._CMD_MORE) - + + def get_ignore_password_change_patterns(self) -> dict[re.Pattern, str]: + return H3CBase.PatternHelper.get_ignore_password_change_patterns() + async def _decide_init_state(self) -> str: """ Decide init state @override @@ -109,3 +112,9 @@ def get_auto_confirm_patterns() -> dict[re.Pattern, str]: @staticmethod def get_more_pattern() -> re.Pattern: return re.compile(H3CBase.PatternHelper._PATTERN_MORE, re.MULTILINE) + + @staticmethod + def get_ignore_password_change_patterns() -> dict[re.Pattern, str]: + return { + re.compile(r"Your password will expire in \d+ days\. Do you want to change it\?", re.MULTILINE): "N" + } \ No newline at end of file diff --git a/packages/agent/src/netdriver_agent/plugins/huawei/huawei.py b/packages/agent/src/netdriver_agent/plugins/huawei/huawei.py index a6ca218..196d4d3 100644 --- a/packages/agent/src/netdriver_agent/plugins/huawei/huawei.py +++ b/packages/agent/src/netdriver_agent/plugins/huawei/huawei.py @@ -45,6 +45,9 @@ def get_mode_prompt_patterns(self) -> dict[Mode, re.Pattern]: def get_more_pattern(self) -> tuple[re.Pattern, str]: return (HuaweiBase.PatternHelper.get_more_pattern(), self._CMD_MORE) + def get_ignore_password_change_patterns(self) -> dict[re.Pattern, str]: + return HuaweiBase.PatternHelper.get_ignore_password_change_patterns() + async def _decide_init_state(self) -> str: """ Decide init state @override @@ -64,7 +67,7 @@ class PatternHelper: # HRP_M _PATTERN_ENABLE = r"^\r{0,1}(HRP_M|HRP_S){0,1}<.+>\s*$" # HRP_S[hostname-vsys-config-config] - _PATTERN_CONFIG = r"^\r{0,1}(HRP_M|HRP_S){0,1}\[.+\]\s*$" + _PATTERN_CONFIG = r"^\r{0,1}(HRP_M|HRP_S){0,1}\[(?![Yy]\/[Nn]).+\]\s*$" # ---- More ---- _PATTERN_MORE = r" ---- More ----" @@ -111,7 +114,7 @@ def get_ignore_error_patterns() -> list[re.Pattern]: r"[a-zA-Z]* (item conflicts|Service item exists\.)" ] return [re.compile(regex_str, re.MULTILINE) for regex_str in regex_strs] - + @staticmethod def get_auto_confirm_patterns() -> dict[re.Pattern, str]: return { @@ -120,7 +123,13 @@ def get_auto_confirm_patterns() -> dict[re.Pattern, str]: re.compile(r"Warning: The current configuration will be written to the device\. Continue\? \[Y\/N\]", re.MULTILINE): "Y", re.compile(r"Warning: This command will invalidate the rule\. Continue\?\[Y\/N\]", re.MULTILINE): "Y" } - + @staticmethod def get_more_pattern() -> re.Pattern: return re.compile(HuaweiBase.PatternHelper._PATTERN_MORE, re.MULTILINE) + + @staticmethod + def get_ignore_password_change_patterns() -> dict[re.Pattern, str]: + return { + re.compile(r"The password needs to be changed, Continue\? \[Y\/N\]", re.MULTILINE): "N" + } diff --git a/packages/agent/tests/plugins/test_h3c_patterns.py b/packages/agent/tests/plugins/test_h3c_patterns.py index a08ab34..b4da5f1 100755 --- a/packages/agent/tests/plugins/test_h3c_patterns.py +++ b/packages/agent/tests/plugins/test_h3c_patterns.py @@ -87,4 +87,15 @@ async def test_error_catch(output: str): async def test_auto_confirm(output: str): auto_confirm_patterns = H3CBase.PatternHelper.get_auto_confirm_patterns() confirm_cmd = regex.catch_auto_confirm_of_output(output, auto_confirm_patterns) + assert confirm_cmd != None + + +@pytest.mark.unit +@pytest.mark.asyncio +@pytest.mark.parametrize("output", [ + ("Your password will expire in 30 days. Do you want to change it?") +]) +async def test_ignore_password_change(output: str): + ignore_password_change_patterns = H3CBase.PatternHelper.get_ignore_password_change_patterns() + confirm_cmd = regex.catch_auto_confirm_of_output(output, ignore_password_change_patterns) assert confirm_cmd != None \ No newline at end of file diff --git a/packages/agent/tests/plugins/test_huawei_patterns.py b/packages/agent/tests/plugins/test_huawei_patterns.py index 20553fc..fefee5c 100755 --- a/packages/agent/tests/plugins/test_huawei_patterns.py +++ b/packages/agent/tests/plugins/test_huawei_patterns.py @@ -45,6 +45,8 @@ async def test_config_pattern(): assert config_pattern.search("HRP_M[USG6000v]") assert config_pattern.search("HRP_S[USG6000v]") assert config_pattern.search("HRP_S[USG6000V2-object-address-set-test obj]") + assert not config_pattern.search("[Y/N]") + assert not config_pattern.search("[y/n]") @pytest.mark.unit @@ -78,6 +80,8 @@ async def test_union_pattern(): assert union_pattern.search("HRP_M[USG6000v]") assert union_pattern.search("HRP_S[USG6000v]") assert union_pattern.search("HRP_S[USG6000V2-object-address-set-test obj]") + assert not union_pattern.search("[Y/N]") + assert not union_pattern.search("[y/n]") @pytest.mark.unit @@ -133,4 +137,15 @@ async def test_error_ignore(output: str): async def test_auto_confirm(output: str): auto_confirm_patterns = HuaweiBase.PatternHelper.get_auto_confirm_patterns() confirm_cmd = regex.catch_auto_confirm_of_output(output, auto_confirm_patterns) + assert confirm_cmd != None + + +@pytest.mark.unit +@pytest.mark.asyncio +@pytest.mark.parametrize("output", [ + ("The password needs to be changed, Continue? [Y/N]") +]) +async def test_ignore_password_change(output: str): + ignore_password_change_patterns = HuaweiBase.PatternHelper.get_ignore_password_change_patterns() + confirm_cmd = regex.catch_auto_confirm_of_output(output, ignore_password_change_patterns) assert confirm_cmd != None \ No newline at end of file From d3a97e2033d4da4eb8c8c970ba02a956453efebf Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 5 Mar 2026 16:21:07 +0800 Subject: [PATCH 14/19] Correlation id loss fix --- packages/agent/src/netdriver_agent/client/session.py | 2 ++ packages/agent/src/netdriver_agent/client/task.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/agent/src/netdriver_agent/client/session.py b/packages/agent/src/netdriver_agent/client/session.py index 3fd91ab..f5800ab 100755 --- a/packages/agent/src/netdriver_agent/client/session.py +++ b/packages/agent/src/netdriver_agent/client/session.py @@ -8,6 +8,7 @@ from typing import Dict, Optional, List, Any, Tuple from asyncio import CancelledError, Queue, QueueFull, get_event_loop +from asgi_correlation_id import correlation_id from asyncssh import PermissionDenied from dependency_injector.providers import Configuration from pydantic import IPvAnyAddress @@ -363,6 +364,7 @@ async def _consume_cmd_queue(self): task = await self._dequeue() if task is None: continue + correlation_id.set(task.context_id) self._idle = False try: # run task withtimeout diff --git a/packages/agent/src/netdriver_agent/client/task.py b/packages/agent/src/netdriver_agent/client/task.py index 06ccf78..b9e528a 100755 --- a/packages/agent/src/netdriver_agent/client/task.py +++ b/packages/agent/src/netdriver_agent/client/task.py @@ -2,8 +2,8 @@ # -*- coding: utf-8 -*- import time from asyncio import Future -from typing import List +from asgi_correlation_id import correlation_id from netdriver_core.dev.mode import Mode from netdriver_core.exception.errors import BaseError @@ -65,6 +65,7 @@ class CmdTask(Task): mode: Mode command: str detail_output: bool + context_id: str def __init__(self, command: str, vsys: str = None, mode: Mode = None, timeout: float = 10, catch_error: bool = True, @@ -73,6 +74,7 @@ def __init__(self, command: str, vsys: str = None, mode: Mode = None, self.command = command self.mode = mode self.detail_output = detail_output + self.context_id = correlation_id.get() def __str__(self): return f"[{self.command}|{self.vsys}|{self.mode}|{self.timeout}]" From e0ee5ff9f3327fcc6f329e3b7e0ed4d24998c377 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 5 Mar 2026 16:31:28 +0800 Subject: [PATCH 15/19] Try task timeout exception --- packages/agent/src/netdriver_agent/client/session.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/agent/src/netdriver_agent/client/session.py b/packages/agent/src/netdriver_agent/client/session.py index f5800ab..b126201 100755 --- a/packages/agent/src/netdriver_agent/client/session.py +++ b/packages/agent/src/netdriver_agent/client/session.py @@ -346,6 +346,10 @@ async def _exec_cmd_task(self, task: CmdTask) -> str: self._logger.info(f"Finished exec cmd task: {task}, cost: {time_consumed:.3f}s") task.set_result(output=output, exception=ExecCmdError(err_msg, output=output) if has_error else None) + except asyncio.CancelledError as e: + self._logger.exception(e) + task.set_result(output=output, exception=ExecCmdTimeout( + msg=f"Exec timed out after {task.timeout} seconds", output=output)) except AsyncTimeoutError as e: task.set_result(output=output, exception=e) except ExecError as e: @@ -354,6 +358,7 @@ async def _exec_cmd_task(self, task: CmdTask) -> str: task.set_result(output=output, exception=e) except BaseException as e: # catch all exception + self._logger.exception(e) task.set_result(output=output, exception=ExecError(e, output=output)) return output @@ -373,6 +378,7 @@ async def _consume_cmd_queue(self): self._logger.warning(f"_consume_cmd_queue cancelled: {e}", exc_info=True) break except asyncio.TimeoutError as e: + self._logger.exception(e) task.set_result(output=output, exception=ExecCmdTimeout(e, output=output)) except BaseException as e: self._logger.exception(e) From 9560cfad1bfb2b1608ffb3f9687cf73f27eb5210 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 5 Mar 2026 18:59:45 +0800 Subject: [PATCH 16/19] Load session profile priority update --- .../src/netdriver_agent/client/session.py | 60 ++++++++----------- 1 file changed, 25 insertions(+), 35 deletions(-) diff --git a/packages/agent/src/netdriver_agent/client/session.py b/packages/agent/src/netdriver_agent/client/session.py index b126201..c205658 100755 --- a/packages/agent/src/netdriver_agent/client/session.py +++ b/packages/agent/src/netdriver_agent/client/session.py @@ -217,28 +217,37 @@ def load_session_profile(self, profiles: Dict[str, Any]) -> Dict[str, Any]: self._logger.debug(f"Load session profile from ip: {self.ip}, profile: {profile}") return profile - model_profile = profiles.get("vendor", {}).get(self.vendor, {}).get(self.model, {}) - if model_profile: - version_profile = model_profile.get(self.version, {}) - base_profile = model_profile.get("base", {}) - # version specific profile is the #2 priority - if version_profile: - self._logger.debug( - f"Load session profile from: {self.vendor}/{self.model}/{self.version}, profile: {version_profile}") - return version_profile - # base profile is the #3 priority - if base_profile: - self._logger.debug( - f"Load session profile from: {self.vendor}/{self.model}/base, profile: {base_profile}") - return base_profile + vendor_profile = profiles.get("vendor", {}).get(self.vendor, {}) + if vendor_profile: + model_profile = vendor_profile.get(self.model, {}) + if model_profile: + # version specific profile is the #2 priority + version_profile = model_profile.get(self.version, {}) + if version_profile: + self._logger.debug( + f"Load session profile from: {self.vendor}/{self.model}/{self.version}, profile: {version_profile}") + return version_profile + + # base profile is the #3 priority + base_profile = model_profile.get("base", {}) + if base_profile: + self._logger.debug( + f"Load session profile from: {self.vendor}/{self.model}/base, profile: {base_profile}") + return base_profile + # base model profile is the #4 priority + base_model_profile = vendor_profile.get('base', {}) + if base_model_profile: + self._logger.debug( + f"Load session profile from: {self.vendor}/base/base, profile: {base_model_profile}") + return base_model_profile global_profile = profiles.get("global", {}) if global_profile: - # Global profile is the #4 priority + # Global profile is the #5 priority self._logger.debug(f"Load session profile from global, profile: {global_profile}") return global_profile else: - # Default profile is the #5 priority + # Default profile is the #6 priority self._logger.debug(f"Load session profile from default, profile: {DEFAULT_SESSION_PROFILE}") return DEFAULT_SESSION_PROFILE @@ -518,25 +527,6 @@ async def send_cmd(self, command: str, vsys: str = None, mode: Mode = None, return await task.get_result() - async def send_cmd_only(self, command: str, vsys: str, mode: Mode): - """ - Execute command in specific mode without output - :param command: command to execute, supoort multi-line command - :param vsys: vsys to execute, if not set, use current vsys - :param mode: mode to execute, if not set, use current mode - - """ - task: CmdTask = CmdTask(command, vsys=vsys, mode=mode) - try: - self._enqueue(task) - except QueueFull: - _msg = f"Send cmd failed! Session: {self.session_key} Cmd: [{ task }]; \ - Reason: Queue is full, please retry or check the agent." - self._logger.error(_msg) - finally: - task.cacnel() - # del task - async def get_display_info(self) -> List[Any]: """Return session information.""" await self._init_task_done From ab1298ed7b374c8d681fd2a835853f2e318dad52 Mon Sep 17 00:00:00 2001 From: sam Date: Fri, 6 Mar 2026 15:57:46 +0800 Subject: [PATCH 17/19] Add h3c S5130S plugin --- .../src/netdriver_agent/models/common.py | 5 ++-- .../src/netdriver_agent/plugins/h3c/h3c.py | 4 +++- .../netdriver_agent/plugins/h3c/h3c_s5130s.py | 24 +++++++++++++++++++ tests/integration/test_h3c_secpath.py | 1 + tests/integration/test_h3c_vsr.py | 1 + 5 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 packages/agent/src/netdriver_agent/plugins/h3c/h3c_s5130s.py diff --git a/packages/agent/src/netdriver_agent/models/common.py b/packages/agent/src/netdriver_agent/models/common.py index d9fe4d6..caf7fbc 100755 --- a/packages/agent/src/netdriver_agent/models/common.py +++ b/packages/agent/src/netdriver_agent/models/common.py @@ -10,7 +10,7 @@ "array": ["ag"], "cisco": ["nexus", "isr.*", "asr.*", "catalyst", "asa"], "huawei": ["usg.*", "ce.*"], - "h3c": ["secpath", "vsr.*"], + "h3c": ["secpath", "vsr.*", "s5130s.*"], "hillstone": ["sg.*"], "juniper": ["ex.*", "qfx.*", "mx.*", "srx.*"], "paloalto": ["pa.*"], @@ -22,7 +22,8 @@ "qianxin": ["nsg.*"], "venustech": ["usg.*"], "chaitin": ["ctdsg.*"], - "topsec": ["ngfw.*"] + "topsec": ["ngfw.*"], + "leadsec": ["power.*"] } _VENDOR_PATTERNS = "|".join(_VENDOR_MODELS.keys()) _models = set() diff --git a/packages/agent/src/netdriver_agent/plugins/h3c/h3c.py b/packages/agent/src/netdriver_agent/plugins/h3c/h3c.py index 2b1ae33..9b71427 100644 --- a/packages/agent/src/netdriver_agent/plugins/h3c/h3c.py +++ b/packages/agent/src/netdriver_agent/plugins/h3c/h3c.py @@ -91,7 +91,9 @@ def get_error_patterns() -> list[re.Pattern]: r".+%.+", r".+doesn't exist.+", r".+does not exist.+", - r"Object group with given name exists with different type." + r"Object group with given name exists with different type.", + r"Permission denied.", + r"Failed to apply .+" ] return [re.compile(regex_str, re.MULTILINE) for regex_str in regex_strs] diff --git a/packages/agent/src/netdriver_agent/plugins/h3c/h3c_s5130s.py b/packages/agent/src/netdriver_agent/plugins/h3c/h3c_s5130s.py new file mode 100644 index 0000000..0fd55fd --- /dev/null +++ b/packages/agent/src/netdriver_agent/plugins/h3c/h3c_s5130s.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from netdriver_core.plugin.plugin_info import PluginInfo +from netdriver_agent.plugins.h3c import H3CBase + + +class H3CS5130S(H3CBase): + """ H3C S5130S Plugin """ + + info = PluginInfo( + vendor="h3c", + model="s5130s.*", + version="base", + description="H3C S5130S Plugin" + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.register_hooks() + + def register_hooks(self): + """ Register hooks for specific commands """ + self.register_hook("save", self.save) \ No newline at end of file diff --git a/tests/integration/test_h3c_secpath.py b/tests/integration/test_h3c_secpath.py index 741c7b1..fc0791b 100755 --- a/tests/integration/test_h3c_secpath.py +++ b/tests/integration/test_h3c_secpath.py @@ -76,6 +76,7 @@ async def test_pull_config(test_client: TestClient, h3c_secpath_dev: dict): assert not response.json().get("err_msg") assert len(response.json().get("result")) == 1 + @pytest.mark.asyncio @pytest.mark.integration async def test_set_and_save(test_client: TestClient, h3c_secpath_dev: dict): diff --git a/tests/integration/test_h3c_vsr.py b/tests/integration/test_h3c_vsr.py index 8529e11..02ff964 100755 --- a/tests/integration/test_h3c_vsr.py +++ b/tests/integration/test_h3c_vsr.py @@ -76,6 +76,7 @@ async def test_pull_config(test_client: TestClient, h3c_vsr_dev: dict): assert not response.json().get("err_msg") assert len(response.json().get("result")) == 1 + @pytest.mark.asyncio @pytest.mark.integration async def test_set_and_save(test_client: TestClient, h3c_vsr_dev: dict): From 31cd710b33f5e27b7987db845d0ada431b245173 Mon Sep 17 00:00:00 2001 From: sam Date: Fri, 6 Mar 2026 16:08:00 +0800 Subject: [PATCH 18/19] Add array apv plugin --- .../agent/src/netdriver_agent/models/common.py | 2 +- .../netdriver_agent/plugins/array/array_apv.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 packages/agent/src/netdriver_agent/plugins/array/array_apv.py diff --git a/packages/agent/src/netdriver_agent/models/common.py b/packages/agent/src/netdriver_agent/models/common.py index caf7fbc..a1103e3 100755 --- a/packages/agent/src/netdriver_agent/models/common.py +++ b/packages/agent/src/netdriver_agent/models/common.py @@ -7,7 +7,7 @@ _VENDOR_MODELS = { - "array": ["ag"], + "array": ["ag", "apv"], "cisco": ["nexus", "isr.*", "asr.*", "catalyst", "asa"], "huawei": ["usg.*", "ce.*"], "h3c": ["secpath", "vsr.*", "s5130s.*"], diff --git a/packages/agent/src/netdriver_agent/plugins/array/array_apv.py b/packages/agent/src/netdriver_agent/plugins/array/array_apv.py new file mode 100644 index 0000000..86c91d0 --- /dev/null +++ b/packages/agent/src/netdriver_agent/plugins/array/array_apv.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from netdriver_core.plugin.plugin_info import PluginInfo +from netdriver_agent.plugins.array import ArrayBase + + +# pylint: disable=abstract-method +class ArrayAPV(ArrayBase): + """ Array APV Plugin """ + + info = PluginInfo( + vendor="array", + model="apv", + version="base", + description="Array APV Plugin" + ) \ No newline at end of file From 838b41e32067d7cc9e7bf9024cd17738f4330f39 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 12 Mar 2026 14:24:30 +0800 Subject: [PATCH 19/19] Optimize check cmd displayed --- .../src/netdriver_agent/client/channel.py | 75 +++++++++++++++++-- .../core/src/netdriver_core/utils/terminal.py | 40 ++++++++++ packages/core/tests/utils/test_io.py | 17 ++++- 3 files changed, 123 insertions(+), 9 deletions(-) diff --git a/packages/agent/src/netdriver_agent/client/channel.py b/packages/agent/src/netdriver_agent/client/channel.py index 54e6557..0eb8745 100755 --- a/packages/agent/src/netdriver_agent/client/channel.py +++ b/packages/agent/src/netdriver_agent/client/channel.py @@ -1,11 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- + from abc import abstractmethod from dependency_injector.providers import Configuration +from netdriver_core.utils.terminal import simulate_output, simulate_output_oct_to_chinese from pydantic import IPvAnyAddress from re import Match, Pattern from typing import Optional, Tuple, List import asyncssh +import re from netdriver_core.exception.errors import ChannelError from netdriver_core.log import logman @@ -148,7 +151,7 @@ async def create(cls, encoding=encode, **kwargs) terminal = await conn.create_process(term_type="ansi", term_size=term_size) terminal.stdout.channel.set_encoding(encoding=encode, errors='replace') - return SSHChannel(conn, terminal, logger=logger) + return SSHChannel(conn, terminal, logger=logger, encode=encode) else: raise ValueError(f"protocol {protocol} not supported.") @@ -201,11 +204,13 @@ class SSHChannel(Channel): def __init__(self, conn: asyncssh.SSHClientConnection, terminal: asyncssh.SSHClientProcess, - logger: object = None) -> None: + logger: object = None, + encode: str = "utf-8") -> None: """ SSH Channel """ self._conn = conn self._terminal = terminal self._logger = logger + self._encode = encode def _check_channel(self): if not self._conn: @@ -241,7 +246,7 @@ async def read_channel_until( :return: str, the data read from the channel """ self._check_channel() - output = ReadBuffer(cmd=cmd) + output = ReadBuffer(cmd=cmd, encode=self._encode) while not self.read_at_eof(): chunk = await self.read_channel(self._read_buffer_size) output.append(chunk) @@ -282,20 +287,74 @@ class ReadBuffer: _cmd: str _is_cmd_displayed: bool = False - def __init__(self, cmd: str = '', line_break: str = '\n') -> None: + def __init__(self, cmd: str = '', line_break: str = '\n', encode: str = None) -> None: """ Initialize read buffer """ self._buffer = [] self._last_line_pos = (0, 0) self._line_break = line_break self._cmd = cmd self._is_cmd_displayed = False + self._encode = encode - def _check_cmd_displayed(self, line: str = '') -> bool: + def _check_cmd_displayed(self, pattern: Pattern, line: str = '') -> bool: if not self._is_cmd_displayed and self._cmd and line: # check if the command is displayed in the line - if self._cmd in line: + if self._cmd_in_line(pattern, line): self._is_cmd_displayed = True log.trace(f"Command '{self._cmd}' is displayed in the line: {line}") + + def _cmd_in_line(self, pattern: Pattern, line: str = '') -> bool: + """check if the line contains cmd""" + + log.trace(f"Line repr = {repr(line)}") + log.trace(f"Line escape = {line.encode('unicode_escape').decode('ascii')}") + + # Topsec output extra ' \r' char + # Fortinet output extra ' \x08' char + if self._cmd in re.sub(r'\s[\r\x08]', '', line): + return True + + # Juniper input extra spaces, and the output will remove the extra spaces + if self._cmd.replace(' ', '') in re.sub(r'[\x07\s]', '', line): + return True + + # Fortinet input Chinese and output octal char + chinese = simulate_output_oct_to_chinese(output=line, encoding=self._encode) + log.trace(f"Line oct to chinese: {repr(chinese)}") + if self._cmd in chinese: + return True + + # Line Remove prompt + for index in range(1, len(line)): + if re.match(pattern, line[:index]): + line = line[index:].lstrip() + break + log.trace(f"Line remove prompt = {repr(line)}") + + # Topsec chinese escape failed, character ignored + if '\ufffd' in line: + line_splits = re.sub(r"(\s\r|\r\n)", '', line).split('\ufffd') + log.trace(f"Line split by \\ufffd = {line_splits}") + for line_split in line_splits: + if line_split not in self._cmd: + return False + return True + + line = simulate_output(line) + log.trace(f"Line simulate output = {repr(line)}") + + # Line simulate output remove prompt + for index in range(1, len(line)): + if re.match(pattern, line[:index]): + line = line[index:].lstrip() + break + log.trace(f"Line simulate output remove prompt = {repr(line)}") + + # Array or Cisco display ultra wide processing + if '$' in line and line.replace('\x08', '').split('$')[0] in self._cmd: + return True + + return False def _is_real_prompt(self) -> bool: if self._cmd: @@ -338,7 +397,7 @@ def check_pattern(self, pattern: Pattern, is_update_checkpos: bool = True) -> Ma while lb_pos != -1: # found a line break, concat the line line = ''.join([line, self._buffer[i][line_start_pos:lb_pos], self._line_break]) - self._check_cmd_displayed(line) + self._check_cmd_displayed(pattern, line) line_start_pos = lb_pos + len(self._line_break) log.trace(f"Checking buffer[{i}][:{line_start_pos}]: {line}") matched = pattern.search(line) @@ -356,7 +415,7 @@ def check_pattern(self, pattern: Pattern, is_update_checkpos: bool = True) -> Ma # no line break found, check the rest of buffer item line = ''.join([line, self._buffer[i][line_start_pos:]]) - self._check_cmd_displayed(line) + self._check_cmd_displayed(pattern, line) line_start_pos += len(line) if i == buffer_size - 1: # if no line break found and no more buffer, check the last line diff --git a/packages/core/src/netdriver_core/utils/terminal.py b/packages/core/src/netdriver_core/utils/terminal.py index 6ead1b5..34d07a4 100755 --- a/packages/core/src/netdriver_core/utils/terminal.py +++ b/packages/core/src/netdriver_core/utils/terminal.py @@ -1,5 +1,9 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- + +import re + + def simulate_output(input: str) -> str: """ Simulate the terminal to handle char stream and get the compressed output Input: @@ -62,3 +66,39 @@ def simulate_output(input: str) -> str: def is_carry_return(escape: str) -> bool: return escape == "\r\r\n" or escape == "\r\n" + + +def oct_to_chinese(oct_str: str, encoding: str ="gbk") -> str: + oct_parts = [p for p in oct_str.split("\\") if p] + valid_bytes = [] + for part in oct_parts: + dec_val = int(part, 8) % 256 + valid_bytes.append(dec_val) + + bytes_data = bytes(valid_bytes) + return bytes_data.decode(encoding, errors="ignore") + + +def simulate_output_oct_to_chinese(output: str, encoding: str ="gbk") -> str: + """ Simulate the terminal to handle octal to Chinese conversion """ + if not output: + return output + oct_pattern = r"(\\\d{3})+" + start = 0 + end = 0 + result = [] + for match in re.finditer(oct_pattern, output): + oct_str = match.group() + start = match.start() + if end == 0: + result.append(output[:start]) + else: + result.pop() + result.append(output[end:start]) + try: + result.append(oct_to_chinese(oct_str=oct_str, encoding=encoding)) + except Exception: + result.append(oct_str) + end = match.end() + result.append(output[end:]) + return "".join(result) if result else output \ No newline at end of file diff --git a/packages/core/tests/utils/test_io.py b/packages/core/tests/utils/test_io.py index fadedf0..a2ac186 100755 --- a/packages/core/tests/utils/test_io.py +++ b/packages/core/tests/utils/test_io.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- + import pytest -from netdriver_core.utils.terminal import simulate_output +from netdriver_core.utils.terminal import simulate_output, simulate_output_oct_to_chinese + @pytest.mark.unit @pytest.mark.asyncio @@ -12,3 +14,16 @@ async def test_compress_output(output: str, expected: str): compressed_output = simulate_output(output) assert compressed_output == expected + + +@pytest.mark.unit +@pytest.mark.asyncio +@pytest.mark.parametrize("output, encoding, result", [ + (r"set name \"\744\670\655\746\626\607\"", "utf-8", r"set name \"中文\""), + (r"set name \"test\744\670\655\746\626\607\"", "utf-8", r"set name \"test中文\""), + (r"set name \"\744\670\655\746\626\607test\"", "utf-8", r"set name \"中文test\""), + (r"set name \"test\744\670\655\746\626\607test\"", "utf-8", r"set name \"test中文test\""), + (r"set name \"test\"", "utf-8", r"set name \"test\"") +]) +async def test_oct_to_chinese(output: str, encoding: str, result: str): + assert simulate_output_oct_to_chinese(output, encoding) == result \ No newline at end of file