scm-harmonic-cmts-admin/cmts_api_env/lib/python3.12/site-packages/netmiko/juniper/juniper.py

315 lines
10 KiB
Python

import re
import warnings
from typing import Optional, Any
from netmiko.no_enable import NoEnable
from netmiko.base_connection import BaseConnection, DELAY_FACTOR_DEPR_SIMPLE_MSG
from netmiko.scp_handler import BaseFileTransfer
class JuniperBase(NoEnable, BaseConnection):
"""
Implement methods for interacting with Juniper Networks devices.
methods. Overrides several methods for Juniper-specific compatibility.
"""
def session_preparation(self) -> None:
"""Prepare the session after the connection has been established."""
pattern = r"[%>$#]"
self._test_channel_read(pattern=pattern)
self.enter_cli_mode()
cmd = "set cli screen-width 511"
self.set_terminal_width(command=cmd, pattern=r"Screen width set to")
# Overloading disable_paging which is confusing
self.disable_paging(
command="set cli complete-on-space off",
pattern=r"Disabling complete-on-space",
)
self.disable_paging(
command="set cli screen-length 0", pattern=r"Screen length set to"
)
self.set_base_prompt()
def _enter_shell(self) -> str:
"""Enter the Bourne Shell."""
return self._send_command_str("start shell sh", expect_string=r"[\$#]")
def _return_cli(self) -> str:
"""Return to the Juniper CLI."""
return self._send_command_str("exit", expect_string=r"[#>]")
def _determine_mode(self, data: str = "") -> str:
"""Determine whether in shell or CLI."""
pattern = r"[%>$#]"
if not data:
self.write_channel(self.RETURN)
data = self.read_until_pattern(pattern=pattern, read_timeout=10)
shell_pattern = r"(?:root@|%|\$)"
if re.search(shell_pattern, data):
return "shell"
elif ">" in data or "#" in data:
return "cli"
else:
raise ValueError(f"Unexpected data returned for prompt: {data}")
def enter_cli_mode(self) -> None:
"""Check if at shell prompt root@ and go into CLI."""
mode = self._determine_mode()
if mode == "shell":
shell_pattern = r"(?:root@|%|\$)"
self.write_channel(self.RETURN)
cur_prompt = self.read_until_pattern(pattern=shell_pattern, read_timeout=10)
if re.search(r"root@", cur_prompt) or re.search(r"^%$", cur_prompt.strip()):
cli_pattern = r"[>#]"
self.write_channel("cli" + self.RETURN)
self.read_until_pattern(pattern=cli_pattern, read_timeout=10)
return
def check_config_mode(
self,
check_string: str = "]",
pattern: str = r"(?m:[>#] $)",
force_regex: bool = False,
) -> bool:
"""
Checks if the device is in configuration mode or not.
?m = Use multiline matching
Juniper unfortunately will use # as a message indicator when not in config mode
For example, with commit confirmed.
"""
return super().check_config_mode(check_string=check_string, pattern=pattern)
def config_mode(
self,
config_command: str = "configure",
pattern: str = r"(?s:Entering configuration mode.*\].*#)",
re_flags: int = 0,
) -> str:
"""
Enter configuration mode.
?s = enables re.DOTALL in regex pattern.
"""
return super().config_mode(
config_command=config_command, pattern=pattern, re_flags=re_flags
)
def exit_config_mode(
self, exit_config: str = "exit configuration-mode", pattern: str = ""
) -> str:
"""Exit configuration mode."""
output = ""
if self.check_config_mode():
confirm_msg = "Exit with uncommitted changes"
pattern = rf"(?:>|{confirm_msg})"
output = self._send_command_str(
exit_config,
expect_string=pattern,
strip_prompt=False,
strip_command=False,
)
if confirm_msg in output:
output += self._send_command_str(
"yes", expect_string=r">", strip_prompt=False, strip_command=False
)
if self.check_config_mode():
raise ValueError("Failed to exit configuration mode")
return output
def commit(
self,
confirm: bool = False,
confirm_delay: Optional[int] = None,
check: bool = False,
comment: str = "",
and_quit: bool = False,
read_timeout: float = 120.0,
delay_factor: Optional[float] = None,
) -> str:
"""
Commit the candidate configuration.
Commit the entered configuration. Raise an error and return the failure
if the commit fails.
Automatically enters configuration mode
default:
command_string = commit
check and (confirm or confirm_dely or comment):
Exception
confirm_delay and no confirm:
Exception
confirm:
confirm_delay option
comment option
command_string = commit confirmed or commit confirmed <confirm_delay>
check:
command_string = commit check
delay_factor: Deprecated in Netmiko 4.x. Will be eliminated in Netmiko 5.
"""
if delay_factor is not None:
warnings.warn(DELAY_FACTOR_DEPR_SIMPLE_MSG, DeprecationWarning)
if check and (confirm or confirm_delay or comment):
raise ValueError("Invalid arguments supplied with commit check")
if confirm_delay and not confirm:
raise ValueError(
"Invalid arguments supplied to commit method both confirm and check"
)
# Select proper command string based on arguments provided
command_string = "commit"
commit_marker = "commit complete"
if check:
command_string = "commit check"
commit_marker = "configuration check succeeds"
elif confirm:
if confirm_delay:
command_string = "commit confirmed " + str(confirm_delay)
else:
command_string = "commit confirmed"
commit_marker = "commit confirmed will be automatically rolled back in"
# wrap the comment in quotes
if comment:
if '"' in comment:
raise ValueError("Invalid comment contains double quote")
comment = f'"{comment}"'
command_string += " comment " + comment
if and_quit:
command_string += " and-quit"
# Enter config mode (if necessary)
output = self.config_mode()
# and_quit will get out of config mode on commit
# hostname might change on commit, and-quit might result in exiting config mode.
re_prompt = re.escape(self.base_prompt)
expect_string = rf"(?:{re_prompt}|[>#])"
output += self._send_command_str(
command_string,
expect_string=expect_string,
strip_prompt=False,
strip_command=False,
read_timeout=read_timeout,
)
if commit_marker not in output:
raise ValueError(f"Commit failed with the following errors:\n\n{output}")
return output
def strip_prompt(self, *args: Any, **kwargs: Any) -> str:
"""Strip the trailing router prompt from the output."""
a_string = super().strip_prompt(*args, **kwargs)
return self.strip_context_items(a_string)
def strip_context_items(self, a_string: str) -> str:
"""Strip Juniper-specific output.
Juniper will also put a configuration context:
[edit]
and various chassis contexts:
{master:0}, {backup:1}
This method removes those lines.
"""
strings_to_strip = [
r"\[edit.*\]",
r"\{master:?.*\}",
r"\{backup:?.*\}",
r"\{line.*\}",
r"\{primary.*\}",
r"\{secondary.*\}",
]
response_list = a_string.split(self.RESPONSE_RETURN)
last_line = response_list[-1]
for pattern in strings_to_strip:
if re.search(pattern, last_line, flags=re.I):
return self.RESPONSE_RETURN.join(response_list[:-1])
return a_string
def cleanup(self, command: str = "exit") -> None:
"""Gracefully exit the SSH session."""
try:
if self.check_config_mode():
self.exit_config_mode()
except Exception:
pass
# Always try to send final 'exit' (command)
self._session_log_fin = True
self.write_channel(command + self.RETURN)
class JuniperSSH(JuniperBase):
pass
class JuniperTelnet(JuniperBase):
def __init__(self, *args: Any, **kwargs: Any) -> None:
default_enter = kwargs.get("default_enter")
kwargs["default_enter"] = "\r\n" if default_enter is None else default_enter
super().__init__(*args, **kwargs)
class JuniperFileTransfer(BaseFileTransfer):
"""Juniper SCP File Transfer driver."""
def __init__(
self,
ssh_conn: "BaseConnection",
source_file: str,
dest_file: str,
file_system: Optional[str] = "/var/tmp",
direction: str = "put",
**kwargs: Any,
) -> None:
super().__init__(
ssh_conn=ssh_conn,
source_file=source_file,
dest_file=dest_file,
file_system=file_system,
direction=direction,
**kwargs,
)
def remote_space_available(self, search_pattern: str = "") -> int:
"""Return space available on remote device."""
search_pattern = r"[%>$#]"
return self._remote_space_available_unix(search_pattern=search_pattern)
def check_file_exists(self, remote_cmd: str = "") -> bool:
"""Check if the dest_file already exists on the file system (return boolean)."""
return self._check_file_exists_unix(remote_cmd=remote_cmd)
def remote_file_size(
self, remote_cmd: str = "", remote_file: Optional[str] = None
) -> int:
"""Get the file size of the remote file."""
return self._remote_file_size_unix(
remote_cmd=remote_cmd, remote_file=remote_file
)
def remote_md5(
self, base_cmd: str = "file checksum md5", remote_file: Optional[str] = None
) -> str:
return super().remote_md5(base_cmd=base_cmd, remote_file=remote_file)
def enable_scp(self, cmd: str = "") -> None:
raise NotImplementedError
def disable_scp(self, cmd: str = "") -> None:
raise NotImplementedError