Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions docs/changelog/2026/february.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
February 2026
==========

February 24 - Unicon v26.2
------------------------



.. csv-table:: Module Versions
:header: "Modules", "Versions"

``unicon.plugins``, v26.2
``unicon``, v26.2




Changelogs
^^^^^^^^^^
--------------------------------------------------------------------------------
Fix
--------------------------------------------------------------------------------

* mock_device
* Fix asyncio deprecation warning for python 3.14

* routers.connection_provider
* Added logic to merge settings dict instead of replacing Settings object
* When settings dict is passed to connect(), it now properly updates existing Settings object using update() method


--------------------------------------------------------------------------------
Fix
--------------------------------------------------------------------------------

* stackwisevirtualconnectionprovider
* Avoid traceback on empty 'show switch' output

* unicon.plugin/cat8k
* Modified the Switchover implementaion of cat8k to connect post switchover
* This is to avoid any prompt mismatch issues post switchover

* generic/statements
* Modified terminal_position_handler
* Changed terminal position response to \x1b[0;0R

* generic/service_pattern
* Modified ping verbose regex patterns for verbose prompts to correctly match the prompt.


--------------------------------------------------------------------------------
New
--------------------------------------------------------------------------------

* linux
* Modified LinuxPatterns
* Add support for linux prompt (server.cisco.com)~


1 change: 1 addition & 0 deletions docs/changelog/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Changelog
.. toctree::
:maxdepth: 2

2026/february
2026/january
2025/december
2025/october
Expand Down
32 changes: 32 additions & 0 deletions docs/changelog_plugins/2026/february.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
February 2026
==========

February 24 - Unicon.Plugins v26.2
------------------------



.. csv-table:: Module Versions
:header: "Modules", "Versions"

``unicon.plugins``, v26.2
``unicon``, v26.2




Changelogs
^^^^^^^^^^
--------------------------------------------------------------------------------
Fix
--------------------------------------------------------------------------------

* generic
* fix for 3.14 runtime emits extra terminal/argparse warnings
* Update PID tokens for C8000V

* iosxe/cat9k/stackwise_virtual
* Added support to handle standby unlocked in designate handles
* Fix the designate handle to wait for the boot process to complete before designating handles.


1 change: 1 addition & 0 deletions docs/changelog_plugins/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Plugins Changelog
.. toctree::
:maxdepth: 2

2026/february
2026/january
2025/december
2025/october
Expand Down
2 changes: 1 addition & 1 deletion src/unicon/plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "26.1"
__version__ = "26.2"

supported_chassis = [
'single_rp',
Expand Down
2 changes: 1 addition & 1 deletion src/unicon/plugins/generic/service_patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def __init__(self):
self.tunnel = r'^.*Tunnel interface number \[.+\]\s?: $'
self.repeat = r'^.*Repeat count \[.+\]\s?: $'
self.size = r'^.*Datagram size \[.+\]\s?: $'
self.verbose = r'^.*Verbose \[.+\]\s?: $'
self.verbose = r'^.*Verbose(\?)? \[.+\]\s?: $'
self.interval = r'^.*Interval in milliseconds \[.+\]: $'
self.packet_timeout = r'^.*Timeout in seconds \[.+\]\s?: $'
self.sending_interval = r'^.*Sending interval in seconds \[.+\]\s?: $'
Expand Down
2 changes: 1 addition & 1 deletion src/unicon/plugins/generic/statements.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@

def terminal_position_handler(spawn, session, context):
""" send terminal position (VT100) """
spawn.send('\x1b[0;200R')
spawn.send('\x1b[0;0R')


def connection_refused_handler(spawn, context):
Expand Down
20 changes: 2 additions & 18 deletions src/unicon/plugins/iosxe/cat8k/service_implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,24 +108,8 @@ def call_service(self, command=None,
sleep(con.settings.POST_SWITCHOVER_WAIT)

con.spawn.sendline()
con.state_machine.go_to(
'any',
con.spawn,
prompt_recovery=self.prompt_recovery,
timeout=con.connection_timeout,
context=self.context
)

con.log.info(f'Waiting {con.settings.POST_SWITCHOVER_WAIT} seconds before going to enable mode')
sleep(con.settings.POST_SWITCHOVER_WAIT)

con.spawn.sendline()
con.state_machine.go_to(
'enable',
con.spawn,
prompt_recovery=self.prompt_recovery,
context=self.context
)

con.connection_provider.connect()
self.result = True

if not sync_standby:
Expand Down
2 changes: 1 addition & 1 deletion src/unicon/plugins/iosxe/cat8k/service_statements.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
continue_timer=True)

switchover_complete = Statement(pattern=pat.switchover_complete,
action='sendline()',
action=None,
loop_continue=False,
continue_timer=False)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
pyATS TEAM (pyats-support@cisco.com, pyats-support-ext@cisco.com)
"""
import re
from unicon.eal.dialogs import Dialog

from unicon.eal.dialogs import Dialog, Statement
from unicon.bases.routers.connection_provider import BaseStackRpConnectionProvider

from genie.metaparser.util.exceptions import SchemaEmptyParserError
Comment thread
lsheikal marked this conversation as resolved.
Comment thread
lsheikal marked this conversation as resolved.

from unicon.plugins.generic.statements import connection_statement_list, custom_auth_statements


class StackwiseVirtualConnectionProvider(BaseStackRpConnectionProvider):
""" Implements Stack Connection Provider,
This class overrides the base class with the
Expand Down Expand Up @@ -36,12 +40,23 @@ def designate_handles(self):
other_alias = None

# Try to go to enable mode on both connections
standby_locked_dialog = Dialog([
Statement(
pattern=r'.*Standby console disabled.*',
action=None,
loop_continue=False,
continue_timer=False,
)
])

for subcon in [subcon1, subcon2]:
try:
subcon.state_machine.go_to(
'enable',
subcon.spawn,
context=subcon.context,
timeout=con.settings.BOOT_TIMEOUT,
dialog=standby_locked_dialog,
)
except Exception:
pass
Expand All @@ -63,7 +78,11 @@ def designate_handles(self):
device = con.device
try:
# To check if the device is in SVL state
output = device.parse("show switch")
try:
output = device.parse("show switch")
except SchemaEmptyParserError:
con.log.debug("show switch returned empty output")
output = {}
stack_info = output.get("switch", {}).get("stack", {})
roles = [switch_info.get("role") for switch_info in stack_info.values()]

Expand Down
67 changes: 64 additions & 3 deletions src/unicon/plugins/linux/patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,78 @@ def __init__(self):
# The reason for using the learn_hostname pattern instead of the shell_prompt pattern
# to learn the hostname, is that the regex in the router implementation matches \S
# which is not exact enough for the known linux prompts.
self.learn_hostname = r'^.*?({a})?(?P<hostname>[-\w]+)\s?([-\w\]/~:\.\d ]+)?([>\$~%#\]])\s*(\x1b\S+\s?)*$'.format(a=ANSI_REGEX)
# Supported prompt formats.
# Linux#
# Linux>
# user@host ~$
# [user@host ~]$
# agent-lab9-pm:~:2017>
# root@agent-lab11-pm:~#
# root@localhost ~%
# vm-7:3>
# \x1b]0;cisco@dev-server:~^Gcisco@dev-server:3>
# (dev) user@dev-1-name dir$
# [user@new-host dir]$
# host ~ #
# host:~ #
# \x1b]0;rally@rally: /workspace\x07rally@rally:/workspace$ \x1b[K
# root@sj21-pxe-03.cisco.com:~/
# [Linux] #
# cxta@mock-server:~$
# (server.cisco.com)~ :
# sma03:testuser 1]
# pod-esa01.cisco.com:testuser 1]
# \x1b[37mapc>
# \x1b[0;32m[user@host:~] >\x1b[m \x1b[m\x0f
self.learn_hostname = r'^.*?({a})?\(?(?P<hostname>[-\w\.]+)\)?\s?([-\w\]/~:\.\d ]+)?([>\$~%#\]]|~/|~\s?:)\s*(\x1b\S+\s?)*$'.format(a=ANSI_REGEX)

# shell_prompt pattern will be used by the 'shell' state after lean_hostname matches
# a known hostname pattern this pattern is set for the shell state at transition
# from learn_hostname to shell, see statemachine for more details.
self.shell_prompt = r'^(.*?(?P<prompt>((\([-\w]+\) |\x1b(?!\[\?2004).*?)?\S+)?%N\s?([-\w\]/~\s:\.\d]+)?[>\$~%#\]]\s?(\x1b\S+\s?)*))$'
# Supported shell prompt formats, %N is replaced with learned hostname.
# Linux$
# Linux#
# Linux>
# user@host ~$
# [user@host ~]$
# agent-lab9-pm:~:2017>
# root@agent-lab11-pm:~#
# root@localhost ~%
# vm-7:3>
# \x1b]0;cisco@dev-server:~^Gcisco@dev-server:3>
# (dev) user@dev-1-name dir$
# [user@new-host dir]$
# host ~ #
# host:~ #
# \x1b]0;rally@rally: /workspace\x07rally@rally:/workspace$ \x1b[K
# root@sj21-pxe-03.cisco.com:~/
# [Linux] #
# cxta@mock-server:~$
# (server.cisco.com)~ :
# sma03:testuser 1]
# pod-esa01.cisco.com:testuser 1]
# \x1b[37mapc>
# \x1b[0;32m[user@host:~] >\x1b[m \x1b[m\x0f
self.shell_prompt = r'^(.*?(?P<prompt>((\([-\w\.]+\) |\x1b(?!\[\?2004).*?)?\S+)?\(?%N\)?\s?([-\w\]/~\s:\.\d]+)?([>\$~%#\]]|~/|~\s?:)\s?(\x1b\S+\s?)*))$'

# default linux prompt with loose matching of the prompt
# this can result in false prompt matching when output has
# one of the prompt characters at the end of the line,
# e.g. XML output or a banner
self.prompt = r'^(.*?([>\$~%\]]|\] # |[^#\s]#|~ #|~/|^admin:|^#|~\s?#\s?)\s?(\x1b\S+\s?)*)$'
# Supported fallback prompt formats.
# >
# $
# ~
# %
# ]
# ] #
# user#
# ~ #
# ~/
# admin:
# #
# ~#
# ~ :
self.prompt = r'^(.*?([>\$~%\]]|\] # |[^#\s]#|~ #|~/|^admin:|^#|~\s?#\s?|~\s?:)\s?(\x1b\S+\s?)*)$'

self.trex_console = r'^(.*?)(?P<prompt>trex>\s*)$'
2 changes: 1 addition & 1 deletion src/unicon/plugins/pid_tokens.csv
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ C6832-X-LE,iosxe,cat6k,c6800,
C6840-X-LE-40G,iosxe,cat6k,c6800,
C6880-X,iosxe,cat6k,c6800,
C6880-X-LE,iosxe,cat6k,c6800,
C8000V,iosxe,cat8k,c8000v,
C8000V,iosxe,c8kv,c8000v,
C8200-1N-4T,iosxe,cat8k,c8200,
C8200-UCPE-1N8,iosxe,cat8k,c8200,
C8500-12X,iosxe,cat8k,c8500,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ exec:
new_state: exec20
"prompt21":
new_state: exec21
"prompt22":
new_state: exec22
"ls": |
/tmp
/var
Expand Down Expand Up @@ -401,6 +403,10 @@ exec21:
prompt: "cxta@mock-server:~$ "
commands: *cmds

exec22:
prompt: "(server.cisco.com)~ : "
commands: *cmds


sma_prompt:
prompt: "sma03:testuser 1] "
Expand Down
2 changes: 1 addition & 1 deletion src/unicon/plugins/tests/test_plugin_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1020,7 +1020,7 @@ class TestEscapeHandler(unittest.TestCase):

def setUp(self):
self.old_term_setting = os.environ.get('TERM')
os.environ['TERM'] = 'VT100'
os.environ['TERM'] = 'xterm'

def test_escape_handler_uav(self):
c = Connection(hostname='Router',
Expand Down
16 changes: 14 additions & 2 deletions src/unicon/plugins/tests/test_plugin_iosxe.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
import os
import time
import unittest
from unittest.mock import patch
from unittest.mock import patch, Mock, MagicMock
from pyats.topology import loader
from unittest.mock import Mock

import unicon
from unicon.plugins.generic.statements import terminal_position_handler
from unicon import Connection
from unicon.eal.dialogs import Dialog, Statement
from unicon.eal.utils import ExpectMatch, MatchMode
Expand Down Expand Up @@ -593,6 +593,18 @@ def test_traceroute_vrf(self):

class TestIosXEluginBashService(unittest.TestCase):

def test_terminal_position_handler(self):
"""Test that terminal_position_handler sends correct VT100 cursor
position response ESC[0;0R without any additional cleanup."""
mock_spawn = MagicMock()
mock_session = {}
mock_context = {}

terminal_position_handler(mock_spawn, mock_session, mock_context)

# Verify the handler sent only the cursor position response
mock_spawn.send.assert_called_once_with('\x1b[0;0R')

def test_bash(self):
c = Connection(hostname='Router',
start=['mock_device_cli --os iosxe --state isr_exec --hostname Router'],
Expand Down
Loading
Loading