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
60 changes: 60 additions & 0 deletions docs/changelog/2026/august.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
August 2026
==========

August 25 - Unicon v26.8
------------------------



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

``unicon.plugins``, v26.8
``unicon``, v26.8




Changelogs
^^^^^^^^^^

unicon
""""""
--------------------------------------------------------------------------------
Fix
--------------------------------------------------------------------------------

* unicon.bases.connection
* Modified Connection connect
* Clear stale previous_credential on the target context before the proxy/non-proxy branch so proxy_connections reconnects also restart credential selection from the first login_creds entry.
* Modified Connection proxy_connect
* Clear stale previous_credential on each proxy hop before its connection provider runs, so every hop of a multi-hop proxy chain restarts credential selection. Only hop 0 goes through setup_connection(); hops 1 and beyond are connected directly and previously retained relogin state from a prior session.
* Added Connection reset_previous_credential
* Helper that clears previous_credential on a connection and its subconnections.

* unicon.bases.routers.connection
* Modified BaseSingleRpConnection and BaseMultiRpConnection setup_connection
* Clear stale previous_credential (parent and subconnection contexts) at the fresh-spawn boundary so every reconnect restarts credential selection from the first login_creds entry (e.g. terminal server), even for plugins that override disconnect() (hvrp, gaia, fxos/ftd).

* unicon.bases.routers.connection_provider
* Modified BaseSingleRpConnectionProvider establish_connection
* Store the credential that actually answered the most recent password prompt as current_credentials, so a terminal-server post action that reaches the device prompt before later login_creds entries are consumed selects the correct credential.
* Modified disconnect cleanup
* Ensured all spawns are closed when sending the exit command fails.

* unicon.bases
* Modified Connection, BaseMultiRpConnection, and BaseStackRpConnectionProvider
* Moved clear-console retries outside connection dialog callbacks so simultaneous stack member refusals are recovered once and retried from a fresh set of spawns.
* Avoided a redundant second stack state detection after a member has already reached a valid non-disable prompt.
* Preserved the second detection for disable prompts that may actually represent a standby-locked stack member.
* Moved the ``CONNECTION_REFUSED_MAX_COUNT`` default into core settings with the retry logic that consumes it.
* Preserved the originating device for proxy connection refusals so recovery never clears an unrelated target console.
* Preserved the originating proxy device while setting up multi-RP subconnections.
* Preserved the final target device when a refusal occurs after connecting through a proxy.
* Modified connection cleanup
* Prevented failed connections and reconnects from retaining stale PTY-backed spawns.
* Removed temporary connection log handlers after successful and failed connection attempts.

* unicon.eal.backend.pty_backend
* Modified Spawn close
* Made PTY cleanup idempotent and added bounded child-process reaping.
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/august
2026/july
2026/june
2026/may
Expand Down
55 changes: 55 additions & 0 deletions docs/changelog_plugins/2026/august.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
August 2026
==========

August 25 - Unicon.Plugins v26.8
--------------------------------



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

``unicon.plugins``, v26.8
``unicon``, v26.8




Changelogs
^^^^^^^^^^

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

* iosxe
* Updated the shared single-RP and dual-RP syntax transition to use ``config check syntax`` so syntax checking works consistently on IOS XE and CAT9k HA devices.
* Updated ``configure(config_syntax_check=True)`` and ``config_syntax()`` to enter syntax-check mode, validate the supplied commands, and return to enable mode.
* Removed the obsolete ``syntax_configlet`` handling and unregistered ``Syntaxsh`` service.
* Modified state machine
* Answer the uncommitted changes prompt when leaving configuration mode on controller-managed single- and dual-RP devices.
* Updated CAT8K reload-to-ROMMON handling to recognize ``System Bootstrap`` output.
* Saves the environment when prompted and sends Ctrl-C at the boot interruption point.
* Added the ``C8300-2N2S-4T2X`` PID mapping for CAT8K C8300 plugin selection.
* Fixed ROMMON boot dialogs to invoke wrapped statement callbacks according to their declared parameters.
* Copied generic connection statements before applying IOS XE-specific callback wrapping.
* Updated the configure service to automatically answer the interactive RSA key modulus prompt with 2048 bits while allowing a caller-provided reply to override the default.
* Added regression and mock-device coverage to verify RSA key generation completes without a caller-provided dialog.

* iosxe/stack
* Modified init_standby
* Added STACK_STANDBY_CONSOLE_ATTACH setting, off by default. Enable it on stacks where every console connects to the active RP.
* Verify which console is on the standby RP using the '*' row of 'show switch' instead of assuming the designated standby handle.
* Attach to the standby RP console when no console reaches it, so device.standby, target='standby' and swap_roles() no longer operate on the active RP without reporting an error.
* Release a console attach left over from an earlier run before probing, so a stale attach is not mistaken for a real standby.
* Modified disconnect
* Detach the standby RP console so the next connect is not refused with 'Address already in use'.
* Modified set_init_commands
* Skip 'show version' on an attached standby console, whose output is rate limited and times out the connect.
* Added stack-specific reload handling for the transition from ``Press RETURN`` to an authentication prompt. The stack reload dialog now avoids an empty username and completes authentication in the same dialog flow.

* generic
* Updated the syslog message pattern to match ``UNIX ERR:tcgetattr:Inappropriate ioctl for device`` output so it is handled as ignorable syslog noise.
* Modified connection_refused_handler
* Propagate a typed ``ConnectionRefusedError`` to Unicon core instead of clearing terminal lines and reconnecting recursively from a connection dialog callback.
* Removed the plugin-owned ``CONNECTION_REFUSED_MAX_COUNT`` default; it is now defined alongside the retry logic in Unicon core.
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/august
2026/july
2026/june
2026/may
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.7"
__version__ = "26.8"

supported_chassis = [
'single_rp',
Expand Down
4 changes: 3 additions & 1 deletion src/unicon/plugins/generic/patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ def __init__(self):
# % WARNING: The master key is not configured, so passwords/secrets might not be encrypted.
# Configure the master key by using the following command: "key config-key password-encrypt <encryption-key>
# Applying config on Switch 3...[DONE]
# UNIX ERR:tcgetattr:Inappropriate ioctl for device
self.syslog_message_pattern = (
r"^.*?(%\w+(-\S+)?-\d+-\w+|"
r"yang-infra:|PKI_SSL_IPC:|Guestshell destroyed successfully|"
Expand All @@ -86,7 +87,8 @@ def __init__(self):
r"Failed to generate persistent self-signed certificate\.|"
r"Secure server will use temporary self-signed certificate\.|"
r"(LC|RP)/\d+/\d+/CPU\d+:\w+\s+\d+\s+\d{2}:\d{2}:\d{2}|"
r"\[OK\]"
r"\[OK\]|"
r"UNIX ERR:tcgetattr:Inappropriate ioctl for device"
r").*\s*$"
)
self.config_locked = r'Configuration (mode )?(is )?locked|Config mode cannot be entered'
Expand Down
1 change: 0 additions & 1 deletion src/unicon/plugins/generic/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ def __init__(self):
self.CONSOLE_TIMEOUT = 60
self.BOOT_TIMEOUT = 600
self.MAX_BOOT_ATTEMPTS = 3
self.CONNECTION_REFUSED_MAX_COUNT = 3

# Temporary enable secret used during setup
# this is used if no password is available
Expand Down
18 changes: 6 additions & 12 deletions src/unicon/plugins/generic/statements.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,12 @@ def terminal_position_handler(spawn, session, context):
def connection_refused_handler(spawn, context):
""" handles connection refused scenarios
"""
context.setdefault('connection_refused_count', 0)
context['connection_refused_count'] += 1
if context.get('connection_refused_count') < spawn.settings.CONNECTION_REFUSED_MAX_COUNT:
if spawn.device:
spawn.device.api.execute_clear_line()
spawn.device.connect()
return
spawn.log.error(
'Too many connection refused events: {}, update setting '
'CONNECTION_REFUSED_MAX_COUNT as needed.'.format(
context['connection_refused_count']))
raise Exception('Connection refused to device %s' % (str(spawn)))
error = ConnectionRefusedError(
'Connection refused to device %s' % str(spawn))
error.spawn = spawn
error.context = context
error.device = getattr(spawn, 'device', None)
raise error


def connection_failure_handler(spawn):
Expand Down
5 changes: 4 additions & 1 deletion src/unicon/plugins/iosxe/cat8k/service_patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ class ReloadPatterns(IosXEPatterns):

def __init__(self):
super().__init__()
self.boot_interrupt_prompt = r'Preparing to autoboot. \[Press Ctrl-C to interrupt\]'
self.boot_interrupt_prompt = (
r'(?:Preparing to autoboot\. \[Press Ctrl-C to interrupt\]|'
r'System Bootstrap, Version .*)'
)
class SwitchoverPatterns:
def __init__(self):
self.save_config = r'.*System configuration has been modified\.\s*Save\?\s*\[yes\/no\]:\s*$'
Expand Down
12 changes: 10 additions & 2 deletions src/unicon/plugins/iosxe/cat8k/service_statements.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

from unicon.eal.dialogs import Statement
from unicon.plugins.iosxe.cat8k.service_patterns import SwitchoverPatterns, ReloadPatterns
from unicon.plugins.generic.service_patterns import reload_patterns
from unicon.plugins.generic.service_statements import (
save_env, confirm_reset, reload_confirm, reload_confirm_ios)
confirm_reset, reload_confirm, reload_confirm_ios)



Expand Down Expand Up @@ -42,6 +43,13 @@
#############################################################################
patterns = ReloadPatterns()

save_env_to_rommon = Statement(
pattern=reload_patterns.savenv,
action='sendline(yes)',
args=None,
loop_continue=True,
continue_timer=False)

boot_interrupt_stmt = Statement(
pattern=patterns.boot_interrupt_prompt,
action='send(\x03)',
Expand All @@ -50,7 +58,7 @@
continue_timer=False)


reload_to_rommon_statement_list = [save_env,
reload_to_rommon_statement_list = [save_env_to_rommon,
confirm_reset,
reload_confirm,
reload_confirm_ios,
Expand Down
1 change: 1 addition & 0 deletions src/unicon/plugins/iosxe/patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def __init__(self):
self.acm_prompt = r'^(.*?)\(acm.*?\)#[\s\x07]*$'
self.syntax_prompt = r'^(.*?)\(syntax.*?\)#[\s\x07]*$'
self.rules_prompt = r'^(.*?)\(rules.*?\)#[\s\x07]*$'
self.rsa_modulus_prompt = r'^.*How many bits in the modulus \[\d+\]:\s*$'


class IosXEReloadPatterns(ReloadPatterns):
Expand Down
70 changes: 43 additions & 27 deletions src/unicon/plugins/iosxe/service_implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
execute_statement_list,
configure_statement_list,
confirm,
rsa_modulus,
want_continue,
)

Expand All @@ -36,6 +37,13 @@
from unicon.plugins.generic.service_implementation import BashService as GenericBashService


def _with_rsa_modulus_fallback(reply):
"""Append the default RSA response after any caller-provided statements."""
if isinstance(reply, Dialog):
return reply + Dialog([rsa_modulus])
return reply


# Simplex Services
# ----------------
class Configure(GenericConfigure):
Expand All @@ -61,10 +69,19 @@ def truncate_trailing_prompt(self, con_state,

self.utils = ConfigUtils()

def call_service(self, command=[], reply=Dialog([]), *args, **kwargs):
reply = _with_rsa_modulus_fallback(reply)
return super().call_service(command, reply, *args, **kwargs)

def pre_service(self, *args, **kwargs):

self.acm_configlet = kwargs.pop('acm_configlet', None)
self.syntax_configlet = kwargs.pop('syntax_configlet', None)
syntax_configlet = kwargs.pop('syntax_configlet', None)
if syntax_configlet:
raise SubCommandFailure(
"'syntax_configlet' is no longer supported. "
"Use configure(..., config_syntax_check=True) "
"or config_syntax() instead.")
self.config_syntax_check = kwargs.pop('config_syntax_check', False)
self.rules = kwargs.pop('rules', False)
self.prompt_recovery = kwargs.get('prompt_recovery', True)
Expand All @@ -84,12 +101,13 @@ def pre_service(self, *args, **kwargs):
self.start_state = 'rules'
self.end_state = 'rules'

elif self.syntax_configlet or self.config_syntax_check:
configlet_name = self.syntax_configlet if self.syntax_configlet else ''
self.connection.state_machine.go_to('syntax', self.connection.spawn,
context={'syntax_configlet': configlet_name})
elif self.config_syntax_check:
target = kwargs.get('target') if self.connection.is_ha else None
handle = self.get_handle(target)
handle.state_machine.go_to(
'syntax', handle.spawn, context=handle.context)
self.start_state = 'syntax'
self.end_state = 'syntax'
self.end_state = 'enable'

else:
super().pre_service(*args, **kwargs)
Expand All @@ -109,10 +127,9 @@ class Config(Configure):

class ConfigSyntax(Configure):

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.start_state = 'syntax'
self.end_state = 'enable'
def pre_service(self, *args, **kwargs):
kwargs['config_syntax_check'] = True
super().pre_service(*args, **kwargs)


class Execute(GenericExecute):
Expand Down Expand Up @@ -155,9 +172,19 @@ def __init__(self, connection, context, **kwargs):
super().__init__(connection, context, **kwargs)
self.dialog += Dialog(configure_statement_list)

def call_service(self, command=[], reply=Dialog([]), *args, **kwargs):
reply = _with_rsa_modulus_fallback(reply)
return super().call_service(command, reply, *args, **kwargs)

def pre_service(self, *args, **kwargs):
self.acm_configlet = kwargs.pop('acm_configlet', None)
self.syntax_configlet = kwargs.pop('syntax_configlet', None)
syntax_configlet = kwargs.pop('syntax_configlet', None)
if syntax_configlet:
raise SubCommandFailure(
"'syntax_configlet' is no longer supported. "
"Use configure(..., config_syntax_check=True) "
"or config_syntax() instead.")
self.config_syntax_check = kwargs.pop('config_syntax_check', False)
self.rules = kwargs.pop('rules', False)
self.prompt_recovery = kwargs.get('prompt_recovery', True)

Expand All @@ -166,10 +193,12 @@ def pre_service(self, *args, **kwargs):
self.start_state = 'acm'
self.end_state = 'acm'

if self.syntax_configlet:
self.connection.state_machine.go_to('syntax', self.connection.spawn,context={'syntax_configlet': self.syntax_configlet})
if self.config_syntax_check:
handle = self.get_handle(kwargs.get('target'))
handle.state_machine.go_to(
'syntax', handle.spawn, context=handle.context)
self.start_state = 'syntax'
self.end_state = 'syntax'
self.end_state = 'enable'
elif self.rules:
if self.connection.connected:
self.connection.state_machine.go_to('rules', self.connection.spawn)
Expand Down Expand Up @@ -489,19 +518,6 @@ def __init__(self, connection, context, **kwargs):
self.service_name = 'tclsh'
self.__dict__.update(kwargs)

class Syntaxsh(BaseService):

def __init__(self, connection, context, **kwargs):
super().__init__(connection, context, **kwargs)
self.start_state = 'enable'
self.end_state = 'syntax_check'
self.service_name = 'syntax_check'

def call_service(self, syntax_file=None, **kwargs):
cmd = f"syntax configlet check {syntax_file}" if syntax_file else "syntax configlet check"
self.connection.spawn.sendline(cmd)
self.connection.state_machine.go_to('syntax_check', self.connection.spawn, **kwargs)

class MaintenanceMode(ContextMgrBaseService):

def __init__(self, connection, context, **kwargs):
Expand Down
7 changes: 6 additions & 1 deletion src/unicon/plugins/iosxe/service_statements.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ def send_response(spawn, response=""):
macro_prompt = Statement(pattern=patterns.macro_prompt,
loop_continue=False)

rsa_modulus = Statement(pattern=patterns.rsa_modulus_prompt,
action='sendline(2048)',
loop_continue=True,
continue_timer=False)


configure_statement_list = [
are_you_sure,
Expand Down Expand Up @@ -117,4 +122,4 @@ def send_response(spawn, response=""):
are_you_sure_confirm = Statement(pattern=pat.are_you_sure_confirm,
action=send_response, args={'response': ''},
loop_continue=True,
continue_timer=False)
continue_timer=False)
Loading
Loading