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

May 26 - Unicon v26.5
------------------------



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

``unicon.plugins``, v26.5
``unicon``, v26.5




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

* iosxe/cat9k
* Updated `HARommon`
* Ensures HA rommon breaks boot on all consoles via active reload plus parallel standby interrupts with improved state validation.

* iosxe/stack
* Updated `StackStateMachine`
* Refactored rommon path to include entire shelf reload and breakboot on all members.
* update `StackRommon` to inherit from `IosXESingleRpStateMachine` to leverage existing rommon logic and ensure consistency with single RP devices.


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/may
2026/april
2026/march
2026/february
Expand Down
69 changes: 69 additions & 0 deletions docs/changelog_plugins/2026/may.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
May 2026
==========

May 26 - Unicon v26.5
------------------------



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

``unicon.plugins``, v26.5
``unicon``, v26.5




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

* nxos/n9k
* AttachModuleConsoleN9k
* Updated to use 'run bash sudo rlogin lc<N>' command for CTC collection support

* nxos
* AttachModuleConsole
* Updated escape character detection to match 'press ~, to exit' message

* iosxe
* Added self-signed secure server certificate warning messages to syslog
* Updated fast_reload_confirm in pattern to match Proceed with fast reload? [confirm].
* Modified c8kv statemachine
* Updated rommon handling and state transitions
* Cleaned up unused code and improved state management
* Modified c8kv statements
* Updated statements for better rommon support
* Modified patterns
* Updated patterns to support c8kv rommon handling
* Modified statements
* Updated general statements for improved rommon compatibility
* Modified patterns
* Updated are_you_sure pattern to make [y] optional, fixing TimeoutError
* Connection provider
* Updated enable invocation to use the connection-level

* iosxr
* Modified execute service
* Fixed state detection overrides so commands that disable detection do not affect later execute calls.
* Passed the per-command detect_state value through service kwargs instead of storing it on the service instance.

* cheetah/ap
* Updated the AP shell prompt pattern to match both ``~`` and ``/`` shell prompts, and added a regression test for the home-directory prompt case.

* unicon.plugins
* Updated the pid_tokens.csv file to include additional pids.

* generic
* Enable
* Added support for passing `prompt_recovery` to the state machine when


--------------------------------------------------------------------------------
Prompt.
--------------------------------------------------------------------------------


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/may
2026/april
2026/march
2026/february
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.4"
__version__ = "26.5"

supported_chassis = [
'single_rp',
Expand Down
2 changes: 1 addition & 1 deletion src/unicon/plugins/cheetah/ap/patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ class CheetahAPPatterns(GenericPatterns):

def __init__(self):
super().__init__()
self.ap_shell_prompt = r'^(.*?)\w+:\/(.*?)#\s?$'
self.ap_shell_prompt = r'^(.*?)\w+:(~|\/)(.*?)#\s?$'
2 changes: 2 additions & 0 deletions src/unicon/plugins/generic/patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ def __init__(self):
r"%Error opening tftp:\/\/255\.255\.255\.255|Autoinstall trying|"
r"audit: kauditd hold queue overflow|SECURITY WARNING|%RSA key|INSECURE DYNAMIC WARNING|"
r"key config-key password-encrypt|"
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").*\s*$"
Expand Down
9 changes: 4 additions & 5 deletions src/unicon/plugins/generic/service_implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,8 @@ def call_service(self, target=None, command='', *args, **kwargs):
sm.go_to(self.start_state,
spawn,
context=handle.context,
timeout=timeout)
timeout=timeout,
prompt_recovery=self.prompt_recovery)
except (UniconAuthenticationError, CredentialsExhaustedError):
# Don't wrap auth errors - re-raise them directly
raise
Expand Down Expand Up @@ -611,7 +612,6 @@ def __init__(self, connection, context, **kwargs):
self.matched_retry_sleep = connection.settings.EXECUTE_MATCHED_RETRY_SLEEP
self.state_change_matched_retries = connection.settings.EXECUTE_STATE_CHANGE_MATCH_RETRIES
self.state_change_matched_retry_sleep = connection.settings.EXECUTE_STATE_CHANGE_MATCH_RETRY_SLEEP
self.detect_state = True

def log_service_call(self):
pass
Expand All @@ -635,7 +635,7 @@ def call_service(self, command=[], # noqa: C901
if allow_state_change is None:
allow_state_change = con.settings.EXEC_ALLOW_STATE_CHANGE

self.detect_state = detect_state if detect_state is not None else self.detect_state
detect_state = True if detect_state is None else detect_state

timeout = timeout or self.timeout

Expand Down Expand Up @@ -693,7 +693,7 @@ def call_service(self, command=[], # noqa: C901
if custom_auth_stmt:
dialog += Dialog(custom_auth_stmt)

if self.detect_state:
if detect_state:
# Add all known states to detect state changes.
for state in sm.states:
# The current state is already added by the service_dialog method
Expand Down Expand Up @@ -3130,4 +3130,3 @@ def __getattr__(self, attr):
else:
raise AttributeError('Device %s and/or connection %s has no attribute %s'
% (self.conn.device, self.conn, attr))

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 @@ -31,7 +31,7 @@ def __init__(self):
self.connection_closed = r'^(.*?)Connection.*? closed|disconnect: Broken pipe'
self.press_return = r'Press RETURN to get started.*'
self.config_session_locked = r'^.*Config session is locked.*user will be pushed back to exec mode'

self.fast_reload_confirm = r'^.*Proceed( with( (quick|fast))? reload)?\?\s*\[confirm\]'

# Traceroute patterns
class TraceroutePatterns(object):
Expand Down
4 changes: 4 additions & 0 deletions src/unicon/plugins/iosxe/c8kv/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
from unicon.plugins.iosxe import IosXEServiceList, IosXESingleRpConnection
from unicon.plugins.iosxe.c8kv.statemachine import IosXEC8kvSingleRpStateMachine

from . import service_implementation as svc

class IosXEC8kvServiceList(IosXEServiceList):
def __init__(self):
super().__init__()
self.reload = svc.Reload


class IosXEC8kvSingleRpConnection(IosXESingleRpConnection):
platform = 'c8kv'
Expand Down
22 changes: 22 additions & 0 deletions src/unicon/plugins/iosxe/c8kv/service_implementation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
""" IOS-XE C8KV service implementations. """

from unicon.eal.dialogs import Dialog
from ..service_implementation import Reload as IosxeReload
from .statements import boot_from_rommon_stmt

from unicon.plugins.generic.service_statements import reload_statement_list
from unicon.plugins.generic.statements import default_statement_list
from unicon.plugins.iosxe.statements import grub_prompt_stmt

from .statements import boot_from_rommon_stmt


class Reload(IosxeReload):
"""C8KV Reload service that handles GRUB boot scenarios."""

def __init__(self, connection, context, **kwargs):
super().__init__(connection, context, **kwargs)
# Override the service dialog to include c8kv specific statements
self.dialog = Dialog([boot_from_rommon_stmt, grub_prompt_stmt] +
reload_statement_list +
default_statement_list)
26 changes: 8 additions & 18 deletions src/unicon/plugins/iosxe/c8kv/statemachine.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,18 @@
GRUB boot mode and golden image recovery.
"""

from unicon.statemachine import State, Path
from unicon.eal.dialogs import Dialog, Statement
from unicon.plugins.iosxe.statemachine import (
IosXESingleRpStateMachine,
IosXEDualRpStateMachine,
boot_from_rommon
)
from unicon.plugins.iosxe.statements import boot_from_rommon_statement_list
from unicon.plugins.generic.service_patterns import ReloadPatterns
from unicon.statemachine import Path
from unicon.eal.dialogs import Dialog
from unicon.plugins.iosxe.statemachine import IosXESingleRpStateMachine
from unicon.plugins.generic.patterns import GenericPatterns
from unicon.plugins.iosxe.cat8k.service_statements import (
reload_to_rommon_statement_list)
from unicon.plugins.iosxe.statements import boot_from_rommon_statement_list

from .statements import boot_from_rommon


generic_patterns = GenericPatterns() # Uses generic patterns to support GRUB prompt


class IosXEC8kvSingleRpStateMachine(IosXESingleRpStateMachine):
"""State machine for single RP Cisco Catalyst 8000V devices.

Expand Down Expand Up @@ -53,25 +50,18 @@ def create(self):
# Get state objects
rommon = self.get_state('rommon')
disable = self.get_state('disable')
enable = self.get_state('enable')

# Update rommon pattern to include GRUB prompt (grub>)
# GenericPatterns.rommon_prompt matches: rommon>, switch:, and grub>
rommon.pattern = generic_patterns.rommon_prompt

# Remove default paths that don't handle GRUB properly
self.remove_path('rommon', 'disable')
self.remove_path('enable', 'rommon')

# Add C8KV-specific rommon-to-disable path
# Uses custom boot_from_rommon_statement_list that handles GRUB
rommon_to_disable = Path(rommon, disable, boot_from_rommon, Dialog(
boot_from_rommon_statement_list))

# Add C8KV-specific enable-to-rommon path for reload operations
enable_to_rommon = Path(enable, rommon, 'reload', Dialog(
reload_to_rommon_statement_list))

# Register the custom paths
self.add_path(rommon_to_disable)
self.add_path(enable_to_rommon)
79 changes: 34 additions & 45 deletions src/unicon/plugins/iosxe/c8kv/statements.py
Original file line number Diff line number Diff line change
@@ -1,58 +1,47 @@
import re
import time
import datetime
import logging

from unicon.eal.dialogs import Statement
from unicon.plugins.generic.statements import (
boot_timeout_stmt,
)

from unicon.plugins.iosxe.patterns import IosXEReloadPatterns, IosXEPatterns
from ..patterns import IosXEPatterns
from ..settings import IosXESettings

log = logging.getLogger(__name__)
reload_patterns = IosXEReloadPatterns()
logger = logging.getLogger(__name__)
patterns = IosXEPatterns()
settings = IosXESettings()


def boot_from_rommon(statemachine, spawn, context):
context['boot_start_time'] = datetime.datetime.now()
context['boot_prompt_count'] = 1
if context.get('grub_boot_image') is None:
logger.info('No grub_boot_image specified, will use default')
else:
logger.info(f"Using grub_boot_image: {context['grub_boot_image']}")
logger.info('Sending escape to trigger boot menu in GRUB')
# C8KV uses GRUB as its bootloader rather than traditional ROMMON.
# Sending ESC interrupts the GRUB autoboot countdown and presents
# the boot menu, allowing selection of a specific boot image.
spawn.send('\x1b')


def boot_image(spawn, context, session):
if not context.get('boot_prompt_count'):
context['boot_prompt_count'] = 1
if context.get('boot_prompt_count') < \
spawn.settings.MAX_BOOT_ATTEMPTS:
if "boot_cmd" in context:
cmd = context.get('boot_cmd')
elif "image_to_boot" in context:
cmd = "boot {}".format(context['image_to_boot']).strip()
elif spawn.settings.FIND_BOOT_IMAGE:
filesystem = spawn.settings.BOOT_FILESYSTEM if \
hasattr(spawn.settings, 'BOOT_FILESYSTEM') else 'flash:'
spawn.buffer = ''
spawn.sendline('dir {}'.format(filesystem))
dir_listing = spawn.expect(patterns.rommon_prompt).match_output
boot_file_regex = spawn.settings.BOOT_FILE_REGEX if \
hasattr(spawn.settings, 'BOOT_FILE_REGEX') else r'(\S+\.bin)'
m = re.search(boot_file_regex, dir_listing)
if m:
boot_image = m.group(1)
cmd = "boot {}{}".format(filesystem, boot_image)
else:
cmd = "boot"
else:
cmd = "boot"
spawn.sendline(cmd)
context['boot_prompt_count'] += 1
def send_escape(spawn, session):
session.setdefault('boot_attempt_count', 0)
if session.get('boot_attempt_count') < settings.MAX_BOOT_ATTEMPTS:
spawn.send('\x1b') # send escape character to trigger boot menu in GRUB
session['boot_attempt_count'] += 1
else:
raise Exception("Too many failed boot attempts have been detected.")
err_info = 'Too many failed boot attempts have been detected.'
raise Exception(err_info)


# Create c8kv specific boot from rommon statement
# C8KV is a virtual platform that exclusively uses
# GRUB bootloader - the ROMMON prompt is always grub>,
# never the classic rommon> or switch: prompts.
boot_from_rommon_stmt = Statement(
pattern=patterns.rommon_prompt,
action=boot_image,
action=send_escape,
args=None,
loop_continue=True,
continue_timer=False)

# This list is extended later, see below
boot_from_rommon_statement_list = [
boot_timeout_stmt,
boot_from_rommon_stmt
]
continue_timer=False
)
1 change: 1 addition & 0 deletions src/unicon/plugins/iosxe/cat9k/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class IosxeCat9kHAServiceList(HAIosXEServiceList):
def __init__(self):
super().__init__()
self.reload = svc.HAReloadService
self.rommon = svc.HARommon


class IosXECat9kSingleRpConnection(IosXESingleRpConnection):
Expand Down
Loading
Loading