diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..8502a0ea --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @CiscoTestAutomation/pyats-genie-devs \ No newline at end of file diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml new file mode 100644 index 00000000..15816be7 --- /dev/null +++ b/.github/workflows/run_tests.yml @@ -0,0 +1,36 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Run Tests + +on: + - push + - pull_request + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + group: [1, 2, 3, 4, 5] + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pyats[full] pytest pytest-split + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip uninstall unicon.plugins -y + - name: Test Unit Tests + run: | + make develop + cd tests + py.test --splits 5 --group ${{ matrix.group}} -v -k 'test_ and not test_connect_mit' + shell: bash diff --git a/.gitignore b/.gitignore index 372fe5c2..0aa403e5 100644 --- a/.gitignore +++ b/.gitignore @@ -55,7 +55,9 @@ uni.log # Files resulting from a git meld merge *.orig +# VSCode +.vscode +.history # ignore auto generate docs docs/user_guide/services/service_dialogs.rst - diff --git a/Makefile b/Makefile index e1fb8a2a..6537e95a 100644 --- a/Makefile +++ b/Makefile @@ -3,9 +3,7 @@ PKG_NAME = unicon.plugins BUILD_DIR = $(shell pwd)/__build__ DIST_DIR = $(BUILD_DIR)/dist SOURCEDIR = . -PROD_USER = pyadm@pyats-ci -PROD_PKGS = /auto/pyats/packages/cisco-shared -PYTHON = python +PYTHON = python3 TESTCMD = runAll --path=tests/ BUILD_CMD = $(PYTHON) setup.py bdist_wheel --dist-dir=$(DIST_DIR) PYPIREPO = pypitest @@ -13,27 +11,34 @@ PYPIREPO = pypitest DEPENDENCIES = robotframework pyyaml dill coverage Sphinx \ sphinxcontrib-napoleon sphinxcontrib-mockautodoc \ - sphinx-rtd-theme asyncssh + sphinx-rtd-theme asyncssh PrettyTable "cryptography>=43.0" .PHONY: clean package distribute develop undevelop help devnet\ - docs test install_build_deps uninstall_build_deps + docs test install_build_deps uninstall_build_deps distribute_staging\ + distribute_staging_external help: @echo "Please use 'make ' where is one of" @echo "" - @echo "package Build the package" - @echo "test Test the package" - @echo "distribute Distribute the package to internal Cisco PyPi server" - @echo "clean Remove build artifacts" - @echo "develop Build and install development package" - @echo "undevelop Uninstall development package" - @echo "docs Build Sphinx documentation for this package" - @echo "install_build_deps does nothing - just following pyATS pkg standard" - @echo "uninstall_build_deps does nothing - just following pyATS pkg standard" + @echo "package Build the package" + @echo "test Test the package" + @echo "distribute Distribute the package to internal Cisco PyPi server" + @echo "distribute_staging Distribute build pkgs to staging area" + @echo "distribute_staging_external Distribute build pkgs to external staging area" + @echo "clean Remove build artifacts" + @echo "develop Build and install development package" + @echo "undevelop Uninstall development package" + @echo "docs Build Sphinx documentation for this package" + @echo "install_build_deps does nothing - just following pyATS pkg standard" + @echo "uninstall_build_deps does nothing - just following pyATS pkg standard" + @echo "changelogs Build compiled changelog file" @echo "" install_build_deps: + @pip install --upgrade pip setuptools wheel + @echo "" + @echo "Done." @echo "" uninstall_build_deps: @@ -47,12 +52,13 @@ docs: @echo "Building $(PKG_NAME) documentation for preview: $@" @echo "" - python docs/gen_dialogs_rst.py > docs/user_guide/services/service_dialogs.rst + python3 docs/gen_dialogs_rst.py > docs/user_guide/services/service_dialogs.rst sphinx-build -b html -c docs -d ./__build__/documentation/doctrees docs/ ./__build__/documentation/html @echo "Completed building docs for preview." @echo "" - + @echo "Done." + @echo "" test: @$(TESTCMD) @@ -68,6 +74,8 @@ package: @echo "" @echo "Completed building: $@" @echo "" + @echo "Done." + @echo "" develop: @echo "" @@ -76,10 +84,12 @@ develop: @echo "" @pip uninstall -y $(PKG_NAME) @pip install $(DEPENDENCIES) - @$(PYTHON) setup.py develop --no-deps + @pip install -e . --no-deps @echo "" @echo "Completed building and installing: $@" @echo "" + @echo "Done." + @echo "" undevelop: @echo "" @@ -87,11 +97,13 @@ undevelop: @echo "Uninstalling $(PKG_NAME) development distributable: $@" @echo "" - @$(PYTHON) setup.py develop --no-deps -q --uninstall + @pip uninstall $(PKG_NAME) -y @echo "" @echo "Completed uninstalling: $@" @echo "" + @echo "Done." + @echo "" clean: @echo "" @@ -115,3 +127,36 @@ distribute: @echo "" @echo "Done." @echo "" + +distribute_staging: + @echo "" + @echo "--------------------------------------------------------------------" + @echo "Copying all distributable to $(STAGING_PKGS)" + @test -d $(DIST_DIR) || { echo "Nothing to distribute! Exiting..."; exit 1; } + @ssh -q $(PROD_USER) 'test -e $(STAGING_PKGS)/$(PKG_NAME) || mkdir $(STAGING_PKGS)/$(PKG_NAME)' + @scp $(DIST_DIR)/* $(PROD_USER):$(STAGING_PKGS)/$(PKG_NAME)/ + @echo "" + @echo "Done." + @echo "" + +distribute_staging_external: + @echo "" + @echo "--------------------------------------------------------------------" + @echo "Copying all distributable to $(STAGING_EXT_PKGS)" + @test -d $(DIST_DIR) || { echo "Nothing to distribute! Exiting..."; exit 1; } + @ssh -q $(PROD_USER) 'test -e $(STAGING_EXT_PKGS)/$(PKG_NAME) || mkdir $(STAGING_EXT_PKGS)/$(PKG_NAME)' + @scp $(DIST_DIR)/* $(PROD_USER):$(STAGING_EXT_PKGS)/$(PKG_NAME)/ + @echo "" + @echo "Done." + @echo "" + +changelogs: + @echo "" + @echo "--------------------------------------------------------------------" + @echo "Generating changelog file" + @echo "" + @python3 -c "from ciscodistutils.make_changelog import main; main('./docs/changelog/undistributed', './docs/changelog/undistributed.rst')" + @python3 -c "from ciscodistutils.make_changelog import main; main('./docs/changelog_plugins/undistributed', './docs/changelog_plugins/undistributed.rst')" + @echo "" + @echo "Done." + @echo "" diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..4dcc6179 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,45 @@ +#### Contribute to documentation + +To contribute, you need to fork the repository, do your modifications and create a new pull request. + +> :warning: **Please make sure you have the full pyats package installed via ```pip install pyats[full]```.** + +To build the docs locally on your machine. Please follow the instructions below + + - Go to the [Unicon.plugins Github repository](https://github.com/CiscoTestAutomation/unicon.plugins) + + - On the top right corner, click ```Fork```. (see https://help.github.com/en/articles/fork-a-repo) + +Screen Shot 2020-12-21 at 2 37 19 PM + + - In your terminal, clone the repo using the command shown below: + ```shell + git clone https://github.com//unicon.plugins.git + ``` + + - ```cd unicon.plugins/docs``` + + - Use ```make install_build_deps``` to install all of the build dependencies + + - Run ```make docs``` to generate documentation in HTML + + - Wait until you see ```Done``` in your terminal + + - The documentation is now built and stored under the directory + ```unicon.plugins/__build__``` + + - Run ```make serve``` to view the documentation on your browser + + - Please create a PR after you have made your changes (see [commit your changes](https://pubhub.devnetcloud.com/media/pyats-development-guide/docs/contribute/contribute.html#commit-your-changes) & [open a PR](https://pubhub.devnetcloud.com/media/pyats-development-guide/docs/contribute/contribute.html#open-a-pull-request)) + +Here are a few examples that could be great pull request: + +- Fix Typos +- Better wording, easier explanation +- More details, examples +- Anything else to enhance the documentation + + +#### How to contribute to the pyATS community + +- For detail on contributing to pyATS, please follow the [contribution guidelines](https://pubhub.devnetcloud.com/media/pyats-development-guide/docs/contribute/contribute.html#) diff --git a/docs/api/modules.rst b/docs/api/modules.rst new file mode 100644 index 00000000..f82f9b2a --- /dev/null +++ b/docs/api/modules.rst @@ -0,0 +1,7 @@ +Unicon API Reference +==================== + +.. toctree:: + :maxdepth: 4 + + unicon diff --git a/docs/api/unicon.bases.linux.rst b/docs/api/unicon.bases.linux.rst new file mode 100644 index 00000000..346e2917 --- /dev/null +++ b/docs/api/unicon.bases.linux.rst @@ -0,0 +1,38 @@ +unicon.bases.linux package +========================== + +Submodules +---------- + +unicon.bases.linux.connection module +------------------------------------ + +.. automodule:: unicon.bases.linux.connection + :members: + :undoc-members: + :show-inheritance: + +unicon.bases.linux.connection_provider module +--------------------------------------------- + +.. automodule:: unicon.bases.linux.connection_provider + :members: + :undoc-members: + :show-inheritance: + +unicon.bases.linux.services module +---------------------------------- + +.. automodule:: unicon.bases.linux.services + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: unicon.bases.linux + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/unicon.bases.routers.rst b/docs/api/unicon.bases.routers.rst new file mode 100644 index 00000000..c96cc178 --- /dev/null +++ b/docs/api/unicon.bases.routers.rst @@ -0,0 +1,38 @@ +unicon.bases.routers package +============================ + +Submodules +---------- + +unicon.bases.routers.connection module +-------------------------------------- + +.. automodule:: unicon.bases.routers.connection + :members: + :undoc-members: + :show-inheritance: + +unicon.bases.routers.connection_provider module +----------------------------------------------- + +.. automodule:: unicon.bases.routers.connection_provider + :members: + :undoc-members: + :show-inheritance: + +unicon.bases.routers.services module +------------------------------------ + +.. automodule:: unicon.bases.routers.services + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: unicon.bases.routers + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/unicon.bases.rst b/docs/api/unicon.bases.rst new file mode 100644 index 00000000..9d81da30 --- /dev/null +++ b/docs/api/unicon.bases.rst @@ -0,0 +1,30 @@ +unicon.bases package +==================== + +Subpackages +----------- + +.. toctree:: + + unicon.bases.linux + unicon.bases.routers + +Submodules +---------- + +unicon.bases.settings module +---------------------------- + +.. automodule:: unicon.bases.settings + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: unicon.bases + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/unicon.core.rst b/docs/api/unicon.core.rst new file mode 100644 index 00000000..00b6a894 --- /dev/null +++ b/docs/api/unicon.core.rst @@ -0,0 +1,38 @@ +unicon.core package +=================== + +Submodules +---------- + +unicon.core.errors module +------------------------- + +.. automodule:: unicon.core.errors + :members: + :undoc-members: + :show-inheritance: + +unicon.core.manager module +-------------------------- + +.. automodule:: unicon.core.manager + :members: + :undoc-members: + :show-inheritance: + +unicon.core.pluginmanager module +-------------------------------- + +.. automodule:: unicon.core.pluginmanager + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: unicon.core + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/unicon.database.rst b/docs/api/unicon.database.rst new file mode 100644 index 00000000..feed3306 --- /dev/null +++ b/docs/api/unicon.database.rst @@ -0,0 +1,22 @@ +unicon.database package +======================= + +Submodules +---------- + +unicon.database.database module +------------------------------- + +.. automodule:: unicon.database.database + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: unicon.database + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/unicon.eal.backend.rst b/docs/api/unicon.eal.backend.rst new file mode 100644 index 00000000..473a244e --- /dev/null +++ b/docs/api/unicon.eal.backend.rst @@ -0,0 +1,22 @@ +unicon.eal.backend package +========================== + +Submodules +---------- + +unicon.eal.backend.pty_backend module +------------------------------------- + +.. automodule:: unicon.eal.backend.pty_backend + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: unicon.eal.backend + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/unicon.eal.rst b/docs/api/unicon.eal.rst new file mode 100644 index 00000000..d6d1489a --- /dev/null +++ b/docs/api/unicon.eal.rst @@ -0,0 +1,69 @@ +unicon.eal package +================== + +Subpackages +----------- + +.. toctree:: + + unicon.eal.backend + +Submodules +---------- + +unicon.eal.bases module +----------------------- + +.. automodule:: unicon.eal.bases + :members: + :undoc-members: + :show-inheritance: + +unicon.eal.dialog_processor module +---------------------------------- + +.. automodule:: unicon.eal.dialog_processor + :members: + :undoc-members: + :show-inheritance: + +unicon.eal.dialogs module +------------------------- + +.. automodule:: unicon.eal.dialogs + :members: + :undoc-members: + :show-inheritance: + +unicon.eal.expect module +------------------------ + +.. automodule:: unicon.eal.expect + :members: + :undoc-members: + :show-inheritance: + +unicon.eal.helpers module +------------------------- + +.. automodule:: unicon.eal.helpers + :members: + :undoc-members: + :show-inheritance: + +unicon.eal.utils module +----------------------- + +.. automodule:: unicon.eal.utils + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: unicon.eal + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/unicon.rst b/docs/api/unicon.rst new file mode 100644 index 00000000..c79e1371 --- /dev/null +++ b/docs/api/unicon.rst @@ -0,0 +1,73 @@ +unicon package +============== + +Subpackages +----------- + +.. toctree:: + + unicon.bases + unicon.core + unicon.database + unicon.eal + unicon.statemachine + +Submodules +---------- + +unicon.logs module +------------------ + +.. automodule:: unicon.logs + :members: + :undoc-members: + :show-inheritance: + +unicon.patterns module +---------------------- + +.. automodule:: unicon.patterns + :members: + :undoc-members: + :show-inheritance: + +unicon.settings module +---------------------- + +.. automodule:: unicon.settings + :members: + :undoc-members: + :show-inheritance: + +unicon.type_checkers module +--------------------------- + +.. automodule:: unicon.type_checkers + :members: + :undoc-members: + :show-inheritance: + +unicon.utils module +------------------- + +.. automodule:: unicon.utils + :members: + :undoc-members: + :show-inheritance: + +unicon.sshutils module +---------------------- + +.. automodule:: unicon.sshutils + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: unicon + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/unicon.statemachine.rst b/docs/api/unicon.statemachine.rst new file mode 100644 index 00000000..300522c5 --- /dev/null +++ b/docs/api/unicon.statemachine.rst @@ -0,0 +1,30 @@ +unicon.statemachine package +=========================== + +Submodules +---------- + +unicon.statemachine.statemachine module +--------------------------------------- + +.. automodule:: unicon.statemachine.statemachine + :members: + :undoc-members: + :show-inheritance: + +unicon.statemachine.statetransition module +------------------------------------------ + +.. automodule:: unicon.statemachine.statetransition + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: unicon.statemachine + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/changelog/2016/december.rst b/docs/changelog/2016/december.rst new file mode 100644 index 00000000..205df381 --- /dev/null +++ b/docs/changelog/2016/december.rst @@ -0,0 +1,15 @@ +December 2016 +============== + +December 06 +------------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v2.1.0b1 + +Features: +^^^^^^^^^ + + - Added Moonshine support for pyATS. diff --git a/docs/changelog/2016/february.rst b/docs/changelog/2016/february.rst new file mode 100644 index 00000000..44ba7292 --- /dev/null +++ b/docs/changelog/2016/february.rst @@ -0,0 +1,19 @@ +February 2016 +============= + +February 1 - v1.0.0 +------------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v1.0.0 + + +Features: +^^^^^^^^^ + + - New pty based expect backend library. + - 100% CPU bug fix + - Aireos Implementation. + diff --git a/docs/changelog/2016/may.rst b/docs/changelog/2016/may.rst new file mode 100644 index 00000000..51190a1f --- /dev/null +++ b/docs/changelog/2016/may.rst @@ -0,0 +1,22 @@ +May 2016 +======== + +May 4 - v2.0.0 +-------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v2.0.0 + + +Features: +^^^^^^^^^ + + - Unicon integration with pyATS topology + - Support for IOSXE platform + - Support for linux platform + - Support for NXOS vdc - Unicon Device logging enhancements + - Shorthand notations for Dialog callbacks + - Renamed `config` service to `Configure` + - Bug fixes diff --git a/docs/changelog/2016/november.rst b/docs/changelog/2016/november.rst new file mode 100644 index 00000000..6cdb2c93 --- /dev/null +++ b/docs/changelog/2016/november.rst @@ -0,0 +1,24 @@ +November 2016 +============= + +November 25 - v2.2.0 +-------------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v2.2.0 + +Features and Bug Fixes +^^^^^^^^^^^^^^^^^^^^^^ + + - Support for NXOSv, iosxrv9, csr1000v, IOSv virtual devices + - SSH based connection for both routers & server enhancements + - Fixed Unicon and parsergen interoperability issue fixed + - Ping service support for sweep ping + - Switchover service enhanced (no wait for standby ) + - Unicon expect timeout issue fixed + - Linux plugin disconnect issue fixed + - Pattern changes for Nxos login and config state + - Unicon expect buffer sync issue fixed + - orphaned processes on disconnect fixed diff --git a/docs/changelog/2016/october.rst b/docs/changelog/2016/october.rst new file mode 100644 index 00000000..6233c835 --- /dev/null +++ b/docs/changelog/2016/october.rst @@ -0,0 +1,35 @@ +October 2016 +============ + +October 1 +--------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v2.1.0b15 + +Features: +^^^^^^^^^ + + - Further increased post-prompt wait for iosxrv plugin to better ensure device + is ready for configuration before configuration is attempted. + + - Refactored iosxrv9k plugin to use common device wait logic, added additional + patterns after CI failures seen. + +October 27 +---------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v2.1.0b17 + +Features +^^^^^^^^ + +- Linux reconnect issue fixed + + + diff --git a/docs/changelog/2016/september.rst b/docs/changelog/2016/september.rst new file mode 100644 index 00000000..94f79df1 --- /dev/null +++ b/docs/changelog/2016/september.rst @@ -0,0 +1,99 @@ +September 2016 +============== + +September 30 +------------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v2.1.0b14 + +Features: +^^^^^^^^^ + + - Increased post-prompt wait for iosxrv plugin to better ensure device + is ready for configuration before configuration is attempted. + + +September 29 +------------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v2.1.0b13 + +Features: +^^^^^^^^^ + + - Added trim_buffer=False to expect during iosxrv and iosxrv9k launchup check. + + +September 28 +------------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v2.1.0b12 + + +Features: +^^^^^^^^^ + + - NXOSv mini-cleaner tuning to account for the virtual platform + displaying extra console text after the switch# prompt. + + +September 27 +------------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v2.1.0b11 + + +Features: +^^^^^^^^^ + + - NXOSv mini-cleaner tuning to account for the virtual platform + displaying extra console text after the login: and Password: prompts. + + - Linux plugin fix for parsergen integration issue. + + +September 26 +------------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v2.1.0b10 + + +Features: +^^^^^^^^^ + + - Introduced mini-cleaner for iosxe/csr1000v + (required by dyntopo/laas to launch Ultra virtual devices). + + - Tuned the iosv, iosxrv, nxosv and iosxrv9k plugins + to behave better when running on a heavily loaded execution server. + + +September 23 +------------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v2.1.0b9 + + +Features: +^^^^^^^^^ + + - Introduced mini-cleaner for iosxrv + (required by dyntopo/laas to launch XRVR). diff --git a/docs/changelog/2017/august.rst b/docs/changelog/2017/august.rst new file mode 100644 index 00000000..d44b2d72 --- /dev/null +++ b/docs/changelog/2017/august.rst @@ -0,0 +1,70 @@ +August 2017 +=========== + +August 10 - v2.3.4 +------------------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v2.3.4 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +The following changes were introduced: + +- generic plugin + + - Refactored telnet handler to allow plugin-specific delay after + initial telnet to the device and before pressing . + +- iosxe/csr1000v plugin + + - Added initial telnet delay before pressing to prevent + timeouts when connecting to some image variants. + +August 8 - v2.3.3 +----------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v2.3.3 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +The following changes were introduced: + +- Linux plugin + + - Refactored prompt stripping for the execute service. + +- Generic plugin + + - Refactored copy service to be more real-time efficient. + +- Device mocking + + - Added mock devices for various iosxe flavors : asr, isr, cat3k. + + - Other packages may now invoke mock devices from their unit tests. diff --git a/docs/changelog/2017/december.rst b/docs/changelog/2017/december.rst new file mode 100644 index 00000000..3cfd4972 --- /dev/null +++ b/docs/changelog/2017/december.rst @@ -0,0 +1,77 @@ +December 2017 +============= + +December 20 - v2.3.8 +-------------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v2.3.8 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- Updates to Linux plugin: + + - Added 'hit and enter' prompt + + - Prompt pattern speed fix + + - Added unittest for pattern speed + + +- Updates to ConfD plugin: + + - Support for CSP (Cloud Services Platform) + + +- Updates to iosxr plugin: + + - Pattern updated to capture the device output in addition to device prompt + + +- Updates to iosxr/moonshine plugin: + + - Improved support for confirmation prompt y/n handling + + +- Updates to iosxe/cat3k and nxos plugins: + + - Fixed some cases in which prompt recovery was not being properly invoked. + +- Updates to iosxe and nxos plugins: + + - Fixed a day-one bug, now post-reload HA sync detection loop works correctly. + +- Generic patterns + + - Updated bad_password pattern + + +- IOS unittest update + + - Added test for password error handling + + +- Mock device updates: + + - Get response text from a list of responses (linear or circular) + + - Mock data loading from directory specified on cli + + +- Developer updates: + + - Support for callable as statemachine command + + - Dotgraph method to create graphical representation of the statemachine diff --git a/docs/changelog/2017/feb.rst b/docs/changelog/2017/feb.rst new file mode 100644 index 00000000..8b2b2e16 --- /dev/null +++ b/docs/changelog/2017/feb.rst @@ -0,0 +1,72 @@ +Feb 2017 +======== + +Feb 8 - v2.2.1 +-------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v2.2.1 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes +^^^^^^^^^^^^^^^^^^^^^^ + - New core feature - hostname learning: + + - Allows the already configured device hostname to be learned if it + is not known. + + - Multiple plugin updates to ensure compatibility with this feature + (generic, iosxv, iosxr, nxosv). + + - Plugin updates: + - Cheetah AP support + + - Generic plugin updates: + + - Updated rommon pattern to better match several IOS and IOSXE devices. + + - Now stringifying service commands to allow them to be passed as + non-string objects. + + - Now allowing for list-like and string-like input objects. + + - telnet escape character callback now waits for a limited time + for chatter to cease before calling sendline. + + - nxos plugin updates: + + - Now allowing for list-like and string-like input objects. + + - iosxe/csr1000v plugin updates - tuned timing parameters + + - iosxr plugin updates: + + - Tuned timing parameters for iosxrv + + - Removed partially implemented iosxr HA execute service, now + using generic plugin implementation. + + - Core updates: + + - Support for ``%N`` hostname substitution outside statemachine. + Needed by some uniclean plugins. + + - Now stringifying objects before sending via spawn. + + - pyATS adapter updates: + + - Now properly rendering start when port specified. + + - Now assigning series and model correctly. + + - Now stringifying the IP address in case it is passed in as an object. diff --git a/docs/changelog/2017/jan.rst b/docs/changelog/2017/jan.rst new file mode 100644 index 00000000..87e0e458 --- /dev/null +++ b/docs/changelog/2017/jan.rst @@ -0,0 +1,14 @@ +Jan 2017 +======== + +Jan 4 +----- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v2.2.1b1 + +Features and Bug Fixes +^^^^^^^^^^^^^^^^^^^^^^ + - Cheetah AP support diff --git a/docs/changelog/2017/july.rst b/docs/changelog/2017/july.rst new file mode 100644 index 00000000..42c14cb6 --- /dev/null +++ b/docs/changelog/2017/july.rst @@ -0,0 +1,30 @@ +July 2017 +========= + +July 10 - v2.3.2 +---------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v2.3.2 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +The following changes were introduced: + +- Device mocking unit test infrastructure + + - Added developer documentation + + - Added ping unit tests to ios plugin diff --git a/docs/changelog/2017/june.rst b/docs/changelog/2017/june.rst new file mode 100644 index 00000000..d5c6aacc --- /dev/null +++ b/docs/changelog/2017/june.rst @@ -0,0 +1,49 @@ +June 2017 +========= + +June 28 - v2.3.1 +---------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v2.3.1 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +The following changes were introduced: + +- Introduction of new plugins : asa and nso + +- Added generic device mocking infrastructure to enable plugin unit test. + +- pyATS testbed/topology support + + - Added support for 'port' option in pyATS testbed file with SSH protocol. + You can now specify the port in the testbed file when SSH is used, + the port will be added as '-p '. + +- ise plugin + + - Bug fix (LINUX_INIT_EXEC_COMMANDS not found) + - Refactored prompt to include hostname + +- linux plugin + + - Corrected prompt pattern + +- nxos plugin + + - Fixes to IPv6 ping + + - Now using "show system redundancy status" instead of "sh redundancy status" diff --git a/docs/changelog/2017/may.rst b/docs/changelog/2017/may.rst new file mode 100644 index 00000000..76843785 --- /dev/null +++ b/docs/changelog/2017/may.rst @@ -0,0 +1,78 @@ +May 2017 +======== + + +May 8 - v2.3.0 +-------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v2.3.0 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +The following changes were introduced as a result of uniclean +IOSXE plugin development: + +- Added IOSXE simplex plugin + + - Pulled up common functionality from iosxe/cat3k plugin to iosxe layer. + + - Added rommon->disable transition logic. + +- Added IOSXE HA plugin + + - Enhanced generic enable/disable prompt patterns to support + IOSXE standby RP prompt. + + - Now properly detecting standby locked state. + + - Pushed down HA role detection logic to generic and nxos plugin layer from + unicon core. + +- Changes to iosxe/cat3k plugin + + - Now throwing exception if a cyclic bootloader reboot loop is detected. + +- Changes to generic plugin + + - Added retry / max_attempts to generic copy service to allow retries when + uniclean image copy fails, this will help build resilience against + sporadic network issues. + + - Added a new prompt removal algorithm to the generic Execute service + (both simplex and HA) + +- Extended the iosxe/cat3k Reload service with a connection locked detection + and wait loop to ensure a stack with a standby peer comes up correctly + +- Enhanced EAL/Pty layer to: + + - perform a graceful shutdown when closing the spawn session. + + - perform a post-shutdown wait to ensure "Connection Refused" is not seen + if a back-to-back disconnect/reconnect is done. + +- Enhanced Settings base class to allow it to be multiply inherited by + uniclean settings object. + +- Enhanced expect logs to include target and match groups for easier debugging. + +- Added baseline support for mocked devices. + +- Added enable_vdc statement to the simplex N7K nxos reload statement list. + +- Various bug fixes arising from Genie integration. + +- Addition of xrutconnect protocol support for Moonshine diff --git a/docs/changelog/2017/november.rst b/docs/changelog/2017/november.rst new file mode 100644 index 00000000..e194e22b --- /dev/null +++ b/docs/changelog/2017/november.rst @@ -0,0 +1,59 @@ +November 2017 +============= + +November 18 - v2.3.7 +-------------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v2.3.7 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- New plugin: Voice Operating System (VOS) + +- New plugin: Cisco Integrated Management Console (CIMC) + +- Updates to generic plugin: + + - pattern update for [confirm] prompt + + - documentation updates to clarify default Dialog for execute service + + +- Updates for ConfD plugin + + - prompt pattern matching update + + - NSO plugin now using confd implementation + + +- Topology handling for linux connection + + - fixed regression with command option for linux connection + + +- NXOS shellexec documentation update + + - example use of 'sudo' + + +- Mock device updates + + - raise error on duplicate state in yaml files + + +- Updates to aireos, confd, generic, ise, nxos plugins + + - Updated plugin regex patterns to improve speed diff --git a/docs/changelog/2017/october.rst b/docs/changelog/2017/october.rst new file mode 100644 index 00000000..8374bab1 --- /dev/null +++ b/docs/changelog/2017/october.rst @@ -0,0 +1,27 @@ +October 2017 +============ + +October 25 - v2.3.6 +------------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v2.3.6 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- Prompt recovery feature added for Dialog, connection and selected services. + +- Updated iosv, nxosv, iosxrv and iosxrv9k plugins to ensure connect works + properly against an Esxi vCenter backend. diff --git a/docs/changelog/2017/september.rst b/docs/changelog/2017/september.rst new file mode 100644 index 00000000..4b43393f --- /dev/null +++ b/docs/changelog/2017/september.rst @@ -0,0 +1,81 @@ +September 2017 +============== + +September 18 - v2.3.5 +--------------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v2.3.5 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- New plugin: ConfD with support for NSO, ESC and NFVIS + +- Update to nxos plugin to support ``shellexec`` on non-MDS platforms. + +- Handle SSH ``continue connecting`` in generic and IOSXR plugin. + +- Added ``%`` to linux prompt pattern. + +- NSO plugin updates + + - Added ``error_pattern`` option to NSO plugin services + + - Updated implementation for execute, configure and command + services to allow ``error_pattern`` option to be specified. + + - Update documentation with examples of error_patterns. + + - Fixed bugs in command stripping and timeout handling + + - Updated unittests + + - Added tests with list of commands for configure and execute services + + - Changed NsoConnection to Connection class. + + - Changed the assertion statements to use unittest assertion methods. + + - Added unittests for passing ``error_pattern`` options. + + - Documentation update + + Added example execute service with list of commands. + + - Changes in ``execute`` service: maintain CLI style, change in output + stripping. + + When you execute a command using the ``execute`` service, the style + that is active before execution is restored at the end of the + execution. + + This means that you cannot use the ``execute`` service to switch styles, + use the ``cli_style`` service to change CLI style. + + Executing the command ``switch cli`` raises an exception and + point to cli_style. + + The output is stripped of whitespace from the right only, + if a CR/LF is present at the start of the output it is stripped. + Previously, whitespace was stripped from both sides of the output text. + +- iosxe and iosxe/cat3k plugin updates: + + - Adapted patterns to be more real-time efficient to better handle long + outputs. + + - Introduced mocked device tests for ASR HA, ISR and CAT3k. + + - Fixed the switchover service so it works properly for ASR HA. diff --git a/docs/changelog/2018/apr.rst b/docs/changelog/2018/apr.rst new file mode 100644 index 00000000..202b6fe6 --- /dev/null +++ b/docs/changelog/2018/apr.rst @@ -0,0 +1,37 @@ +April 2018 +========== + +April 30 - v3.1.0 +----------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v3.1.0 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + # to use the new robot-framework dependencies, d: + bash$ pip install unicon[robot] + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ +- Updated XR prompt pattern to match complete prompt + +- Added back os.sync to send method as this was causing issues on some platforms. + +- Updates in generic plugin escape handler + After connecting to a device via console server, do not send 'enter' + if an authentication prompt is shown + +- added ``unicon.robot`` module: now Unicon comes with robot-framework keywords. + +- note that you must install robotframework in order to import ``unicon.robot`` + module. diff --git a/docs/changelog/2018/february.rst b/docs/changelog/2018/february.rst new file mode 100644 index 00000000..09b53da8 --- /dev/null +++ b/docs/changelog/2018/february.rst @@ -0,0 +1,89 @@ +February 2018 +============= + +February 12 - v3.0.1 +-------------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v3.0.1 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- Linux connection and plugin updates + + - Added ping service + + - Added learn hostname feature + + Linux connections now support the `learn_hostname` connect option to + automatically learn the hostname. + + This improves reliable prompt matching as the default prompt matching may + result in false positives when the command output contains one of the + prompt pattern characters `> # % ~ $` at the end of a line. + + - Prompt stripping update + + Only the last matching prompt is stripped from the output. + When using the default prompt, false prompt matches may strip + parts of the output instead of the prompt. + + - Updated unittests + + The linux plugin unittests now cover about a dozen known prompts + to validate prompt pattern matching and hostname learning. + + +- iosxr plugin updates: + + - Fixed bug in iosxr and iosxr/iosxrv plugins that was causing incorrect + output from device.execute. + + - Update to Moonshine plugin: + Make the prompt regex be more restrictive, by using the regex enforced in + XR command files. + + +- nxos plugin updates: + + - HA reload service now rediscovers active/standby roles + to accommodate targets that may switch roles after reload. + + +- Core features + + - Updated plugin discovery mechanism to support external plugin packages. + + - Removed OS static checking list, and made a warning instead. + + - New feature that allows user to specify initial exec and config command + when connecting to a device. Users can now specify `init_exec_commands` + and `init_config_commands` options when connecting to a device. + + - The terminal variable is now set to VT100 before launching the telnet or + ssh connection to a device. This is to tell devices not to use fancy ANSI + escape characters (e.g. colors) in the prompt. The escape characters + conflict with the (Linux) learn_hostname feature. + + - Removed os.sync from send method as this was causing hung sessions + on some platforms. + + +- Testing related features + + - Added mock_device_cli console script to run mock device as + a standalone program. + + diff --git a/docs/changelog/2018/january.rst b/docs/changelog/2018/january.rst new file mode 100644 index 00000000..89fb7251 --- /dev/null +++ b/docs/changelog/2018/january.rst @@ -0,0 +1,25 @@ +January 2018 +============ + +January 8 - v2.3.9 +-------------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v2.3.9 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- Fixed connect failure on the iosxr family of plugins by repairing + enable and config prompts. diff --git a/docs/changelog/2018/jul.rst b/docs/changelog/2018/jul.rst new file mode 100644 index 00000000..bd9c09f6 --- /dev/null +++ b/docs/changelog/2018/jul.rst @@ -0,0 +1,101 @@ +July 2018 +========= + + +Jul 16 - v3.2.0 +--------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v3.2.0 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +.. warning:: + + Please be advised that some changes have been introduced into the execute + service that may be potentially backwards incompatible for some users. + + - The execute service no longer forces enable mode as its start and end + state. + + - The execute service now returns a dictionary when multiple commands + are passed either in a list or via a multiline string. + + - Multiline strings are now executed line by line, expecting a prompt after + each line. This may not address all scenarios, users may need to change + their command to use a list with multiline string as documented in the + user guide. + +- New plugin: StarOS with support for Starent OS. + +- New plugin: Firepower Extensible Operating System (FXOS). + +- Robot keyword fixes: + + - Correctly use configure timeout. + + - Fix send control character. + +- ConfD plugin update: + Added option to ignore chatty terminal output with execute() and configure() + services. + + +- Generic plugin updates: + + - Updated generic yes/no pattern. + + - Limit the number of password attempts by the default password handler to 3. + + - Fix default password handler logic that alternates between + enable and tacacs password. + + +- Add bash_console service for IOSXE and NXOS, add attach_console for NXOS. + + +- Enhanced NXOS/IOSXE/IOSXR plugins to accomodate pyATS :ref:`connectionpool` + feature. + + +- Core feature updates: + + - Added SSH tunnel feature + + - Changed backend buffer matching to use maximum search buffer size + (default: 8K bytes). This change significantly improves pattern matching + speeds for large command output. + + - Bug fix for line_password that was passed incorrectly. + + - CLI proxy bugfix for ssh username not being specified. + + - Refactored service error pattern handling to match by line. + + +- Mock device updates: + + - Added SSH server support. + + - Support for device hostname variable %N. + + - Fix usage of mock device directory parameter. + + +- IOSXR admin pattern updates: + + - Added ASR9K series handles for admin patterns. + + - Updated IOSXRV series admin patterns. + \ No newline at end of file diff --git a/docs/changelog/2018/jun.rst b/docs/changelog/2018/jun.rst new file mode 100644 index 00000000..f5c193e5 --- /dev/null +++ b/docs/changelog/2018/jun.rst @@ -0,0 +1,31 @@ +June 2018 +========= + + +Jun 7 - v3.1.2 +-------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v3.1.2 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- NOTE : This version has been re-released under a previously released + version but now has additional content. + +- Generic plugin fixes: + + - Fix generic HA reload class to handle console interchange after reload. + + - Added prompt_recovery option for generic sync_state service. diff --git a/docs/changelog/2018/march.rst b/docs/changelog/2018/march.rst new file mode 100644 index 00000000..1b6658bb --- /dev/null +++ b/docs/changelog/2018/march.rst @@ -0,0 +1,80 @@ +March 2018 +========== + +March 9 - v3.0.2 +---------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v3.0.2 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- Generic plugin updates + + - Fixed a hang observed on initial telnet connection. + +- Linux plugin updates + + - Bug fix to the learn_hostname feature. + +- Core bug fixes + + - Fixed a bug in spawn.expect, now users may inspect which pattern in + a list matched the output, bringing the feature into alignment with + dialog.process. + + - Fixed a bug in the addplugin helper. + + - Plugins can set the TERM attribute to set the TERM environment variable. + Some plugins require this setting in order to ignore ANSI escape sequences + coming from the device. + + +March 27 - v3.0.3 +----------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v3.0.3 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- Linux plugin update + + - new NXOS subcommand for attaching to consoles on linecards + + - new NXOS subcommand for attaching to bash shell using context managers + +- CIMC plugin update + Add response for `Enter 'yes' or 'no' to confirm` pattern + +- New feature: CLI proxy + This feature allows users to connect to devices via another device. + +- Updates to VOS plugin + regex pattern updates + support for Continue (y/n) prompt + set pagination to off by default diff --git a/docs/changelog/2018/may.rst b/docs/changelog/2018/may.rst new file mode 100644 index 00000000..39406d28 --- /dev/null +++ b/docs/changelog/2018/may.rst @@ -0,0 +1,55 @@ +May 2018 +======== + + +May 12 - v3.1.2 +--------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v3.1.2 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ +- IOSXE plugin updates: + + - Fixed a minor bug in the newly refactored ping service, now an explicitly set + ping command has the highest precedence. + + + +May 7 - v3.1.1 +-------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v3.1.1 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ +- IOSXE plugin updates: + + - Refactored the ping service to allow it to properly handle vrf + specification. + +- NXOS plugin update: + + - now bash_console() ``feature bash`` command respects timeout value diff --git a/docs/changelog/2018/nov.rst b/docs/changelog/2018/nov.rst new file mode 100644 index 00000000..1ddc0ab7 --- /dev/null +++ b/docs/changelog/2018/nov.rst @@ -0,0 +1,82 @@ +November 2018 +============= + +Nov 27 - v3.4.3 +--------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v3.4.3 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + +Features and Bug Fixes +^^^^^^^^^^^^^^^^^^^^^^ + +- Raise EOF exception if spawn closed or terminated. + +- Add new ``junos`` plugin to support Juniper devices. + +- Reload service: + + - Send 'n' key only once for POAP prompt. + +- Add new services to the ``iosxr`` plugin: + - attach_console + - bash_console + - admin_console + - admin_attach_console + - admin_bash_console + + +Nov 15 - v3.4.2 +--------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v3.4.2 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + +Features and Bug Fixes +^^^^^^^^^^^^^^^^^^^^^^ + +- Copy Service + + - Added reply parameter for passing additional Dialog. + + - Server parameter is no longer mandatory if source or dest contains + the server IP address. + + - The dest_file parameter may now be specified on nxos platforms. + + +- Configure Service + + - Bug fix to improve support for large configurations by ensuring the + ``timeout`` parameter is properly respected. + + +- Reload Service + + - Fixed an issue seen on nxos reload by tightening up the configure + prompt pattern. + + +- iosxr Plugin + + - Bug fixes to confirm prompt handling. diff --git a/docs/changelog/2018/oct.rst b/docs/changelog/2018/oct.rst new file mode 100644 index 00000000..1a2030d5 --- /dev/null +++ b/docs/changelog/2018/oct.rst @@ -0,0 +1,40 @@ +October 2018 +============ + +Oct 9 - v3.4.0 +--------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v3.4.0 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + +Features and Bug Fixes +^^^^^^^^^^^^^^^^^^^^^^ + +- fixed unicon logging with pyATS where forkeds/pcalls were not being + respected + +- added new feature where now each device gets its own overall send/receive + log + +- significantly optimized unicon log handling in general + +- optimized log output to be more human friendly, indicating which device + it's coming from + +- removed blinker package dependency + +- modified yaml.load to yaml.safe_load for CVE-2017-18342 + +- Linux prompt pattern updated for ESA WSA and SMA appliances + +- Update Confd/NFVIS plugin to allow default hostname of nfvis diff --git a/docs/changelog/2018/sept.rst b/docs/changelog/2018/sept.rst new file mode 100644 index 00000000..3001d2c9 --- /dev/null +++ b/docs/changelog/2018/sept.rst @@ -0,0 +1,70 @@ +Sept 2018 +========= + +Sept 5 - v3.3.0 +--------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v3.3.0 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- NXOS plugin fix: + Fixed broken ping6 service for HA NXOS connection. + +- Username and password details can now be specified per connnection. + +- Plugin update for IOS-XR/NCS5K + Added reload service for NCS5K devices + +- CLI proxy bugfix + List of commands was only executing the last command + +- XR pattern fixes + +- Ignore chatty terminal output in generic execute service + settings.IGNORE_CHATTY_TERM_OUTPUT is to True to ignore previous terminal output + before executing commands. + +- CLI proxy bugfix + List of commands was only executing the last command + +- Learn hostname default pattern update + execute() now returns output when default hostnames like Switch or Router are used + +- ASA plugin updates + Basic unittest for ASA plugin + Pattern update to account for priority and state in prompt + Settings inheritance from generic settings + +- CLI proxy bugfix + List of commands was only executing the last command + +- IOS-XR plugin updates + Added switchover service + execute service fixes + admin execute fixes + Add 'xr' as a possible prompt, as it is the default for spitfire + Improve failed config handling + Add a grouping to match everything before the prompt to the moonshine patterns + Make the XR prompt matching more restrictive + XRv launch wait updates using dialogs + +- Fix bug preventing passing the logfile argument + +- AireOS plugin update + Bugfix for error pattern setting + Unittest for AireOS plugin + +- NXOS plugin fix shell prompt pattern diff --git a/docs/changelog/2019/april.rst b/docs/changelog/2019/april.rst new file mode 100755 index 00000000..73c69179 --- /dev/null +++ b/docs/changelog/2019/april.rst @@ -0,0 +1,69 @@ +April 2019 +========== + +April 29th +---------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v19.4.0 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- learn_hostname feature updated to allow common plugin-specific default device + names such as `Router` to be learned if no hostname has been set on the + device. + +- The iosxr plugin enable pattern is now more strict. + +- Removal of legacy proxy implementation + +- Add timing support for preface in mock_device + +- Fix linux statemachine issue on slow connection setup + +- Now allowing settings to be replaced when specified as an object on + connection setup. + Specifying settings as a dictionary still updates the existing settings. + +- New Traceroute command + +- Added error patterns to iosxe, iosxr, nxos and fxos plugins. + + +April 1st +--------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v19.0.2 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- junos plugin connection statemenent fixes + +- Added response for 'Connected.' message on connect (e.g. when connecting via serial console) + +- Updated IOSXR enable prompt pattern to fix hostname learning diff --git a/docs/changelog/2019/aug.rst b/docs/changelog/2019/aug.rst new file mode 100644 index 00000000..c7c8689f --- /dev/null +++ b/docs/changelog/2019/aug.rst @@ -0,0 +1,138 @@ +August 2019 +=========== + +August 27th +----------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v19.8 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ +- core + + - fix base Connection to log plugin model info + + - fixed XR-UT topology adapter + + - Allow login_creds to be specified in pyATS connect() call to override + the credentials in the testbed YAML. + +- generic plugin + + - add bulk_chunk_lines and bulk_chunk_sleep arguments for generic configure service + + - add generic confirm_prompt_y_n_stmt statement + + - fix copy service issue where retry sometimes exits unexpectedly + + - enhance copy service to send ctrl+c when TimeoutError happens in order to recover device into enable state + +- iosxe plugin + + - fix iosxe/csr1000v plugin services + + - add iosxe/cat3k/ewlc plugin + + - add iosxe/csr1000v/vewlc plugin + +- aireos plugin + + - add prompt_recovery support for aireos reload service + +- linux plugin + + - Update the Linux and ise plugins to properly detect a failed password attempt. + +- ios plugin + + - add error_patterns verification for ios execute service + + +August 7th +---------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v19.7.5 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- Change to the following plugins iosxr, iosxr/spitfire, generic, nxos + + - When prompting for administrator's password, the current credential is + now reused. + +August 2nd +---------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v19.7.4 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- Introduction of nxos/n5k plugin + +- Spawn now sets terminal size on a best-effort basis. + +- Fixed issue preventing many services from being called in a multithreaded + environment. + +August 1st +---------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v19.7.3 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- Fixed iosxr plugin issue that was preventing the standby RP from being + detected. + + diff --git a/docs/changelog/2019/dec.rst b/docs/changelog/2019/dec.rst index 9bc46a01..d579e588 100644 --- a/docs/changelog/2019/dec.rst +++ b/docs/changelog/2019/dec.rst @@ -7,7 +7,7 @@ December 17th .. csv-table:: Module Versions :header: "Modules", "Versions" - ``unicon.plugins``, v19.12 + ``unicon``, v19.12 Install Instructions @@ -15,7 +15,7 @@ Install Instructions .. code-block:: bash - bash$ pip install unicon.plugins + bash$ pip install unicon Upgrade Instructions @@ -23,11 +23,14 @@ Upgrade Instructions .. code-block:: bash - bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon Features and Bug Fixes: ^^^^^^^^^^^^^^^^^^^^^^^ +- core + + - enhance Connection logfile to handle special characters in hostname and alias - generic plugin diff --git a/docs/changelog/2019/jan.rst b/docs/changelog/2019/jan.rst new file mode 100644 index 00000000..25c0cb8d --- /dev/null +++ b/docs/changelog/2019/jan.rst @@ -0,0 +1,142 @@ +January 2019 +============ + +Jan 31 +------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v3.4.7 + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + +Features and Bug Fixes +^^^^^^^^^^^^^^^^^^^^^^ + +- Fixed a timeout related issue that was causing switchover service to fail. + + +Jan 23 +------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v3.4.6 + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + +Features and Bug Fixes +^^^^^^^^^^^^^^^^^^^^^^ + +- New plugin: ACI + +- Generic plugin + + - Update connect statements to handle setup prompts + + - press enter on 'kerberos no realm message' with username prompt + + - Added log_file service. + +- Updated hostname learning to strip ansi escape codes from learned hostname + +- Fix robot keyword error pattern handling in config keyword + +- Added error pattern to linux plugin to catch 'No such file or directory' errors + + +Jan 21 +------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v3.4.5 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + +Features and Bug Fixes +^^^^^^^^^^^^^^^^^^^^^^ + +- Added package dependency that was missing from v3.4.4. + + +Jan 19 +------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v3.4.4 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + +Features and Bug Fixes +^^^^^^^^^^^^^^^^^^^^^^ + +- Added restore_state_pattern to state machine. + +- Now generic copy service is only retrying in case of suspected bad + network connection. + +- Added support for replace and force parameter on xr config service. + +- Added 'junos' to list of supported OS, created unit test to flag when + the list of supported OS doesn't match the available plugin list. + +- Added new generic services transmit / receive. + +- Fixed a bug with password handling where enable and tacacs passwords were + getting mixed + +- Optimized log output to be more human friendly, indicating which device + it's coming from + +- Removed blinker package dependency + +- Option to maintain initial state (mit) on connect + +- XR plugin ping service now accepts vrf as input and passes it as part + of the ping command (as opposed to the generic implementation which + expects the device to prompt for vrf). + +- Now SpawnInitError exception is raised if the spawn start command is + not present and executable. + +- Adapt the generic HA reload service to accept the reload_command parameter + to align it with the simplex reload service. + +- Add standby support for bash_console service + +- Update ASA plugin to use generic connection statements, move init command to exec mode + +- Password handling refactoring + +- Playback functionality has been added. You can now record your device and + save it to file. This allow to re-run script without having any device + + diff --git a/docs/changelog/2019/jul.rst b/docs/changelog/2019/jul.rst new file mode 100644 index 00000000..26f4cad0 --- /dev/null +++ b/docs/changelog/2019/jul.rst @@ -0,0 +1,174 @@ +July 2019 +========= + +July 31st +--------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v19.7.2 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- Updated linux prompt pattern to handle additional cases. +- Updated pattern failures seen on device connection. + + +July 30th +--------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v19.7 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ +- core + + - fix StateTransition do_transitions to return correct output + + - fix dialogs with multi thread to send command to correct connection + + - inherit base Connection from Lockable and add RLock for BaseService + + - improve performance by enhancing pty_backend to support different modes in match_buffer. + By default, match_mode_detect is enabled. Detect rules are as below: + + - search whole buffer with re.DOTALL if: + + - pattern contains any of: \\r, \\n + + - pattern equals to any of: .*, ^.*$, .*$, ^.*, .+, ^.+$, .+$, ^.+ + + - Else if pattern ends with $, will only match last line + + - In other situations, search whole buffer with re.DOTALL + + - improve performance by compiling regex patterns first in dialog_processor + + - improve performance by removing re.search again in truncate_trailing_prompt + + - add connection "host" in SSHTunnel and topology adapter + + +- added credential support + + - When pyATS integration is used, + + - If a ``default`` credential is supplied, then a credential of any other + name is looked up explicitly and is not found, the ``default`` credential + is used instead. + + - credentials supplied to the connection contain any credentials defined + at the device and testbed levels as well. + + - If one or more credentials are supplied: + + - The ``tacacs`` and ``passwords`` pyATS testbed keys are ignored. + + - Use of any of the following `unicon.Unicon.Connection` arguments cause a + deprecation warning to be raised : + + - ``username`` + - ``password`` + - ``enable_password`` + - ``tacacs_password`` + - ``line_password`` + + - The following credential names are expected to be defined explicitly: + + - ``enable`` : This credential defines the password to be sent when + bringing routing devices to their enable mode. + + - ``sudo`` : The fsos/ftd plugin expects this credential to contain + the sudo password. + + - ``ssh`` : When setting up an sshtunnel against a server specified in + a pyATS testbed servers block, this credential must be defined against + that server block. + + - The ``login_creds`` argument (specified either in pyATS connection + block or as a `unicon.Unicon.Connection` parameter), now controls + the order credentials are applied when username/password prompts are + received while connecting to the device. + + - The ``prompts/login`` and ``prompts/password`` parameters are now + expected to be explicitly set in the pyATS connection block or + as `unicon.Unicon.Connection` parameters. + + - The switchover service now accepts a ``switchover_creds`` parameter that + allows users to define what credentials to use should a username or + password prompt occur during switchover. + + - The reload service now accepts a ``reload_creds`` parameter that + allows users to define what credentials to use should a username or + password prompt occur during reload. + + - The execute service no longer responds to username/password requests, + users are expected to pass in their own dialog if this kind of handling + is required. + + +- generic plugin + + - add flatten_splitlines_command method in generic utils to flatten commands + + - add get_handle method in BaseService for all services to reuse + + - add bulk argument for Configure service to send commands in one sendline + + - refactor generic Configure service, and now HaConfigureService inherits from Configure + + - fix several bugs in BaseService and generic HaExecService + + +- iosxr plugin + + - fix potential bugs in iosxr execute and configure related services + + - add HaAdminExecute and HaAdminConfigure services for iosxr + + - fix asr9k plugin services admin_execute, admin_configure and admin_bash_console on 64-bit asr9k + + - added dual RP support to iosxr/spitfire plugin. + + +- junos plugin + + - fix junos plugin configure service + + +- nxos plugin + + - added VDC related robot commands. + + +- asa plugin + + - added warning to ASA plugin patterns. + + +- ios plugin + + - added vrf support in ios plugin ping service. It now accepts vrf as input and passes it as part of the ping command diff --git a/docs/changelog/2019/jun.rst b/docs/changelog/2019/jun.rst new file mode 100755 index 00000000..79402083 --- /dev/null +++ b/docs/changelog/2019/jun.rst @@ -0,0 +1,113 @@ +June 2019 +========= + +June 25th +--------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v19.6 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- iosxr plugin + + - Now handling "Enter secret:" and "Enter secret again:" correctly. + + - iosxr/spitfire regex fixes, added init config commands with timeout. + + - spitfire plugin now accepts username and enable password. + +- nxos plugin + + - Added guestshell service. + + - Config lock fix + + - add utils method retry_state_machine_go_to + + - add arguments in generic Configure and HaConfigure service for retrying go_to config sate + + - add retry go_to config sate in nxos Reload and HANxosReloadService + + - fix nxos configuration locked problem after reload + + - add nxos n9k plugin whose reload service supports image_to_boot argument + + +- generic plugin + + - Fix reload service that was hanging when mgmt connection was attempted. + + - Updated execute() service to allow override of default service dialogs by + passing `service_dialog` + + - improve ping extd_ping judgement and fix endless ping dialog on erroneous + value + + - Copy service now correctly detects "Could not resolve hostname" as an error + +- asa plugin + + - update to handle --more-- prompt. + +- ios plugin + + - add iol plugin including switchover support for dIOL devices. + +- core + + - modifed ``unicon_record``, ``unicon_replay``, ``unicon_speed`` environment + variables to ``UNICON_RECORD``, ``UNICON_REPLAY``, and ``UNICON_REPLAY_SPEED``. + + - Disconnect timers may now be updated via Settings object + + - Dialogs are now documented using autogenerated documentation for connect() + and execute() services. + + - Mock device updates: + Updated code that replaces the string ESC in prompt with \1xb. + Print the command that was deemed invalid. + Added ASA mock device to test more prompt handling. + + - The 'init_exec_commands' and 'init_config_commands' options can now be + passed via the connection block in the yaml topology file. + + - use SimpleDialogProcessor instead of AlarmBasedDialogProcessor + + + +June 3rd +-------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v19.5.1 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- Remove hard ``asyncssh`` package dependency. + Now users requiring SSH mocks must manually install the + ``asyncssh`` package. diff --git a/docs/changelog/2019/march.rst b/docs/changelog/2019/march.rst new file mode 100755 index 00000000..2b4c3203 --- /dev/null +++ b/docs/changelog/2019/march.rst @@ -0,0 +1,62 @@ +March 2019 +========== + +March 12th +---------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v19.0.1 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- Fix bug where prompt recovery was not applied on initial connection during + init command execution. + +- Fix bug to ensure protocol is not required in connection block when command + key is specified. + +March 4th +--------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v19.0 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- Introducing the new iosxr/spitfire plugin for Lindt platform. + +- Record new doc, and fix a limitation for cython package. + +- Add enable password option in utils clear_line + +- Support passing of settings object as Settings() class, dict or AttributeDict + +- support for python 3.7 + +- Add support for custom login and password prompts. + +- New Robot keyword to set Unicon settings. diff --git a/docs/changelog/2019/may.rst b/docs/changelog/2019/may.rst new file mode 100755 index 00000000..e8e841b0 --- /dev/null +++ b/docs/changelog/2019/may.rst @@ -0,0 +1,74 @@ +May 2019 +======== + +May 28th +-------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v19.5.0 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- iosxr plugin + + - Enhance following patterns to support different versions of iosxr: + run_prompt, admin_prompt, admin_conf_prompt, admin_run_prompt + + - Fixed admin_attach_console on iosxr plugin, it now exits correctly. + +- Update user guide to remove prompt argument from bash_console service + +- Added ASA plugin error pattern. + +- Generic plugin + + - The generic switchover service now respects the timeout parameter. + + - Added retries option to the generic HA config service. + + - Now ensuring device is brought back to ``enable`` state after + reload or switchover. + +- Core changes + + - Enhance RawSpawn expect, add argument "log_timeout" to control + whether log Timeout info + +- Introducing iosxe/sdwan plugin with config commit support. + + + +May 8th +------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v19.4.1 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- Added return_output option to the reload service of the generic, nxos and + iosxe/cat3k plugins, now allowing reload output to be returned. diff --git a/docs/changelog/2019/nov.rst b/docs/changelog/2019/nov.rst index 7fc3776d..9b2652ef 100644 --- a/docs/changelog/2019/nov.rst +++ b/docs/changelog/2019/nov.rst @@ -1,13 +1,14 @@ November 2019 ============= -November 27th +November 26th ------------- .. csv-table:: Module Versions :header: "Modules", "Versions" - ``unicon.plugins``, v19.11.1 + ``unicon``, v19.11 + Install Instructions @@ -15,7 +16,7 @@ Install Instructions .. code-block:: bash - bash$ pip install unicon.plugins + bash$ pip install unicon Upgrade Instructions @@ -23,44 +24,26 @@ Upgrade Instructions .. code-block:: bash - bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon Features and Bug Fixes: ^^^^^^^^^^^^^^^^^^^^^^^ +- core -- aireos plugin - - - remove f-strings that is not supported on python 3.4 and 3.5 - - -November 26th -------------- - -.. csv-table:: Module Versions - :header: "Modules", "Versions" - - ``unicon.plugins``, v19.11 + - separate plugins from unicon to be a sinlge package unicon.plugins + - use mock_device_cli instead of python run mock_device -Install Instructions -^^^^^^^^^^^^^^^^^^^^ - -.. code-block:: bash - - bash$ pip install unicon.plugins - + - add matched_retries for Statement to avoid transient match on output -Upgrade Instructions -^^^^^^^^^^^^^^^^^^^^ - -.. code-block:: bash + - enhance UniconStreamHandler to handle UnicodeEncodeError - bash$ pip install --upgrade unicon.plugins + - enhance RawPtySpawn to set environment variable via via settings + - enhance RawSpawn to use shlex for start command split -Features and Bug Fixes: -^^^^^^^^^^^^^^^^^^^^^^^ + - now allow settings.DEFAULT_LEARNED_HOSTNAME to be used by plugins - generic plugin diff --git a/docs/changelog/2019/oct.rst b/docs/changelog/2019/oct.rst new file mode 100644 index 00000000..99b87a19 --- /dev/null +++ b/docs/changelog/2019/oct.rst @@ -0,0 +1,72 @@ +October 2019 +============ + +October 29th +------------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v19.10 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ +- core + + - Replace self.FLAG of spwan with self.has_buffer_left + + - Remove unnecessary Timeout log from setup_connection of BaseSingleRpConnection and BaseDualRpConnection + + - Add changes to correct truncation logic in add_state_pattern + + - Fix prompt recovery for connect/disconnect/connect scenario + + - Fix mock_device with SSH connections + + - Fix incorrect plugin selection for some scenarios + +- generic plugin + + - Fix issue that receive service always fails after first receive attempt + + - Fix generic get_mode service + + - Change some service regex patterns to be looser on \s number + + - Enhance Execute and HaExecute to remove backspace and escape sequence in output + + - Enhance execute service to remove "--More--" in output + + - Fix copy service to raise exception when "No such file or directory" is reported + +- nxos plugin + + - Add reload_creds to nxos and nxos/n5k plugins + +- iosxr plugin + + - Add reload_creds to iosxr ncs5k plugin + + - Enhance spitfire plugin connect to look at ZTP lock and config lock to ensure + initial connect does not fail right after reimage + + - Fix nxos HA connection to correctly handle "--More--" during connect stage + + - Add "logging console disable" into iosxr init configure command + + - Fix iosxr ask9k switchover service by changing STANDBY_STATE_REGEX + + - Enhance TraceroutePatterns for iosxr + +- iosxe plugin + + - Fix iosxe HA execute to correctly handle "--More--" diff --git a/docs/changelog/2019/sept.rst b/docs/changelog/2019/sept.rst new file mode 100644 index 00000000..e7e84811 --- /dev/null +++ b/docs/changelog/2019/sept.rst @@ -0,0 +1,51 @@ +September 2019 +============== + +September 24th +-------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v19.9 + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ +- core + + - Enhance SSHTunnel for multi-threaded connect + + - Add changes to correct the invalid learned_hostname + + - Add testbed YAML patchability for connection arguments, settings and service attributes + + - Add changes to allow user-defined dialog on connect + + - Log exception details when connection fails + +- generic plugin + + - Enhance execute service to remove backspaces and previous characters from command output + + - Use POST_HA_RELOAD_CONFIG_SYNC_WAIT for HAReloadService to bring standby into any state + +- asa plugin + + - New asa/asav plugin + +- linux plugin + + - Add Linux return code check feature + +- iosxr plugin + + - Fix HAReloadService for iosxr diff --git a/docs/changelog/2020/april.rst b/docs/changelog/2020/april.rst new file mode 100644 index 00000000..31fc173f --- /dev/null +++ b/docs/changelog/2020/april.rst @@ -0,0 +1,40 @@ +April 2020 +============ + +April 28th +------------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v20.4 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ +* Fixed unittests failures seen with multiprocessing on Mac-py38 environment + +* Added `goto_enable` and `standby_goto_enable` key to generic connect service to + allow user to disable device behavior of going to enable state in every device + connect call, Default is True not to interrupt intuitive device behavior + +* Added dialog callback for credentials + +* See also the unicon.plugins changelog. + diff --git a/docs/changelog/2020/august.rst b/docs/changelog/2020/august.rst new file mode 100644 index 00000000..213b2fe0 --- /dev/null +++ b/docs/changelog/2020/august.rst @@ -0,0 +1,37 @@ +August 2020 +============ + +August 25th +------------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v20.8 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ +* Infrastructure changed to support multi-connections(dual, stack & quad) +* Infrastructure changed to support learn os feature in generic plugin +* Enhanced cli proxy feature, now can support HA device +* Allowed to set the connection terminal size via ROWS and COLUMNS environment variables for the connection +* Updated spawn read method to ignore non-utf8 decoder errors +* Allowed prompt_recovery to pass from connection class variable to service variable +* Added trim line option in the unicon logging to trim empty lines diff --git a/docs/changelog/2020/december.rst b/docs/changelog/2020/december.rst new file mode 100644 index 00000000..931e9151 --- /dev/null +++ b/docs/changelog/2020/december.rst @@ -0,0 +1,57 @@ +December 2020 +============= + +December 15th +------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v20.12 + ``unicon``, v20.12 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +* IOSXE plugin + - Updated regex for config prompt + - Fixed patterns and added ca_profile for its config to be matched +* IOSXR plugin + * NCS5K plugin + - Fixed HA Reload to use correct credentials +* NXOS ACI Plugin + * Added configure service + * Removed deprecation message from nxos->aci->n9k + * Fixed a bug where the buffer might not be empty after connecting to the device +* ASA Plugin + - Add error_pattern to capture `*** WARNING ***` +* FXOS/FTD Plugin + - Added support for "* " in chassis prompt, e.g. "FirePower* #" +* Linux + * Added passphrase pattern in connection dialogs + * Made it possible to override the shell prompt from the connection settings +* Core + * Added feature to extend list settings from testbed file + * Fixed log issue when pyats managed_handlers's tasklog stream is None + * Fixed parse_spawn_command for ha device to get the right subconnection context + * Fixed ssh command username issue + * Enhacnced ha device connectivity check diff --git a/docs/changelog/2020/feb.rst b/docs/changelog/2020/feb.rst new file mode 100644 index 00000000..0ebf0c5b --- /dev/null +++ b/docs/changelog/2020/feb.rst @@ -0,0 +1,70 @@ +February 2020 +============= + +February 25 +------------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v20.2 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +* Added `debug_statement` boolean argument to `Statement class` to print the statement matched pattern. + +* Added `STATEMENT_LOG_DEBUG` boolean argument to `Settings class` to print all the matched patterns. + +* Add python3.8 support. + +* See also the unicon.plugins changelog. + + +January 16th +------------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v19.12.1 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ +- robot + + - fix for getting ats or pyats library diff --git a/docs/changelog/2020/jan.rst b/docs/changelog/2020/jan.rst new file mode 100644 index 00000000..c9f3d74a --- /dev/null +++ b/docs/changelog/2020/jan.rst @@ -0,0 +1,65 @@ +January 2020 +============ + +January 28th +------------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v20.1 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ +- No core changes. + +- See also the unicon.plugins changelog. + + +January 16th +------------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v19.12.1 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ +- robot + + - fix for getting ats or pyats library diff --git a/docs/changelog/2020/july.rst b/docs/changelog/2020/july.rst new file mode 100644 index 00000000..15b4c30d --- /dev/null +++ b/docs/changelog/2020/july.rst @@ -0,0 +1,35 @@ +July 2020 +============ + +July 28th +------------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v20.7 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ +* Added deprecation warning for expect_log service + +* Fixed adapter issue to allow debug logging enable by device.connect(debug=True) + +* Enhanced verify connectivity functionality diff --git a/docs/changelog/2020/june.rst b/docs/changelog/2020/june.rst new file mode 100644 index 00000000..002379ef --- /dev/null +++ b/docs/changelog/2020/june.rst @@ -0,0 +1,39 @@ +June 2020 +============ + +July 7th +------------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v20.6 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ +* Enhanced device connectivity verification functionality + +* Fixed bug in switch to vdc keywords + +* Removed old expect_log, use device.log.setLevel to enable/disable debug internal log + +* Updates to mock_device to handle keystrokes + +* Used %1B for Escape code instead of ESC in mock data diff --git a/docs/changelog/2020/may.rst b/docs/changelog/2020/may.rst new file mode 100644 index 00000000..3fe46e5e --- /dev/null +++ b/docs/changelog/2020/may.rst @@ -0,0 +1,32 @@ +May 2020 +============ + +May 26th +------------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v20.5 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ +* No new features + diff --git a/docs/changelog/2020/october.rst b/docs/changelog/2020/october.rst new file mode 100644 index 00000000..a72c8760 --- /dev/null +++ b/docs/changelog/2020/october.rst @@ -0,0 +1,33 @@ +October 2020 +============ + +October 27th +------------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v20.10 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +* Removed pyats dependency from adapter +* Updated UT for CICD docker container diff --git a/docs/changelog/2020/sept.rst b/docs/changelog/2020/sept.rst new file mode 100644 index 00000000..8da700cd --- /dev/null +++ b/docs/changelog/2020/sept.rst @@ -0,0 +1,35 @@ +September 2020 +============== + +September 29th +-------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon``, v20.9 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +* Fixed learn_hostname for ha standby device +* Updated dialog processor default timeout to use spawn timeout instead +* Updated general connect function to use self.connected to check connectivity +* Updated dual_rp connection when chassis type is specified, subconnection use single_rp chassis type diff --git a/docs/changelog/2021/april.rst b/docs/changelog/2021/april.rst new file mode 100644 index 00000000..b4489437 --- /dev/null +++ b/docs/changelog/2021/april.rst @@ -0,0 +1,47 @@ +April 2021 +========== + +April 27th +---------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v21.4 + ``unicon``, v21.4 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- +* adapters.topology + * Modified Unicon: + * Fixed - debug argument not being propagated in multi_rp connections + +* Dialog processor + * Modified SimpleDialogProcessor: + * log statement debugs via debug log level + * Removed STATEMENT_LOG_DEBUG settings, use connect(debug=True) instead +* NXOS service statments + * Added new statment to handle multiple call for abort provisiong + * Added new pattern to nxos reload patterns + + diff --git a/docs/changelog/2021/august.rst b/docs/changelog/2021/august.rst new file mode 100644 index 00000000..d230a9f2 --- /dev/null +++ b/docs/changelog/2021/august.rst @@ -0,0 +1,34 @@ +August 2021 +======== + +August 31st +------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v21.8 + ``unicon``, v21.8 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +N/A + + diff --git a/docs/changelog/2021/december.rst b/docs/changelog/2021/december.rst new file mode 100644 index 00000000..2b38b671 --- /dev/null +++ b/docs/changelog/2021/december.rst @@ -0,0 +1,54 @@ +December 2021 +============= + +December 14 - Unicon v21.12 +--------------------------- + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v21.12 + ``unicon``, v21.12 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* playback + * Mock + * Refactored mock to work with Aireos devices + +* bases + * Modified routers services + * Corrected service log message + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* playback + * _mock_helper + * Created helper module to handle various device commands + + diff --git a/docs/changelog/2021/february.rst b/docs/changelog/2021/february.rst new file mode 100644 index 00000000..80965e03 --- /dev/null +++ b/docs/changelog/2021/february.rst @@ -0,0 +1,22 @@ +February 2021 +============= + +february 26th +------------- + + +-------------------------------------------------------------------------------- + Features and Bug Fixes: +-------------------------------------------------------------------------------- + +* Dialogs + * Fix bug in dialog processing related to timeout statement + +* STATEMACHINE + * Fix bug in detect_state method if last match is empty + +* Hostname learning + * Passive hostname learning is enabled by default + +* Pluginmanager + * Added warning message if plugin class is replaced with another class diff --git a/docs/changelog/2021/january.rst b/docs/changelog/2021/january.rst new file mode 100644 index 00000000..a8b0d0a9 --- /dev/null +++ b/docs/changelog/2021/january.rst @@ -0,0 +1,59 @@ +January 2021 +============= + +January 27th +------------- + + +-------------------------------------------------------------------------------- + Features and Bug Fixes: +-------------------------------------------------------------------------------- + +* GENERIC PLUGIN + * 'Attach' Service Implementation. This Requires Plugins To Support The 'Module' State. + * Added 'Target_Standby_State' Keyword Argument For Rp_State Check In Reload Service + * Updated Traceroute Service To Check For Valid Keyword Arguments + * Added Configure Statement List Dialog To Configure Service + +* NXOS PLUGIN + * Added 'Attach' Service + * Added Configure_Dual Service For Nxos Plugin + * Fixed Configure Pattern To Enable Learning Hostname If The Device Is In Config State + +* LINUX PLUGIN + * Added Handler For 'Sudo' Password + +* IOS, IOSXE, IOSXR PLUGINS + * Added Configure Error Pattern To Ios, Iosxe And Iosxr + +* DOCUMENTATION + * Updated Dialog Docgen Script To Include Configure Dialogs + +* IOSXE PLUGIN + * Updated Configure Statement List To Handle Yes/No Prompt + * Added Support For Grub Menu In The Reload Service + +* APIC PLUGIN + * Refactored Reload Service To Support Ssh Based Reloads + * Added 'Shell' State + +* ASA PLUGIN + * Added Firepower 2K (Fp2K) Platform Support + +* FXOS PLUGIN + +* GENERIC + * Add Support For Hostname Change With Non-Bulk Config Commands + +* REMOVED ACI/APIC PLUGIN (USE OS APIC INSTEAD) + +* REMOVED ACI/N9K PLUGIN (USE OS NXOS, PLATFORM=ACI INSTEAD) + +* REMOVED NXOS/ACI/N9K PLUGIN (USE OS NXOS, PLATFORM=ACI INSTEAD) + +* ALL PLUGINS + * `Series` Has Been Renamed To `Platform` + +* ADDED NEW HP COMWARE PLUGINS + + diff --git a/docs/changelog/2021/july.rst b/docs/changelog/2021/july.rst new file mode 100644 index 00000000..02798bbe --- /dev/null +++ b/docs/changelog/2021/july.rst @@ -0,0 +1,57 @@ +July 2021 +======== + +July 27 +------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v21.7 + ``unicon``, v21.7 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* connection provider + * Added fix to clear the previous connection data for HA unlock_standby + * Refactored HA initialization for dual RP connections + +* mock device + * changed basicConfig from stderr to stdout from mock device to prevent stderr output + +* statemachine + * Log warning when `add_state_pattern` is used + +* prompt recovery + * Use warning on hostname mismatch instead of raising exception + +* mock device + * Handle unicode errors and log error message if they occur + +* playback + * Enhanced not to show unexpected warning based on recording + + + + diff --git a/docs/changelog/2021/june.rst b/docs/changelog/2021/june.rst new file mode 100644 index 00000000..3462090e --- /dev/null +++ b/docs/changelog/2021/june.rst @@ -0,0 +1,51 @@ +June 2021 +======== + +June 29 +------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v21.6 + ``unicon``, v21.6 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxe/sdwan + * Added configure dialog statement for commit 'Proceed' prompt + +* topology + * Fix handling of debug keyword argument + +* connection + * Modified logic in 'connected' check to improve remote disconnect detection + * Added warning log message if reconnect occurs + +* unicon.eal.dialogs + * Fixed `sendline_cred_user` and `sendline_cred_pass` implementation + +* generic + * Do not insert username for device SSH command + diff --git a/docs/changelog/2021/march.rst b/docs/changelog/2021/march.rst new file mode 100644 index 00000000..efb3be5e --- /dev/null +++ b/docs/changelog/2021/march.rst @@ -0,0 +1,49 @@ +March 2021 +========== + +March 30th +---------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v21.3 + ``unicon``, v21.3 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* statemachine + * detect_state() now passes the connection context to go_to() + +* connections + * Refactor is_connected to use connected implementation + * Fix bug with file descriptor on disconnect/close + +* device ERROR_PATTERN settings + * Add integration test for device settings from topology + +* device custom settings + * Added support for execute, configure and traceroute timeouts from custom key for backward compatibility with Genie + + diff --git a/docs/changelog/2021/may.rst b/docs/changelog/2021/may.rst new file mode 100644 index 00000000..23e9c33f --- /dev/null +++ b/docs/changelog/2021/may.rst @@ -0,0 +1,49 @@ +May 2021 +======== + +May 25 +------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v21.5 + ``unicon``, v21.5 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* dialog processor + * Modified the prompt_recovery logging message so it's more clear. + * Modified dialog processer logic to avoid duplicate match data when trim_buffer is False + +* connection + * Updated connection class 'connected' logic to detect connection closure by remote device + * Modified connect() implementation to return the complete connection log + +* sshutils + * Use netstat command to find available port for ssh tunnel diff --git a/docs/changelog/2021/october.rst b/docs/changelog/2021/october.rst new file mode 100644 index 00000000..3e0bad78 --- /dev/null +++ b/docs/changelog/2021/october.rst @@ -0,0 +1,21 @@ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* playback + * Modified mock + * Updated create_yaml pattern to separate nested configure command calls + * Fixed error handling for configure commands + * Modified mock + * Changed create_yaml behavior to store configure commands in mock_data yaml files + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* dialog + * Modified Statement + * Moved statement_action_helper outside of Statement so it can + + diff --git a/docs/changelog/2021/september.rst b/docs/changelog/2021/september.rst new file mode 100644 index 00000000..4361d8f7 --- /dev/null +++ b/docs/changelog/2021/september.rst @@ -0,0 +1,40 @@ +September 2021 +======== + +September 28 +------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v21.9 + ``unicon``, v21.9 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- +* No new features + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- +* No Changes diff --git a/docs/changelog/2022/april.rst b/docs/changelog/2022/april.rst new file mode 100644 index 00000000..a388f6c0 --- /dev/null +++ b/docs/changelog/2022/april.rst @@ -0,0 +1,52 @@ +April 2022 +========== + +April 26 - Unicon v22.4 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v22.4 + ``unicon``, v22.4 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* router.services + * Add context to pre_service state transition + +* router connection provider + * Updated hostname learning for HA connections + +* mock device + * Added ctrl-c handler while writing output + + diff --git a/docs/changelog/2022/august.rst b/docs/changelog/2022/august.rst new file mode 100644 index 00000000..1d58fcbb --- /dev/null +++ b/docs/changelog/2022/august.rst @@ -0,0 +1,41 @@ +August 2022 +========== + +August 30 - Unicon v22.8 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v22.8 + ``unicon``, v22.8 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* connection parse_spawn_command + * Fixed Although `login_creds='local'` is specified, `default` is selected + + diff --git a/docs/changelog/2022/february.rst b/docs/changelog/2022/february.rst new file mode 100644 index 00000000..b90ac788 --- /dev/null +++ b/docs/changelog/2022/february.rst @@ -0,0 +1,66 @@ +February 2022 +========== + +February 24 - Unicon v22.2 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v22.2 + ``unicon``, v22.2 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* routers/connection provider + * Updates to allow hostname learning when device is found in config mode + +* bases + * Modified BaseCommonRpConnectionProvider + * Added shared implementation of learn_tokens method to reduce duplicate code + * Modified BaseSingleRpConnectionProvider + * Remove duplicate code from learn_tokens + * Modified BaseMultiRpConnectionProvider + * Remove duplicate code from learn_tokens + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* statemachine + * add_path + * add index to identify where to add the new path in self.paths + * add_state + * add index to identify where to add the new state in self.states + + diff --git a/docs/changelog/2022/january.rst b/docs/changelog/2022/january.rst new file mode 100644 index 00000000..09a7b59f --- /dev/null +++ b/docs/changelog/2022/january.rst @@ -0,0 +1,59 @@ +January 2022 +============ + +January 25 - Unicon v22.1 +--------------------------- + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v22.1 + ``unicon``, v22.1 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* bases + * Modified proxy connection + * Added "proxy" to AssertionError message to make it more specific + * Fixed Error when closing a non existent self.spawn + * Modified proxy connection + * Fixed connection error when recording device using proxy connections + +* logs + * Modified UniconFileHandler + * Added specific handling of 'locale' encoding because of Python 3.10 changes to default encoding + +* bases/routers + * Modified BaseSingleRpConnectionProvider + * Added option to invoke device token learning if learn_tokens connection option is set + * Modified BaseMultiRpConnectionProvider + * Added option to invoke device token learning if learn_tokens connection option is set + +* connection provider + * Added support for ROMMON init commands + * Updated hostname learning for Dual RP + + diff --git a/docs/changelog/2022/july.rst b/docs/changelog/2022/july.rst new file mode 100644 index 00000000..5a0fcea1 --- /dev/null +++ b/docs/changelog/2022/july.rst @@ -0,0 +1,40 @@ +July 2022 +========== + +July 26 - Unicon v22.7 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v22.7 + ``unicon``, v22.7 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- +* Playback + * Modified _mock_helper: + * Added dictionary to support IOSXR mock data generation + diff --git a/docs/changelog/2022/june.rst b/docs/changelog/2022/june.rst new file mode 100644 index 00000000..73d9f37c --- /dev/null +++ b/docs/changelog/2022/june.rst @@ -0,0 +1,47 @@ +June 2022 +========== + +June 28 - Unicon v22.6 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v22.6 + ``unicon``, v22.6 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* routers + * Connection_provider + * update designate handle for BaseStackRpConnectionProvider to support + + diff --git a/docs/changelog/2022/march.rst b/docs/changelog/2022/march.rst new file mode 100644 index 00000000..8d6e91c0 --- /dev/null +++ b/docs/changelog/2022/march.rst @@ -0,0 +1,55 @@ +March 2022 +========== + +March 29 - Unicon v22.3 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v22.3 + ``unicon``, v22.3 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* plugin manager + * Modified plugin log message to include module + * Change plugin override warnings to debug logs + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxe + * fix the issue for the situtation when reply passed in reload service for ha devices + + diff --git a/docs/changelog/2022/may.rst b/docs/changelog/2022/may.rst new file mode 100644 index 00000000..4dd2e53f --- /dev/null +++ b/docs/changelog/2022/may.rst @@ -0,0 +1,57 @@ +May 2022 +========== + +May 31 - Unicon v22.5 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v22.5 + ``unicon``, v22.5 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* router connection provider + * Updated hostname learning for HA connections + +* mock device + * Added ctrl-c handler while writing output + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* enhancement for retry and service_dialog arguments + * Allow user to simply pass empty list to initialize with empty dialog + + diff --git a/docs/changelog/2022/november.rst b/docs/changelog/2022/november.rst new file mode 100644 index 00000000..4322868c --- /dev/null +++ b/docs/changelog/2022/november.rst @@ -0,0 +1,43 @@ +November 2022 +========== + +November 28 - Unicon v22.11 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v22.11 + ``unicon``, v22.11 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + bash$ pip install unicon.plugins + bash$ pip install unicon +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* Router + * Removed timeout_pattern in BaseServices: + The timeout pattern was causing issues as it was getting matched for device output instead actual error pattern. +* Connection + * Modified logic for pattern detection of existing username + Previous pattern detection for username would match if username was used in the ProxyJump ssh option. \ No newline at end of file diff --git a/docs/changelog/2022/october.rst b/docs/changelog/2022/october.rst new file mode 100644 index 00000000..38ca1533 --- /dev/null +++ b/docs/changelog/2022/october.rst @@ -0,0 +1,33 @@ +october 2022 +========== + +October 25 - Unicon v22.10 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v22.10 + ``unicon``, v22.10 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + bash$ pip install unicon.plugins + bash$ pip install unicon +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + +Changelog: + + - No changes \ No newline at end of file diff --git a/docs/changelog/2022/september.rst b/docs/changelog/2022/september.rst new file mode 100644 index 00000000..b42a5d23 --- /dev/null +++ b/docs/changelog/2022/september.rst @@ -0,0 +1,50 @@ +September 2022 +========== + +September 27 - Unicon v22.9 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v22.9 + ``unicon``, v22.9 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* connection base + * add option log_propagate to control whether logger for the connection propagates logs to parent + * add option no_pyats_tasklog to prevent Unicon from adding pyats tasklog handler + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* mock_device + * Fixed issue with HA mode mock device when asyncssh package is not installed + + diff --git a/docs/changelog/2023/april.rst b/docs/changelog/2023/april.rst new file mode 100644 index 00000000..e7993ad0 --- /dev/null +++ b/docs/changelog/2023/april.rst @@ -0,0 +1,38 @@ +April 2023 +========== + +April 25 - Unicon v23.4 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v23.4 + ``unicon``, v23.4 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ diff --git a/docs/changelog/2023/august.rst b/docs/changelog/2023/august.rst new file mode 100644 index 00000000..ae56c0fe --- /dev/null +++ b/docs/changelog/2023/august.rst @@ -0,0 +1,63 @@ +August 2023 +========== + +August 29 - Unicon v23.8 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v23.8 + ``unicon``, v23.8 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* unicon + * Added support for os_flavor as plugin selector attribute +* unicon.bases.linux + * Added init_connection to connection provider: + * added init_connection method for initializing the device + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxe + * stack: + * Update mock data for stack devices for standby lock. +* cheetah + * Add support for devshell in cheetah OS based wireless access points +* iosxe + * Update enable secret setup dialog logic to support devices without password or with short password +* Generic + * Added recovery for Reload and HaRelaod: + * Recover device using golden image if reload is failed with an exception diff --git a/docs/changelog/2023/february.rst b/docs/changelog/2023/february.rst new file mode 100644 index 00000000..bb07996f --- /dev/null +++ b/docs/changelog/2023/february.rst @@ -0,0 +1,38 @@ +February 2023 +========== + +February 28 - Unicon v23.2 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v23.2 + ``unicon``, v23.2 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ diff --git a/docs/changelog/2023/january.rst b/docs/changelog/2023/january.rst new file mode 100644 index 00000000..8f977f6b --- /dev/null +++ b/docs/changelog/2023/january.rst @@ -0,0 +1,62 @@ +January 2023 +========== + +January 31 - Unicon v23.1 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v23.1 + ``unicon``, v23.1 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ + + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- +* iosxe + * Added Configure Error Patterns + * "% SR feature is not configured yet, please enable Segment-routing first." + * "% {address} overlaps with {interfaces}" + * "%{interface} is linked to a VRF. Enable {protocol} on that VRF first." + * "% VRF {vrf} not configured" + * "% Incomplete command." + * "%CLNS: System ID ({system_id}) must not change when defining additional area addresses" + * "% Specify remote-as or peer-group commands first" + * "% Policy commands not allowed without an address family" + * Added switchover proceed pattern + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- +* IOSXE + * Add support for chassis keyword argument for bash console service + diff --git a/docs/changelog/2023/july.rst b/docs/changelog/2023/july.rst new file mode 100644 index 00000000..74bbe41c --- /dev/null +++ b/docs/changelog/2023/july.rst @@ -0,0 +1,63 @@ +July 2023 +========== + +July 24 - Unicon v23.7 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v23.7 + ``unicon``, v23.7 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* patterns + * Modified confirm prompt patterns to support Abort Copy + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* generic + * attach_mdoule + * add a debug flag to attach_mdoule for going to debug mode + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxe + * Change the regex for sw_num to ' \d '. + + diff --git a/docs/changelog/2023/june.rst b/docs/changelog/2023/june.rst new file mode 100644 index 00000000..65e5735f --- /dev/null +++ b/docs/changelog/2023/june.rst @@ -0,0 +1,50 @@ +June 2023 +========== + +June 27 - Unicon v23.6 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v23.6 + ``unicon``, v23.6 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* unicon.eal.backend + * Refactored backend to use telnetlib by default. All telnet connections will now use `telnetlib` implementation instead of system telnet. + * Set `.settings.BACKEND = "unicon.eal.backend.pty_backend"` to revert to the system telnet client. + +* unicon.mock + * Update mock_device_cli to work with telnetlib backend + + diff --git a/docs/changelog/2023/march.rst b/docs/changelog/2023/march.rst new file mode 100644 index 00000000..4242e00b --- /dev/null +++ b/docs/changelog/2023/march.rst @@ -0,0 +1,49 @@ +March 2023 +========== + +March 28 - Unicon v23.3 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v23.3 + ``unicon``, v23.3 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxr + * asr9k + * Modified call_service in service_implementation + * Added sleep between retry connect for Dual RP connection error + + diff --git a/docs/changelog/2023/may.rst b/docs/changelog/2023/may.rst new file mode 100644 index 00000000..7e645df2 --- /dev/null +++ b/docs/changelog/2023/may.rst @@ -0,0 +1,53 @@ +May 2023 +========== + +May 30 - Unicon v23.5 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v23.5 + ``unicon``, v23.5 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* backend + * Pass device object to Spawn class + +* unicon.bases.routers.connection_provider + * Modified logic in designate_handles to use plugin settings + +* bases + * Modified Connection + * Expanded PauseOnPhrase to be able to pause on device output + + diff --git a/docs/changelog/2023/november.rst b/docs/changelog/2023/november.rst new file mode 100644 index 00000000..c5b210bf --- /dev/null +++ b/docs/changelog/2023/november.rst @@ -0,0 +1,50 @@ +November 2023 +========== + +November 27 - Unicon v23.11 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v23.11 + ``unicon``, v23.11 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* unicon + * Modified test_init_commands + * Added SONiC as a valid OS + +* pluginmanager + * Modified get_plugin method to allow missing keys in lookup + + diff --git a/docs/changelog/2023/october.rst b/docs/changelog/2023/october.rst new file mode 100644 index 00000000..3b34aed9 --- /dev/null +++ b/docs/changelog/2023/october.rst @@ -0,0 +1,77 @@ +October 2023 +============ + +October 31 - Unicon v23.10 +-------------------------- + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v23.10 + ``unicon``, v23.10 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* unicon.eal.backend + * Added telnetlib backend. + * Set `.settings.BACKEND = "auto"` to use the new telnetlib backend. + +* unicon.mock + * Update mock_device_cli to work with telnetlib backend + +* unicon.bases.connection + * learn_hostname + * skip hostname learning if the device is in bash shell. + +* unicon.bases.routers + * Modified BaseSingleRpConnectionProvider + * Updated establish_connection method to update cred_list in context if login_creds is not None + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* unicon.adapters + * updated topology + * add fallback credentials to the context for each connection. + * Update pattern for invalid password. + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* cheetah + * ap + * Added support for device reload. + + diff --git a/docs/changelog/2023/september.rst b/docs/changelog/2023/september.rst new file mode 100644 index 00000000..f4e70155 --- /dev/null +++ b/docs/changelog/2023/september.rst @@ -0,0 +1,46 @@ +September 2023 +============== + +September 26 - Unicon v23.9 +--------------------------- + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v23.9 + ``unicon``, v23.9 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* unicon.routers.bases + * Add device alias to service log + + diff --git a/docs/changelog/2024/September.rst b/docs/changelog/2024/September.rst new file mode 100644 index 00000000..2806f924 --- /dev/null +++ b/docs/changelog/2024/September.rst @@ -0,0 +1,45 @@ +September 2024 +========== + +September 24 - Unicon v24.9 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v24.9 + ``unicon``, v24.9 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* backend.spawn0 + * Modified RawSpawn + * Added check for when a decode error occurs n amount of times + +* unicon + * topology + * Fixed logic for proxy connection. + * sshtunnel + * Added -o EnableEscapeCommandline=yes to ssh-options. + +* unicon.bases + * Added message argument to log_service_call + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* generic + * Added upwards error propagation for decode errors + + diff --git a/docs/changelog/2024/april.rst b/docs/changelog/2024/april.rst new file mode 100644 index 00000000..d00f23e6 --- /dev/null +++ b/docs/changelog/2024/april.rst @@ -0,0 +1,102 @@ +April 2024 +========== + + - Unicon v24.4 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v24.4 + ``unicon``, v24.4 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* sshutils + * add_tunnel + * add logic to handle allocating ports based on the tunnel type. + +* unicon + * Bases/Routers + * Do learn hostname if only the learn pattern is in the statmachine patterns. + * Update the connection init logic. + * Patterns + * Add Bad secrets to bad_passwords pattern. + +* unicon/bases + * Router/connection_provider + * Update logic to not learn the hostname when the device is in shell mode. + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* unicon + * Connection provider + * Add args and kwargs for connect function + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* iosxe + * statemachine + * add pki_hexmode state for iosxe + +* iosxr + * Added get_commit_cmd + * Added support for 'commit best-effort' command. + +* stackresetstandbyrp + * Added iosxe/stack StackResetStandbyRP + * iosxe/stack service reset_standby_rp + * Check whole stack readiness to decide the result of reset_standby_rp + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxr/spitfire + * Modified Prompt Recovery Commands + * Updated prompt recovery commands to user CTRL+C. + +* iosxe + * connection provider + * Get the pattern for the enable statment from state machine for handeling device prompts after + +* resetstandbyrp + * Modified generic ResetStandbyRP + * Fixed to handle the optinal argument "reply" + + diff --git a/docs/changelog/2024/august.rst b/docs/changelog/2024/august.rst new file mode 100644 index 00000000..fb03cbd1 --- /dev/null +++ b/docs/changelog/2024/august.rst @@ -0,0 +1,38 @@ +August 2024 +========== + +August 27 - Unicon v24.8 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v24.8 + ``unicon``, v24.8 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ diff --git a/docs/changelog/2024/february.rst b/docs/changelog/2024/february.rst new file mode 100644 index 00000000..cfcbdb21 --- /dev/null +++ b/docs/changelog/2024/february.rst @@ -0,0 +1,61 @@ +February 2024 +========== + +February 27 - Unicon v24.2 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v24.2 + ``unicon``, v24.2 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* connection_provider + * Updated try/except to log error message as warning + +* unicon.eal + * Add EOF handler for connection errors with telnet backend + +* sshutils + * Add a new pattern for add tunnel + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* utils + * AbstractTokenDiscovey + * Update the logic so the paltform set to sdwan if device is in controller mode. + + diff --git a/docs/changelog/2024/january.rst b/docs/changelog/2024/january.rst new file mode 100644 index 00000000..813f8be1 --- /dev/null +++ b/docs/changelog/2024/january.rst @@ -0,0 +1,68 @@ +January 2024 +========== + +30 - Unicon v24.1 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v24.1 + ``unicon``, v24.1 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* pty_backend + * Modified error handling logic to allow dialog to process statements on subprocess exit + +* utils + * Update ansi pattern to allow imports + +* statemachine + * Update hostname logic to handle hostnames with special characters + +* unicon + * Add CLI option to enable debug logs + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* service_implementation + * Modified Reload service + * Removed sendline after reload + +* iosxr + * Modified moonshine UTs + * Updated wrong import statements in standalone_ping_test.py and config_test.py UTs. + + diff --git a/docs/changelog/2024/july.rst b/docs/changelog/2024/july.rst new file mode 100644 index 00000000..2b9608b1 --- /dev/null +++ b/docs/changelog/2024/july.rst @@ -0,0 +1,46 @@ +July 2024 +========== + +July 30 - Unicon v24.7 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v24.7 + ``unicon``, v24.7 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* generic + * Updated with `sleep_time` to handle the copy command + + diff --git a/docs/changelog/2024/june.rst b/docs/changelog/2024/june.rst new file mode 100644 index 00000000..fe7ceafb --- /dev/null +++ b/docs/changelog/2024/june.rst @@ -0,0 +1,63 @@ +June 2024 +========== + + - Unicon v24.6 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v24.6 + ``unicon``, v24.6 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* stackswitchover + * Modified to wait for known switch state before continuing to check all stack members + +* stackreload + * Modified to always check all stack memebers after reload + * Modified to work for newer platforms + +* iosxe/stack + * Reload Service + * fix the logic for reloading stack devices to wait for all the members to be ready. + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* iosxe.stack.utils + * Added new method wait_for_any_state + * wait for any known state to bypass possible timing issues + + diff --git a/docs/changelog/2024/march.rst b/docs/changelog/2024/march.rst new file mode 100644 index 00000000..5b7c0c79 --- /dev/null +++ b/docs/changelog/2024/march.rst @@ -0,0 +1,76 @@ +March 2024 +========== + + - Unicon v24.3 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v24.3 + ``unicon``, v24.3 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* backend + * Option to use `UNICON_BACKEND` environment variable to select backend + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* bases + * connection + * add operating_mode to the connection object + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* discovery_tokens + * Add prompt_recovery to dialog + +* iosxe + * Connection provider + * Add support for operating mode detection on connect() + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxr + * Modified connection provider + * Updated connection provider for handeling token discovery. + + diff --git a/docs/changelog/2024/may.rst b/docs/changelog/2024/may.rst new file mode 100644 index 00000000..1986ea03 --- /dev/null +++ b/docs/changelog/2024/may.rst @@ -0,0 +1,59 @@ +May 2024 +========== + +May 28 - Unicon v24.5 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v24.5 + ``unicon``, v24.5 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* playback + * mock_helper + * Added show version | include operating mode to list of recorded commands + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxe + * cat9k + * Modified summary.py + * Added reload_confirm_iosxe to reload_to_rommon_statement_list + * Added post time + * Added POST_SWITCHOVER_WAIT before enable + + diff --git a/docs/changelog/2024/november.rst b/docs/changelog/2024/november.rst new file mode 100644 index 00000000..dc60d032 --- /dev/null +++ b/docs/changelog/2024/november.rst @@ -0,0 +1,29 @@ +November 2024 +========== + +November 26 - Unicon v24.11 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v24.11 + ``unicon``, v24.11 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* unicon/bases + * Router/connection_provider + * Updated designate_handles to not change state of standby if it is locked. + * Added quad device specific unlock_standby method to execute configs only on Active console + + diff --git a/docs/changelog/2024/october.rst b/docs/changelog/2024/october.rst new file mode 100644 index 00000000..fe00ac57 --- /dev/null +++ b/docs/changelog/2024/october.rst @@ -0,0 +1,32 @@ +October 2024 +========== + +October 29 - Unicon v24.10 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v24.10 + ``unicon``, v24.10 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* generic + * service_implementations/Configure + * update the pattern for update_hostname in the configure service. + +* iosxr/spitfire + * patterns + * remove the config and enable pattern + + diff --git a/docs/changelog/2025/april.rst b/docs/changelog/2025/april.rst new file mode 100644 index 00000000..a4b76776 --- /dev/null +++ b/docs/changelog/2025/april.rst @@ -0,0 +1,76 @@ +April 2025 +========== + +April 29 - Unicon v25.4 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.4 + ``unicon``, v25.4 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* connection + * Added os learn version + * Added ability to set learn_tokens and overwrite_testbed_tokens from a config file or environment variable + * Environment Variables + * UNICON_LEARN_TOKENS + * UNICON_OVERWRITE_TESTBED_TOKENS + * UNICON_LEARN_AND_OVERWRITE_TOKENS + * Config File + * [unicon] + * learn_tokens + * overwrite_testbed_tokens + * learn_and_overwrite_tokens + +* connection_provider + * Modified update_os_version + * Updated logic to execute 'show install summary' only on first connnection + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* linux + * Modified linux connection provider + * Wait for connection_timeout/2 on initial connection for the device to respond with some output + +* generic + * Add TRANSITION_WAIT setting to make transition wait time configurable + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* linux + * Added support for prompt_line + +* iosxe + * IosXEPatterns + * Updated the recovery-mode regex to match prompt + +* iosxe_mock_data.yaml + * Added 'show install summary' output in mock yaml + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* iosxe + * add test for learn os + + diff --git a/docs/changelog/2025/august.rst b/docs/changelog/2025/august.rst new file mode 100644 index 00000000..57792877 --- /dev/null +++ b/docs/changelog/2025/august.rst @@ -0,0 +1,49 @@ +August 2025 +========== + +August 23 - Unicon v25.8 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.8 + ``unicon``, v25.8 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* unicon/bases + * router + * Added a statement to logout dialog to handle the standby console locked + * Added check in designate_handle to skip goto enable when standby is locked + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* cheetah + * Added cheetah ap tokens in pids csv file + +* iosxe/cat9k/9610 + * Added the support for stackwise virtual for c9610 devices + * Added SVLStackReload and SVLStackSwitchover services + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* generic/patterns + * Modified syslog_message_pattern to handle additional syslog message formats. + + diff --git a/docs/changelog/2025/december.rst b/docs/changelog/2025/december.rst new file mode 100644 index 00000000..d2b29fab --- /dev/null +++ b/docs/changelog/2025/december.rst @@ -0,0 +1,36 @@ +December 2025 +========== + +December 30 - Unicon v25.11 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.11 + ``unicon``, v25.11 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* unicon + * adapters/topology.py + * Added support for reverse SSH connections. + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* patterns + * Updated the regex for username prompt for the linux VMs + + diff --git a/docs/changelog/2025/february.rst b/docs/changelog/2025/february.rst new file mode 100644 index 00000000..f91bcd29 --- /dev/null +++ b/docs/changelog/2025/february.rst @@ -0,0 +1,45 @@ +February 2025 +========== + +February 25 - Unicon v25.2 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.2 + ``unicon``, v25.2 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* router.connection_provider + * Modified disconnect + * Added sendline('exit') on disconnect + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* generic + * updated exception for Recover device using golden image if reload is failed. + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* iosxe + * Added to Configure Error Patterns + * Added the regex to match error pattern "127.0 / 255.0 is an invalid network." + + diff --git a/docs/changelog/2025/january.rst b/docs/changelog/2025/january.rst new file mode 100644 index 00000000..c9c0b793 --- /dev/null +++ b/docs/changelog/2025/january.rst @@ -0,0 +1,59 @@ +January 2025 +========== + + - Unicon v25.1 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.1 + ``unicon``, v25.1 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* backend + * New match mode support for last line ignoring whitespace + +* learn_tokens + * Update learn_os_prompt to account for config mode + +* unicon + * Fix the dialog processor to trigger actions only when statements match patterns(HA/Stack) + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxr + * Added SwitchoverDisallowedError exception to raise when redundancy switchover is disallowed on the device. + +* unicon.plugins + * Added Reload + * Added support to pick max value of RELOAD_RECONNECT_WAIT or POST_RELOAD_WAIT + * Base Execute + * pass backend decode error + * generic + * Updated regex patterns to prevent matching of test case names that contain the words "failure" or "fail_". This change ensures that test cases with failure-related names no longer trigger errors during processing. + +* iosxe/pattern + * Allow 'DDNS' to config prompt patterns + +* generic + * Added 'copy_overwrite_handler' in the service_statements.py to handle + +* iosxe + * Added below config error patterns + * % VLAN [] already in use + * Added below config error patterns + * % VNI is either already in use or exceeds the maximum allowable VNIs. \ No newline at end of file diff --git a/docs/changelog/2025/july.rst b/docs/changelog/2025/july.rst new file mode 100644 index 00000000..4f7f956c --- /dev/null +++ b/docs/changelog/2025/july.rst @@ -0,0 +1,42 @@ +July 2025 +========== + +July 29 - Unicon v25.7 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.7 + ``unicon``, v25.7 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* bases/router + * connection_provider + * Updated logout service logic for single rp and multi rp connections + +* router/connection + * Initialized the UNICON_BACKEND_DECODE_ERROR_LIMIT to None for iosxr HA device connections + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* mock device + * Add mock device for svl stack + +* iosxe + * Added cert-trustpool config pattern + + diff --git a/docs/changelog/2025/june.rst b/docs/changelog/2025/june.rst new file mode 100644 index 00000000..6d1e3920 --- /dev/null +++ b/docs/changelog/2025/june.rst @@ -0,0 +1,44 @@ +June 2025 +========== + +June 29 - Unicon v25.6 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.6 + ``unicon``, v25.6 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* bases/connection + * Added logout() method + * Added logout implementation for routers + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* unicon + * Refactor plugin loading from pkg_resources.iter_entry_points to importlib.metadata.entry_points + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* iosxe + * Added support for fast reload pattern + + diff --git a/docs/changelog/2025/march.rst b/docs/changelog/2025/march.rst new file mode 100644 index 00000000..9eb04471 --- /dev/null +++ b/docs/changelog/2025/march.rst @@ -0,0 +1,51 @@ +March 2025 +========== + +March 25 - Unicon v25.3 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.3 + ``unicon``, v25.3 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* unicon.robot + * Modified Robot Library UniconRobot.py + * Address SyntaxWarning in UniconRobot.py showing up in python >= 3.12 + +* mock_device + * Updated mock device to handle ctrl-c for HA tests + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* connection_provider + * Modified execute_init_commands + * Updated logic to config init commands only on first connnection + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* generic + * Added fallback credentials to login_handler statement + +* iosxe + * Added grub statement in the list 'boot_from_rommon_statement_list' for + + diff --git a/docs/changelog/2025/may.rst b/docs/changelog/2025/may.rst new file mode 100644 index 00000000..a7e23904 --- /dev/null +++ b/docs/changelog/2025/may.rst @@ -0,0 +1,41 @@ +May 2025 +========== + + - Unicon v25.5 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.5 + ``unicon``, v25.5 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Add +-------------------------------------------------------------------------------- + +* connection provider + * moved the logic of boot_device to a separate function before designating handles + * added the init_active to handle the learn_hostname instead of having it in designate handles + * Store "current_credentials" under device.credentials when credentials are used + +* connection + * Added logging per subconnection for DualRp, Stack and Quad connection + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* generic + * service implementation + * Update the state for debug mode in attach service. + + diff --git a/docs/changelog/2025/october.rst b/docs/changelog/2025/october.rst new file mode 100644 index 00000000..ca84a9a8 --- /dev/null +++ b/docs/changelog/2025/october.rst @@ -0,0 +1,62 @@ +October 2025 +========== + +October 28 - Unicon v25.10 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.10 + ``unicon``, v25.10 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* unicon + * Modified TestUniconSettings + * Fixed regex pattern to correctly match Invalid input error message in exec command. + +* plugins/linux + * Updated unit tests to accommodate the removal of the default 'uptime' command from LINUX_INIT_EXEC_COMMANDS. + +* adapters + * Updated the log collection to check for runtime directory before moving + +* modified basemultirpconnectionprovider + * Updated token discovery to handle standby locked devices + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* unicon/bases/linux/connection + * Added peripheral support for Linux OS devices + * Updated BaseLinuxConnection to pass device to Spawn initialization, enabling clearing of busy console lines for Linux-based platforms + + +-------------------------------------------------------------------------------- + Add +-------------------------------------------------------------------------------- + +* basemultirpconnection + * Added swap_roles in Multi RP connection which is parent class to have it handled for other connections. + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxe/cat9k/stackwise_virtual + * Enhanced the designate handles for condition where a standby & b active + + diff --git a/docs/changelog/2025/september.rst b/docs/changelog/2025/september.rst new file mode 100644 index 00000000..73f1e031 --- /dev/null +++ b/docs/changelog/2025/september.rst @@ -0,0 +1,39 @@ +September 2025 +========== + +September 30 - Unicon v25.9 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.9 + ``unicon``, v25.9 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* backend/spawn + * Modified backend spawn implementation, updated logic to optimize buffer matching + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* generic/service_statements + * Fixed the copy_overwrite_handler prompt + +* iosxe/stack + * Added pattern for install image to match + * Fixed the stack reload for the return_output true condition + + diff --git a/docs/changelog/2026/february.rst b/docs/changelog/2026/february.rst new file mode 100644 index 00000000..3290beff --- /dev/null +++ b/docs/changelog/2026/february.rst @@ -0,0 +1,62 @@ +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/c8kv + * imported connection provider from internal plugin to fix system mode insecure command issue. + +* 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)~ + + diff --git a/docs/changelog/2026/january.rst b/docs/changelog/2026/january.rst new file mode 100644 index 00000000..87cc9876 --- /dev/null +++ b/docs/changelog/2026/january.rst @@ -0,0 +1,85 @@ +January 2026 +========== + +January 27 - Unicon v26.1 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v26.1 + ``unicon``, v26.1 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* unicon/patterns + * Update connection refused pattern to include "Requested line is busy!" + +* routers/connection_providers + * connect + * Unwrap connection kwargs and assign to device object for the arguments to be used by underlying connection providers. + +* bases/router/connection_provider + * Use enable service to transition to enable mode + + +-------------------------------------------------------------------------------- + Add +-------------------------------------------------------------------------------- + +* nxos/n9kv + * Added AttachModuleConsoleN9k service to attach to module console of N9K devices. + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* iosxe/c8kv/statemachine + * Added IosXEC8kvSingleRpStateMachine and IosXEC8kvDualRpStateMachine + * Added new state machine for C8KV devices to support boot statement + +* iosxe/cat9k/c9350/stack + * Added the support for stack for c9350 devices + * Added C9350StackReload service + + +-------------------------------------------------------------------------------- + Recovery. +-------------------------------------------------------------------------------- + +* iosxe/c8kv/statements + * Added boot_image statement for C8KV devices + * Modified the statement to support C8KV grub> mode by adding send(cmd) + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* pid_tokens + * Updated proper platform/model for IR1101 devices. + +* generic/service_pattern + * Modified ping validate pattern to match the "Validate reply data? [no]" prompt correctly in generic patterns. + +* generic/service_implementation + * enable + * Updated UniconAuthenticationError and CredentialsExhaustedError as exceptions as they were wrapped inside the subcommand failure for a failing UT. + +* iosxe/patterns + * Updated enable_prompt regex patterns to include 'eWLC' and allow alphanumeric characters in the device identifier section. + +* generic/statemachine + * Fixed config transition retry handling to avoid resending configure terminal when configuration mode is already entered. + + diff --git a/docs/changelog/index.rst b/docs/changelog/index.rst index cb6c150a..c0c7357e 100644 --- a/docs/changelog/index.rst +++ b/docs/changelog/index.rst @@ -4,5 +4,107 @@ Changelog .. toctree:: :maxdepth: 2 + 2026/february + 2026/january + 2025/december + 2025/october + 2025/september + 2025/august + 2025/july + 2025/june + 2025/may + 2025/april + 2025/march + 2025/february + 2025/january + 2024/november + 2024/october + 2024/September + 2024/august + 2024/july + 2024/june + 2024/may + 2024/april + 2024/march + 2024/february + 2024/january + 2023/november + 2023/october + 2023/september + 2023/august + 2023/july + 2023/june + 2023/may + 2023/april + 2023/march + 2023/february + 2023/january + 2022/november + 2022/october + 2022/september + 2022/august + 2022/july + 2022/june + 2022/may + 2022/april + 2022/march + 2022/february + 2022/january + 2021/december + 2021/october + 2021/september + 2021/august + 2021/july + 2021/june + 2021/may + 2021/april + 2021/march + 2021/february + 2021/january + 2020/december + 2020/october + 2020/sept + 2020/august + 2020/july + 2020/june + 2020/may + 2020/april + 2020/feb + 2020/jan 2019/dec 2019/nov + 2019/oct + 2019/sept + 2019/aug + 2019/jul + 2019/jun + 2019/may + 2019/april + 2019/march + 2019/jan + 2018/nov + 2018/oct + 2018/sept + 2018/jul + 2018/jun + 2018/may + 2018/apr + 2018/march + 2018/february + 2018/january + 2017/december + 2017/november + 2017/october + 2017/september + 2017/august + 2017/july + 2017/june + 2017/may + 2017/feb + 2017/jan + 2016/december + 2016/november + 2016/october + 2016/september + 2016/may + 2016/february diff --git a/docs/changelog/undistributed.rst b/docs/changelog/undistributed.rst deleted file mode 100644 index b6abdd5d..00000000 --- a/docs/changelog/undistributed.rst +++ /dev/null @@ -1,2 +0,0 @@ -Features and Bug Fixes: -^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/changelog/undistributed/template.rst b/docs/changelog/undistributed/template.rst new file mode 100644 index 00000000..7cdb37f3 --- /dev/null +++ b/docs/changelog/undistributed/template.rst @@ -0,0 +1,30 @@ +Only one changelog file per pull request. Combine these two templates where applicable. + +Templates +========= + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- +* + * : + * + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- +* + * : + * + +Examples +======== + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- +* Module + * Modified Class: + * Changed variable. + * Updated some value to some value + diff --git a/docs/changelog_plugins/2019/dec.rst b/docs/changelog_plugins/2019/dec.rst new file mode 100644 index 00000000..b70701f0 --- /dev/null +++ b/docs/changelog_plugins/2019/dec.rst @@ -0,0 +1,77 @@ +December 2019 +============= + +December 19th +------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v19.12.1 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- Introduction of ios/pagent plugin + + +December 17th +------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v19.12 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- generic plugin + + - add "Invalid host" error pattern for ping service + + - enhance copy service to handle wildcard copy + +- asa plugin + + - asa plugin is now using hostname in prompt patterns + +- aireos plugin + + - handle 'Press Enter to continue' prompt following certain command + + - enhance command error pattern which has % character before Error diff --git a/docs/changelog_plugins/2019/nov.rst b/docs/changelog_plugins/2019/nov.rst new file mode 100644 index 00000000..7fc3776d --- /dev/null +++ b/docs/changelog_plugins/2019/nov.rst @@ -0,0 +1,95 @@ +November 2019 +============= + +November 27th +------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v19.11.1 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- aireos plugin + + - remove f-strings that is not supported on python 3.4 and 3.5 + + +November 26th +------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v19.11 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- generic plugin + + - add prompt matched_retries for execute service to avoid transient match on output + + - add resolve_as_number option for traceroute service + +- nxos plugin + + - add corresponding error patterns for configure service + +- linux plugin + + - enhance linux plugin to set TERM vt100 and LC_ALL C by default + +- iosxe plugin + + - enhance iosxe/cat3k to find boot image from rommon + + - add vrf argument for iosxe traceroute service + +- sdwan plugin + + - add plugins sdwan/viptela and sdwan/iosxe + +- aireos plugin + + - enhance to support known states + + - enhance to support for hostname learning + + - now execute service raises SubCommandFailure if error is detected in CLI output. diff --git a/docs/changelog_plugins/2020/april.rst b/docs/changelog_plugins/2020/april.rst new file mode 100644 index 00000000..4a7157ba --- /dev/null +++ b/docs/changelog_plugins/2020/april.rst @@ -0,0 +1,64 @@ +April 2020 +============= + +April 28th +------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v20.4 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +* Enhanced aci plugin implementation to have it available under nxos plugins + +* Update prompt for latest OpenSSH. + +* Enhance IOSXR enable pattern to accomodate different preceding card/slot. + +* Adding `copy` service to the HA IOSXE plugin implementation. + +* Supporting `reset_standby_rp` on IOSXE. + +* Updating XR spitfire plugin run prompts pattern. + +* Updating XR spitfire plugin run prompts pattern. + +* Updating mdcli and classiccli prompts pattern. + +* Fixed aci plugins unittests and added new ones for the new plugins structure. + +* Updating XR spitfire plugin run prompts pattern. + +* Add 'Incorrect input' and 'HELP' error pattern for Aireos plugin + +* Add nxos plugin configure error pattern for 'ERROR' and 'Invalid number' + +* Fixing unittest after recent user contribution on standby behavior + +* AireOS plugin updates: + * HA support for WLC + * Access Point (ap) as subplugin + +* Added SSH passphrase handler to generic plugin + +* Added Windows plugin diff --git a/docs/changelog_plugins/2020/august.rst b/docs/changelog_plugins/2020/august.rst new file mode 100644 index 00000000..3528c36c --- /dev/null +++ b/docs/changelog_plugins/2020/august.rst @@ -0,0 +1,41 @@ +August 2020 +------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v20.8 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +* Updated terminal size settings for NXOS/ACI/N9K and linux plugins. + +* [APIC] Added 'Error' to the list of error_patterns + +* [ASA] Added statement to handle for 'Proceed with reload?' + +* [IOSXE] Changed IOSXE plugin shell_prompt (non-greedy match on wildcard) +* [IOSXE] Added stack and quad plugins to support devices with stack/quad chassis type + +* [IOSXR] Updated IOSXR/ncs5k STANDBY_STATE_REGEX in the setttings +* [IOSXR] Added asr9k/ncs5k ha reload service + +* [Generic] Added learn_os feature for generic plugins redirect to corresponding plugin connection diff --git a/docs/changelog_plugins/2020/december.rst b/docs/changelog_plugins/2020/december.rst new file mode 100644 index 00000000..56f01b22 --- /dev/null +++ b/docs/changelog_plugins/2020/december.rst @@ -0,0 +1,45 @@ +December 2020 +------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v20.12 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +* IOSXE plugin + - Updated regex for config prompt + - Fixed patterns and added ca_profile for its config to be matched +* IOSXR plugin + * NCS5K plugin + - Fixed HA Reload to use correct credentials +* NXOS ACI Plugin + * Added configure service + * Removed deprecation message from nxos->aci->n9k + * Fixed a bug where the buffer might not be empty after connecting to the device +* ASA Plugin + - Add error_pattern to capture '*** WARNING ***' +* FXOS/FTD Plugin + - Added support for "* " in chassis prompt, e.g. "FirePower* #" +* Linux + * Added passphrase pattern in connection dialogs + * Made it possible to override the shell prompt from the connection settings diff --git a/docs/changelog_plugins/2020/feb.rst b/docs/changelog_plugins/2020/feb.rst new file mode 100644 index 00000000..81610956 --- /dev/null +++ b/docs/changelog_plugins/2020/feb.rst @@ -0,0 +1,110 @@ +February 2020 +============= + +February 25 +------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v20.2 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- Added python 3.8 support. + +- ise plugin + + - Updated prompt pattern to expect prompts ending with ``>``. + +- aireos plugin + + - support for Cisco Capwap Simulator as default hostname + +- iosxr/spitfire plugin + + - Fixed a bug that was preventing switch between BMC and x86 modes. + +- sdwan plugin + + - deprecated sdwan/iosxe plugin + - added os: viptela support + +February 18th +------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v20.1.2 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- Now reacting properly to ``Password OK`` prompt. + +February 10th +------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v20.1.1 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- Support devices that could have multiple enable passwords. + Allow enable_password to be specified as part of a credential. diff --git a/docs/changelog_plugins/2020/jan.rst b/docs/changelog_plugins/2020/jan.rst new file mode 100644 index 00000000..7c5bdd8c --- /dev/null +++ b/docs/changelog_plugins/2020/jan.rst @@ -0,0 +1,54 @@ +January 2020 +============= + +January 28th +------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v20.1 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +- Introduction of sros plugin for Nokia SR devices. + +- Added switchto service to iosxr/spitfire plugin. + +- aireos plugin: + + - Handle 'Would you like to save them now?' prompt. + +- nxos and fxos/ftd plugins: + + - Fix a bug where credentials were not properly converted to plaintext. + +- iosxe plugin + + - Now copy service passes in vrf via the command line instead of + expecting to be prompted for vrf. + + - iosxe configure service now responds to confirm/want to continue prompts. + +- generic and iosxe/cat3k plugins + + - Fixed reload service timeout issue, now waiting longer when + connecting after reload. diff --git a/docs/changelog_plugins/2020/july.rst b/docs/changelog_plugins/2020/july.rst new file mode 100644 index 00000000..432fe875 --- /dev/null +++ b/docs/changelog_plugins/2020/july.rst @@ -0,0 +1,42 @@ +July 2020 +------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v20.7 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +* Fixed unittest corresponding to check connectivity enhancement + +* Reverted back commit_retry until getting confirmation from the user + +* [LINUX] Updated truncate_trailing_prompt to accept regex without regex groups + +* [IOSXE] Fixed IOSXE state machine enable to disable dialog issue + +* [IOSXR] Added configure_exclusive service to IOSXR plugin + +* [NXOS] Enhanced NXOS reload service added reconnect_sleep argument +* [NXOS] Fixed incorrect login when password prompt occur before the username prompt + +* [APIC] Enhanced apic configure prompt pattern to support various configure prompt diff --git a/docs/changelog_plugins/2020/june.rst b/docs/changelog_plugins/2020/june.rst new file mode 100644 index 00000000..3ac68102 --- /dev/null +++ b/docs/changelog_plugins/2020/june.rst @@ -0,0 +1,53 @@ +June 2020 +------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v20.6 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +* Fixed ambiguous python shebang in mock devices +* Updated generic configure pattern to include ca-trustpoint + +* [IOSXE] Added recovery-mode support +* [IOSXE] Updated shell pattern + +* [IOSXR] Added commit retry timer that can be controlled under settings in the testbed yaml file +* [IOSXR] Updated admin_config prompt +* [IOSXR] Fixed enable pattern + +* [AIREOS] Added Invalid error_patterns +* [AIREOS] Enhanced reload pattern +* [AIREOS] Fixed HA execute service to use service dialogs +* [AIREOS/IOS] Added logging console disable to INIT_EXEC_COMMANDS + +* [JUNOS] Enhanced plugin to fail on commit failures +* [JUNOS] Updated CONFIGURE_ERROR_PATTERN in setting + +* [STAROS] Updated error patterns and exec init commands + +* [LINUX] Updated single hash prompt pattern + +* [NXOS] Fixed switchover timeout hard code issue + +* [CIMC] Update CIMC prompt pattern diff --git a/docs/changelog_plugins/2020/may.rst b/docs/changelog_plugins/2020/may.rst new file mode 100644 index 00000000..8ab751fc --- /dev/null +++ b/docs/changelog_plugins/2020/may.rst @@ -0,0 +1,41 @@ +May 2020 +------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v20.5 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +* Updated reset_standby logic + +* Added IOSXE cat9k plugin unit test + +* Updated shell pattern on IOSXE and added the corresponding unit test + +* Fixed bash_console access in case of spitfire plugin + +* Added dialog to the enable->disable statemachine transition under IOSXE + +* Fixing enable pattern in IOSXR plugin + +* Additional NXOS error patterns diff --git a/docs/changelog_plugins/2020/october.rst b/docs/changelog_plugins/2020/october.rst new file mode 100644 index 00000000..3b498ddc --- /dev/null +++ b/docs/changelog_plugins/2020/october.rst @@ -0,0 +1,39 @@ +October 2020 +------------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v20.10 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +* [Generic] Fix switchover service issue while trying to bring standby rp to enable mode + +* [IOSXE] Enhance stack switchover service to handle username/password prompt +* [IOSXE] Enhancing IOSXE configure service for supporting wireless controllers different prompts + +* [NXOS] Added plugin specific configure service allowing commit functionality + +* [Linux] Added regex pattern for handling ESXi server prompt + +* [JUNOS] Changed self.commit_cmd from 'commit' to 'commit synchronize' +* [JUNOS] Added regex pattern to self.CONFIGURE_ERROR_PATTERN diff --git a/docs/changelog_plugins/2020/sept.rst b/docs/changelog_plugins/2020/sept.rst new file mode 100644 index 00000000..c3cf9c49 --- /dev/null +++ b/docs/changelog_plugins/2020/sept.rst @@ -0,0 +1,45 @@ +September 2020 +-------------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v20.9 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +* [IOSXE] Added Traceroute service for Ha connection +* [IOSXE] Enhanced stack reload and switchover service +* [IOSXE] Enhanced disable_prompt and enable_prompt regex pattern + +* [Junos] Updated regex to check more commit failures +* [Junos] Fix junos configure service duplicated commit + +* [FXOS/FTD] Add 'Are you sure' statement - sendline(y) +* [FXOS] Add 'Invalid Command' and 'Ambiguous command' error patterns + +* [Aireos] Add 'Warning' error_pattern +* [Aireos] Add error pattern checking during reload service + +* [ASA] Add execute statement dialogs to execute service +* [ASA] Added reload_statements to reload service + +* [NXOS] Update HA_INIT_CONFIG_COMMANDS to add 'line vty' and 'exec-timeout 0' diff --git a/docs/changelog_plugins/2021/april.rst b/docs/changelog_plugins/2021/april.rst new file mode 100644 index 00000000..1a8c817a --- /dev/null +++ b/docs/changelog_plugins/2021/april.rst @@ -0,0 +1,102 @@ +April 2021 +========== + +April 27th +---------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v21.4 + ``unicon``, v21.4 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* AIREOS PLUGIN + * Add error_pattern for `^[Rr]Equest [Ff]Ailed And R'^(.*?) Already In Use` + * Add error_pattern For `Wlan Identifier Is Invalid` and `^Request Failed` + +* NXOS/ACI + * Inherit services from nxos plugin + +* GENERIC PLUGIN + * Add syslog message handler to `connect`, `execute` and `configure` services + +* IOSXE/CAT9K + * Support `rommon()` and `reload()` services + +* IOSXE + * New exec error_pattern to match '% Bad IP address or host name% Unknown command or computer name, or unable to find computer address' + * New configure error_pattern to match '% IP routing table does not exist' + +* GENERIC EXECUTE AND CONFIGURE SERVICES + * Added `append_error_pattern` argument + +* NXOS + * Added `skip_poap` statement for reload service + * Add execute statement list for `execute` service + * Add add error_pattern for "command failed...aborting" + +* NXOS PLUGIN + * Add dialog to handle commit confirm message + * Use 'commit' as default commit command for `configure_dual` service + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* NXOS/ACI + * attach_console service for NXOS/ACI plugin + +* IOSXR + * Updated `run_prompt` pattern to accept more variety + +* IOSXR/SPITFIRE + * Fixed failed config handling when transitioning from config to enable state + +* IOSXR/MOONSHINE + * Updated shell prompt pattern + +* AIREOS PLUGIN + * Changed error_pattern `^(%\S*)?Error` To `^(%\S*)?(Error|error)` so it's case insensitive + + +* JUNOS PLUGIN + * Update `configure` service to allow `commit_cmd` override + +* IOSXE + * Updated config prompt pattern to include "cloud" + +* IOSXE/CSR1000V + * Use IOSXE config prompt pattern + +* GENERAL + * Use plugin specific config prompt for config state transition + * Enable 'service prompt config' if we detect no prompt on config transition + +* SETUP.PY + * Update version check to allow users to build local versions + + diff --git a/docs/changelog_plugins/2021/august.rst b/docs/changelog_plugins/2021/august.rst new file mode 100644 index 00000000..b4780790 --- /dev/null +++ b/docs/changelog_plugins/2021/august.rst @@ -0,0 +1,44 @@ +August 2021 +======== + +August 31st +------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v21.8 + ``unicon``, v21.8 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* generic + * Refactored ping service + * Automatically set extd_ping to 'y' if extended option is specified + * Handle invalid input errors + * Add address to ping command if no other options are given + * Deprecated arguments `int` and `src_addr` for ``interface`` and ``source`` + * Modified reload service, added `raise_on_error` option + + diff --git a/docs/changelog_plugins/2021/december.rst b/docs/changelog_plugins/2021/december.rst new file mode 100644 index 00000000..781d6573 --- /dev/null +++ b/docs/changelog_plugins/2021/december.rst @@ -0,0 +1,61 @@ +December 2021 +============= + +December 14 - Unicon.Plugins v21.12 +----------------------------------- + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v21.12 + ``unicon``, v21.12 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* iosxe + * Added tclsh support + * Added cat8k plugin + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxr/spitfire + * Update the default init exec commands to use full terminal command + +* generic + * Added permission denied statement using a unicon core pattern + * Modified service implementation + * Corrected service log message + +* nxos + * Fix reload to use reconnect_sleep argument as buffer settle wait time + +* iosxe + * Modified service implementation + * Corrected service log message + + diff --git a/docs/changelog_plugins/2021/february.rst b/docs/changelog_plugins/2021/february.rst new file mode 100644 index 00000000..9483594d --- /dev/null +++ b/docs/changelog_plugins/2021/february.rst @@ -0,0 +1,78 @@ +February 2021 +============ + +February 23rd +------------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v21.2 + ``unicon``, v21.2 + + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* Generic plugin + * Add syslog message handler to connect, execute and configure services + +* IOSXE/CAT9K + * Support `rommon()` and `reload()` services + +* Generic execute and configure services + * Added `append_error_pattern` argument + +* Aireos plugin + * Add ERROR_PATTERN for ^[Rr]equest [Ff]ailed and r'^(.*?) already in use' + * Add ERROR_PATTERN for r'WLAN Identifier is invalid' and r'^Request failed' + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* setup.py + * Update version check to allow users to build local versions + +* NXOS plugin + * Add dialog to handle commit confirm message + * Use 'commit' as default commit command for configure_dual service + +* NXOS/ACI + * Inherit services from NXOS plugin + * attach_console service for nxos/aci plugin + +* IOSXR/Moonshine + * Updated shell prompt pattern + +* Junos plugin + * Update configure service, allow commit_cmd override + +* IOSXE + * Updated config prompt pattern to include "cloud" + +* IOSXE/CSR1000V + * Use IOSXE config prompt pattern + +* Aireos plugin + * Changed ERROR_PATTERN '^(%\s*)?Error' to '^(%\s*)?(Error|ERROR)' so it is case insensitive \ No newline at end of file diff --git a/docs/changelog_plugins/2021/january.rst b/docs/changelog_plugins/2021/january.rst new file mode 100644 index 00000000..52c51926 --- /dev/null +++ b/docs/changelog_plugins/2021/january.rst @@ -0,0 +1,83 @@ +January 2021 +============ + +January 27th +------------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v21.1 + ``unicon``, v21.1 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* GENERIC PLUGIN + * 'Attach' Service Implementation. This Requires Plugins To Support The 'Module' State. + * Added 'Target_Standby_State' Keyword Argument For Rp_State Check In Reload Service + * Updated Traceroute Service To Check For Valid Keyword Arguments + * Added Configure Statement List Dialog To Configure Service + +* NXOS PLUGIN + * Added 'Attach' Service + * Added Configure_Dual Service For Nxos Plugin + * Fixed Configure Pattern To Enable Learning Hostname If The Device Is In Config State + +* LINUX PLUGIN + * Added Handler For 'Sudo' Password + +* IOS, IOSXE, IOSXR PLUGINS + * Added Configure Error Pattern To Ios, Iosxe And Iosxr + +* DOCUMENTATION + * Updated Dialog Docgen Script To Include Configure Dialogs + +* IOSXE PLUGIN + * Updated Configure Statement List To Handle Yes/No Prompt + * Added Support For Grub Menu In The Reload Service + +* APIC PLUGIN + * Refactored Reload Service To Support Ssh Based Reloads + * Added 'Shell' State + +* ASA PLUGIN + * Added Firepower 2K (Fp2K) Platform Support + +* FXOS PLUGIN + +* GENERIC + * Add Support For Hostname Change With Non-Bulk Config Commands + +* REMOVED ACI/APIC PLUGIN (USE OS APIC INSTEAD) + +* REMOVED ACI/N9K PLUGIN (USE OS NXOS, PLATFORM=ACI INSTEAD) + +* REMOVED NXOS/ACI/N9K PLUGIN (USE OS NXOS, PLATFORM=ACI INSTEAD) + +* ALL PLUGINS + * `Series` Has Been Renamed To `Platform` + +* ADDED NEW HP COMWARE PLUGINS + + diff --git a/docs/changelog_plugins/2021/july.rst b/docs/changelog_plugins/2021/july.rst new file mode 100644 index 00000000..a859650b --- /dev/null +++ b/docs/changelog_plugins/2021/july.rst @@ -0,0 +1,102 @@ +July 2021 +======== + +July 27th +------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v21.7 + ``unicon``, v21.7 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxe + * removed basicConfig from mock device to prevent stderr output + * Updated statemachine standby locked state detection + +* aireos + * removed basicConfig from mock device to prevent stderr output + +* asa + * removed basicConfig from mock device to prevent stderr output + +* confd + * removed basicConfig from mock device to prevent stderr output + +* dell/dellos6 + * removed basicConfig from mock device to prevent stderr output + +* eos + * removed basicConfig from mock device to prevent stderr output + +* fxos + * removed basicConfig from mock device to prevent stderr output + +* gaia + * removed basicConfig from mock device to prevent stderr output + +* hpcomware + * removed basicConfig from mock device to prevent stderr output + +* ios + * removed basicConfig from mock device to prevent stderr output + +* iosxr + * removed basicConfig from mock device to prevent stderr output + +* ironware + * removed basicConfig from mock device to prevent stderr output + +* junos + * removed basicConfig from mock device to prevent stderr output + +* nxos + * removed basicConfig from mock device to prevent stderr output + +* vos + * removed basicConfig from mock device to prevent stderr output + +* generic + * Removed disconnect/connect from HA reload + * Fixed state transition on ping failure + +* generic + * Updated Reload in service_implementation.py + +* general + * Updated ``guestshell`` service for use with IOSXE and NXOS + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* nxos/mds + * Add support for Target Initiator Emulator (TIE) + + + + diff --git a/docs/changelog_plugins/2021/june.rst b/docs/changelog_plugins/2021/june.rst new file mode 100644 index 00000000..15dcdaf5 --- /dev/null +++ b/docs/changelog_plugins/2021/june.rst @@ -0,0 +1,56 @@ +June 2021 +======== + +June 29 +------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v21.6 + ``unicon``, v21.6 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* generic + * Updated switchover service, removed configure retry logic + +* iosxe + * Updated switchover service, renamed dialog argument to reply + +* nxos + * Remove VDC switchback from disconnect. No longer needed thanks to VDC detection. + * Handle more prompt for 'show vdc' on connect + +* generic + * Mock device updates for device SSH command + +* generic plugin + * Refactor reload service + * return complete console output if return_output=True + * executes init commands after reload + * reconnect if disconnected + * wait at least POST_RELOAD_WAIT seconds for terminal to settle + + diff --git a/docs/changelog_plugins/2021/march.rst b/docs/changelog_plugins/2021/march.rst new file mode 100644 index 00000000..53e4b01a --- /dev/null +++ b/docs/changelog_plugins/2021/march.rst @@ -0,0 +1,87 @@ +March 2021 +========== + +March 30th +---------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v21.3 + ``unicon``, v21.3 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* IOSXE/pattern + * Allow 'WLC' to default prompt patterns + +* Comware + * Changed from `hp_comware` to `comware` + +* IOSXE/CAT9K + * image_to_boot argument support for reload service + +* Generic + * Add default error patterns to ERROR_PATTERN setting + * Add default error patterns to CONFIGURE_ERROR_PATTERN setting + +* IOSXE + * Add bell char to enable prompt pattern + +* Generic configure service + * Fix config lock retry implementation + * Allow exit, end, commit, abort commands to exit config state + +* IOSXE/stack + * Refactor switchover service + +* NXOS + * Update configure error patterns + +* IOSXE/STACK + * fix bash_console dialog + +* statemachine + * detect_state() now passes the connection context to go_to() + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* NXOS + * Add 'mode' to configure() service as argument. + * configure_dual service is now deprecated. + * Fixed `switchto` and `switchback` service and added UTs + +* FXOS/FP4K + * New plugin for Firepower 4000 series + +* FXOS/FP9K + * New plugin for Firepower 9000 series + +* ASA + * New ASA plugin error pattern added to catch "Removing object-group (TEST_NETWORK) failed; it does not exist" + + diff --git a/docs/changelog_plugins/2021/may.rst b/docs/changelog_plugins/2021/may.rst new file mode 100644 index 00000000..592bbcfc --- /dev/null +++ b/docs/changelog_plugins/2021/may.rst @@ -0,0 +1,83 @@ +May 2021 +======== + +May 25th +-------- + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v21.5 + ``unicon``, v21.5 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxr/spitfire + * Updated module prompt pattern + +* documentation + * Fix prompt pattern examples + * Update docs for SSH passphrase credential + +* unittests + * Update unittests to reflect changes in connect() return + +* sros + * Automatically connect when calling execute() and device is not connected. + * Add init commands + +* nxos unittest + * Added unittest to verify show logging output + +* generic + * Support enable secret prompts + +* generic configure + * Fix config state change where incorrect 'service prompt config' would be sent + +* iosxe/csr1000v + * Cleanup statemachine + +* nxos + * Add VDC detection logic + +* iosxr + * Update config error pattern + +* eos + * new plugin 'eos' for arista eos platform + +* gaia + * New plugin 'gaia' for Check Point Gaia OS platform + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* iosxe + * Added execute statement for 'Do you want to remove the above files?' + +* nxos + * Added configure error pattern to catch '% Ambiguous command at '^' marker.' diff --git a/docs/changelog_plugins/2021/october.rst b/docs/changelog_plugins/2021/october.rst new file mode 100644 index 00000000..a266dbd6 --- /dev/null +++ b/docs/changelog_plugins/2021/october.rst @@ -0,0 +1,25 @@ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* nxos + * Modified copy service + * Fixed to handle source_file properly + +* iosxe + * Refactored rommon state transition, reload and rommon services + +* generic + * Added buffer_wait statement and refactored chatty_term_wait code + * Added wait to config transition to avoid false negatives in config transition + * Add match for unconfigured WLC to default hostname pattern + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* iosxe/cat9k + * Added support for container shell + + diff --git a/docs/changelog_plugins/2021/september.rst b/docs/changelog_plugins/2021/september.rst new file mode 100644 index 00000000..63f810a5 --- /dev/null +++ b/docs/changelog_plugins/2021/september.rst @@ -0,0 +1,50 @@ +September 2021 +======== + +September 28th +------ + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v21.9 + ``unicon``, v21.9 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxr + * Fixed learn_hostname not working for iosxrv9k platform + +* generic + * Fix the default dialog statements used in reload services + * Fix reload service to return True or False instead of raise an exception + +* nxos + * configure will raise incomplete command error when appropriate + +* iosxr/spitfire + * Use generic pre-connection statement list to handle syslog messages on connect + +* linux + * Add `sudo` service diff --git a/docs/changelog_plugins/2022/april.rst b/docs/changelog_plugins/2022/april.rst new file mode 100644 index 00000000..37409fbc --- /dev/null +++ b/docs/changelog_plugins/2022/april.rst @@ -0,0 +1,60 @@ +April 2022 +========== + +April 26 - Unicon.Plugins v22.4 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v22.4 + ``unicon``, v22.4 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* iosxr/spitfire + * Added dedicated reload service + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxe + * Update SDWAN unittests + +* iosxe/cat9k + * Added reload service for HA connections + +* mock data + * updated mock data, replaced hostname with %N + + diff --git a/docs/changelog_plugins/2022/august.rst b/docs/changelog_plugins/2022/august.rst new file mode 100644 index 00000000..edb98bfc --- /dev/null +++ b/docs/changelog_plugins/2022/august.rst @@ -0,0 +1,63 @@ +August 2022 +========== + +August 30 - Unicon.Plugins v22.8 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v22.8 + ``unicon``, v22.8 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* generic + * Update the default hostname pattern to avoid matching enable pattern against config prompt + * Update syslog regex pattern for guestshell log message + +* iosxe + * Added new config prompts related to getvpn gdoi in patterns.py + * Added wsma prompts to config prompt pattern + * Refactor grub boot handler + * Refactor iosxe reload service, rename context variable boot_image to grub_boot_image + * Update press_any_key regex pattern + * Update grub_prompt regex pattern + * Add escape char regex setting `ESCAPE_CHAR_PROMPT_PATTERN` + * Add grub regex pattern setting `GRUB_REGEX_PATTERN` to match menu entries + +* linux + * Updated linux prompt pattern + +* general + * Update regex patterns in CopyPatterns to be more strict + +* iosxe/cat9k + * Updated the container shell prompt pattern + +* iosxe/cat8k + * Added Reload and HAReload + diff --git a/docs/changelog_plugins/2022/february.rst b/docs/changelog_plugins/2022/february.rst new file mode 100644 index 00000000..77ec3770 --- /dev/null +++ b/docs/changelog_plugins/2022/february.rst @@ -0,0 +1,52 @@ +February 2022 +========== + +February 24 - Unicon.Plugins v22.2 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v22.2 + ``unicon``, v22.2 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* utils + * Modified AbstractTokenDiscovery + * Extended prompt dialog to handle output containing "--More--" + * Modified load_pid_token_csv_file + * Renamed to load_token_csv_file + * Adjusted logic to support dynamic csv loading based on header fields + * Added an optional `key` argument to allow for different keys to be specified other than pid + + diff --git a/docs/changelog_plugins/2022/january.rst b/docs/changelog_plugins/2022/january.rst new file mode 100644 index 00000000..84feab65 --- /dev/null +++ b/docs/changelog_plugins/2022/january.rst @@ -0,0 +1,78 @@ +January 2022 +========== + +January 25 - Unicon.Plugins v22.1 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v22.1 + ``unicon``, v22.1 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* generic + * Added `CONFIG_TRANSITION_WAIT` setting to allow changes the config transition wait time + +* iosxe/iec3400 + * New plugin for IEC3400 device + +* iosxe/cat8k + * Updated switchover implementation + * Added POST_SWITCHOVER_WAIT setting + * Added missing context to dialog + * Added option to return output + +* iosxe + * Added support for ROMMON init commands + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* iosxe + * Added new model under c9800 called c9800-cl + * Added cat4k plugin + +* hvrp + * New plugin to connect to Huawei devices + +* iosxe/c9800/ewc_ap + * Add new plugin for C9800/EWC_AP platform + +* utils + * Added AbstractTokenDiscovery + * Added mechanism to learn, standardize, and apply device abstraction tokens + +* nxos + * Added l2rib client support to statemachine + * New `l2rib_dt` service \ No newline at end of file diff --git a/docs/changelog_plugins/2022/july.rst b/docs/changelog_plugins/2022/july.rst new file mode 100644 index 00000000..bd874d87 --- /dev/null +++ b/docs/changelog_plugins/2022/july.rst @@ -0,0 +1,71 @@ +July 2022 +========== + +July 26 - Unicon.Plugins v22.7 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v22.7 + ``unicon``, v22.7 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* general + * Add 'line vty 0 4' exec timeout to default init config commands + +* iosxe + * cat3k + * Updated error_pattern logic in service_implementation.py + * iec3400 + * Updated error_pattern logic in service_implementation.py + * quad + * Updated error_pattern logic in service_implementation.py + * stack + * Updated error_pattern logic in service_implementation.py + +* iosxr + * asr9k + * Updated error_pattern logic in service_implementation.py + * ncs5k + * Updated error_pattern logic in service_implementation.py + +* nxos + * Updated error_pattern logic in service_implementation.py + * aci + * Updated error_pattern logic in service_implementation.py + +* generic + * Updated error_pattern logic in service_implementation.py + + diff --git a/docs/changelog_plugins/2022/june.rst b/docs/changelog_plugins/2022/june.rst new file mode 100644 index 00000000..1f4b5546 --- /dev/null +++ b/docs/changelog_plugins/2022/june.rst @@ -0,0 +1,47 @@ +June 2022 +========== + +June 28 - Unicon.Plugins v22.6 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v22.6 + ``unicon``, v22.6 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- +* iosxe + * settings: + * add POST_BOOT_TIMEOUT and BOOT_POSTCHECK_INTERVAL +* iosxe/stack + * settings: + * add STACK_BOOT_TIMEOUT diff --git a/docs/changelog_plugins/2022/march.rst b/docs/changelog_plugins/2022/march.rst new file mode 100644 index 00000000..e27ff1b9 --- /dev/null +++ b/docs/changelog_plugins/2022/march.rst @@ -0,0 +1,68 @@ +March 2022 +========== + +March 29 - Unicon.Plugins v22.3 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v22.3 + ``unicon``, v22.3 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxe + * Add support for switch and rp keyword arguments for bash console service + * Added host-list to config pattern + +* iosxe/cat8k + * Fix switchover service transitions + +* all + * Moved the pid_tokens.csv file to properly include it during packaging + +* generic + * Added broken pipe to the reload connection_closed pattern + * Fix loading of token info file + +* dnos6 + * NON BACKWARDS-COMPATIBLE CHANGE removed dell os and os6 platform, replaced with dnos6 os + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* dnos10 + * added plugin support for dnos10 + + diff --git a/docs/changelog_plugins/2022/may.rst b/docs/changelog_plugins/2022/may.rst new file mode 100644 index 00000000..a9fd3eca --- /dev/null +++ b/docs/changelog_plugins/2022/may.rst @@ -0,0 +1,62 @@ +May 2022 +========== + +May 31 - Unicon.Plugins v22.5 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v22.5 + ``unicon``, v22.5 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxe + * Updated prompt stripping util for configure service + * Add macro state to statemachine and configure service + +* iosxe/cat9k + * Added reload service for HA connections + +* mock data + * updated mock data, replaced hostname with %N + +* ios/pagent + * Add state for emulator prompt + +* generic + * Update PID mapping file, rename some of the tokens. Use lowercase for model. + +* iosxr + * Update bash prompt pattern + + diff --git a/docs/changelog_plugins/2022/november.rst b/docs/changelog_plugins/2022/november.rst new file mode 100644 index 00000000..1446e4ab --- /dev/null +++ b/docs/changelog_plugins/2022/november.rst @@ -0,0 +1,59 @@ +November 2022 +========== + +November 28 - Unicon.Plugins v22.11 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v22.11 + ``unicon``, v22.11 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + bash$ pip install unicon.plugins + bash$ pip install unicon +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +-------------------------------------------------------------------------------- + Add +-------------------------------------------------------------------------------- + +* generic + * Added support for trex console in linux + +* iosxe/sdwan + * Added config transaction support for ha devices. + +* generic + * configure + * add allow_state_change for configure service. + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* sros + * Updated plugin for error detection and improved configuration handling + +* generic + * Fix the copy service pattern for tftp_addr + +* iosxr + * Modified reload service for asr9k and ncs5k + * Checking the buffer for settling down and using the connection provider + +* topology + * Modified terminal_server schema doc to capture issues with incorrect schema. \ No newline at end of file diff --git a/docs/changelog_plugins/2022/october.rst b/docs/changelog_plugins/2022/october.rst new file mode 100644 index 00000000..db1f1f59 --- /dev/null +++ b/docs/changelog_plugins/2022/october.rst @@ -0,0 +1,35 @@ +October 2022 +========== + +October 25 - Unicon.Plugins v22.10 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v22.10 + ``unicon``, v22.10 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + bash$ pip install unicon.plugins + bash$ pip install unicon +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- +* nxos/patterns + * Modified NxosPatterns: + * Modified config_prompt to handle bell character \ No newline at end of file diff --git a/docs/changelog_plugins/2022/september.rst b/docs/changelog_plugins/2022/september.rst new file mode 100644 index 00000000..95ee8187 --- /dev/null +++ b/docs/changelog_plugins/2022/september.rst @@ -0,0 +1,54 @@ +September 2022 +========== + +September 27 - Unicon.Plugins v22.9 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v22.9 + ``unicon``, v22.9 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* generic + * Added setting for state change prompt retries, default to 3 second wait + * Update sudo regex pattern + * Updated the session data to handle the reoccurring dialog issue + * Update copy error pattern to ignore self-signed certificate failure + * Add handlers for ping options extended_verbose, timestamp_count, record_hops, src_route_type + +* iosxr/ncs5k + * Updated the mock data for ncs5k + +* iosxe + * Added a RELOAD_WAIT to iosxe settings + +* hvrp + * Updated the pattern and setting to support configuration and error detection. + + diff --git a/docs/changelog_plugins/2023/april.rst b/docs/changelog_plugins/2023/april.rst new file mode 100644 index 00000000..8c7cf9ad --- /dev/null +++ b/docs/changelog_plugins/2023/april.rst @@ -0,0 +1,46 @@ +April 2023 +========== + +April 25 - Unicon.Plugins v23.4 +------------------------------- + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v23.4 + ``unicon``, v23.4 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* linux + * Update prompt stripping implementation + + diff --git a/docs/changelog_plugins/2023/august.rst b/docs/changelog_plugins/2023/august.rst new file mode 100644 index 00000000..58b0ff86 --- /dev/null +++ b/docs/changelog_plugins/2023/august.rst @@ -0,0 +1,63 @@ +August 2023 +=========== + +August 29 - Unicon.Plugins v23.8 +-------------------------------- + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v23.8 + ``unicon``, v23.8 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* unicon + * Added support for os_flavor as plugin selector attribute +* unicon.bases.linux + * Added init_connection to connection provider: + * added init_connection method for initializing the device + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxe + * stack: + * Update mock data for stack devices for standby lock. +* cheetah + * Add support for devshell in cheetah OS based wireless access points +* iosxe + * Update enable secret setup dialog logic to support devices without password or with short password +* Generic + * Added recovery for Reload and HaRelaod: + * Recover device using golden image if reload is failed with an exception diff --git a/docs/changelog_plugins/2023/february.rst b/docs/changelog_plugins/2023/february.rst new file mode 100644 index 00000000..1394dfce --- /dev/null +++ b/docs/changelog_plugins/2023/february.rst @@ -0,0 +1,50 @@ +February 2023 +============= + +February 24 - Unicon.Plugins v23.2 +---------------------------------- + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v23.2 + ``unicon``, v23.2 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxe/cat9k + * Update container exit commands + +* linux + * Update linux pattern to work with RADkit interactive connections. + + diff --git a/docs/changelog_plugins/2023/january.rst b/docs/changelog_plugins/2023/january.rst new file mode 100644 index 00000000..c4df89cd --- /dev/null +++ b/docs/changelog_plugins/2023/january.rst @@ -0,0 +1,52 @@ +January 2023 +============ + +January 31 - Unicon v23.1 +------------------------- + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v23.1 + ``unicon``, v23.1 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ + + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxe + * Add eWLC to default bash prompt + +* ons + * New plugin for Optical Networking System (ons) for TL1 prompt + + diff --git a/docs/changelog_plugins/2023/july.rst b/docs/changelog_plugins/2023/july.rst new file mode 100644 index 00000000..e8fe6daa --- /dev/null +++ b/docs/changelog_plugins/2023/july.rst @@ -0,0 +1,47 @@ +July 2023 +========= + +July 24 - Unicon.Plugins v23.7 +------------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v23.7 + ``unicon``, v23.7 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxe + * Update confirm pattern and statements to support Abort Copy + * Added configuration error patterns + + diff --git a/docs/changelog_plugins/2023/june.rst b/docs/changelog_plugins/2023/june.rst new file mode 100644 index 00000000..94a0bca0 --- /dev/null +++ b/docs/changelog_plugins/2023/june.rst @@ -0,0 +1,46 @@ +June 2023 +========== + +June 27 - Unicon.Plugins v23.6 +------------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v23.6 + ``unicon``, v23.6 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxe + * Update mock data for rommon boot unittest + + diff --git a/docs/changelog_plugins/2023/march.rst b/docs/changelog_plugins/2023/march.rst new file mode 100644 index 00000000..1dc06b6a --- /dev/null +++ b/docs/changelog_plugins/2023/march.rst @@ -0,0 +1,47 @@ +March 2023 +========== + +March 28 - Unicon.Plugins v23.3 +------------------------------- + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v23.3 + ``unicon``, v23.3 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* nxos + * Update rommon state support to support automated boot + + diff --git a/docs/changelog_plugins/2023/may.rst b/docs/changelog_plugins/2023/may.rst new file mode 100644 index 00000000..056a8758 --- /dev/null +++ b/docs/changelog_plugins/2023/may.rst @@ -0,0 +1,66 @@ +May 2023 +========== + +May 30 - Unicon.Plugins v23.5 +----------------------------- + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v23.5 + ``unicon``, v23.5 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* generic + * If 'connection refused' is seen on connect, try to clear the console line + * Update reload pattern to support quick reload prompt + +* iosxe + * Update tclsh pattern to handle truncated hostnames + +* junos + * Update prompt patterns to avoid backtracking + +* iosxr + * Fix start command for moonshine HA connections + + +-------------------------------------------------------------------------------- + Modify +-------------------------------------------------------------------------------- + +* iosxr + * asr9k + * Modified call_service in service_implementation + * removed genie dependency + + diff --git a/docs/changelog_plugins/2023/november.rst b/docs/changelog_plugins/2023/november.rst new file mode 100644 index 00000000..93db0044 --- /dev/null +++ b/docs/changelog_plugins/2023/november.rst @@ -0,0 +1,54 @@ +November 2023 +========== + +November 27 - Unicon.Plugins v23.11 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v23.11 + ``unicon``, v23.11 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* sonic + * Add connection class for SONiC that inherits from Linux + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* generic + * Update logic for switchover service to detect standby state + + diff --git a/docs/changelog_plugins/2023/october.rst b/docs/changelog_plugins/2023/october.rst new file mode 100644 index 00000000..04cf9fe1 --- /dev/null +++ b/docs/changelog_plugins/2023/october.rst @@ -0,0 +1,64 @@ +October 2023 +============ + +October 31 - Unicon.Plugins v23.10 +---------------------------------- + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v23.10 + ``unicon``, v23.10 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* generic + * Empty sendline to get the prompt for go_to any state + +* iosxe/cat9k + * Updated container ssh prompt pattern + +* nxos + * Modified + * Added alt_cred password lab2 in nxos_mock_data_n5k.yaml + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* generic + * Adding fallback credentials for handling authentication failure. + +* iosxe + * Adding new password statement for setting up the password on the device after the device has booted in controller mode. + + diff --git a/docs/changelog_plugins/2023/september.rst b/docs/changelog_plugins/2023/september.rst new file mode 100644 index 00000000..9c04de89 --- /dev/null +++ b/docs/changelog_plugins/2023/september.rst @@ -0,0 +1,51 @@ +September 2023 +============== + +September 26 - Unicon.Plugins v23.9 +----------------------------------- + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v23.9 + ``unicon``, v23.9 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* generic + * Update reload pattern to support iosxe reload prompt + * Updated to expose post_reload_wait_time to reload API + +* iosxe + * StackRommon support + * StackEnable support + + diff --git a/docs/changelog_plugins/2024/September.rst b/docs/changelog_plugins/2024/September.rst new file mode 100644 index 00000000..a74c4d1d --- /dev/null +++ b/docs/changelog_plugins/2024/September.rst @@ -0,0 +1,42 @@ +September 2024 +========== + +September 24 - Unicon.Plugins v24.9 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v24.9 + ``unicon``, v24.9 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxr + * Added support for APIC patterns + +* iosxe + * Update config prompt pattern to support CA cert map + +* generic + * Update execute() service log message to include device alias + * Add parse method to bash_console context manager with abstraction fallback to linux os + + +-------------------------------------------------------------------------------- + Add +-------------------------------------------------------------------------------- + +* apic plugin + * Added Regex in post_service in Execute to remove extra junk values. + + diff --git a/docs/changelog_plugins/2024/april.rst b/docs/changelog_plugins/2024/april.rst new file mode 100644 index 00000000..2ef86bdf --- /dev/null +++ b/docs/changelog_plugins/2024/april.rst @@ -0,0 +1,67 @@ +April 2024 +========== + + - Unicon.Plugins v24.4 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v24.4 + ``unicon``, v24.4 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* generic + * Use stricter pattern for enable password + * Update standby locked pattern + * Add connection closed statement to execute service + * Add standby locked state to single RP statemachine + * Update escape character handler timing settings + * Revert adding connection closed statement to execute service + * Update config transition logic + * Add `result_check_per_command` option to disable/enable error checking per configuration command + +* iosxe + * Fix operating mode logic + * More prompt handling updated + * Added statements to token discovery dialog + +* iosxr + * Add standby locked state to single RP statemachine + * Change default behavior of ``configure()`` service, error check after all commands by default + * Add handler for `show configuration failed` errors to ``configure()`` service. + * Add `SHOW_CONFIG_FAILED_CMD` setting for command to use, default `show configuration failed` + +* other + * update pid token list + + diff --git a/docs/changelog_plugins/2024/august.rst b/docs/changelog_plugins/2024/august.rst new file mode 100644 index 00000000..66a7ef25 --- /dev/null +++ b/docs/changelog_plugins/2024/august.rst @@ -0,0 +1,38 @@ +August 2024 +========== + +August 27 - Unicon.Plugins v24.8 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v24.8 + ``unicon``, v24.8 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ diff --git a/docs/changelog_plugins/2024/february.rst b/docs/changelog_plugins/2024/february.rst new file mode 100644 index 00000000..ea612936 --- /dev/null +++ b/docs/changelog_plugins/2024/february.rst @@ -0,0 +1,59 @@ +February 2024 +========== + +February 27 - Unicon.Plugins v24.2 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v24.2 + ``unicon``, v24.2 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* generic + * Add EOF statement to handle connection loss when using telnet backend + +* iosxe + * Added below config error patterns + * % Invalid address + * % Deletion of RD in progress; wait for it to complete + + +-------------------------------------------------------------------------------- + Add +-------------------------------------------------------------------------------- + +* utils + * Update assertRegexpMatches to assertRegex to fix attribute error in python 3.12 + + diff --git a/docs/changelog_plugins/2024/january.rst b/docs/changelog_plugins/2024/january.rst new file mode 100644 index 00000000..9765faba --- /dev/null +++ b/docs/changelog_plugins/2024/january.rst @@ -0,0 +1,66 @@ +January 2024 +========== + +30 - Unicon.Plugins v24.1 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v24.1 + ``unicon``, v24.1 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Add +-------------------------------------------------------------------------------- + +* generic + * Added more prompt support in connection statement list + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxe + * Added unittests to test hostnames with special characters\ + * Update settings for reload API, change SYSLOG_WAIT to 10 seconds + * cat9k + * Update image_to_boot for HA device. (active and standby rp) + +* generic, iosxe + * Update config transition logic, increase wait time for prompt + +* generic + * Update response to setup dialog to "no" instead of "n" + +* linux + * Update linux hostname learning pattern to handle ANSI characters in prompt + + diff --git a/docs/changelog_plugins/2024/july.rst b/docs/changelog_plugins/2024/july.rst new file mode 100644 index 00000000..73ef6427 --- /dev/null +++ b/docs/changelog_plugins/2024/july.rst @@ -0,0 +1,46 @@ +July 2024 +========== + +July 30 - Unicon.Plugins v24.7 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v24.7 + ``unicon``, v24.7 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxe + * add "disable_selinux" parameter to bash_console service, to automatically + + diff --git a/docs/changelog_plugins/2024/june.rst b/docs/changelog_plugins/2024/june.rst new file mode 100644 index 00000000..cd7b898e --- /dev/null +++ b/docs/changelog_plugins/2024/june.rst @@ -0,0 +1,55 @@ +June 2024 +========== + + - Unicon.Plugins v24.6 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v24.6 + ``unicon``, v24.6 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* other + * Update os value to iosxe for model C1100 + * Updated os value to iosxe for ISR1100 submodel + + +-------------------------------------------------------------------------------- + Add +-------------------------------------------------------------------------------- + +* iosxr + * Added `admin_host` state support + + diff --git a/docs/changelog_plugins/2024/march.rst b/docs/changelog_plugins/2024/march.rst new file mode 100644 index 00000000..37b8e7f5 --- /dev/null +++ b/docs/changelog_plugins/2024/march.rst @@ -0,0 +1,54 @@ +March 2024 +========== + + - Unicon.Plugins v24.3 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v24.3 + ``unicon``, v24.3 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* cheetah/ap + * Add more prompt handler to statemachine + +* token discovery + * Add more prompt to dialog + * Update dialog timeout + +* generic + * Added encryption selection pattern + * Removed duplicate enable_secret_handler and setup_enter_selection functions + + diff --git a/docs/changelog_plugins/2024/may.rst b/docs/changelog_plugins/2024/may.rst new file mode 100644 index 00000000..aaa05730 --- /dev/null +++ b/docs/changelog_plugins/2024/may.rst @@ -0,0 +1,51 @@ +May 2024 +========== + +May 28 - Unicon.Plugins v24.5 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v24.5 + ``unicon``, v24.5 + +Install Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install unicon.plugins + bash$ pip install unicon + +Upgrade Instructions +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + bash$ pip install --upgrade unicon.plugins + bash$ pip install --upgrade unicon + +Features and Bug Fixes: +^^^^^^^^^^^^^^^^^^^^^^^ + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* sros + * Updated mdcli regex prompt to accommodate various output + +* iosxe + * CAT9K + * Updated regex in Rommon service + * Modified learn_tokens to go to enable mode before sending stop PnP disovery + + diff --git a/docs/changelog_plugins/2024/november.rst b/docs/changelog_plugins/2024/november.rst new file mode 100644 index 00000000..d72930e1 --- /dev/null +++ b/docs/changelog_plugins/2024/november.rst @@ -0,0 +1,50 @@ +November 2024 +========== + +November 26 - Unicon.Plugins v24.11 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v24.11 + ``unicon``, v24.11 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxe + * Added UT for Quad device to test scenario when standby console is disabled + +* iosxr + * SPITFIRE plugin + * Added UNICON_BACKEND_DECODE_ERROR_LIMIT with a default value of 10, to handle scenarios when the device is slow + +* hvrp + * Update config pattern + * Update configure service to handle immediate vs two-stage config mode + +* nxos + * modify regex to handle new error pattern for NXOS + +* generic + * Modified enable_secret regex pattern to accommodate various outputs + * Updated password_handler to pass password if password key in context dict + + +-------------------------------------------------------------------------------- + Add +-------------------------------------------------------------------------------- + +* iosxe + * Update prompt recovery command + + diff --git a/docs/changelog_plugins/2024/october.rst b/docs/changelog_plugins/2024/october.rst new file mode 100644 index 00000000..6772e328 --- /dev/null +++ b/docs/changelog_plugins/2024/october.rst @@ -0,0 +1,36 @@ +October 2024 +========== + +October 29 - Unicon.Plugins v24.10 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v24.10 + ``unicon``, v24.10 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* pid tokens + * Updated PID tokens to support NCS devices + + +-------------------------------------------------------------------------------- + Add +-------------------------------------------------------------------------------- + +* apic plugin + * Modified the regex patterns in the post_service method in Execute to remove extra junk values and retain the newline character in the output. + * Added a configure class to eliminate extra junk values from the output. + + diff --git a/docs/changelog_plugins/2025/april.rst b/docs/changelog_plugins/2025/april.rst new file mode 100644 index 00000000..82df75a0 --- /dev/null +++ b/docs/changelog_plugins/2025/april.rst @@ -0,0 +1,45 @@ +April 2025 +========== + +April 29 - Unicon.Plugins v25.4 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.4 + ``unicon``, v25.4 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxr + * Update admin host pattern + * Update prompt commands to recover console + +* generic + * Updated output variable by passing count argument, To get get rid of messages like 'DeprecationWarning 'count' is passed as positional argument' + * Update escape handler to support a list of prompt commands + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* iosxe + * Cat9k + * Support for HA ROMMON + * Added support for no enable password being set. A UniconAuthenticationError will be raised if the enable password is not set and the user tries to enable the device. + +* generic + * Return output of HAReloadService to match with generic ReloadService + + diff --git a/docs/changelog_plugins/2025/august.rst b/docs/changelog_plugins/2025/august.rst new file mode 100644 index 00000000..8abf503f --- /dev/null +++ b/docs/changelog_plugins/2025/august.rst @@ -0,0 +1,38 @@ +August 2025 +========== + +August 23 - Unicon.Plugins v25.8 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.8 + ``unicon``, v25.8 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* iosxe + * Updated prompts patterns to handle c9800 virtual devices. + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxe + * Updated syslog pattern to handle security log message + +* generic + * Handle tclsh continuation prompt "+>" on initial connection. + + diff --git a/docs/changelog_plugins/2025/december.rst b/docs/changelog_plugins/2025/december.rst new file mode 100644 index 00000000..767c4c85 --- /dev/null +++ b/docs/changelog_plugins/2025/december.rst @@ -0,0 +1,44 @@ +December 2025 +========== + +December 30 - Unicon.Plugins v25.11 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.11 + ``unicon``, v25.11 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxe + * Added cursor position handling in bash ContextMgr to prevent delays during shell initialization. + +* nxos + * Updated the LC bash prompt pattern to include an anchor and improve prompt detection performance. + +* generic + * Updated syslog pattern to handle insecure dynamic warning message for SSH hostkey with insufficient key length. + +* linux + * Updated prompt patterns to better handle ANSI escape sequences in prompts + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* generic/settings.py + * Updated the temporary enable secret to include a special character. + + diff --git a/docs/changelog_plugins/2025/february.rst b/docs/changelog_plugins/2025/february.rst new file mode 100644 index 00000000..abc6f684 --- /dev/null +++ b/docs/changelog_plugins/2025/february.rst @@ -0,0 +1,36 @@ +February 2025 +========== + +February 25 - Unicon.Plugins v25.2 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.2 + ``unicon``, v25.2 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* generic + * Updates to setup patterns + * Update connection refused handler to clear line after max count + * Update token discovery to use rv1 parsers + * Update token discovery to handle standby locked devices + +* staros + * Update prompt pattern + +* iosxe + * Updated the enable, disable and maintenance states to support `(unlicensed)` prompt + + diff --git a/docs/changelog_plugins/2025/january.rst b/docs/changelog_plugins/2025/january.rst new file mode 100644 index 00000000..204d5a75 --- /dev/null +++ b/docs/changelog_plugins/2025/january.rst @@ -0,0 +1,40 @@ +January 2025 +========== + + - Unicon.Plugins v25.1 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.1 + ``unicon``, v25.1 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxr + * Update monitor service prompt pattern + * Fix action pattern regex + * Update logic support matching case and space insensitive actions + * SPITFIRE plugin + * Added a new pattern to recognize the prompt seen when showtech collection times out and the script tries to exit by sending kill signal. Also, added the statement to run while the pattern matches + * Added UNICON_BACKEND_DECODE_ERROR_LIMIT with a default value of 10, to handle scenarios when the device is slow + * Add statements to reload dialog + * Add pattern for "Do you wish to continue" + * Add syslog statement to config state transition + +* generic + * Update learn_os_prompt to account for config mode + * update syslog message pattern + +* unicon.plugins + * Fix syntax warning \ No newline at end of file diff --git a/docs/changelog_plugins/2025/july.rst b/docs/changelog_plugins/2025/july.rst new file mode 100644 index 00000000..d45e1738 --- /dev/null +++ b/docs/changelog_plugins/2025/july.rst @@ -0,0 +1,39 @@ +July 2025 +========== + +July 29 - Unicon.Plugins v25.7 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.7 + ``unicon``, v25.7 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxr + * switchover + * Fixed timeout handling by using explicit timeout parameter instead of self.timeout. + * Update monitor service prompt pattern + * Increase monitor stop timeout + * Update execute() service to exit unsupported modes (e.g. monitor mode) + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* iosxe + * Added support for 9500 and 9500x SVL switchover + + diff --git a/docs/changelog_plugins/2025/june.rst b/docs/changelog_plugins/2025/june.rst new file mode 100644 index 00000000..c4f1c46a --- /dev/null +++ b/docs/changelog_plugins/2025/june.rst @@ -0,0 +1,51 @@ +June 2025 +========== + +June 29 - Unicon.Plugins v25.6 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.6 + ``unicon``, v25.6 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxr/spitfire + * Update ZTP lock check + +* unicon.plugins + * IOSXE + * Stack + * Fixed `image_to_boot` parameter in reload service so that it is actually used in the reload process. + +* generic + * Update syslog pattern + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* iosxe + * IosXEPatterns + * Added the ca-trustpool regex to match prompt for adding a CA certificate to the trustpool + * Added ACM rules state and transition support to the IOS-XE plugin. + * Enhanced Configure and HAConfigure services to support ACM rules CLI via the rules argument, using context-driven state transitions. + * Added post-service transitions to gracefully return to enable mode after configuration. + +* nxos + * Introducing a new service l2rib_pycl in NXOS plugin as a replacement for the existing service l2rib_dt + * Deprecating the existing service l2rib_dt + + diff --git a/docs/changelog_plugins/2025/march.rst b/docs/changelog_plugins/2025/march.rst new file mode 100644 index 00000000..107b4d42 --- /dev/null +++ b/docs/changelog_plugins/2025/march.rst @@ -0,0 +1,19 @@ +March 2025 +========== + +March 25 - Unicon.Plugins v25.3 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.3 + ``unicon``, v25.3 + + + + +Changelogs +^^^^^^^^^^ diff --git a/docs/changelog_plugins/2025/may.rst b/docs/changelog_plugins/2025/may.rst new file mode 100644 index 00000000..696fff0f --- /dev/null +++ b/docs/changelog_plugins/2025/may.rst @@ -0,0 +1,64 @@ +May 2025 +========== + + - Unicon.Plugins v25.5 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.5 + ``unicon``, v25.5 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Add +-------------------------------------------------------------------------------- + +* iosxe/cat9k/stackwise_virtual + * Added support for SVL + +* iosxe/cat9k/c9500x/stackwise_virtual + * Added support for SVL + +* generic + * Add loghandler for subconnections to capture the buffer output + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* generic + * Made it so incorrect login errors will attempt to use fallback credentials + +* nxos + * Add support for bash_console with module argument. + * Make l2rib_dt_prompt pattern more strict + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* nxos + * Added support for configure session + * When using device.configure() you can now pass a session name with session="session_name" + * IE device.configure("...", session="my_session") + +* iosxe + * IosXEPatterns + * Updated the recovery-mode regex to match prompt for both mode + * Added the rp-rec-mode regex to match prompt + * Added acm state and transition support to IOS-XE plugin. + * Enhanced Configure and HAConfigure services to support ACM CLI via acm_configlet argument using context-driven state transitions. + * Added context-based transition function to enter ACM mode using acm configlet create . + * Added post-service transition to gracefully return to enable mode after configuration. + + diff --git a/docs/changelog_plugins/2025/october.rst b/docs/changelog_plugins/2025/october.rst new file mode 100644 index 00000000..8af0480c --- /dev/null +++ b/docs/changelog_plugins/2025/october.rst @@ -0,0 +1,47 @@ +October 2025 +========== + +October 28 - Unicon.Plugins v25.10 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.10 + ``unicon``, v25.10 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* iosxe/c9800 + +* plugins/linux + * Added uptime command to LINUX_INIT_EXEC_COMMANDS by default. + +* unicon + * Added UT for the fix added in unicon + +* iosxe + * Updated the disable prompt pattern not to recongnise grub as disable mode prompt. + + +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* iosxe + * Updated exec error pattern + * Fixed ACM Configure service end-state handling. + +* generic + * Updated syslog pattern to handle RSA key log message + + diff --git a/docs/changelog_plugins/2025/september.rst b/docs/changelog_plugins/2025/september.rst new file mode 100644 index 00000000..f3fb1a26 --- /dev/null +++ b/docs/changelog_plugins/2025/september.rst @@ -0,0 +1,41 @@ +September 2025 +========== + +September 30 - Unicon v25.9 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v25.9 + ``unicon``, v25.9 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* generic + * Update config error pattern + +* iosxe + * Updated disable_prompt to ensure no false positives + * Added support to syntax prompt + +* pid tokens + * Updated PID tokens to support C9500X-60L4D devices + + +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- + +* iosxe/cat9k add rommon variable support in reload service + + diff --git a/docs/changelog_plugins/2026/february.rst b/docs/changelog_plugins/2026/february.rst new file mode 100644 index 00000000..e8189ef4 --- /dev/null +++ b/docs/changelog_plugins/2026/february.rst @@ -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. + + diff --git a/docs/changelog_plugins/2026/january.rst b/docs/changelog_plugins/2026/january.rst new file mode 100644 index 00000000..d33fe64d --- /dev/null +++ b/docs/changelog_plugins/2026/january.rst @@ -0,0 +1,40 @@ +January 2026 +========== + +January 27 - Unicon v26.1 +------------------------ + + + +.. csv-table:: Module Versions + :header: "Modules", "Versions" + + ``unicon.plugins``, v26.1 + ``unicon``, v26.1 + + + + +Changelogs +^^^^^^^^^^ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- + +* generic + * Update enable service to user transition dialog + * Update escape_char_stmt to handle 2 check for buffer for connection refuse + +* iosxe + * Add Enable service to explicitly add "enable" command + +* unicon.plugins + * IOSXE/C9500/SVL_STACK + * Add dis_state prompt statement to stack_switchover_stmt_list preventing timeout when the standby comes up at disable mode. + * IOSXE + * updated the configure service logic to support the multiline banner + +* iosxr + * Updated the run_prompt regex to avoid mixing standalone # in execution output with enable prompt. + + diff --git a/docs/changelog_plugins/changelog_lsheikal_update_generic_password_ok_20251112073999.rst b/docs/changelog_plugins/changelog_lsheikal_update_generic_password_ok_20251112073999.rst new file mode 100644 index 00000000..6de866d9 --- /dev/null +++ b/docs/changelog_plugins/changelog_lsheikal_update_generic_password_ok_20251112073999.rst @@ -0,0 +1,5 @@ +-------------------------------------------------------------------------------- + New +-------------------------------------------------------------------------------- +* generic/statements.py + * Updated the password_ok_stmt to use escape_char_callback instead of sendline. diff --git a/docs/changelog_plugins/index.rst b/docs/changelog_plugins/index.rst new file mode 100644 index 00000000..8e5f4bb3 --- /dev/null +++ b/docs/changelog_plugins/index.rst @@ -0,0 +1,75 @@ +Plugins Changelog +================= + +.. toctree:: + :maxdepth: 2 + + 2026/february + 2026/january + 2025/december + 2025/october + 2025/september + 2025/august + 2025/july + 2025/june + 2025/may + 2025/april + 2025/march + 2025/february + 2025/january + 2024/november + 2024/october + 2024/September + 2024/august + 2024/july + 2024/june + 2024/may + 2024/april + 2024/march + 2024/february + 2024/january + 2023/november + 2023/october + 2023/september + 2023/august + 2023/july + 2023/june + 2023/may + 2023/april + 2023/march + 2023/february + 2023/january + 2022/november + 2022/october + 2022/september + 2022/august + 2022/july + 2022/june + 2022/may + 2022/april + 2022/march + 2022/february + 2022/january + 2021/december + 2021/october + 2021/september + 2021/august + 2021/july + 2021/june + 2021/may + 2021/april + 2021/march + 2021/february + 2021/january + 2020/december + 2020/october + 2020/sept + 2020/august + 2020/july + 2020/june + 2020/may + 2020/april + 2020/feb + 2020/jan + 2019/dec + 2019/nov diff --git a/docs/changelog_plugins/undistributed/template.rst b/docs/changelog_plugins/undistributed/template.rst new file mode 100644 index 00000000..65314f95 --- /dev/null +++ b/docs/changelog_plugins/undistributed/template.rst @@ -0,0 +1,5 @@ +-------------------------------------------------------------------------------- + Fix +-------------------------------------------------------------------------------- +* + * diff --git a/docs/conf.py b/docs/conf.py index 3b83cf93..d7960ac6 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -36,18 +36,21 @@ 'sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'sphinx.ext.intersphinx', - 'sphinxcontrib.mockautodoc', + # 'sphinxcontrib.mockautodoc', ] if os.environ.get('DEVNET', None) == 'true': intersphinx_mapping = { 'python': ('http://docs.python.org/3.6', None ), 'pyats': ('https://pubhub.devnetcloud.com/media/pyats/docs', None ), + 'unicon': ('https://pubhub.devnetcloud.com/media/unicon/docs', None ), } else: intersphinx_mapping = { 'python': ('http://docs.python.org/3.6', None ), 'pyats': ('http://wwwin-pyats.cisco.com/documentation/latest', None ), + 'unicon': ('http://wwwin-pyats.cisco.com/cisco-shared/unicon/latest', + None ), } @@ -66,7 +69,7 @@ # General information about the project. project = 'Unicon Plugins' -copyright = '2014-2019, Cisco Systems Inc.' +copyright = '2014-2020, Cisco Systems Inc.' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/docs/developer_guide/eal.rst b/docs/developer_guide/eal.rst new file mode 100644 index 00000000..7edcfdcd --- /dev/null +++ b/docs/developer_guide/eal.rst @@ -0,0 +1,785 @@ +Expect Abstraction Library +=========================== + +Introduction +------------ +Expect Abstraction Library (EAL), as the name suggests, is a python based +avatar of `Tcl/Expect`_ library. This package attempts to bring in most of the +useful features of Expect in a pythonic flavour. + +**EAL** provides classes and structures required to programatically control +any *interactive command*. For interactive programs, whose order of +interactions varies based on the user input, we can use **dialogs**. **Dialogs** +can dymamically invoke different callbacks based on the corresponding pattern +match. + +EAL provides the lower most abstraction level to **Unicon** to perform device +interactions. This library can be used even outside the context of device +connection, for invocation of general shell commands; for example invoking +an interactive shell program on a linux system. + +This library brings in following major API's and settings. + +* spawn +* expect +* send +* log_user +* no_transfer +* exp_continue +* dialogs +* timeout + +This is how a simple EAL program could look like. + +.. code-block:: python + :linenos: + + from unicon.eal.expect import Spawn + prompt = r"^.*bash\$$\s?" + s = Spawn(spawn_command="telnet 1.2.3.4") + s.expect([r"username:"]) + s.send("admin\r") + s.expect([r"password:"]) + s.sendline("lab") # same as send but doesn't require carriage return + s.expect([prompt]) + s.close() + +Challenges +---------- +Implementing an Expect like library is a bit of task in Python becuase of +following two reasons: + +**Event Driven**: Python, unlike Tcl is not *event driven* language at core. +Becuase of this, python lacks asyncronous event loops. We need asyncronous event +loops for precise tracking of timeouts. + +.. note:: + + Python 3 included asyncio as a core library for carrying out asyncronous + tasks. + +**Eval**: Python does not encourage evaluation of arbitrary code. Yes it is +allowed, but it should be only used only in situation when standard python +techniques do not work. Whereas it is quite common in Tcl to pass chunks of +code as arguments, which the receiving function can invoke in caller's context. +Because of this self imposed limitation, it is difficult to created nested +**Expect** blocks containing patterns and action/callback pairs. + +**Globals**: Globals are strongly discouraged in python. In absence of global +variables we need some special ways to handle situations where we need our +callback functions to communicate with each other. + +**EAL** tries its best to overcome these problems and provide an intuitive set +of APIs to handle interactive shell commands. + +Why Not Pexpect +--------------- + +One common question we often receive is: + + Why not `pexpect`_ ! + +In our benchmark tests we found pexpect to be significantly slower than +Tcl/Expect. The order of difference was enough for us to consider different +possible options. It also lacks concept of *Dialogs*, without which, it is +difficult to scale pexpect programs. + +We also included the following libraries in our benchmark tests. + +* `Telnetlib`_ +* `Exscript`_ +* `Paramiko`_ + +Under The Hood +-------------- + +*EAL* is developed based on `pty`_ library. Pty is a standard python package +for in-memory handling of pseudo terminals. + +Pty library forks a process and provides socket like objects for communicating + with those processes. + +EAL uses this as follows: + +* fork a process. +* in the forked process, exec the ssh command which will connect to localhost. +* once we have the shell, issue the command which needs to be spawned. +* forked process returns a file descriptor, use this for inter process communication. +* destroy the process when the scope of spawned command is over. + +Currently this option is looking scalable and provides extremely good +performance, almost at par with Tcl/Expect or sometimes even better. + +Spawn +----- + +You can ``Spawn`` any command to interact with it. Once the command is spawned +you can interact with it using APIs like ``send`` and ``expect``. + +This is how, in a nutshell, it works + +.. code-block:: python + + from unicon.eal.expect import Spawn + s = Spawn("telnet 1.2.3.4 1000") + # now we have spawn object s + + s.send("\r") + ret = s.expect(['pattern']) + + +Example Shell Script +-------------------- + +Since we do not find interactive commands commonly on linux platforms, hence we +will use the following shell program during all our subsequent examples in this +chapter. Please make sure you save the following shell program as ``router.sh`` +on your Linux/Mac system. All the example which will follow from here will spawn +``router.sh``. You may require to give it execute permission:: + + chmod 755 router.sh + +Credentials for the router:: + + username: admin + password: lab + enable password: lablab + +Here is the source code of ``router.sh``: + +.. literalinclude:: ../user_guide/examples/router.sh + :linenos: + :language: bash + +This is a sample run of this script. It is just a minimal script to simulate a +router kind of stuff:: + + $ ./router.sh + Trying X.X.X.X ... + Escape character is '^]'. + Press enter to continue ... + + username: admin + password: + sim-router>enable + password: lab + sim-router#show clock + Fri Oct 23 01:55:16 IST 2015 + sim-router# + +It is only capable to doing following things which is just enough for our +purpose. + +* perform a login. +* going to enable mode with enable command. +* running ``show clock`` command. + +Spawning Our First Command +-------------------------- + +Now let us **spawn** the ``router.sh``. This is how it can be done. We are +assuming that ``router.sh`` is in the current directory, or else you can provide +the fully qualified path. + +.. code-block:: python + + import os + from unicon.eal.expect import Spawn + router_command = os.path.join(os.getcwd(), 'router.sh') + s = Spawn(router_command) + +Following events happen when above code is executed. + +* an ssh session to localhost is created. This will be manifested a minimal lag. +* a new tty session is created inside the ssh connection. +* ``router.sh`` is invoked. + +.. note:: + + You may also see the login banner of localhost, which is normal. This has + nothing to do with the spawned command. + +Using Send Command +------------------ + +In case you have executed the ``router.sh``, you will notice that it waits for +you to press the ``ENTER`` button, before it can show the username prompt. This +is the exact place where it waits:: + + Press enter to continue ... + +Hence let us send the the carriage return. + +.. code-block:: python + + s.sendline() + # we can also do it like this. + s.send("\r") + # both are equivalent. + +``send/sendline`` methods do not return anything, even if they do, it is +irrelevant. Either your command will be sent or an exception will be raised. + +Expect The Expected +-------------------- + +After the sending the carriage return we expect the **username:** prompt. Hence +let us write a pattern to handle this. + +.. code-block:: python + + ret = s.expect([r'username:\s?$']) + +If the above pattern is not received within the specified amount of time, then +a ``TimeoutError`` is raised. By default, the timeout value is *10*. Let us +reduce it since we know our ``router.sh`` will take almost no time to show +the **username** prompt. + +.. code-block:: python + + ret = s.expect([r'username:\s?$'], timeout=5) + +Let us generalise the above program a bit. We may come across some routers +where username prompt doesn't look like ``username:``, it may also show up like +``login::``. The good news is, ``expect`` method can take a list of patterns. + +.. code-block:: python + + ret = s.expect([r'username:\s?$', r'login:\s?$'], timeout=5) + + +By default, match_mode_detect is enabled. Detect rules are as below: + +1. search whole buffer with re.DOTALL if: + +- pattern contains any of: \\r, \\n + +- pattern equals to any of: .*, ^.*$, .*$, ^.*, .+, ^.+$, .+$, ^.+ + +2. If pattern ends with $ but not \$, will only match last line + +3. In other situations, search whole buffer with re.DOTALL + +Now let's introspect on the return object. The return object contains the +following: + +* last_match: the ``re`` match object. +* match_output: the exact text which matched in the buffer. +* last_match_index: the index of pattern in the list which matched. +* last_match_mode: the match mode eg. search whole buffer with re.DOTALL, only match last line + +Generally you will be interested in the ``match_output``. + +Now lets sum it up and complete the above program to login and run a command. +``show clock``. Most of the program is self explanatory. + +.. literalinclude:: ../user_guide/examples/1_eal_simple_sendex.py + :language: python + :linenos: + +.. note:: + + A note on pattern matching and buffer size. The default search size is 8K + which is used to search up to 8K bytes at the end of the buffer. This speeds + up pattern matching for very large command output. To specify a different + search size, use the `search_size` parameter. Using ``0`` will search the + complete buffer. + + You can check and set the default search size using the `SEARCH_SIZE` setting. + + .. code-block:: python + + ret = s.expect([r'huge pattern .* matching more than 8K'], timeout=60, search_size=16000) + + >>> s.settings.SEARCH_SIZE + 8192 + >>> + >>> s.settings.SEARCH_SIZE = 16000 + >>> s.settings.SEARCH_SIZE + 16000 + >>> + +EOF Exception +------------- + +If the spawn connection has terminated/closed (like someone clear console line or +close() is called on spawn) then any call to send/expect will raise an EOF exception. + +.. code-block:: python + + from unicon.eal.expect import Spawn + s = Spawn("telnet 127.0.0.1 15000") + s.close() + s.expect([r"username:"]) # This will raise EOF + s.send('some data') # this will raise EOF + # Spawn again if EOF raise + from unicon.core.errors import EOF + try: + s.expect(r'.*') + except EOF as e: + print('Spawn not available, Re-Spawn.') + s = Spawn('telnet 127.0.0.1 15000') + + +Need For Dialogs +---------------- + +Above programs looks complete, but it has few limitations. We can use +``send/expect`` pair when we know for sure, that sequence of interaction will +never change. Think of a hypothetical situation, in the above example, if the +``router.sh`` prompts for *password* before *username* ! In such situation, +above program will timeout, even though it knows how to handle the password +prompt. The order of interaction cannot be taken for granted in all the +situations. + +We need to interact with commands which prompts for different things based on +the user input, and our program should be able to handle it. The better example +could be ``copy`` command on the router. On different platforms, and with +different copy protocols we see different questions being asked. And it is +expected from our API's to handle all such variations, in order to produce a +platform agnostic API. + +**Dialogs** provide a way to handle exactly the same situation. It allows us to +club all the anticipated interactions in one structure. It is agnostic to +sequence of interaction as long as dialog knows how to handle it. At semantic +level this how it looks. + +.. code-block:: python + + d = Dialog([statement_1, + statement_2, + ..., + ..., + statement_n]) + # to execute or process a dialog. + d.process(s) + # here s is the spawn instance on which this dialog has to + # be targeted. + +In **EAL** Dialog is a class which is constituted of *Statements*. Before we +go forward, lets study ``Statement`` class, the building block of a dialog. + +Statements +----------- + +Statements are building blocks of Dialogs. It has following constituents. + +* **pattern**: pattern for which the statments get triggered. (mandatory) +* **action**: any callable which needs to be called once the pattern is matched. (optional) +* **args**: a dict which contains arguments to *action*, if any. (default value ``None``) +* **loop_continue**: whether to continue the dialog after this statement match. (default value ``False``) +* **continue_timer**: the dialog timeout gets reset after every match. (default value ``True``) +* **debug_statement**: log the matched pattern if set to True. (default value ``False``) +* **trim_buffer**: whether to remove match content from buffer. (default value ``True``) +* **matched_retries**: retry times if statement pattern is matched. (default value 0) +* **matched_retry_sleep**: sleep between retries. (default value 0.02 seconds) + +This is how an statement can be constructed. + +.. code-block:: python + :linenos: + + # create a simple callback function + def send_password(spawn, password): + spawn.sendline(password) + + from unicon.eal.dialogs import Statement + s = Statement(pattern=r'password:', + action=send_password, + args={'password': 10}, + loop_continue=True, + continue_timer=False, + trim_buffer=True, + debug_statement=True) + +Feel free to use lambdas in case you find it convenient for simple operations + +.. code-block:: python + + # By using lambda, same thing can be written as below. + # in this we don't need to define the callback functions. + from unicon.eal.dialogs import Statement + s = Statement(pattern=r'password:', + action=lambda spawn, password: spawn.sendline(password) + args={'password': 10}, + loop_continue=True, + continue_timer=False) + +Notice the ``args`` in both the examples. We have not supplied any value for +the argument ``spawn`` even though the callback function (or the lambda) depends +on it. EAL performs dependency injection for few thinngs which cannot be +determined while contructing the ``Statement`` object. We will see it in detail +in the next section. + +.. note:: + + Mention ``args``, ``loop_continue``, ``continue_timer`` only if you want to + change the the default values. This will help reducting the clutter. + +**Timeout Statement**: +By default, if none of Statement patterns get match within timeout period +``TimeoutError`` execption gets raised. If we want to add some custom action when +timeout occurs before ``TimeoutError`` execption, this can be done by adding a +``Statement`` with pattern set as ``__timeout__``. Action set for this Statement +will get invoke if timeout occurs. + +.. code-block:: python + + def custom_timeout_method(spawn): + print('None of patterns matched within timeout period.') + + s = Statement(pattern='__timeout__', + action=custom_timeout_method, + loop_continue=False, + continue_timer=False) + +.. note:: + + Make sure to set ``continue_timer`` as ``False`` for timeout statement, else it may + will end up in infinite loop. If ``continue_timer`` set as ``True``, then ``Dialog`` will + start trying to match all patterns again and timeout period will be reset to + original one. + + +Dependency Injection in Statements +----------------------------------- + +Few things which cannot be determined at the time of construction of Statement +objects, are dependency injected by the EAL framework. There are three such +things. + +* spawn +* context (an attribute dict) +* session (an attribute dict) + +**spawn**: +Since same dialog instance can be used on multiple spawns instances, hence user +cannot determine its (spawn) value at the time creating ``Statement`` instance. +If your callback requires *spawn* then, just mention it in signature. +You dont't need to provide its value in the ``args`` section. + +**context**: +It is possible to have a situation when the value of some of the arguments of +the callback needs to be determined at the runtime. One good example could be +fetching the *password* from some config file, on which the developer has no +control. In such situations, same callback function could be written like this. + +.. code-block:: python + :linenos: + + def send_password(spawn, context): + spawn.sendline(context.password) + + from unicon.utils import AttributeDict + ctx = AttributeDict({'password': 'lab'}) + + from unicon.eal.dialogs import Statement + s = Statement(pattern=r'password:', + action=send_password, + loop_continue=True, + continue_timer=False) + # we are assuming we have more statements s1 and s3 + # also we have one spawn instance named s. + d = Dialog([s, s1, s3]) + d.process(s, context=ctx) + +.. note:: + + we don't need ``args`` in above statement as both the values will be + injected in runtime. + +**session**: It is used for scenarios where different callback functions in +a dialog would require to communicate with each other. *session* provides a way +for inter callback communication. It is an ``AttributeDict`` which can be +treated as dictionary. It is also required if the same statement matched more +than once during an interaction and the callback function is expected to +behave differently in both the entries. We will have an example for this later. + +Whenever a dialog processing begins, a blank ``session`` dict is +initialized. Any callback function can add or access any value to it. Since it +is a dictionary, hence all the rules for handling *dict*s are +applicable. It is strongly recommended to check for the presence of a key before +accessing it. Becuase it can always happen that statement callback function +which was supposed to *set the value* has not been invoked yet. This precaution +will help avoiding ``KeyError``. + +To be able to use the session dict we need to mention it in the callback +signature, else it will not be injected. + +We will use these concepts in the later part of the document to make things +clear. + +Dialogs Revisited +----------------- + +In this section we will cover two different ways the dialogs can be created. + +.. code-block:: python + :linenos: + + # as said, dialog is a list of statements + d = Dialog([ + Statement(pattern=r'^pat1', + action=first_callback, + args=dict(a=1), + loop_continue=True, + continue_timer=False), + Statement(pattern=r'^pat2', + action=second_callback, + args=None, + loop_continue=False, + continue_timer=False), + Statement(pattern=r'^pat3', + action=third_callback, + args=None, + loop_continue=True, + continue_timer=False), + ]) + +As we can see there is a lot of typing involved. We can also use a shorthand +notation. Same dialog can also be represented as follows. + +.. code-block:: python + :linenos: + + d = Dialog([ + [r'^pat1', first_callback, {'a':1}, True, False], + [r'^pat2', second_callback, None, False, False], + [r'^pat3', third_callback, None, True, False], + ]) + +Above style is a lot compact. Here we only need to provide arguments required +by the ``Statement`` class as a list. But while using above notation please +make sure to provide all the default arguments in case any of the default +values are changed. + +.. note:: + + Please make sure to have at least one statement in the dialog having its + ``loop_continue`` value as False, else the dialog will run into infinite + loop, till it times out. We can't call it a bug becuase sometimes it is a + desired feature. But almost always you will not want an infinite loop. + +Dialog Shorthand Notation +------------------------- + +.. versionadded:: 1.1.0 + +As you can see above, we are required to write callback function even for +very trivial operations like sending a character `y` or `yes`. Sometimes +writting even little lambda functions also cause a lot of clutter. + +It is good to know how callback functions work but for very trivial operations +you can use special string notation to get the the job done. For example if you +need to send a "yes" followed by a new line character. You can do it like this:: + + Dialog([ + [r'pattern', 'sendline(yes)', None, False, False] + ]) + +As you can see in the above example, you don't need to define `sendline` +function. We have more such *string based callbacks*. You can send any +string by changing the *string* inside the parenthesis. For example to +send `xx` you can write it as `sendline(xx)`. + +.. note:: + + Please make sure you don't use any quotations line `''` or `""` + inside the parenthesis. + +===================== ============================================== +String Callbacks Description +===================== ============================================== +sendline(x) sends the `x` followed by a new line character +send(x) sends the `x` without a new line character +send_ctx(x) sends the value in the context dict with key `x`, without a new line character. +sendline_ctx(x) sends the value in the context dict with key `x`, follwed by a new line character +send_session(x) sends the value in the session dict with key `x`, without a new line character. +sendline_session(x) sends the value in the session dict with key `x`, followed by a new line character. +sendline_cred_user(x) sends the username for credential with key `x`, followed by a new line character. +sendline_cred_pass(x) sends the password for credential with key `x`, followed by a new line character. +===================== ============================================== + +In the next section we would see how to use this in practice. + +Putting It All Together +----------------------- + +Let us now try to put all the above concepts to work. First we will try the +following assigenment:: + + login to the router to reach the disable prompt + +The program to handle this could look like this. We will call it our +**version 1**. + +.. literalinclude:: ../user_guide/examples/2_dialog_with_three_callbacks.py + :language: python + :linenos: + :emphasize-lines: 10-20 + +One thing we can quickly notice here, is that all the callback functions look a +like. In the first glance we can say that there is scope for some optimization. +Rather that writting three callback functions, all of which look alike, we can +improve it by using only one callaback function. + +Let's see our **version 2**, this is more `DRY`_ than the previous. + +.. literalinclude:: ../user_guide/examples/3_dialog_with_one_callback.py + :language: python + :linenos: + +But there is still room for improvement. In fact, our lone callback function +is essentially performing a very trivial task, i.e sending a command. We can +actually write it *inline* using *lambda functions*. Our **version 3** + +.. literalinclude:: ../user_guide/examples/4_using_lambda.py + :language: python + :linenos: + +Now let's use the *shorthand* notation which we learnt in the last section. +This can make the overall composition look even more compact and lucid. Here +is **version 4** + +.. literalinclude:: ../user_guide/examples/5_using_shorthand.py + :language: python + :linenos: + +Based on your preference you can use either of version 2 or 3 or 4. But we +will strongly recommed to use version *4*, i.e. the one which follows shorthand +notation, whenever and whereever possible. It reduces the chances or including +an erroneous callback function and also avoids code duplication. + +Using Session +------------- + +Now lets extend the problem a bit:: + + What if we have to take the router till enable mode, unlike the previous + example where we are only going till disable mode. + +In the first glance it may just look like a linear extension to the previous +problem, but it is not. It may tempt us to solve it by just adding one more +*statement* in the *dialog*. But notice the fact that *login password prompt* +and *enable password prompt* look the same. Hence the following statement will +trigger twice:: + + [r'password:\s?$', send_command, {'command': 'lab'}, False, False] + +But on the second occassion it has to send the enable password. We can't have +two statements having the same pattern in a dialog. We need to solve this by +doing something at the callaback level. Our callback must have a way to +understand whether it has been called for the first time or the second time, in +order to decide which password to send. Here we can use ``session`` to our +rescue. + +.. literalinclude:: ../user_guide/examples/6_using_session.py + :language: python + :linenos: + +Similar approch can be taken to solve situations where two callaback in two +different callabacks have to communicate with each other. ``session`` is unique +to the whole dialog context. + +The same code can be also we re-written using shorthand notation as follows. We +would recommed you to use this version over the one which was just illustrated. + +.. literalinclude:: ../user_guide/examples/7_using_shorthand_with_session.py + :language: python + :linenos: + +.. _prompt_recovery_label: + +Prompt Recovery Feature +------------------------ +Prompt recovery feature will try to recover device after normal dialog timeout occurs. This is just an attempt to bring device to stable state and this does not guarantee to bring device to stable state in every scenario. + +Use case: Once device booted up with image, console messages displayed over terminal, because of these console log messages over terminal unicon is unable to match the device prompt. Sending a enter key to device bring the device prompt at front and unicon matches device prompt. After reload, console messages can interfere with prompt matching, especially during reload and configuration operations + +This feature is available for Dialog, Connect and Services. + +**Usage** + +By default this feature is disabled. To enable it, use it in this way: + +.. code-block:: python + + Dialog.process(spawn, prompt_recovery=True) + # In Unicon + device = Connection(hostname='R1', start=['telnet x.x.x.x'], prompt_recovery=True] + device.connect() + # In pyATS + device.connect(prompt_recovery=True) + device.service(command, prompt_recovery=True) + +**Prompt recovery configurations** + +prompt_recovery can be configure using below 3 settings: + + * PROMPT_RECOVERY_COMMANDS : List of prompt recovery commands. + Default value: `['\\r', '\\025', '\\032', '\\r', '\\x1E']`. '\\025' is Ctrl-U, '\\032' is Ctrl-Z and '\\x1E' is Ctrl-^ + For Linux connection type default command list is: `['\\r', '\\x03', '\\r']` + `\\x03` is Ctrl-C. + * PROMPT_RECOVERY_INTERVAL : Timeout period after sending each prompt recovery command, in secs. + Default value: 10 secs + * PROMPT_RECOVERY_RETRIES : Number of prompt recovery retires to perform. + Default value: 1 + +Users can also alter these values at run time by setting these values as dialog context. + +Example: + +.. code-block:: python + + from unicon.utils import AttributeDict + ctx = AttributeDict() + ctx.prompt_recovery_interval = 30 + dialog.process(dev.spawn, context = ctx) + +``dialog`` is Dialog object. +``dev`` is device connection object. + +**Working of prompt recovery feature** + +When prompt_recovery is enable, below steps followed: + +#. After normal Dialog Timeout occurs. Unicon will not return Timeout exception at that moment, + it will try to recover it to known state. Here known state means, try to match all the patterns + in dialog again after sending `PROMPT_RECOVERY_COMMANDS` to device. +#. List of command which are set to `PROMPT_RECOVERY_COMMANDS` are send to device, one at a time + and new timeout period is set, value of this new timeout period is `PROMPT_RECOVERY_INTERVAL`. +#. After sending each `PROMPT_RECOVERY_COMMANDS` command, unicon waits if device comes to any known + stable state. If device comes to any of known stable state, Dialog processing is complete and + dialog process is considered as successful. +#. After sending all `PROMPT_RECOVERY_COMMANDS` commands to device, one at a time, if device does + not comes to known stable state then Timeout exception will be raised. +#. Step 2 will get repeated `PROMPT_RECOVERY_RETRIES` times. Example, Value of 1 to `PROMPT_RECOVERY_RETRIES` + means, all commands set to `PROMPT_RECOVERY_COMMANDS` will be sent to device once. If its set as 2, + then all commands will be send two times and the sequence of commands will be like below + +.. code-block:: text + + PROMPT_RECOVERY_COMMANDS = [cmd1, cmd2, cmd3] + PROMPT_RECOVERY_RETRIES = 2 + +Commands to device will be send in below sequence to device + +.. code-block:: text + + cmd1, cmd2, cmd3, cmd1, cmd2, cmd3 + + +.. log_user X +.. no transfer X +.. exp_internal X +.. changing sessions. X +.. using regexes with end marker X +.. Thread Saftey X + +.. _pty: https://docs.python.org/3.2/library/pty.html +.. _DRY: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself +.. _Tcl/Expect: http://www.tcl.tk/man/expect5.31/expect.1.html +.. _Telnetlib: https://docs.python.org/2/library/telnetlib.html +.. _Paramiko: http://www.paramiko.org +.. _Exscript: https://github.com/knipknap/exscript +.. _pexpect: https://pexpect.readthedocs.org diff --git a/docs/developer_guide/images/bench.jpg b/docs/developer_guide/images/bench.jpg new file mode 100644 index 00000000..75328f64 Binary files /dev/null and b/docs/developer_guide/images/bench.jpg differ diff --git a/docs/developer_guide/images/connection.jpeg b/docs/developer_guide/images/connection.jpeg new file mode 100644 index 00000000..41cb61c8 Binary files /dev/null and b/docs/developer_guide/images/connection.jpeg differ diff --git a/docs/developer_guide/images/statemachine.jpeg b/docs/developer_guide/images/statemachine.jpeg new file mode 100644 index 00000000..c7908a42 Binary files /dev/null and b/docs/developer_guide/images/statemachine.jpeg differ diff --git a/docs/developer_guide/ios_mock_data.yaml b/docs/developer_guide/ios_mock_data.yaml index b76834b2..b2c2cd3b 100644 --- a/docs/developer_guide/ios_mock_data.yaml +++ b/docs/developer_guide/ios_mock_data.yaml @@ -12,7 +12,7 @@ password: new_state: exec exec: - prompt: "Router> " + prompt: "%N> " commands: "show version": &SV | Cisco IOS Software, 7200 Software (C7200P-ADVENTERPRISEK9-M), Experimental Version 15.0(20100325:222114) [scube_alto-gclendon-alto_precollapse 221] @@ -76,7 +76,7 @@ exec: new_state: enable enable: - prompt: "Router#" + prompt: "%N#" commands: "term length 0": "" "term width 0": "" @@ -86,16 +86,19 @@ enable: config: - prompt: "Router(conf)#" + prompt: "%N(conf)#" commands: "no logging console": "" + "line vty 0 4": + new_state: config_line "line console 0": new_state: config_line config_line: - prompt: "Router(config-line)#" + prompt: "%N(config-line)#" commands: "exec-timeout 0": "" + "line vty 0 4": "" "end": new_state: enable diff --git a/docs/developer_guide/plugins.rst b/docs/developer_guide/plugins.rst index 53c11683..97a5fdc0 100644 --- a/docs/developer_guide/plugins.rst +++ b/docs/developer_guide/plugins.rst @@ -15,12 +15,12 @@ There are two methods of writing plugins for Unicon. 2. by writing your own package, which installs using ``pip`` and extends Unicon functionality without having to modify the core Unicon code. -Both methods have its pros & cons. See below for details. +Both methods have their pros & cons. See below for details. Contribution ------------ -Any plugins contributed to Unicon code under ``unicon.plugins repository``, +Any plugins contributed to Unicon code under the ``unicon.plugins`` repository, becomes part of Unicon. This is a great method to use if your plugin is generic, since it is installed automatically as part of every Unicon installation. @@ -30,16 +30,16 @@ verified by its developers, the next version of Unicon release will incorporate your plugin. Under this repository, Unicon follows a hierarchical directory structure for writing -plugins, which is distributes based on the OS, series, model of the platform -which the plugin implements. Any new OS implementations will contribute to a -new sub directory under ``unicon.plugins/plugins`` and its series/model will go under that +plugins, which is distributed based on the OS, platform, model of the platform +which the plugin implements. Any new OS implementations will contribute to a +new sub-directory under ``unicon.plugins/plugins`` and its platform/model will go under that. .. image:: images/plugins.jpg Unicon also has a generic plugin which implements the common behaviour seen across various platform. For any unknown or not implemented os, unicon loads -generic plugin and uses its `` Connection `` , also generic platform will be used as -a reference/starting point for new platform implementation +generic plugin and uses its `Connection`, also generic platform will be used as +a reference/starting point for new platform implementation. **Recommendations** : @@ -65,12 +65,12 @@ plugin separately. There are few major steps involved in creating your own plugin package: - 1. create the plugin module content following the instructions in this page - on how to create a plugin. + 1. create the plugin module content following the instructions on this page + on how to create a plugin. - .. note:: + .. note:: - make sure the ``__init__.py`` of your top-level package imports + make sure the ``__init__.py`` of your top-level package imports and/or contains the implemented ``Connection`` plugin class. 2. create the plugin package by writing a ``setup.py`` setup script. There @@ -86,7 +86,7 @@ There are few major steps involved in creating your own plugin package: ) and replace ```` with your platform's string name, and - ```` being the name of plugin module you developed. + ```` being the name of plugin module you developed. .. note:: @@ -109,12 +109,12 @@ There are few major steps involved in creating your own plugin package: ) And voila! Once your plugin is installed (either via ``pip install`` or -``python setup.py develop`` for development mode), it will be loaded +``python setup.py develop`` for development mode), it will be loaded automatically by Unicon. .. _Writing a Setup Script: https://docs.python.org/3/distutils/setupscript.html -For more details, follow the detailed Unicon plugin example +For more details, follow the detailed Unicon plugin example presented at https://github.com/CiscoDevNet/pyats-plugin-examples. Implementing a New Platform @@ -123,52 +123,55 @@ Implementing a New Platform Creating a Unicon plugin for a new platform can be sub divided into four main steps, - * Creating a Connection Class:- - Defines all the attributes required for this connection. - * Writing Connection Provider:- - Provides methods to connect and disconnect this platform - * Creating State Machine:- - Defines all the supported states for this platform and handles state transitions - * Creating all required Services:- - Defines all the supported services for this platform +* Creating a Connection Class: + * Defines all the attributes required for this connection. +* Writing Connection Provider: + * Provides methods to connect and disconnect this platform +* Creating State Machine: + * Defines all the supported states for this platform and handles state transitions +* Creating all required Services: + * Defines all the supported services for this platform Connection class ---------------- Connection class serves as the starting point for the device connection. -Unicon PluginManager bases on the platform to create the right connection class, +Unicon PluginManager is based on the platform to create the right connection class, which in turn initializes all its required components, such as connection provider, state machine, supported services and etc. -Users implementing new platform has define connection class, with the required -parameters which are listed below in this section, new connection class +Users implementing a new platform have to define a ``Connection class``, with the required +parameters which are listed below in this section. The new ``Connection`` class should satisfy the following conditions - * It should be subclass(direct or indirect) of ``Connection`` or ``BaseSingleRpConnection`` or ``BaseDualRpConnection`` + * It should be subclass (direct or indirect) of ``Connection``, ``BaseSingleRpConnection`` or ``BaseDualRpConnection`` - * Connection class follows class hierarchy which are aligned/derived according to the os, series and model + * ``Connection`` follows class hierarchy which is aligned/derived according to the os, platform and model - * Based the chasis type there should be separate definition of the class + * Based the chassis type, there should be a separate definition of the class -Connection class takes the following mandatory parameters +The ``Connection`` class takes the following mandatory parameters - * os = OS for which the implementation is intended - * series = Platform series of this implementation - * model = Model which this implementation supports - * chassis_type = Hardware chassis type single_rp, dual_rp or stack - * connection_provider_class = Class which implements actual step for - connecting to a device - * state_machine_class = State machine to be used - * subcommand_list = List of subcommand supported - * settings = Settings to be used for this connection +========================= ======================================== +Parameter Description +========================= ======================================== +os OS for which the implementation is intended +platform Platform of this implementation +model Model which this implementation supports +chassis_type Hardware chassis type single_rp, dual_rp or stack +connection_provider_class Class which implements actual step for connecting to a device +state_machine_class State machine to be used +subcommand_list List of subcommand supported +settings Settings to be used for this connection +========================= ======================================== -os and chassis_type of the implementation has to be mentioned in the connection. +``os`` and ``chassis_type`` of the implementation has to be mentioned in the connection. .. code-block:: python # Example Connection class Nxos single Rp connection class NxosSingleRpConnection(BaseSingleRpConnection): os = 'nxos' - series = None + platform = None chassis_type = 'single_rp' state_machine_class = NxosSingleRpStateMachine connection_provider_class = NxosSingleRpConnectionProvider @@ -178,33 +181,34 @@ os and chassis_type of the implementation has to be mentioned in the connection. # Example Connection class Nxos Dual Rp connection class NxosDualRPConnection(BaseDualRpConnection): os = 'nxos' - series = None + platform = None chassis_type = 'dual_rp' state_machine_class = NxosDualRpStateMachine connection_provider_class = NxosDualRpConnectionProvider subcommand_list = HANxosServiceList settings = NxosSettings() -Base Connection (e.g `BaseSingleRpConnection` and `BaseDualRpConnection`) classes of -unicon defines the workflow of connection and it satisfies all common needs of -router connection, user may not need to override any of the method unless there is +Base Connection (e.g `BaseSingleRpConnection` +and `BaseDualRpConnection`) classes of +unicon defines the workflow of ``Connection`` and it satisfies all common needs of +router connection, the user may not need to override any of the methods unless there is specific scenario to handle. Connection Provider ------------------- -The connection class for any platform depends on connection provider for initiation a -connection. As the name suggests their role is to provide a method to let the +The connection class for any platform depends on the connection provider for initiating a +connection. As the name suggests, their role is to provide a method to let the application connect and disconnect to the device. -This class provides two essential methods namely connect and disconnect. -Connect method defines all the steps involved in connection process, which are +This class provides two essential methods, namely ``connect`` and ``disconnect``. +The ``connect`` method defines all the steps involved in the connection process, which are defined as separate methods. These steps vary depending on the chassis type and the device, changing the behaviour of these can be achieved by overriding the method corresponding to each step. -In the case of singleRP the steps involved in connection process are +In the case of singleRP the steps involved in the connection process are: 1. get_connection_dialog 2. establish_connection 3. init_handle @@ -212,8 +216,8 @@ In the case of singleRP the steps involved in connection process are This is handled by the `BaseSingleRpConnectionProvider` class. -Whereas DualRp does few additional step like designate handles, initialize/unlock -standby and assign ha mode. +Whereas DualRp does a few additional steps like designate handles, initialize/unlock +standby, and assign ha mode. This is handled by the `BaseDualRpConnectionProvider` class. @@ -221,30 +225,30 @@ standby and assign ha mode. Pattern ------- -For all patterns used by match_buffer, eg. dialog, statemachine, expect, +For all patterns used by ``match_buffer``, eg. dialog, statemachine, expect, by default, pty_backend match_buffer will detect the match mode. It can be turned off by passing match_mode_detect=False to spawn or by changing settings. Rules: -1. search whole buffer with re.DOTALL if: +1. search the whole buffer with re.DOTALL if: -- pattern contains any of: r'\n', r'\r', . -- pattern equals to any of: r'.*', r'^.*$', r'.*$', r'^.*', r'.+', r'^.+$', r'.+$', r'^.+' + - pattern contains any of: r'\n', r'\r', . + - pattern equals to any of: r'.*', r'^.*$', r'.*$', r'^.*', r'.+', r'^.+$', r'.+$', r'^.+' -2. If pattern ends with '$' but not r'\$', match_buffer will only match last line +2. If the pattern ends with '$' but not r'\\$', match_buffer will only match the last line -3. In other situations, search whole buffer with re.DOTALL +3. In other situations, search the whole buffer with re.DOTALL StateMachine ------------ -State machine class holds the details of all supported states for a platform +The State Machine class holds the details of all supported states for a platform and handles the transition of the device to different states. -Each platform has their own state machine class. State machine class provides -a create method where all the device states have to be created. -State Machine should be sub class of ``StateMachine`` class from +Each platform has their own state machine class. The State Machine class provides +a ``create`` method where all the device states have to be created. +The State Machine should be sub class of ``StateMachine`` class from ``unicon.statemachine`` .. code-block:: python @@ -260,9 +264,10 @@ State Machine should be sub class of ``StateMachine`` class from self.create_path(enable, config, 'config term', None) self.create_path(config, enable, 'end', None) -For more detailed document on state machine refer - - add link state machine detail document here +.. + Add link to detailed documentation here + For more detailed document on state machine refer + Creating New Services --------------------- @@ -271,11 +276,11 @@ Refer detailed document :ref:`new-service-creation` Settings -------- -Unicon Connection behavior can changed by modifying its settings. The default +Unicon Connection behavior can be changed by modifying its settings. The default settings for unicon is 'unicon.setting.Settings', users can inherit and -change this settings if they wish to provide any platform or plugin level -setting. Unicon connection class takes an additional input settings, which -can be used to provide plugin/platform level settings +change these settings if they wish to provide any platform or plugin level +setting. Unicon ``Connection`` class takes additional input settings, which +can be used to provide plugin/platform level settings. .. code-block:: python @@ -289,11 +294,11 @@ can be used to provide plugin/platform level settings **Recommendations** : - * We strictly recommend to follow generic plugins file and class structure + * We strictly recommend to follow the generic plugins file and class structure while implementing your new platforms. - * Also its highly recommended to use the generic plugins Statemachine and services - as the base class for your implementations statemachine and services. + * It is also highly recommended to use the generic plugins Statemachine and services + as the base class for your implementation's statemachine and services. -Consider adding `DEFAULT_HOSTNAME_PATTERN` attribute for `Settings` object for +Consider adding the `DEFAULT_HOSTNAME_PATTERN` attribute to the `Settings` object for the `learn_hostname` feature to work. Refer :ref:`learn-hostname-feature`. diff --git a/docs/developer_guide/service_framework.rst b/docs/developer_guide/service_framework.rst new file mode 100644 index 00000000..8ae11765 --- /dev/null +++ b/docs/developer_guide/service_framework.rst @@ -0,0 +1,233 @@ +.. _new-service-creation: + +How to write a new Service +============================ + +Let us divide this task into 3 topics. + + 1. Steps involved in service implementation. + 2. Writing a sample service. + 3. How to attach a service to connection object. + +Steps involved in service implementation +----------------------------------------- + +We shall divide this implementation into 4 steps steps, which includes. + +1. What are pre-requisites I need to take care before running the service. +we call it as **'pre_service'**. One of them will be if connection is established, +before try to execute a service on the connection. Change the state of the device +to initial state for the service to be in. + +Before start coding pre_service, let us go through __init__ of BaseService Class. + * *connection* : Device connection object + + * *context* : Context info from user (more details we can get it from connection class) + + * *timeout_pattern* : Will have list of timeout patterns, I would like to match in device response after service execution. + + * *error_pattern* : Will have list of error patterns, I would like to match in device response after service execution. + + * *start_state* : Which state, device should be in before executing the service. + + * *end_state* : Which state, device should be after executing the service. + + * *result* : result attribute will have return response from device after service execution. Which will be used to evaluate the service result. + +.. code-block:: python + + def __init__(self, connection, context, **kwargs): + self.connection = connection + self.context = context + self.timeout_pattern = ['Timeout occurred', ] + self.error_pattern = ["my command error"] + self.start_state = 'enable' + self.end_state = 'enable' + self.result = None + self.__dict__.update(kwargs) + + + def pre_service(self, *args, **kwargs): + # Check if connection is established. If reconnect option is enabled + # then reconnect and execute the command, or raise error. + + if self.connection.is_connected: + return + elif self.connection.reconnect: + self.connection.connect() + else: + raise ConnectionError("Connection is not established to device") + + # Bring the device to required state to issue a command. + + self.connection.state_machine.go_to(self.start_state, + self.connection.spawn, + context=self.connection.context) + + + + +2. Actual service implementation goes here, we call it **'call_service'**. + +.. code-block:: python + + def call_service(self, command, dialog=Dialog([]) *args, **kwargs): + # Command to issue on device is `command` + con = self.connection + con.log.debug("+++ run_command +++ ") + con.spawn.sendline(command) + # self.result attribute will be used at result validation. + self.result = con.spawn.expect(.*#?) + + +.. note:: + + Any input object sent by the user calling your service, if not passed + directly to the ``send`` or ``sendline`` spawn method, must be properly + converted to a string form. Users are allowed to specify non-string + objects as input. + + Also, if your service accepts lists of objects, make sure you also + accept list-like objects that are instances of collections.Sequence. + + +3. **'post_service'** includes reverting the device to earlier state. One of +them will be bringing the device to end state of that service after service execution. +for example after reload service device must be in enable state. + +.. code-block:: python + + def post_service(self, *args, **kwargs): + # Bring the device back to end state. + self.connection.state_machine.go_to(self.end_state, + self.connection.spawn, + context=self.connection.context) + +4. Final step will be **'get_service_result'** will verify the self.result (response of each service) +with service error_pattern and timeout_pattern. If self.result doesn't match +any of the above pattern, service result will be considered pass or it +raises SubCommandFailure exception. + +.. code-block:: python + + def get_service_result(self): + # return True is self.result has string in it or raise exception. + + if re.search("xvy", self.result): + self.result = True + return self.result + else : + raise SubcommandFailure("xyz is not found in device response") + + +Writing a sample service +------------------------ + +For example I wanted to implement a service which issue given command in +*enable* mode and device and get the return response from device. Then return +the device back in *disable* mode. + +Also, if the command we are trying to run will prompt a dialog/take additional +input, Use **'Dialog'** (list of Statements) which are expected to prompt. + + +.. code-block:: python + + # Import BaseService + + from unicon.bases.routers.services import BaseService + from unicon.eal.dialogs import Dialog + + + class RunCommand(BaseService): + def __init__(self, connection, context, **kwargs): + self.connection = connection + self.context = context + self.timeout_pattern = ['Timeout', "Timed Out" ] + self.error_pattern = ["error", "abort"] + self.start_state = 'enable' + self.end_state = 'disable' + self.result = None + self.__dict__.update(kwargs) + + def pre_service(self, *args, **kwargs): + # Check if connection is established + if self.connection.is_connected: + return + elif self.connection.reconnect: + self.connection.connect() + else: + raise ConnectionError("Connection is not established to device") + + # Bring the device to required state to issue a command. + self.connection.state_machine.go_to(self.start_state, + self.connection.spawn, + context=self.connection.context) + + def call_service(self, command, + dialog=Dialog([]), + timeout=20, + *args, **kwargs): + # Command to issue on device is `command` + con = self.connection + con.log.debug("+++ run_command +++ ") + if dialog is None: + # Run command + con.spawn.sendline(command) + # self.result attribute will be used at result validation. + self.result = con.spawn.expect(.*#?) + else: + self.result = dialog.process(con.spawn, timeout=timeout) + + + def post_service(self, *args, **kwargs): + # Bring the device back to end state which is disable + self.connection.state_machine.go_to(self.end_state, + self.connection.spawn, + context=self.connection.context) + + def get_service_result(self): + # Base class get_service will verify error and timeout pattern and return + # self.result content if no error found. + pass + + +How to attach a service to connection object +-------------------------------------------- +Make an entry in the service list and pass on the service list to Connection class. + +.. code-block:: python + + from unicon.plugins.generic.services import ServiceList, HAServiceList + from .*. import implementation RunCommand + + class NxosServiceList(ServiceList): + def __init__(self): + super().__init__() + # Add the command defined to existing service list + self.run_command = RunCommand + + class NXOSConnection(BaseDualRpConnection): + os = 'nxos' + platform = None + chassis_type = 'dual_rp + state_machine_class = IosDualRpStateMachine + connection_provider_class = IosDualRpConnectionProvider + subcommand_list = NxosServiceList ; < update subcommand_list with new list defined + settings = IosConnectionSettings() + +Implementing prompt_recovery feature in service +----------------------------------------------- +To add prompt_recovery feature in plugin service, plugin developers can use prompt_recovery argument with `Dialog.process()` and `go_to()`. +`prompt_recovery` attribute for the service is set at the time of `pre_service` function. +If `pre_service` is implemented as part of service implementation then `super()` need to use in `pre_service`. + +**Prompt recovery configuration** + +Prompt recovery can configure using below three settings: + + * PROMPT_RECOVERY_COMMANDS : List of command which need to send after normal dialog timeout. It should be a list. Plugin developers can set or append values. New commands can be appended to `PROMPT_RECOVERY_COMMANDS` or can be overwritten with new value. + * PROMPT_RECOVERY_INTERVAL : Timeout period after sending each prompt recovery command, in secs. Type is int. Default value: 10 secs + * PROMPT_RECOVERY_RETRIES : Number of prompt recovery retires to perform. Type is int. Default value: 1 + +These settings should go in plugin settings file, so that platform specific values utilize. diff --git a/docs/developer_guide/statemachine.rst b/docs/developer_guide/statemachine.rst new file mode 100644 index 00000000..5d2a4c1f --- /dev/null +++ b/docs/developer_guide/statemachine.rst @@ -0,0 +1,275 @@ +State Machine +============= + +Statemachine is a major building block of a connection object. It enables the +connection handle to smoothly traverse across different *router states*. +This is how it fits into overall scheme of things. + +.. image:: images/connection.jpeg + +We define *router states* as different *router modes*, e.g. enable, disable, +config, rommon etc. Hence statemachine provides a software abstraction of all +the router modes, and it keeps the connection library always in sync with the +actual device mode. + +We need to implement the statemachine for all the platform implementations and +if this step is done correctly, we can safely assume that at least half of the +platform implementation is over. + +In this chapter we will go through the important APIs and using an example +device we will try to implement a working statemachine. + +Before you go further, please make sure you have gone through +:doc:`Expect Abstraction Library <../user_guide/eal>` + +.. note:: + + It is not mandatory that states must be a router mode. In dual + rp connections, we even treat *standby locked* also as one of the states. + It all depends on how do we want to abstract to the device in software. + +Structure +---------- + +The *statemachine* consists of following two things: + +* **States**: Individual states representing one of the router modes. +* **Paths**: Migration paths between the states. + +Following is the block diagram for the same. + +.. image:: images/statemachine.jpeg + +State +--------- + +As said in the previous section, it depicts one of the router modes. We identify +a router mode using the prompt pattern. For example this is how we can define +the enable state and disable state. + +.. code-block:: python + :linenos: + + from unicon.statemachine import State + # enable state + enable = State('enable', r'^.*(%N-standby|%N-sdby|%N)*#\s?#') + # disable state + disable = State('disable', r'^.*(%N-standby|%N-sdby|%N)*#\s?>') + # config state + config = State('config', r'^.(%N\(config\))#\s?') + +What is ``%N`` ? Since the hostname of the device is not known at the point of +creating states, hence it is a just a markup indicating hostname. All the ``%N`` +would be replaced by the actual hostname of the device during runtime. + +API Guide For State +------------------- + +.. autoclass:: unicon.statemachine.statemachine.State + :noindex: + :members: __init__, add_state_pattern, restore_state_pattern + +Path +---------- + +*Path* objects contain all the information for migrating from one state to +another. It requires the following arguments. + +* **from_state**: state object from which migration will start. (mandatory) +* **to_state**: state object to which migration will end. (mandatory) +* **command**: command required to initiate the migration. (mandatory) +* **dialog**: dialog object for negotiating any interaction because of *command* (optional) + +Continuing from the previous example, lets add a few ``Path``. + +.. code-block:: python + :linenos: + + from unicon.statemachine import State + disable_to_enable = Path(disable, enable, "enable", None) + enable_to_disable = Path(enable, disable, "disable", Dialog()) + enable_to_config = Path(enable, config, "config term", Dialog()) + config_to_enable = Path(config, enable, "end", None) + +Please note that ``Dialog`` in above example will be different in all the lines, +based on the nature of interaction caused by the ``command``, a blank ``Dialog`` +has been used just for example. + +The ``command`` option can also be a callable function. This can be used in case +a single command sent with `sendline()` is not sufficient. Below example +sends an escape character and quits a telnet session as part of a state transition. + +The arguments, `statemachine`, `spawn` and `context` are mandatory. + +.. code-block:: python + :linenos: + + def escape_telnet(statemachine, spawn, context): + spawn.send('~') + spawn.expect(r'telnet>\s?$', timeout=5) + spawn.sendline('q') + + module_console_to_chassis = Path(module_console, chassis, escape_telnet, None) + + +Statemachine +------------- + +To create a *statemachine* class, we need to subclass from ``StateMachine``, +which is the base class. This base class has all the relevant instrumentation +required for creating shortest paths between any two given states. It uses all +the ``Path`` instances to make way from any given state to any state. It +provides APIs required for state migration, which we shall see shortly. + +Let's create a sample *statemachine* class. All the ``State`` and the ``Path`` +instances, which we created above are eventually fed into the custom +statemachine class. + +.. code-block:: python + :linenos: + + from unicon.statemachine import StateMachine + from unicon.statemachine import State + from unicon.statemachine import Path + class MyStateMachine(StateMachine): + def create(self): + # enable state + enable = State('enable', r'^.*(%N-standby|%N-sdby|%N)*#\s?#') + # disable state + disable = State('disable', r'^.*(%N-standby|%N-sdby|%N)*#\s?>') + # config state + config = State('config', r'^.(%N\(config\))#\s?') + # create all the paths + disable_to_enable = Path(disable, enable, "enable", None) + enable_to_disable = Path(enable, disable, "disable", Dialog()) + enable_to_config = Path(enable, config, "config term", Dialog()) + config_to_enable = Path(config, enable, "end", None) + # add all the states to statemachine + self.add_state(enable) + self.add_state(disable) + self.add_state(config) + self.add_state(enable) + # add all the paths to statemachine + self.add_path(disable_to_enable) + self.add_path(enable_to_disable) + self.add_path(enable_to_config) + self.add_path(config_to_enable) + # at the time of creating statemachine instance you should be aware of the + # hostname of the device. + sm = MyStateMachine("") + +.. note:: + + Please note that we don't want same ``State`` and ``Path`` instances to be + reused by different *statemachine* classes. Hence we create ``State`` and + ``Path`` instances inside the scope of *statemachine* class. + + +Using The StateMachine +----------------------- + +Now that the *statemachine* instance is created, let's take it for a spin. +However, before we do so please ensure that you have already connected to the +device (statemachine doesn't know how to connect) and you have a ``spawn`` +instance. It can be used against any ``spawn`` instance which is already +connected to the device. + +Here is a sample run from the statemachine of a ``connection`` instance, +connected to a single rp IOS device. + +.. code-block:: python + :linenos: + + In [4]: sm = con.sm # con is the connection handle + In [5]: s = con.spawn + In [6]: # getting the current state + In [7]: sm.current_state + Out[7]: 'enable' + In [8]: # to list all the states in statemachine + In [9]: sm.states + Out[9]: [enable, disable, config, rommon] + In [10]: # to list all the paths, __str__ of paths have to tweaked for better legibility + In [11]: sm.paths + Out[11]: + [enable->disable, + enable->config, + enable->rommon, + disable->enable, + config->enable, + rommon->disable] + In [13]: # to get a state object by name. + In [14]: state = sm.get_state('enable') + In [15]: type(state) + Out[15]: unicon.statemachine.statemachine.State + In [16]: state + Out[16]: enable + In [17]: # to get a path by name + In [18]: path = sm.get_path('enable', 'disable') + In [19]: type(path) + Out[19]: unicon.statemachine.statemachine.Path + In [20]: path + Out[20]: enable->disable + In [28]: # to move to any given state + In [29]: sm.go_to('config', s) + config term + Enter configuration commands, one per line. End with CNTL/Z. + si-tvt-7200-28-41(config)# + In [30]: sm.go_to('enable', s) + end + si-tvt-7200-28-41# + In [31]: + In [31]: # go to any valid state in the state machine. + In [32]: sm.go_to('any', s) + '' + si-tvt-7200-28-41# + si-tvt-7200-28-41# + In [33]: sm.current_state + Out[33]: 'enable' + +.. _learn-hostname-feature: + +``learn_hostname`` feature support requirements +----------------------------------------------- + +Hostname pattern substitution using the ``%N`` markup is typically required +for routing and switching platforms that expect the hostname passed into the +unicon Connection object (or the pyATS device name being connected to) +to be already configured on the device in order for the state patterns +containing ``%N`` to match. + +This feature enables connection to a device with an unknown hostname +configured. It does so by analyzing the device prompt, learning a potentially +different hostname and then using that hostname for all subsequent +``%N`` markup substitutions. + +All state patterns containing a chain of ``%N`` hostname variants must +specify them in a most-to-least-specific manner in order for this feature +to work correctly. A hostname cannot be learned if none of the device's +state patterns contain the ``%N`` hostname markup. + +Here's a simplified example from the generic plugin patterns illustrating this. +Note the final pattern in the chain is simply ``%N`` :: + + self.enable_prompt = r'^(.*?)(%N-standby|%N-stby|%N)(\(boot\))*#\s?$' + +The connection provider object's ``establish_connection`` method is responsible +for driving this feature. + +If for some reason, ``learn_hostname`` is unable to detect hostname then unicon +compares unicon buffer with default hostname pattern set to ``Settings`` +Attribute ``DEFAULT_HOSTNAME_PATTERN``. Default value is +``r'RouterRP|Router|[Ss]witch|Controller|ios'`` + +API Guide For StateMachine +-------------------------- + +.. autoclass:: unicon.statemachine.StateMachine + :members: __init__, current_state, create_state, add_state, get_state, remove_state, create_path, add_path, get_path, remove_path, find_all_paths, get_shortest_path, go_to, create, add_default_statements + :noindex: + +.. Putting All Together +.. --------------------- + +.. Now based on the concepts learned above let's get our hands dirty with some real +.. code. From here on, we will try to create a statemachine class for one our +.. target platform. The target platform in this case is a router diff --git a/docs/developer_guide/unittests.rst b/docs/developer_guide/unittests.rst index 266d4982..c605d048 100644 --- a/docs/developer_guide/unittests.rst +++ b/docs/developer_guide/unittests.rst @@ -99,7 +99,7 @@ Running the mock device: **High Availability (HA) mock device** To create a High Availability (HA) mock device that simulates multiple RPs -or a stack of devices, you need to specifiy the '--ha' option with multiple +or a stack of devices, you need to specify the '--ha' option with multiple states specified using the '--state' option, separated by a comma, for example: @@ -235,6 +235,24 @@ of a line. The line and char interval timings are optional and can be omitted. - ",,," - ",,," + keys: + # same kind of response structure as commands + "": "" + "": |2 + + + # response with additional options + "": + + # (optional) state transition + new_state: + + # ... etc, see above + + # special keys: Control-X where X is one of 0ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_ + # example: ctrl-y + "ctrl-y": "Control Y pressed" + Example data: @@ -275,7 +293,8 @@ Create YAML data with the state, prompt and command(s) that you want to match. Note: the above example data is incomplete, see -:download:`ios_mock_data.yaml ` for all the data. +:download:`ios_mock_data.yaml ` +for a more complete example. Create a unittest that executes the mock device with the state that you created. diff --git a/docs/gen_dialogs_rst.py b/docs/gen_dialogs_rst.py index 28a27dc6..55d1e0c0 100644 --- a/docs/gen_dialogs_rst.py +++ b/docs/gen_dialogs_rst.py @@ -2,7 +2,8 @@ # Q&D Helper script to generate the ReStructered text file for the dialogs # prints to stdout -import os, sys +import os +import sys import unicon import traceback from unicon import Connection @@ -26,9 +27,9 @@ def find_plugins(): if len(p): plugin_attributes.os = p[0] if len(p) > 1: - plugin_attributes.series = p[1] + plugin_attributes.platform = p[1] else: - plugin_attributes.series = None + plugin_attributes.platform = None if len(p) > 2: plugin_attributes.model = p[2] else: @@ -94,14 +95,14 @@ def print_dialogs(service, dialogs): .. note:: This document is automatically generated and is intended to document - the default per-platform patterns used to match CLI dialogs for each + the default per-platform patterns used to match CLI dialogs for each plugin, and the corresponding action when a pattern is matched. """) def plugin_os(p): - if p.series: - return '%s%s' % (p.os, p.series) + if p.platform: + return '%s%s' % (p.os, p.platform) else: return p.os @@ -112,22 +113,22 @@ def plugin_os(p): plugin_name = p.os _os = p.os - if p.series: - plugin_name += "/%s" % p.series - series = p.series + if p.platform: + plugin_name += "/%s" % p.platform + platform = p.platform else: - series = None - + platform = None + try: - c = Connection(hostname='Router', start=['bash'], os=_os, series=series, log_stdout=False) - # c = Connection(hostname='Router', start=['bash'], os=_os, series=series) + c = Connection(hostname='Router', start=['bash'], os=_os, platform=platform, log_stdout=False) + # c = Connection(hostname='Router', start=['bash'], os=_os, platform=platform) c.init_service() c.connection_provider = c.connection_provider_class(c) - - except: - print('---------------- ERROR ---------------', file = sys.stderr) + + except Exception: + print('---------------- ERROR ---------------', file=sys.stderr) traceback.print_exc() - print('--------------------------------------', file = sys.stderr) + print('--------------------------------------', file=sys.stderr) else: print('\n\n') @@ -138,4 +139,11 @@ def plugin_os(p): print_dialogs('connect', c.connection_provider.get_connection_dialog()) - print_dialogs('execute', c.execute.dialog if c.execute.dialog else Dialog([])) + try: + print_dialogs('execute', c.execute.dialog if c.execute.dialog else Dialog([])) + if hasattr(c, 'configure'): + print_dialogs('configure', c.configure.dialog if c.configure.dialog else Dialog([])) + except Exception: + print('---------------- ERROR ---------------', file=sys.stderr) + traceback.print_exc() + print('--------------------------------------', file=sys.stderr) diff --git a/docs/index.rst b/docs/index.rst index 61bde194..edf2b2aa 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,69 +1,65 @@ -Unicon: Plugins -=============== +Unicon: The Connection Library +============================== -.. note:: +Unicon is a package aiming to provide a unified connection experience to network +devices through typical command-line management interface. By wrapping the +underlying session (eg, telnet, ssh), Unicon provides: - This is the developer documentation for Unicon plugins. +- direct and proxied connections through any common CLI interface (telnet, ssh, serial etc) +- power of expect-like programming without having to deal with low-level logic +- multi-vendor support through an agnostic API interface +- seamless handling of CLI modes (eg, enable, configure, admin-configure mode) +- rejected commands, command error detections +- value-add stateful services (specific to the platform) - This portion of documentation is specific to everything related to the - individual platform plugins (their services, apis, patterns). +and is extensible: platform supports and services are implemented via +open-source plugins. - This index file is not directly used in the final Unicon documentation hosted - on DevNet - only the sub-pages/content are. The final Unicon documentation - retroactively includes the user guides for these plugins. - - -Unicon is a framework for developing device control libraries for routers, -switches and servers. It is developed purely in Python. - -Unicon is the default connection class implementation used in Cisco pyATS -framework. In addition, Unicon is also test framework agnostic and can be used -with/without `Cisco pyATS`_. +Unicon is the standard, go-to CLI connection implementation for `Cisco pyATS`_ +framework. .. _Cisco pyATS: https://developer.cisco.com/site/pyats/ This package was initially developed internally in Cisco, and is now -available to the general public starting late 2017 through `Cisco DevNet`_. - -``unicon.plugins`` is plugins for different platforms. All the platform -implementations are arranged in a hierarchical fashion in order to provide -a good fault isolation. +released to the general public starting late 2017 through `Cisco DevNet`_. - https://developer.cisco.com/site/pyats/ + https://developer.cisco.com/pyats/ .. _Cisco DevNet: https://developer.cisco.com/ -------------------------------------------------------------------------------- -User Guide ----------- - .. toctree:: :maxdepth: 2 :caption: User Guide user_guide/introduction user_guide/supported_platforms + user_guide/connection + user_guide/passwords + user_guide/proxy user_guide/services/index user_guide/services/service_dialogs - -Developer Guide ---------------- + robot/index .. toctree:: - :maxdepth: 2 - :caption: Developer Guide + :maxdepth: 2 + :caption: Developer Guide - developer_guide/plugins - developer_guide/unittests + playback/index + developer_guide/plugins + developer_guide/service_framework + developer_guide/eal + developer_guide/statemachine + developer_guide/unittests -Change Log ----------- .. toctree:: - :maxdepth: 2 - :caption: Resources + :maxdepth: 2 + :caption: Resources - changelog/index + api/modules + changelog/index + changelog_plugins/index .. sectionauthor:: ATS Team diff --git a/docs/playback/index.rst b/docs/playback/index.rst new file mode 100644 index 00000000..e17092eb --- /dev/null +++ b/docs/playback/index.rst @@ -0,0 +1,156 @@ +Playback +======== + +Demos and devices, don't mix together. In the middle of a demo, the +device will react differently than expected just for the sake of it. + +`Unicon.playback` records all interaction with any device and can be +replayed later at any time. With this recording it is then possible to create a +:ref:`mock device`. A mock device is awesome! It gives you a python +device which can be connected to, and output show commands. Perfect for demo. + +Here are a few possible scenario with record/playback: + +* Create examples/demo/training with recorded device interaction, no need to have a device available! +* Reproduce parser, library, script issues without having the device available! +* Devices are not always available, record it once and it can be used forever! + +`Unicon.playback` is the perfect tool for training, reproduce complicated +issues in scripts and even just to manage your device availability. + +This is perfect when sending a bug report for certain tools where the device +interaction is needed. Record the session, and send the recorded directory. + +Replay can manipulate time, allowing re-run script much faster than it was +recorded. + +Here is a recording of it in action. + +.. raw:: html + + + +Record +------ + +At the end of your command line, add the record arguments. There is a single +argument which accepts a directory to store the recording. The recording +generates a pickle_ file per device. If the directory does not exist, it will +create it automatically. + +.. csv-table:: Record argument + :header: Argument, Description + :widths: 30, 70 + + ``--record``, "Directory where to store the recording" + +Here are a few examples on how to use it. + +.. code-block:: bash + + easypy jobfile.py -testbed_file mytestbed.yaml --record recording1 + python script.py -testbed_file mytestbed.yaml --record recording1 + +In case the dash argument cannot be used, environment variable +``UNICON_RECORD`` can be used instead. + +.. code-block:: bash + + export UNICON_RECORD=recording1 + +.. note:: + + There is currently a limitation with Pcall, only one device connection can + be recorded. + +Replay +------ + +Now you can use the recorded information instead of reserving the device. To +replay, add the replay argument. This will not connect to the devices but +instead use the recorded session. + +.. csv-table:: Replay argument + :header: Argument, Description + :widths: 30, 70 + + ``--replay``, "Directory where the stored recording is held" + ``--speed``, "Modify the speed of device interaction" + +Here a few examples on how to use it. + +.. code-block:: bash + + easypy jobfile.py -testbed_file mytestbed.yaml --replay recording1 + python script.py -testbed_file mytestbed.yaml --replay recording1 + + # Let's make it 4 times faster + easypy jobfile.py -testbed_file mytestbed.yaml --replay recording1 --speed 4 + + # Let's make it 4 times slower + easypy jobfile.py -testbed_file mytestbed.yaml --replay recording1 --speed .25 + +In case the dash argument cannot be used, environment variable +``UNICON_REPLAY`` and ``UNICON_REPLAY_SPEED`` can be used instead. + +.. code-block:: bash + + export UNICON_REPLAY=recording1 + export UNICON_REPLAY_SPEED=4 + +Mock Device +----------- + +Unicon provides the functionality to create a :ref:`mock device `. This +is driven by a yaml which can either be created manually or created dynamically +from a recording. + +.. code-block:: bash + + python -m unicon.playback.mock --recorded-data recorded/nx-osv-1 --output data/nxos/mock_data.yaml + +This file can then be used to create a mock device. + +.. code-block:: bash + + python -m unicon.mock.mock_device --os nxos --mock_data_dir data --state connect + +This provides a device which can be interacted and used in testscript. + +.. code-block:: bash + + connections: + defaults: + class: 'unicon.Unicon' + a: + command: mock_device_cli --os iosxe --mock_data_dir data --state connect + protocol: unknown + +Here is a recording on creating a mock with a big amount of show commands. + +.. raw:: html + + + +.. _pickle: https://docs.python.org/3/library/pickle.html + +By default, when a mock device is created, it will only store the first output of each command in the YAML file, regardless of the number of times the command was executed. +If you wish to record all the commands and to be able to execute them multiple times, you can do so by passing the argument ``--allow-repeated-commands``. + +.. code-block:: bash + + python -m unicon.playback.mock --recorded-data recorded/nx-osv-1 --output data/nxos/mock_data.yaml --allow-repeated-commands + +If you take a look at the resulting YAML file, you will notice that each stored command will have a structure similar to the one below: + +.. code-block:: yaml + + execute: + commands: + show interfaces GigabitEthernet1: + response: + - "GigabitEthernet1 is up, line protocol is up..." + - "GigabitEthernet1 is up, line protocol is up..." + response_type: circular + +With this yaml file you will never run out of outputs for this command as it will circle between the outputs every time the command is called. \ No newline at end of file diff --git a/docs/robot/index.rst b/docs/robot/index.rst new file mode 100644 index 00000000..72244777 --- /dev/null +++ b/docs/robot/index.rst @@ -0,0 +1,76 @@ +RobotFramework Support +====================== + +.. sidebar:: Quick References + + - `RobotFramework`_ + - `Unicon Keywords`_ + +.. _RobotFramework: http://robotframework.org/ +.. _Unicon Keywords: ../robot.html + +Robot Framework is generic Python/Java test automation framework that focuses +on acceptance test automation by through English-like keyword-driven test +approach. + +Starting Unicon v3.1.0, Robot Framework support has been added through the +optional ``robot`` sub-package under `Unicon.robot` namespace umbrella. This enables +RobotFramework users to leverage key aspects of Genie without having to reinvent +the wheel. Robot Framework libraries have also been added for pyATS and Genie. + +Installation +------------ + +Robot Framework support is an optional component under Unicon. To use it, you +must install this package explicitly: + +.. code-block:: bash + + pip install unicon[robot] + + +Features +-------- + +- Execute command on device +- Configure command on device +- Enable/Disable device output +- Set Unicon settings + +Keywords +-------- + +For the complete set of keywords supported by this package, refer to +`Unicon Keywords`_. + +Example +------- + +.. code-block:: robotframework + + # Example + # ------- + # + # Demonstration of Unicon Robot Framework Keywords + + *** Settings *** + Library ats.robot.pyATSRobot + Library unicon.robot.UniconRobot + + *** Test Cases *** + + Connect to device + use testbed "testbed.yaml" + # Remove default connection commands + set unicon setting "HA_INIT_CONFIG_COMMANDS" "" on device "nx-osv-1" + connect to device "uut" + + Execute command + execute "show devices list" on device "uut" + configure "router bgp 100" on device "uut" + + Execute command in parallel on multiple devices + execute "show devices list" in parallel on devices "uut" + + Disconnect from device + disconnect from device "uut" diff --git a/docs/user_guide/connection.rst b/docs/user_guide/connection.rst new file mode 100644 index 00000000..dcfbbd64 --- /dev/null +++ b/docs/user_guide/connection.rst @@ -0,0 +1,1617 @@ +Connection Basics +================= + +.. _unicon_connection: + + +There are two primary ways of creating device CLI connections using Unicon: + +1. using pyATS testbed YAML files +2. using native Python Unicon APIs + + +Using pyATS Testbed YAML +------------------------ + +The simplest way to create a device connection using Unicon is through a +pyATS testbed YAML file. + +The testbed YAML file contains all necessary information that instructs Unicon +on *how* a connection should be established (eg, using what parameters and +credentials). + +.. code-block:: yaml + + # Example + # ------- + # + # a simple pyATS testbed YAML file + + + devices: + csr1000v-1: + type: router + os: iosxe + credentials: + default: + password: cisco123 + username: admin + enable: + password: cisco123 + connections: + cli: + protocol: ssh + ip: 168.10.1.35 + +Note that in the above file, the following key values are used by Unicon +to identify the proper plugin to use to create the underlying connection: + + * ``os:`` - OS details of the device [required] + * ``platform:`` - platform of the the device [optional] + * ``model:`` - platform model of the device [optional] + +If an equivalent unicon connection plugin is not found for a device, unicon +will use the ``generic plugin``. + +.. tip:: + + The supported OS and platform information can be found here: `Supported Platforms`_. + + +.. _Supported Platforms: introduction.html#supported-platforms + +Now, you can establish connectivity to this device within your +test scripts, or within Python: + +.. code-block:: python + + # Example + # ------- + # + # using the above testbed yaml file in pyATS + + from pyats.topology import loader + + testbed = loader.load('my-testbed.yaml') + + device = testbed.devices['csr1000v-1'] + + device.connect() + + device.execute('show version') + +Customizing Your Connection +""""""""""""""""""""""""""" + +In order to allow users to tune unicon plugin behavior without the need for +any code changes, several kinds of overrides may be made from the testbed +YAML itself. + +Connecting to a pyATS device via unicon ultimately results in a unicon +:ref:`Connection` object being created. + +The following parameters are eligible for override under the ``arguments`` key +in the testbed YAML connection block: + +- ``learn_hostname`` +- ``prompt_recovery`` +- ``init_exec_commands`` +- ``init_config_commands`` +- ``mit`` +- ``log_stdout`` +- ``debug`` +- ``goto_enable`` +- ``standby_goto_enable`` + + +.. _settings_control: + +A connection, once created, has a ``settings`` parameter whose contents and +defaults are plugin-dependent. It is possible to override these settings in the +testbed YAML file via the ``settings`` key or by setting the values of the +connection object `settings` attribute. + +.. _controlled_settings: + +**Backend implementation** + +Unicon uses `telnetlib` for telnet connection and `ssh` unix client for telnet +and SSH connections respectively. This was changed from release 23.6 onwards. +Previous release would use `telnet` unix client by default. To switch to the +unix telnet client instead of using telnetlib, set the ``BACKEND`` setting to +`unicon.eal.backend.pty_backend` in the testbed yaml file. + +.. code-block:: yaml + + devices: + : + connections: + : + settings: + BACKEND: unicon.eal.backend.pty_backend # default is "auto" + + +**Error pattern handling** + +If you want to execute services that could fail to execute properly and you want to verify +this automatically using a specific error pattern, you can specify the `error_pattern` +option with a list of regular expressions to match on the output. This option is available +for the execute service. + +The regex pattern is matched using the python multiline option (re.M) so you can use +the start of line (`^`) character to match specific line output. + +.. code-block:: python + + >>> c.execute('show interface invalid', error_pattern=['^% Invalid']) + +If you want to avoid errors being detected with any command, you can set the settings object +`ERROR_PATTERN` to an empty list. The current generic default is an empty list. + +.. code-block:: python + + >>> from pyats.topology import loader + >>> + >>> tb = loader.load('testbed.yaml') + >>> ncs = tb.devices.ncs + >>> + >>> ncs.connect(via='cli') + >>> ncs.settings.ERROR_PATTERN=[] + +The default error patterns can be seen by printing the settings.ERROR_PATTERN attribute. + +.. code-block:: python + + >>> ncs.settings.ERROR_PATTERN + ['Error:', 'syntax error', 'Aborted', 'result false'] + +Alternatively, you can pass an empty list when executing a command to avoid error pattern checking. + +.. code-block:: python + + >>> c.execute('show command error', error_pattern=[]) + +You can also append a pattern to the existing patterns defined in the settings when executing a command +(e.g. to add an error pattern for a specific command to execute). + +.. code-block:: python + + >>> c.execute('show command error', append_error_pattern=['^specific error pattern']) + +**Environment variables** + +If you want to set environment variables for the connection, you can set them +by adding key-value pairs to the `ENV` dictionary. + +.. code-block:: python + + >>> uut.settings.ENV = {'MYENV': 'mystring'} + +**Terminal size settings** + +To set the terminal size (rows, cols) you can use the `ROWS` and `COLUMNS` +environment variables. The default terminal size is 24 x 80. Some plugins +like linux and nxos/aci have their own defaults. + +.. code-block:: python + + >>> uut.settings.ENV = {'ROWS': 200, 'COLUMNS': 200} + +**Printing matched patterns** + +If you want to print the dialog statements matched patterns during the run, +you need to set the log level to logging.DEBUG or connect with debug=True. + +Default value is False. + +.. code-block:: python + + >>> from pyats.topology import loader + >>> + >>> tb = loader.load('testbed.yaml') + >>> uut = tb.devices['uut'] + >>> + >>> uut.connect() + >>> uut.log.setLevel(logging.DEBUG) + +Alternative: + + >>> uut.connect(debug=True) + + +**Service attributes** + +A connection is assigned a plugin-dependent list of services when it is created. +It is possible to override any service attribute from the testbed YAML file +via the ``service_attributes`` key. + + +The following testbed YAML shows these three kinds of override: + +.. code-block:: yaml + + device1: + os: 'nxos' + platform: 'n7k' + type: 'router' + credentials: + default: + username: lab + password: lab + connections: + a: + protocol: telnet + ip: 10.64.70.11 + port: 2042 + + arguments: + connection_timeout: 120 + mit: True + + settings: + ESCAPE_CHAR_CHATTY_TERM_WAIT: 1 + + service_attributes: + ping: + timeout: 1234 + + +.. note :: + + Details specified under the ``arguments``, ``settings`` or + ``service_attributes`` connection block keys take + precedence over any identically-named details passed to the + ``device.connect()`` call. + + Using the above testbed YAML as an example: + + Calling ``device1.connect(connection_timeout=240)`` + results in ``device1.connection_timeout`` being set to 120. + + Calling + ``device1.connect(settings=dict(ESCAPE_CHAR_CHATTY_TERM_WAIT=10))`` + results in ``device1.settings.ESCAPE_CHAR_CHATTY_TERM_WAIT`` being set to 1. + + Calling ``device1.connect(service_attributes=dict(ping=dict(timeout=1)))`` + results in ``device1.ping.timeout`` being set to 1234. + + +If you want to change to default timeout value for execute and configure service, +you can set the ``EXEC_TIMEOUT`` and ``CONFIG_TIMEOUT`` in the testbed file: + +.. code-block:: yaml + + device1: + os: 'nxos' + platform: 'n7k' + type: 'router' + credentials: + default: + username: lab + password: lab + connections: + a: + protocol: telnet + ip: 10.64.70.11 + port: 2042 + + settings: + EXEC_TIMEOUT: 120 + CONFIG_TIMEOUT: 120 + + +**EOF Exception handling** + +If device connection is closed/terminated unexpectedly during service calling, we can reconnect +to device. EOF exception is raised by Spawn when connection is not available. + +Sample usage: + +.. code-block:: python + + from unicon.core.errors import EOF, SubCommandFailure + try: + d.execute(cmd) # or any service call. + except SubCommandFailure as e: + if isinstance(e.__cause__, EOF): + print('Connection closed, try reconnect') + d.disconnect() + d.connect() + + + +Example: Single NXOS +"""""""""""""""""""" + +Every other platform can use the same sample file by changing the os, platform, model. The Moonshine platform does not require a username or password, so +these are omitted (see below for an example). + +.. code-block:: yaml + + step-n7k-1: + os: 'nxos' + platform: 'n7k' + type: 'router' + credentials: + default: + username: lab + password: lab + connections: + defaults: + class: 'unicon.Unicon' + a: + protocol: telnet + ip: 10.64.70.11 + port: 2042 + +For more info on testbed refer to :ref:`topology` package. + + +**Connecting to the device using the above testbed file:** + +.. note:: + + unicon Connection arguments may be passed in the pyATS + ``device.connect()``. For example: ``device.connect(learn_hostname=True)``. + + + +.. code-block:: python + + >>> from pyats.topology import loader + >>> tb = loader.load("testbed.yaml") + >>> uut = tb.devices['step-n7k-1'] + >>> uut.connect() + + 2016-04-06T12:06:50: %UNICON-INFO: +++ initializing context +++ + + 2016-04-06T12:06:50: %UNICON-INFO: +++ initializing state_machine +++ + + 2016-04-06T12:06:50: %UNICON-INFO: +++ initializing services +++ + + 2016-04-06T12:06:50: %UNICON-INFO: adding service ping : + + 2016-04-06T12:06:50: %UNICON-INFO: adding service reload : + + 2016-04-06T12:06:50: %UNICON-INFO: adding service sendline : + + 2016-04-06T12:06:50: %UNICON-INFO: adding service list_vdc : + + 2016-04-06T12:06:50: %UNICON-INFO: adding service copy : + + 2016-04-06T12:06:50: %UNICON-INFO: adding service switchto : + + 2016-04-06T12:06:50: %UNICON-INFO: adding service disable : + + 2016-04-06T12:06:50: %UNICON-INFO: adding service send : + + 2016-04-06T12:06:50: %UNICON-INFO: adding service delete_vdc : + + 2016-04-06T12:06:50: %UNICON-INFO: adding service ping6 : + + 2016-04-06T12:06:50: %UNICON-INFO: adding service execute : + + 2016-04-06T12:06:50: %UNICON-INFO: adding service enable : + + 2016-04-06T12:06:50: %UNICON-INFO: adding service shellexec : + + 2016-04-06T12:06:50: %UNICON-INFO: adding service switchback : + + 2016-04-06T12:06:50: %UNICON-INFO: adding service config : + + 2016-04-06T12:06:50: %UNICON-INFO: adding service create_vdc : + + 2016-04-06T12:06:50: %UNICON-INFO: adding service expect : + + 2016-04-06T12:06:50: %UNICON-INFO: adding service log_user : + + 2016-04-06T12:06:50: %UNICON-INFO: connection to step-n7k-1 + + 2016-04-06T12:06:50: %UNICON-INFO: +++ connection to spawn_command: telnet 10.64.70.24 2061, id: 4358177400 +++ + + 2016-04-06T12:06:50: %UNICON-INFO: telnet 10.64.70.24 2061 + Trying 10.64.70.24... + Connected to ts-nostg-mm18.cisco.com. + Escape character is '^]'. + + step-n7k-1# + 2016-04-06T12:06:51: %UNICON-INFO: +++ initializing handle +++ + + 2016-04-06T12:06:51: %UNICON-INFO: +++ execute +++ + term length 0 + step-n7k-1# + 2016-04-06T12:06:51: %UNICON-INFO: +++ execute +++ + term width 511 + step-n7k-1# + 2016-04-06T12:06:51: %UNICON-INFO: +++ execute +++ + terminal session-timeout 0 + step-n7k-1# + 2016-04-06T12:06:51: %UNICON-INFO: +++ config +++ + config term + Enter configuration commands, one per line. End with CNTL/Z. + step-n7k-1(config)# no logging console + step-n7k-1(config)# line console + step-n7k-1(config-console)# exec-timeout 0 + step-n7k-1(config-console)# terminal width 511 + step-n7k-1(config-console)# end + step-n7k-1# + + +Example: Linux Server +""""""""""""""""""""" + +Specifying linux device in testbed file template is almost the same as router template, except Unicon +looks for `linux` block in the device details and os has to be mentioned as `linux` + +.. code-block:: yaml + + mohamoha-ads: + os: 'linux' + credentials: + default: + username: admin + password: password + connections: + defaults: + class: 'unicon.Unicon' + linux: + protocol: ssh + ip: mohamoha-ads + type: 'linux' + + +**Connecting to linux machine using above testbed file:** + +.. code-block:: python + + >>> from pyats.topology import loader + >>> tb = loader.load("testbed.yaml") + + >>> server = tb.devices['mohamoha-ads'] + + >>> server.connect() + + 2016-04-06T12:10:49: %UNICON-INFO: +++ initializing context +++ + + 2016-04-06T12:10:49: %UNICON-INFO: +++ initializing state_machine +++ + + 2016-04-06T12:10:49: %UNICON-INFO: +++ initializing services +++ + + 2016-04-06T12:10:49: %UNICON-INFO: adding service send : + + 2016-04-06T12:10:49: %UNICON-INFO: adding service execute : + + 2016-04-06T12:10:49: %UNICON-INFO: adding service sendline : + + 2016-04-06T12:10:49: %UNICON-INFO: adding service expect : + + 2016-04-06T12:10:49: %UNICON-INFO: adding service log_user : + + 2016-04-06T12:10:49: %UNICON-INFO: connection to mohamoha-ads + + 2016-04-06T12:10:49: %UNICON-INFO: +++ connection to spawn_command: ssh -l mohamoha 64.103.223.250, id: 4366516064 +++ + + 2016-04-06T12:10:49: %UNICON-INFO: ssh -l mohamoha 64.103.223.250 + + Last login: Mon Apr 4 16:12:21 2016 from 10.232.8.212 + Cisco Linux 5.50-5Server Kickstarted on: Sat Jun 13 05:53:15 PDT 2009. + + bgl-ads-842:129> + 2016-04-06T12:10:49: %UNICON-INFO: +++ initializing handle +++ + + 2016-04-06T12:10:49: %UNICON-INFO: Attaching all Subcommands + +**Connection to Linux with additional SSH options:** + +If you want the linux connection to take additional ssh options, then it's better +to use `command` key. Unicon will take the value of `command` and spawns. +Command value should be the complete command to be spawned. + +.. code-block:: yaml + + mohamoha-ads: + os: 'linux' + credentials: + default: + username: admin + password: password + connections: + defaults: + class: 'unicon.Unicon' + linux: + command: 'ssh -l admin 10.1.1.1 -oHostKeyAlgorithms=+ssh-dss' + type: 'linux' + + +**Connecting to another TCP port using SSH:** + +If you want to connect to another port with SSH, you can use the port option in the testbed file: + +.. code-block:: yaml + + lnx-vm: + os: 'linux' + credentials: + default: + username: admin + password: password + connections: + defaults: + class: 'unicon.Unicon' + linux: + protocol: ssh + ip: 10.1.1.1 + port: 2200 + type: 'linux' + + + +Example: Moonshine +"""""""""""""""""" + +.. _unicon user_guide connection moonshine: + +Specifying a Moonshine device in the testbed file template is again very similar to the above examples, +except Unicon looks for the `iosxr` os and `moonshine` type and platform, and no username or password is +required. + +.. code-block:: yaml + + bringup: + xrut: + base_dir: /auto/xrut/xrut-gold + sim_dir: /path/to/my/xrut/sim/dir + devices: + moonshine-1: + os: iosxr + platform: moonshine + type: moonshine + credentials: + default: + username: admin + password: password + connections: + defaults: {class: unicon.XRUTConnect} + a: {protocol: xrutconnect} + +Please note that devices using the xrutconnect protocol should specify the default connection class as +unicon.XRUTConnect. + +For information on how to create such a testbed file via the `xrutbringup` command, passing in a logical +testbed file and a clean.yaml file, please see :ref:`dyntopo xrut working examples moonshine` . + + +Example: NSO +"""""""""""" + +.. _unicon user_guide connection nso: + +To connect to the Network Service Orchestrator CLI via SSH, use the 'nso' OS type and specify the +ssh port under the connection details. + +.. code-block:: yaml + + # example testbed.yaml file for NSO CLI + devices: + ncs: + os: nso + credentials: + default: + username: admin + password: password + connections: + defaults: + class: unicon.Unicon + via: cli + con: + command: ncs_cli -C + cli: + credentials: + nso: + username: admin + password: cisco1234 + login_creds: nso + protocol: ssh + ip: 127.0.0.1 + port: 2024 + + + +**Connecting to NSO CLI via SSH using above testbed file:** + +As shown in the example below, use the connect() method to initiate the connection, +specify the 'via' option if no default is configured under the connection defaults. + +The ncs.conf configuration file section for the SSH service for NSO is shown below. + +.. code-block:: xml + + + true + + + + + true + 0.0.0.0 + 2024 + + + +This example uses the 'cli' connection which initiates a SSH session the to default port of the NSO SSH service. + +.. code-block:: python + + >>> from pyats.topology import loader + >>> tb = loader.load("testbed.yaml") + + >>> ncs = tb.devices.ncs + + >>> ncs.connect(via='cli') + + 2017-06-02T08:15:55: %UNICON-INFO: +++ initializing context +++ + + 2017-06-02T08:15:55: %UNICON-INFO: +++ initializing state_machine +++ + + 2017-06-02T08:15:55: %UNICON-INFO: +++ initializing services +++ + + 2017-06-02T08:15:55: %UNICON-INFO: adding service execute : + + 2017-06-02T08:15:55: %UNICON-INFO: adding service cli_style : + + 2017-06-02T08:15:55: %UNICON-INFO: adding service log_user : + + 2017-06-02T08:15:55: %UNICON-INFO: adding service sendline : + + 2017-06-02T08:15:55: %UNICON-INFO: adding service expect : + + 2017-06-02T08:15:55: %UNICON-INFO: adding service configure : + + 2017-06-02T08:15:55: %UNICON-INFO: adding service send : + + 2017-06-02T08:15:55: %UNICON-INFO: connection to ncs + + 2017-06-02T08:15:55: %UNICON-INFO: +++ connection to spawn_command: ssh -l admin 127.0.0.1 -p 2024, id: 140683073268704 +++ + + 2017-06-02T08:15:55: %UNICON-INFO: ssh -l admin 127.0.0.1 -p 2024 + admin@127.0.0.1's password: + + admin connected from 127.0.0.1 using ssh on nso-dev-server + admin@ncs# + 2017-06-02T08:15:55: %UNICON-INFO: +++ initializing handle +++ + + 2017-06-02T08:15:55: %UNICON-INFO: +++ None +++ + paginate false + admin@ncs# + 2017-06-02T08:15:55: %UNICON-INFO: +++ execute +++ + screen-length 0 + admin@ncs# + 2017-06-02T08:15:55: %UNICON-INFO: +++ execute +++ + screen-width 0 + admin@ncs# + 2017-06-02T08:15:55: %UNICON-INFO: Attaching all Subcommands + >>> + + + +**Connecting to NSO CLI via ncs_cli command using above testbed file** + +It is also possible to run the ncs_cli command to initiate the CLI session, +use the 'command' option in the testbed.yaml file to specify the ncs_cli command. + +Specify the 'via' option if the default is not specified in the connection defaults. + + +.. code-block:: python + + >>> ncs.connect(via='con') + + 2017-06-02T08:19:19: %UNICON-INFO: +++ initializing context +++ + + 2017-06-02T08:19:19: %UNICON-INFO: +++ initializing state_machine +++ + + 2017-06-02T08:19:19: %UNICON-INFO: +++ initializing services +++ + + 2017-06-02T08:19:19: %UNICON-INFO: adding service send : + + 2017-06-02T08:19:19: %UNICON-INFO: adding service log_user : + + 2017-06-02T08:19:19: %UNICON-INFO: adding service configure : + + 2017-06-02T08:19:19: %UNICON-INFO: adding service cli_style : + + 2017-06-02T08:19:19: %UNICON-INFO: adding service execute : + + 2017-06-02T08:19:19: %UNICON-INFO: adding service sendline : + + 2017-06-02T08:19:19: %UNICON-INFO: adding service expect : + + 2017-06-02T08:19:19: %UNICON-INFO: connection to ncs + + 2017-06-02T08:19:19: %UNICON-INFO: +++ connection to spawn_command: ncs_cli -C, id: 140374808144136 +++ + + 2017-06-02T08:19:19: %UNICON-INFO: ncs_cli -C + + dwapstra connected from 10.0.2.2 using ssh on nso-dev-server + dwapstra@ncs# + 2017-06-02T08:19:19: %UNICON-INFO: +++ initializing handle +++ + + 2017-06-02T08:19:19: %UNICON-INFO: +++ None +++ + paginate false + dwapstra@ncs# + 2017-06-02T08:19:19: %UNICON-INFO: +++ execute +++ + screen-length 0 + dwapstra@ncs# + 2017-06-02T08:19:19: %UNICON-INFO: +++ execute +++ + screen-width 0 + dwapstra@ncs# + 2017-06-02T08:19:19: %UNICON-INFO: Attaching all Subcommands + + + +Example: ConfD +"""""""""""""" + +.. _unicon user_guide connection confd: + +To connect to ConfD based CLI via SSH, use the 'confd' OS type and specify the +ssh port (if needed) under the connection details. + +For NSO, the 'os' needs to be specified, 'platform' can be omitted. +For CSP, ESC and NFVIS, the 'platform' needs to be specified. + +.. code-block:: yaml + + # example testbed.yaml file for NSO CLI + devices: + ncs: + os: confd + type: router + # platform: 'csp', 'esc' or 'nfvis' + credentials: + default: + username: admin + password: cisco1234 + connections: + defaults: + class: unicon.Unicon + via: cli + cli: + protocol: ssh + ip: 127.0.0.1 + port: 2024 + + + +Example: VOS +"""""""""""" + +.. _unicon user_guide connection vos: + +To connect to Cisco Unified Collaboration based CLI via SSH, use the 'vos' OS type and specify the +ssh port (if needed) under the connection details. + +.. code-block:: yaml + + # example testbed.yaml file for VOS CLI + devices: + cm: + os: vos + type: server + credentials: + default: + username: admin + password: cisco1234 + connections: + defaults: + class: unicon.Unicon + via: cli + cli: + protocol: ssh + ip: 10.0.0.1 + port: 22 + + +pyATS Connection Pool +--------------------- +Unicon (IOSXE, NXOS and IOSXR) plugins now support creating a pool of shareable +connections to be distributed among device action requests promoting speed and +avoiding race condition and deadlocks. + +.. code-block:: python + + # Example + # ------- + # + # Connection pool using unicon.Unicon class example + # Assuming we have a device that is defined in the testbed yaml file as above + + # using the above device, create a pool of 5 workers + >>> device.start_pool(alias = 'pool', ----- > Connection pool will be accessed as "device.pool" + via = 'mgmt', ----- > Connection name as in testbed yaml + size = 5) + + # Now all action requests sent to the device will run simultaneously on the + # 5 connections (knows as workers) on a first come first serve basis. + +Check here for more details on pyATS `Connection Pool`_ feature. + +.. _Connection Pool: https://pubhub.devnetcloud.com/media/pyats/docs/connections/sharing.html#connection-pools + + + +Python APIs +----------- + +This section covers how to connect to a device in standalone mode, using raw +Python APIs directly. + +To connect to a device, you need. + * IP address + * Hostname + * OS + * Credentials + +Please make sure that device is up and booted. In the following +example, we are establishing connection to a *dual rp* NXOS device. + +.. code-block:: python + + from unicon import Connection + dev = Connection(hostname='n7k2-1', + start=['telnet 172.27.114.43 2037', + 'telnet 172.27.114.43 2038'], + credentials={'default': {'username': 'admin', 'password': 'Cisc0123'}}, + os='nxos') + dev.connect() + +Arguments: + + * **hostname**: must be same as the exact hostname of the device. + Do not append prompt characters like '#' or '$' + + * **os**: The os of the device to connect to. This selects a unicon plugin. + + * **start**: It must be a list of commands which needs to be invoked for starting a connection. + Generally it will be of the format `telnet xxx xxx`. But it could take any value. + + * **credentials**: A dictionary of named credentials used to interact with the device. + + * **platform**: The platform of the device to connect to. This selects a + unicon sub-plugin under the given plugin identified with the ``os`` + argument. *(Optional)* + + * **model**: The model of the device to connect to. This selects a + unicon sub-sub-plugin under the given plugin identified with the ``os`` + and ``platform`` arguments. *(Optional)* + + * **connection_timeout**: Connection timeout value to connect the device. + Default value is ``60 sec``. *(Optional)* + + * **proxy_connections**: Connection object which is use to establish proxy connection. + Default value is ``None``. *(Optional)* + + * **alias**: Connection alias. Default value is ``None``. *(Optional)* + + * **login_creds**: A single credential name or a list of credentials for + authenticating against the device. Default value is ``default``. *(Optional)* + + * **cred_action**: A dictionary with credential names and post password action statement. + This allows the user to specify e.g. `sendline` to be sent after a credential password. + The typical use case is a terminal server connection where a return will get a response + from the device. *(Optional)* + + * **learn_hostname**: Set to `True` if the actual hostname set on the device + differs from the hostname parameter. *(Optional)* + + * **learn_os**: Set to `True` if the device os is not provided, it will try to + learn the device os and redirect to the learned plugin. *(Optional)* + + * **prompt_recovery**: Set `True` for using prompt recovery feature. Default value is `False`. + Click :ref:`prompt_recovery_label` for more information on the feature. *(Optional)* + + * **init_exec_commands**: List of exec commands to use when initializing the connection. + This option overrules the default settings for the plugin and uses the + user specified initialization commands. Can also be passed in the + connection block in the yaml file. *(Optional)* + + * **init_config_commands**: List of config commands to use when initializing the connection. + This option overrules the default settings for the plugin and uses the user specified initialization commands. + Config commands will not be executed on the standby RP. + Config commands are not available on Linux and ISE plugins. Can also be + passed in the connection block in the yaml file. *(Optional)* + + * **logfile**: Filename to log all device interaction to. By default, a file will + be created in /tmp based on the hostname, via (if specified) and timestamp. *(Optional)* + + * **log_buffer**: Set to `True` to use a log_buffer instead of a logfile, no logfile will be created. + The log buffer can be accessed via connection.log_buffer attribute. *(Optional)* + + * **mit**: Boolean option to maintain initial state on connect. The state detected + on connect() is maintained, no connection initialization is done and the + exec and config initialization commands are not executed. It is possible to use + the `mit` option with HA connections, however please note that HA initialization is not done. + Default is False. For more info on device state, see :doc:`Statemachine <../developer_guide/statemachine>` + *(Optional)* + + * **settings**: Dictionary or Settings class instance with updated settings for this connection. + Pass a dictionary to update some of the settings, or pass a Settings object with all settings. + *(Optional)* + + * **overwrite_settings**: Boolean option to allow settings to be appended (if the attribute is a list). + *(Optional)* + + * **log_stdout**: Boolean option to enable/disable logging to standard output. Default is True. + *(Optional)* + + * **log_propagate**: Boolean option to enable/disable propagating logs from connection logger + to parent logger (e.g. whether logs for `unicon.N7K-BESTPROD2-SSR-P1.cli.1663541251` logger + should propagate to `unicon` logger). Default is False. *(Optional)* + + * **no_pyats_tasklog**: Boolean option to enable/disable logging to pyats tasklog. Default is False. + *(Optional)* + + * **debug**: Boolean option to enable/disable internal debug logging. + *(Optional)* + + * **service_attributes**: Dictionary whose keys are service names + and whose values are dictionaries containing key/value pairs to set on the + named service. + *(Optional)* + + * **connect_reply**: Dialog object which user wants to be added in the connection dialog. + *(Optional)* + + * **goto_enable**: Boolean option to enable/disable connection behavior to go to enable state + after setting up connection. Default is True. + *(Optional)* + + * **standby_goto_enable**: Boolean option to enable/disable standby connection behavior to go to + enable state after setting up connection. Default is True. + *(Optional)* + + * **trim_line**: Boolean option to enable line trimming if the line has additional `\\r\\n` characters. + *(Optional)* + + * **reconnect**: Boolean option to enable automatic reconnect in case the connection has not been made + or the connection was lost. Default: True + *(Optional)* + +For *Single RP* connection, `start` will be a list with only one element. + +.. note:: + + Connecting to many routing and switching platforms usually requires + the configured hostname to be known in advance. + However, sometimes the configured hostname on such a device may be + unknown and may differ from the ``hostname`` parameter. + + When ``learn_hostname=True`` is specified: + + * unicon attempts to learn the hostname of the device + by examining the device's prompt. + + * If no hostname can be learned, a warning is thrown and the + learned hostname is set to a generic pattern. + + * If the learned hostname differs from the ``hostname`` parameter, + ``dev.previous_hostname`` is set to the original hostname and + ``dev.hostname`` is overwritten with the newly learned hostname. + + * Once set, the ``learn_hostname`` setting can only be changed by + destroying and recreating the Connection object. + + * The hostname of the device does not contain the characters : + #, whitespace characters. + + +.. note:: + + Passive hostname learning is enabled by default and will + give a warning if the device hostname does not match the learned + hostname. The learned hostname is only used if `learn_hostname=True`. + + A timeout may occur if the prompt pattern uses the hostname, + the timeout error includes the hostname and a hint to check + the hostname if a mismatch was detected. + + +.. note:: + + When using the Linux plugin, it is recommended to use ``learn_hostname=True``. + With the default prompt pattern for the Linux plugin there is a risk of false prompt + matching if the output contains one of the prompt characters `> # % ~ $` at the end of a line. + + +**Disconnecting** + +To disconnect a session, you can call the `disconnect()` method from a Unicon connection. +This will terminate the subprocess that is handling the device connection. By default, +Unicon waits about 10 seconds after the process is terminated before returning from the method. +This is to prevent connection issues on rapid connect/disconnect sequences. + +To change the default timers used when disconnecting, you can change the `GRACEFUL_DISCONNECT_WAIT_SEC` and +`POST_DISCONNECT_WAIT_SEC` settings on the Settings object. + +.. code-block:: python + + dev.settings.GRACEFUL_DISCONNECT_WAIT_SEC = 0 + dev.settings.POST_DISCONNECT_WAIT_SEC = 0 + + +.. _unicon_extend_settings_attributes: + +Extend Settings Attributes +"""""""""""""""""""""""""" + +It is possible to extend list settings attributes of the connection like ``ERROR_PATTERN`` +and ``CONFIGURE_ERROR_PATTERN`` by using ``overwrite_settings=False`` argument. + +.. code-block:: python + + from unicon import Connection + settings = {'ERROR_PATTERN': ['test', 'error']} + dev = Connection(hostname='asr1000', + start=['telnet 172.27.114.43 2037'], + credentials={'default': {'username': 'admin', 'password': 'Cisc0123'}}, + os='iosxe', + settings=settings, + overwrite_settings=False) + dev.connect() + dev.settings.ERROR_PATTERN + ['test', + 'error', + '^%\\s*[Ii]nvalid (command|input)', + '^%\\s*[Ii]ncomplete (command|input)', + '^%\\s*[Aa]mbiguous (command|input)'] + + # this can be done from testbed yaml as well + # the following is an example testbed + devices: + PE1: + alias: uut + os: iosxe + credentials: + default: + password: cisco + username: admin + enable: + password: cisco + connections: + defaults: + class: unicon.Unicon + a: + protocol: telnet + ip: 1.1.1.1 + port: 2039 + arguments: + overwrite_settings: False + settings: + EXEC_TIMEOUT: 300 + ERROR_PATTERN: + - testbed + - my ERROR + + +.. _unicon_override_service_attributes: + +Overriding Service Attributes +""""""""""""""""""""""""""""" + +When a connection is created, various services are attached to it. The +selected plugin determines the list of supported services. + +It is possible to override attributes of one or more services by specifying +the ``service_attributes`` parameter. + +.. code-block:: python + + from unicon import Connection + dev = Connection(hostname='n7k2-1', + start=['telnet 172.27.114.43 2037'], + credentials={'default': {'username': 'admin', 'password': 'Cisc0123'}}, + os='nxos', + service_attributes=dict( + traceroute=dict(timeout=123), + ping=dict(timeout=456))) + dev.connect() + dev.traceroute.timeout + 123 + dev.ping.timeout + 456 + + +Logging +------- + +Every unicon device connection Logger has 3 handlers. + +#. Screen Handler: This logs messages on stdout +#. File Handler: This logs messages in file /tmp/--.log. This is default log file. To modify the file value, the logfile parameter can be used. +#. pyATS TaskLog Handler: This logs messages in pyats TaskLog file + +Change logfile when connecting. + +In unicon standalone mode: + +.. code-block:: python + + dev = Connection(hostname=uut_hostname, + start=[uut_start_cmd], + logfile='user-provided-file') + +With pyATS: + +.. code-block:: python + + dev.connect(logfile='user-provided-file') + +Log level of device output and service messages is `INFO`. + +To disable unicon device connection logging, we can set logger level above `logging.INFO`. + +.. code-block:: python + + import logging + uut.log.setLevel(logging.WARNING) + +To enable debug logs, use below: + +.. code-block:: python + + import logging + uut.log.setLevel(logging.DEBUG) + +Debug log now integrates with pyATS testbed yaml file. You can enable it +by define the `debug: True` in the yaml file: + +.. code-block:: python + + devices: + PE1: + connections: + defaults: + class: 'unicon.Unicon' + debug: True + +To disable logging to standard output, use the `log_stdout` boolean option. + +In unicon standalone mode: + +.. code-block:: python + + dev = Connection(hostname=uut_hostname, + start=[uut_start_cmd], + log_stdout=False) + +With pyATS: + +.. code-block:: python + + dev.connect(log_stdout=False) + + +Prompt Recovery Usage +--------------------- + +In unicon, device connection is 2 step process: + +#. Create Device `Connection` object +#. Invoke `connect()` on Device `Connection` object. + +The `prompt_recovery` is valid for per connect() call. +To use `prompt_recovery` feature user need to specify it per call i.e when connecting next time, user need to set it again as `True`. + +Examples: + +To use `prompt_recovery` feature in unicon, use it in the following way: + +.. code-block:: python + + from unicon import Connection + device = Connection(hostname='R2', start=['telnet localhost 15000'], prompt_recovery=True) + device.connect() + +If user wishes to enable `prompt_recovery` after creating Device Connection object, it can be done in the following way: + +.. code-block:: python + + from unicon import Connection + device = Connection(hostname='R2', start=['telnet localhost 15000']) + device.context.prompt_recovery=True + device.connect() + +When using with pyats, the feature can be used in the following way: + +.. code-block:: python + + device = testbed['R1'] + device.connect(prompt_recovery=True) + +In pyats, to use `prompt_recovery` in next `connect()` call, use `device.destroy()` to disconnect connection and +use `device.connect(prompt_recovery=True)` again. + + +Login and Password Prompts +-------------------------- + +Unicon generic plugin uses the following regular expressions to match login and password prompts: + +#. Login pattern: `r'^.*([Uu]sername|[Ll]ogin): ?$'` +#. Password pattern: `r'^.*[Pp]assword( for )?(\S+)?: ?$'` + +While creating a connection, Unicon sends username and password when the device prompt matches the above patterns. + +In some cases, change in login/password prompts on device may lead to connection failure if the default +patterns no longer match. + +To handle such situations, user can provide custom regular expression pattern to match with different +login and password prompts on the device. + +It can be done by setting regular expression to `LOGIN_PROMPT` and `PASSWORD_PROMPT` attributes of device `settings`. + +Example: + +.. code-block:: python + + # Unicon standalone mode + dev = Connection(hostname='R2', start=['telnet x.x.x.x'],\ + credentials={{'default': {'username': 'admin', 'password': 'Cisc0123'}}) + dev.settings.LOGIN_PROMPT = r'USERNAME:\s?$' + dev.settings.PASSWORD_PROMPT = r'PASSWORD:\s$' + +In pyATS testbed yaml file, this can be set in the following way: + +.. code-block:: yaml + + devices: + R2 + credentials: + default: + username: admin + password: Cisc0123 + connections: + defaults: {class: 'unicon.Unicon'} + a: + protocol: telnet + ip: x.x.x.x + port: 2042 + prompts: + login: "USERNAME:\s*$" + password: "PASSWORD:\s*$" + + +The login and password patterns are also applicable for login/password prompts displayed during +`reload()`, `switchover()` services. It is possible to override the login and +password dialogs and other default dialogs in the execute service by specifying the +`service_dialog` option in the execute statement. See `execute service`_. + +.. _execute service: services/generic_services.html#execute + +This setting attribute are not applicable for `ise` plugin. + +These settings attributes are supported on below plugins: + +#. generic +#. iosxr +#. junos +#. linux +#. aireos + + +Learn Device OS +-------------------- + +Unicon generic plugin now can learn the device os/platform and redirect the connection to use corresponding plugins. +This can be done if you pass `learn_os` argument in `device.connect(learn_os=True)`. + +Example: + +In pyATS testbed.yaml file, no `os` is provided: + +.. code-block:: yaml + + devices: + Router: + alias: uut + type: xe + credentials: + default: + password: cisco + username: cisco + enable: + password: cisco + connections: + defaults: + class: unicon.Unicon + a: + protocol: telnet + ip: x.x.x.x + port: xxxx + +In pyATS shell: + +.. code-block:: python + + # pyats shell --testbed-file testbed.yaml + >>> from genie.testbed import load + >>> testbed = load('testbed.yaml') + ------------------------------------------------------------------------------- + >>> dev = testbed.devices['uut'] + >>> dev.connect(learn_os=True) + # dev.connect() << if learn_os is not provided, then it will use generic plugin + + + device's os is not provided, unicon may not use correct plugins + + 2020-08-11 16:17:37,909: %UNICON-INFO: +++ Router logfile /tmp/Router-cli-20200811T161737899.log +++ + + 2020-08-11 16:17:37,910: %UNICON-INFO: +++ Unicon plugin generic +++ + Trying x.x.x.x... + + + 2020-08-11 16:17:37,951: %UNICON-INFO: +++ connection to spawn: telnet x.x.x.x xxxx, id: 140643774849992 +++ + + 2020-08-11 16:17:37,952: %UNICON-INFO: connection to Router + + 2020-08-11 16:17:37,952: %UNICON-INFO: Learning device Router os + Connected to x.x.x.x. + Escape character is '^]'. + + Router# + + 2020-08-11 16:17:38,543: %UNICON-INFO: +++ Router: executing command 'show version' +++ + show version + Cisco IOS Software, IOS-XE Software (PPC_LINUX_IOSD-ADVIPSERVICES-M), Version 15.2(4)S, RELEASE SOFTWARE (fc4) + Technical Support: http://www.cisco.com/techsupport + Copyright (c) 1986-2012 by Cisco Systems, Inc. + Compiled Mon 23-Jul-12 19:02 by mcpre + + IOS XE Version: 03.07.00.S + + Cisco IOS-XE software, Copyright (c) 2005-2012 by cisco Systems, Inc. + All rights reserved. Certain components of Cisco IOS-XE software are + licensed under the GNU General Public License ("GPL") Version 2.0. The + software code licensed under GPL Version 2.0 is free software that comes + with ABSOLUTELY NO WARRANTY. You can redistribute and/or modify such + GPL code under the terms of GPL Version 2.0. For more details, see the + documentation or "License Notice" file accompanying the IOS-XE software, + or the applicable URL provided on the flyer accompanying the IOS-XE + software. + + + ROM: IOS-XE ROMMON + + Router uptime is 31 weeks, 2 hours, 15 minutes + Uptime for this control processor is 31 weeks, 2 hours, 18 minutes + System returned to ROM by reload + System image file is "bootflash:asr1000rp1-advipservices.03.07.00.S.152-4.S.bin" + Last reload reason: PowerOn + + + cisco Router-F (2RU) processor with 1698793K/6147K bytes of memory. + Processor board ID FOX1405GDVK + 12 Gigabit Ethernet interfaces + 32768K bytes of non-volatile configuration memory. + 4194304K bytes of physical memory. + 7798783K bytes of eUSB flash at bootflash:. + + Configuration register is 0x2102 + + Router# + + 2020-08-11 16:17:40,221: %UNICON-INFO: Learned device os: iosxe + + 2020-08-11 16:17:40,222: %UNICON-INFO: + Learned device os: iosxe + Redirect to corresponding plugins. + + 2020-08-11 16:17:52,263: %UNICON-INFO: +++ Router logfile /tmp/Router-cli-20200811T161752253.log +++ + + 2020-08-11 16:17:52,263: %UNICON-INFO: +++ Unicon plugin iosxe +++ + Trying x.x.x.x... + + + 2020-08-11 16:17:52,288: %UNICON-INFO: +++ connection to spawn: telnet x.x.x.x xxxx, id: 140643774387984 +++ + + 2020-08-11 16:17:52,288: %UNICON-INFO: connection to Router + Connected to x.x.x.x. + Escape character is '^]'. + + Router# + + 2020-08-11 16:17:52,896: %UNICON-INFO: +++ initializing handle +++ + + 2020-08-11 16:17:52,897: %UNICON-INFO: +++ Router: executing command 'term length 0' +++ + term length 0 + Router# + + 2020-08-11 16:17:53,113: %UNICON-INFO: +++ Router: executing command 'term width 0' +++ + term width 0 + Router# + + 2020-08-11 16:17:53,310: %UNICON-INFO: +++ Router: executing command 'show version' +++ + show version + Cisco IOS Software, IOS-XE Software (PPC_LINUX_IOSD-ADVIPSERVICES-M), Version 15.2(4)S, RELEASE SOFTWARE (fc4) + Technical Support: http://www.cisco.com/techsupport + Copyright (c) 1986-2012 by Cisco Systems, Inc. + Compiled Mon 23-Jul-12 19:02 by mcpre + + IOS XE Version: 03.07.00.S + + Cisco IOS-XE software, Copyright (c) 2005-2012 by cisco Systems, Inc. + All rights reserved. Certain components of Cisco IOS-XE software are + licensed under the GNU General Public License ("GPL") Version 2.0. The + software code licensed under GPL Version 2.0 is free software that comes + with ABSOLUTELY NO WARRANTY. You can redistribute and/or modify such + GPL code under the terms of GPL Version 2.0. For more details, see the + documentation or "License Notice" file accompanying the IOS-XE software, + or the applicable URL provided on the flyer accompanying the IOS-XE + software. + + + ROM: IOS-XE ROMMON + + Router uptime is 31 weeks, 2 hours, 15 minutes + Uptime for this control processor is 31 weeks, 2 hours, 18 minutes + System returned to ROM by reload + System image file is "bootflash:asr1000rp1-advipservices.03.07.00.S.152-4.S.bin" + Last reload reason: PowerOn + + + cisco Router-F (2RU) processor with 1698793K/6147K bytes of memory. + Processor board ID FOX1405GDVK + 12 Gigabit Ethernet interfaces + 32768K bytes of non-volatile configuration memory. + 4194304K bytes of physical memory. + 7798783K bytes of eUSB flash at bootflash:. + + Configuration register is 0x2102 + + Router# + + 2020-08-11 16:17:55,027: %UNICON-INFO: +++ Router: config +++ + config term + Enter configuration commands, one per line. End with CNTL/Z. + Router(config)#no logging console + Router(config)#line console 0 + Router(config-line)#exec-timeout 0 + Router(config-line)#end + Router# + + +Device Abstraction Token Discovery +---------------------------------- + +Device abstraction tokens are device specific data points that allow pyATS, Genie, and Unicon to alter program behavior to best suit each device. +These tokens include: + +- `device.os` +- `device.os_flavor` +- `device.version` +- `device.platform` +- `device.model` +- `device.pid` + +During the initial connection to a device, Unicon will learn the device abstraction tokens using the following steps: + +#. Execute the following show commands on the device: + - `show version` + - `show inventory` + - `uname -a` +#. Convert the raw output into dictionaries using parsers +#. The dictionaries are used to retrieve specific data which are then assigned as abstraction tokens under the device object +#. Finally, Unicon will redirect to the correct connection plugin. + +.. note:: + The data gathered by executing the show commands is only used to set up the device abstraction tokens. + +To make use of this feature, you can choose from the following actions: + +1. Set the `learn_tokens` argument to True when calling `device.connect` + +.. code-block:: python + + device.connect(learn_tokens=True) + +2. Use device connection settings in the testbed file + +.. code-block:: yaml + + devices: + device1: + ... + connections: + defaults: + class: unicon.Unicon + a: + ... + settings: + learn_tokens: True + +3. Use device connection arguments in the testbed file + +.. code-block:: yaml + + devices: + device1: + ... + connections: + defaults: + class: unicon.Unicon + a: + ... + arguments: + learn_tokens: True + +4. Use a pyats.conf file + +.. code-block:: ini + + [unicon] + learn_tokens = True + +5. Use an environment variable + +.. code-block:: bash + + export UNICON_LEARN_TOKENS=1 + +By default, token discovery will not overwrite tokens that you have already defined in your testbed file. +It will only assign discovered tokens to the device object if the token does not yet exist or if the value is generic. For example: `platform: generic`. + +You can override this behavior if you'd like. Using the `overwrite_testbed_tokens` flag will cause any discovered token to be assigned to the device object regardless of what has been defined in the testbed. +This flag can be set in the same way as `learn_tokens`: + +1. Set the `overwrite_testbed_tokens` argument to True when calling `device.connect` + +.. code-block:: python + + device.connect(learn_tokens=True, overwrite_testbed_tokens=True) + +2. Use device connection settings in the testbed file + +.. code-block:: yaml + + devices: + device1: + ... + connections: + defaults: + class: unicon.Unicon + a: + ... + settings: + LEARN_DEVICE_TOKENS: True + OVERWRITE_TESTBED_TOKENS: True + +3. Use device connection arguments in the testbed file + +.. code-block:: yaml + + devices: + device1: + ... + connections: + defaults: + class: unicon.Unicon + a: + ... + arguments: + learn_tokens: True + overwrite_testbed_tokens: True + +4. Use a pyats.conf file + +.. code-block:: ini + + [unicon] + learn_tokens = True + overwrite_testbed_tokens = True + +5. Use an environment variable + +.. code-block:: bash + + export UNICON_LEARN_TOKENS=1 + export UNICON_OVERWRITE_TESTBED_TOKENS=1 \ No newline at end of file diff --git a/docs/user_guide/examples/1_eal_simple_sendex.py b/docs/user_guide/examples/1_eal_simple_sendex.py new file mode 100644 index 00000000..bc96d1d1 --- /dev/null +++ b/docs/user_guide/examples/1_eal_simple_sendex.py @@ -0,0 +1,23 @@ +import os +from unicon.eal.expect import Spawn, TimeoutError +router_command = os.path.join(os.getcwd(), 'router.sh') +prompt = 'sim-router' +enable_prompt = prompt + '#' +disable_prompt = prompt + '>' +s = Spawn(router_command) +try: + s.sendline() + s.expect([r'username:\s?$', r'login:\s?$'], timeout=5) + s.sendline('admin') + s.expect([r'password:\s?$'], timeout=5) + s.sendline('lab') + s.expect([disable_prompt]) + s.sendline('enable') + s.expect([r'password:\s?$'], timeout=5) + s.sendline('lablab') + s.expect([enable_prompt]) + s.sendline('show clock') + s.expect([enable_prompt]) +except TimeoutError as err: + print('errored becuase of timeout') + diff --git a/docs/user_guide/examples/2_dialog_with_three_callbacks.py b/docs/user_guide/examples/2_dialog_with_three_callbacks.py new file mode 100644 index 00000000..952be3bf --- /dev/null +++ b/docs/user_guide/examples/2_dialog_with_three_callbacks.py @@ -0,0 +1,34 @@ +import os +from unicon.eal.expect import Spawn, TimeoutError +from unicon.eal.dialogs import Statement, Dialog + +router_command = os.path.join(os.getcwd(), 'router.sh') +prompt = 'sim-router' +enable_prompt = prompt + '#' +disable_prompt = prompt + '>' + +# callback to send password +def send_password(spawn, password='lab'): + spawn.sendline(password) + +# callback to send username +def send_username(spawn, username="admin"): + spawn.sendline(username) + +# callback to send new line +def send_new_line(spawn): + spawn.sendline() + +# construct the dialog +d = Dialog([ + [r'enter to continue \.\.\.', send_new_line, None, True, False], + [r'username:\s?$', send_username, None, True, False], + [r'password:\s?$', send_password, None, True, False], + [disable_prompt, None, None, False, False], +]) + +s = Spawn(router_command) + +# at this stage we are anticipating the program to wait for a new line +d.process(s) +s.close() diff --git a/docs/user_guide/examples/3_dialog_with_one_callback.py b/docs/user_guide/examples/3_dialog_with_one_callback.py new file mode 100644 index 00000000..6c48d081 --- /dev/null +++ b/docs/user_guide/examples/3_dialog_with_one_callback.py @@ -0,0 +1,30 @@ +import os +from unicon.eal.expect import Spawn, TimeoutError +from unicon.eal.dialogs import Statement, Dialog + +router_command = os.path.join(os.getcwd(), 'router.sh') +prompt = 'sim-router' +enable_prompt = prompt + '#' +disable_prompt = prompt + '>' + +# callback to send any command or a new line character +def send_command(spawn, command=None): + if command is not None: + spawn.sendline(command) + else: + spawn.sendline() + +# construct the dialog +d = Dialog([ + [r'enter to continue \.\.\.', send_command, None, True, False], + [r'username:\s?$', send_command, {'command': 'admin'}, True, False], + [r'password:\s?$', send_command, {'command': 'lab'}, True, False], + [disable_prompt, None, None, False, False], +]) + +s = Spawn(router_command) + +# at this stage we are anticipating the program to wait for a new line +d.process(s) + +s.close() diff --git a/docs/user_guide/examples/4_using_lambda.py b/docs/user_guide/examples/4_using_lambda.py new file mode 100644 index 00000000..d1653874 --- /dev/null +++ b/docs/user_guide/examples/4_using_lambda.py @@ -0,0 +1,22 @@ +import os +from unicon.eal.expect import Spawn, TimeoutError +from unicon.eal.dialogs import Statement, Dialog + +router_command = os.path.join(os.getcwd(), 'router.sh') +prompt = 'sim-router' +enable_prompt = prompt + '#' +disable_prompt = prompt + '>' + +# construct the dialog +d = Dialog([ + [r'enter to continue \.\.\.', lambda spawn: spawn.sendline(), None, True, False], + [r'username:\s?$', lambda spawn: spawn.sendline("admin"), None, True, False], + [r'password:\s?$', lambda spawn: spawn.sendline("lab"), None, True, False], + [disable_prompt, None, None, False, False], +]) + +s = Spawn(router_command) + +# at this stage we are anticipating the program to wait for a new line +d.process(s) +s.close() diff --git a/docs/user_guide/examples/5_using_shorthand.py b/docs/user_guide/examples/5_using_shorthand.py new file mode 100644 index 00000000..ee23e936 --- /dev/null +++ b/docs/user_guide/examples/5_using_shorthand.py @@ -0,0 +1,23 @@ +import os +from unicon.eal.expect import Spawn, TimeoutError +from unicon.eal.dialogs import Statement, Dialog + +router_command = os.path.join(os.getcwd(), 'router.sh') +prompt = 'sim-router' +enable_prompt = prompt + '#' +disable_prompt = prompt + '>' + +# construct the dialog +# we can see how shorthand notation makes the code look even more leaner. +d = Dialog([ + [r'enter to continue \.\.\.', 'sendline()', None, True, False], + [r'username:\s?$', 'sendline(admin)', None, True, False], + [r'password:\s?$', 'sendline(lab)', None, True, False], + [disable_prompt, None, None, False, False], +]) + +s = Spawn(router_command) + +# at this stage we are anticipating the program to wait for a new line +d.process(s) +s.close() diff --git a/docs/user_guide/examples/6_using_session.py b/docs/user_guide/examples/6_using_session.py new file mode 100644 index 00000000..20fc8756 --- /dev/null +++ b/docs/user_guide/examples/6_using_session.py @@ -0,0 +1,35 @@ +import os +from unicon.eal.expect import Spawn, TimeoutError +from unicon.eal.dialogs import Statement, Dialog + +router_command = os.path.join(os.getcwd(), 'router.sh') +prompt = 'sim-router' +enable_prompt = prompt + '#' +disable_prompt = prompt + '>' + +# callback to send password +def send_passwd(spawn, session, enablepw, loginpw): + if 'flag' not in session: + # this is first entry hence we need to send login password. + session.flag = True + spawn.sendline(loginpw) + else: + # if we come here that means it is second entry and here. + # we need to send the enable password. + spawn.sendline(enablepw) + +# construct the dialog +d = Dialog([ + [r'enter to continue \.\.\.', lambda spawn: spawn.sendline(), None, True, False], + [r'username:\s?$', lambda spawn: spawn.sendline("admin"), None, True, False], + [r'password:\s?$', send_passwd, {'enablepw': 'lablab', 'loginpw': 'lab'}, True, False], + [disable_prompt, lambda spawn: spawn.sendline("enable"), None, True, False], + [enable_prompt, None, None, False, False], +]) + +s = Spawn(router_command) + +# at this stage we are anticipating the program to wait for a new line +d.process(s) + +s.close() diff --git a/docs/user_guide/examples/7_using_shorthand_with_session.py b/docs/user_guide/examples/7_using_shorthand_with_session.py new file mode 100644 index 00000000..861a72c4 --- /dev/null +++ b/docs/user_guide/examples/7_using_shorthand_with_session.py @@ -0,0 +1,38 @@ +import os +from unicon.eal.expect import Spawn, TimeoutError +from unicon.eal.dialogs import Statement, Dialog + +router_command = os.path.join(os.getcwd(), 'router.sh') +prompt = 'sim-router' +enable_prompt = prompt + '#' +disable_prompt = prompt + '>' + +# callback to send password, we still need this callback +# because shorthand notation is for handling trivial payloads. +# this function does little more than that. +def send_passwd(spawn, session, enablepw, loginpw): + if 'flag' not in session: + # this is first entry hence we need to send login password. + session.flag = True + spawn.sendline(loginpw) + else: + # if we come here that means it is second entry and here. + # we need to send the enable password. + spawn.sendline(enablepw) + +# construct the dialog. +# here we see how shorthand notation can make the code look leaner. +d = Dialog([ + [r'enter to continue \.\.\.', 'sendline()', None, True, False], + [r'username:\s?$', "sendline(admin)", None, True, False], + [r'password:\s?$', send_passwd, {'enablepw': 'lablab', 'loginpw': 'lab'}, True, False], + [disable_prompt, 'sendline(enable)', None, True, False], + [enable_prompt, None, None, False, False], +]) + +s = Spawn(router_command) + +# at this stage we are anticipating the program to wait for a new line +d.process(s) + +s.close() diff --git a/docs/user_guide/examples/router.sh b/docs/user_guide/examples/router.sh new file mode 100755 index 00000000..2cf26dc3 --- /dev/null +++ b/docs/user_guide/examples/router.sh @@ -0,0 +1,91 @@ +#!/bin/bash +hostname="sim-router" +disable_prompt="$hostname>" +enable_prompt="$hostname#" +config_prompt="$hostname(config)#" +echo "Trying X.X.X.X ... +Escape character is '^]'. +Press enter to continue ..." +read escape_char + +if [[ $escape_char == "" ]] +then + echo -n "username: " + read username + if [[ $username == "admin" ]] + then + echo -n "password: " + read -s password + if [[ $password == "lab" ]] + then + echo + #echo -n "$disable_prompt" + else + echo "bad password" + exit 1 + fi + else + echo "wrong username" + exit 1 + fi +fi + +prompt=$disable_prompt +while true +do + echo -n $prompt + read resp + # enable command + if [[ $resp == "enable" || $resp == "en" ]] + then + password="" + echo -n "password: " + read password + if [[ $password == "lablab" ]] + then + prompt=$enable_prompt + else + echo "Bad Password" + fi + # show clock command + elif [[ $resp == "show clock" || $resp == "sh clock" ]] + then + echo $(date) + + # config mode. + elif [[ $resp == "config" || $resp == "config term" ]] + then + # check if we are in enable mode + if [[ $prompt == $enable_prompt ]] + then + echo -n "Configuring from terminal, memory, or network [terminal]? " + read resp + if [[ $resp == "" ]] + then + prompt=$config_prompt + fi + else + echo "you need to be in enable mode" + fi + # config end + elif [[ $resp == "end" ]] + then + # check if are in config mode + if [[ $prompt == $config_prompt ]] + then + prompt=$enable_prompt + else + echo "you need to be in config mode" + fi + # going to disable mode. + elif [[ $resp == "disable" ]] + then + # check if we are in enable mode first + if [[ $prompt == $enable_prompt ]] + then + prompt=$disable_prompt + else + echo "you need to be in enable mode" + fi + fi +done diff --git a/docs/user_guide/passwords.rst b/docs/user_guide/passwords.rst new file mode 100644 index 00000000..92d24066 --- /dev/null +++ b/docs/user_guide/passwords.rst @@ -0,0 +1,569 @@ +Password Handling +================= + +Passwords are defined in the testbed YAML file. This document describes the +password handling logic used by the different plugins. +Passwords are managed through device credentials in pyATS, +the next section explains how credentials are handled in pyATS. + +.. _credentials: + +Credentials +----------- + +The ``credentials`` connection parameter defines a dictionary of named +credentials. A credential is a dictionary typically containing both +``username`` and ``password`` keys. + +The ``login_creds`` connection parameter defines an optional sequence of +credential names to try. Each time the device prompts for a username or +password, the current credential is set to the next credential in the sequence +if a current credential has not already been set. +When a password is sent, the current credential is unset. The one exception +is when entering an administrator's password on a routing device coming up +without configuration, in this case the current credential is reused. +If the sequence has been exhausted and no more credentials are available to +satisfy a username/password prompt, a +`CredentialsExhaustedError` is +raised. + +Credentials are not retried, any username or password failure causes a +`UniconAuthenticationError` +to be raised, unless fallback credentials are defined. See :ref:`fallback_credentials`. + +It is possible to specify the password to use for routing devices to enter +enable mode. This may be done via the ``enable_password`` entry under the +current credential, or via a separate credential called ``enable``. +Please see :ref:`unicon_enable_password_handling` for details. + +Passwords specified as a :ref:`secret_strings` are automatically decoded prior +to being sent to the device. + +In pyATS Testbed YAML +""""""""""""""""""""" + +Credentials may be specified on a per-testbed, per-device or per-connection +basis, as documented in :ref:`topology_credential_password_modeling`. + + +.. code-block:: python + + from pyats.topology import loader + tb = loader.load(""" + devices: + my_device: + type: router + credentials: + default: + username: admin + password: Cisc0123 + alternate: + username: alt_username + password: alt_password + termserv: + username: tsuser + password: tspw + enable: + password: enablepw + connections: + defaults: {class: 'unicon.Unicon'} + a: + protocol: ssh + ip: 10.64.70.11 + port: 2042 + login_creds: [termserv, default] + ssh_options: "-v -i /path/to/identityfile" + + """) + dev = tb.devices.my_device + dev.connect() + + # To connect using different credentials than is contained in the + # testbed YAML ``login_creds`` key: + dev.destroy() + dev.connect(login_creds=['termserv', 'alternate']) + + +In Python +""""""""" + +.. code-block:: python + + dev = Connnection(hostname=uut_hostname, + start=[uut_start_cmd], + credentials={\ + {'default': {'username': 'admin', 'password': 'Cisc0123'}},\ + {'enable': {'password': 'enablepw'}},\ + {'termserv': {'username': 'tsuser', 'password': 'tspw'}},\ + }, + login_creds = ['termserv', 'default'], + ) + + +Post credential action +"""""""""""""""""""""" + +In certain cases, e.g. when using a serial console server, an action is needed to get a response +from the device connected to the serial port. There are two ways to configure this action. +The first one is using a setting, the second one is using a post credential action. +The post credential action takes precedence over the setting. + +Example credentials for a device. + +.. code-block:: yaml + + my_device: + type: router + credentials: + default: + username: admin + password: Cisc0123 + terminal_server: + username: tsuser + password: tspw + + +Setting the credential action via `settings` in python. + +.. code-block:: python + + # Name of the credential after which a "sendline()" should be executed + dev.settings.SENDLINE_AFTER_CRED = 'terminal_server' + + +Settings can also be specified for the connection in the topology file as shown below. + +.. code-block:: yaml + + connections: + cli: + settings: + SENDLINE_AFTER_CRED: terminal_server + + +The post credential action supports ``send`` and ``sendline``, you can specify a string to be sent, +e.g. `send( )` to send a space or `send(\\x03)` to send Ctrl-C. Quotes should not be specified. + +.. code-block:: yaml + + connections: + cli: + login_creds: [terminal_server, default] + arguments: + cred_action: + terminal_server: + post: sendline() + + + +Unicon supports passwords specified in encrypted form. Please see +:ref:`topology_credential_password_modeling` for details. + +Typically, credentials may be specified using any preferred name. + +However, the following credentials are specified using well-known reserved +names: + + * ``default`` : The default credential, which is the fallback if a named + credential is not specified. + + * ``enable`` : The password sent when bringing a routing device to enable mode. + Please see :ref:`unicon_enable_password_handling` for details. + + * ``sudo`` : The fxos/ftd plugin requires this (see note below). + + * ``ssh`` : Used to authenticate against an ssh tunnel server. + See :ref:`unicon_ssh_tunnel` for details. + + * ``bmc`` : The iosxr/spitfire plugin requires this (see note below). + +These passwords can be defined in the testbed YAML file in the `testbed` +section, for each `device`, or at the connection level. + +.. code-block:: yaml + + # generic passwords + testbed: + credentials: + default: + username: admin + password: cisco123 + enable: + password: my_enable_pw + + +The usage of these credentials depends on the plugin. +The generic plugin is used when no ``os`` is specified in the testbed YAML file. +The generic implementation is used also by most other +plugins except (currently) the `linux` and `asa` plugins. + +.. code-block:: yaml + + devices: + lnx: + type: linux + os: linux + credentials: + default: + username: cisco + password: cisco + +If ``username`` is not defined in the credentials, the default username for +Linux is the OS user that is running the python script. +The default linux password is empty (""). + +For all other devices, the default password logic is used (unless otherwise +specified by the specific plugin). + +``login_creds`` is used to describe the order of credentials to use on +initial login. If not specified, the ``default`` credential is used. +Please see :ref:`credentials` for more details. + +.. code-block:: yaml + + devices: + my_device: + type: router + os: ios + credentials: + default: + username: cisco + password: secret + enable: + password: enable + connections: + vty1: + credentials: + default: + username: cisco1 + password: secret1 + vty2: + credentials: + first: + username: first_user + password: first_pw + default: + username: cisco2 + password: secret2 + enable: + password: enable2 + login_creds: [first, default] + +.. _unicon_enable_password_handling: + +Enable password handling +------------------------ + +The following example shows a case where a device may have multiple enable +passwords. +For example, different credentials could apply depending on whether or not a +RADIUS server is reachable. + +.. code-block:: yaml + + devices: + my_device: + type: router + os: ios + credentials: + default: + username: cisco + password: secret + enable_password: enable + local: + username: cisco_local + password: secret_local + enable_password: enable_local + +The following command connects to the router and enters enable mode using +``local`` credential authentication: + +.. code-block:: python + + device.connect(login_creds='local') + +The following command connects to the router and enters enable mode using +``default`` credential authentication: + +.. code-block:: python + + device.connect() + +How enable password is chosen +----------------------------- + +When a router asks for an enable password, the password sent is determined +by the following checks. If all checks are done and still no enable password +is found then an exception is raised. + +#. The ``enable_password`` field of the credential specified by the ``login_creds`` in the connect call. +#. The ``default`` credential ``enable_password`` +#. The ``enable`` credential ``password`` (legacy) +#. The ``default`` credential ``password`` (legacy) + + +Password sequences in service calls +----------------------------------- + +Several services, including ``reload`` and ``switchover``, accept a +credential list that is used to authenticate against a sequence of +username/password prompts encountered while the service is running. + + +Authentication Failure +---------------------- + +The following response pattern generates a bad password exception: + +.. code-block:: python + + bad_passwords = r'^.*?% (Bad passwords|Access denied|Authentication failed)' + +.. _fallback_credentials: + +Fallback Credentials +-------------------- + +In case of authentication failure, fallback credentials are used to try and login to the device +when they are defined for the connection. You can define one or more device credentials and define +them as fallback credentials by adding them to the fallback credentials list under the `defaults` section or under the connection. +The fallback credentials credentials will be used in sequence. If none of the combinations work on the device a bad password exception is raised. + +Below an example of a testbed with fallback credentials defined. + +.. code-block:: yaml + + devices: + my_device: + type: router + os: ios + credentials: + default: + username: cisco + password: secret + enable: + password: enable + set_1: + password: lab + username: lab + set_2: + password: admin + username: admin + connections: + defaults: + class: unicon.Unicon + fallback_credentials: + - set1 + - set2 + netconf: + class: yang.connector.Netconf + ip: 1.2.3.4 + port: 830 + protocol: netconf + telnet: + ip: 1.2.3.4 + port: 23 + protocol: telnet + +Example of fallback credentials per connection: + +.. code-block:: yaml + + devices: + my_device: + type: router + os: ios + credentials: + default: + username: cisco + password: secret + enable: + password: enable + connections: + netconf: + class: yang.connector.Netconf + ip: 1.2.3.4 + port: 830 + protocol: netconf + fallback_credentials: + - set_1 + credentials: + default: + username: cisco + password: secret + set_1: + password: lab + username: lab + telnet: + ip: 1.2.3.4 + port: 23 + protocol: telnet + fallback_credentials: + - set_2 + credentials: + default: + username: cisco + password: secret + set_2: + password: admin + username: admin + + +Environment variables +--------------------- + +You can use the environment variable syntax in the topology file so you don't +have to store passwords on the filesystem. + +.. code-block:: yaml + + credentials: + default: + username: "%ENV{PYATS_USERNAME}" + password "%ENV{PYATS_USERNAME}" + enable: + password "%ENV{PYATS_ENABLE_PASS}" + + +Passwords on HA enabled devices +------------------------------- + +Credentials are specified against the ``a:`` connection for HA enabled devices: + + +.. code-block:: yaml + + devices: + ha_device + type: router + os: ios + credentials: + default: + username: cisco + password: secret + enable: + password: enable + connections: + a: + credentials: + default: + username: cisco1 + password: secret1 + protocol: telnet + ip: 1.1.1.1 + port: 2001 + b: + protocol: telnet + ip: 1.1.1.1 + port: 2002 + + + + +Linux password logic +-------------------- + +When connecting to the device, the password from the current credential is used. +If another password prompt appears during command execution, +no response is sent and the command will timeout by default. + +To execute commands using `sudo`, use the ``sudo`` service. See +:ref:`linux_sudo` + +If connecting via ssh, the username of the currently logged in user is used +by default if not otherwise specified via credentials or via ``command`` +or ``ssh_options`` keys in one of the following forms: + +``ssh -l username
`` or ``ssh username@
``. + +In order to execute a command that leads to a username/password prompt, +you must explicitly add the password statement to the reply Dialog. +If the default password statement is used (as in the example shown below), +a single username/password prompt is responded to using the ``default`` +credential. + +Example code using the password statement: + +.. code-block:: python + + from unicon.eal.dialogs import Dialog + from unicon.plugins.generic.statements import password_stmt + + dialog = Dialog() + dialog.append(password_stmt) + + device.execute('command that prompts for password', reply=dialog) + + +ASA password logic +------------------ + +If the pattern `'^.+?@.+?'s +password: *$'` is seen, the password of the +current credential is sent. + +If the pattern `'^.*Password:\s?$'` is seen, the password of the +``enable`` credential is sent. + +Please see :ref:`unicon_enable_password_handling` for details. + + +iosxr/Spitfire password logic +----------------------------- + +The typical credential sequence is used to authenticate against each +username/password request from the device. + +However, if a BMC login prompt is seen, the password used is taken from the +``bmc`` credential instead. + + +fxos/ftd password logic +----------------------- + +When transitioning from ftd_expert to ftd_expert_root state, the password from the ``sudo`` credential is sent if specified. +Otherwise, the password from the ``default`` credential is sent. Otherwise, a +`UniconAuthenticationError` is raised. + +nxos password logic +------------------- + +The ``switchto`` service accepts a ``vdc_cred`` argument that identifies a +named credential to use to authenticate against the VDC. + +SSH passphrase +-------------- + +You can specify the ``passphrase`` that will be used to respond to the `Enter passphrase for key` prompt +as part of the credential block. + +.. code-block:: yaml + + devices: + my_device: + type: router + os: ios + credentials: + default: + username: cisco + password: secret + passphrase: secret phrase + + +SSH Options +----------- + +You can specify additional SSH options (such as identity/key files) using the +`ssh_options` key as part of the connection block: + +.. code-block:: yaml + + devices: + my_device: + type: router + os: ios + connections: + vty: + protocol: ssh + ip: 10.64.70.11 + port: 2042 + ssh_options: "-i /path/to/id_rsa -o UserKnownHostsFile /dev/null" \ No newline at end of file diff --git a/docs/user_guide/proxy.rst b/docs/user_guide/proxy.rst new file mode 100644 index 00000000..9e34f0af --- /dev/null +++ b/docs/user_guide/proxy.rst @@ -0,0 +1,797 @@ +Connection Through Proxies +========================== + +There are several ways to connect to a device via a 'proxied' connection, i.e. +connecting to a device through another system. Unicon supports CLI proxy and +SSH tunnel features. CLI proxy allows a device to connect via another +(Unicon supported) device, SSH tunnel uses the SSH client to create TCP +tunnels to connect to another device via a SSH connection. + +.. _unicon_cli_proxy: + +CLI Proxy +--------- + +The CLI proxy works by connecting via one or more proxy devices and executing +a command to start the connection to the next device. The command can be +specified explicitly as part of the proxy definition or it can be determined +based on the connection details (i.e. `protocol`, `ip` and `port` or `command`). + +Multiple intermediate devices are supported, you can specify as many proxy +hosts and commands as needed to connect to the target device. Proxy devices +must be defined as a device in the topology file including the relevant +connection details and credentials. Device connection details are used for the +first proxy device only, connection details of intermediate devices are ignored, +you need to explicitly specify a command to connect to an intermediate device. + +When the **CLI proxy** feature is when used as part of pyATS the proxy needs to be +specified in the topology YAML file. + +.. note:: + + If the proxy device has more than one connection defined, you must + specify the 'via' settings under the connection defaults of the proxy device. + + +CLI proxy with pyATS topology integration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Example topology file with a ``proxy`` configuration for the ``cli`` connection. +Please note that credentials have been left out of this example. + +.. code:: yaml + + devices: + jumphost: + os: linux + type: linux + connections: + cli: + protocol: ssh + ip: 127.0.0.1 + port: 2222 + Router: + os: ios + type: router + connections: + defaults: + class: unicon.Unicon + cli: + protocol: telnet + ip: 127.0.0.1 + port: 64001 + proxy: jumphost + + +Connection log (abbreviated) for above example: + +.. code:: + + %UNICON-INFO: ssh 127.0.0.1 -p 2222 + Last login: Wed Jan 24 08:02:24 2018 from 10.0.2.2 + admin@host:~$ + %UNICON-INFO: +++ initializing handle +++ + stty cols 200 + admin@host:~$ stty rows 200 + admin@host:~$ + %UNICON-INFO: +++ connection to spawn_command: ssh 127.0.0.1 -p 2222, id: 4394786888 +++ + telnet 127.0.0.1 64001 + Trying 127.0.0.1... + Connected to 127.0.0.1. + Escape character is '^]'. + + Router# + %UNICON-INFO: +++ initializing handle +++ + +In the above log, you can see the command ``telnet 127.0.0.1 64001`` is +executed to connect to the target device. This command is derived +automatically from the connection details of the target device. + +.. note:: + + There is no support for *hierarchical* proxy configurations. If you need + to pass multiple devices to get to the target device, you need to specify + a list of proxy devices for that device. If a proxy device has a proxy + specified for its connection, it is ignored. + + +CLI Proxy topology schema +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: yaml + + devices: + + : + connections: + + # proxy device only, command is derived from connection details + : + proxy: # proxy device name + + # proxy with specific command + : + proxy: + device: # proxy device name + command: # command to connect to target device + + # proxy with lists of commands + : + proxy: + - device: # proxy device name + command: [ , ] # list of commands, + # the last command connects + # to the next proxy device + - device: # list of commands using different syntax + command: + - + - + + # multiple proxy devices, last device without specific command + # derives the command from the connection details + : + proxy: + - device: # proxy device name + command: + - # command to connect to next proxy device + - device: + + # multiple proxy devices with a list of commands for one of the hosts + : + proxy: + - device: # proxy device name + command: + - + - + - device: # proxy device name + command: + +.. note:: + + When connecting with SSH via IOS-XR devices, make sure to use the + ``username`` keyword in the proxy command. If the username keyword + is not used, Unicon will automatically add `-l \` syntax + to the command line. + + Example: + + .. code:: yaml + + cli: + protocol: telnet + ip: 10.2.3.3 + proxy: + - device: XR1 + command: ssh 1.2.3.5 username cisco # Command including username + + +CLI proxy with Unicon standalone Connections +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The *CLI Proxy* feature can also be used when using Unicon in standalone mode. +Proxy connections can be specified via the `proxy_connections` argument of the +Connection class. + +The `proxy_connections` argument expects a list of ``Connection`` objects with the +start parameter containing the command to be executed to connect to the +next device. If multiple commands should be executed, a list of lists should be +passed, e.g. ``start=[['cmd1','cmd2','cmd3']]`` + +Below example shows a single proxy connection used to reach the IOS router `R01`. + +.. code:: python + + proxy_conn = Connection(hostname='lnx2', + start=['ssh -p 2222 localhost'], + os='linux', + credentials={'default': {'username': 'admin', 'password': 'cisco'}}) + + c = Connection(hostname='R01', + start=['telnet 10.3.3.1'], + os='ios', + credentials={'default': {'username': 'admin', 'password': 'cisco'}}, + proxy_connections=[proxy_conn]) + c.connect() + + + +CLI Proxy examples +~~~~~~~~~~~~~~~~~~ + +**Connecting to ConfD/NSO CLI via a linux server** + +.. code:: yaml + + devices: + lnx: + os: linux + type: linux + credentials: + default: + username: cisco + password: cisco + connections: + defaults: + class: unicon.Unicon + cli: + protocol: ssh + ip: 127.0.0.1 + port: 2222 + nso: + os: confd + type: nso + credentials: + default: + username: admin + password: admin + connections: + defaults: + class: unicon.Unicon + cli: + command: ncs_cli -u admin -C + proxy: lnx + +.. code:: python + + from pyats.topology import loader + tb = loader.load('nso.yaml') + + # Connect to target device, proxy connection is done automatically + n = tb.devices.nso + n.connect(via='cli') + + +**Connecting to a VNF console via Cloud Services Platform (CSP)** + +.. code:: yaml + + # Example with IOS VNF on CSP + + devices: + Router: + type: router + os: ios + credentials: + default: + username: cisco + password: cisco + connections: + defaults: + class: unicon.Unicon + cli: + command: telnet 7005 + proxy: csp + csp: + type: nfvi + os: confd + platform: csp + credentials: + default: + username: admin + password: admin + connections: + defaults: + class: unicon.Unicon + cli: + protocol: ssh + ip: 172.27.132.75 + +.. code:: python + + from pyats.topology import loader + tb = loader.load('csp.yaml') + + # Connect to target device, proxy connection is done automatically + r = tb.devices.Router + r.connect(via='cli') + + +**Connecting via multiple proxy devices** + +Topology file with target device `Sw03` and three intermediate devices, `lnx`, `R01` and `R02`. + +.. code:: yaml + + testbed: + credentials: + default: + username: cisco + password: cisco + devices: + lnx: + type: linux + os: linux + connections: + defaults: + class: unicon.Unicon + cli: + protocol: ssh + ip: 127.0.0.1 + port: 2222 + + R01: + os: ios + type: router + connections: + defaults: + class: unicon.Unicon + cli: + protocol: telnet + ip: 127.0.0.1 + port: 64001 + + R02: + os: ios + type: router + connections: + defaults: + class: unicon.Unicon + cli: + protocol: telnet + ip: 127.0.0.1 + port: 64002 + + Sw03: + os: ios + type: switch + connections: + defaults: + class: unicon.Unicon + cli: + protocol: telnet + ip: 10.2.3.3 + proxy: + - device: lnx + command: telnet 10.3.3.1 # Command specifies how to connect to R01 + - device: R01 + command: telnet 2.2.2.2 # Command specifies how to connect to R02 + - device: R02 # no command, use the connection details of Sw03 + + +Example script and abbreviated connection log. + +.. code:: python + + >>> + >>> from pyats.topology import loader + >>> tb = loader.load('cliproxy.yaml') + >>> sw = tb.devices['Sw03'] + >>> sw.connect()) + + 2018-02-13T12:20:53: %UNICON-INFO: +++ initializing context +++ + + ... + + 2018-02-13T12:20:53: %UNICON-INFO: connection via proxy lnx + + 2018-02-13T12:20:53: %UNICON-INFO: connection to lnx + + Linux$ + 2018-02-13T12:20:53: %UNICON-INFO: +++ initializing handle +++ + + 2018-02-13T12:20:53: %UNICON-INFO: connection via proxy R01 + + 2018-02-13T12:20:53: %UNICON-INFO: connection to R01 + telnet 10.3.3.1 + Trying 10.3.3.1... + Connected to 10.3.3.1. + Escape character is '^]'. + + + User Access Verification + + Password: + R01> + 2018-02-13T12:20:53: %UNICON-INFO: +++ initializing handle +++ + enable + Password: + R01# + 2018-02-13T12:20:53: %UNICON-INFO: connection via proxy R02 + + 2018-02-13T12:20:53: %UNICON-INFO: connection to R02 + telnet 2.2.2.2 + Trying 2.2.2.2... + Connected to 2.2.2.2. + Escape character is '^]'. + + + User Access Verification + + Password: + R02> + 2018-02-13T12:20:53: %UNICON-INFO: +++ initializing handle +++ + enable + Password: + R02# + 2018-02-13T12:20:53: %UNICON-INFO: connection to Sw03 + telnet 10.2.3.3 + Trying 10.2.3.3 ... Open + + User Access Verification + + Password: + Sw03> + + +**CLI proxy with standalone Unicon Connections** + +Below example code and abbreviated execution log shows how to instantiate the +Connection objects to create a proxied connection. + +.. code:: python + + >>> from unicon import Connection + >>> + >>> proxy_conn = Connection(hostname='lnx2', + ... start=['ssh lnx2'], + ... os='linux', + ... credentials={'default': {'username': 'admin', 'password': 'cisco'}}) + + >>> + >>> c = Connection(hostname='R01', + ... start=['telnet 10.3.3.1'], + ... os='ios', + ... credentials={'default': {'username': 'admin', 'password': 'cisco'}}) + ... proxy_connections=[proxy_conn]) + + >>> c.connect() + + 2018-02-13T12:56:30: %UNICON-INFO: connection via proxy lnx2 + + 2018-02-13T12:56:30: %UNICON-INFO: connection to lnx2 + + Linux$ + 2018-02-13T12:56:31: %UNICON-INFO: +++ initializing handle +++ + + 2018-02-13T12:56:31: %UNICON-INFO: connection to R01 + telnet 10.3.3.1 + Trying 10.3.3.1... + Connected to 10.3.3.1. + Escape character is '^]'. + + + User Access Verification + + Password: + R01> + 2018-02-13T12:56:31: %UNICON-INFO: +++ initializing handle +++ + + +**CLI proxy with Dual RP device** + +Below example code shows how to use CLI proxy for dual rp device. + +.. code:: yaml + + # Example with IOSXE Ha device - testbed.yaml + + devices: + Router: + alias: uut + os: iosxe + credentials: + default: + password: cisco + username: cisco + enable: + password: cisco + connections: + defaults: + class: unicon.Unicon + a: + protocol: telnet + ip: 1.1.1.1 + port: 2001 + proxy: jump_host + b: + protocol: telnet + ip: 172.27.114.25 + port: 2002 + proxy: jump_host + + jump_host: + alias: jh + connections: + cli: + ip: 2.2.2.2 + port: 22 + protocol: ssh + credentials: + default: + password: pyats + username: virl + os: linux + type: linux + +.. code:: python + + >>> # pyats shell --testbed-file testbed.yaml + >>> from genie.testbed import load + >>> testbed = load('testbed.yaml') + ------------------------------------------------------------------------------- + >>> d = testbed.devices['uut'] + >>> d.connect() + + 2020-08-14 14:08:15,959: %UNICON-INFO: +++ Router logfile /tmp/Router-cli-20200814T140815956.log +++ + + 2020-08-14 14:08:15,960: %UNICON-INFO: +++ Unicon plugin iosxe +++ + + 2020-08-14 14:08:15,995: %UNICON-INFO: +++ Router logfile /tmp/Router-cli-20200814T140815956.log +++ + + 2020-08-14 14:08:15,996: %UNICON-INFO: +++ Unicon plugin iosxe +++ + + 2020-08-14 14:08:16,033: %UNICON-INFO: +++ Router logfile /tmp/Router-cli-20200814T140815956.log +++ + + 2020-08-14 14:08:16,036: %UNICON-INFO: +++ Unicon plugin iosxe +++ + + 2020-08-14 14:08:16,039: %UNICON-INFO: connection via proxy jump_host + + 2020-08-14 14:08:16,053: %UNICON-INFO: +++ connection to spawn: ssh -l virl 2.2.2.2 -p 22, id: 139774725172192 +++ + + 2020-08-14 14:08:16,054: %UNICON-INFO: connection to jump_host + virl@2.2.2.2's password: + Welcome to Ubuntu 16.04.5 LTS (GNU/Linux 4.4.0-139-generic x86_64) + + Last login: Fri Aug 14 18:06:18 2020 from 10.0.10.1 + virl@cisco.com:~$ + + 2020-08-14 14:08:19,351: %UNICON-INFO: +++ initializing handle +++ + + + 2020-08-14 14:08:19,351: %UNICON-INFO: connection via proxy jump_host + + 2020-08-14 14:08:19,362: %UNICON-INFO: +++ connection to spawn: ssh -l virl 2.2.2.2 -p 22, id: 139774725151152 +++ + + 2020-08-14 14:08:19,363: %UNICON-INFO: connection to jump_host + virl@2.2.2.2's password: + Welcome to Ubuntu 16.04.5 LTS (GNU/Linux 4.4.0-139-generic x86_64) + + Last login: Fri Aug 14 18:08:19 2020 from 10.0.10.1 + virl@cisco.com:~$ + + 2020-08-14 14:08:22,638: %UNICON-INFO: +++ initializing handle +++ + + 2020-08-14 14:08:22,640: %UNICON-INFO: +++ connection to spawn: ssh -l virl 2.2.2.2 -p 22, id: 139774725172192 +++ + + 2020-08-14 14:08:22,641: %UNICON-INFO: +++ connection to spawn: ssh -l virl 2.2.2.2 -p 22, id: 139774725151152 +++ + telnet 1.1.1.1 2001 + Trying 1.1.1.1... + Connected to 1.1.1.1. + Escape character is '^]'. + + Router-stby# + Router-stby# + + telnet 1.1.1.1 2002 + Trying 1.1.1.1... + Connected to 1.1.1.1. + Escape character is '^]'. + + Router# + Router# + Router-stby# + >>> + + +.. _unicon_ssh_tunnel: + +SSH Tunnel +---------- + +The SSH tunnel feature uses the escape sequence feature of the `ssh` command +line client to create TCP tunnel connections via a (linux) server. This server +acts as a 'jumphost' or proxy device to connect to devices that are +reachable only through this server and not directly. + +Connections via the SSH tunnel feature make a TCP connection to the device via +the SSH connection. + +The current implementation supports connections from the SSH client host (i.e. +where the pyATS script runs) to devices behind the (linux) server in the lab. + +You can find more information on the escape sequence of the OpenSSH client here: +|ssh_link|. + +.. |ssh_link| raw:: html + + SSH escape characters + + +To configure a connection to use the SSH tunnel feature, configure ``sshtunnel`` key under +the connection and add the ``host`` key with the device name or server name as the value. + +The SSH tunnel host can be a testbed server or can be another device from the testbed. + + + +SSH tunnel with pyATS topology integration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Example topology file with a ``sshtunnel`` configuration for the ``a`` connection of device R2. + +.. code:: yaml + + testbed: + servers: + js: + address: 127.0.0.1 + credentials: + ssh: + username: cisco + password: cisco + custom: + port: 2222 + ssh_options: -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null + + devices: + R2: + os: ios + type: router + credentials: + default: + username: cisco + password: cisco + connections: + defaults: + class: unicon.Unicon + a: + protocol: ssh + ip: 10.0.0.1 + port: 22 + sshtunnel: + host: js + + +Example script and abbreviated connection log. + +.. code:: python + + >>> + >>> from pyats.topology import loader + >>> tb = loader.load('sshtunnel.yaml') + >>> r2 = tb.devices['R2'] + >>> r2.connect()) + 2018-03-29T18:19:26: %UNICON-INFO: Connecting proxy host js + + 2018-03-29T18:19:26: %UNICON-INFO: connection to js + + 2018-03-29T18:19:26: %UNICON-INFO: +++ connection to spawn_command: ssh -l cisco -p 2222 127.0.0.1 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null, id: 4440916152 +++ + + 2018-03-29T18:19:26: %UNICON-INFO: ssh -l cisco -p 2222 127.0.0.1 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null + Warning: Permanently added '[127.0.0.1]:2222' (RSA) to the list of known hosts. + Password: + + Linux$ + 2018-03-29T18:19:26: %UNICON-INFO: +++ initializing handle +++ + stty cols 200 + Linux$ stty rows 200 + Linux$ + 2018-03-29T18:19:26: %UNICON-INFO: Attaching all Subcommands + 2018-03-29T18:19:26: %UNICON-INFO: Adding tunnel 127.0.0.1:20001 for 10.0.0.1:22 + 2018-03-29T18:19:26: %UNICON-INFO: Device 'R2' connection 'a' via new SSH tunnel 127.0.0.1:20001 + + 2018-03-29T18:19:26: %UNICON-INFO: connection to R2 + + 2018-03-29T18:19:26: %UNICON-INFO: +++ connection to spawn_command: ssh -l cisco 127.0.0.1 -p 20001 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null, id: 4442821240 +++ + + 2018-03-29T18:19:26: %UNICON-INFO: ssh -l cisco 127.0.0.1 -p 20001 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null + Warning: Permanently added '[127.0.0.1]:20001' (RSA) to the list of known hosts. + Password: + Username: cisco + Password: cisco + R2> + 2018-03-29T18:19:26: %UNICON-INFO: +++ initializing handle +++ + enable + Password: cisco + R2# + 2018-03-29T18:19:26: %UNICON-INFO: +++ execute +++ + term length 0 + R2# + 2018-03-29T18:19:26: %UNICON-INFO: +++ execute +++ + term width 0 + R2# + + +**SSH tunnel with IPv6 target device** + +Below example topology file shows a router device that is reachable via IPv6 via the IPv4 jump host. + +Unicon will create a SSH connection to the jump host and create the IPv4 tunnel that connects to the IPv6 target device from the jump host. + +.. code:: yaml + + devices: + js: + os: linux + type: server + credentials: + default: + username: cisco + password: cisco + connections: + defaults: + class: unicon.Unicon + ssh: + protocol: ssh + ip: 10.0.0.1 + port: 22 + ssh_options: -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null + + R1: + os: ios + type: router + credentials: + default: + username: cisco + password: cisco + connections: + defaults: + class: unicon.Unicon + vty: + protocol: ssh + ip: 2001:abcd::1 + sshtunnel: + host: js + + + +SSH Tunnel topology schema +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code:: YAML + + devices: + + : + connections: + + : + sshtunnel: + # tunnel device name is required + host: + # optional settings + tunnel_ip: # default: 127.0.0.1 + tunnel_port: # default: automatic from port 20000 and up + + +SSH Tunnel with standalone Unicon Connections +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Below example code shows how to instantiate the +Connection objects to create a tunneled connection. + +.. code:: python + + from unicon import Connection + + proxy = Connection(hostname='linux', + start=['ssh jumphost'], + os='linux', + credentials={'default': {'username': 'cisco', 'password': 'cisco'}}) + proxy.connect() + + from unicon.sshutil import sshtunnel + + tunnel_port = sshtunnel.add_tunnel( + proxy_conn=proxy, + target_address='1.1.1.1', + target_port=23 + ) + + c = Connection(hostname='R1', + start=['telnet 127.0.0.1 {}'.format(tunnel_port)], + os='ios', + credentials={'default': {'username': 'cisco', 'password': 'cisco'}}) + c.connect() + + + +Limitations +----------- + +- UDP tunnels are currently not supported. + + + +.. sectionauthor:: Dave Wapstra + diff --git a/docs/user_guide/services/aci.rst b/docs/user_guide/services/aci.rst index 81902da9..be05e48c 100644 --- a/docs/user_guide/services/aci.rst +++ b/docs/user_guide/services/aci.rst @@ -1,23 +1,23 @@ ACI === -This section lists the services which are supported on Application Centric Infrastructure (ACI). +This section lists the services which are supported for Application Centric Infrastructure (ACI). * `execute <#execute>`__ * `configure <#configure>`__ * `reload <#reload>`__ -The ACI plugin supports only APIC and N9K (in ACI mode) using the `series` option. Specify ``aci`` -as `os` option and ``apic`` or ``n9k`` as the `series` option. +The ACI plugin supports only APIC and N9K (in ACI mode). Specify ``os=apic`` for APIC, specify +``os=nxos`` and ``platform=aci`` for N9K. .. note:: - The ``connect`` service for ACI plugin supports detection of the `setup` state of the APIC and + The ``connect`` service supports detection of the `setup` state of the APIC and `boot` state of the N9K switches in ACI mode. If the `connect()` service finds the devices in `setup` or `boot` state, it is up to the user to handle the transition to the `enable` state. -The following generic services are also avaiable: +The following generic services are also available: * `send`_ * `sendline`_ @@ -78,3 +78,11 @@ The default reload command for ACPI is `acidiag reboot`, the default reload command for N9K is `reload`. The `discovery_timeout` is only supported for N9K devices. + +*Settings* + +You can adjust the following timer settings for the APIC reload service: + +* `POST_RELOAD_WAIT` (default: 330) # How long to wait after the reload command has been executed +* `RELOAD_RECONNECT_ATTEMPTS` (default: 3) # After wait, how many times to try to connect +* `RELOAD_TIMEOUT` (default: 420) # Overall timeout for reload service diff --git a/docs/user_guide/services/asa_fp2k.rst b/docs/user_guide/services/asa_fp2k.rst new file mode 100644 index 00000000..0afdf2ea --- /dev/null +++ b/docs/user_guide/services/asa_fp2k.rst @@ -0,0 +1,145 @@ +ASA/FP2K +======== + +This section lists the services which are supported with FXOS Firepower 2000 series platform plugin. +This plugin is used when `os=fxos` and `platform=fp2k` are specified. + + * `execute <#execute>`__ + * `configure <#configure>`__ + * `fxos <#execute>`__ + * `fxos_mgmt <#execute>`__ + * `sudo <#execute>`__ + * `disable <#execute>`__ + * `enable <#execute>`__ + * `rommon <#execute>`__ + * `reload <#reload>`__ + * `switchto <#switchto>`__ + +The following generic services are also available: + + * send + * sendline + * expect + + +execute +------- + +The services ``fxos``, ``fxos_mgmt``, ``sudo``, ``disable``, ``enable``, ``rommon`` +are aliases to the execute service and are based on the generic execute implementation. +You can use these methods to switch between states and/or execute commands in that state. + +When the `execute()` method is used, there is no state change performed, the current state +is left 'as-is' and commands are executed in the state the device is in. + +For more information see `execute `__ + +Example usage of the execute services: + +.. code-block:: python + + # simple execute call + output = dev.execute("show version") + + # switch state and execute + dev.fxos() + dev.execute('show version') + dev.execute('another command') + + # switch state and execute combined + dev.fxos_mgmt('show version') + + # bring device to rommon and execute command in rommon mode + # Note: the device will stay in rommon unless the state is switched + dev.rommon('help') + + +configure +--------- + +For more information see `configure `__ + + + +reload +------ + +The reload service executes a device reboot via the `enable` prompt. This works with console connections +and with SSH based connections. When SSH is used, the service automatically disconnects and reconnects. +The console output is captured and returned to the caller. + +=============== ======================= ================================================================ +Argument Type Description +=============== ======================= ================================================================ +reload_command str reload command to be issued on device. + default reload_command is "reboot" +reply Dialog additional dialogs/new dialogs which are not handled by default. +timeout int timeout value in sec, Default value is 600 sec +=============== ======================= ================================================================ + +The following settings can be updated to influence the timers used: + +.. code-block:: + + # Timeout for console based reboot + BOOT_TIMEOUT (default: 600 seconds) + + # How many seconds to wait before trying to reconnect after rebooting the device + RELOAD_WAIT (default: 420 seconds) + + # How many times to try to reconnect + RELOAD_RECONNECT_ATTEMPTS (default: 3) + + +Example execution: + +.. code-block:: python + + # console output is returned + output = dev.reload() + + +switchto +-------- + +The `switchto` service is a helper method to switch between CLI states. This can be used to switch +to more specific states than e.g. the ``fxos`` method. + +Supported states are: + +* `fxos` +* `fxos scope \` +* `fxos admin` +* `fxos root` (sudo) +* `disable` +* `enable` +* `rommon` +* `sudo` +* `config` + +=================== ======================== ==================================================== +Argument Type Description +=================== ======================== ==================================================== +to_state str or list target state(s) to switch to +timeout int (default 60 sec) timeout value for the command execution takes. +=================== ======================== ==================================================== + +The ``fxos`` state allows to specify a `scope` to switch to, e.g. `/system/services`. +See below for an example. + +Example usage of the execute services: + +.. code-block:: python + + # switch to fxos state + dev.switchto("fxos") + + # switch to sudo state + dev.switchto("sudo") + + # switch to specific scope in fxos state + dev.switchto("fxos scope /system/services") + + # switch via several states + # this switches to fxos, then ftd and then sudo + dev.switchto(['fxos', 'sudo']) diff --git a/docs/user_guide/services/cimc.rst b/docs/user_guide/services/cimc.rst index 178e3b70..18b7e5bd 100644 --- a/docs/user_guide/services/cimc.rst +++ b/docs/user_guide/services/cimc.rst @@ -5,7 +5,7 @@ This section lists the services which are supported on Cisco Integrated Manageme * `execute <#execute>`__ -The following generic services are also avaiable: +The following generic services are also available: * `send`_ * `sendline`_ diff --git a/docs/user_guide/services/confd.rst b/docs/user_guide/services/confd.rst index 77570e5d..caf2d5d5 100644 --- a/docs/user_guide/services/confd.rst +++ b/docs/user_guide/services/confd.rst @@ -3,7 +3,7 @@ ConfD This section lists the services which are supported with ConfD based CLI. This plugin can be used with NSO, CSP, ESC and NFVIS. For the CSP, ESC and NFVIS plugins, specify the -series as `csp`, `esc` or `nfvis` respectively. +platform as `csp`, `esc` or `nfvis` respectively. * `execute <#execute>`__ * `configure <#configure>`__ @@ -16,7 +16,6 @@ The following generic services are also available: * `send `__ * `sendline `__ * `expect `__ - * `expect_log `__ * `log_user `__ @@ -118,13 +117,13 @@ automatically detect CLI state changes. You can use 'config', 'exit', 'end' and commands to switch CLI state or CLI style, this will be detected automatically. When you execute a command using the 'execute' service, the CLI style that is active before -exection will be restored at the end of the execution. This means that you cannot use +execution will be restored at the end of the execution. This means that you cannot use the `execute` service to switch styles, use the `cli_style` service for to change CLI style. -Executing the commmand `switch cli` by itself will raise an exception and point to cli_style. +Executing the command `switch cli` by itself will raise an exception and point to cli_style. You *can* use the 'switch cli' command as part of a series of commands to be executed. The commands to execute can be specified as a single command, a newline separated list of -commands or a list of commands. +commands, or a list of commands. .. code-block:: python diff --git a/docs/user_guide/services/ftd.rst b/docs/user_guide/services/ftd.rst index 8784e1ca..a7edc894 100644 --- a/docs/user_guide/services/ftd.rst +++ b/docs/user_guide/services/ftd.rst @@ -1,7 +1,7 @@ FXOS/FTD ======== -This section lists the services which are supported with FXOS Firepower Threat Defence (FTD) Unicon plugin. This plugin is used when `os=fxos` and `series=ftd` are specified. +This section lists the services which are supported with FXOS Firepower Threat Defence (FTD) Unicon plugin. This plugin is used when `os=fxos` and `platform=ftd` are specified. * `execute <#execute>`__ * `switchto <#switchto>`__ @@ -11,7 +11,6 @@ The following generic services are also available: * send * sendline * expect - * expect_log * log_user diff --git a/docs/user_guide/services/fxos.rst b/docs/user_guide/services/fxos.rst index 724fc311..893465dd 100644 --- a/docs/user_guide/services/fxos.rst +++ b/docs/user_guide/services/fxos.rst @@ -2,58 +2,147 @@ FXOS ==== This section lists the services which are supported with Firepower Extensible Operating System (FXOS) Unicon plugin. +This plugin supports Firepower 2000 series. * `execute <#execute>`__ + * `ftd <#execute>`__ + * `fxos <#execute>`__ + * `fxos_mgmt <#execute>`__ + * `expert <#execute>`__ + * `sudo <#execute>`__ + * `disable <#execute>`__ + * `enable <#execute>`__ + * `rommon <#execute>`__ + * `config <#config>`__ + * `switchto <#switchto>`__ + * `reload <#reload>`__ The following generic services are also available: * send * sendline * expect - * expect_log * log_user execute ------- -This service is used to execute arbitrary commands on the device. It is -intended to execute non-interactive commands. In case you want to execute -an command that uses interactive responses use `reply` option to specify -the Dialog object that handles the responses. +The services ``ftd``, ``fxos``, ``fxos_mgmt``, ``expert``, ``sudo``, ``disable``, ``enable``, +``rommon`` are aliases to the execute service and are based on the generic execute implementation. +You can use these methods to switch between states and/or execute commands in that state. -============= ====================== ===================================================== -Argument Type Description -============= ====================== ===================================================== -command str, list command(s) to execute -timeout int (default 60 sec) (optional) timeout value for the overall interaction. -reply Dialog (optional) additional dialog object -============= ====================== ===================================================== +When the `execute()` method is used, there is no state change performed, the current state +is left 'as-is' and commands are executed in the state the device is in. -The `execute` service returns the output of the command in string format if a single command -is passed. If multiple commands are passed, the returned data is a dictionary with the commands -as keys and the responses as values. You can expect a TimeoutError, StateMachineError or -SubCommandFailure error in case anything goes wrong. +For more information see `execute `__ -The commands to execute can be specified as a single command, a newline separated list of -commands or a list of commands. +Example usage of the execute services: .. code-block:: python - >>> response = device.execute('show version') - >>> type(response) - - >>> + # simple execute call + output = dev.execute("show version") - >>> response = device.execute('show version\nshow arp') - >>> type(response) - - >>> + # switch state and execute + dev.fxos() + dev.execute('show version') + dev.execute('another command') - >>> response = device.execute(['show version','show arp']) - >>> type(response) - - >>> + # switch state and execute combined + dev.ftd('show version') + # bring device to rommon and execute command in rommon mode + # Note: the device will stay in rommon unless the state is switched + dev.rommon('help') +config +------ + +For more information see `configure `__ + + + +switchto +-------- + +The `switchto` service is a helper method to switch between CLI states. This can be used to switch +to more specific states than e.g. the ``fxos`` method. + +The following states are supported: + +* `fireos` +* `ftd` +* `fxos` +* `fxos mgmt` +* `expert` +* `sudo` +* `disable` +* `enable` +* `rommon` + +=================== ======================== ==================================================== +Argument Type Description +=================== ======================== ==================================================== +to_state str or list target state(s) to switch to +timeout int (default 60 sec) timeout value for the command execution takes. +=================== ======================== ==================================================== + +The ``fxos`` state allows to specify a `scope` to switch to, e.g. `/system/services`. +See below for an example. + +Example usage of the execute services: + +.. code-block:: python + + # switch to fxos state + dev.switchto("fxos") + + # switch to sudo state + dev.switchto("sudo") + + # switch to specific scope in fxos state + dev.switchto("fxos scope /system/services") + + # switch via several states + # this switches to fxos, then ftd and then sudo + dev.switchto(['fxos', 'ftd', 'sudo']) + + +reload +------ + +The reload service executes a device reboot via the ftd prompt. This works with console connections +and with SSH based connections. When SSH is used, the service automatically disconnects and reconnects. +The console output is captured and returned to the caller. + +=============== ======================= ================================================================ +Argument Type Description +=============== ======================= ================================================================ +reload_command str reload command to be issued on device. + default reload_command is "reboot" +reply Dialog additional dialogs/new dialogs which are not handled by default. +timeout int timeout value in sec, Default value is 600 sec +=============== ======================= ================================================================ + +The following settings can be updated to influence the timers used: + +.. code-block:: + + # Timeout for console based reboot + BOOT_TIMEOUT (default: 600 seconds) + + # How many seconds to wait before trying to reconnect after rebooting the device + RELOAD_WAIT (default: 420 seconds) + + # How many times to try to reconnect + RELOAD_RECONNECT_ATTEMPTS (default: 3) + + +Example execution: + +.. code-block:: python + + # console output is returned + output = dev.reload() diff --git a/docs/user_guide/services/fxos_fp4k.rst b/docs/user_guide/services/fxos_fp4k.rst new file mode 100644 index 00000000..156d08e1 --- /dev/null +++ b/docs/user_guide/services/fxos_fp4k.rst @@ -0,0 +1,45 @@ +FXOS/FP4K +========= + +This section lists the services which are supported with Firepower Extensible Operating System (FXOS) Unicon plugin +for Firepower 4000 series platforms. This plugin is used when `os=fxos` and `platform=fp4k` are specified. + +This plugin is based on the FXOS plugin, for other supported services, see `FXOS `__ + + * `switchto <#switchto>`__ + +Note: `reload` service has not been implemented at this time. + + +switchto +-------- + +The `switchto` service is a helper method to switch between CLI states. This can be used to switch +to more specific states than e.g. the ``fxos`` method. + +The following states are supported: + + * `enable` + * `disable` + * `config` + * `ftd` + * `expert` + * `sudo` + * `fxos` + * `fxos scope \` + * `fxos mgmt` + * `adapter []` + * `cimc []` + * `module [] [console|telnet]` + * `fxos switch` + * `adapter shell` + * `adapter shell fls` + * `adapter shell mcp` + +=================== ======================== ==================================================== +Argument Type Description +=================== ======================== ==================================================== +to_state str or list target state(s) to switch to +timeout int (default 60 sec) timeout value for the command execution takes. +=================== ======================== ==================================================== + diff --git a/docs/user_guide/services/fxos_fp9k.rst b/docs/user_guide/services/fxos_fp9k.rst new file mode 100644 index 00000000..f819ae03 --- /dev/null +++ b/docs/user_guide/services/fxos_fp9k.rst @@ -0,0 +1,7 @@ +FXOS/FP9K +========= + +This section lists the services which are supported with Firepower Extensible Operating System (FXOS) Unicon plugin +for Firepower 9000 series platforms. This plugin is used when `os=fxos` and `platform=fp9k` are specified. + +This plugin is based on the Firepower 4000 series, see `FXOS/FP4K `__ diff --git a/docs/user_guide/services/gaia.rst b/docs/user_guide/services/gaia.rst new file mode 100644 index 00000000..b5dc3f90 --- /dev/null +++ b/docs/user_guide/services/gaia.rst @@ -0,0 +1,87 @@ +Check Point Gaia OS +=================== + +This section lists the services which are supported with the Gaia OS (gaia) Unicon plugin. This plugin is used when `os=gaia` is specified. + + * `execute <#execute>`__ + * `switchto <#switchto>`__ + * `ping <#ping>`__ + * `traceroute <#traceroute>`__ + +The following generic services are also available: + + * send + * sendline + * expect + +**Supported CLI states** + +The gaia plugin supports two device CLI states: `clish` and `expert`: +The `switchto` service can be used to switch between CLI states. The initial state of the device +is detected on initial connection - both 'expert' and 'clish' are supported as valid device defaults. + +execute +------- + +This service is used to execute arbitrary commands on the device. It is +intended to execute non-interactive commands. In case you want to execute +an command that uses interactive responses use `reply` option to specify +the Dialog object that handles the responses. + +============= ====================== ===================================================== +Argument Type Description +============= ====================== ===================================================== +command str, list command(s) to execute +timeout int (default 60 sec) (optional) timeout value for the overall interaction. +reply Dialog (optional) additional dialog object +============= ====================== ===================================================== + +The `execute` service returns the output of the command in string format if a single command +is passed. If multiple commands are passed, the returned data is a dictionary with the commands +as keys and the responses as values. You can expect a TimeoutError, StateMachineError or +SubCommandFailure error in case anything goes wrong. + +The commands to execute can be specified as a single command, a newline separated list of +commands or a list of commands. + +.. code-block:: python + + >>> response = device.execute('show version all') + >>> type(response) + + >>> + + >>> response = device.execute('show version all\nshow arp dynamic all') + >>> type(response) + + >>> + + >>> response = device.execute(['show version all','show arp dynamic all']) + >>> type(response) + + >>> + + +switchto +-------- + +This service is used to switch to a specific device CLI state. Supported states are: + +* `clish` +* `expert` + +============= ====================== ===================================================== +Argument Type Description +============= ====================== ===================================================== +target str Target device CLI state +timeout int (default 60 sec) (optional) timeout value for the overall interaction. +============= ====================== ===================================================== + +Examples: + +.. code-block:: python + + >>> device.switchto('expert') + >>> + >>> device.switchto('clish') + >>> diff --git a/docs/user_guide/services/generic_services.rst b/docs/user_guide/services/generic_services.rst index c2f215b9..580dde2f 100644 --- a/docs/user_guide/services/generic_services.rst +++ b/docs/user_guide/services/generic_services.rst @@ -10,67 +10,9 @@ such services which are applicable on both HA and Non-HA platform. There could be cases when a particular platform supports more services than listed below or there could be some omissions as well. In that case please refer to the platform specific service documentations. For example -NXOS supports `vdc` handling APIs which are not relevant on other platfroms +NXOS supports `vdc` handling APIs which are not relevant on other platforms line XR or IOS etc. Also in case of linux we only have `execute` service. -**Error pattern handling** - -If you want to execute services that could fail to execute properly and you want to verify -this automatically using a specific error pattern, you can specify the `error_pattern` -option with a list of regular expressions to match on the output. This option is available -for the execute service. - -The regex pattern is matched using the python multiline option (re.M) so you can use -the start of line (`^`) character to match specific line output. - -.. code-block:: python - - >>> c.execute('show interface invalid', error_pattern=['^% Invalid']) - -If you want to avoid errors being detected with any command, you can set the settings object -`ERROR_PATTERN` to an empty list. The current generic default is an empty list. - -.. code-block:: python - - >>> from pyats.topology import loader - >>> - >>> tb = loader.load('testbed.yaml') - >>> ncs = tb.devices.ncs - >>> - >>> ncs.connect(via='cli') - >>> ncs.settings.ERROR_PATTERN=[] - -The default error patterns can be seen by printing the settings.ERROR_PATTERN attribute. - -.. code-block:: python - - >>> ncs.settings.ERROR_PATTERN - ['Error:', 'syntax error', 'Aborted', 'result false'] - -Alternatively, you can pass an empty list when executing a command to avoid error pattern checking. - -.. code-block:: python - - >>> c.execute('show command error', error_pattern=[]) - -**EOF Exception handling** - -If device connection is closed/terminated unexpectedly during service calling, we can reconnect -to device. EOF exception is raised by Spawn when connection is not available. - -Sample usage: - -.. code-block:: python - - from unicon.core.errors import EOF, SubCommandFailure - try: - d.execute(cmd) # or any service call. - except SubCommandFailure as e: - if isinstance(e.__cause__, EOF): - print('Connection closed, try reconnect') - d.disconnect() - d.connect() - execute ------- @@ -89,7 +31,7 @@ Refer :ref:`prompt_recovery_label` for details on prompt_recovery feature. .. note:: - Not all platforms allow command exection on the standby RP as it + Not all platforms allow command execution on the standby RP as it may not be possible to unlock the standby RP. Please check before using this option. @@ -106,25 +48,26 @@ If you want to pass a multiline string as a single command, you should pass a list where the list item as a multiline string, see example below. -=================== ======================== ==================================================== -Argument Type Description -=================== ======================== ==================================================== -timeout int (default 60 sec) timeout value for the command execution takes. -reply Dialog additional dialog -command str command to execute on device handle -target standby/active by default commands will be executed on active, - use target=standby to execute command on standby. -prompt_recovery bool (default False) Enable/Disable prompt recovery feature -error_pattern list List of regex strings to check output for errors. -search_size int (default 8K bytes) maximum size in bytes to search at the - end of the buffer -allow_state_change bool (default False) By default, end state should be same as start state. - If True, end state can be any valid state. -service_dialog Dialog service_dialog overrides the execute service - dialog. -matched_retries int (default 1) retry times if statement pattern is matched -matched_retry_sleep float (default 0.05 sec) sleep between matched_retries -=================== ======================== ==================================================== +==================== ======================== ==================================================== +Argument Type Description +==================== ======================== ==================================================== +timeout int (default 60 sec) timeout value for the command execution takes. +reply Dialog additional dialog +command str command to execute on device handle +target standby/active by default commands will be executed on active, + use target=standby to execute command on standby. +prompt_recovery bool (default False) Enable/Disable prompt recovery feature +error_pattern list List of regex strings to check output for errors. +append_error_pattern list List of regex strings append to error_pattern. +search_size int (default 8K bytes) maximum size in bytes to search at the + end of the buffer +allow_state_change bool (default False) By default, end state should be same as start state. + If True, end state can be any valid state. +service_dialog Dialog service_dialog overrides the execute service + dialog. +matched_retries int (default 1) retry times if statement pattern is matched +matched_retry_sleep float (default 0.05 sec) sleep between matched_retries +==================== ======================== ==================================================== By default, device start state should be same as end state. For example, if we use `execute()` service when device is at enable state then after running the command, @@ -210,23 +153,24 @@ is specified as standby. Use `prompt_recovery` argument for using on prompt_recovery feature. -================ ======================= ======================================== -Argument Type Description -================ ======================= ======================================== -timeout int (default 60 sec) timeout value for the command execution takes. -error_pattern list List of regex strings to check output for errors. -reply Dialog additional dialog -command list list of commands to configure -prompt_recovery bool (default False) Enable/Disable prompt recovery feature -force bool (default False) For XR, run commit force at end of config. -replace bool (default False) For XR, run commit replace at end of config. -lock_retries int (default 0) retry times if config mode is locked -lock_retry_sleep int (default 2 sec) sleep between lock_retries -target str (default "active") Target RP where to execute service, for DualRp only -bulk bool (default False) If False, send all commands in one sendline. If True, send commands in chunked mode -bulk_chunk_lines int (default 50) maximum number of commands to send per chunk, 0 means to send all commands in a single chunk -bulk_chunk_sleep float (default 0.5 sec) sleep between sending command chunks -================ ======================= ======================================== +==================== ======================= ======================================== +Argument Type Description +==================== ======================= ======================================== +timeout int timeout value for the command execution takes. +error_pattern list List of regex strings to check output for errors. +append_error_pattern list List of regex strings append to error_pattern. +reply Dialog additional dialog +command list list of commands to configure +prompt_recovery bool (default False) Enable/Disable prompt recovery feature +force bool (default False) For XR, run commit force at end of config. +replace bool (default False) For XR, run commit replace at end of config. +lock_retries int (default 0) retry times if config mode is locked +lock_retry_sleep int (default 2 sec) sleep between lock_retries +target str (default "active") Target RP where to execute service, for DualRp only +bulk bool (default False) If False, send all commands in one sendline. If True, send commands in chunked mode +bulk_chunk_lines int (default 50) maximum number of commands to send per chunk, 0 means to send all commands in a single chunk +bulk_chunk_sleep float (default 0.5 sec) sleep between sending command chunks +==================== ======================= ======================================== @@ -246,7 +190,7 @@ bulk_chunk_sleep float (default 0.5 sec) sleep between sending command chunk rtr.configure(cmd, force=True) rtr.configure(cmd, replace=True) -For `(os='iosxe', series='sdwan')` plugin, `configure()` service issue `config-transaction` +For `(os='iosxe', platform='sdwan')` plugin, `configure()` service issue `config-transaction` command in place of `'config term` and run `commit` command before moving out of config mode. .. code-block:: python @@ -322,8 +266,8 @@ return : expect ------ -Match a list of patterns against the buffer . If target is passed as standby, -patterns matchs against the buffer on standby spawn channel. +Match a list of patterns against the buffer. If target is passed as standby, +patterns matches against the buffer on standby spawn channel. =========== =========== ======================================== Argument Type Description @@ -421,43 +365,15 @@ This service takes no arguments. expect_log ---------- -Service to enable expect internal logging. - - -========== ======================= ======================================== -Argument Type Description -========== ======================= ======================================== -enable bool to enable or disable expect internal logs -filename str filename to which logs will be logged -logto str (stadout/file/both) to log logs only on file/console/both. - By default it enables on both file and screen, - provided filename is specified. If not it will - log the message on screen. -========== ======================= ======================================== - +This service is removed. Please use Connection logger setLevel API +to enable/disable internal debug logging. .. code-block:: python - Example:: - - rtr.expect_log(filename='/ws/lshekhar-bgl/rtr-expect.log', enable=True) - rtr.execute("term length 0") - - Expect Sending term length 0 - Expect Got :: 'term len' - Expect Got :: 'gth 0\r\r\n\rn7k2-1# ' - Expect Got :: 'term length 0\r\r\n\rn7k2-1# ' - Pattern Matched:: ^(.*?)(n7k2-1|Router|RouterRP|RouterRP-standby|n7k2-1-standby|n7k2-1\(standby\)|n7k2-1-sdby|(S|s)witch|Controller|ios|-Slot[0-9]+)(\(boot\))*#\s?$ - Pattern List:: ['^.*--\\s?[Mm]ore\\s?--', '^.*\\[confirm\\(y/n\\)?\\]', '^.*\\[yes/no\\]\\s?:?$', '^(.*?)(n7k2-1|Router|RouterRP|RouterRP-standby|n7k2-1-standby|n7k2-1\\(standby\\)|n7k2-1-sdby|(S|s)witch|Controller|ios|-Slot[0-9]+)(\\(boot\\))*#\\s?$'] - - rtr.execute("term width 511") + Example :: - Expect Sending term width 511 - Expect Got :: 'term width 511\r\r\n' - Expect Got :: '\rn7k2-1# ' - Expect Got :: 'term width 511\r\r\n\rn7k2-1# ' - Pattern Matched:: ^(.*?)(n7k2-1|Router|RouterRP|RouterRP-standby|n7k2-1-standby|n7k2-1\(standby\)|n7k2-1-sdby|(S|s)witch|Controller|ios|-Slot[0-9]+)(\(boot\))*#\s?$ - Pattern List:: ['^.*--\\s?[Mm]ore\\s?--', '^.*\\[confirm\\(y/n\\)?\\]', '^.*\\[yes/no\\]\\s?:?$', '^(.*?)(n7k2-1|Router|RouterRP|RouterRP-standby|n7k2-1-standby|n7k2-1\\(standby\\)|n7k2-1-sdby|(S|s)witch|Controller|ios|-Slot[0-9]+)(\\(boot\\))*#\\s?$', '^.*--\\s?[Mm]ore\\s?--', '^.*\\[confirm\\(y/n\\)?\\]', '^.*\\[yes/no\\]\\s?:?$', '^(.*?)(n7k2-1|Router|RouterRP|RouterRP-standby|n7k2-1-standby|n7k2-1\\(standby\\)|n7k2-1-sdby|(S|s)witch|Controller|ios|-Slot[0-9]+)(\\(boot\\))*#\\s?$'] + rtr.connect() + rtr.log.setLevel(logging.DEBUG) log_user @@ -491,7 +407,7 @@ Return `True`, if file handler updated with new filename. Example :: rtr.log_file(filename='/some/path/uut.log') - rtr.log_file() # Returns currect FileHandler filename + rtr.log_file() # Returns current FileHandler filename enable @@ -499,9 +415,11 @@ enable Service to change the device mode to enable from any state. Brings the standby handle to enable state, if standby is passed as input. +If command is given, it will be issued on the device to become in enable mode. arg : * target='standby' + * command='enable 7' return : * True on Success, raise SubCommandFailure on failure. @@ -513,6 +431,7 @@ handle to enable state, if standby is passed as input. rtr.enable() rtr.enable(target='standby') + rtr.enable(command='enable 7') disable @@ -548,7 +467,7 @@ Argument Description addr Destination address proto protocol(ip/ipv6) count Number of pings to transmit -src_add IP for source field in ping packet +source Source address or interface data_pat data pattern that would be used to perform ping. dest_end ending network 127 address dest_start beginning network 127 address @@ -567,7 +486,7 @@ tunnel Tunnel interface number tos TOS field value multicast multicast addr udp (y/n) enable/disable UDP transmission for ipv6. -int Interface +interface Interface vcid VC Identifier topo topology nam verbose (y/n) enable/disable verbose mode @@ -606,6 +525,37 @@ record_hops Number of hops output = ping(addr="9.33.11.41") output = ping(addr="10.2.1.1", extd_ping='yes') + +switchto +-------- + +The `switchto` service is a helper method to switch between CLI states. This can be used to switch +to known states in the statemachine, e.g. 'enable' or 'rommon' (if supported by the plugin). + +=================== ======================== ==================================================== +Argument Type Description +=================== ======================== ==================================================== +to_state str or list target state(s) to switch to +timeout int (default 60 sec) timeout value for the command execution takes. +=================== ======================== ==================================================== + +.. code-block:: python + + #Example + -------- + + >>> dev.state_machine.states + [disable, enable, config, rommon, shell] + >>> + >>> dev.switchto('config') + + %UNICON-INFO: +++ switchto: config +++ + config term + R1(conf)# + >>> + + + traceroute ---------- @@ -714,18 +664,23 @@ due to console messages over terminal and this results in reload timeout. In such a case `prompt_recovery` can be used to recover the device. Refer :ref:`prompt_recovery_label` for details on prompt_recovery feature. -=============== ======================= ======================================== -Argument Type Description -=============== ======================= ======================================== -reload_command str reload command to be issued on device. - default reload_command is "reload" -dialog Dialog additional dialogs/new dialogs which are not handled by default. -timeout int timeout value in sec, Default Value is 300 sec -reload_creds list or str ('default') Credentials to use if device prompts for user/pw. -prompt_recovery bool (default False) Enable/Disable prompt recovery feature -return_output bool (default False) Return namedtuple with result and reload command output - This option is available for generic, nxos and iosxe/cat3k (single rp) plugin. -=============== ======================= ======================================== +===================== ======================= ==================================================================================== +Argument Type Description +===================== ======================= ==================================================================================== +reload_command str reload command to be issued on device. + default reload_command is "reload" +reply Dialog additional dialogs/new dialogs which are not handled by default. +timeout int timeout value in sec, Default Value is 300 sec +reload_creds list or str ('default') Credentials to use if device prompts for user/pw. +prompt_recovery bool (default False) Enable/Disable prompt recovery feature +return_output bool (default False) Return namedtuple with result and reload command output + This option is available for generic, nxos and iosxe/cat3k (single rp) plugin. +image_to_boot str Image to boot from rommon. Available for iosxe/cat3k and iosxe/cat9k +error_pattern list List of regex strings to check output for errors. +append_error_pattern list List of regex strings append to error_pattern. +post_reload_wait_time int (default 60) Number of seconds to wait after reload, before reconnecting, + Default Value is 60 sec +===================== ======================= ==================================================================================== return : * True on Success @@ -769,12 +724,20 @@ Service to execute commands in the router Bash. ``bash_console`` gives you a router-like object to execute commands on using python context managers. -========== ====================== ======================================== -Argument Type Description -========== ====================== ======================================== -timeout int (default 60 sec) timeout in sec for executing commands -target str 'standby' to bring standby console to bash. -========== ====================== ======================================== +=========== ====================== ======================================== +Argument Type Description +=========== ====================== ======================================== +timeout int (default 60 sec) timeout in sec for executing commands +target str 'standby' to bring standby console to bash. +enable_bash bool (default: True) enable bash service on device. +switch str switch to connect to (optional) +rp str rp to connect to (optional) +chassis str chassis to connect to (optional) +=========== ====================== ======================================== + +Bash service will be enabled by default on devices that require the service to +be configured (e.g. NXOS). Bash configuration will be done on first invocation +of the bash_console service. .. code-block:: python @@ -787,6 +750,46 @@ target str 'standby' to bring standby console to bas output1 = bash.execute('ls', target='standby') output2 = bash.execute('pwd', target='standby' ) + # connect bash console on standby RP + with device.bash_console(switch='standby', rp='active') as bash: + output1 = bash.execute('ls') + + # connect bash console on active chassis + with device.bash_console(chassis='active r0') as bash: + output1 = bash.execute('ls') + output2 = bash.execute('pwd') + + # connect bash console on standby chassis + with device.bash_console(chassis='standby r0') as bash: + output1 = bash.execute('ls') + output2 = bash.execute('pwd') + + +guestshell +---------- + +Service to execute commands in the Linux "guest shell" available on certain +NXOS and IOSXE platforms. ``guestshell`` gives you a router-like object to execute +commands on using a Python context manager. + +================= ======== =================================================================== +Argument Type Description +================= ======== =================================================================== +enable_guestshell boolean Explicitly enable the guestshell before attempting to enter. +timeout int (10) Timeout for "guestshell enable", "guestshell", and "exit" commands. +retries int (20) Number of retries (x 5 second interval) to attempt to enable guestshell. +================= ======== =================================================================== + +.. code-block:: python + + with device.guestshell(enable_guestshell=True, retries=30) as gs: + output = gs.execute("ifconfig") + + with device.guestshell() as gs: + output1 = gs.execute('pwd') + output2 = gs.execute('ls -al') + + Dual RP Services @@ -863,6 +866,31 @@ Service return running configuration of the device. rtr.get_config(target='standby') +guestshell +---------- + +Service to execute commands in the Linux "guest shell" available on certain +NXOS and IOSXE platforms. ``guestshell`` gives you a router-like object to execute +commands on using a Python context manager. + +================= ======== =================================================================== +Argument Type Description +================= ======== =================================================================== +enable_guestshell boolean Explicitly enable the guestshell before attempting to enter. +timeout int (10) Timeout for "guestshell enable", "guestshell", and "exit" commands. +retries int (20) Number of retries (x 5 second interval) to attempt to enable guestshell. +================= ======== =================================================================== + +.. code-block:: python + + with device.guestshell(enable_guestshell=True, retries=30) as gs: + output = gs.execute("ifconfig") + + with device.guestshell() as gs: + output1 = gs.execute('pwd') + output2 = gs.execute('ls -al') + + sync_state ---------- @@ -891,17 +919,17 @@ Service to switchover the device. Refer :ref:`prompt_recovery_label` for details on `prompt_recovery` argument. -=============== ======================= ======================================== -Argument Type Description -=============== ======================= ======================================== -command str switchover command to be issued on device. - default command is "redundancy force-switchover" -dialog Dialog additional dialogs/new dialogs which are not handled by default. -timeout int timeout value in sec, Default Value is 500 sec -sync_standby boolean Flag to decide whether to wait for standby to be UP or Not. default: True -prompt_recovery boolean Enable/Disable prompt recovery feature. Default is False. -reload_creds list or str ('default') Credentials to use if device prompts for user/pw. -=============== ======================= ======================================== +================ ======================= ========================================================================= +Argument Type Description +================ ======================= ========================================================================= +command str switchover command to be issued on device. + default command is "redundancy force-switchover" +reply Dialog additional dialogs/new dialogs which are not handled by default. +timeout int timeout value in sec, Default Value is 500 sec +sync_standby boolean Flag to decide whether to wait for standby to be UP or Not. default: True +prompt_recovery boolean Enable/Disable prompt recovery feature. Default is False. +switchover_creds list or str ('default') Credentials to use if device prompts for user/pw. +================ ======================= ========================================================================= return : * True on Success @@ -935,7 +963,7 @@ Argument Type Description =============== ========== ======================================== command str command to be issued on device. default command is "redundancy reload peer" -dialog Dialog additional dialogs/new dialogs which are not handled by default. +reply Dialog additional dialogs/new dialogs which are not handled by default. timeout int timeout value in sec, Default Value is 500 sec =============== ========== ======================================== @@ -955,3 +983,242 @@ timeout int timeout value in sec, Default Value is 500 sec # If command is other than 'redundancy reload peer' rtr.reset_standby_rp(command="command which invoke reload on standby-rp", timeout=600) + + + +Stack RP Services +================= + +In addition to the common services, following are applicable only for +*ha* platforms with *stack* RP. + + +get_rp_state +------------ + +Service to get the redundancy state of the device rp. Returns peer rp +state if peer rp alias is passed as input. + + +========== ====================== ======================================== +Argument Type Description +========== ====================== ======================================== +target str target rp to check rp state. Default value is `active` +timeout int (default 60 sec) timeout in sec for executing commands +========== ====================== ======================================== + +return : + + * Target rp state on Success. Possible states ACTIVE, STANDBY, MEMBER + + * raise SubCommandFailure on failure. + +.. code-block:: python + + #Example + -------- + + rtr.get_rp_state() + rtr.get_rp_state(target='standby') + + +switchover +---------- + +Service to switchover the stack device. + +Refer :ref:`prompt_recovery_label` for details on `prompt_recovery` argument. + + +=============== ======================= ======================================== +Argument Type Description +=============== ======================= ======================================== +command str switchover command to be issued on device. + default command is "redundancy force-switchover" +reply Dialog additional dialogs/new dialogs which are not handled by default. +timeout int timeout value in sec, Default Value is 600 sec +prompt_recovery boolean Enable/Disable prompt recovery feature. Default is False. +=============== ======================= ======================================== + + return : + * True on Success + + * raise SubCommandFailure on failure. + + +.. code-block:: python + + Example :: + + rtr.switchover() + + # If switchover command is other than 'redundancy force-switchover' + rtr.switchover(command="command which invoke switchover", + timeout=700) + + # using prompt_recovery option + rtr.switchover(prompt_recovery=True) + + +reload +------ + +Service to reload the stack device. + +==================== ======================= ================================================================================ +Argument Type Description +==================== ======================= ================================================================================ +reload_command str reload command to be issued on device. + default reload_command is "redundancy reload shelf" +reply Dialog additional dialogs/new dialogs which are not handled by default. +timeout int timeout value in sec, Default Value is 900 sec +image_to_boot str image to boot from rommon state +prompt_recovery bool (default False) Enable/Disable prompt recovery feature +return_output bool (default False) Return namedtuple with result and reload command output +raise_on_error bool (default: True) Raise exception on error +error_pattern list List of regex strings to check output for errors. +append_error_pattern list List of regex strings append to error_pattern. +member int the member to be reloaded. +==================== ======================= ================================================================================ + + return : + * True on Success + + * raise SubCommandFailure on failure. + + * If return_output is True, return a namedtuple with result and reload command output + +.. code-block:: python + + #Example + -------- + + rtr.reload() + # If reload command is other than 'redundancy reload shelf' + rtr.reload(reload_command="reload location all", timeout=400) + + # using prompt_recovery option + rtr.reload(prompt_recovery=True) + + # using return_output + result, output = rtr.reload(return_output=True) + + + +Quad RP Services +================ + +In addition to the common services, following are applicable only for +*ha* platforms with *quad* RP. + + +get_rp_state +------------ + +Service to get the redundancy state for the quad rp device. Returns target rp +state if target is passed as input. + + +========== ====================== ======================================== +Argument Type Description +========== ====================== ======================================== +target str target rp to check rp state. Default value is `active` +timeout int (default 60 sec) timeout in sec for executing commands +========== ====================== ======================================== + +return : + + * Target rp state on Success. Possible states ACTIVE, STANDBY, MEMBER, IN_CHASSIS_STANDBY + + * raise SubCommandFailure on failure. + +.. code-block:: python + + #Example + -------- + + rtr.get_rp_state() + rtr.get_rp_state(target='standby') + + +switchover +---------- + +Service to switchover the quad rp device. + +Refer :ref:`prompt_recovery_label` for details on `prompt_recovery` argument. + + +=============== ======================= ======================================== +Argument Type Description +=============== ======================= ======================================== +command str switchover command to be issued on device. + default command is "redundancy force-switchover" +reply Dialog additional dialogs/new dialogs which are not handled by default. +timeout int timeout value in sec, Default Value is 600 sec +sync_standby boolean Flag to decide whether to wait for standby to be UP or Not. default: True +prompt_recovery boolean Enable/Disable prompt recovery feature. Default is False. +=============== ======================= ======================================== + + return : + * True on Success + + * raise SubCommandFailure on failure. + + +.. code-block:: python + + Example :: + + rtr.switchover() + + # If switchover command is other than 'redundancy force-switchover' + rtr.switchover(command="command which invoke switchover", + timeout=700) + + # Switchover and not wait for standby to + rtr.switchover(sync_standby=False) + + # using prompt_recovery option + rtr.switchover(prompt_recovery=True) + + +reload +------ + +Service to reload the quad rp device. + +==================== ======================= ======================================== +Argument Type Description +==================== ======================= ======================================== +reload_command str reload command to be issued on device. + default reload_command is "reload" +reply Dialog additional dialogs/new dialogs which are not handled by default. +timeout int timeout value in sec, Default Value is 900 sec +prompt_recovery bool (default False) Enable/Disable prompt recovery feature +return_output bool (default False) Return namedtuple with result and reload command output +error_pattern list List of regex strings to check output for errors. +append_error_pattern list List of regex strings append to error_pattern. +==================== ======================= ======================================== + + return : + * True on Success + + * raise SubCommandFailure on failure. + + * If return_output is True, return a namedtuple with result and reload command output + +.. code-block:: python + + #Example + -------- + + rtr.reload() + # If reload command is other than 'reload' + rtr.reload(reload_command="reload location all", timeout=600) + + # using prompt_recovery option + rtr.reload(prompt_recovery=True) + + # using return_output + result, output = rtr.reload(return_output=True) diff --git a/docs/user_guide/services/index.rst b/docs/user_guide/services/index.rst index b3ea2feb..ce6e5bf8 100644 --- a/docs/user_guide/services/index.rst +++ b/docs/user_guide/services/index.rst @@ -9,16 +9,26 @@ This part of the document covers all the services supported by Unicon. what_are_services generic_services aci + asa_fp2k cimc confd ftd fxos + fxos_fp4k + fxos_fp9k + gaia + iosxe + iosxe_c9800_ewc_ap iosxr junos linux nso - nxos + nxos + nxos_mds + sdwan + sros staros vos + windows .. sectionauthor:: ATS Team diff --git a/docs/user_guide/services/iosxe.rst b/docs/user_guide/services/iosxe.rst new file mode 100644 index 00000000..e74be8d3 --- /dev/null +++ b/docs/user_guide/services/iosxe.rst @@ -0,0 +1,84 @@ +IOSXE +===== + +This section lists down all those services which are only specific to IOSXE. +For list of all the other service please refer this: +:doc:`Common Services `. + +rommon +------ + +Service to bring the device to rommon mode and execute commands (optional). +If commands are specified, the router will be brought to rommon mode and +the commands will be executed. If no commands are specified, +the router will be brought to rommon mode only. + +To bring the router back to enable mode, you can use the `enable()` service. +See examples below. + +The command to be executed can be passed as a multiline string or a list. + +=============== ======================= ======================================== +Argument Type Description +=============== ======================= ======================================== +command str or list command(s) to be issued on device. +reply Dialog additional dialogs/new dialogs which are not handled by default. +timeout int timeout value in sec, Default Value is 600 sec +prompt_recovery bool (default False) Enable/Disable prompt recovery feature +=============== ======================= ======================================== + + return : + * (str) command output + +.. code-block:: python + + # bring device to rommon mode + rtr.rommon() + + # specify timeout to bring device to rommon mode + rtr.rommon(timeout=1800) + + # execute command in rommon mode + rtr.rommon('MANUAL_BOOT=yes') + + # bring router to rommon mode + rtr.rommon() + + # execute rommon command + rtr.execute('MANUAL_BOOT=yes') + + # If the router is in rommon mode, you can use enable() + # to bring router to enable mode + + # boot with default boot command + rtr.enable() + # boot with specified image + rtr.enable(image='flash:packages.conf') + + +maintenance_mode +---------------- + +Service to bring the device to maintenance mode. +The service is intended to be used as a context manager. +see example below. + + +.. code-block:: python + + # using a context manager + with uut.maintenance_mode() as m: + m.execute('help'): + + # using switchto command + uut.switchto('maintenance') + uut.execute('help') + uut.switchto('enable') + +*Settings* + +You can adjust the following timer settings for the maintenance service: + +* `MAINTENANCE_MODE_WAIT_TIME` (default: 30) # How long to wait before sending enter to check the prompt +* `MAINTENANCE_MODE_TIMEOUT` (default: 2400) # Overall timeout for maintenance mode + diff --git a/docs/user_guide/services/iosxe_c9800_ewc_ap.rst b/docs/user_guide/services/iosxe_c9800_ewc_ap.rst new file mode 100644 index 00000000..ee364459 --- /dev/null +++ b/docs/user_guide/services/iosxe_c9800_ewc_ap.rst @@ -0,0 +1,53 @@ +IOSXE/C9800/EWC_AP +================== + +This section lists down all those services which are only specific to C9800/EWC_AP platform/model. + +For list of all the other service please refer this: +:doc:`Common Services `. + + +bash_console +------------ + +Service to execute commands in the router Bash. ``bash_console`` +gives you a router-like object to execute commands on using python context +managers. + +After entering bash shell, the commands in `BASH_INIT_COMMANDS` setting are executed. + +========== ====================== ======================================== +Argument Type Description +========== ====================== ======================================== +chassis int (default: 1) Chassis identifier to connect to +timeout int (default 60 sec) timeout in sec for executing commands +target str 'standby' to bring standby console to bash. +========== ====================== ======================================== + +.. code-block:: python + + with device.bash_console() as bash: + output1 = bash.execute('ls') + output2 = bash.execute('pwd') + + +ap_shell +-------- + +Service to bring the device to AP shell and execute commands. + +When entering the shell, the `HA_INIT_EXEC_COMMANDS` from the `cheetah/ap` plugin settings +will be executed. + +=============== ======================= ============================================= +Argument Type Description +=============== ======================= ============================================= +timeout int timeout value in sec, Default Value is 60 sec +=============== ======================= ============================================= + +.. code-block:: python + + # bring device to rommon mode + with device.ap_shell() as shell: + shell.execute('show version') + diff --git a/docs/user_guide/services/iosxr.rst b/docs/user_guide/services/iosxr.rst index 27397c35..3dee3f3d 100644 --- a/docs/user_guide/services/iosxr.rst +++ b/docs/user_guide/services/iosxr.rst @@ -125,6 +125,102 @@ Has same arguments as generic configure service. output = device.admin_configure('no logging console') +configure_exclusive +------------------- + +Service to configure device while locking the +router configuration. The system configuration can be made +only from the login terminal. +Has same arguments as generic configure service. + +.. code-block:: python + + output = device.configure_exclusive('logging console disable') + + +monitor +------- + +The monitor service can be used with the `monitor interface` command. You can +also pass `action` commands to execute while the monitor is running. For +example `clear` (lowercase) will send the key associated with the action as +shown in the output, i.e. Clear="c" will send "c" for action "clear". + +=============== ====================== ================================================== +Argument Type Description +=============== ====================== ================================================== +command str monitor command to execute ('monitor' is optional) + or action to send (e.g. 'clear') +reply Dialog additional dialog +timeout int (default 60 sec) timeout value for the overall interaction. +=============== ====================== ================================================== + +Example: + +.. code-block:: python + + rtr.monitor('monitor interface GigabitEthernet0/0/0/0') + + # execute `monitor interface` + rtr.monitor('interface') + + # tail the output for 10 seconds + rtr.monitor.tail(timeout=10) + + output = rtr.monitor.stop() + + # send an action to the device + rtr.monitor('clear') + rtr.monitor('bytes') + rtr.monitor('general') + rtr.monitor('IPv4 uni') # this can be called with 'ipv4 uni' or 'ipv4uni' as well. + + +monitor.get_buffer +~~~~~~~~~~~~~~~~~~ + +To get the output that has been buffered by the monitor service, you can use the `monitor.get_buffer` +method. This will return all output from the start of the monitor command until the moment of execution +of this service. + +===================== ====================== =================================================== +Argument Type Description +===================== ====================== =================================================== +truncate bool (default: False) If true, will truncate the current buffer. +===================== ====================== =================================================== + +.. code-block:: python + + output = rtr.monitor.get_buffer() + + +monitor.tail +~~~~~~~~~~~~ + +The monitor.tail method can be used to monitor the output logging after the ``monitor`` service +has been used to start the monitor. + +===================== ====================== =================================================== +Argument Type Description +===================== ====================== =================================================== +timeout int (seconds) maximum time to wait before returning output. +===================== ====================== =================================================== + +.. code-block:: python + + output = rtr.monitor.tail(timeout=30) + + +monitor.stop +~~~~~~~~~~~~ + +Stop the monitor and return all output. + +.. code-block:: python + + output = rtr.monitor.stop() + + Sub-Plugins ----------- @@ -132,7 +228,6 @@ Spitfire ^^^^^^^^ The spitfire sub plugin supports all services provided by :doc `Common Services `. -It currently doesnt support any of the DUAL RP Services . In addition to the common services spitfire also supports the following services @@ -140,7 +235,8 @@ attach_console """""""""""""" Service to attach to line card console/Standby RP to execute commands in. Returns a -router-like object to execute commands on using python context managers. +router-like object to execute commands on using python context managers.This service is +supported in HA as well. ==================== ====================== ======================================== Argument Type Description @@ -159,3 +255,32 @@ prompt str bash prompt (default # ) output1 = conn.execute('ls') output2 = conn.execute('pwd') +switchto +"""""""" + +Service to switch the router console to any state that user needs in order to perform +his tests. The api becomes a no-op if the console is already at the state user wants +to reach. This service is supported in HA as well. + + +The states available to switch to are : + +* enable +* config +* bmc +* xr_bash +* xr_run +* xr_env + +==================== ====================== ======================================== +Argument Type Description +==================== ====================== ======================================== +target_state str target state user wants the console at +timeout int (default in None) timeout in sec for executing commands +==================== ====================== ======================================== + +.. code-block:: python + + device.switchto("xr_env") + .... some commands that need to be run in xr_env state .... + device.switchto("enable") diff --git a/docs/user_guide/services/junos.rst b/docs/user_guide/services/junos.rst index 82dba2f3..9bf0f89b 100644 --- a/docs/user_guide/services/junos.rst +++ b/docs/user_guide/services/junos.rst @@ -2,17 +2,49 @@ Junos ===== This section lists down all those services which are only specific to Junos. + + * `configure <#configure>`__ + For a list of all the other services please refer to :doc:`Common Services `. -.. important:: +.. note:: - In argument table + Currently supports simplex ( non-HA ) devices only. - * values in parenthesis are default values. - * mandatory arguments are marked with `*`. +configure +--------- -.. note:: +For more information see `configure `__ - Currently supports simplex ( non-HA ) devices only. +The default commit command for Junos is `commit synchronize`. + +If you want to configure the device without automatically executing the commit command, +you can override the `commit_cmd` attribute for the configure service in the topology +file or set the `commit_cmd` service attribute in python directly. + +.. code:: yaml + + # Example override of commit_cmd for configure service + + devices: + EX1: + os: junos + type: router + connections: + cli: + protocol: telnet + ip: 127.0.0.1 + port: 64001 + service_attributes: + configure: + commit_cmd: "" + + +.. code:: python + + dev.configure.commit_cmd = "" + dev.configure('config commands') + dev.configure('more config commands') + dev.configure('commit') diff --git a/docs/user_guide/services/linux.rst b/docs/user_guide/services/linux.rst index 3975e77c..09b76d18 100644 --- a/docs/user_guide/services/linux.rst +++ b/docs/user_guide/services/linux.rst @@ -3,6 +3,36 @@ Linux This section lists the services which are supported on Linux. +** Prompt and Shell Prompt overriding ** + +By default, Unicon is able to detect most variations of the bash shell prompt. However, in +instances where another shell is being used (such as `zsh` or `fish`) it may have difficulty +in detecting your prompt thus leaving the connection hanging. In the event this occurs you +can override your prompt using the `PROMPT` setting in your testbed file like so: + +.. code-block:: yaml + + devices: + linux_device: + connections: + cli: + settings: + PROMPT: '' + +If `learn_hostname` is set to True, Unicon will attempt to learn and store the hostname +of you device in memory and switch the prompt to accommodate for that. It too can be overridden +with the `SHELL_PROMPT` setting like so: + +.. code-block:: yaml + + devices: + linux_device: + connections: + cli: + settings: + SHELL_PROMPT: '' + +Use `%N` in your regex to specify where the hostname should be located. execute ------- @@ -319,3 +349,77 @@ Example with custom error pattern to trigger exception. >>> +.. _linux_sudo: + +sudo +---- + +This service is used to execute commands using ``sudo`` on the device. This can be +used to get a root shell by using `device.sudo()`, as `sudo bash` is the default +command. + +=============== ====================== ============================================ +Argument Type Description +=============== ====================== ============================================ +command str command to execute with sudo (default: bash) +timeout int (default 60 sec) timeout value for the overall interaction. +reply Dialog additional dialog +prompt_recovery bool (default False) Enable/Disable prompt recovery feature +=============== ====================== ============================================ + +The sudo password can be specified in the testbed file under the `sudo` credentials. + +.. code-block:: yaml + + lnx: + os: linux + credentials: + default: + username: cisco + password: cisco + sudo: + password: sudo_password + + +Example with device.sudo(). + +.. code-block:: python + + In [3]: dev.sudo() + + 2021-08-04 14:36:53,472: %UNICON-INFO: +++ lnx with alias 'cli': executing command 'sudo bash' +++ + sudo bash + [sudo] password for cisco: + Linux# + Out[3]: '[sudo] password for cisco: ' + + In [4]: + + +Example with sudo command argument. + +.. code-block:: + + In [5]: dev.sudo('ls') + + 2021-08-04 14:37:58,397: %UNICON-INFO: +++ lnx with alias 'cli': executing command 'sudo ls' +++ + sudo ls + /tmp + /var + /opt + Linux$ + Out[5]: '/tmp\r\n/var\r\n/opt' + + In [6]: + +trex_console +------------ + +In order to use trex_console, the trex has to be installed and must be specified in $PATH. +Upon which you can use this service to execute trex-console commands + +.. code-block:: python + + with c.trex_console() as trex_cli: + output1 = trex_cli.execute('ls') + output2 = trex_cli.execute('help') diff --git a/docs/user_guide/services/nxos.rst b/docs/user_guide/services/nxos.rst index b390dd60..8fb98286 100644 --- a/docs/user_guide/services/nxos.rst +++ b/docs/user_guide/services/nxos.rst @@ -13,11 +13,55 @@ For list of all the other service please refer this: * mandatory arguments are marked with `*`. +bash_console +------------ + +Service to execute commands in the router Bash. ``bash_console`` +gives you a router-like object to execute commands on using python context +managers. + +=========== ====================== =========================================== +Argument Type Description +=========== ====================== =========================================== +timeout int (default 60 sec) timeout in sec for executing commands +target str 'standby' to bring standby console to bash. +enable_bash bool (default: True) enable bash service on device. +module str module to connect to (optional) +command str command to use with `run bash {command}` + (optional) +=========== ====================== =========================================== + +Bash service will be enabled by default on devices that require the service to +be configured. Bash configuration will be done on first invocation of the +bash_console service. + +You can specify the module name to use rlogin from the bash (root) shell to +connect to the module shell. The command will default to `sudo su`. + +.. code-block:: python + + with device.bash_console() as bash: + output1 = bash.execute('ls') + output2 = bash.execute('pwd') + + with device.bash_console(module='lc1') as bash: + output1 = bash.execute('ls') + output2 = bash.execute('pwd') + +To run commands in the root shell, use `command="sudo su"`. + +.. code-block:: python + + with device.bash_console(command='sudo su') as bash: + output1 = bash.execute('ls') + output2 = bash.execute('pwd') + + shellexec --------- -Service to execute commands on the Bourne-Again SHell (Bash). - +Service to execute commands on the Bourne-Again SHell (Bash). Similar to +``bash_console``. ========== ====================== ======================================== Argument Type Description @@ -62,10 +106,85 @@ to respond to the password prompt. Credentials are available in ``rtr.credentia cmd = ['sudo su root', 'uname -a', 'whoami', 'exit'] device.shellexec(cmd, reply=Dialog([password_stmt])) + +configure +--------- + +Service to execute commands on configuration mode. + +================ ======================== ==================================================== +Argument Type Description +================ ======================== ==================================================== +command list list of commands to configure +reply Dialog additional dialog +timeout int timeout value for the command execution takes. +error_pattern list List of regex strings to check output for errors. +prompt_recovery bool (default False) Enable/Disable prompt recovery feature +target str (default "active") Target RP where to execute service, for DualRp only +mode str (default: "default") Mode to configure ("default" or "dual") +================ ======================== ==================================================== + + +.. code-block:: python + + rtr.configure(['feature isis', 'commit'], mode="dual") + + # config dual-stage + # Enter configuration commands, one per line. End with CNTL/Z. + # R1(config-dual-stage)# feature isis + # R1(config-dual-stage)# commit + # Verification Succeeded. + + # Proceeding to apply configuration. This might take a while depending on amount of configuration in buffer. + # Please avoid other configuration changes during this time. + # Configuration committed by user 'admin' using Commit ID : 1000000002 + # R1(config-dual-stage)# end + # R1# + + +If you want to bring device to configure dual stage, you can use the `go_to` function in state machine +and use `'config_dual': True` as the context. The following is an example to do that. + +.. code-block:: python + + rtr.state_machine.go_to('config', rtr.spawn, context={'config_dual': True}) + + # config dual-stage + # Enter configuration commands, one per line. End with CNTL/Z. + # R1(config-dual-stage)# + + # execute command in configure dual stage + rtr.execute('no logging console') + + # R1(config-dual-stage)# no logging console + # R1(config-dual-stage)# + + +attach +------ + +Service to attach to line card to execute commands in. Returns a +router-like object to execute commands on using python context managers. + +==================== ====================== ================================================= +Argument Type Description +==================== ====================== ================================================= +module_num int module number to attach to +timeout int (default 60 sec) timeout in sec for executing commands +target standby/active by default commands will be executed on active, + use target=standby to execute command on standby. +==================== ====================== ================================================= + +.. code-block:: python + + with device.attach(1) as lc_1: + output1 = lc_1.execute('show interface') + + attach_console -------------- -Service to attach to line card console to execute commands in. Returns a +Service to attach to line card console to execute commands in. Returns a router-like object to execute commands on using python context managers. ==================== ====================== ======================================== @@ -76,7 +195,7 @@ login_name str name to login with, default: r default_escape_chars str default escape char, default: ~, change_prompt str new prompt to change to for ez automation timeout int (default 60 sec) timeout in sec for executing commands -prompt str bash prompt (defaut: bash-\d.\d# ) +prompt str bash prompt (default: bash-\d.\d# ) ==================== ====================== ======================================== .. code-block:: python @@ -96,7 +215,7 @@ Argument Description addr Destination address proto protocol(ip/ipv6) count Number of pings to transmit -src_add IP for source field in ping packet +source Source address or interface data_pat data pattern that would be used to perform ping. dest_end ending network 127 address dest_start beginning network 127 address @@ -115,7 +234,7 @@ tunnel Tunnel interface number tos TOS field value multicast multicast addr udp (y/n) enable/disable UDP transmission for ipv6. -int Interface +interface Interface vcid VC Identifier topo topology nam verbose (y/n) enable/disable verbose mode @@ -279,7 +398,7 @@ Most of the time simply providing the VDC name is just good enough. step-n7k-2-vdc1(config-console)# end step-n7k-2-vdc1# Out[3]: 'vdc1' -You see a relatively longer output becuase everytime it switches to a new VDC, +You see a relatively longer output because every time it switches to a new VDC, the terminal is reinitialized. .. note:: @@ -291,7 +410,7 @@ switchback ----------- It is just the opposite of `switchto`. It is used to return to the *default* -VDC. This sevice takes no mandatory arguments. +VDC. This service takes no mandatory arguments. ========== ====================== ============================= Argument Type Description @@ -413,31 +532,6 @@ command str (no vdc) alternate command. in. Isn't is obvious !! -guestshell ----------- - -Service to execute commands in the Linux "guest shell" available on certain -Nexus platforms. ``guestshell`` gives you a router-like object to execute -commands on using a Python context manager. - -================= ======== =================================================================== -Argument Type Description -================= ======== =================================================================== -enable_guestshell boolean Explicitly enable the guestshell before attempting to enter. -timeout int (10) Timeout for "guestshell enable", "guestshell", and "exit" commands. -retries int (20) Number of retries (x 5 second interval) to attempt to enable guestshell. -================= ======== =================================================================== - -.. code-block:: python - - with device.guestshell(enable_guestshell=True, retries=30) as gs: - output = gs.execute("ifconfig") - - with device.guestshell() as gs: - output1 = gs.execute('pwd') - output2 = gs.execute('ls -al') - - reload ------ @@ -460,6 +554,7 @@ config_lock_retries int (default 20) retry times if config mode config_lock_retry_sleep int (default 9 sec) sleep between config_lock_retries image_to_boot str n9k plugin only: boot from specified image if device goes into loader state reload_creds list or str ('default') Credentials to use if device prompts for user/pw. +reconnect_sleep int (default 60 sec) sleep time interval before reconnect device ======================= ======================= ======================================== return : @@ -484,3 +579,59 @@ reload_creds list or str ('default') Credentials to use if devi # using return_output result, output = rtr.reload(return_output=True) + +l2rib_pycl +---------- + +Layer 2 Routing Information Base (L2RIB) pyclient service. + +With this service, the l2rib tool can be used to execute l2rib_pycl commands. +The service is intended to be used as a context manager, see example below. + +======================= ======================= =============================================== +Argument Type Description +======================= ======================= =============================================== +client_id int or str (optional) Client identifier for l2rib_pycl tool. + By default, a random ID will be used. +======================= ======================= =============================================== + + +.. code-block:: python + + # default client ID (random) + with rtr.l2rib_pycl() as l2rib_pycl: + l2rib_pycl.execute('l2rib command') + + # specific client ID + with rtr.l2rib_pycl(client_id=1000) as l2rib_pycl: + l2rib_pycl.execute('l2rib command') + +l2rib_dt +-------- + +This service is similar to the l2rib_pycl service. +It will be deprecated in the future and users are encouraged to use the +l2rib_pycl service instead. + +Layer 2 Routing Information Base (L2RIB) developer tool service. + +With this service, the l2rib tool can be used to execute commands. The service +is intended to be used as a context manager, see example below. + +======================= ======================= =============================================== +Argument Type Description +======================= ======================= =============================================== +client_id int (optional) Client identifier for l2rib_dt tool. + By default, a random ID will be used. +======================= ======================= =============================================== + + +.. code-block:: python + + # default client ID (random) + with rtr.l2rib_dt() as l2rib: + l2rib.execute('l2rib command') + + # specific client ID + with rtr.l2rib_dt(client_id=1000) as l2rib: + l2rib.execute('l2rib command') diff --git a/docs/user_guide/services/nxos_mds.rst b/docs/user_guide/services/nxos_mds.rst new file mode 100644 index 00000000..87a151e7 --- /dev/null +++ b/docs/user_guide/services/nxos_mds.rst @@ -0,0 +1,31 @@ +NXOS/MDS +======== + +This section lists down all those services which are only specific to NXOS/MDS platforms. + + +tie +--- + +Service to execute commands on the Target Initiator Emulator (TIE) + +========== ======================== ================================================= +Argument Type Description +========== ======================== ================================================= +command str or list (default []) string or list of commands +timeout int (default 60 sec) timeout in sec for executing command on shell. +target standby/active by default commands will be executed on active, + use target=standby to execute command on standby. +========== ======================== ================================================= + +.. code-block:: python + + cmd = ['cmd1', 'cmd2'] + sw.tie(cmd) + +You can use this service as a context manager. + +.. code-block:: python + + with sw.tie() as tie: + tie.execute('cmd') diff --git a/docs/user_guide/services/sdwan.rst b/docs/user_guide/services/sdwan.rst new file mode 100644 index 00000000..b6537b1a --- /dev/null +++ b/docs/user_guide/services/sdwan.rst @@ -0,0 +1,63 @@ +SDWAN +====== + +The Software Defined Wide Area Network (SDWAN) OS plugin (`sdwan`) supports ``viptela`` devices. + +If you are using SDWAN on Viptela platforms, specify either one of below configs, they use the same plugin implementation. + +.. code-block:: yaml + + sdwan1: + os: sdwan + platform: viptela + connections: + cli: + protocol: ssh + ip: 1.2.3.4 + +.. code-block:: yaml + + sdwan2: + os: viptela + connections: + cli: + protocol: ssh + ip: 1.2.3.4 + + +This section lists the services which are supported: + + * `reload <#reload>`__ + +Both plugins support below generic services: + + * `execute `__ + * `configure `__ + * `send `__ + * `sendline `__ + * `expect `__ + * `log_user `__ + + +reload +------ + +Reload service for the sdwan/viptela plugin. When used on the console will return the reboot log. +Console sessions will be detected automatically based on the logs observed during the initial connection. + +============== ====================== ===================================================== +Argument Type Description +============== ====================== ===================================================== +reload_command str command to execute to reload the device +timeout int (default 600 sec) (optional) timeout value for the overall interaction. +reply Dialog (optional) additional dialog object +============== ====================== ===================================================== + +.. code-block:: python + + # When running on the console, the boot log will be returned. + boot_log = viptela.reload() + + +.. sectionauthor:: Dave Wapstra + diff --git a/docs/user_guide/services/sros.rst b/docs/user_guide/services/sros.rst new file mode 100644 index 00000000..0a4cb236 --- /dev/null +++ b/docs/user_guide/services/sros.rst @@ -0,0 +1,184 @@ +SROS +==== + +This section documents the services available for Nokia SR-OS (a.k.a. TiMOS). +The implementations to Nokia SR-OS follows documentation available at: +https://infocenter.nokia.com/public/7750SR160R1A/index.jsp?topic=%2Fcom.sr.mdcli%2Fhtml%2Fusing_mdcli.html + + +switch_cli_engine +----------------- + +API to switch CLI engine for this device connection + +========= ===== =========================================================== +Argument Type Description +========= ===== =========================================================== +engine str CLI engine name (mdcli, classiccli) +========= ===== =========================================================== + +.. code-block:: python + + # Example + # ------- + + # switch to md-cli + device.switch_cli_engine('mdcli') + + # switch to classic-cli + device.switch_cli_engine('classiccli') + +get_cli_engine +-------------- + +returns the current cli-engine set for this device connection. + +.. code-block:: python + + # Example + # ------- + + current_engine = device.get_cli_engine() + + +execute +------- + +Similar to generic "execute" service, this api runs arbitrary commands on the +target device, which yields output, and returns to prompt. + +This API will issue the provided command on **current** active CLI engine, +internally calling the respective "specific command". Eg: + +- if the device is in **MD-CLI** mode, issues command using ``mdcli_execute`` + +- if the device is in **classic-CLI** mode, issues command using + ``classiccli_execute`` + +.. code-block:: python + + # Example + # ------- + + # set to md-cli mode + device.switch_cli_engine('mdcli') + + # device.execute() will now issue command using mdcli mode + output = device.execute('show version') + + # switch back to classic cli mode, and issue classic-cli commands + device.switch_cli_engine('classiccli') + output = device.execute('show router interface "coreloop"') + +configure +--------- + +Similar to generic "configure" service, this api applies the provided config +to target device and commits it. + +This API will issue the provided command on **current** active CLI engine, +internally calling the respective "specific command". Eg: + +- if the device is in **MD-CLI** mode, issues command using ``mdcli_configure`` + +- if the device is in **classic-CLI** mode, issues command using + ``classiccli_configure`` + +This API accepts a positional argument ``mode`` (used by md-cli), specifying +the config mode. Defaults to ``mode='private'``. + +.. code-block:: python + + # Example + # ------- + + # set to md-cli + device.switch_cli_engine('mdcli') + + # apply configuration + output = device.configure('router interface coreloop ipv4 primary address 1.1.1.1 prefix-length 32') + + # apply configuration using specific configuration mode + # (default mode is 'private', and can be changed via configuration) + output = device.configure('delete router interface "coreloop" ipv4', mode='private') + + # switch to classic-cli & apply config + device.switch_cli_engine('classiccli') + output = device.configure('configure router interface "coreloop" address 111.1.1.1 255.255.255.255') + + +mdcli_execute +------------- + +The specific service that implements ``execute()`` api under MD-CLI + +.. code-block:: python + + # Example + # ------- + output = device.mdcli_execute('show version') + output = device.mdcli_execute('show router interface "coreloop"') + +mdcli_configure +--------------- + +The specific service that implements ``configure()`` api under MD-CLI + + +One more different argument from `configure` of "Common Services": + +========= ===== =========================================================== +Argument Type Description +========= ===== =========================================================== +mode str Configuration mode (exclusive, global, private, read-only) +========= ===== =========================================================== + +.. code-block:: python + + # Example + # ------- + + cmd = 'router interface coreloop ipv4 primary address 1.1.1.1 prefix-length 32' + output = device.mdcli_configure(cmd) # configure on default mode "private" + output = device.mdcli_configure(cmd, mode='global') # configure on mode "global" + device.mdcli_configure.mode = 'global' # change default mode to "global" + output = device.mdcli_configure(cmd) # configure on mode "global" + +classiccli_execute +------------------ + +The specific service that implements ``execute()`` api under Classic-CLI + +.. code-block:: python + + # Example + # ------- + + output = device.classiccli_execute('show version') + output = device.classiccli_execute('show router interface "coreloop"') + +classiccli_configure +-------------------- +The specific service that implements ``configure()`` api under classic-CLI + +.. code-block:: python + + # Example + # ------- + + cmd = 'configure router interface "coreloop" address 111.1.1.1 255.255.255.255' + output = device.classiccli_configure(cmd) + + + +Other Services +-------------- + +The following low-level, generic services are also supported for Nokia SR-OS. +See :doc:`Common Services ` documentation for usage details. + +- ``send`` +- ``sendline`` +- ``expect`` +- ``log_user`` +- ``log_file`` diff --git a/docs/user_guide/services/staros.rst b/docs/user_guide/services/staros.rst index 9fe76426..dbe6086c 100644 --- a/docs/user_guide/services/staros.rst +++ b/docs/user_guide/services/staros.rst @@ -5,13 +5,13 @@ This section lists the services which are supported on Starent OS (staros). * `execute <#execute>`__ * `configure <#configure>`__ + * `monitor <#monitor>`__ -The following generic services are also avaiable: +The following generic services are also available: * `send `__ * `sendline `__ * `expect `__ - * `expect_log `__ * `log_user `__ @@ -57,13 +57,11 @@ Service to configure device with list of `commands`. Config without config_command will take device to config mode. Commands can be a string or list. reply option can be passed for the interactive config command. Use `prompt_recovery` argument for using `prompt_recovery` feature. -Refer :ref:`prompt_recovery_label` for details -on prompt_recovery feature. =============== ====================== ======================================== Argument Type Description =============== ====================== ======================================== -timeout int (default 60 sec) timeout value for the command execution takes. +timeout int (default 30 sec) timeout value for the command execution takes. reply Dialog additional dialog command str or list string or list of commands to configure prompt_recovery bool (default False) Enable/Disable prompt recovery feature @@ -81,3 +79,112 @@ prompt_recovery bool (default False) Enable/Disable prompt recovery featu output = rtr.configure(cmd) + +monitor +------- + +The monitor service can be used with the `monitor subscriber` command. You can pass +keyword arguments to configure settings for the monitor command. + +=============== ====================== ======================================== +Argument Type Description +=============== ====================== ======================================== +command str monitor command to execute ('monitor' is optional) + or 'stop' to stop the monitor. + str Name of the option to set + str Name of the option to set +... +=============== ====================== ======================================== + +Example: + +.. code-block:: python + + mme.monitor('subscriber imsi 000110000000001', app_specific_diameter={'diameter_gy': 'on', 'diameter_gx_ty_gxx': 'on'}) + + mme.monitor('subscriber next-call', stun='on', sessmgr='on') + +All settings that follow the CLI output syntax of ``NN - Service name ( OFF)`` are +supported as long as the response for the status is similar to `*** Service name ( state) ***`. + +The `Service Name` will be translated to `service_name` for use with keyword arguments. +E.g. ``13 - RADIUS Auth (ON )`` can be updated using keyword argument `radius_auth='off'`. + +The option `app_specific_diameter` is a special case as it requires sub options to be +specified. You can pass sub options via a dictionary like this: + +.. code-block:: python + + mme.monitor('subscriber imsi 000110000000001', app_specific_diameter={'diameter_gy': 'on'}) + +Similar to standard options, the names are translated from e.g. `DIAMETER Gx/Ty/Gxx` to +`diameter_gx_ty_gxx`. + +Other non-standard options are `RADIUS Dictionary` and `GTPP Dictionary`, you can pass the +target value and the implementation will try to reach that by repeatedly sending the option +key(s) up to a maximum of known number of options. E.g. you can specify ``custom12`` as a +target for `radius_dictionary`. + +.. code-block:: python + + mme.monitor('subscriber imsi 000110000000001', radius_dictionary='custom12') + +The monitor service will start the command and return, you can use the sub-service ``monitor.tail`` +to monitor the output on the console. + +To stop the monitor and return the buffered output, use `output = mme.monitor('stop')` + +You can inspect the current state of the monitor settings via the ``monitor.monitor_state`` object. +This is a dictionary with all the settings and their current values. + +.. code-block:: python + + from pprint import pprint + pprint(mme.monitor.monitor_state) + + + +monitor.get_buffer +~~~~~~~~~~~~~~~~~~ + +To get the output that has been buffered by the monitor service, you can use the `monitor.get_buffer` +method. This will return all output from the start of the monitor command until the moment of execution +of this service. + +===================== ====================== =================================================== +Argument Type Description +===================== ====================== =================================================== +truncate bool (default: False) If true, will truncate the current buffer. +===================== ====================== =================================================== + +.. code-block:: python + + output = mme.monitor.get_buffer() + + +monitor.tail +~~~~~~~~~~~~ + +The monitor.tail method can be used to monitor the output logging after the ``monitor`` service +has been used to start the monitor. If you pass the option `return_on_match=True`, the +output will be returned when the call finished pattern (default: ``Call Finished``) is seen. + +===================== ====================== =================================================== +Argument Type Description +===================== ====================== =================================================== +timeout int (seconds) maximum time to wait before returning output. +pattern str (regex) Regex pattern to monitor for (default: .*Call Finished.*) +return_on_match bool (default: True) If True, returns output if pattern is seen. +stop_monitor_on_match bool (default: False) Stops the monitor session if True. +===================== ====================== =================================================== + + +.. code-block:: python + + output = mme.monitor.tail(timeout=300, return_on_match=True, stop_monitor_on_match=True) + + +monitor.stop +~~~~~~~~~~~~ + +Stop the monitor. diff --git a/docs/user_guide/services/vos.rst b/docs/user_guide/services/vos.rst index ce43199a..bbbc3d2d 100644 --- a/docs/user_guide/services/vos.rst +++ b/docs/user_guide/services/vos.rst @@ -10,12 +10,10 @@ The following generic services are also available: * `send`_ * `sendline`_ * `expect`_ - * `expect_log`_ .. _send: generic_services.html#send .. _sendline: generic_services.html#sendline .. _expect: generic_services.html#expect -.. _expect_log: generic_services.html#expect-log @@ -36,7 +34,7 @@ reply Dialog (optional) additional dialog lines int (default 100) (optional) number of lines to capture when paging =============== ====================== ===================================================== -Tehe `execute` service returns the output of the command in string format +The `execute` service returns the output of the command in string format or it raises an exception. You can expect a SubCommandFailure error in case anything goes wrong. @@ -46,7 +44,7 @@ The execute service will response to the following prompts automatically: * options: q=quit, n=next, p=prev, b=begin, e=end (lines 61 - 80 of 207554) : The response to the first prompt will be to send a space. For the second prompt, -paging will be done by sending `n` automtically for up to 100 lines by default. +paging will be done by sending `n` automatically for up to 100 lines by default. If you want to capture more lines, specify the `lines` option. The paging prompts will be stripped from the output. diff --git a/docs/user_guide/services/windows.rst b/docs/user_guide/services/windows.rst new file mode 100644 index 00000000..f3138a14 --- /dev/null +++ b/docs/user_guide/services/windows.rst @@ -0,0 +1,19 @@ +Windows +======= + +The Unicon Windows plugin allows you to connect to Windows systems with command line interface (CMD shell). + +This is an experimental plugin, there are some issues with ANSI stripping and powershell is not supported. + +The following generic services are available: + + * `execute`_ + * `send`_ + * `sendline`_ + * `expect`_ + +.. _execute: generic_services.html#execute +.. _send: generic_services.html#send +.. _sendline: generic_services.html#sendline +.. _expect: generic_services.html#expect + diff --git a/docs/user_guide/supported_platforms.rst b/docs/user_guide/supported_platforms.rst index 66d6722d..aeae35e7 100644 --- a/docs/user_guide/supported_platforms.rst +++ b/docs/user_guide/supported_platforms.rst @@ -1,58 +1,104 @@ Supported Platforms =================== -At the moment `unicon.plugins` supports the following network device types, -described as their OS (network operation system), series (platform series), and -model (specific model support). +At the moment `unicon.plugins` supports the following network device types, +described as their OS (network operation system), platform and +model (specific model support). These values help Unicon load the most accurate connection plugin for the given network device, and corresponds to ther pyATS testbed YAML counterparts. +For example, if ``os=iosxe`` and ``platform=abc``, since ``abc`` is not found in +the iosxe table, it will fallback to use the generic ``iosxe`` plugin. If +``os=iosxe`` and ``platform=cat3k``, it will use the specific plugin ``iosxe/cat3k``. + +.. tip:: + + The priority to pick up which plugin is: chassis_type > os > platform > model > submodel. + + +.. important:: + + The specific device definitions are being added in a `PID tokens`_ file to + explicitly match a device PID with the expected os, platform, model. Please + ensure that devices you are using are accurately represented as this will + serve as the source of truth for `Genie Abstract`_ as well in a near future + update. + +.. _PID tokens: https://github.com/CiscoTestAutomation/unicon.plugins/blob/master/src/unicon/plugins/pid_tokens.csv +.. _Genie Abstract: https://pubhub.devnetcloud.com/media/genie-docs/docs/abstract/index.html + .. csv-table:: Unicon Supported Platforms :align: center - :widths: 20, 20, 20, 40 - :header: "os", "series", "model", "Comments" + :widths: 20, 20, 20, 20, 40 + :header: "os", "platform", "model", "submodel", "Comments" - ``aci``, ``apic`` - ``aci``, ``n9k`` + ``apic`` ``aireos`` ``asa`` ``asa``, ``asav`` + ``asa``, ``fp2k`` ``cheetah``, ``ap`` ``cimc`` + ``comware`` ``confd`` ``confd``, ``esc`` ``confd``, ``nfvis`` - ``fxos`` - ``fxos``, ``ftd`` + ``dnos6`` + ``dnos10`` + ``fxos``,,,,"Tested with FP2K." + ``fxos``, ``fp4k`` + ``fxos``, ``fp9k`` + ``fxos``, ``ftd``,,,"Deprecated, please use one of the other fxos plugins." + ``gaia``, , , , "Check Point Gaia OS" + ``hvrp`` ``ios``, ``ap`` ``ios``, ``iol`` ``ios``, ``iosv`` + ``ios``, ``pagent``,,,"See example below." ``iosxe`` ``iosxe``, ``cat3k`` ``iosxe``, ``cat3k``, ``ewlc`` + ``iosxe``, ``cat8k`` + ``iosxe``, ``cat9k``, + ``iosxe``, ``cat9k``, ``c9500``, ``c9500x``, "See example below." + ``iosxe``, ``c9800`` + ``iosxe``, ``c9800``, ``ewc_ap`` ``iosxe``, ``csr1000v`` ``iosxe``, ``csr1000v``, ``vewlc`` + ``iosxe``, ``iec3400`` ``iosxe``, ``sdwan`` ``iosxr`` + ``iosxr``, ``asr9k`` ``iosxr``, ``iosxrv`` ``iosxr``, ``iosxrv9k`` ``iosxr``, ``moonshine`` ``iosxr``, ``ncs5k`` ``iosxr``, ``spitfire`` + ``ironware`` ``ise`` - ``linux``, , , "Generic Linux server with bash prompts" + ``linux``, , , , "Generic Linux server with bash prompts" + ``nd``, , , , "Nexus Dashboard (ND) Linux server. identical to os: linux" ``nxos`` ``nxos``, ``mds`` ``nxos``, ``n5k`` + ``nxos``, ``n7k`` ``nxos``, ``n9k`` ``nxos``, ``nxosv`` - ``nso`` + ``nxos``, ``aci`` + ``nso``,,,, "Network Service Orchestrator" + ``ons``,,,, "Optical Networking System" + ``sdwan``, ``viptela``,,,"Identical to os=viptela." + ``sros`` ``staros`` ``vos`` ``junos`` + ``eos`` + ``sros`` + ``viptela``,,,,"Identical to os=sdwan, platform=viptela." + ``windows``,,,,"Only command shell (cmd) is supported. Powershell is not supported" -To use this table - locate your device's os/series/model information, and fill +To use this table - locate your device's os/platform/model information, and fill your pyATS testbed YAML with it: .. code-block:: yaml @@ -73,8 +119,8 @@ your pyATS testbed YAML with it: .. tip:: - in the above example, ``series`` and ``model`` is not provided, hence Unicon - will use the most generic ``os==iosxe`` connection implementation for my + in the above example, ``platform`` and ``model`` is not provided, hence Unicon + will use the most generic ``os=iosxe`` connection implementation for my device. @@ -87,7 +133,7 @@ Example: Single Router devices: router_hostname: os: iosxe - series: csr1000v + platform: csr1000v model: vewlc type: iosxe credentials: @@ -116,7 +162,7 @@ Example: HA router devices: router_hostname: os: nxos - series: n9k + platform: n9k type: nxos credentials: default: @@ -138,6 +184,120 @@ Example: HA router ip: 2.2.2.2 +Example: Stack router +--------------------- + +**Stack router has connections peer_1, peer_2, peer_3** + +.. code-block:: yaml + + devices: + router_hostname: + os: iosxe + platform: cat3k + type: iosxe + chassis_type: stack <<< define the chassis_type as 'stack' + credentials: + default: + username: xxx + password: yyy + enable: + password: zzz + connections: + defaults: + class: unicon.Unicon + connections: [peer_1, peer_2, peer_3] <<< define the connections to use + peer_1: + protocol: telnet + ip: 1.1.1.1 + port: 2001 + member: 1 <<< peer rp id + peer_2: + protocol: telnet + ip: 1.1.1.1 + port: 2002 + member: 2 <<< peer rp id + peer_3: + protocol: telnet + ip: 1.1.1.1 + port: 2003 + member: 3 <<< peer rp id + +Example: Stackwise Virtual Router +--------------------------------- + +.. code-block:: yaml + + devices: + router_hostname: + os: iosxe + platform: cat9k + model: c9500 + submodel: c9500x + chassis_type: stackwise_virtual <<< define the chassis_type as 'stackwise_virtual' + credentials: + default: + username: xxx + password: yyy + enable: + password: zzz + connections: + defaults: + class: unicon.Unicon + a: + protocol: telnet + ip: 1.1.1.1 + port: 2001 + b: + protocol: telnet + ip: 1.1.1.1 + port: 2002 + +Example: Quad Sup router +------------------------ + +**Quad Sup router has two chassis 1, 2 and 4 connections a, b, c, d** + +.. code-block:: yaml + + devices: + router_hostname: + os: iosxe + platform: cat9k + type: iosxe + chassis_type: quad <<< define the chassis_type as 'quad' + credentials: + default: + username: xxx + password: yyy + enable: + password: zzz + connections: + defaults: + class: unicon.Unicon + connections: [a, b, c, d] <<< define the connections to use + a: + protocol: telnet + ip: 1.1.1.1 + port: 2001 + member: 1 <<< chassis id + b: + protocol: telnet + ip: 1.1.1.1 + port: 2002 + member: 2 <<< chassis id + c: + protocol: telnet + ip: 1.1.1.1 + port: 2003 + member: 1 <<< chassis id + d: + protocol: telnet + ip: 1.1.1.1 + port: 2004 + member: 2 <<< chassis id + + Example: Linux Server --------------------- @@ -155,3 +315,42 @@ Example: Linux Server linux: protocol: ssh ip: 2.2.2.2 + + +Example: IOS Pagent +------------------- + +The ios/pagent plugin requires the ``pagent_key`` to be specified +as an argument to connection. When the device transitions to enable state +the plugin enters the pagent key for you. + +.. code-block:: yaml + + device.connect(pagent_key='123412341234') + +Alternatively, you could specify the pagent key as an argument in your +pyATS testbed YAML: + +.. code-block:: yaml + + # Example + # ------- + # + # testbed yaml for a single pagent device using Unicon + + device1: + os: 'ios' + platform: 'pagent' + type: 'router' + credentials: + default: + username: lab + password: lab + connections: + a: + protocol: telnet + ip: 10.64.70.11 + port: 2042 + + arguments: + pagent_key: '123412341234' diff --git a/setup.py b/setup.py index 016b0a38..f05dcf46 100755 --- a/setup.py +++ b/setup.py @@ -28,9 +28,14 @@ def build_version_range(version): for any given version, return the major.minor version requirement range eg: for version '3.4.7', return '>=3.4.0, <3.5.0' ''' - req_ver = version.split('.') - version_range = '>= %s.%s.0, < %s.%s.0' % \ - (req_ver[0], req_ver[1], req_ver[0], int(req_ver[1])+1) + non_local_version = version.split('+')[0] + req_ver = non_local_version.split('.') + if 'rc' in version: + version_range = '>= %s.%s.0rc0, < %s.%s.0' % \ + (req_ver[0], req_ver[1], req_ver[0], int(req_ver[1])+1) + else: + version_range = '>= %s.%s.0, < %s.%s.0' % \ + (req_ver[0], req_ver[1], req_ver[0], int(req_ver[1])+1) return version_range @@ -43,8 +48,10 @@ def version_info(*paths): # compute version range version, version_range = version_info('src', 'unicon', 'plugins', '__init__.py') -install_requires = ['pyyaml', - 'unicon {range}'.format(range = version_range)], +install_requires = ['unicon {range}'.format(range = version_range), + 'pyyaml', + 'PrettyTable', + 'cryptography>=43.0'] # launch setup setup( @@ -105,6 +112,7 @@ def version_info(*paths): 'tests/mock_data/*/*.txt', 'tests/mock_data/*/*/*.txt', 'tests/unittest/ssh_host_key', + 'pid_tokens.csv' ]}, # Standalone scripts diff --git a/src/unicon/plugins/__init__.py b/src/unicon/plugins/__init__.py index 588f7821..735a174a 100644 --- a/src/unicon/plugins/__init__.py +++ b/src/unicon/plugins/__init__.py @@ -1,30 +1,46 @@ - -__version__ = '19.12' +__version__ = "26.2" supported_chassis = [ 'single_rp', 'dual_rp', 'stack', + 'quad', + 'stackwise_virtual' ] supported_os = [ + 'aci', + 'aireos', + 'apic', + 'asa', + 'cheetah', + 'cimc', + 'comware', + 'confd', + 'dnos6', + 'dnos10', + 'eos', + 'fxos', + 'gaia', 'generic', + 'hvrp', 'ios', - 'nxos', 'iosxe', 'iosxr', - 'aireos', - 'linux', - 'cheetah', + 'ironware', 'ise', - 'asa', - 'nso', - 'confd', - 'vos', - 'cimc', - 'fxos', 'junos', - 'staros', - 'aci', + 'linux', + 'nd', + 'nso', + 'nxos', + 'ons', 'sdwan', + 'slxos', + 'sonic', + 'sros', + 'staros', + 'viptela', + 'vos', + 'windows' ] diff --git a/src/unicon/plugins/aci/__init__.py b/src/unicon/plugins/aci/__init__.py deleted file mode 100644 index 0012ea1c..00000000 --- a/src/unicon/plugins/aci/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -__author__ = "dwapstra" - -from .apic.connection import AciApicConnection -from .n9k.connection import AciN9KConnection diff --git a/src/unicon/plugins/aci/apic/service_implementation.py b/src/unicon/plugins/aci/apic/service_implementation.py deleted file mode 100644 index 2a1bfb01..00000000 --- a/src/unicon/plugins/aci/apic/service_implementation.py +++ /dev/null @@ -1,127 +0,0 @@ -__author__ = "dwapstra" - -import re -from time import sleep -from unicon.bases.routers.services import BaseService -from unicon.plugins.generic.statements import connection_statement_list -from unicon.plugins.generic.service_implementation import Execute as GenericExecute -from unicon.plugins.generic import GenericUtils -from unicon.core.errors import SubCommandFailure -from unicon.eal.dialogs import Dialog - -from .patterns import AciPatterns -from .service_statements import reload_statement_list - -utils = GenericUtils() - - -class Execute(GenericExecute): - """ Execute Service implementation - - Service to executes exec_commands on the device and return the - console output. reply option can be passed for the interactive exec - command. - - Arguments: - command: exec command - reply: Additional Dialog patterns for interactive exec commands. - timeout : Timeout value in sec, Default Value is 60 sec - lines: number of lines to capture when paging is active. Default: 100 - - Returns: - True on Success, raise SubCommandFailure on failure - - Example: - .. code-block:: python - - output = dev.execute("show command") - - """ - - def __init__(self, connection, context, **kwargs): - # Connection object will have all the received details - super().__init__(connection, context, **kwargs) - - def post_service(self, *args, clean_output=True, **kwargs): - super().post_service(*args, **kwargs) - - if clean_output: - if isinstance(self.result, str): - output = self.result - output = utils.remove_ansi_escape_codes(output) - output = re.sub('.\x08', '', output) - output = re.sub('%\s+\r ', '', output) - self.result = output - - -class Reload(BaseService): - - def __init__(self, connection, context, **kwargs): - # Connection object will have all the received details - super().__init__(connection, context, **kwargs) - self.start_state = 'enable' - self.end_state = 'enable' - self.service_name = 'reload' - self.timeout = connection.settings.RELOAD_TIMEOUT - self.dialog = Dialog(reload_statement_list) - self.__dict__.update(kwargs) - - - def call_service(self, - reload_command='acidiag reboot', - dialog=Dialog([]), - timeout=None, - *args, - **kwargs): - - con = self.connection - timeout = timeout or self.timeout - - fmt_msg = "+++ reloading %s " \ - "with reload_command '%s' " \ - "and timeout %s seconds +++" - con.log.info(fmt_msg % (self.connection.hostname, - reload_command, - timeout)) - - con.state_machine.go_to(self.start_state, - con.spawn, - prompt_recovery=self.prompt_recovery, - context=self.context) - - if not isinstance(dialog, Dialog): - raise SubCommandFailure( - "dialog passed must be an instance of Dialog") - - dialog += self.dialog - con.spawn.sendline(reload_command) - try: - self.result = dialog.process(con.spawn, - timeout=timeout, - prompt_recovery=self.prompt_recovery, - context=self.context) - if self.result: - self.result = self.result.match_output - - con.log.info('Reload done, waiting %s seconds' % con.settings.POST_RELOAD_WAIT) - sleep(con.settings.POST_RELOAD_WAIT) - - con.sendline() - - con.state_machine.go_to('any', - con.spawn, - prompt_recovery=self.prompt_recovery, - context=self.context, - timeout=con.connection_timeout, - dialog=con.connection_provider.get_connection_dialog()) - - if con.state_machine.current_state == 'enable': - con.connection_provider.init_handle() - except Exception as err: - raise SubCommandFailure("Reload failed %s" % err) - - if isinstance(self.result, str): - self.result = self.result.replace(reload_command, "", 1) - - con.log.info("+++ Reload completed +++") - diff --git a/src/unicon/plugins/aci/apic/service_statements.py b/src/unicon/plugins/aci/apic/service_statements.py deleted file mode 100644 index c28a49c8..00000000 --- a/src/unicon/plugins/aci/apic/service_statements.py +++ /dev/null @@ -1,41 +0,0 @@ -__author__ = "dwapstra" - -from unicon.eal.dialogs import Statement -from .service_patterns import AciReloadPatterns - -pat = AciReloadPatterns() - - -class AciReloadStatements(object): - - def __init__(self): - self.restart_proceed = Statement(pattern=pat.restart_proceed, - action='sendline(y)', - loop_continue=True, - continue_timer=False) - - self.factory_reset = Statement(pattern=pat.factory_reset, - action='sendline(Y)', - loop_continue=True, - continue_timer=False) - - self.press_any_key = Statement(pattern=pat.press_any_key, - action=None, - args=None, - loop_continue=False, - continue_timer=False) - - self.login = Statement(pattern=pat.login, - action=None, - args=None, - loop_continue=False, - continue_timer=False) - -s = AciReloadStatements() - -reload_statement_list = [s.factory_reset, - s.restart_proceed, - s.press_any_key, # loop_continue=False - s.login # loop_continue=False - ] - diff --git a/src/unicon/plugins/aci/n9k/patterns.py b/src/unicon/plugins/aci/n9k/patterns.py deleted file mode 100644 index e9678b75..00000000 --- a/src/unicon/plugins/aci/n9k/patterns.py +++ /dev/null @@ -1,9 +0,0 @@ -__author__ = "dwapstra" - -from unicon.plugins.generic.patterns import GenericPatterns - -class AciPatterns(GenericPatterns): - def __init__(self): - super().__init__() - self.enable_prompt = r'^(.*?)((%N)|\(none\))#' - self.loader_prompt = r'^(.*?)loader >\s*$' diff --git a/src/unicon/plugins/aci/n9k/statemachine.py b/src/unicon/plugins/aci/n9k/statemachine.py deleted file mode 100644 index a444c9d6..00000000 --- a/src/unicon/plugins/aci/n9k/statemachine.py +++ /dev/null @@ -1,31 +0,0 @@ -""" State machine for Aci """ - -__author__ = "dwapstra" - -import re - -from unicon.core.errors import SubCommandFailure, StateMachineError -from unicon.plugins.generic.statements import GenericStatements -from unicon.plugins.generic.statemachine import default_statement_list -from unicon.statemachine import State, Path, StateMachine -from unicon.eal.dialogs import Dialog, Statement - -from .patterns import AciPatterns - -patterns = AciPatterns() -statements = GenericStatements() - - - -class AciStateMachine(StateMachine): - - def __init__(self, hostname=None): - super().__init__(hostname) - - def create(self): - enable = State('enable', patterns.enable_prompt) - boot = State('boot', patterns.loader_prompt) - - self.add_state(enable) - self.add_state(boot) - self.add_default_statements(default_statement_list) diff --git a/src/unicon/plugins/aireos/__init__.py b/src/unicon/plugins/aireos/__init__.py index 3599a682..67cb15c1 100644 --- a/src/unicon/plugins/aireos/__init__.py +++ b/src/unicon/plugins/aireos/__init__.py @@ -1,13 +1,15 @@ - -from unicon.bases.routers.connection import BaseSingleRpConnection -from unicon.plugins.generic.connection_provider import GenericSingleRpConnectionProvider from unicon.eal.dialogs import Dialog -from unicon.plugins.aireos.settings import AireosSettings -from unicon.plugins.aireos.statemachine import AireosStateMachine -from unicon.plugins.generic import ServiceList -from unicon.plugins.aireos import service_implementation as svc +from unicon.plugins.generic import ServiceList, GenericSingleRpConnection, GenericDualRPConnection +from unicon.plugins.generic.connection_provider import GenericSingleRpConnectionProvider + +from unicon.plugins.generic import service_implementation as svc from .patterns import AireosPatterns +from .settings import AireosSettings +from .statemachine import AireosStateMachine, AireosDualRpStateMachine +from .connection_provider import AireosDualRpConnectionProvider +from . import service_implementation as aireos_svc + p = AireosPatterns() @@ -15,18 +17,34 @@ class AireosServiceList(ServiceList): def __init__(self): super().__init__() - self.reload = svc.AireosReload - self.ping = svc.AireosPing - self.copy = svc.AireosCopy - self.execute = svc.AireosExecute - self.configure = svc.AireosConfigure + self.reload = aireos_svc.AireosReload + self.ping = aireos_svc.AireosPing + self.copy = aireos_svc.AireosCopy + self.execute = aireos_svc.AireosExecute + self.configure = aireos_svc.AireosConfigure -class AireosConnection(BaseSingleRpConnection): +class HAAireosServiceList(AireosServiceList): + def __init__(self): + super().__init__() + self.execute = aireos_svc.AireosHaExecute + + +class AireosConnection(GenericSingleRpConnection): os = 'aireos' - series = None + platform = None chassis_type = 'single_rp' state_machine_class = AireosStateMachine connection_provider_class = GenericSingleRpConnectionProvider subcommand_list = AireosServiceList settings = AireosSettings() + + +class AireosDualRPConnection(GenericDualRPConnection): + os = 'aireos' + platform = None + chassis_type = 'dual_rp' + subcommand_list = HAAireosServiceList + state_machine_class = AireosDualRpStateMachine + connection_provider_class = AireosDualRpConnectionProvider + settings = AireosSettings() diff --git a/src/unicon/plugins/aireos/ap/__init__.py b/src/unicon/plugins/aireos/ap/__init__.py new file mode 100644 index 00000000..e7416595 --- /dev/null +++ b/src/unicon/plugins/aireos/ap/__init__.py @@ -0,0 +1,16 @@ + +from unicon.plugins.generic import ServiceList, GenericSingleRpConnection, GenericDualRPConnection +from unicon.plugins.generic.connection_provider import GenericSingleRpConnectionProvider + +from .settings import AireosAPSettings +from .statemachine import AireosAPStateMachine + + +class AireosAPConnection(GenericSingleRpConnection): + os = 'aireos' + platform = 'ap' + chassis_type = 'single_rp' + state_machine_class = AireosAPStateMachine + connection_provider_class = GenericSingleRpConnectionProvider + subcommand_list = ServiceList + settings = AireosAPSettings() diff --git a/src/unicon/plugins/aireos/ap/settings.py b/src/unicon/plugins/aireos/ap/settings.py new file mode 100644 index 00000000..5808a50f --- /dev/null +++ b/src/unicon/plugins/aireos/ap/settings.py @@ -0,0 +1,18 @@ +from unicon.plugins.generic.settings import GenericSettings + + +class AireosAPSettings(GenericSettings): + def __init__(self): + super().__init__() + self.HA_INIT_EXEC_COMMANDS = [ + 'terminal length 0', + 'terminal width 0', + 'exec-timeout 0 0', + 'logging console disable' + ] + self.HA_INIT_CONFIG_COMMANDS = [] + + self.ERROR_PATTERN = [ + r'^%\s*[Ii]nvalid input detected', + r'^%\s*[Ii]ncomplete' + ] diff --git a/src/unicon/plugins/aireos/ap/statemachine.py b/src/unicon/plugins/aireos/ap/statemachine.py new file mode 100644 index 00000000..b9b02981 --- /dev/null +++ b/src/unicon/plugins/aireos/ap/statemachine.py @@ -0,0 +1,28 @@ +from unicon.eal.dialogs import Dialog +from unicon.statemachine import Path, State, StateMachine +from unicon.plugins.generic.patterns import GenericPatterns +from unicon.plugins.generic.statements import GenericStatements +from unicon.plugins.generic.statements import default_statement_list, authentication_statement_list + +statements = GenericStatements() + +patterns = GenericPatterns() + + +class AireosAPStateMachine(StateMachine): + def create(self): + + disable = State('disable', patterns.disable_prompt) + enable = State('enable', patterns.enable_prompt) + + self.add_state(enable) + self.add_state(disable) + + enable_to_disable = Path(enable, disable, 'disable', None) + disable_to_enable = Path(disable, enable, 'enable', + Dialog([statements.enable_password_stmt, statements.bad_password_stmt])) + + self.add_path(disable_to_enable) + self.add_path(enable_to_disable) + + self.add_default_statements(default_statement_list) diff --git a/src/unicon/plugins/aireos/connection_provider.py b/src/unicon/plugins/aireos/connection_provider.py new file mode 100644 index 00000000..bb6b58d3 --- /dev/null +++ b/src/unicon/plugins/aireos/connection_provider.py @@ -0,0 +1,62 @@ + +from unicon.plugins.generic.connection_provider import GenericDualRpConnectionProvider + + +class AireosDualRpConnectionProvider(GenericDualRpConnectionProvider): + + + def connect(self): + """ Connects, initializes and designates handle + """ + con = self.connection + + con.log.info('+++ connection to %s +++' % str(self.connection.a.spawn)) + con.log.info('+++ connection to %s +++' % str(self.connection.b.spawn)) + self.establish_connection() + + # Maintain initial state + if not con.mit: + + con.log.info('+++ designating handles +++') + self.designate_handles() + + # Run initial exec/configure commands on the active, which is + # supposed to disable console logging. + con.log.info('+++ initializing active handle +++') + self.init_active() + + # con.log.info('+++ initializing standby handle +++') + # self.init_standby() + + def designate_handles(self): + """ Identifies the Role of each handle and designates if it is active or + standby and bring the active RP to enable state """ + con = self.connection + + if con.a.state_machine.current_state == 'standby': + target_rp = 'b' + other_rp = 'a' + elif con.b.state_machine.current_state == 'standby': + target_rp = 'a' + other_rp = 'b' + else: + con.log.info("None of the sessions are currently in standby state") + target_rp = 'a' + other_rp = 'b' + target_handle = getattr(con, target_rp) + other_handle = getattr(con, other_rp) + + con._set_active_alias(target_rp) + con._set_standby_alias(other_rp) + + target_handle.state_machine.go_to('enable', + target_handle.spawn, + context=con.context, + timeout=con.connection_timeout, + dialog=self.get_connection_dialog(), + ) + con._handles_designated = True + + def assign_ha_mode(self): + self.connection.a.mode = 'sso' + self.connection.b.mode = 'sso' diff --git a/src/unicon/plugins/aireos/patterns.py b/src/unicon/plugins/aireos/patterns.py index 6220c95e..53b85d38 100644 --- a/src/unicon/plugins/aireos/patterns.py +++ b/src/unicon/plugins/aireos/patterns.py @@ -5,7 +5,7 @@ class AireosPatterns(GenericPatterns): def __init__(self): super().__init__() - self.base_prompt = r'^(.*?)\(%N\)\s*' + self.base_prompt = r'^(.*?)\((%N|Cisco Capwap Simulator)\)\s*' self.enable_prompt = self.base_prompt + r'>\s*$' self.show_prompt = self.base_prompt + r'show>\s*$' self.config_prompt = self.base_prompt + r'config>\s*$' @@ -16,6 +16,7 @@ def __init__(self): self.reset_prompt = self.base_prompt + r'reset>\s*$' self.save_prompt = self.base_prompt + r'save>\s*$' self.shell_prompt = r'bash.*#\s*$' + self.standby_exec = r'^(.*?)\((%N|Cisco Capwap Simulator)-Standby\)\s*>\s*?' class AireosReloadPatterns(UniconCorePatterns): @@ -24,7 +25,7 @@ def __init__(self): self.force_reboot = r'^(.*?)Do you still want to force a reboot \(y/N\)' self.are_you_sure = r'^(.*?)Are you sure you (would like to reset the system|want to start)\?\s*\(y/N\)' self.enter_user_name = r'^(.*?)Enter User Name \(.*\)' - + self.are_you_sure = r'(.*?)Are you sure.*\([yY]/[nN]\)\s*$' class AireosPingPatterns(UniconCorePatterns): def __init__(self): @@ -47,5 +48,6 @@ class AireosExecutePatterns(UniconCorePatterns): def __init__(self): super().__init__() self.press_any_key = r'(.*?)Press any key to continue' - self.are_you_sure = r'(.*?)Are you sure .*\([yY]/[nN]\) *?$' - self.press_enter_stmt = r'(.?)Press Enter to continue.*' \ No newline at end of file + self.are_you_sure = r'(.*?)Are you sure.*\([yY]/[nN]\)\s*$' + self.press_enter_stmt = r'(.?)Press Enter to continue.*' + self.would_you_like_to_save = r'Would you like to save them now?\? \(y/N\)' \ No newline at end of file diff --git a/src/unicon/plugins/aireos/service_implementation.py b/src/unicon/plugins/aireos/service_implementation.py index d2f51c73..51bfcd5d 100644 --- a/src/unicon/plugins/aireos/service_implementation.py +++ b/src/unicon/plugins/aireos/service_implementation.py @@ -5,7 +5,9 @@ from unicon.core.errors import SubCommandFailure from unicon.eal.dialogs import Dialog -from unicon.plugins.generic.service_implementation import Execute as GenericExecute, Configure as GenericConfigure +from unicon.plugins.generic.service_implementation import Execute as GenericExecute, \ + Configure as GenericConfigure, \ + HaExecService as GenericHaExecute from unicon.plugins.generic.utils import GenericUtils from .patterns import (AireosPatterns, AireosReloadPatterns, AireosPingPatterns, AireosCopyPatterns) @@ -25,6 +27,13 @@ def __init__(self, connection, context, **kwargs): self.dialog += Dialog(execute_statements) +class AireosHaExecute(GenericHaExecute): + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.dialog += Dialog(execute_statements) + + class AireosConfigure(GenericConfigure): def __init__(self, connection, context, **kwargs): @@ -38,11 +47,9 @@ def __init__(self, connection, context, **kwargs): self.connection = connection self.context = context self.timeout_pattern = ['Timeout occurred', ] - self.error_pattern = [r'Invalid', r'Incorrect', r'HELP'] self.start_state = 'enable' self.end_state = 'enable' self.result = None - self.service_name = 'reload' self.timeout = self.connection.settings.RELOAD_TIMEOUT self.dialog_reload = Dialog(reload_statements) # add the keyword arguments to the object @@ -52,18 +59,29 @@ def call_service(self, reload_command='reset system forced', dialog=Dialog([]), timeout=None, + error_pattern=None, **kwargs): con = self.connection timeout = timeout or self.timeout con.log.debug('+++ reloading %s with reload_command %s and timeout is %s +++' % (self.connection.hostname, reload_command, timeout)) + if error_pattern is None: + self.error_pattern = con.settings.ERROR_PATTERN + else: + self.error_pattern = error_pattern + + dialog += self.dialog_reload try: con.spawn.sendline(reload_command) self.result = dialog.process(con.spawn, timeout=timeout, prompt_recovery=self.prompt_recovery) + if self.result: + self.result = self.result.match_output + self.result = self.get_service_result() + con.state_machine.go_to('any', con.spawn, context=self.context, @@ -85,7 +103,6 @@ def __init__(self, connection, context, **kwargs): self.start_state = 'enable' self.end_state = 'enable' self.result = None - self.service_name = 'ping' self.timeout = 60 # add the keyword arguments to the object @@ -132,8 +149,8 @@ def __init__(self, connection, context, **kwargs): self.error_pattern = [] self.start_state = 'enable' self.end_state = 'enable' + self.timeout = 300 self.result = None - self.service_name = 'copy' self.dialog = Dialog([ [pr.are_you_sure, lambda spawn: spawn.sendline('y'), @@ -190,9 +207,9 @@ def call_service(self, timeout=None, *args, **kwargs): param['source_file'] = os.path.basename(param['source_file']) # Sets the time it takes to download and trigger reboot - if param['mode'] is 'code': + if param['mode'] == 'code': timeout = 200 - elif param['mode'] is 'simconfig': + elif param['mode'] == 'simconfig': timeout = 50 else: raise SubCommandFailure('Copy mode must be \'code\' or \'simconfig\'') diff --git a/src/unicon/plugins/aireos/service_statements.py b/src/unicon/plugins/aireos/service_statements.py index 029bb6ed..0332b4fd 100644 --- a/src/unicon/plugins/aireos/service_statements.py +++ b/src/unicon/plugins/aireos/service_statements.py @@ -29,12 +29,16 @@ def __init__(self): self.press_enter_stmt = [execute_patterns.press_enter_stmt, 'sendline()', None, True, False] + self.would_you_like_to_save_stmt = [execute_patterns.would_you_like_to_save, + 'sendline(y)', + None, True, False] aireos_statements = AireOsStatements() reload_statements = [aireos_statements.are_you_sure_stmt, aireos_statements.force_reboot_stmt, - aireos_statements.enter_user_name_stmt] # loop_continue=False + aireos_statements.enter_user_name_stmt, # loop_continue=False + aireos_statements.would_you_like_to_save_stmt] execute_statements = [aireos_statements.press_any_key_stmt, aireos_statements.yes_no_stmt, diff --git a/src/unicon/plugins/aireos/settings.py b/src/unicon/plugins/aireos/settings.py index 12eec18e..d1119d96 100644 --- a/src/unicon/plugins/aireos/settings.py +++ b/src/unicon/plugins/aireos/settings.py @@ -14,12 +14,20 @@ def __init__(self): ] self.RELOAD_TIMEOUT = 400 self.ERROR_PATTERN = [ - r'^(%\s*)?Error:', + r'^(%\s*)?(Error|ERROR)', r'syntax error', r'Aborted', r'result false', r'^This command has been deprecated', - r'^Incorrect usage.' + r'^Incorrect usage.', + r'^Incorrect input', + r'^HELP', + r'^[Ii]nvalid', + r'^[Ww]arning', + r'WLAN Identifier is invalid', + r'^Request failed', + r'^[Rr]equest [Ff]ailed', + r'^(.*?) already in use' ] self.LOGIN_PROMPT = r'^.*?User:\s*$' self.DEFAULT_LEARNED_HOSTNAME = r'(.*?)' diff --git a/src/unicon/plugins/aireos/statemachine.py b/src/unicon/plugins/aireos/statemachine.py index 6dacf0ac..56c0fb25 100644 --- a/src/unicon/plugins/aireos/statemachine.py +++ b/src/unicon/plugins/aireos/statemachine.py @@ -74,3 +74,12 @@ def create(self): self.add_path(shell_to_enable) self.add_default_statements(default_statement_list) + + +class AireosDualRpStateMachine(AireosStateMachine): + + def create(self): + super().create() + + standby = State('standby', p.standby_exec) + self.add_state(standby) diff --git a/src/unicon/plugins/apic/__init__.py b/src/unicon/plugins/apic/__init__.py new file mode 100644 index 00000000..c38f1d07 --- /dev/null +++ b/src/unicon/plugins/apic/__init__.py @@ -0,0 +1 @@ +from .connection import AciApicConnection diff --git a/src/unicon/plugins/aci/apic/connection.py b/src/unicon/plugins/apic/connection.py similarity index 74% rename from src/unicon/plugins/aci/apic/connection.py rename to src/unicon/plugins/apic/connection.py index 30a34f5d..1d2a4077 100644 --- a/src/unicon/plugins/aci/apic/connection.py +++ b/src/unicon/plugins/apic/connection.py @@ -1,7 +1,7 @@ -from unicon.plugins.generic import GenericSingleRpConnection, service_implementation as svc +from unicon.plugins.generic import GenericSingleRpConnection from unicon.plugins.generic.connection_provider import GenericSingleRpConnectionProvider -from unicon.plugins.generic import ServiceList, service_implementation as svc +from unicon.plugins.generic import ServiceList from unicon.eal.dialogs import Statement from . import service_implementation as aci_svc @@ -11,7 +11,7 @@ class AciApicConnectionProvider(GenericSingleRpConnectionProvider): """ - Connection provider class for aci connections. + Connection provider class for apic connections. """ def __init__(self, *args, **kwargs): @@ -34,31 +34,28 @@ def update_state(con, state): def init_handle(self): con = self.connection - con._is_connected = True - if con.state_machine.current_state != 'setup': + if con.state_machine.current_state not in ['setup', 'shell']: super().init_handle() - class AciApicServiceList(ServiceList): - """ aci services. """ + """ apic services. """ def __init__(self): super().__init__() self.execute = aci_svc.Execute - self.configure = svc.Configure + self.configure = aci_svc.Configure self.reload = aci_svc.Reload class AciApicConnection(GenericSingleRpConnection): """ - Connection class for aci connections. + Connection class for apic connections. """ - os = 'aci' - series = 'apic' + + os = 'apic' chassis_type = 'single_rp' state_machine_class = AciStateMachine connection_provider_class = AciApicConnectionProvider subcommand_list = AciApicServiceList - settings = AciSettings() - + settings = AciSettings() \ No newline at end of file diff --git a/src/unicon/plugins/aci/apic/patterns.py b/src/unicon/plugins/apic/patterns.py similarity index 73% rename from src/unicon/plugins/aci/apic/patterns.py rename to src/unicon/plugins/apic/patterns.py index 895db1b0..4605c0c1 100644 --- a/src/unicon/plugins/aci/apic/patterns.py +++ b/src/unicon/plugins/apic/patterns.py @@ -1,15 +1,19 @@ __author__ = "dwapstra" +from unicon.utils import ANSI_REGEX from unicon.plugins.generic.patterns import GenericPatterns -class AciPatterns(GenericPatterns): + +class ApicPatterns(GenericPatterns): def __init__(self): super().__init__() - self.enable_prompt = r'^(.*?)(%N)#' - self.config_prompt = r'^(.*?)(%N)\(config\)#' - self.bash_prompt = r'^(.*?)[-\.\w]+@(%N):(~|[-\w]+)>\s*$' + self.learn_hostname = r'^.*?({a})?(?P[-\w]+)\s?([-\w\]/~:\.\d ]+)?([>\$~%#\]])\s*(\x1b\S+)?$'.format(a=ANSI_REGEX) + self.enable_prompt = r'^(.*?)((\x1b\S+)?\x00)*(%N)#\s*(\x1b\S+)?$' + self.config_prompt = r'^(.*?)((\x1b\S+)?\x00)*(%N)\(config.*\)#\s*(\x1b\S+)?$' + self.shell_prompt = r'^(.*?)((\x1b\S+)?\x00)*\[[-\.\w]+@((%N)\s+.*?\]#)\s*(\x1b\S+)?$' + -class AciSetupPatterns(object): +class ApicSetupPatterns(object): def __init__(self): super().__init__() self.fabric_name = r'^(.*?)Enter the fabric name' diff --git a/src/unicon/plugins/apic/service_implementation.py b/src/unicon/plugins/apic/service_implementation.py new file mode 100644 index 00000000..0d10911d --- /dev/null +++ b/src/unicon/plugins/apic/service_implementation.py @@ -0,0 +1,192 @@ +__author__ = "dwapstra" + +import io +import re +import logging +from time import sleep +from unicon.logs import UniconStreamHandler, UNICON_LOG_FORMAT +from unicon.bases.routers.services import BaseService +from unicon.plugins.generic.service_implementation import (Execute as GenericExecute, + Configure as GenericConfigure) +from unicon.plugins.generic import GenericUtils +from unicon.core.errors import SubCommandFailure +from unicon.eal.dialogs import Dialog + +from .service_statements import reload_statement_list + +utils = GenericUtils() + + +def clean_command_output(output): + """ Function to clean command output by removing unwanted characters """ + output = utils.remove_ansi_escape_codes(output) + output = re.sub('.\x08', '', output) + output = re.sub(r'%\s+\r ', '', output) + output = re.sub(r'\x00+', '', output) + output = re.sub(r'[\r%]+', '', output) + return output + + +class Execute(GenericExecute): + """ Execute Service implementation + + Service to executes exec_commands on the device and return the + console output. reply option can be passed for the interactive exec + command. + + Arguments: + command: exec command + reply: Additional Dialog patterns for interactive exec commands. + timeout : Timeout value in sec, Default Value is 60 sec + + Returns: + True on Success, raise SubCommandFailure on failure + + Example: + .. code-block:: python + + output = dev.execute("show command") + + """ + + def __init__(self, connection, context, **kwargs): + # Connection object will have all the received details + super().__init__(connection, context, **kwargs) + + def post_service(self, *args, clean_output=True, **kwargs): + super().post_service(*args, **kwargs) + + if clean_output and isinstance(self.result, str): + self.result = clean_command_output(self.result) + + +class Configure(GenericConfigure): + """ Configure Service implementation + + Service to execute configuration commands on the device and return the + console output. The `reply` option can be passed for interactive configuration commands. + + Arguments: + commands : list/single config command + reply: Addition Dialogs for interactive config commands. + timeout : Timeout value in sec, Default Value is 30 sec + + Returns: + True on Success, raises SubCommandFailure on failure + + Example: + .. code-block:: python + output = rtr.configure('no logging console') + cmd =['hostname si-tvt-7200-28-41', 'no logging console'] + output = dev.configure(cmd) + """ + def __init__(self, connection, context, **kwargs): + # Connection object will have all the received details + super().__init__(connection, context, **kwargs) + + def post_service(self, *args, clean_output=True, **kwargs): + super().post_service(*args, **kwargs) + + if clean_output and isinstance(self.result, str): + self.result = clean_command_output(self.result) + + +class Reload(BaseService): + + def __init__(self, connection, context, **kwargs): + # Connection object will have all the received details + super().__init__(connection, context, **kwargs) + self.start_state = 'enable' + self.end_state = 'enable' + self.timeout = connection.settings.RELOAD_TIMEOUT + self.dialog = Dialog(reload_statement_list) + self.log_buffer = io.StringIO() + lb = UniconStreamHandler(self.log_buffer) + lb.setFormatter(logging.Formatter(fmt=UNICON_LOG_FORMAT)) + self.connection.log.addHandler(lb) + self.__dict__.update(kwargs) + + def call_service(self, + reload_command='acidiag reboot', + dialog=Dialog([]), + timeout=None, + *args, + **kwargs): + + # Clear log buffer + self.log_buffer.seek(0) + self.log_buffer.truncate() + + con = self.connection + timeout = timeout or self.timeout + + fmt_msg = "+++ reloading %s " \ + "with reload_command '%s' " \ + "and timeout %s seconds +++" + con.log.info(fmt_msg % (self.connection.hostname, + reload_command, + timeout)) + + con.state_machine.go_to(self.start_state, + con.spawn, + prompt_recovery=self.prompt_recovery, + context=self.context) + + if not isinstance(dialog, Dialog): + raise SubCommandFailure( + "dialog passed must be an instance of Dialog") + + dialog += self.dialog + con.spawn.sendline(reload_command) + try: + self.result = dialog.process(con.spawn, + timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=self.context) + except Exception as err: + raise SubCommandFailure("Reload failed\n" + "Error: {}\n" + "Buffer: {}".format(err, repr(con.spawn.buffer))) + + if self.result: + self.result = self.result.match_output + + if self.context.get('console'): + con.log.info('Reload done, waiting %s seconds' % con.settings.POST_RELOAD_WAIT) + sleep(con.settings.POST_RELOAD_WAIT) + + con.sendline() + + con.state_machine.go_to('any', + con.spawn, + prompt_recovery=self.prompt_recovery, + context=self.context, + timeout=con.connection_timeout, + dialog=con.connection_provider.get_connection_dialog()) + + if con.state_machine.current_state == 'enable': + con.connection_provider.init_handle() + else: + con.log.debug('Did not detect a console session, will try to reconnect...') + con.log.info('Disconnecting...') + con.disconnect() + + reload_wait = con.settings.POST_RELOAD_WAIT + for x in range(con.settings.RELOAD_RECONNECT_ATTEMPTS): + con.log.info('Waiting for {} seconds'.format(reload_wait / (x + 1))) + sleep(reload_wait / (x + 1)) + con.log.info('Trying to connect... attempt #{}'.format(x + 1)) + try: + con.connect() + except Exception: + con.log.warning('Connection failed') + if con.is_connected: + break + + if not con.is_connected: + raise SubCommandFailure('Reload failed - could not reconnect') + + con.log.info("+++ Reload completed +++") + + self.log_buffer.seek(0) + self.result = self.log_buffer.read() diff --git a/src/unicon/plugins/aci/apic/service_patterns.py b/src/unicon/plugins/apic/service_patterns.py similarity index 91% rename from src/unicon/plugins/aci/apic/service_patterns.py rename to src/unicon/plugins/apic/service_patterns.py index dea00f17..7e2287ad 100644 --- a/src/unicon/plugins/aci/apic/service_patterns.py +++ b/src/unicon/plugins/apic/service_patterns.py @@ -2,7 +2,8 @@ from unicon.plugins.generic.service_patterns import ReloadPatterns -class AciReloadPatterns(ReloadPatterns): + +class ApicReloadPatterns(ReloadPatterns): def __init__(self): super().__init__() self.restart_proceed = r'^(.*?)This command will restart (this device|the APIC), Proceed\? \[y/N\]' diff --git a/src/unicon/plugins/apic/service_statements.py b/src/unicon/plugins/apic/service_statements.py new file mode 100644 index 00000000..ed157811 --- /dev/null +++ b/src/unicon/plugins/apic/service_statements.py @@ -0,0 +1,45 @@ +__author__ = "dwapstra" + +from unicon.eal.dialogs import Statement +from unicon.plugins.generic.service_statements import connection_closed + +from .service_patterns import ApicReloadPatterns + + +pat = ApicReloadPatterns() + + +class ApicReloadStatements(object): + def __init__(self): + self.restart_proceed = Statement(pattern=pat.restart_proceed, + action='sendline(y)', + loop_continue=True, + continue_timer=False) + + self.factory_reset = Statement(pattern=pat.factory_reset, + action='sendline(Y)', + loop_continue=True, + continue_timer=False) + + self.press_any_key = Statement(pattern=pat.press_any_key, + action=None, + args=None, + loop_continue=False, + continue_timer=False) + + self.login = Statement(pattern=pat.login, + action=None, + args=None, + loop_continue=False, + continue_timer=False) + + + +apic_stmts = ApicReloadStatements() + +reload_statement_list = [apic_stmts.factory_reset, + apic_stmts.restart_proceed, + apic_stmts.press_any_key, # loop_continue=False + apic_stmts.login, # loop_continue=False + connection_closed # loop_continue=False + ] diff --git a/src/unicon/plugins/aci/apic/settings.py b/src/unicon/plugins/apic/settings.py similarity index 73% rename from src/unicon/plugins/aci/apic/settings.py rename to src/unicon/plugins/apic/settings.py index a06a4574..c1239958 100644 --- a/src/unicon/plugins/aci/apic/settings.py +++ b/src/unicon/plugins/apic/settings.py @@ -17,5 +17,10 @@ def __init__(self): 'terminal width 0' ] self.HA_INIT_CONFIG_COMMANDS = [] + self.ERROR_PATTERN = [ + r'^(%\s*)?Error', + ] - self.POST_RELOAD_WAIT = 180 + self.POST_RELOAD_WAIT = 330 + self.RELOAD_RECONNECT_ATTEMPTS = 3 + self.RELOAD_TIMEOUT = 420 diff --git a/src/unicon/plugins/aci/apic/statemachine.py b/src/unicon/plugins/apic/statemachine.py similarity index 65% rename from src/unicon/plugins/aci/apic/statemachine.py rename to src/unicon/plugins/apic/statemachine.py index df7bbafb..5972d548 100644 --- a/src/unicon/plugins/aci/apic/statemachine.py +++ b/src/unicon/plugins/apic/statemachine.py @@ -1,20 +1,16 @@ -""" State machine for Aci """ +""" State machine for APIC """ __author__ = "dwapstra" -import re - -from unicon.core.errors import SubCommandFailure, StateMachineError from unicon.plugins.generic.statements import GenericStatements from unicon.plugins.generic.statemachine import default_statement_list from unicon.statemachine import State, Path, StateMachine -from unicon.eal.dialogs import Dialog, Statement -from .patterns import AciPatterns, AciSetupPatterns +from .patterns import ApicPatterns, ApicSetupPatterns -patterns = AciPatterns() +patterns = ApicPatterns() statements = GenericStatements() -setup_patterns = AciSetupPatterns() +setup_patterns = ApicSetupPatterns() class AciStateMachine(StateMachine): @@ -24,13 +20,18 @@ def __init__(self, hostname=None): def create(self): enable = State('enable', patterns.enable_prompt) - config = State('config', patterns.config_prompt) + config = State('config', patterns.config_prompt) + shell = State('shell', patterns.shell_prompt) + learn_hostname = State('learn_hostname', patterns.learn_hostname) setup = State('setup', list(setup_patterns.__dict__.values())) self.add_state(enable) self.add_state(config) + self.add_state(learn_hostname) self.add_state(setup) + self.add_state(shell) + self.add_path(Path(learn_hostname, enable, None, None)) enable_to_config = Path(enable, config, 'configure', None) config_to_enable = Path(config, enable, 'end', None) diff --git a/src/unicon/plugins/asa/ASAv/__init__.py b/src/unicon/plugins/asa/ASAv/__init__.py index 77b30f17..d87240cb 100644 --- a/src/unicon/plugins/asa/ASAv/__init__.py +++ b/src/unicon/plugins/asa/ASAv/__init__.py @@ -12,7 +12,7 @@ def __init__(self): class ASAvConnection(BaseSingleRpConnection): os = 'asa' - series = 'asav' + platform = 'asav' chassis_type = 'single_rp' state_machine_class = ASAStateMachine connection_provider_class = ASAConnectionProvider diff --git a/src/unicon/plugins/asa/ASAv/service_implementation.py b/src/unicon/plugins/asa/ASAv/service_implementation.py index 51c7fcc1..a3f0d678 100644 --- a/src/unicon/plugins/asa/ASAv/service_implementation.py +++ b/src/unicon/plugins/asa/ASAv/service_implementation.py @@ -7,5 +7,4 @@ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) self.start_state = 'enable' self.end_state = 'enable' - self.service_name = 'reload' self.dialog = Dialog(asa_reload_stmt_list) diff --git a/src/unicon/plugins/asa/__init__.py b/src/unicon/plugins/asa/__init__.py index 741525bb..247d4b9d 100644 --- a/src/unicon/plugins/asa/__init__.py +++ b/src/unicon/plugins/asa/__init__.py @@ -3,12 +3,19 @@ from .provider import ASAConnectionProvider from unicon.plugins.generic import ServiceList from .settings import ASASettings +from .service_implementation import ASAExecute, ASAReload + +class ASAServiceList(ServiceList): + def __init__(self): + super().__init__() + self.execute = ASAExecute + self.reload = ASAReload class ASAConnection(BaseSingleRpConnection): os = 'asa' - series = None + platform = None chassis_type = 'single_rp' state_machine_class = ASAStateMachine connection_provider_class = ASAConnectionProvider - subcommand_list = ServiceList + subcommand_list = ASAServiceList settings = ASASettings() diff --git a/src/unicon/plugins/asa/fp2k/__init__.py b/src/unicon/plugins/asa/fp2k/__init__.py new file mode 100644 index 00000000..2c112a9a --- /dev/null +++ b/src/unicon/plugins/asa/fp2k/__init__.py @@ -0,0 +1,54 @@ +from unicon.plugins.generic.connection_provider import GenericSingleRpConnectionProvider +from unicon.plugins.generic import GenericSingleRpConnection, ServiceList +from unicon.plugins.fxos import service_implementation as fxos_svc + +from . import service_implementation as svc + +from .statemachine import AsaFp2kStateMachine +from .settings import AsaFp2kSettings + + +class AsaFp2kConnectionProvider(GenericSingleRpConnectionProvider): + """ + Connection provider class for fp2k connections. + """ + def __init__(self, *args, **kwargs): + + """ Initializes the generic connection provider + """ + super().__init__(*args, **kwargs) + self.connection.settings.MORE_CONTINUE = 'q' + + def init_handle(self): + con = self.connection + con.state_machine.detect_state(con.spawn) + self.execute_init_commands() + self.connection.settings.MORE_CONTINUE = ' ' + + +class AsaFp2kServiceList(ServiceList): + """ fp2k services. """ + + def __init__(self): + super().__init__() + self.fxos = fxos_svc.FXOS + self.fxos_mgmt = fxos_svc.FXOSManagement + self.sudo = fxos_svc.Sudo + self.disable = fxos_svc.Disable + self.enable = fxos_svc.Enable + self.reload = svc.Reload + self.switchto = svc.Switchto + self.rommon = fxos_svc.Rommon + + +class AsaFp2kConnection(GenericSingleRpConnection): + """ + Connection class for fp2k connections. + """ + os = 'asa' + platform = 'fp2k' + chassis_type = 'single_rp' + state_machine_class = AsaFp2kStateMachine + connection_provider_class = AsaFp2kConnectionProvider + subcommand_list = AsaFp2kServiceList + settings = AsaFp2kSettings() diff --git a/src/unicon/plugins/asa/fp2k/patterns.py b/src/unicon/plugins/asa/fp2k/patterns.py new file mode 100644 index 00000000..822b410e --- /dev/null +++ b/src/unicon/plugins/asa/fp2k/patterns.py @@ -0,0 +1,11 @@ +__author__ = "dwapstra" + +from unicon.plugins.fxos.patterns import FxosPatterns + + +class AsaFp2kPatterns(FxosPatterns): + def __init__(self): + super().__init__() + self.fxos_prompt = r'^(.*?)firepower.*?#\s*$' + self.broken_pipe = r'.*Connection to .*Broken pipe' + self.reload_confirm = r'^(.*?)Proceed with reload\? \[confirm\]' diff --git a/src/unicon/plugins/asa/fp2k/service_implementation.py b/src/unicon/plugins/asa/fp2k/service_implementation.py new file mode 100644 index 00000000..2846cb51 --- /dev/null +++ b/src/unicon/plugins/asa/fp2k/service_implementation.py @@ -0,0 +1,145 @@ +import io +import re +import time +import logging + +from unicon.eal.dialogs import Dialog +from unicon.bases.routers.services import BaseService +from unicon.logs import UniconStreamHandler, UNICON_LOG_FORMAT + +from unicon.plugins.fxos.statements import FxosStatements +from unicon.plugins.generic.service_implementation import Switchto as GenericSwitchto + +from .statements import reload_statements + +fxos_statements = FxosStatements() + + +class Switchto(GenericSwitchto): + """ Switch to a certain CLI state + """ + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + + def call_service(self, to_state, + timeout=None, + *args, **kwargs): + + if not self.connection.connected: + return + + con = self.connection + sm = self.get_sm() + + dialog = Dialog([fxos_statements.command_not_completed_stmt]) + + timeout = timeout if timeout is not None else self.timeout + + if isinstance(to_state, str): + to_state_list = [to_state] + elif isinstance(to_state, list): + to_state_list = to_state + else: + raise Exception('Invalid switchto to_state type: %s' % repr(to_state)) + + for to_state in to_state_list: + m1 = re.match(r'fxos scope (.*)', to_state) + m2 = re.match(r'fxos (admin|root)', to_state) + if m1: + scope = m1.group(1) + self.context._scope = scope + to_state = 'fxos_scope' + con.state_machine.go_to('fxos', con.spawn, + context=self.context, + hop_wise=True, + timeout=timeout) + elif m2: + con_mode = m2.group(1).strip() + if con_mode == 'admin': + to_state = 'fxos' + self.context._fxos_connect_mode = con_mode + elif con_mode == 'root': + to_state = 'sudo' + self.context._fxos_connect_mode = '' + else: + con.log.warning('%s is not a valid fxos connect mode, ignoring switchto' % con_mode) + self.context._fxos_connect_mode = '' + return + else: + to_state = to_state.replace(' ', '_') + + valid_states = [x.name for x in sm.states] + if to_state not in valid_states: + con.log.warning('%s is not a valid state, ignoring switchto' % to_state) + return + + con.state_machine.go_to(to_state, + con.spawn, + context=self.context, + hop_wise=True, + timeout=timeout, + dialog=dialog) + + self.end_state = sm.current_state + + +class Reload(BaseService): + def __init__(self, connection, context, **kwargs): + # Connection object will have all the received details + super().__init__(connection, context, **kwargs) + self.start_state = 'enable' + self.end_state = 'enable' + self.service_name = 'reload' + self.timeout = self.connection.settings.BOOT_TIMEOUT + self.log_buffer = io.StringIO() + lb = UniconStreamHandler(self.log_buffer) + lb.setFormatter(logging.Formatter(fmt=UNICON_LOG_FORMAT)) + self.connection.log.addHandler(lb) + self.dialog = Dialog(reload_statements) + self.__dict__.update(kwargs) + + def call_service(self, reload_command='reload', reply=Dialog([]), timeout=None, *args, **kwargs): + # Clear log buffer + self.log_buffer.seek(0) + self.log_buffer.truncate() + + con = self.connection + timeout = timeout or self.timeout + con.log.debug("+++ reloading %s with reload_command %s " + "and timeout is %s +++" % (self.connection.hostname, reload_command, timeout)) + + dialog = reply + self.dialog + con.spawn.sendline(reload_command) + self.result = dialog.process(con.spawn, + timeout=timeout or self.timeout, + prompt_recovery=self.prompt_recovery, + context=self.context) + + console = con.context.get('console', False) + if not console: + con.log.debug('Did not detect a console session, will try to reconnect...') + try: + con.spawn.expect('.+', timeout=10, log_timeout=False) + except TimeoutError: + pass + con.log.info('Disconnecting...') + con.disconnect() + for x in range(con.settings.RELOAD_RECONNECT_ATTEMPTS): + con.log.info('Waiting for {} seconds'.format(con.settings.RELOAD_WAIT)) + time.sleep(con.settings.RELOAD_WAIT / (x + 1)) + con.log.info('Trying to connect... attempt #{}'.format(x + 1)) + try: + con.connect() + except Exception: + con.log.warning('Connection failed') + if con.is_connected: + break + + if not con.is_connected: + return False, 'Reload failed - could not reconnect' + else: + self.log_buffer.seek(0) + output = self.log_buffer.read() + return True, output + diff --git a/src/unicon/plugins/asa/fp2k/settings.py b/src/unicon/plugins/asa/fp2k/settings.py new file mode 100644 index 00000000..e62a7bb4 --- /dev/null +++ b/src/unicon/plugins/asa/fp2k/settings.py @@ -0,0 +1,20 @@ +__author__ = "dwapstra" + +from unicon.plugins.fxos.settings import FxosSettings + + +class AsaFp2kSettings(FxosSettings): + """" Generic platform settings """ + def __init__(self): + """ initialize + """ + super().__init__() + self.HA_INIT_EXEC_COMMANDS = [] + self.HA_INIT_CONFIG_COMMANDS = [] + + self.ERROR_PATTERN = [ + r'^%?\s*?Syntax error:', + r'^\s*?% Invalid command' + ] + + self.PROMPT_RECOVERY_COMMANDS = ['\x01\x0b', '\r', '\x03', '\r'] diff --git a/src/unicon/plugins/asa/fp2k/statemachine.py b/src/unicon/plugins/asa/fp2k/statemachine.py new file mode 100644 index 00000000..458c1cbd --- /dev/null +++ b/src/unicon/plugins/asa/fp2k/statemachine.py @@ -0,0 +1,83 @@ +""" State machine for ASA/FP2K """ + +__author__ = "dwapstra" + +from unicon.statemachine import Path +from unicon.eal.dialogs import Dialog +from unicon.plugins.fxos.statemachine import FxosStateMachine + +from .statements import boot_to_rommon_statements +from .patterns import AsaFp2kPatterns + + +patterns = AsaFp2kPatterns() + + +def connect_fxos(statemachine, spawn, context): + mode = context.get('_fxos_connect_mode', '') + spawn.sendline('connect fxos {}'.format(mode).strip()) + + +def send_ctrl_caret_x(statemachine, spawn, context): + # Send Ctrl-^X + spawn.send('\x1ex') + + +def enable_to_rommon_transition(statemachine, spawn, context): + dialog = Dialog(boot_to_rommon_statements) + spawn.sendline('reload') + dialog.process(spawn, timeout=spawn.settings.BOOT_TIMEOUT, context=context) + spawn.sendline() + + +class AsaFp2kStateMachine(FxosStateMachine): + + def __init__(self, hostname=None): + super().__init__(hostname) + + def create(self): + super().create() + + enable = self.get_state('enable') + disable = self.get_state('disable') + config = self.get_state('config') + ftd_expert = self.get_state('expert') + ftd_expert_root = self.get_state('sudo') + fxos = self.get_state('fxos') + fxos_mgmt = self.get_state('fxos_mgmt') + ftd = self.get_state('ftd') + rommon = self.get_state('rommon') + + fxos.pattern = patterns.fxos_prompt + + self.remove_path(ftd, ftd_expert) + self.remove_path(ftd_expert, ftd) + self.remove_path(ftd_expert, ftd_expert_root) + self.remove_path(ftd_expert_root, ftd_expert) + self.remove_path(ftd, fxos) + self.remove_path(fxos, ftd) + self.remove_path(ftd, enable) + self.remove_path(enable, ftd) + self.remove_path(ftd, disable) + self.remove_path(ftd, config) + self.remove_path(disable, ftd) + self.remove_path(config, ftd) + self.remove_path(ftd, rommon) + self.remove_path(fxos_mgmt, rommon) + self.remove_path(rommon, fxos) + + self.remove_state(ftd) + + enable_to_fxos = Path(enable, fxos, connect_fxos, None) + fxos_to_enable = Path(fxos, enable, send_ctrl_caret_x, None) + enable_to_ftd_expert_root = Path(enable, ftd_expert_root, 'connect fxos root', None) + ftd_expert_root_to_enable = Path(ftd_expert_root, enable, 'exit', None) + enable_to_rommon = Path(enable, rommon, enable_to_rommon_transition, None) + rommon_to_disable = Path(rommon, disable, 'boot', None) + + self.add_path(enable_to_fxos) + self.add_path(fxos_to_enable) + self.add_path(enable_to_ftd_expert_root) + self.add_path(ftd_expert_root_to_enable) + self.add_path(enable_to_rommon) + self.add_path(rommon_to_disable) diff --git a/src/unicon/plugins/asa/fp2k/statements.py b/src/unicon/plugins/asa/fp2k/statements.py new file mode 100644 index 00000000..e21efdb7 --- /dev/null +++ b/src/unicon/plugins/asa/fp2k/statements.py @@ -0,0 +1,36 @@ +from unicon.eal.dialogs import Statement +from unicon.plugins.fxos.statements import fxos_statements +from unicon.plugins.generic.statements import update_context + +from .patterns import AsaFp2kPatterns + + +patterns = AsaFp2kPatterns() + + +reload_confirm_stmt = Statement(pattern=patterns.reload_confirm, + action='send(y)', + args=None, + loop_continue=True, + continue_timer=False) + +broken_pipe_stmt = Statement(pattern=patterns.broken_pipe, + action=update_context, + args={'console': False}, + loop_continue=False) + +restarting_system_stmt = Statement(pattern=patterns.restarting_system, + action=update_context, + args={'console': True}, + loop_continue=True, + continue_timer=True) + + +reload_statements = [ + reload_confirm_stmt, broken_pipe_stmt, restarting_system_stmt, + Statement(pattern=patterns.disable_prompt) +] + +boot_to_rommon_statements = reload_statements + [ + fxos_statements.boot_interrupt_stmt, Statement(pattern=patterns.rommon_prompt) +] diff --git a/src/unicon/plugins/asa/patterns.py b/src/unicon/plugins/asa/patterns.py index bdea62fd..f6a22d90 100644 --- a/src/unicon/plugins/asa/patterns.py +++ b/src/unicon/plugins/asa/patterns.py @@ -10,3 +10,8 @@ def __init__(self): self.enable_password = r'^.*Password:\s?$' self.bad_passwords = r'^Permission denied, please try again.$' self.disconnect_message = r'^Connection to .+? closed by remote host$' + self.reload_confirm = r'^(.*?)Proceed with reload\? \[confirm\]' + self.error_reporting = r'^(.*?)Would you like to enable anonymous error reporting to help improve the product\? \[Y\]es, \[N\]o, \[A\]sk later\s*$' + self.save_changes = r'^(.*?)config has been modified. Save\? \[Y]es\/\[N]o\/\[S]ave all/\[C]ancel:\s*?' + self.begin_config_replication = r'Beginning [Cc]onfiguration [Rr]eplication:? (from mate|Sending to mate)' + self.end_config_replication = r'End [Cc]onfiguration [Rr]eplication (to|from) mate' diff --git a/src/unicon/plugins/asa/provider.py b/src/unicon/plugins/asa/provider.py index e544be99..cb46eb40 100644 --- a/src/unicon/plugins/asa/provider.py +++ b/src/unicon/plugins/asa/provider.py @@ -9,5 +9,5 @@ class ASAConnectionProvider(GenericSingleRpConnectionProvider): def get_connection_dialog(self): dialog = super().get_connection_dialog() - dialog += Dialog([statements.bad_password_stmt]) + dialog += Dialog(statements.connection_statements) return dialog diff --git a/src/unicon/plugins/asa/service_implementation.py b/src/unicon/plugins/asa/service_implementation.py new file mode 100644 index 00000000..2777a792 --- /dev/null +++ b/src/unicon/plugins/asa/service_implementation.py @@ -0,0 +1,21 @@ +import os +import re + +from unicon.eal.dialogs import Dialog +from unicon.plugins.generic.service_implementation import Execute as GenericExecute, \ + Reload as GenericReload + +from .patterns import (ASAPatterns) +from .statements import execute_statements, reload_statements + +class ASAExecute(GenericExecute): + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.dialog += Dialog(execute_statements) + +class ASAReload(GenericReload): + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.dialog += Dialog(reload_statements) \ No newline at end of file diff --git a/src/unicon/plugins/asa/settings.py b/src/unicon/plugins/asa/settings.py index f250dbe9..cb345730 100644 --- a/src/unicon/plugins/asa/settings.py +++ b/src/unicon/plugins/asa/settings.py @@ -24,5 +24,7 @@ def __init__(self): self.MAX_COPY_ATTEMPTS = 2 self.ERROR_PATTERN = [ r'^ERROR:', - r'^WARNING:' + r'^WARNING:', + r'\*{1,} WARNING \*{1,}', + r'^Removing.*?failed.*?$' ] diff --git a/src/unicon/plugins/asa/statemachine.py b/src/unicon/plugins/asa/statemachine.py index 2c683bcb..312cdeb4 100644 --- a/src/unicon/plugins/asa/statemachine.py +++ b/src/unicon/plugins/asa/statemachine.py @@ -9,12 +9,11 @@ class ASAStateMachine(StateMachine): def create(self): p = patterns.ASAPatterns() - + enable = State('enable', p.enable_prompt) disable = State('disable', p.disable_prompt) config = State('config', p.config_prompt) - enable_to_disable = Path(enable, disable, 'disable', None) enable_to_config = Path(enable, config, 'config term', None) diff --git a/src/unicon/plugins/asa/statements.py b/src/unicon/plugins/asa/statements.py index 0d906690..54e4b826 100644 --- a/src/unicon/plugins/asa/statements.py +++ b/src/unicon/plugins/asa/statements.py @@ -16,6 +16,8 @@ from unicon.plugins.utils import (get_current_credential, common_cred_password_handler, ) +from unicon.plugins.generic.statements import enable_password_handler + from unicon.core.errors import UniconAuthenticationError from unicon.utils import to_plaintext @@ -24,19 +26,6 @@ patterns = ASAPatterns() settings = ASASettings() -def enable_password_handler(spawn, context, session): - credentials = context.get('credentials') - enable_credential = credentials[ENABLE_CRED_NAME] if credentials else None - if enable_credential: - try: - spawn.sendline(to_plaintext(enable_credential['password'])) - except KeyError as exc: - raise UniconAuthenticationError("No password has been defined " - "for credential {}.".format(ENABLE_CRED_NAME)) - - else: - spawn.sendline(context['enable_password']) - def line_password_handler(spawn, context, session): credential = get_current_credential(context=context, session=session) @@ -66,6 +55,7 @@ def escape_char_handler(spawn): spawn.sendline() + login_password = Statement(pattern=patterns.line_password, action=line_password_handler, args=None, @@ -104,7 +94,42 @@ def escape_char_handler(spawn): disconnect_error_stmt = Statement(pattern=patterns.disconnect_message, action=connection_failure_handler, - args={ - 'err': 'received disconnect from router'}, + args=None, loop_continue=False, continue_timer=False) + +reload_confirm_stmt = Statement(pattern=patterns.reload_confirm, + action='sendline(y)', + args=None, + loop_continue=True, + continue_timer=False) + +error_reporting_stmt = Statement(pattern=patterns.error_reporting, + action='sendline(A)', + args=None, + loop_continue=True, + continue_timer=False) + +save_config_stmt = Statement(pattern=patterns.save_changes, + action='sendline(S)', + args=None, + loop_continue=True, + continue_timer=False) + +begin_replication_stmt = Statement(pattern=patterns.begin_config_replication, + action=sendline, + args=None, + loop_continue=True, + continue_timer=False) + +end_replication_stmt = Statement(pattern=patterns.end_config_replication, + action=sendline, + args=None, + loop_continue=True, + continue_timer=False) + +connection_statements = [bad_password_stmt, begin_replication_stmt, end_replication_stmt] + +execute_statements = [error_reporting_stmt, save_config_stmt, begin_replication_stmt, end_replication_stmt] + +reload_statements = [save_config_stmt, begin_replication_stmt, end_replication_stmt] \ No newline at end of file diff --git a/src/unicon/plugins/cheetah/ap/__init__.py b/src/unicon/plugins/cheetah/ap/__init__.py index e225dfa5..f96c3b3e 100644 --- a/src/unicon/plugins/cheetah/ap/__init__.py +++ b/src/unicon/plugins/cheetah/ap/__init__.py @@ -1,27 +1,27 @@ __author__ = "Giacomo Trifilo " - from unicon.bases.routers.connection import BaseSingleRpConnection -from unicon.plugins.generic.statemachine import GenericSingleRpStateMachine from unicon.plugins.generic import ServiceList from unicon.plugins.generic import GenericSingleRpConnectionProvider from unicon.plugins.cheetah.ap.settings import ApSettings from unicon.plugins.cheetah.ap import service_implementation as svc from unicon.plugins.generic import service_implementation as gsvc +from unicon.plugins.iosxe import service_implementation as iosxe_svc +from .statemachine import ApSingleRpStateMachine class ApServiceList(ServiceList): def __init__(self): + super().__init__() self.execute = svc.Execute self.send = gsvc.Send self.sendline = gsvc.Sendline self.expect = gsvc.Expect self.enable = gsvc.Enable self.disable = gsvc.Disable - self.reload = gsvc.Reload - self.expect_log = gsvc.ExpectLogging + self.reload = svc.Reload self.log_user = gsvc.LogUser - + self.bash_console = iosxe_svc.BashService class ApSingleRpConnectionProvider(GenericSingleRpConnectionProvider): @@ -40,7 +40,6 @@ def set_init_commands(self): def init_handle(self): con = self.connection - con._is_connected = True con.state_machine.go_to('enable', self.connection.spawn, context=self.connection.context, @@ -50,9 +49,9 @@ def init_handle(self): class ApSingleRpConnection(BaseSingleRpConnection): os = 'cheetah' - series = 'ap' + platform = 'ap' chassis_type = 'single_rp' - state_machine_class = GenericSingleRpStateMachine + state_machine_class = ApSingleRpStateMachine connection_provider_class = ApSingleRpConnectionProvider subcommand_list = ApServiceList - settings = ApSettings() + settings = ApSettings() \ No newline at end of file diff --git a/src/unicon/plugins/cheetah/ap/patterns.py b/src/unicon/plugins/cheetah/ap/patterns.py new file mode 100644 index 00000000..89d05fb4 --- /dev/null +++ b/src/unicon/plugins/cheetah/ap/patterns.py @@ -0,0 +1,12 @@ +""" Generic Cheetah AP Patterns. """ + +__author__ = "Naveen " + +from unicon.plugins.generic.patterns import GenericPatterns + + +class CheetahAPPatterns(GenericPatterns): + + def __init__(self): + super().__init__() + self.ap_shell_prompt = r'^(.*?)\w+:\/(.*?)#\s?$' \ No newline at end of file diff --git a/src/unicon/plugins/cheetah/ap/service_implementation.py b/src/unicon/plugins/cheetah/ap/service_implementation.py index 93e690f6..56c0d67f 100644 --- a/src/unicon/plugins/cheetah/ap/service_implementation.py +++ b/src/unicon/plugins/cheetah/ap/service_implementation.py @@ -3,9 +3,12 @@ from unicon.plugins.generic.service_implementation import \ Execute as GenericExecute +from unicon.plugins.generic.service_implementation import \ + Reload as GenericReload from unicon.eal.dialogs import Dialog from unicon.plugins.iosxe.service_statements import confirm +from .service_statement import ap_reload_list class Execute(GenericExecute): def call_service(self, command=None, reply=Dialog([]), timeout=None, *args, @@ -14,3 +17,8 @@ def call_service(self, command=None, reply=Dialog([]), timeout=None, *args, super().call_service(command, reply=reply + Dialog([confirm,]), timeout=timeout, *args, **kwargs) + +class Reload(GenericReload): + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.dialog = self.dialog + Dialog(ap_reload_list) \ No newline at end of file diff --git a/src/unicon/plugins/cheetah/ap/service_patterns.py b/src/unicon/plugins/cheetah/ap/service_patterns.py new file mode 100644 index 00000000..05d4f343 --- /dev/null +++ b/src/unicon/plugins/cheetah/ap/service_patterns.py @@ -0,0 +1,8 @@ +"""AP Reload Service Patterns""" + +from unicon.plugins.generic.service_patterns import ReloadPatterns + +class APReloadPatterns(ReloadPatterns): + def __init__(self): + super().__init__() + self.ap_shell_prompt = r'^Proceed with reload (command (\W+cold\W)?)?(\?) (\[)+confirm+(\])$' \ No newline at end of file diff --git a/src/unicon/plugins/cheetah/ap/service_statement.py b/src/unicon/plugins/cheetah/ap/service_statement.py new file mode 100644 index 00000000..ab271cde --- /dev/null +++ b/src/unicon/plugins/cheetah/ap/service_statement.py @@ -0,0 +1,35 @@ +""" +Module: + unicon.plugins.generic +Authors: + pyATS TEAM (pyats-support@cisco.com, pyats-support-ext@cisco.com) +Description: + Module for defining all Services Statement, handlers(callback) and Statement + list for service dialog would be defined here. +""" + +from time import sleep + +from unicon.eal.dialogs import Statement +from unicon.plugins.generic.statements import chatty_term_wait +from .service_patterns import APReloadPatterns +from unicon.plugins.generic.service_statements import reload_statement_list + +pat = APReloadPatterns() + + +def send_response(spawn, response=""): + chatty_term_wait(spawn) + spawn.sendline(response) + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# +# Reload Statements +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# + +ap_shell_prompt = Statement(pattern=pat.ap_shell_prompt, + action=send_response, args={'response': '\r'}, + loop_continue=True, + continue_timer=False) + +ap_reload_list = list(reload_statement_list) +ap_reload_list.insert(0,ap_shell_prompt) \ No newline at end of file diff --git a/src/unicon/plugins/cheetah/ap/settings.py b/src/unicon/plugins/cheetah/ap/settings.py index bccb3d66..6ef4faf2 100644 --- a/src/unicon/plugins/cheetah/ap/settings.py +++ b/src/unicon/plugins/cheetah/ap/settings.py @@ -13,4 +13,4 @@ def __init__(self): 'terminal width 0', 'show version', 'logging console disable', - ] + ] \ No newline at end of file diff --git a/src/unicon/plugins/cheetah/ap/statemachine.py b/src/unicon/plugins/cheetah/ap/statemachine.py new file mode 100644 index 00000000..881b9d2d --- /dev/null +++ b/src/unicon/plugins/cheetah/ap/statemachine.py @@ -0,0 +1,50 @@ +from unicon.plugins.generic.statemachine import GenericSingleRpStateMachine +from unicon.plugins.generic.statements import GenericStatements, default_statement_list +from unicon.statemachine import State, Path, StateMachine +from unicon.eal.dialogs import Dialog, Statement +from unicon.plugins.generic.patterns import GenericPatterns + +from .patterns import CheetahAPPatterns + +statements = GenericStatements() +patterns = CheetahAPPatterns() + + +class ApSingleRpStateMachine(GenericSingleRpStateMachine): + + def create(self): + + ########################################################## + # State Definition + ########################################################## + + disable = State('disable', patterns.disable_prompt) + enable = State('enable', patterns.enable_prompt) + shell = State('shell', patterns.ap_shell_prompt) + + ########################################################## + # Path Definition + ########################################################## + + disable_to_enable = Path(disable, enable, 'enable', Dialog([ + statements.enable_password_stmt, + statements.bad_password_stmt, + statements.syslog_stripper_stmt + ])) + enable_to_disable = Path(enable, disable, 'disable', None) + + # Adding SHELL state to Cheetah platform. + enable_to_shell = Path(enable, shell, 'devshell', None) + shell_to_enable = Path(shell, enable, 'exit', None) + + # Add State and Path to State Machine + self.add_state(shell) + self.add_state(disable) + self.add_state(enable) + + self.add_path(disable_to_enable) + self.add_path(enable_to_shell) + self.add_path(shell_to_enable) + self.add_path(enable_to_disable) + + self.add_default_statements(default_statement_list) diff --git a/src/unicon/plugins/cimc/__init__.py b/src/unicon/plugins/cimc/__init__.py index 29e10464..e5a15d18 100644 --- a/src/unicon/plugins/cimc/__init__.py +++ b/src/unicon/plugins/cimc/__init__.py @@ -14,7 +14,6 @@ class CimcConnectionProvider(GenericSingleRpConnectionProvider): """ def init_handle(self): con = self.connection - con._is_connected = True self.execute_init_commands() @@ -25,7 +24,6 @@ def __init__(self): self.send = svc.Send self.sendline = svc.Sendline self.expect = svc.Expect - self.expect_log = svc.ExpectLogging self.log_user = svc.LogUser self.execute = cimc_svc.Execute @@ -35,7 +33,7 @@ class CimcConnection(GenericSingleRpConnection): Connection class for cimc connections. """ os = 'cimc' - series = None + platform = None chassis_type = 'single_rp' state_machine_class = CimcStateMachine connection_provider_class = CimcConnectionProvider diff --git a/src/unicon/plugins/cimc/patterns.py b/src/unicon/plugins/cimc/patterns.py index 6d9736b3..53752abe 100644 --- a/src/unicon/plugins/cimc/patterns.py +++ b/src/unicon/plugins/cimc/patterns.py @@ -5,5 +5,5 @@ class CimcPatterns(GenericPatterns): def __init__(self): super().__init__() - self.prompt = r'^(.*)\S+\s?(/\w+)*\s?#\s*$' + self.prompt = r'^(.*?)\S+\s?(/\w+)*\s?#\s*$' self.enter_yes_or_no = r"^(.*?)Enter 'yes' or 'no' to confirm.*->\s*$" diff --git a/src/unicon/plugins/comware/__init__.py b/src/unicon/plugins/comware/__init__.py new file mode 100755 index 00000000..94884dcd --- /dev/null +++ b/src/unicon/plugins/comware/__init__.py @@ -0,0 +1,43 @@ +''' +Author: Renato Almeida de Oliveira +Contact: renato.almeida.oliveira@gmail.com +https://twitter.com/ORenato_Almeida +https://www.youtube.com/c/RenatoAlmeidadeOliveira +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' + +from unicon.bases.routers.connection import BaseSingleRpConnection +from unicon.plugins.generic import ServiceList + +from unicon.plugins.comware.statemachine import HPComwareSingleRpStateMachine +from unicon.plugins.comware.connection_provider import HPComwareSingleRpConnectionProvider +from unicon.plugins.comware import service_implementation as svc +from unicon.plugins.comware.settings import HPSettings + + +class HPComwareServiceList(ServiceList): + '''HP Comware Service List + ''' + + def __init__(self): + super().__init__() + self.execute = svc.HPExecute + self.configure = svc.HPConfigure + self.save = svc.HPSave + self.ping = svc.HPComwarePing + self.traceroute = svc.HPComwareTraceroute + + +class HPComwareSingleRPConnection(BaseSingleRpConnection): + '''HPComwareSingleRPConnection + + HP Comware platform support. + ''' + os = 'comware' + chassis_type = 'single_rp' + state_machine_class = HPComwareSingleRpStateMachine + connection_provider_class = HPComwareSingleRpConnectionProvider + subcommand_list = HPComwareServiceList + settings = HPSettings() + diff --git a/src/unicon/plugins/comware/connection_provider.py b/src/unicon/plugins/comware/connection_provider.py new file mode 100644 index 00000000..5d21d829 --- /dev/null +++ b/src/unicon/plugins/comware/connection_provider.py @@ -0,0 +1,42 @@ +''' +Author: Renato Almeida de Oliveira +Contact: renato.almeida.oliveira@gmail.com +https://twitter.com/ORenato_Almeida +https://www.youtube.com/c/RenatoAlmeidadeOliveira +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' + +from unicon.bases.routers.connection_provider import BaseSingleRpConnectionProvider +from unicon.plugins.generic.statements import custom_auth_statements, connection_statement_list +from unicon.eal.dialogs import Dialog +from unicon.plugins.comware.patterns import HPComwarePatterns + +patterns = HPComwarePatterns() + + +class HPComwareSingleRpConnectionProvider(BaseSingleRpConnectionProvider): + """ Implements Generic singleRP Connection Provider, + This class overrides the base class with the + additional dialogs and steps required for + connecting to any device via generic implementation + """ + def __init__(self, *args, **kwargs): + + """ Initializes the generic connection provider + """ + super().__init__(*args, **kwargs) + + def get_connection_dialog(self): + """ creates and returns a Dialog to handle all device prompts + appearing during initial connection to the device. + See statements.py for connnection statement lists + """ + con = self.connection + custom_auth_stmt = custom_auth_statements( + patterns.login_prompt, + patterns.password) + return con.connect_reply + \ + Dialog(custom_auth_stmt + connection_statement_list + if custom_auth_stmt else connection_statement_list) + diff --git a/src/unicon/plugins/comware/patterns.py b/src/unicon/plugins/comware/patterns.py new file mode 100755 index 00000000..6434b81a --- /dev/null +++ b/src/unicon/plugins/comware/patterns.py @@ -0,0 +1,23 @@ +''' +Author: Renato Almeida de Oliveira +Contact: renato.almeida.oliveira@gmail.com +https://twitter.com/ORenato_Almeida +https://www.youtube.com/c/RenatoAlmeidadeOliveira +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' +import re + +from unicon.plugins.generic.patterns import GenericPatterns + + +class HPComwarePatterns(GenericPatterns): + def __init__(self): + super().__init__() + self.login_prompt = r'^ *login as: *$' + self.user_exec_mode = r'^.*<%N>$' + self.config_mode = r'^ *\[%N(-.*)?\]$' + self.password = r'^.* password: $' + self.save_confirm = r'The current configuration will be written to the device\. Are you sure\? \[Y/N\]:' + self.file_save = r'^.*\(To leave the existing filename unchanged, press the enter key\):' + self.overwrite = r'^.* exists, overwrite\? \[Y/N\]:' diff --git a/src/unicon/plugins/comware/service_implementation.py b/src/unicon/plugins/comware/service_implementation.py new file mode 100755 index 00000000..a38e2b2d --- /dev/null +++ b/src/unicon/plugins/comware/service_implementation.py @@ -0,0 +1,207 @@ +''' +Author: Renato Almeida de Oliveira +Contact: renato.almeida.oliveira@gmail.com +https://twitter.com/ORenato_Almeida +https://www.youtube.com/c/RenatoAlmeidadeOliveira +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' + +from unicon.bases.routers.services import BaseService +from unicon.core.errors import SubCommandFailure +from unicon.eal.dialogs import Dialog +from unicon.eal.dialogs import Statement +from unicon.plugins.generic.service_implementation import ( + Execute as GenericExecute, + Configure as GenericConfigure, +) +from unicon.plugins.comware.service_statements import ( + save_confirm, + sendPath, + send_response +) +from unicon.plugins.comware.patterns import HPComwarePatterns + +from time import sleep + +patterns = HPComwarePatterns() + + +class HPExecute(GenericExecute): + pass + + +class HPConfigure(GenericConfigure): + pass + + +class HPSave(GenericExecute): + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.dialog += Dialog([save_confirm]) + self.error_pattern = [r'The file name is invalid\(does not end with \.cfg\)\!'] + + def call_service(self, file_path=None, overwrite=True): + + file_path = Statement(pattern=patterns.file_save, + action=sendPath, args={'path': file_path}, + loop_continue=True, + continue_timer=False) + self.dialog.append(file_path) + if(overwrite is False): + self.error_pattern += [r'^.*exists, overwrite\? \[Y/N\]:'] + save_overwrite = Statement(pattern=patterns.overwrite, + action=send_response, + args={'response': 'N'}, + loop_continue=True, + continue_timer=False) + self.dialog.append(save_overwrite) + else: + self.error_pattern = [] + save_overwrite = Statement(pattern=patterns.overwrite, + action=send_response, + args={'response': 'Y'}, + loop_continue=True, + continue_timer=False) + self.dialog.append(save_overwrite) + + + super().call_service("save") + + +class HPComwarePing(BaseService): + + def __init__(self, connection, context, **kwargs): + self.connection = connection + self.context = context + self.timeout_pattern = ['Timeout occurred', ] + self.error_pattern = [r'Unknown host', r'HELP'] + self.start_state = 'enable' + self.end_state = 'enable' + self.result = None + self.timeout = 60 + + # add the keyword arguments to the object + self.__dict__.update(kwargs) + + def call_service(self, addr, proto='ip', timeout=60, count=5, **kwargs): + con = self.connection + total_timeout = timeout * count + cmd = 'ping ' + if((proto == 'ip') or (proto == 'ipv6')): + cmd = cmd + proto + " " + else: + raise SubCommandFailure("Protocol should be ip or ipv6") + if('src_addr' in kwargs): + src_addr = kwargs['src_addr'] + source_cmd = "-a {src_addr} ".format(src_addr=src_addr) + cmd = cmd + source_cmd + if(isinstance(count, int)): + count_cmd = "-c {count} ".format(count=count) + cmd = cmd + count_cmd + if(isinstance(timeout, int)): + timeout_ms = timeout * 1000 + if( timeout_ms > 65535): + raise SubCommandFailure('Timeout should be less than 65.535 s') + timeout_cmd = "-t {timeout_ms} ".format(timeout_ms=timeout_ms) + cmd = cmd + timeout_cmd + if('vrf' in kwargs): + vrf = kwargs['vrf'] + vrf_cmd = "-vpn-instance {vrf} ".format(vrf=vrf) + cmd = cmd + vrf_cmd + if('ttl' in kwargs): + ttl = kwargs['ttl'] + ttl_cmd = "-h {ttl} ".format(ttl=ttl) + cmd = cmd + ttl_cmd + + cmd = cmd + addr + con.spawn.sendline(cmd) + + + try: + # Wait for prompt + state = con.state_machine.get_state('enable') + self.result = con.spawn.expect(state.pattern, timeout=total_timeout).match_output + except KeyboardInterrupt: + con.spawn.sendline('\x03') + sleep(0.5) + state = con.state_machine.get_state('enable') + self.result = con.spawn.expect(state.pattern, timeout=timeout).match_output + raise SubCommandFailure('Execution Interrupted') + except Exception: + raise SubCommandFailure('Ping failed') + + if self.result.rfind(self.connection.hostname): + self.result = self.result[:self.result.rfind(self.connection.hostname)].strip() + + +class HPComwareTraceroute(BaseService): + + def __init__(self, connection, context, **kwargs): + self.connection = connection + self.context = context + self.timeout_pattern = ['Timeout occurred', ] + self.error_pattern = [r'Destination not found inside Max Hop Count', + r'Incorrect', r'HELP'] + self.start_state = 'enable' + self.end_state = 'enable' + self.result = None + self.timeout = 60*20 + + # add the keyword arguments to the object + self.__dict__.update(kwargs) + + def call_service(self, addr, proto='ip', timeout=60, probes=3, max_ttl=30, **kwargs): + con = self.connection + total_timeout = timeout * probes * probes * max_ttl + cmd = 'tracert ' + if((proto == 'ip') or (proto == 'ipv6')): + if(proto == 'ipv6'): + cmd = cmd + proto + " " + else: + raise SubCommandFailure("Protocol should be ip or ipv6") + if('src_addr' in kwargs): + src_addr = kwargs['src_addr'] + source_cmd = "-a {src_addr} ".format(src_addr=src_addr) + cmd = cmd + source_cmd + if(isinstance(probes,int)): + probes_cmd = "-q {probes} ".format(probes=probes) + cmd = cmd + probes_cmd + if(isinstance(timeout, int)): + timeout_ms = timeout * 1000 + if( timeout_ms > 65535): + raise SubCommandFailure('Timeout should be less than 65.535 s') + timeout_cmd = "-w {timeout_ms} ".format(timeout_ms=timeout_ms) + cmd = cmd + timeout_cmd + if('vrf' in kwargs): + vrf = kwargs['vrf'] + vrf_cmd = "-vpn-instance {vrf} ".format(vrf=vrf) + cmd = cmd + vrf_cmd + if('min_ttl' in kwargs): + min_ttl = kwargs['min_ttl'] + min_ttl_cmd = "-f {min_ttl} ".format(min_ttl=min_ttl) + cmd = cmd + min_ttl_cmd + if(isinstance(max_ttl,int)): + max_ttl_cmd = "-m {max_ttl} ".format(max_ttl=max_ttl) + cmd = cmd + max_ttl_cmd + + cmd = cmd + addr + con.spawn.sendline(cmd) + + + try: + # Wait for prompt + state = con.state_machine.get_state('enable') + self.result = con.spawn.expect(state.pattern, timeout=total_timeout).match_output + except KeyboardInterrupt: + con.spawn.sendline('\x03') + sleep(0.5) + state = con.state_machine.get_state('enable') + self.result = con.spawn.expect(state.pattern, timeout=timeout).match_output + raise SubCommandFailure('Execution Interrupted') + except Exception: + raise SubCommandFailure('Traceroute failed') + + if self.result.rfind(self.connection.hostname): + self.result = self.result[:self.result.rfind(self.connection.hostname)].strip() diff --git a/src/unicon/plugins/comware/service_statements.py b/src/unicon/plugins/comware/service_statements.py new file mode 100644 index 00000000..1ca63ccc --- /dev/null +++ b/src/unicon/plugins/comware/service_statements.py @@ -0,0 +1,35 @@ +''' +Author: Renato Almeida de Oliveira +Contact: renato.almeida.oliveira@gmail.com +https://twitter.com/ORenato_Almeida +https://www.youtube.com/c/RenatoAlmeidadeOliveira +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' + +from unicon.eal.dialogs import Statement +from unicon.plugins.comware.patterns import HPComwarePatterns + +from time import sleep + +patterns = HPComwarePatterns() + + +def send_response(spawn, response=""): + sleep(0.5) + spawn.sendline(response) + + +def sendPath(spawn, path=None): + sleep(0.5) + if path is not None: + spawn.sendline(path) + else: + spawn.sendline() + + +save_confirm = Statement(pattern=patterns.save_confirm, + action=send_response, args={'response': 'Y'}, + loop_continue=True, + continue_timer=False) + diff --git a/src/unicon/plugins/comware/settings.py b/src/unicon/plugins/comware/settings.py new file mode 100755 index 00000000..bf682c8b --- /dev/null +++ b/src/unicon/plugins/comware/settings.py @@ -0,0 +1,21 @@ +''' +Author: Renato Almeida de Oliveira +Contact: renato.almeida.oliveira@gmail.com +https://twitter.com/ORenato_Almeida +https://www.youtube.com/c/RenatoAlmeidadeOliveira +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' + +from unicon.plugins.generic.settings import GenericSettings + + +class HPSettings(GenericSettings): + + def __init__(self): + # inherit any parent settings + super().__init__() + self.CONNECTION_TIMEOUT = 60*5 + self.ESCAPE_CHAR_CALLBACK_PRE_SENDLINE_PAUSE_SEC = 1 + self.HA_INIT_EXEC_COMMANDS = ['screen-length disable'] + self.HA_INIT_CONFIG_COMMANDS = [] \ No newline at end of file diff --git a/src/unicon/plugins/comware/statemachine.py b/src/unicon/plugins/comware/statemachine.py new file mode 100755 index 00000000..a9d8cc5b --- /dev/null +++ b/src/unicon/plugins/comware/statemachine.py @@ -0,0 +1,63 @@ +''' +Author: Renato Almeida de Oliveira +Contact: renato.almeida.oliveira@gmail.com +https://twitter.com/ORenato_Almeida +https://www.youtube.com/c/RenatoAlmeidadeOliveira +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' + +from unicon.statemachine import State, Path +from unicon.plugins.comware.patterns import HPComwarePatterns +from unicon.plugins.generic.statemachine import GenericSingleRpStateMachine + + +patterns = HPComwarePatterns() + + +class HPComwareSingleRpStateMachine(GenericSingleRpStateMachine): + """ + Defines HP Comware StateMachine for singleRP + Statemachine keeps in track all the supported states + for this platform, also have detail about moving from + one state to another + """ + def create(self): + """creates the hp comware state machine""" + + super().create() + ########################################################## + # Remove unused paths and state + ########################################################## + self.remove_path('enable', 'rommon') + self.remove_path('rommon', 'disable') + self.remove_path('disable', 'enable') + + self.remove_state('rommon') + self.remove_state('disable') + ########################################################## + # Remove replaced states and paths + ########################################################## + self.remove_path('enable','config') + self.remove_path('config','enable') + + self.remove_state('enable') + self.remove_state('config') + ########################################################## + # State Definition + ########################################################## + enable = State('enable', patterns.user_exec_mode) + config = State('config', patterns.config_mode) + ########################################################## + # Path Definition + ########################################################## + + enable_to_config = Path(enable, config, 'system-view', None) + config_to_enable = Path(config, enable, 'return', None) + + # Add State and Path to State Machine + self.add_state(enable) + self.add_state(config) + + self.add_path(enable_to_config) + self.add_path(config_to_enable) diff --git a/src/unicon/plugins/confd/__init__.py b/src/unicon/plugins/confd/__init__.py index 7b4f76f1..da98560f 100644 --- a/src/unicon/plugins/confd/__init__.py +++ b/src/unicon/plugins/confd/__init__.py @@ -71,7 +71,6 @@ def init_handle(self): """ Executes the init commands on the device """ con = self.connection - con._is_connected = True con.state_machine.detect_state(con.spawn) if con.state_machine.current_cli_style == 'cisco': @@ -95,12 +94,12 @@ def __init__(self): self.send = svc.Send self.sendline = svc.Sendline self.expect = svc.Expect - self.expect_log = svc.ExpectLogging self.log_user = svc.LogUser self.execute = confd_svc.Execute self.configure = confd_svc.Configure self.cli_style = confd_svc.CliStyle self.command = confd_svc.Command + self.expect_log = svc.ExpectLogging class ConfdConnection(GenericSingleRpConnection): @@ -108,7 +107,7 @@ class ConfdConnection(GenericSingleRpConnection): Connection class for ConfD connections. """ os = 'confd' - series = None + platform = None chassis_type = 'single_rp' state_machine_class = ConfdStateMachine connection_provider_class = ConfdConnectionProvider diff --git a/src/unicon/plugins/confd/csp/__init__.py b/src/unicon/plugins/confd/csp/__init__.py index 6acc7db3..b68265c7 100644 --- a/src/unicon/plugins/confd/csp/__init__.py +++ b/src/unicon/plugins/confd/csp/__init__.py @@ -16,7 +16,7 @@ def __init__(self): class CspSingleRPConnection(ConfdConnection): os = 'confd' - series = 'csp' + platform = 'csp' chassis_type = 'single_rp' state_machine_class = CspStateMachine connection_provider_class = ConfdConnectionProvider diff --git a/src/unicon/plugins/confd/csp/service_implementation.py b/src/unicon/plugins/confd/csp/service_implementation.py index 48fd5135..15177de9 100644 --- a/src/unicon/plugins/confd/csp/service_implementation.py +++ b/src/unicon/plugins/confd/csp/service_implementation.py @@ -51,7 +51,6 @@ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) self.start_state = 'cisco_exec' self.end_state = 'cisco_exec' - self.service_name = 'reload' self.timeout = connection.settings.RELOAD_TIMEOUT self.__doc__ = self.__doc__.format(connection.settings.RELOAD_TIMEOUT) diff --git a/src/unicon/plugins/confd/esc/__init__.py b/src/unicon/plugins/confd/esc/__init__.py index 8a186dc4..f6724ffc 100644 --- a/src/unicon/plugins/confd/esc/__init__.py +++ b/src/unicon/plugins/confd/esc/__init__.py @@ -13,7 +13,7 @@ def __init__(self): class EscSingleRPConnection(ConfdConnection): os = 'confd' - series = 'esc' + platform = 'esc' chassis_type = 'single_rp' state_machine_class = ConfdStateMachine connection_provider_class = ConfdConnectionProvider diff --git a/src/unicon/plugins/confd/nfvis/__init__.py b/src/unicon/plugins/confd/nfvis/__init__.py index 750939e1..ab53ef82 100644 --- a/src/unicon/plugins/confd/nfvis/__init__.py +++ b/src/unicon/plugins/confd/nfvis/__init__.py @@ -14,7 +14,7 @@ def __init__(self): class NfvisSingleRPConnection(ConfdConnection): os = 'confd' - series = 'nfvis' + platform = 'nfvis' chassis_type = 'single_rp' state_machine_class = NfvisStateMachine connection_provider_class = ConfdConnectionProvider diff --git a/src/unicon/plugins/confd/service_implementation.py b/src/unicon/plugins/confd/service_implementation.py index c81b03ac..8f419977 100644 --- a/src/unicon/plugins/confd/service_implementation.py +++ b/src/unicon/plugins/confd/service_implementation.py @@ -48,8 +48,9 @@ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) self.timeout_pattern = ['Timeout occurred', ] self.result = None - self.service_name = 'command' self.timeout = connection.settings.EXEC_TIMEOUT + self.start_state = 'any' + self.end_state = 'any' def call_service(self, command, reply=Dialog([]), @@ -68,7 +69,9 @@ def call_service(self, command, if not isinstance(command, str): raise SubCommandFailure('Command is not a string: %s' % type(command)) - if not isinstance(reply, Dialog): + if (reply is None) or (reply == []): + reply = Dialog([]) + elif not isinstance(reply, Dialog): raise SubCommandFailure( "dialog passed via 'reply' must be an instance of Dialog") @@ -80,7 +83,7 @@ def call_service(self, command, if 'service_dialog' in kwargs: service_dialog = kwargs['service_dialog'] - if service_dialog is None: + if (service_dialog is None) or (service_dialog == []): service_dialog = Dialog([]) elif not isinstance(service_dialog, Dialog): raise SubCommandFailure( @@ -139,8 +142,9 @@ class Configure(BaseService): """ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) - self.service_name = 'configure' self.timeout = connection.settings.CONFIG_TIMEOUT + self.start_state = 'any' + self.end_state = 'any' def call_service(self, command=[], reply=Dialog([]), @@ -173,7 +177,9 @@ def call_service(self, command=[], if isinstance(command, str): command = command.splitlines() self.command_list_is_empty = False - if not isinstance(reply, Dialog): + if (reply is None) or (reply == []): + reply = Dialog([]) + elif not isinstance(reply, Dialog): raise SubCommandFailure( "dialog passed via 'reply' must be an instance of Dialog") @@ -247,13 +253,9 @@ class Execute(GenericServices.Execute): """ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) - self.service_name = 'execute' def pre_service(self, command, *args, **kwargs): - super().pre_service(*args, **kwargs) sm = self.get_sm() - con = self.connection - self.saved_cli_style = sm.current_cli_style if 'style' in kwargs: style = kwargs['style'] @@ -263,6 +265,7 @@ def pre_service(self, command, *args, **kwargs): elif sm.current_cli_style == 'juniper': if style[0].lower() == 'c': self.start_state = "cisco_" + sm.current_cli_mode + super().pre_service(*args, **kwargs) def post_service(self, *args, **kwargs): sm = self.get_sm() @@ -295,8 +298,9 @@ class CliStyle(BaseService): def __init__(self, connection, context, **kwargs): # Connection object will have all the received details super().__init__(connection, context, **kwargs) + self.start_state = 'any' + self.end_state = 'any' self.__dict__.update(kwargs) - self.service_name = 'cli_style' def call_service(self, style, *args, **kwargs): # Get current state of the state machine and determine end state diff --git a/src/unicon/plugins/confd/settings.py b/src/unicon/plugins/confd/settings.py index 0bc1ac97..235b5353 100644 --- a/src/unicon/plugins/confd/settings.py +++ b/src/unicon/plugins/confd/settings.py @@ -27,7 +27,7 @@ def __init__(self): self.JUNIPER_INIT_CONFIG_COMMANDS = [] # Prompt prefixes will be removed from the output by the configure() and execute() services - self.JUNIPER_PROMPT_PREFIX = "\[edit\]" + self.JUNIPER_PROMPT_PREFIX = r"\[edit\]" self.ERROR_PATTERN = [ 'Error:', diff --git a/src/unicon/plugins/dnos10/__init__.py b/src/unicon/plugins/dnos10/__init__.py new file mode 100644 index 00000000..970df39c --- /dev/null +++ b/src/unicon/plugins/dnos10/__init__.py @@ -0,0 +1,27 @@ +''' +Author: Knox Hutchinson +Contact: https://dataknox.dev +https://twitter.com/data_knox +https://youtube.com/c/dataknox +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' + +from unicon.bases.routers.connection import BaseSingleRpConnection +from unicon.plugins.generic import GenericSingleRpConnectionProvider +from .statemachine import Dnos10SingleRpStateMachine +from .services import Dnos10ServiceList +from .settings import Dnos10Settings + + +class Dnos10SingleRPConnection(BaseSingleRpConnection): + '''Dnos10SingleRPConnection + + Dell OS10 PowerSwitch support + ''' + os = 'dnos10' + chassis_type = 'single_rp' + state_machine_class = Dnos10SingleRpStateMachine + connection_provider_class = GenericSingleRpConnectionProvider + subcommand_list = Dnos10ServiceList + settings = Dnos10Settings() diff --git a/src/unicon/plugins/dnos10/patterns.py b/src/unicon/plugins/dnos10/patterns.py new file mode 100644 index 00000000..ee7417ec --- /dev/null +++ b/src/unicon/plugins/dnos10/patterns.py @@ -0,0 +1,19 @@ +''' +Author: Knox Hutchinson +Contact: https://dataknox.dev +https://twitter.com/data_knox +https://youtube.com/c/dataknox +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' +from unicon.plugins.generic.patterns import GenericPatterns + + +class Dnos10Patterns(GenericPatterns): + def __init__(self): + super().__init__() + self.login_prompt = r' *login here: *?' + self.disable_mode = r'\w+>$' + self.privileged_mode = r'\w+[^\(config\)]#$' + self.config_mode = r'\w+\(config[-\w]+\)#$' + self.password = r'Password:' diff --git a/src/unicon/plugins/dnos10/services.py b/src/unicon/plugins/dnos10/services.py new file mode 100644 index 00000000..77c4024b --- /dev/null +++ b/src/unicon/plugins/dnos10/services.py @@ -0,0 +1,41 @@ +''' +Author: Knox Hutchinson +Contact: https://dataknox.dev +https://twitter.com/data_knox +https://youtube.com/c/dataknox +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' +import logging + +from unicon.plugins.generic.service_implementation import Execute as GenericExec +from unicon.plugins.ios.iosv import IosvServiceList + +logger = logging.getLogger(__name__) + + +class Execute(GenericExec): + ''' + Demonstrating how to augment an existing service by updating its call + service method + ''' + + def call_service(self, *args, **kwargs): + # custom... code here + logger.info('execute service called') + + # call parent + super().call_service(*args, **kwargs) + + +class Dnos10ServiceList(IosvServiceList): + ''' + class aggregating all service lists for this platform + ''' + + def __init__(self): + # use the parent servies + super().__init__() + + # overwrite and add our own + self.execute = Execute diff --git a/src/unicon/plugins/dnos10/settings.py b/src/unicon/plugins/dnos10/settings.py new file mode 100644 index 00000000..4e9763c9 --- /dev/null +++ b/src/unicon/plugins/dnos10/settings.py @@ -0,0 +1,21 @@ +''' +Author: Knox Hutchinson +Contact: https://dataknox.dev +https://twitter.com/data_knox +https://youtube.com/c/dataknox +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' + +from unicon.plugins.generic.settings import GenericSettings + + +class Dnos10Settings(GenericSettings): + + def __init__(self): + # inherit any parent settings + super().__init__() + self.CONNECTION_TIMEOUT = 60*5 + self.ESCAPE_CHAR_CALLBACK_PRE_SENDLINE_PAUSE_SEC = 1 + self.HA_INIT_EXEC_COMMANDS = [] + self.HA_INIT_CONFIG_COMMANDS = [] \ No newline at end of file diff --git a/src/unicon/plugins/dnos10/statemachine.py b/src/unicon/plugins/dnos10/statemachine.py new file mode 100644 index 00000000..26aba35d --- /dev/null +++ b/src/unicon/plugins/dnos10/statemachine.py @@ -0,0 +1,37 @@ +''' +Author: Knox Hutchinson +Contact: https://dataknox.dev +https://twitter.com/data_knox +https://youtube.com/c/dataknox +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' + +from unicon.statemachine import Path +from unicon.eal.dialogs import Dialog +from unicon.plugins.generic.statemachine import GenericSingleRpStateMachine +from . import statements as stmts + + +class Dnos10SingleRpStateMachine(GenericSingleRpStateMachine): + + def create(self): + ''' + statemachine class's create() method is its entrypoint. This showcases + how to setup a statemachine in Unicon. + ''' + super().create() + + # remove some known path + self.remove_path('enable', 'rommon') + self.remove_path('rommon', 'disable') + self.remove_state('rommon') + + self.remove_path('disable', 'enable') + enable = self.get_state('enable') + disable = self.get_state('disable') + disable_to_enable = Path(disable, + enable, + 'enable', + Dialog([stmts.password_stmt])) + self.add_path(disable_to_enable) diff --git a/src/unicon/plugins/dnos10/statements.py b/src/unicon/plugins/dnos10/statements.py new file mode 100644 index 00000000..8226703e --- /dev/null +++ b/src/unicon/plugins/dnos10/statements.py @@ -0,0 +1,40 @@ +''' +Author: Knox Hutchinson +Contact: https://dataknox.dev +https://twitter.com/data_knox +https://youtube.com/c/dataknox +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' +from unicon.eal.dialogs import Statement +from unicon.plugins.generic.statements import GenericStatements +from .patterns import Dnos10Patterns +from unicon.plugins.generic.statements import enable_password_handler + +statements = GenericStatements() +patterns = Dnos10Patterns() + +def login_handler(spawn, context, session): + spawn.sendline(context['enable_password']) + +def send_enabler(spawn, context, session): + spawn.sendline('enable') + +# define the list of statements particular to this platform +login_stmt = Statement(pattern=patterns.login_prompt, + action=login_handler, + args=None, + loop_continue=True, + continue_timer=False) + +enable_stmt = Statement(pattern=patterns.disable_mode, + action=send_enabler, + args=None, + loop_continue=True, + continue_timer=False) + +password_stmt = Statement(pattern=patterns.password, + action=enable_password_handler, + args=None, + loop_continue=True, + continue_timer=False) diff --git a/src/unicon/plugins/dnos6/__init__.py b/src/unicon/plugins/dnos6/__init__.py new file mode 100644 index 00000000..304190f2 --- /dev/null +++ b/src/unicon/plugins/dnos6/__init__.py @@ -0,0 +1,27 @@ +''' +Author: Knox Hutchinson +Contact: https://dataknox.dev +https://twitter.com/data_knox +https://youtube.com/c/dataknox +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' + +from unicon.bases.routers.connection import BaseSingleRpConnection +from unicon.plugins.generic import GenericSingleRpConnectionProvider +from .statemachine import Dnos6SingleRpStateMachine +from .services import Dnos6ServiceList +from .settings import Dnos6Settings + + +class Dnos6SingleRPConnection(BaseSingleRpConnection): + '''Dnos6SingleRPConnection + + Dell OS6 PowerSwitch support. + ''' + os = 'dnos6' + chassis_type = 'single_rp' + state_machine_class = Dnos6SingleRpStateMachine + connection_provider_class = GenericSingleRpConnectionProvider + subcommand_list = Dnos6ServiceList + settings = Dnos6Settings() diff --git a/src/unicon/plugins/dnos6/patterns.py b/src/unicon/plugins/dnos6/patterns.py new file mode 100644 index 00000000..c7da66c0 --- /dev/null +++ b/src/unicon/plugins/dnos6/patterns.py @@ -0,0 +1,19 @@ +''' +Author: Knox Hutchinson +Contact: https://dataknox.dev +https://twitter.com/data_knox +https://youtube.com/c/dataknox +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' +from unicon.plugins.generic.patterns import GenericPatterns + + +class Dnos6Patterns(GenericPatterns): + def __init__(self): + super().__init__() + self.login_prompt = r' *login here: *?' + self.disable_mode = r'\w+>$' + self.privileged_mode = r'\w+[^\(config\)]#$' + self.config_mode = r'\w+\(config[-\w]+\)#$' + self.password = r'Password:' diff --git a/src/unicon/plugins/dnos6/services.py b/src/unicon/plugins/dnos6/services.py new file mode 100644 index 00000000..84b33352 --- /dev/null +++ b/src/unicon/plugins/dnos6/services.py @@ -0,0 +1,41 @@ +''' +Author: Knox Hutchinson +Contact: https://dataknox.dev +https://twitter.com/data_knox +https://youtube.com/c/dataknox +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' +import logging + +from unicon.plugins.generic.service_implementation import Execute as GenericExec +from unicon.plugins.ios.iosv import IosvServiceList + +logger = logging.getLogger(__name__) + + +class Execute(GenericExec): + ''' + Demonstrating how to augment an existing service by updating its call + service method + ''' + + def call_service(self, *args, **kwargs): + # custom... code here + logger.info('execute service called') + + # call parent + super().call_service(*args, **kwargs) + + +class Dnos6ServiceList(IosvServiceList): + ''' + class aggregating all service lists for this platform + ''' + + def __init__(self): + # use the parent servies + super().__init__() + + # overwrite and add our own + self.execute = Execute diff --git a/src/unicon/plugins/dnos6/settings.py b/src/unicon/plugins/dnos6/settings.py new file mode 100644 index 00000000..b7f641ef --- /dev/null +++ b/src/unicon/plugins/dnos6/settings.py @@ -0,0 +1,21 @@ +''' +Author: Knox Hutchinson +Contact: https://dataknox.dev +https://twitter.com/data_knox +https://youtube.com/c/dataknox +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' + +from unicon.plugins.generic.settings import GenericSettings + + +class Dnos6Settings(GenericSettings): + + def __init__(self): + # inherit any parent settings + super().__init__() + self.CONNECTION_TIMEOUT = 60*5 + self.ESCAPE_CHAR_CALLBACK_PRE_SENDLINE_PAUSE_SEC = 1 + self.HA_INIT_EXEC_COMMANDS = [] + self.HA_INIT_CONFIG_COMMANDS = [] \ No newline at end of file diff --git a/src/unicon/plugins/dnos6/statemachine.py b/src/unicon/plugins/dnos6/statemachine.py new file mode 100644 index 00000000..9784c867 --- /dev/null +++ b/src/unicon/plugins/dnos6/statemachine.py @@ -0,0 +1,37 @@ +''' +Author: Knox Hutchinson +Contact: https://dataknox.dev +https://twitter.com/data_knox +https://youtube.com/c/dataknox +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' + +from unicon.statemachine import Path +from unicon.eal.dialogs import Dialog +from unicon.plugins.generic.statemachine import GenericSingleRpStateMachine +from . import statements as stmts + + +class Dnos6SingleRpStateMachine(GenericSingleRpStateMachine): + + def create(self): + ''' + statemachine class's create() method is its entrypoint. This showcases + how to setup a statemachine in Unicon. + ''' + super().create() + + # remove some known path + self.remove_path('enable', 'rommon') + self.remove_path('rommon', 'disable') + self.remove_state('rommon') + + self.remove_path('disable', 'enable') + enable = self.get_state('enable') + disable = self.get_state('disable') + disable_to_enable = Path(disable, + enable, + 'enable', + Dialog([stmts.password_stmt])) + self.add_path(disable_to_enable) diff --git a/src/unicon/plugins/dnos6/statements.py b/src/unicon/plugins/dnos6/statements.py new file mode 100644 index 00000000..65570544 --- /dev/null +++ b/src/unicon/plugins/dnos6/statements.py @@ -0,0 +1,44 @@ +''' +Author: Knox Hutchinson +Contact: https://dataknox.dev +https://twitter.com/data_knox +https://youtube.com/c/dataknox +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' +from unicon.eal.dialogs import Statement +from unicon.plugins.generic.statements import GenericStatements +from .patterns import Dnos6Patterns +from unicon.plugins.generic.statements import enable_password_handler + +statements = GenericStatements() +patterns = Dnos6Patterns() + +def login_handler(spawn, context, session): + spawn.sendline(context['enable_password']) + +def send_enabler(spawn, context, session): + spawn.sendline('enable') + + +def confirm_imaginary_handler(spawn): + spawn.sendline('i concur') + +# define the list of statements particular to this platform +login_stmt = Statement(pattern=patterns.login_prompt, + action=login_handler, + args=None, + loop_continue=True, + continue_timer=False) + +enable_stmt = Statement(pattern=patterns.disable_mode, + action=send_enabler, + args=None, + loop_continue=True, + continue_timer=False) + +password_stmt = Statement(pattern=patterns.password, + action=enable_password_handler, + args=None, + loop_continue=True, + continue_timer=False) diff --git a/src/unicon/plugins/eos/__init__.py b/src/unicon/plugins/eos/__init__.py new file mode 100644 index 00000000..17cbe428 --- /dev/null +++ b/src/unicon/plugins/eos/__init__.py @@ -0,0 +1,25 @@ +''' +Author: Richard Day +Contact: https://www.linkedin.com/in/richardday/, https://github.com/rich-day + +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' + +from unicon.bases.routers.connection import BaseSingleRpConnection +from unicon.plugins.generic import GenericSingleRpConnectionProvider +from .statemachine import EOSSingleRpStateMachine +from .services import EOSServiceList +from .settings import EOSSettings + +class EOSSingleRPConnection(BaseSingleRpConnection): + ''' + Support for Arista EOS platform + ''' + os = 'eos' + platform = None + chassis_type = 'single_rp' + state_machine_class = EOSSingleRpStateMachine + subcommand_list = EOSServiceList + settings = EOSSettings() + connection_provider_class = GenericSingleRpConnectionProvider diff --git a/src/unicon/plugins/eos/patterns.py b/src/unicon/plugins/eos/patterns.py new file mode 100644 index 00000000..4bfc7266 --- /dev/null +++ b/src/unicon/plugins/eos/patterns.py @@ -0,0 +1,19 @@ +''' +Author: Richard Day +Contact: https://www.linkedin.com/in/richardday/, https://github.com/rich-day + +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' + +import re +from unicon.plugins.generic.patterns import GenericPatterns + + +class EOSPatterns(GenericPatterns): + def __init__(self): + super().__init__() + self.login_prompt = r'^ *login: *?' + self.disable_mode = r'^(.*?)\w+>$' + self.privileged_mode = r'^(.*?)\w+[^\(config\)]#$' + self.password = r'Password:' \ No newline at end of file diff --git a/src/unicon/plugins/eos/services.py b/src/unicon/plugins/eos/services.py new file mode 100644 index 00000000..3cf0382f --- /dev/null +++ b/src/unicon/plugins/eos/services.py @@ -0,0 +1,39 @@ +''' +Author: Richard Day +Contact: https://www.linkedin.com/in/richardday/, https://github.com/rich-day + +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' + +import logging + +from unicon.plugins.generic.service_implementation import Execute as GenericExec +from unicon.plugins.ios.iosv import IosvServiceList + +logger = logging.getLogger(__name__) + + +class Execute(GenericExec): + ''' + Demonstrating how to augment an existing service by updating its call + service method + ''' + def call_service(self, *args, **kwargs): + # custom... code here + #logger.info('execute service called') + + # call parent + super().call_service(*args, **kwargs) + +class EOSServiceList(IosvServiceList): + ''' + class aggregating all service lists for this platform + ''' + + def __init__(self): + # use the parent servies + super().__init__() + + # overwrite and add our own + self.execute = Execute diff --git a/src/unicon/plugins/eos/settings.py b/src/unicon/plugins/eos/settings.py new file mode 100644 index 00000000..421115d2 --- /dev/null +++ b/src/unicon/plugins/eos/settings.py @@ -0,0 +1,18 @@ +''' +Author: Richard Day +Contact: https://www.linkedin.com/in/richardday/, https://github.com/rich-day + +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' + +from unicon.plugins.generic.settings import GenericSettings + +class EOSSettings(GenericSettings): + + def __init__(self): + super().__init__() + self.CONNECTION_TIMEOUT = 60*5 + self.HA_INIT_CONFIG_COMMANDS = [ + 'no logging console' + ] \ No newline at end of file diff --git a/src/unicon/plugins/eos/statemachine.py b/src/unicon/plugins/eos/statemachine.py new file mode 100644 index 00000000..e8b0d8be --- /dev/null +++ b/src/unicon/plugins/eos/statemachine.py @@ -0,0 +1,31 @@ +''' +Author: Richard Day +Contact: https://www.linkedin.com/in/richardday/, https://github.com/rich-day + +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' + +from unicon.statemachine import Path +from unicon.eal.dialogs import Dialog +from unicon.plugins.generic.statemachine import GenericSingleRpStateMachine +from . import statements as stmts + +class EOSSingleRpStateMachine(GenericSingleRpStateMachine): + + def create(self): + + super().create() + + self.remove_path('enable', 'rommon') + self.remove_path('rommon', 'disable') + self.remove_state('rommon') + + self.remove_path('disable', 'enable') + enable = [state for state in self.states if state.name == 'enable'][0] + disable = [state for state in self.states if state.name == 'disable'][0] + disable_to_enable = Path(disable, + enable, + 'enable', + Dialog([stmts.password_stmt])) + self.add_path(disable_to_enable) diff --git a/src/unicon/plugins/eos/statements.py b/src/unicon/plugins/eos/statements.py new file mode 100644 index 00000000..7f5bde23 --- /dev/null +++ b/src/unicon/plugins/eos/statements.py @@ -0,0 +1,41 @@ +''' +Author: Richard Day +Contact: https://www.linkedin.com/in/richardday/, https://github.com/rich-day + +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' + +from unicon.eal.dialogs import Statement +from unicon.plugins.generic.statements import GenericStatements +from .patterns import EOSPatterns +from unicon.bases.routers.connection import ENABLE_CRED_NAME +from unicon.plugins.generic.statements import enable_password_handler + +statements = GenericStatements() +patterns = EOSPatterns() + +def login_handler(spawn, context, session): + spawn.sendline(context['enable_password']) + +def send_enabler(spawn, context, session): + spawn.sendline('enable') + +login_stmt = Statement(pattern=patterns.login_prompt, + action=login_handler, + args=None, + loop_continue=True, + continue_timer=False) + +enable_stmt = Statement(pattern=patterns.disable_mode, + action=send_enabler, + args=None, + loop_continue=True, + continue_timer=False) + + +password_stmt = Statement(pattern=patterns.password, + action=enable_password_handler, + args=None, + loop_continue=True, + continue_timer=False) \ No newline at end of file diff --git a/src/unicon/plugins/fxos/__init__.py b/src/unicon/plugins/fxos/__init__.py index 83163861..64c230f0 100644 --- a/src/unicon/plugins/fxos/__init__.py +++ b/src/unicon/plugins/fxos/__init__.py @@ -1,8 +1,56 @@ __author__ = "dwapstra" -from .connection import FxosConnection, FxosServiceList +from unicon.plugins.generic import GenericSingleRpConnection, ServiceList +from unicon.plugins.generic.connection_provider import GenericSingleRpConnectionProvider -# import other connections so they can be found via plugin discovery -from .ftd.connection import FtdConnection +from . import service_implementation as svc +from .statemachine import FxosStateMachine +from .settings import FxosSettings +class FxosConnectionProvider(GenericSingleRpConnectionProvider): + """ + Connection provider class for fxos connections. + """ + def __init__(self, *args, **kwargs): + + """ Initializes the generic connection provider + """ + super().__init__(*args, **kwargs) + self.connection.settings.MORE_CONTINUE = 'q' + + def init_handle(self): + con = self.connection + self.execute_init_commands() + self.connection.settings.MORE_CONTINUE = ' ' + + +class FxosServiceList(ServiceList): + """ fxos services. """ + + def __init__(self): + super().__init__() + self.switchto = svc.Switchto + self.fireos = svc.FireOS + self.ftd = svc.FTD + self.fxos = svc.FXOS + self.fxos_mgmt = svc.FXOSManagement + self.expert = svc.Expert + self.sudo = svc.Sudo + self.disable = svc.Disable + self.enable = svc.Enable + self.rommon = svc.Rommon + self.reload = svc.Reload + + +class FxosConnection(GenericSingleRpConnection): + """ + Connection class for fxos connections. + """ + os = 'fxos' + platform = None + chassis_type = 'single_rp' + state_machine_class = FxosStateMachine + connection_provider_class = FxosConnectionProvider + subcommand_list = FxosServiceList + settings = FxosSettings() diff --git a/src/unicon/plugins/fxos/connection.py b/src/unicon/plugins/fxos/connection.py deleted file mode 100644 index c95f1327..00000000 --- a/src/unicon/plugins/fxos/connection.py +++ /dev/null @@ -1,36 +0,0 @@ -from unicon.plugins.generic import GenericSingleRpConnection, service_implementation as svc -from unicon.plugins.generic.connection_provider import GenericSingleRpConnectionProvider - -from unicon.plugins.generic import ServiceList, service_implementation as svc -from .statemachine import FxosStateMachine -from .settings import FxosSettings - -class FxosConnectionProvider(GenericSingleRpConnectionProvider): - """ - Connection provider class for fxos connections. - """ - - def init_handle(self): - con = self.connection - con._is_connected = True - self.execute_init_commands() - - -class FxosServiceList(ServiceList): - """ fxos services. """ - - def __init__(self): - super().__init__() - - -class FxosConnection(GenericSingleRpConnection): - """ - Connection class for fxos connections. - """ - os = 'fxos' - series = None - chassis_type = 'single_rp' - state_machine_class = FxosStateMachine - connection_provider_class = FxosConnectionProvider - subcommand_list = FxosServiceList - settings = FxosSettings() \ No newline at end of file diff --git a/src/unicon/plugins/fxos/fp4k/__init__.py b/src/unicon/plugins/fxos/fp4k/__init__.py new file mode 100644 index 00000000..dc23fe61 --- /dev/null +++ b/src/unicon/plugins/fxos/fp4k/__init__.py @@ -0,0 +1,30 @@ +__author__ = "dwapstra" + +from .. import FxosConnectionProvider +from .. import FxosConnection +from .. import FxosServiceList + +from .settings import FxosFp4kSettings +from .statemachine import FxosFp4kStateMachine +from . import service_implementation as svc + + +class FxosF4pkServiceList(FxosServiceList): + """ fxos services. """ + + def __init__(self): + super().__init__() + self.reload = svc.Reload + + +class FxosFp4kConnection(FxosConnection): + """ + Connection class for fxos/fp4k connections. + """ + os = 'fxos' + platform = 'fp4k' + chassis_type = 'single_rp' + state_machine_class = FxosFp4kStateMachine + connection_provider_class = FxosConnectionProvider + subcommand_list = FxosF4pkServiceList + settings = FxosFp4kSettings() diff --git a/src/unicon/plugins/fxos/fp4k/service_implementation.py b/src/unicon/plugins/fxos/fp4k/service_implementation.py new file mode 100644 index 00000000..df1bc877 --- /dev/null +++ b/src/unicon/plugins/fxos/fp4k/service_implementation.py @@ -0,0 +1,13 @@ + +from unicon.bases.routers.services import BaseService +from ..service_implementation import Reload as FxosReload + + +class Reload(FxosReload): + + def pre_service(self, *args, **kwargs): + self.prompt_recovery = self.connection.prompt_recovery + if 'prompt_recovery' in kwargs: + self.prompt_recovery = kwargs.get('prompt_recovery') + # switch to local-mgmt to execute the reboot command + self.connection.fxos_mgmt() diff --git a/src/unicon/plugins/fxos/fp4k/settings.py b/src/unicon/plugins/fxos/fp4k/settings.py new file mode 100644 index 00000000..5c1fb002 --- /dev/null +++ b/src/unicon/plugins/fxos/fp4k/settings.py @@ -0,0 +1,13 @@ +from ..settings import FxosSettings + + +class FxosFp4kSettings(FxosSettings): + """" FXOS/FP4k platform settings """ + + def __init__(self): + super().__init__() + + # What pattern to wait for after system restart + self.BOOT_WAIT_PATTERN = r'^.*?vdc 1 has come online' + # How many times the boot_wait_msg should occur to determine boot has finished + self.BOOT_WAIT_PATTERN_COUNT = 1 diff --git a/src/unicon/plugins/fxos/fp4k/statemachine.py b/src/unicon/plugins/fxos/fp4k/statemachine.py new file mode 100644 index 00000000..612ee933 --- /dev/null +++ b/src/unicon/plugins/fxos/fp4k/statemachine.py @@ -0,0 +1,297 @@ +from time import sleep +from unicon.statemachine import State, Path +from unicon.core.errors import StateMachineError, TimeoutError as UniconTimeoutError +from unicon.eal.dialogs import Dialog, Statement +from unicon.plugins.generic.statements import GenericStatements, update_context, chatty_term_wait, syslog_wait_send_return + +from ..statemachine import FxosStateMachine +from ..patterns import FxosPatterns +from ..statements import fxos_statements + +patterns = FxosPatterns() +generic_statements = GenericStatements() + +enable_dialog = Dialog([ + fxos_statements.enable_username_stmt, + fxos_statements.enable_password_stmt, + generic_statements.syslog_msg_stmt +]) + + +def connect_module(state_machine, spawn, context): + """ Module state change handler + + When connecting to the module, the connection can end up in different states. + This state change handler is detecting the state and perfoming additional state + transitions as needed by calling the go_to statemachine service. + """ + sm = state_machine + + spawn.sendline('connect module {} {}'.format(context.get('_module', 1), context.get('_mod_con_type', 'console'))) + sm.go_to('any', + spawn, + timeout=spawn.timeout, + context=context, + dialog=Dialog([generic_statements.escape_char_stmt]) + enable_dialog) + + if sm.current_state != 'module': + sm.go_to('module', spawn, context=context, hop_wise=True, timeout=spawn.timeout) + + # send newline so the state transition can pick up the new state + spawn.sendline() + + +def module_to_fxos_transition(statemachine, spawn, context): + if context.get('_mod_con_type') == 'telnet': + spawn.sendline('exit') + dialog = Dialog([ + Statement(pattern=patterns.no_such_command, + action='send(~)', + args=None, + loop_continue=True), + Statement(pattern=patterns.telnet_escape_prompt, + action='sendline(q)', args=None, + loop_continue=False), + ]) + statemachine.go_to('any', spawn, timeout=spawn.timeout, dialog=dialog) + spawn.sendline() + else: + try: + spawn.send('~\x17') # ~ should be sufficient, but tests show ctrl-w is needed + spawn.expect(patterns.telnet_escape_prompt, + timeout=10, + log_timeout=False) + spawn.sendline('q') + except UniconTimeoutError: + spawn.sendline('\x17') # Ctrl-W to clear the line + chatty_term_wait(spawn) + spawn.sendline('exit') + + +def ftd_to_module_transition(statemachine, spawn, context): + if context.get('console'): + spawn.sendline('exit') + else: + raise StateMachineError('Not on console, cannot transition') + + +def connect_adapter(state_machine, spawn, context): + spawn.sendline('connect adapter %s' % context.get('_adapter_module', '1/1/1')) + + +def connect_cimc(state_machine, spawn, context): + spawn.sendline('connect cimc %s' % context.get('_cimc_module', '1/1')) + + +def module_to_asa_transition(statemachine, spawn, context, to_state): + # If the module is not running ASA, connect to FTD first + # This is known if we have tried to connect before + if context.get('_module_{}_asa'.format( + context.get('_module', 1))) is False: + spawn.sendline('connect ftd') + statemachine.go_to('ftd', + spawn, + timeout=spawn.timeout, + context=context) + spawn.sendline('system support diagnostic-cli') + else: + spawn.sendline('connect asa') + chatty_term_wait(spawn) + + # Check if we ended up on the ASA or stayed on the module + # Set flag so next time we go via FTD directly + dialog = Dialog([ + Statement(pattern=patterns.asa_is_not_running, + action=update_context, + args={'_module_{}_asa'.format( + context.get('_module', 1)): False}, + loop_continue=True) + ]) + enable_dialog + statemachine.go_to(['disable', 'enable', 'config', 'module'], + spawn, + timeout=spawn.timeout, + context=context, + dialog=dialog) + + # If we stayed on the module, probably ASA is not running + # Connect via FTD + if statemachine.current_state == 'module': + spawn.sendline('connect ftd') + statemachine.go_to('ftd', + spawn, + timeout=spawn.timeout, + context=context, + dialog=enable_dialog) + spawn.sendline('system support diagnostic-cli') + statemachine.go_to(['disable', 'enable', 'config'], + spawn, + timeout=spawn.timeout, + context=context, + dialog=enable_dialog) + + # If we did not end up in the target state, go there now + if statemachine.current_state != to_state: + statemachine.go_to(to_state, + spawn, + timeout=spawn.timeout, + context=context, + dialog=enable_dialog) + + spawn.sendline() + + +def raise_ftd_not_running(): + raise StateMachineError('FTD is not running') + + +def module_to_asa_disable_transition(statemachine, spawn, context): + module_to_asa_transition(statemachine, spawn, context, 'disable') + + +def module_to_asa_config_transition(statemachine, spawn, context): + module_to_asa_transition(statemachine, spawn, context, 'config') + + +def module_to_asa_enable_transition(statemachine, spawn, context): + module_to_asa_transition(statemachine, spawn, context, 'enable') + + +def asa_to_ftd_transition(statemachine, spawn, context): + spawn.read_update_buffer() + spawn.send('\x01d') # Ctrl-A D + statemachine.go_to(['ftd', 'module'], + spawn, + timeout=spawn.timeout, + context=context) + if statemachine.current_state == 'module': + spawn.sendline('connect ftd') + dialog = Dialog([ + Statement(pattern=patterns.ftd_is_not_running, + action=raise_ftd_not_running, + args=None) + ]) + statemachine.go_to(['disable', 'enable', 'config', 'module'], + spawn, + timeout=spawn.timeout, + dialog=dialog) + else: + spawn.sendline() + + +def asa_to_module_transition(statemachine, spawn, context): + spawn.read_update_buffer() + spawn.send('\x01d') # Ctrl-A D + statemachine.go_to(['ftd', 'module'], spawn, timeout=spawn.timeout, context=context) + if statemachine.current_state == 'ftd': + ftd_to_module_transition(statemachine, spawn, context) + else: + spawn.sendline() + + +class FxosFp4kStateMachine(FxosStateMachine): + + def __init__(self, hostname=None): + super().__init__(hostname) + + def create(self): + super().create() + + enable = self.get_state('enable') + disable = self.get_state('disable') + config = self.get_state('config') + ftd = self.get_state('ftd') + fxos = self.get_state('fxos') + rommon = self.get_state('rommon') + + self.remove_path(ftd, fxos) + self.remove_path(fxos, ftd) + + self.remove_path(ftd, rommon) + + self.remove_path(enable, ftd) + self.remove_path(disable, ftd) + self.remove_path(config, ftd) + + adapter = State('adapter', patterns.adapter_prompt) + adapter_shell = State('adapter_shell', patterns.adapter_shell_prompt) + adapter_shell_fls = State('adapter_shell_fls', patterns.adapter_shell_fls) + adapter_shell_mcp = State('adapter_shell_mcp', patterns.adapter_shell_mcp) + cimc = State('cimc', patterns.cimc_prompt) + fxos_switch = State('fxos_switch', patterns.fxos_switch_prompt) + module = State('module', patterns.module_prompt) + + fxos_to_adapter = Path(fxos, adapter, connect_adapter, None) + adapter_to_adapter_shell = Path(adapter, adapter_shell, 'connect', None) + adapter_shell_to_adapter = Path(adapter_shell, adapter, 'exit', None) + adapter_shell_to_adapter_shell_fls = Path(adapter_shell, adapter_shell_fls, 'attach-fls', None) + adapter_shell_fls_to_adapter_shell = Path(adapter_shell_fls, adapter_shell, 'exit', None) + adapter_shell_to_adapter_shell_mcp = Path(adapter_shell, adapter_shell_mcp, 'attach-mcp', None) + adapter_shell_mcp_to_adapter_shell = Path(adapter_shell_mcp, adapter_shell, 'exit', None) + + adapter_to_fxos = Path(adapter, fxos, 'exit', None) + fxos_to_cimc = Path(fxos, cimc, connect_cimc, None) + cimc_to_fxos = Path(cimc, fxos, 'exit', None) + fxos_to_fxos_switch = Path(fxos, fxos_switch, 'connect fxos', None) + fxos_switch_to_fxos = Path(fxos_switch, fxos, 'exit', None) + + fxos_to_module = Path(fxos, module, connect_module, None) + module_to_fxos = Path(module, fxos, module_to_fxos_transition, None) + + module_to_ftd = Path(module, ftd, 'connect ftd', Dialog([ + Statement(patterns.ftd_console_exit, + action=update_context, + args={'console': True}, + loop_continue=True), + Statement(pattern=patterns.ftd_is_not_running, + action=raise_ftd_not_running, + args=None) + ])) + ftd_to_module = Path(ftd, module, ftd_to_module_transition, None) + + module_to_disable = Path(module, disable, module_to_asa_disable_transition, None) + disable_to_module = Path(disable, module, asa_to_module_transition, None) + module_to_enable = Path(module, enable, module_to_asa_enable_transition, None) + enable_to_module = Path(enable, module, asa_to_module_transition, None) + module_to_config = Path(module, config, module_to_asa_config_transition, None) + config_to_module = Path(config, module, asa_to_module_transition, None) + + disable_to_ftd = Path(disable, ftd, asa_to_ftd_transition, None) + enable_to_ftd = Path(enable, ftd, asa_to_ftd_transition, None) + config_to_ftd = Path(config, ftd, asa_to_ftd_transition, None) + + self.add_state(adapter) + self.add_state(cimc) + self.add_state(fxos_switch) + self.add_state(module) + self.add_state(adapter_shell) + self.add_state(adapter_shell_fls) + self.add_state(adapter_shell_mcp) + + self.add_path(fxos_to_adapter) + self.add_path(adapter_to_fxos) + self.add_path(adapter_to_adapter_shell) + self.add_path(adapter_shell_to_adapter) + self.add_path(adapter_shell_to_adapter_shell_fls) + self.add_path(adapter_shell_fls_to_adapter_shell) + self.add_path(adapter_shell_to_adapter_shell_mcp) + self.add_path(adapter_shell_mcp_to_adapter_shell) + + self.add_path(fxos_to_cimc) + self.add_path(cimc_to_fxos) + self.add_path(fxos_to_module) + self.add_path(module_to_fxos) + self.add_path(fxos_to_fxos_switch) + self.add_path(fxos_switch_to_fxos) + self.add_path(module_to_ftd) + self.add_path(ftd_to_module) + + self.add_path(module_to_disable) + self.add_path(disable_to_module) + self.add_path(module_to_enable) + self.add_path(enable_to_module) + self.add_path(module_to_config) + self.add_path(config_to_module) + + self.add_path(disable_to_ftd) + self.add_path(enable_to_ftd) + self.add_path(config_to_ftd) diff --git a/src/unicon/plugins/fxos/fp9k/__init__.py b/src/unicon/plugins/fxos/fp9k/__init__.py new file mode 100644 index 00000000..30a03db0 --- /dev/null +++ b/src/unicon/plugins/fxos/fp9k/__init__.py @@ -0,0 +1,21 @@ +__author__ = "dwapstra" + +from .. import FxosConnectionProvider +from .. import FxosConnection +from .. import FxosSettings +from .. import FxosServiceList + +from ..fp4k.statemachine import FxosFp4kStateMachine + + +class FxosFp9kConnection(FxosConnection): + """ + Connection class for fxos/fp9k connections. + """ + os = 'fxos' + platform = 'fp9k' + chassis_type = 'single_rp' + state_machine_class = FxosFp4kStateMachine + connection_provider_class = FxosConnectionProvider + subcommand_list = FxosServiceList + settings = FxosSettings() diff --git a/src/unicon/plugins/fxos/ftd/connection.py b/src/unicon/plugins/fxos/ftd/connection.py index bdf5d8fd..ea2195ee 100644 --- a/src/unicon/plugins/fxos/ftd/connection.py +++ b/src/unicon/plugins/fxos/ftd/connection.py @@ -1,9 +1,9 @@ +import warnings from time import sleep from unicon.plugins.generic import GenericSingleRpConnection, ServiceList -from unicon.plugins.generic import service_implementation as svc -from unicon.eal.dialogs import Dialog, Statement +from unicon.eal.dialogs import Dialog -from ..connection import FxosConnectionProvider +from .. import FxosConnectionProvider from . import service_implementation as ftd_svc from .statemachine import FtdStateMachine from .statements import FtdStatements @@ -17,6 +17,17 @@ class FtdConnectionProvider(FxosConnectionProvider): """ Connection provider class for fxos connections. """ + + def __init__(self, *args, **kwargs): + """ Initializes the connection provider + """ + warnings.warn("This plugin fxos/ftd is deprecated, it has been" + "replaced by fxos, fxos/fp4k and fxos/fp9k." + "Please update your topology file.", + DeprecationWarning) + + super().__init__(*args, **kwargs) + def get_connection_dialog(self): dialog = Dialog([ftd_statements.cssp_stmt, ftd_statements.command_not_completed_stmt]) @@ -25,7 +36,6 @@ def get_connection_dialog(self): def init_handle(self): con = self.connection - con._is_connected = True self.execute_init_commands() def disconnect(self): @@ -45,7 +55,6 @@ def disconnect(self): con.expect('.*') con.log.info('Closing connection...') con.spawn.close() - self.connection._is_connected = False class FtdServiceList(ServiceList): @@ -61,7 +70,7 @@ class FtdConnection(GenericSingleRpConnection): Connection class for fxos connections. """ os = 'fxos' - series = 'ftd' + platform = 'ftd' chassis_type = 'single_rp' state_machine_class = FtdStateMachine connection_provider_class = FtdConnectionProvider diff --git a/src/unicon/plugins/fxos/ftd/patterns.py b/src/unicon/plugins/fxos/ftd/patterns.py index 64c6a019..242f62e0 100644 --- a/src/unicon/plugins/fxos/ftd/patterns.py +++ b/src/unicon/plugins/fxos/ftd/patterns.py @@ -5,7 +5,7 @@ class FtdPatterns(GenericPatterns): def __init__(self): super().__init__() - self.chassis_prompt = r'^(.*?)[-\.\w]+#\s*$' + self.chassis_prompt = r'^(.*?)[-\.\w]+(\*\s)?#\s*$' self.chassis_scope_prompt = r'^(.*?)[-\.\w]+(\s+(/[-\w]+)+)\*?\s?#\s*$' self.fxos_prompt = r'^(.*?)[-\.\w]+\s?\(fxos\)#\s*$' self.local_mgmt_prompt = r'^(.*?)[-\.\w]+\(local-mgmt\)#\s*$' @@ -20,4 +20,6 @@ def __init__(self): self.cssp_pattern = r'^.*? +Type \? for list of commands' self.sudo_incorrect_password_pattern = r'^.*?sudo: \d+ incorrect password attempts' - self.command_not_completed = r'Syntax error: The command is not completed' \ No newline at end of file + self.command_not_completed = r'Syntax error: The command is not completed' + + self.are_you_sure = r'(.*?)Are you sure.*?\(yes\/no\)\s*$' \ No newline at end of file diff --git a/src/unicon/plugins/fxos/ftd/service_implementation.py b/src/unicon/plugins/fxos/ftd/service_implementation.py index 996bc6ec..fc010275 100644 --- a/src/unicon/plugins/fxos/ftd/service_implementation.py +++ b/src/unicon/plugins/fxos/ftd/service_implementation.py @@ -25,7 +25,6 @@ class Switchto(BaseService): def __init__(self, connection, context, **kwargs): # Connection object will have all the received details super().__init__(connection, context, **kwargs) - self.service_name = 'switchto' self.timeout = connection.settings.EXEC_TIMEOUT self.context = context @@ -62,9 +61,9 @@ def call_service(self, target, raise Exception('Invalid switchto target type: %s' % repr(target)) for target_state in target_list: - m1 = re.match('module\s+(\d+)\s+console', target_state) - m2 = re.match('cimc\s+(\S+)', target_state) - m3 = re.match('chassis scope (.*)', target_state) + m1 = re.match(r'module\s+(\d+)\s+console', target_state) + m2 = re.match(r'cimc\s+(\S+)', target_state) + m3 = re.match(r'chassis scope (.*)', target_state) if m1: mod = m1.group(1) self.context._module = mod diff --git a/src/unicon/plugins/fxos/ftd/statemachine.py b/src/unicon/plugins/fxos/ftd/statemachine.py index 14be141d..41a97b66 100644 --- a/src/unicon/plugins/fxos/ftd/statemachine.py +++ b/src/unicon/plugins/fxos/ftd/statemachine.py @@ -7,6 +7,7 @@ from unicon.core.errors import SubCommandFailure, StateMachineError from unicon.statemachine import State, Path, StateMachine from unicon.eal.dialogs import Dialog, Statement +from unicon.utils import to_plaintext from unicon.plugins.generic.statements import GenericStatements from unicon.plugins.generic.patterns import GenericPatterns @@ -36,7 +37,7 @@ def connect_module_console(state_machine, spawn, context): dialog = Dialog([escape_char_stmt]) dialog += Dialog([Statement(state.pattern, loop_continue=False) for state in sm.states]) spawn.sendline('connect module %s console' % context.get('_module', 1)) - sm.go_to('any', spawn, dialog=Dialog([escape_char_stmt])) + sm.go_to('any', spawn, timeout=spawn.timeout, dialog=Dialog([escape_char_stmt])) if sm.current_state != 'module_console': sm.go_to('module_console', spawn, @@ -73,7 +74,8 @@ def sudo_password_handler(spawn, context): credentials = context.get('credentials') if credentials: try: - spawn.sendline(credentials[SUDO_CRED_NAME]['password']) + spawn.sendline( + to_plaintext(credentials[SUDO_CRED_NAME]['password'])) except KeyError as exc: raise UniconAuthenticationError("No password has been defined " "for credential '{}'.".format(SUDO_CRED_NAME)) diff --git a/src/unicon/plugins/fxos/ftd/statements.py b/src/unicon/plugins/fxos/ftd/statements.py index 08a0c7bd..af080626 100644 --- a/src/unicon/plugins/fxos/ftd/statements.py +++ b/src/unicon/plugins/fxos/ftd/statements.py @@ -37,3 +37,9 @@ def __init__(self): args=None, loop_continue=True, continue_timer=False) + + self.are_you_sure_stmt = Statement(patterns.are_you_sure, + action='sendline(y)', + args=None, + loop_continue=True, + continue_timer=False) \ No newline at end of file diff --git a/src/unicon/plugins/fxos/patterns.py b/src/unicon/plugins/fxos/patterns.py index f8d55a71..8519f936 100644 --- a/src/unicon/plugins/fxos/patterns.py +++ b/src/unicon/plugins/fxos/patterns.py @@ -2,7 +2,63 @@ from unicon.plugins.generic.patterns import GenericPatterns + class FxosPatterns(GenericPatterns): def __init__(self): super().__init__() - self.shell_prompt = r'^(.*?([>\$~%]|(/[-\w]+)*\*?[^#]+#))\s?$' + self.disable_prompt = r'^(.*?)([-\.\w]+)(/\S+)*>\s*$' + self.enable_prompt = r'^(.*?)([-\.\w]+)(/\S+)*#\s*$' + self.config_prompt = r'^(.*?)\(\S*(con|cfg|ipsec-profile).*\)#\s?$' + self.username = r'^(\S+ login:|Username:)\s*$' + + self.ftd_prompt = r'^(.*?)\n>\s*$' + self.ftd_expert_prompt = r'^(.*?)[-\.\w]+@[-\.\w]+:[~/\w]+\s?\$\s*$' + self.ftd_expert_root_prompt = r'^(.*?)[-\.\w]+@[-\.\w]+:[~/\w]+\s?#\s*$' + self.ftd_reboot_confirm = r"Please enter 'YES' or 'NO':\s*$" + + self.fxos_prompt = r'^(.*?)[-\.\w]+(\*\s)?#\s*$' + self.fxos_scope_prompt = r'^(.*?)[-\.\w]+(\s+(/[-\w]+)+)\*?\s?#\s*$' + self.fxos_local_mgmt_prompt = r'^(.*?)[-\.\w]+\(local-mgmt\)#\s*$' + self.fxos_mgmt_reboot_confirm = r'Do you still want to reboot\? \(yes/no\):\s*$' + self.fxos_switch_prompt = r'^(.*?)[-\.\w]+\s?\(fxos\)#\s*$' + + self.boot_interrupt = r'(Use BREAK or ESC to interrupt boot|Use BREAK, ESC or CTRL\+L to interrupt boot)' + self.rommon_prompt = r'^(.*?)rommon.*>\s*$' + + self.adapter_prompt = r'^(.*?)adapter \S+ #\s*$' + self.adapter_shell_prompt = r'^(.*?)adapter \S+ \(top\):\d+#\s*$' + self.adapter_shell_fls = r'^(.*?)adapter \S+ \(fls\):\d+#\s*$' + self.adapter_shell_mcp = r'^(.*?)adapter \S+ \(mcp\):\d+#\s*$' + + self.cimc_prompt = r'^(.*?)\[\s+[-\w]+\s+\]#\s*$' + self.module_prompt = r'^(.*?)Firepower-module\d+>\s*$' + + self.cssp_pattern = r'^.*? +Type \? for list of commands' + self.sudo_incorrect_password_pattern = r'^.*?sudo: \d+ incorrect password attempts' + + self.bell_pattern = r'^.*\x07$' + self.command_not_completed = r'Syntax error: The command is not completed' + + # show version glean patterns + self.fxos_glean_pattern = r'\s*Version: ' + self.asa_glean_pattern = r'Cisco Adaptive Security Appliance Software' + + self.you_came_from_fxos = r"You came from FXOS Service Manager. Please enter 'exit' to go back." + + self.config_call_home_prompt = r'the product\? \[Y\]es, \[N\]o, \[A\]sk later:\s*$' + + self.restarting_system = r'Restarting system' + self.system_going_down = r'The system is going down for reboot NOW' + self.reboot_requested = r'Reboot requested by the user' + self.boot_wait_msg = r'^.*?port-manager: Alert: (.*?) link changed to UP' + + self.type_exit = r"Type (exit) or Ctrl-] followed by . to quit." + self.escape_character = r"Escape character is '(~)'" + + self.ftd_console_exit = r'Connecting to ftd\(ftd\) console... enter exit to return to bootCLI' + + self.asa_is_not_running = r'asa is not running' + self.ftd_is_not_running = r'ftd is not running' + + self.no_such_command = r'No such command' + self.telnet_escape_prompt = r'telnet>\s?$' diff --git a/src/unicon/plugins/fxos/service_implementation.py b/src/unicon/plugins/fxos/service_implementation.py new file mode 100644 index 00000000..75a2522d --- /dev/null +++ b/src/unicon/plugins/fxos/service_implementation.py @@ -0,0 +1,321 @@ +__author__ = "dwapstra" + +import io +import re +import time +import logging + +from unicon.bases.routers.services import BaseService +from unicon.core.errors import SubCommandFailure +from unicon.eal.dialogs import Dialog, Statement +from unicon.logs import UniconStreamHandler, UNICON_LOG_FORMAT + +from unicon.plugins.generic.service_implementation import ( + Execute, Switchto as GenericSwitchto +) +from unicon.plugins.generic.statements import GenericStatements +from unicon.plugins.generic import GenericUtils + +from .statements import ( + FxosStatements, reload_statements, + login_statements, boot_wait) +from .patterns import FxosPatterns + +utils = GenericUtils() +fxos_statements = FxosStatements() +fxos_patterns = FxosPatterns() +generic_statements = GenericStatements() + + +class Switchto(GenericSwitchto): + """ Switch to a certain CLI state + """ + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + + def call_service(self, to_state, + timeout=None, + *args, **kwargs): + + if not self.connection.connected: + return + + con = self.connection + sm = self.get_sm() + + dialog = Dialog([fxos_statements.command_not_completed_stmt]) + + timeout = timeout if timeout is not None else self.timeout + + if isinstance(to_state, str): + to_state_list = [to_state] + elif isinstance(to_state, list): + to_state_list = to_state + else: + raise Exception('Invalid switchto to_state type: %s' % repr(to_state)) + + for to_state in to_state_list: + m1 = re.match(r'module(\s+(\d+))?(\s+(console|telnet))?', to_state) + m2 = re.match(r'cimc(\s+(\S+))?', to_state) + m3 = re.match(r'fxos[ _]scope ?(.*)', to_state) + m4 = re.match(r'adapter(\s+(\S+))?', to_state) + if m1: + mod = m1.group(2) or 1 + con_type = m1.group(4) or 'console' + self.context._module = mod + self.context._mod_con_type = con_type + to_state = 'module' + elif m2: + mod = m2.group(2) or '1/1' + self.context._cimc_module = mod + to_state = 'cimc' + con.state_machine.go_to('fxos', con.spawn, + context=self.context, + hop_wise=True, + timeout=timeout) + elif m3: + scope = m3.group(1) + if not scope: + con.log.warning('No scope specified, ignoring switchto') + continue + else: + self.context._scope = scope + to_state = 'fxos_scope' + con.state_machine.go_to('fxos', con.spawn, + context=self.context, + hop_wise=True, + timeout=timeout) + elif m4: + mod = m4.group(2) or '1/1' + self.context._adapter_module = mod + to_state = 'adapter' + else: + to_state = to_state.replace(' ', '_') + + valid_states = [x.name for x in sm.states] + if to_state not in valid_states: + con.log.warning( + '%s is not a valid state, ignoring switchto' % to_state) + continue + + con.state_machine.go_to(to_state, + con.spawn, + context=self.context, + hop_wise=True, + timeout=timeout, + dialog=dialog) + + self.end_state = sm.current_state + + +class FxosExecute(Execute): + def log_service_call(self): + self.connection.log.info('+++ {}: {} +++'.format( + self.connection.hostname, self.service_name)) + + +class FTD(FxosExecute): + """ Brings device to the FTD Prompt and execute any commands specified + """ + def __init__(self, connection, context, **kwargs): + # Connection object will have all the received details + super().__init__(connection, context, **kwargs) + self.start_state = 'ftd' + self.end_state = 'ftd' + self.service_name = 'ftd' + self.timeout = 120 + self.__dict__.update(kwargs) + + +class FireOS(FTD): + def call_service(self, *args, **kwargs): + self.connection.log.warning('**** "fireos" service is deprecated. ' + + 'Please use "ftd" service ****') + super().call_service(*args, **kwargs) + + +class FXOS(FxosExecute): + """ Brings device to the FXOS Prompt and execute any commands specified + """ + def __init__(self, connection, context, **kwargs): + # Connection object will have all the received details + super().__init__(connection, context, **kwargs) + self.start_state = 'fxos' + self.end_state = 'fxos' + self.service_name = 'fxos' + self.timeout = 120 + self.__dict__.update(kwargs) + + +class Expert(FxosExecute): + """ Brings device to the FireOS Expert Prompt and execute any commands specified + """ + def __init__(self, connection, context, **kwargs): + # Connection object will have all the received details + super().__init__(connection, context, **kwargs) + self.start_state = 'expert' + self.end_state = 'expert' + self.service_name = 'expert' + self.timeout = 60 + self.__dict__.update(kwargs) + + +class Sudo(FxosExecute): + """ Brings device to the FireOS Sudo Prompt and execute any commands specified + """ + def __init__(self, connection, context, **kwargs): + # Connection object will have all the received details + super().__init__(connection, context, **kwargs) + self.start_state = 'sudo' + self.end_state = 'sudo' + self.service_name = 'sudo' + self.timeout = 60 + self.__dict__.update(kwargs) + + +class Disable(FxosExecute): + """ Brings device to the Lina Disable prompt and executes command specified + """ + def __init__(self, connection, context, **kwargs): + # Connection object will have all the received details + super().__init__(connection, context, **kwargs) + self.start_state = 'disable' + self.end_state = 'disable' + self.service_name = 'disable' + self.timeout = 60 + self.__dict__.update(kwargs) + + +class Enable(FxosExecute): + """ Brings device to the Lina Enable prompt and executes commands specified + """ + def __init__(self, connection, context, **kwargs): + # Connection object will have all the received details + super().__init__(connection, context, **kwargs) + self.start_state = 'enable' + self.end_state = 'enable' + self.service_name = 'enable' + self.timeout = 60 + self.__dict__.update(kwargs) + + +class Rommon(FxosExecute): + """ Brings device to the Rommon prompt and executes commands specified + """ + def __init__(self, connection, context, **kwargs): + # Connection object will have all the received details + super().__init__(connection, context, **kwargs) + self.start_state = 'rommon' + self.end_state = 'rommon' + self.service_name = 'rommon' + self.timeout = 600 + self.__dict__.update(kwargs) + + +class FXOSManagement(FxosExecute): + """ Brings device to the FXOS mgmt prompt and executes commands specified + """ + def __init__(self, connection, context, **kwargs): + # Connection object will have all the received details + super().__init__(connection, context, **kwargs) + self.start_state = 'fxos_mgmt' + self.end_state = 'fxos_mgmt' + self.service_name = 'fxos_mgmt' + self.timeout = 60 + self.__dict__.update(kwargs) + + +class Reload(BaseService): + def __init__(self, connection, context, **kwargs): + # Connection object will have all the received details + super().__init__(connection, context, **kwargs) + self.start_state = 'fxos' # start in fxos and switch to ftd in pre_service for console detection and reboot + self.end_state = 'fxos' + self.service_name = 'reload' + self.timeout = self.connection.settings.BOOT_TIMEOUT + self.log_buffer = io.StringIO() + lb = UniconStreamHandler(self.log_buffer) + lb.setFormatter(logging.Formatter(fmt=UNICON_LOG_FORMAT)) + self.connection.log.addHandler(lb) + self.dialog = Dialog(reload_statements) + self.__dict__.update(kwargs) + + def pre_service(self, *args, **kwargs): + super().pre_service(*args, **kwargs) + # Force switch to ftd so we can detect if we are on console or not and execute the reboot command + self.connection.ftd() + + def call_service(self, reload_command='reboot', reply=Dialog([]), timeout=None, *args, **kwargs): # noqa C901 + # Clear log buffer + self.log_buffer.seek(0) + self.log_buffer.truncate() + + con = self.connection + timeout = timeout or self.timeout + con.log.debug("+++ reloading %s with reload_command %s " + "and timeout is %s +++" % (self.connection.hostname, reload_command, timeout)) + + console = con.context.get('console', False) + + if console: + dialog = reply + self.dialog + con.spawn.sendline(reload_command) + try: + con.log.info('Rebooting system..') + # reload and wait until 'Restarting system' is seen + self.result = dialog.process(con.spawn, + timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=self.context) + + con.log.info('Waiting for boot to finish..') + # Wait until boot is done + boot_wait(con.spawn, timeout=timeout or self.timeout) + + con.log.info('Reload done, waiting %s seconds' % con.settings.POST_RELOAD_WAIT) + time.sleep(con.settings.POST_RELOAD_WAIT) + + dialog = Dialog(login_statements + [Statement(fxos_patterns.fxos_prompt)]) + + con.log.info('Trying to login..') + # try to login + con.spawn.sendline() + self.result = dialog.process(con.spawn, + timeout=timeout or self.timeout, + prompt_recovery=self.prompt_recovery, + context=self.context) + + con.state_machine.detect_state(con.spawn) + except Exception as err: + raise SubCommandFailure("Reload failed %s" % err) + else: + con.log.debug('Did not detect a console session, will try to reconnect...') + dialog = reply + self.dialog + con.spawn.sendline(reload_command) + self.result = dialog.process(con.spawn, + timeout=timeout or self.timeout, + prompt_recovery=self.prompt_recovery, + context=self.context) + try: + con.spawn.expect('.+', timeout=10, log_timeout=False) + except TimeoutError: + pass + con.log.info('Disconnecting...') + con.disconnect() + for x in range(con.settings.RELOAD_RECONNECT_ATTEMPTS): + con.log.info('Waiting for {} seconds'.format(con.settings.RELOAD_WAIT)) + time.sleep(con.settings.RELOAD_WAIT) + con.log.info('Trying to connect... attempt #{}'.format(x + 1)) + try: + con.connect() + except Exception: + con.log.warning('Connection failed') + if con.is_connected: + break + + if not con.is_connected: + raise SubCommandFailure('Reload failed - could not reconnect') + + self.log_buffer.seek(0) + self.result = self.log_buffer.read() diff --git a/src/unicon/plugins/fxos/settings.py b/src/unicon/plugins/fxos/settings.py index 5109e924..a058504b 100644 --- a/src/unicon/plugins/fxos/settings.py +++ b/src/unicon/plugins/fxos/settings.py @@ -17,5 +17,24 @@ def __init__(self): self.TERM = 'vt100' self.ERROR_PATTERN = [ - r'^Error:' + r'ERROR: % Invalid input detected', + r'^Error:', + r'^%\s*[Ii]nvalid [Cc]ommand', + r"^%\s*Ambiguous command at '\^' marker", + r'^.*\x07' ] + + # Increasing the expect timeout since its used for go_to state transitions + # The transition to diagnostic CLI take take 10+ seconds due to in-use session + self.EXPECT_TIMEOUT = 15 + + self.RELOAD_WAIT = 420 + self.RELOAD_RECONNECT_ATTEMPTS = 3 + self.POST_RELOAD_WAIT = 60 + + self.BOOT_TIMEOUT = 600 + + # What pattern to wait for after system restart + self.BOOT_WAIT_PATTERN = r'^.*User enable_1 logged in to' + # How many times the boot_wait_msg should occur to determine boot has finished + self.BOOT_WAIT_PATTERN_COUNT = 1 diff --git a/src/unicon/plugins/fxos/statemachine.py b/src/unicon/plugins/fxos/statemachine.py index b7eabe87..1b027792 100644 --- a/src/unicon/plugins/fxos/statemachine.py +++ b/src/unicon/plugins/fxos/statemachine.py @@ -1,24 +1,287 @@ -""" State machine for Fxos """ +""" State machine for FXOS """ __author__ = "dwapstra" +import re -from unicon.plugins.generic.statements import GenericStatements -from unicon.core.errors import SubCommandFailure, StateMachineError from unicon.statemachine import State, Path, StateMachine from unicon.eal.dialogs import Dialog, Statement +from unicon.core.errors import StateMachineError +from unicon.utils import AttributeDict + +from unicon.plugins.generic.statements import GenericStatements, sudo_password_handler, update_context +from unicon.plugins.generic.patterns import GenericPatterns from .patterns import FxosPatterns +from .statements import fxos_statements, default_statement_list, boot_wait, boot_to_rommon_statements patterns = FxosPatterns() -statements = GenericStatements() +generic_statements = GenericStatements() +generic_patterns = GenericPatterns() + + +def change_fxos_scope(state_machine, spawn, context): + scopes = [s for s in context.get('_scope', "").split('/') if s] + for scope in scopes: + spawn.sendline("scope %s" % scope) + spawn.expect(state_machine.get_state('fxos_scope').pattern, trim_buffer=False) + + +def sudo_failed(): + raise Exception('sudo failed') + + +def send_ctrl_a_d(state_machine, spawn, context): + spawn.read_update_buffer() + ctrl_a_d = '\x01d' + spawn.send(ctrl_a_d) + + +def ftd_fxos_transition(statemachine, spawn, context): + sm = statemachine + console = context.get('console', False) + if console: + spawn.sendline('exit') + else: + spawn.sendline('connect fxos') + spawn.expect('.+', trim_buffer=False) + sm.go_to(['ftd', 'fxos'], spawn, context=context, timeout=spawn.timeout) + if sm.current_state == 'ftd': + spawn.sendline('exit') + context.update({'console': True}) + else: + spawn.sendline() + + +def fxos_ftd_transition(statemachine, spawn, context): + sm = statemachine + console = context.get('console', False) + if console: + spawn.sendline('connect ftd') + else: + dialog = Dialog([ + generic_statements.login_stmt, generic_statements.password_stmt, + Statement(pattern=patterns.you_came_from_fxos, + action=update_context, + args={'console': True}, + loop_continue=True) + ]) + spawn.sendline('exit') + # Wait a bit using expect, login prompt could appear + spawn.expect('.+', trim_buffer=False) + sm.go_to(['ftd', 'disable', 'fxos'], spawn, context=context, timeout=spawn.timeout, dialog=dialog) + if sm.current_state == 'fxos': + spawn.sendline('connect ftd') + context.update({'console': True}) + else: + spawn.sendline() + + +def ftd_to_multi_transition(statemachine, spawn, context): + spawn.sendline('system support diagnostic-cli') + spawn.sendline() + statemachine.go_to(['disable', 'enable', 'config'], spawn, timeout=spawn.timeout) + + +def ftd_to_disable_transition(statemachine, spawn, context): + ftd_to_multi_transition(statemachine, spawn, context) + dialog = Dialog([fxos_statements.enable_password_stmt]) + statemachine.go_to('disable', spawn, timeout=spawn.timeout, context=context, dialog=dialog) + spawn.sendline() + + +def ftd_to_enable_transition(statemachine, spawn, context): + ftd_to_multi_transition(statemachine, spawn, context) + dialog = Dialog([fxos_statements.enable_password_stmt]) + statemachine.go_to('enable', spawn, timeout=spawn.timeout, context=context, dialog=dialog) + spawn.sendline() + + +def ftd_to_config_transition(statemachine, spawn, context): + ftd_to_multi_transition(statemachine, spawn, context) + dialog = Dialog([fxos_statements.enable_password_stmt]) + statemachine.go_to('config', spawn, timeout=spawn.timeout, context=context, dialog=dialog) + spawn.sendline() + + +def boot_fxos(statemachine, spawn, context): + spawn.sendline('boot') + + boot_wait(spawn, timeout=spawn.settings.BOOT_TIMEOUT) + + spawn.sendline() + dialog = Dialog([generic_statements.login_stmt, generic_statements.password_stmt, + Statement(patterns.fxos_prompt)]) + dialog.process(spawn, context=context) + spawn.sendline() class FxosStateMachine(StateMachine): + STATE_GLEAN = AttributeDict({ + 'fxos': AttributeDict(dict( + command='show version | inc Version', + pattern=patterns.fxos_glean_pattern)), + 'enable': AttributeDict(dict( + command='show version | inc Version', + pattern=patterns.asa_glean_pattern)) + }) + def __init__(self, hostname=None): super().__init__(hostname) def create(self): - shell = State('shell', patterns.shell_prompt) - self.add_state(shell) + ftd = State('ftd', patterns.ftd_prompt) + ftd_expert = State('expert', patterns.ftd_expert_prompt) + ftd_expert_root = State('sudo', patterns.ftd_expert_root_prompt) + fxos = State('fxos', patterns.fxos_prompt) + fxos_scope = State('fxos_scope', patterns.fxos_scope_prompt) + fxos_local_mgmt = State('fxos_mgmt', patterns.fxos_local_mgmt_prompt) + enable = State('enable', patterns.enable_prompt) + disable = State('disable', patterns.disable_prompt) + config = State('config', patterns.config_prompt) + rommon = State('rommon', patterns.rommon_prompt) + + ftd_to_ftd_expert = Path(ftd, ftd_expert, 'expert', None) + ftd_expert_to_ftd = Path(ftd_expert, ftd, 'exit', None) + + ftd_expert_to_ftd_expert_root = Path( + ftd_expert, ftd_expert_root, 'sudo su -', + Dialog([ + Statement(generic_patterns.password, sudo_password_handler, None, True, False), + Statement(patterns.sudo_incorrect_password_pattern, sudo_failed) + ])) + ftd_expert_root_to_ftd_expert = Path(ftd_expert_root, ftd_expert, 'exit', None) + + enable_to_disable = Path(enable, disable, 'disable', None) + enable_to_config = Path(enable, config, 'config term', Dialog([fxos_statements.config_call_home_stmt])) + + disable_to_enable = Path(disable, enable, 'enable', Dialog([ + fxos_statements.enable_username_stmt, + fxos_statements.enable_password_stmt + ])) + + config_to_enable = Path(config, enable, 'end', None) + + ftd_to_fxos = Path(ftd, fxos, ftd_fxos_transition, None) + fxos_to_ftd = Path(fxos, ftd, fxos_ftd_transition, None) + fxos_scope_to_fxos = Path(fxos_scope, fxos, 'top', None) + fxos_to_fxos_scope = Path(fxos, fxos_scope, change_fxos_scope, None) + + ftd_to_disable = Path(ftd, disable, ftd_to_disable_transition, Dialog([fxos_statements.enable_password_stmt])) + + ftd_to_enable = Path(ftd, enable, ftd_to_enable_transition, Dialog([fxos_statements.enable_password_stmt])) + + ftd_to_config = Path(ftd, config, ftd_to_config_transition, Dialog([fxos_statements.enable_password_stmt])) + + disable_to_ftd = Path(disable, ftd, send_ctrl_a_d, None) + enable_to_ftd = Path(enable, ftd, send_ctrl_a_d, None) + config_to_ftd = Path(config, ftd, send_ctrl_a_d, None) + + fxos_to_local_mgmt = Path(fxos, fxos_local_mgmt, 'connect local-mgmt', None) + local_mgmt_to_fxos = Path(fxos_local_mgmt, fxos, 'exit', None) + + local_mgmt_to_rommon = Path(fxos_local_mgmt, rommon, 'reboot', Dialog(boot_to_rommon_statements)) + ftd_to_rommon = Path(ftd, rommon, 'reboot', Dialog(boot_to_rommon_statements)) + + rommon_to_fxos = Path(rommon, fxos, boot_fxos, None) + + self.add_state(enable) + self.add_state(disable) + self.add_state(config) + self.add_state(ftd) + self.add_state(ftd_expert) + self.add_state(ftd_expert_root) + self.add_state(fxos) + self.add_state(fxos_scope) + self.add_state(fxos_local_mgmt) + self.add_state(rommon) + + self.add_path(enable_to_disable) + self.add_path(enable_to_config) + self.add_path(config_to_enable) + self.add_path(disable_to_enable) + self.add_path(ftd_to_ftd_expert) + self.add_path(ftd_expert_to_ftd) + self.add_path(ftd_expert_to_ftd_expert_root) + self.add_path(ftd_expert_root_to_ftd_expert) + self.add_path(ftd_to_fxos) + self.add_path(fxos_to_ftd) + self.add_path(fxos_to_fxos_scope) + self.add_path(fxos_scope_to_fxos) + self.add_path(ftd_to_enable) + self.add_path(enable_to_ftd) + self.add_path(ftd_to_disable) + self.add_path(ftd_to_config) + self.add_path(disable_to_ftd) + self.add_path(config_to_ftd) + self.add_path(fxos_to_local_mgmt) + self.add_path(local_mgmt_to_fxos) + self.add_path(local_mgmt_to_rommon) + self.add_path(ftd_to_rommon) + self.add_path(rommon_to_fxos) + + self.add_default_statements(default_statement_list) + + def detect_state(self, spawn, context=AttributeDict()): + """ Detect the device state and glean the actual state if multiple matches are found. + """ + state_matches = [] + result = spawn.match + if result: + prompt = result.match_output.splitlines()[-1] + for state in self.states: + if re.search(state.pattern, prompt): + state_matches.append(state) + + spawn.log.debug('statemachine detected state(s): {}'.format(state_matches)) + if len(state_matches) > 1: + # If the current state is in the detected states, assume we can keep the same state + # If not, try to glean the actual state + if self.current_state not in [s.name for s in state_matches]: + self.glean_state(spawn, state_matches) + elif len(state_matches) == 1: + self.update_cur_state(state_matches[0].name) + else: + spawn.sendline() + super().go_to('any', spawn, context) + + def glean_state(self, spawn, possible_states): + """ Try to figure out the state by sending commands and verifying the matches against known output. + """ + # Create list of commands to execute + glean_command_map = {} + state_patterns = [] + for state in possible_states: + state_patterns.append(state.pattern) + glean_data = self.STATE_GLEAN.get(state.name, None) + if glean_data: + if glean_data.command in glean_command_map: + glean_command_map[glean_data.command][glean_data.pattern] = state + else: + glean_command_map[glean_data.command] = {} + glean_command_map[glean_data.command][glean_data.pattern] = state + + if not glean_command_map: + raise StateMachineError('Unable to detect state, multiple states possible and no glean data available') + + # Execute each glean commnd and check for pattern match + for glean_cmd in glean_command_map: + glean_pattern_map = glean_command_map[glean_cmd] + dialog = Dialog(default_statement_list + [Statement(p) for p in state_patterns]) + + spawn.sendline(glean_cmd) + result = dialog.process(spawn) + if result: + output = result.match_output + for glean_pattern in glean_pattern_map: + if re.search(glean_pattern, output): + self.update_cur_state(glean_pattern_map[glean_pattern]) + return + + def go_to(self, to_state, spawn, **kwargs): + spawn.log.debug('statemachine goto: {} -> {}'.format(self.current_state, to_state)) + super().go_to(to_state, spawn, **kwargs) + if to_state == 'any' and self.current_state in self.STATE_GLEAN: + glean_states = [self.get_state(name) for name in self.STATE_GLEAN] + self.glean_state(spawn, glean_states) diff --git a/src/unicon/plugins/fxos/statements.py b/src/unicon/plugins/fxos/statements.py new file mode 100644 index 00000000..93c9c893 --- /dev/null +++ b/src/unicon/plugins/fxos/statements.py @@ -0,0 +1,157 @@ +__author__ = "dwapstra" + +import re + +from unicon.eal.dialogs import Statement, Dialog +from unicon.plugins.generic.statements import generic_statements, chatty_term_wait +from unicon.plugins.generic.service_statements import connection_closed + +from unicon.utils import to_plaintext +from unicon.core.errors import UniconAuthenticationError + +from .patterns import FxosPatterns + +patterns = FxosPatterns() + + +def flag_ssh_session(spawn, context, session): + context._ssh_session = True + spawn.log.info('SSH session detected') + + +def clear_command_line(spawn, context, session): + """ Clear the command line by sending Ctr-A Ctrl-K """ + CTRL_A = '\x01' + CTRL_K = '\x0b' + spawn.sendline("%s%s\r" % (CTRL_A, CTRL_K)) + + +def enable_username_handler(spawn, context, session): + credentials = context.get('credentials') + enable_username = to_plaintext(credentials.get('enable', {}).get('username', '')) + spawn.sendline(enable_username) + + +# Overriding the generic enable password handler, since the password for ASA can be empty +def enable_password_handler(spawn, context, session): + if 'password_attempts' not in session: + session['password_attempts'] = 1 + else: + session['password_attempts'] += 1 + if session.password_attempts > spawn.settings.PASSWORD_ATTEMPTS: + raise UniconAuthenticationError('Too many enable password retries') + + credentials = context.get('credentials') + enable_password = to_plaintext(credentials.get('enable', {}).get('password', '')) + spawn.sendline(enable_password) + + +def boot_wait(spawn, timeout=600): + def count(spawn, context, session): + m = re.findall(spawn.settings.BOOT_WAIT_PATTERN, spawn.buffer, re.M) + session['matches'] = session.get('matches', len(m)) + len(m) + matches = session['matches'] + if matches >= spawn.settings.BOOT_WAIT_PATTERN_COUNT: + raise ValueError + + wait_dialog = Dialog([Statement(spawn.settings.BOOT_WAIT_PATTERN, + action=count, + loop_continue=True, + continue_timer=True)]) + while True: + try: + wait_dialog.process(spawn, timeout=timeout) + except ValueError: + break + + # Wait a bit until the terminal is finished logging the interfaces messages + chatty_term_wait(spawn) + + +class FxosStatements(object): + def __init__(self): + ''' + All FTD Statements + ''' + self.cssp_stmt = Statement(patterns.cssp_pattern, + action=flag_ssh_session, + args=None, + loop_continue=True, + continue_timer=False) + + self.bell_stmt = Statement(patterns.bell_pattern, + action=clear_command_line, + args=None, + loop_continue=True, + continue_timer=False) + + self.command_not_completed_stmt = Statement(patterns.command_not_completed, + action=clear_command_line, + args=None, + loop_continue=True, + continue_timer=False) + + self.config_call_home_stmt = Statement(patterns.config_call_home_prompt, + action='sendline(n)', + args=None, + loop_continue=True, + continue_timer=False) + + self.ftd_reboot_confirm_stmt = Statement(patterns.ftd_reboot_confirm, + action='sendline(yes)', + args=None, + loop_continue=True, + continue_timer=False) + + self.fxos_mgmt_reboot_stmt = Statement(patterns.fxos_mgmt_reboot_confirm, + action='sendline(yes)', + args=None, + loop_continue=True, + continue_timer=False) + + self.enable_username_stmt = Statement(patterns.username, + action=enable_username_handler, + args=None, + loop_continue=True, + continue_timer=False) + + self.enable_password_stmt = Statement(patterns.password, + action=enable_password_handler, + args=None, + loop_continue=True, + continue_timer=False) + + self.boot_interrupt_stmt = Statement(patterns.boot_interrupt, + action='send(\x1b)', + args=None, + loop_continue=True, + continue_timer=False) + + +fxos_statements = FxosStatements() + +default_statement_list = [ + fxos_statements.cssp_stmt, + fxos_statements.bell_stmt, + fxos_statements.command_not_completed_stmt, + generic_statements.more_prompt_stmt +] + +reload_statements = [ + fxos_statements.fxos_mgmt_reboot_stmt, + fxos_statements.ftd_reboot_confirm_stmt, + Statement(patterns.restarting_system, loop_continue=False), + Statement(patterns.reboot_requested, loop_continue=False), + connection_closed +] + +boot_to_rommon_statements = [ + fxos_statements.fxos_mgmt_reboot_stmt, + fxos_statements.ftd_reboot_confirm_stmt, + fxos_statements.boot_interrupt_stmt +] + +login_statements = [ + generic_statements.login_stmt, + generic_statements.password_stmt, +] diff --git a/src/unicon/plugins/gaia/__init__.py b/src/unicon/plugins/gaia/__init__.py new file mode 100644 index 00000000..acfc31f0 --- /dev/null +++ b/src/unicon/plugins/gaia/__init__.py @@ -0,0 +1,100 @@ +''' +Author: Sam Johnson +Contact: samuel.johnson@gmail.com +https://github.com/TestingBytes + +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' + +from unicon.plugins.generic.connection_provider import GenericSingleRpConnectionProvider +from unicon.plugins.generic import GenericSingleRpConnection, ServiceList +from unicon.plugins.generic import service_implementation as svc +from unicon.plugins.linux import service_implementation as linux_svc +from unicon.plugins.gaia import service_implementation as gaia_svc +from unicon.plugins.gaia.statemachine import GaiaStateMachine +from unicon.plugins.gaia.settings import GaiaSettings +from time import sleep + + +class GaiaConnectionProvider(GenericSingleRpConnectionProvider): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # used for tracking the initial state - it impacts the commands used + # for state changes + self.initial_state = '' + + def init_handle(self): + con = self.connection + + self.initial_state = con.state_machine.current_state + + # The state machine path commands are different depending on the + # initial state. If the default shell is configured to be 'expert' + # mode the path commands are: + # 'clish' for expert -> clish + # 'exit' for clish -> expert + + # If the initial state is determined to be 'expert' mode, the + # commands are updated and the switchto service is used to put + # the gateway into clish mode. + + if self.initial_state == 'expert': + path = con.state_machine.get_path('clish', 'expert') + path.command = 'exit' + + path = con.state_machine.get_path('expert', 'clish') + path.command = 'clish' + + # switch to clish if in expert on connect + con.switchto('clish') + + if self.connection.goto_enable: + con.state_machine.go_to('clish', + self.connection.spawn, + context=self.connection.context, + prompt_recovery=self.prompt_recovery, + timeout=self.connection.connection_timeout) + + self.execute_init_commands() + + def disconnect(self): + """ Logout and disconnect from the device + """ + + con = self.connection + if con.connected: + con.log.info('disconnecting...') + con.switchto(self.initial_state) + con.sendline('exit') + sleep(2) + con.log.info('closing connection...') + con.spawn.close() + + +class GaiaServiceList(ServiceList): + """ gaia services """ + def __init__(self): + super().__init__() + + self.execute = gaia_svc.GaiaExecute + self.sendline = svc.Sendline + self.ping = linux_svc.Ping + self.traceroute = gaia_svc.GaiaTraceroute + self.switchto = gaia_svc.GaiaSwitchTo + + +class GaiaConnection(GenericSingleRpConnection): + """ + Connection class for Gaia OS connections + """ + + os = 'gaia' + platform = None + chassis_type = 'single_rp' + state_machine_class = GaiaStateMachine + connection_provider_class = GaiaConnectionProvider + subcommand_list = GaiaServiceList + settings = GaiaSettings() diff --git a/src/unicon/plugins/gaia/patterns.py b/src/unicon/plugins/gaia/patterns.py new file mode 100644 index 00000000..c0644c5d --- /dev/null +++ b/src/unicon/plugins/gaia/patterns.py @@ -0,0 +1,43 @@ +''' +Author: Sam Johnson +Contact: samuel.johnson@gmail.com +https://github.com/TestingBytes + +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example + +Description: + This subpackage defines patterns for Check Point Gaia OS +''' + +from unicon.plugins.generic.patterns import GenericPatterns + + +class GaiaPatterns(GenericPatterns): + def __init__(self): + super().__init__() + + # This system is for authorized use only. + # login: admin + # Password: + self.login_prompt = r'^(.*?)login:\s*$' + self.password_prompt = r'^(.*?)Password:\s*$' + + # Last login: Tue Mar 23 22:11:15 on ttyS0 + # hostname> + self.clish_prompt = r'^(.*?)%N>\s*$' + + # hostname> expert + # Enter expert password: + self.expert_password_prompt = r'^(.*?)Enter expert password:\s*$' + + # hostname> expert + # Enter expert password: + # + # Wrong password. + self.expert_password_failed = r'^(.*?)Wrong password\.\s*$' + + # Warning! All configurations should be done through clish + # You are in expert mode now. + # [Expert@hostname:0]# + self.expert_prompt = r'^(.*?)\[\w+\@%N\:\d?\]#\s*$' diff --git a/src/unicon/plugins/gaia/service_implementation.py b/src/unicon/plugins/gaia/service_implementation.py new file mode 100644 index 00000000..df1e2e53 --- /dev/null +++ b/src/unicon/plugins/gaia/service_implementation.py @@ -0,0 +1,36 @@ +''' +Author: Sam Johnson +Contact: samuel.johnson@gmail.com +https://github.com/TestingBytes + +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' + +from unicon.plugins.generic.service_implementation import Execute as GenericExecute +from unicon.plugins.generic.service_implementation import Switchto as GenericSwitchto +from unicon.plugins.generic.service_implementation import Traceroute as GenericTraceroute + + +class GaiaExecute(GenericExecute): + pass + + +class GaiaTraceroute(GenericTraceroute): + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.start_state = 'clish' + self.end_state = 'clish' + + def call_service(self, addr, command='traceroute', timeout=None, error_pattern=None, **kwargs): + super().call_service( + addr, + command=f'traceroute {addr}', + timeout=timeout, + error_pattern=error_pattern, + **kwargs) + + +class GaiaSwitchTo(GenericSwitchto): + pass diff --git a/src/unicon/plugins/gaia/settings.py b/src/unicon/plugins/gaia/settings.py new file mode 100644 index 00000000..df9594e5 --- /dev/null +++ b/src/unicon/plugins/gaia/settings.py @@ -0,0 +1,26 @@ +''' +Author: Sam Johnson +Contact: samuel.johnson@gmail.com +https://github.com/TestingBytes + +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' + +from unicon.plugins.generic.settings import GenericSettings + + +class GaiaSettings(GenericSettings): + + def __init__(self): + # inherit any parent settings + super().__init__() + self.CONNECTION_TIMEOUT = 300 + self.ESCAPE_CHAR_CALLBACK_PRE_SENDLINE_PAUSE_SEC = 1 + self.HA_INIT_EXEC_COMMANDS = ['set clienv rows 0'] + self.HA_INIT_CONFIG_COMMANDS = [] + + self.ERROR_PATTERN = [ + r'^.*?command not found.*$', + r'^.*?[Ii]nvalid command.*$' + ] diff --git a/src/unicon/plugins/gaia/statemachine.py b/src/unicon/plugins/gaia/statemachine.py new file mode 100644 index 00000000..600f1b8a --- /dev/null +++ b/src/unicon/plugins/gaia/statemachine.py @@ -0,0 +1,45 @@ +''' +Author: Sam Johnson +Contact: samuel.johnson@gmail.com +https://github.com/TestingBytes + +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' + +from unicon.plugins.gaia.statements import GaiaStatements +from unicon.statemachine import Path, State +from unicon.plugins.generic.statemachine import GenericSingleRpStateMachine +from .patterns import GaiaPatterns +from unicon.eal.dialogs import Dialog + +patterns = GaiaPatterns() +statements = GaiaStatements() + + +class GaiaStateMachine(GenericSingleRpStateMachine): + + def __init__(self, hostname=None): + super().__init__(hostname) + + def create(self): + ''' + statemachine class's create() method is its entrypoint. This showcases + how to setup a statemachine in Unicon. + ''' + + clish = State("clish", patterns.clish_prompt) + expert = State("expert", patterns.expert_prompt) + + self.add_state(clish) + self.add_state(expert) + + # Assume inital state is 'clish'. If 'expert' is detected by + # GaiaConnectionProvider.init_handle. These Path commands will + # be changed at runtime. + + clish_to_expert = Path(clish, expert, 'expert', Dialog([statements.expert_password_stmt])) + expert_to_clish = Path(expert, clish, 'exit', None) + + self.add_path(clish_to_expert) + self.add_path(expert_to_clish) diff --git a/src/unicon/plugins/gaia/statements.py b/src/unicon/plugins/gaia/statements.py new file mode 100644 index 00000000..7a198f8e --- /dev/null +++ b/src/unicon/plugins/gaia/statements.py @@ -0,0 +1,41 @@ +''' +Contents largely inspired by sample Unicon repo: +https://github.com/CiscoDevNet/pyats-plugin-examples/tree/master/unicon_plugin_example/src/unicon_plugin_example +''' + +from unicon.eal.dialogs import Statement +from unicon.plugins.generic.statements import GenericStatements +from .patterns import GaiaPatterns +from unicon.utils import to_plaintext +from time import sleep + +statements = GenericStatements() +patterns = GaiaPatterns() + + +def expert_password_handler(spawn, context, session): + credentials = context.get('credentials') + expert_credential_password = credentials.get('expert', {}).get('password') + + expert_password = to_plaintext(expert_credential_password) + sleep(0.1) + spawn.sendline(expert_password) + sleep(0.1) + spawn.sendline() + + +class GaiaStatements(GenericStatements): + """ + Class that defines the Statements for Gaia plugin + implementation + """ + + def __init__(self): + super().__init__() + + self.expert_password_stmt = Statement( + pattern=patterns.expert_password_prompt, + action=expert_password_handler, + args=None, + loop_continue=True, + continue_timer=False) diff --git a/src/unicon/plugins/generic/__init__.py b/src/unicon/plugins/generic/__init__.py index 57fdb4d1..31e1d666 100644 --- a/src/unicon/plugins/generic/__init__.py +++ b/src/unicon/plugins/generic/__init__.py @@ -57,9 +57,12 @@ def __init__(self): self.ping = svc.Ping self.traceroute = svc.Traceroute self.copy = svc.Copy - self.expect_log = svc.ExpectLogging self.log_user = svc.LogUser self.log_file = svc.LogFile + self.expect_log = svc.ExpectLogging + self.attach = svc.AttachModuleService + self.switchto = svc.Switchto + self.guestshell = svc.GuestshellService class HAServiceList(ServiceList): @@ -82,7 +85,7 @@ def __init__(self): class GenericSingleRpConnection(BaseSingleRpConnection): """ Defines Generic Connection class for singleRP """ os = 'generic' - series = None + platform = None chassis_type = 'single_rp' state_machine_class = GenericSingleRpStateMachine connection_provider_class = GenericSingleRpConnectionProvider @@ -94,7 +97,7 @@ class GenericDualRPConnection(BaseDualRpConnection): """ Defines Generic Connection class for DualRP """ os = 'generic' - series = None + platform = None chassis_type = 'dual_rp' state_machine_class = GenericDualRpStateMachine connection_provider_class = GenericDualRpConnectionProvider diff --git a/src/unicon/plugins/generic/connection_provider.py b/src/unicon/plugins/generic/connection_provider.py index 65898dc7..76cccea4 100644 --- a/src/unicon/plugins/generic/connection_provider.py +++ b/src/unicon/plugins/generic/connection_provider.py @@ -67,4 +67,4 @@ def get_connection_dialog(self): self.connection.settings.PASSWORD_PROMPT) return con.connect_reply + \ Dialog(custom_auth_stmt + connection_statement_list - if custom_auth_stmt else connection_statement_list) + if custom_auth_stmt else connection_statement_list) \ No newline at end of file diff --git a/src/unicon/plugins/generic/patterns.py b/src/unicon/plugins/generic/patterns.py index 8316c26f..4ccb1c71 100644 --- a/src/unicon/plugins/generic/patterns.py +++ b/src/unicon/plugins/generic/patterns.py @@ -24,7 +24,7 @@ def __init__(self): """ super().__init__() # self.enable_prompt = r'.*%N#\s?$' - self.default_hostname_pattern = r'RouterRP|Router|[Ss]witch|Controller|ios' + self.default_hostname_pattern = r'WLC|RouterRP|Router|[Ss]witch|Controller|ios' self.enable_prompt = r'^(.*?)(Router|Router-stby|Router-sdby|RouterRP|RouterRP-standby|%N-standby|%N\(standby\)|%N-sdby|%N-stby|(S|s)witch|(S|s)witch\(standby\)|Controller|ios|-Slot[0-9]+|%N)(\(boot\))*#\s?$' @@ -32,17 +32,17 @@ def __init__(self): self.disable_prompt = r'^(.*?)(Router|Router-stby|Router-sdby|RouterRP|RouterRP-standby|%N-standby|%N-sdby|%N-stby|(S|s)witch|s(S|s)witch\(standby\)|Controller|ios|-Slot[0-9]+|%N)(\(boot\))*>\s?$' # self.config_prompt = r'.*%N\(config.*\)#\s?$' - self.config_prompt = r'^(.*)\(.*(con|cfg|ipsec-profile)\S*\)#\s?$' - self.rommon_prompt = r'rommon[\s\d]*>\s?$' + self.config_prompt = r'^(.*)\(.*(con|cfg|ipsec-profile|ca-trustpoint|gkm-local-server)\S*\)#\s?$' + self.rommon_prompt = r'^(.*?)(rommon[\s\d]*>|switch:|grub>)\s*(\x1b\S+)?$' # self.standby_enable_prompt = r'^(.*?)(RouterRP-standby|%N-standby|%N-sdby|%N\(standby\))#\s?$' # self.standby_disable_prompt = r'^(.*?)(RouterRP-standby|%N-standby|%N-sdby|%N\(standby\))>\s?$' - self.standby_locked = r'[S|s]tandby console disabled' + self.standby_locked = r'^.*?([S|s]tandby console disabled|This \(D\)RP Node is not ready or active for login \/configuration.*)' self.shell_prompt = r'^(.*)%N\(shell\)>\s?' self.disconnect_message = r'Received disconnect from .*:' self.password_ok = r'Password OK\s*$' - self.continue_connect = r'Are you sure you want to continue connecting \(yes/no\)' + self.continue_connect = r'Are you sure you want to continue connecting \(yes/no(/\[fingerprint\])?\)' self.cisco_commit_changes_prompt = r'Uncommitted changes found, commit them\? \[yes/no/CANCEL\]' self.juniper_commit_changes_prompt = r'Discard changes and continue\? \[yes,no\]' @@ -51,5 +51,57 @@ def __init__(self): self.press_ctrlx = r"^(.*?)Press Ctrl\+x to Exit the session" self.connected = r'^(.*?)Connected.' - self.enter_basic_mgmt_setup = r'Would you like to enter basic management setup\? \[yes/no\]:' - self.kerberos_no_realm = r'^(.*)Kerberos: No default realm defined for Kerberos!' + self.enter_basic_mgmt_setup = r'Would you like to enter basic management setup\? \[yes/no\]:\s*$' + self.kerberos_no_realm = r'^(.*)Kerberos:\s*No default realm defined for Kerberos!\s*$' + + self.passphrase_prompt = r'^.*Enter passphrase for key .*?:\s*?' + + self.learn_os_prompt = r'^(.*?(?\$~%]|[^#\s]#|~ #|~/|^admin:|^#)\s?(\x1b\S+)?)$' + + self.sudo_password_prompt = r'^.*(\[sudo\] password for .*?:|This is your UNIX password:)\s*$' + + # *Sep 6 23:13:38.188: %PNP-6-PNP_SDWAN_STARTED: PnP SDWAN started (7) via (pnp-sdwan-abort-on-cli) by (pid=3, pname=Exec) + # *Sep 6 23:18:11.702: %ENVIRONMENTAL-1-ALERT: Temp: Inlet 1, Location: R0, State: Warning, Reading: 45 Celsius + # *Sep 6 17:43:41.291: %Cisco-SDWAN-RP_0-CFGMGR-4-WARN-300005: New admin password not set yet, waiting for daemons to read initial config. + # Guestshell destroyed successfully + # %Error opening tftp://255.255.255.255/network-confg (Timed out) + # %Error opening tftp://255.255.255.255/cisconet.cfg (Timed out) + # %Error opening tftp://255.255.255.255/switch-confg (Timed out) + # LC/0/2/CPU0:Sep 10 00:54:42.841 + # RP/0/0/CPU0:Oct 9 01:44:47.875 + # *May 28 09:01:05.136: yang-infra: Default hostkey created (NETCONF_SSH_RSA_KEY.server) + # *May 28 09:01:11.975: PKI_SSL_IPC: SUDI certificate chain and key pair are invalid + # SECURITY WARNING - Module: SSH, Command: crypto key generate rsa ..., Reason: SSH RSA host key uses insufficient key length, Remediation: Configure SSH RSA host key with minimum key length of 3072 bits + # Switch#[OK] + self.syslog_message_pattern = ( + r"^.*?(%\w+(-\S+)?-\d+-\w+|" + r"yang-infra:|PKI_SSL_IPC:|Guestshell destroyed successfully|" + r"%Error opening tftp:\/\/255\.255\.255\.255|Autoinstall trying|" + r"audit: kauditd hold queue overflow|SECURITY WARNING|%RSA key|INSECURE DYNAMIC WARNING|" + r"(LC|RP)/\d+/\d+/CPU\d+:\w+\s+\d+\s+\d{2}:\d{2}:\d{2}|" + r"\[OK\]" + r").*\s*$" + ) + self.config_locked = r'Configuration (mode )?(is )?locked|Config mode cannot be entered' + + self.config_start = r'Enter configuration commands, one per line\.\s+End with CNTL/Z\.\s*$' + + self.enable_secret = r'^.*?(Enter|Confirm) enable secret( \[\])?:\s*$' + self.enable_password = r'^.*?enable[\r\n]*.*?[Pp]assword( for )?(\S+)?: ?$' + + self.enter_your_selection_2 = r'^.*?Enter your selection( \[2])?:\s*$' + + self.guestshell_prompt = r'^(.*)\[\S+@guestshell\s+.*\][#\$]\s?$' + + self.press_any_key = r'^.*?Press any key to continue\..*?$' + + # VT100 patterns + self.get_cursor_position = r'\x1b\[6n' + + self.new_password = r'^(Enter new password|Confirm password):\s*$' + + self.enter_your_encryption_selection_2 = r'^.*?Enter your encryption selection( \[2])?:\s*$' + + self.no_password_set = r'^.*% (No password set|Error in authentication.).*' + + self.tclsh_continue = r'^\+\>\s?$' diff --git a/src/unicon/plugins/generic/service_implementation.py b/src/unicon/plugins/generic/service_implementation.py index b3e09a43..98cfe2cf 100644 --- a/src/unicon/plugins/generic/service_implementation.py +++ b/src/unicon/plugins/generic/service_implementation.py @@ -12,22 +12,31 @@ """ -import re, os +import io +import os +import re +import copy import logging import collections import ipaddress from itertools import chain +import time import warnings +from datetime import datetime, timedelta from time import sleep from unicon.bases.routers.services import BaseService from unicon.core.errors import SubCommandFailure, StateMachineError, \ - CopyBadNetworkError, TimeoutError + CopyBadNetworkError, TimeoutError, UniconBackendDecodeError, \ + UniconAuthenticationError, CredentialsExhaustedError from unicon.eal.dialogs import Dialog from unicon.eal.dialogs import Statement -from unicon.eal.utils import expect_log -from unicon.plugins.generic.statements import chatty_term_wait, custom_auth_statements +from unicon.plugins.generic.statements import ( + chatty_term_wait, + custom_auth_statements, + buffer_settled, + default_statement_list) from unicon.plugins.generic.service_statements import reload_statement_list, \ ping_dialog_list, extended_ping_dialog_list, copy_statement_list, \ ha_reload_statement_list, switchover_statement_list, \ @@ -35,20 +44,30 @@ from unicon.plugins.generic.service_patterns import CopyPatterns from unicon.utils import AttributeDict, to_plaintext from unicon import logs +from unicon.logs import UniconStreamHandler, UNICON_LOG_FORMAT from unicon.plugins.generic.utils import GenericUtils -from .service_statements import execution_statement_list +from .service_statements import execution_statement_list, configure_statement_list +from .statements import disable_enable_transition_statements +from unicon.plugins.generic.statemachine import config_transition utils = GenericUtils() ReloadResult = collections.namedtuple('ReloadResult', ['result', 'output']) -def exec_state_change_action(spawn, err_state, sm): - msg = "Expected device to reach at '{}' state, but landed on '{}' state."\ +class SwitchoverResult: + def __init__(self, result, output, **kwargs): + self.result = result + self.output = output + + +def invalid_state_change_action(spawn, err_state, sm): + msg = "Expected device to reach '{}' state, but landed on '{}' state."\ .format(sm.current_state, err_state.name) # Update device current state with unexpected state. sm.update_cur_state(err_state) raise StateMachineError(msg) + class Send(BaseService): """Service to send the command/string with "\\r" to spawned channel. @@ -192,6 +211,7 @@ def call_service(self, patterns, timeout=None, def get_service_result(self): return self.result + class ReceiveService(BaseService): """match a pattern from spawn buffer @@ -223,11 +243,13 @@ class ReceiveService(BaseService): def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) - self.service_name = 'receive' + def pre_service(self, *args, **kwargs): pass + def post_service(self, *args, **kwargs): pass + def call_service(self, pattern, timeout=None, size=None, trim_buffer=True, target=None, *args, **kwargs): @@ -235,21 +257,21 @@ def call_service(self, pattern, timeout=None, self.result = False self.connection.receiveBuffer = '' try: - if pattern == r'nopattern^': + if pattern == r'nopattern^': sleep(timeout or 10) - self.connection.receiveBuffer = spawn.expect(r'.*', size, - *args, **kwargs).match_output + self.connection.receiveBuffer = spawn.expect(r'.*', size, *args, **kwargs).match_output else: - self.connection.receiveBuffer = spawn.expect(pattern, - timeout, size, *args, **kwargs).match_output + self.connection.receiveBuffer = spawn.expect(pattern, timeout, size, *args, **kwargs).match_output self.result = True except TimeoutError: pass except Exception as err: raise SubCommandFailure("Receive service failed", err) from err + def get_service_result(self): return self.result + class ReceiveBufferService(BaseService): """Returns data match by receive() service pattern. @@ -269,21 +291,26 @@ class ReceiveBufferService(BaseService): def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) - self.service_name = 'receive_buffer' + def pre_service(self, *args, **kwargs): pass + def post_service(self, *args, **kwargs): pass + def call_service(self): try: self.result = self.connection.receiveBuffer except AttributeError as err: - raise SubCommandFailure( - "receive_buffer should be invoke after receive call", err) + raise SubCommandFailure("receive_buffer should be invoked after receive call", err) except Exception as err: raise SubCommandFailure("Error in receive_buffer", err) from err + def get_service_result(self): - return self.result + result = copy.copy(self.result) + delattr(self.connection, 'receiveBuffer') + return result + class LogUser(BaseService): """ Service to enable or disable a device logs on screen. @@ -321,7 +348,7 @@ def call_service(self, enable, target=None, *args, **kwargs): else: # add it try: - from pyats.log import managed_handlers + from pyats.log import managed_handlers # noqa except Exception: sh = logs.UniconStreamHandler() sh.setFormatter(logging.Formatter(fmt='[%(asctime)s] %(message)s')) @@ -341,6 +368,7 @@ def call_service(self, enable, target=None, *args, **kwargs): def get_service_result(self): return self.result + class LogFile(BaseService): """ Service to get or change Device FileHandler file. If no argument passed then it return current filename of FileHandler. @@ -390,40 +418,18 @@ def call_service(self, filename=None, *args, **kwargs): def get_service_result(self): return self.result + class ExpectLogging(BaseService): r""" Service to enable expect internal logging. - By default it enables on both file and screen, provided filename is specified. - If not it will log the message on screen. - Arguments: - filename: File name to log the messages enable: True/False for enabling and disabling the expect_log - logto: stdout/file to enable logging on screen/file or both. - Example: .. code:: - rtr.expect_log(filename='/ws/lshekhar-bgl/rtr-expect.log', enable=True) - rtr.execute("term length 0") - Expect Sending term length 0 - Expect Got :: 'term len' - Expect Got :: 'gth 0\\r\\r\\n\\rn7k2-1# ' - Expect Got :: 'term length 0\\r\\r\\n\\rn7k2-1# ' - Pattern Matched:: ^(.*?)(n7k2-1|Router|RouterRP|RouterRP-standby|n7k2-1-standby|n7k2-1\(standby\)|n7k2-1-sdby|(S|s)witch|Controller|ios|-Slot[0-9]+)(\(boot\))*#\s?$ - Pattern List:: ['^.*--\\s?[Mm]ore\\s?--', '^.*\\[confirm\\(y/n\\)?\\]', '^.*\\[yes/no\\]\\s?:?$', '^(.*?)(n7k2-1|Router|RouterRP|RouterRP-standby|n7k2-1-standby|n7k2-1\\(standby\\)|n7k2-1-sdby|(S|s)witch|Controller|ios|-Slot[0-9]+)(\\(boot\\))*#\\s?$'] - - .. code:: - - rtr.execute("term width 511") - Expect Sending term width 511 - Expect Got :: 'term width 511\\r\\r\\n' - Expect Got :: '\\rn7k2-1# ' - Expect Got :: 'term width 511\\r\\r\\n\\rn7k2-1# ' - Pattern Matched:: ^(.*?)(n7k2-1|Router|RouterRP|RouterRP-standby|n7k2-1-standby|n7k2-1\(standby\)|n7k2-1-sdby|(S|s)witch|Controller|ios|-Slot[0-9]+)(\(boot\))*#\s?$ - Pattern List:: ['^.*--\\s?[Mm]ore\\s?--', '^.*\\[confirm\\(y/n\\)?\\]', '^.*\\[yes/no\\]\\s?:?$', '^(.*?)(n7k2-1|Router|RouterRP|RouterRP-standby|n7k2-1-standby|n7k2-1\\(standby\\)|n7k2-1-sdby|(S|s)witch|Controller|ios|-Slot[0-9]+)(\\(boot\\))*#\\s?$', '^.*--\\s?[Mm]ore\\s?--', '^.*\\[confirm\\(y/n\\)?\\]', '^.*\\[yes/no\\]\\s?:?$', '^(.*?)(n7k2-1|Router|RouterRP|RouterRP-standby|n7k2-1-standby|n7k2-1\\(standby\\)|n7k2-1-sdby|(S|s)witch|Controller|ios|-Slot[0-9]+)(\\(boot\\))*#\\s?$'] + rtr.expect_log(enable=True) """ def log_service_call(self): @@ -435,23 +441,17 @@ def pre_service(self, *args, **kwargs): def post_service(self, *args, **kwargs): pass - def call_service(self, filename='', - enable=False, - logto='stdout', + def call_service(self, enable=False, *args, **kwargs): con = self.connection - filename = filename - enable = enable - logto = logto - con.log.debug("+++ expect_log +++") - try: - expect_log(filename=filename, - enable=enable, - logto=logto) - except Exception as err: - raise SubCommandFailure("Failed to enable/disable expect_log", - err) + if enable: + con.log.info("+++ enable debug logging +++") + con.log.setLevel(logging.DEBUG) + else: + con.log.info("+++ disable debug logging +++") + con.log.setLevel(logging.INFO) + self.result = True def get_service_result(self): @@ -482,19 +482,45 @@ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) self.start_state = 'enable' self.end_state = 'enable' - self.service_name = 'enable' + self.dialog = Dialog(disable_enable_transition_statements) self.__dict__.update(kwargs) - def call_service(self, target=None, *args, **kwargs): + def pre_service(self, *args, **kwargs): + self.prompt_recovery = self.connection.prompt_recovery + if 'prompt_recovery' in kwargs: + self.prompt_recovery = kwargs.get('prompt_recovery') + + def call_service(self, target=None, command='', *args, **kwargs): + handle = self.get_handle(target) spawn = self.get_spawn(target) sm = self.get_sm(target) + timeout = kwargs.get('timeout', None) or handle.settings.ENABLE_TIMEOUT + + # If the device is in rommon, enable() will use the + # image_to_boot info to boot the image specified + # by the user. This is used by boot_image in iosxe/statements.py + # (IOSXE only implementation at this time.) + handle.context["image_to_boot"] = \ + kwargs.get("image_to_boot", kwargs.get('image', '')) + + # override command to be enable when command is given + if command: + disable = sm.get_state('disable') + enable = sm.get_state('enable') + pt = sm.get_path(disable, enable) + sm.paths[sm.paths.index(pt)].command = command try: sm.go_to(self.start_state, spawn, - context=self.context) + context=handle.context, + timeout=timeout) + except (UniconAuthenticationError, CredentialsExhaustedError): + # Don't wrap auth errors - re-raise them directly + raise except Exception as err: raise SubCommandFailure("Failed to Bring device to Enable State", err) from err + self.result = True @@ -521,16 +547,16 @@ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) self.start_state = 'disable' self.end_state = 'disable' - self.service_name = 'disable' self.__dict__.update(kwargs) def call_service(self, target=None, *args, **kwargs): + handle = self.get_handle(target) spawn = self.get_spawn(target) sm = self.get_sm(target) try: sm.go_to(self.start_state, spawn, - context=self.context) + context=handle.context) except Exception as err: raise SubCommandFailure("Failed to Bring device to Disable State", err) from err @@ -577,13 +603,15 @@ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) self.start_state = 'any' self.end_state = 'any' - self.service_name = 'execute' self.timeout = connection.settings.EXEC_TIMEOUT self.__dict__.update(kwargs) self.utils = utils self.dialog = Dialog(execution_statement_list) self.matched_retries = connection.settings.EXECUTE_MATCHED_RETRIES 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 @@ -591,20 +619,24 @@ def log_service_call(self): def post_service(self, *args, **kwargs): pass - def call_service(self, command=[], + def call_service(self, command=[], # noqa: C901 reply=Dialog([]), timeout=None, error_pattern=None, + append_error_pattern=None, search_size=None, allow_state_change=None, matched_retries=None, matched_retry_sleep=None, + detect_state=None, *args, **kwargs): con = self.connection sm = self.get_sm() 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 + timeout = timeout or self.timeout if error_pattern is None: @@ -612,6 +644,11 @@ def call_service(self, command=[], else: self.error_pattern = error_pattern + if append_error_pattern: + if not isinstance(append_error_pattern, list): + raise ValueError('append_error_pattern should be a list') + self.error_pattern += append_error_pattern + # user specified search buffer size if search_size is not None: con.spawn.search_size = search_size @@ -623,14 +660,16 @@ def call_service(self, command=[], matched_retry_sleep = self.matched_retry_sleep \ if matched_retry_sleep is None else matched_retry_sleep - if not isinstance(reply, Dialog): + if (reply is None) or (reply == []): + reply = Dialog([]) + elif not isinstance(reply, Dialog): raise SubCommandFailure( "dialog passed via 'reply' must be an instance of Dialog") # service_dialog overrides the default execution dialogs if 'service_dialog' in kwargs: service_dialog = kwargs['service_dialog'] - if service_dialog is None: + if (service_dialog is None) or (service_dialog == []): service_dialog = Dialog([]) elif not isinstance(service_dialog, Dialog): raise SubCommandFailure( @@ -654,24 +693,25 @@ def call_service(self, command=[], if custom_auth_stmt: dialog += Dialog(custom_auth_stmt) - # Add all known states to detect state changes. - for state in sm.states: - # The current state is already added by the service_dialog method - if state.name != sm.current_state: - if allow_state_change: - dialog.append(Statement( - pattern=state.pattern, - matched_retries=matched_retries, - matched_retry_sleep=matched_retry_sleep - )) - else: - dialog.append(Statement( - pattern=state.pattern, - action=exec_state_change_action, - args={'err_state': state, 'sm': sm}, - matched_retries=matched_retries, - matched_retry_sleep=matched_retry_sleep - )) + if self.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 + if state.name != sm.current_state: + if allow_state_change: + dialog.append(Statement( + pattern=state.pattern, + matched_retries=self.state_change_matched_retries, + matched_retry_sleep=self.state_change_matched_retry_sleep + )) + else: + dialog.append(Statement( + pattern=state.pattern, + action=invalid_state_change_action, + args={'err_state': state, 'sm': sm}, + matched_retries=self.state_change_matched_retries, + matched_retry_sleep=self.state_change_matched_retry_sleep + )) # store the last used dialog, used by unittest self._last_dialog = dialog @@ -692,8 +732,10 @@ def call_service(self, command=[], command_output = {} for command in commands: - con.log.info("+++ %s: executing command '%s' +++" - % (self.connection.hostname, command)) + + message = f"executing command '{command}'" + super().log_service_call(message) + con.sendline(command) try: dialog_match = dialog.process( @@ -705,9 +747,11 @@ def call_service(self, command=[], if dialog_match: self.result = dialog_match.match_output self.result = self.get_service_result() - sm.detect_state(con.spawn) + sm.detect_state(con.spawn, con.context) except StateMachineError: raise + except UniconBackendDecodeError: + pass except Exception as err: raise SubCommandFailure("Command execution failed", err) from err @@ -715,13 +759,13 @@ def call_service(self, command=[], output = self.utils.truncate_trailing_prompt( sm.get_state(sm.current_state), self.result, - hostname=self.connection.hostname, + hostname=con.hostname, result_match=dialog_match, ) output = self.extra_output_process(output) output = output.replace(command, "", 1) # only strip first newline and leave formatting intact - output = re.sub(r"^\r?\r\n", "", output, 1) + output = re.sub(r"^\r?\r\n", "", output, count=1) output = output.rstrip() if command in command_output: @@ -743,7 +787,7 @@ def call_service(self, command=[], if self.end_state != 'any': sm.go_to(self.end_state, con.spawn, prompt_recovery=self.prompt_recovery, - context=self.connection.context) + context=con.context) def extra_output_process(self, output): # remove backspace and ansi escape sequence from output @@ -764,6 +808,9 @@ class Configure(BaseService): reply: Addition Dialogs for interactive config commands. timeout : Timeout value in sec, Default Value is 30 sec error_pattern: list of regex to detect command errors + allow_state_change: If True allow the state change during the + configuration otherwise raise state machine error if the state + changes during configuration. target: Target RP where to execute service, for DualRp only lock_retries: retry times if config mode is locked, default is 0 lock_retry_sleep: sleep between retries, default is 2 sec @@ -775,6 +822,8 @@ class Configure(BaseService): 0 means to send all commands in a single chunk bulk_chunk_sleep: sleep between sending command chunks, default is 0.5 sec + result_check_per_command: boolean option, check results after + each command (default: True) Returns: command output on Success, raise SubCommandFailure on failure @@ -792,14 +841,16 @@ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) self.start_state = 'config' self.end_state = 'enable' - self.service_name = 'config' self.timeout = connection.settings.CONFIG_TIMEOUT + self.dialog = Dialog(configure_statement_list) self.commit_cmd = '' - self.lock_retries = connection.settings.CONFIG_LOCK_RETRIES - self.lock_retry_sleep = connection.settings.CONFIG_LOCK_RETRY_SLEEP self.bulk = connection.settings.BULK_CONFIG self.bulk_chunk_lines = connection.settings.BULK_CONFIG_CHUNK_LINES self.bulk_chunk_sleep = connection.settings.BULK_CONFIG_CHUNK_SLEEP + self.valid_transition_commands = ['end', 'exit'] + self.valid_transition_states = ['config_pki_hexmode'] + 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.__dict__.update(kwargs) class ConfigUtils(GenericUtils): @@ -815,66 +866,113 @@ def truncate_trailing_prompt(self, con_state, self.utils = ConfigUtils() def pre_service(self, *args, **kwargs): + sm = self.get_sm() self.prompt_recovery = kwargs.get('prompt_recovery', False) - def call_service(self, + # Backward compatibility with old config lock implementation + con = self.connection + settings = con.settings + settings.CONFIG_LOCK_RETRIES = kwargs.get('lock_retries', settings.CONFIG_LOCK_RETRIES) + settings.CONFIG_LOCK_RETRY_SLEEP = kwargs.get('lock_retry_sleep', settings.CONFIG_LOCK_RETRY_SLEEP) + + super().pre_service(*args, **kwargs) + + def post_service(self, *args, **kwargs): + pass + + def call_service(self, # noqa: C901 command=[], reply=Dialog([]), timeout=None, error_pattern=None, + append_error_pattern=None, + allow_state_change=None, target=None, - lock_retries=None, - lock_retry_sleep=None, bulk=None, bulk_chunk_lines=None, bulk_chunk_sleep=None, + result_check_per_command=True, *args, **kwargs): + + self.result_check_per_command = result_check_per_command + con = self.connection + sm = self.get_sm() + handle = self.get_handle(target) timeout = timeout or self.timeout + if allow_state_change is None: + allow_state_change = con.settings.CONFIGURE_ALLOW_STATE_CHANGE + if error_pattern is None: - self.error_pattern = self.connection.settings.CONFIGURE_ERROR_PATTERN + self.error_pattern = \ + handle.settings.CONFIGURE_ERROR_PATTERN else: self.error_pattern = error_pattern + if append_error_pattern: + if not isinstance(append_error_pattern, list): + raise ValueError('append_error_pattern should be a list') + self.error_pattern += append_error_pattern + bulk = self.bulk if bulk is None else bulk - bulk_chunk_lines = self.bulk_chunk_lines if bulk_chunk_lines is None \ - else bulk_chunk_lines - bulk_chunk_sleep = self.bulk_chunk_sleep if bulk_chunk_sleep is None \ - else bulk_chunk_sleep - if 'retries' in kwargs: - warnings.warn('**** "retries" argument is deprecated.' - ' Please use "lock_retries" ****', - category=DeprecationWarning) - lock_retries = lock_retries or kwargs['retries'] - if 'retry_sleep' in kwargs: - warnings.warn('**** "retry_sleep" argument is deprecated.' - ' Please use "lock_retry_sleep" ****', - category=DeprecationWarning) - lock_retry_sleep = lock_retry_sleep or kwargs['retry_sleep'] - lock_retries = self.lock_retries if lock_retries is None \ - else lock_retries - lock_retry_sleep = self.lock_retry_sleep if lock_retry_sleep is None \ - else lock_retry_sleep + bulk_chunk_lines = self.bulk_chunk_lines \ + if bulk_chunk_lines is None else bulk_chunk_lines + bulk_chunk_sleep = self.bulk_chunk_sleep \ + if bulk_chunk_sleep is None else bulk_chunk_sleep + if not isinstance(reply, Dialog): raise SubCommandFailure('"reply" must be an instance of Dialog') - handle = self.get_handle(target) - self.utils.retry_handle_state_machine_go_to( - handle, - self.start_state, - lock_retries, - lock_retry_sleep, - context=self.connection.context, - prompt_recovery=self.prompt_recovery - ) + + def config_state_change(spawn, from_state, sm): + last_cmd = spawn.last_sent.strip() + # check if the last command is not in the list of valid commands and the state is not in the list of valid states + # for transition + if last_cmd not in self.valid_transition_commands and from_state.name not in self.valid_transition_states: + invalid_state_change_action( + spawn, err_state=from_state, sm=sm) + else: + sm.update_cur_state(from_state) + self.result = '' if command: flat_cmd = self.utils.flatten_splitlines_command(command) - dialog = self.service_dialog(service_dialog=reply) - sp = handle.spawn + dialog = self.dialog + self.service_dialog(handle=handle, service_dialog=reply) + # Add all known states to detect state changes. + for state in sm.states: + # The current state is already added by the service_dialog method + if state.name != sm.current_state: + if allow_state_change: + dialog.append(Statement( + pattern=state.pattern, + matched_retries=self.state_change_matched_retries, + matched_retry_sleep=self.state_change_matched_retry_sleep + )) + else: + dialog.append(Statement( + pattern=state.pattern, + action=config_state_change, + args={'from_state': state, 'sm': sm}, + matched_retries=self.state_change_matched_retries, + matched_retry_sleep=self.state_change_matched_retry_sleep + )) + + banner_lines, command_lines, banner_delim = self.get_banner_lines(flat_cmd) + + # Populate context for banner_text_handler only if banner was detected + if banner_lines: + self.connection.log.info('Banner detected, configuring banners without state detection') + + # Send banner lines + for line in banner_lines: + handle.spawn.sendline(line) + time.sleep(0.1) + handle.spawn.read_update_buffer() + + if bulk: - indicator = self.connection.settings.BULK_CONFIG_END_INDICATOR - cmd_lst = list(chain(flat_cmd, [indicator])) + indicator = handle.settings.BULK_CONFIG_END_INDICATOR + cmd_lst = list(chain(command_lines, [indicator])) if bulk_chunk_lines == 0: chunks = [cmd_lst] else: @@ -882,35 +980,86 @@ def call_service(self, for i in range(0, len(cmd_lst), bulk_chunk_lines)] for idx, chunk in enumerate(chunks, 1): chunk_cmd = '\n'.join(chunk) - sp.sendline(chunk_cmd) + handle.spawn.sendline(chunk_cmd) if idx != len(chunks): sleep(bulk_chunk_sleep) + handle.spawn.read_update_buffer() else: try: - sp.expect([indicator], timeout=timeout, + handle.spawn.expect([indicator], timeout=timeout, trim_buffer=False) - self.result, _, sp.buffer = \ - sp.buffer.rpartition(indicator) + self.result, _, handle.spawn.buffer = \ + handle.spawn.buffer.rpartition(indicator) except Exception as err: raise SubCommandFailure('Configuration failed', err) from err self.process_dialog_on_handle(handle, dialog, timeout) if self.commit_cmd: - sp.sendline(self.commit_cmd) + handle.spawn.sendline(self.commit_cmd) self.process_dialog_on_handle(handle, dialog, timeout) else: - cmds = chain(flat_cmd, [self.commit_cmd]) \ - if self.commit_cmd else flat_cmd + cmds = chain(command_lines, [self.commit_cmd]) \ + if self.commit_cmd else command_lines for cmd in cmds: - sp.sendline(cmd) + handle.spawn.sendline(cmd) + self.update_hostname_if_needed([cmd]) self.process_dialog_on_handle(handle, dialog, timeout) - handle.state_machine.go_to( + # To handle the session + if handle.context.get('config_session_locked'): + self.connection.log.warning('Config locked, waiting {} seconds'.format( + self.connection.settings.CONFIG_LOCK_RETRY_SLEEP)) + sleep(self.connection.settings.CONFIG_LOCK_RETRY_SLEEP) + config_transition(handle.state_machine, handle.spawn, handle.context) + handle.context['config_session_locked'] = False + handle.spawn.sendline(cmd) + self.process_dialog_on_handle(handle, dialog, timeout) + + # store config_result so it can be returned to the user later + config_result = self.result + output = handle.state_machine.go_to( self.end_state, handle.spawn, prompt_recovery=self.prompt_recovery, timeout=timeout, context=self.context ) + # set self.result, this is used by get_server_result to check for errors + self.result = output + # check for errors in the transition to the end_state + self.get_service_result() + # return the config_result to the user via self.result + self.result = config_result + + + def get_banner_lines(self, config_lines): + """ Process lines related to the banner command + Args: + config_lines (list): list of config lines + Returns: + tuple: (banner_lines, command_lines, banner_delim) + """ + banner_lines = [] + command_lines = [] + banner_delim = None + + for line in config_lines: + + match = re.match(r'^\s*banner\s+(login|motd|exec|incoming)\s+(\S)', line) + if match: + banner_lines.append(line) + banner_delim = match.group(2) + continue + + if banner_delim: + banner_lines.append(line) + # End of banner when delimiter repeats as a full line + if line.strip() == banner_delim: + banner_delim = None + continue + + command_lines.append(line) + + return banner_lines, command_lines, banner_delim def process_dialog_on_handle(self, handle, dialog, timeout): try: @@ -918,27 +1067,44 @@ def process_dialog_on_handle(self, handle, dialog, timeout): handle.spawn, timeout=timeout, prompt_recovery=self.prompt_recovery, - context=self.context + context=handle.context ) + except StateMachineError: + raise except Exception as err: - raise SubCommandFailure('Configuration failed', err) \ - from err + raise SubCommandFailure("Command execution failed", err) from err cmd_result = self.utils.truncate_trailing_prompt( handle.state_machine.get_state(handle.state_machine.current_state), cmd_result.match_output, - hostname=self.connection.hostname, + hostname=handle.hostname, result_match=cmd_result) self.result += cmd_result + if self.result_check_per_command: + try: + self.get_service_result() + except SubCommandFailure: + # Go to end state after command failure, + handle.state_machine.go_to(self.end_state, + handle.spawn, + context=self.context) + raise + + def update_hostname_if_needed(self, cmd_list): + for cmd in cmd_list: + m = re.match(r'^\s*(hostname|switchname) (\S+)', cmd) + if m: + self.connection.hostname = m.group(2) + return class Config(Configure): def call_service(self, *args, **kwargs): - self.connection.log.warn('**** This service is deprecated. ' + - 'Please use "configure" service ****') + self.connection.log.warning('**** This service is deprecated. Please use "configure" service ****') super().call_service(*args, **kwargs) + class Reload(BaseService): """Service to reload the device. @@ -946,9 +1112,9 @@ class Reload(BaseService): reload_command: reload command to be issued. default is "reload" reload_creds: credential or list of credentials to use to respond to username/password prompts. - dialog: Dialog which include list of Statements for - additional dialogs prompted by reload command, in-case - it is not in the current list. + reply: Dialog which include list of Statements for + additional dialogs prompted by reload command, in-case + it is not in the current list. timeout: Timeout value in sec, Default Value is 300 sec return_output: If True, return a namedtuple with result and output result is True if reload is successful. @@ -970,41 +1136,76 @@ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) self.start_state = 'enable' self.end_state = 'enable' - self.service_name = 'reload' self.timeout = connection.settings.RELOAD_TIMEOUT - self.dialog = Dialog(reload_statement_list) + self.dialog = Dialog(reload_statement_list + default_statement_list) + self.log_buffer = io.StringIO() self.__dict__.update(kwargs) def call_service(self, reload_command='reload', dialog=Dialog([]), + reply=Dialog([]), timeout=None, return_output=False, reload_creds=None, + raise_on_error=True, + error_pattern=None, + append_error_pattern=None, + post_reload_wait_time = None, *args, **kwargs): + con = self.connection timeout = timeout or self.timeout - fmt_msg = "+++ reloading %s " \ - " with reload_command %s " \ - "and timeout is %s +++" - con.log.debug(fmt_msg % (self.connection.hostname, - reload_command, - timeout)) + syslog_wait = con.settings.SYSLOG_WAIT + con.settings.SYSLOG_WAIT = con.settings.RELOAD_SYSLOG_WAIT + + if error_pattern is None: + self.error_pattern = con.settings.ERROR_PATTERN + else: + self.error_pattern = error_pattern - con.state_machine.go_to(self.end_state, - con.spawn, - prompt_recovery=self.prompt_recovery, - context=self.context) + if post_reload_wait_time is None: + self.post_reload_wait_time = con.settings.POST_RELOAD_WAIT + else: + self.post_reload_wait_time = post_reload_wait_time + + if not isinstance(self.error_pattern, list): + raise ValueError('error_pattern should be a list') + if append_error_pattern: + if not isinstance(append_error_pattern, list): + raise ValueError('append_error_pattern should be a list') + self.error_pattern += append_error_pattern + + lb = UniconStreamHandler(self.log_buffer) + lb.setFormatter(logging.Formatter(fmt=UNICON_LOG_FORMAT)) + self.connection.log.addHandler(lb) + + # Clear log buffer + self.log_buffer.seek(0) + self.log_buffer.truncate() + + fmt_msg = "+++ reloading %s " \ + " with reload_command '%s' " \ + "and timeout is %s seconds +++" + con.log.info(fmt_msg % (self.connection.hostname, reload_command, timeout)) + + if reply: + if dialog: + con.log.warning("**** Both 'reply' and 'dialog' were provided " + "to the reload service. Ignoring 'dialog'.") + dialog = reply + elif dialog: + warnings.warn('**** "dialog" parameter is deprecated. ' + 'Use "reply" instead. ****', + category=DeprecationWarning) if not isinstance(dialog, Dialog): raise SubCommandFailure( "dialog passed must be an instance of Dialog") - dialog = dialog dialog += self.dialog - custom_auth_stmt = custom_auth_statements(con.settings.LOGIN_PROMPT, - con.settings.PASSWORD_PROMPT) + custom_auth_stmt = custom_auth_statements(con.settings.LOGIN_PROMPT, con.settings.PASSWORD_PROMPT) if custom_auth_stmt: dialog += Dialog(custom_auth_stmt) @@ -1014,30 +1215,93 @@ def call_service(self, else: context = self.context + start_time = current_time = datetime.now() + timeout_time = timedelta(seconds=timeout) con.spawn.sendline(reload_command) + try: - reload_output=dialog.process(con.spawn, - timeout=timeout, - prompt_recovery=self.prompt_recovery, - context=context) - con.state_machine.go_to( - 'any', - con.spawn, - context=self.context, - prompt_recovery=self.prompt_recovery, - timeout=con.connection_timeout, - dialog=con.connection_provider.get_connection_dialog() - ) - con.state_machine.go_to('enable', - con.spawn, - prompt_recovery=self.prompt_recovery, - context=self.context) - except Exception as err: - raise SubCommandFailure("Reload failed %s" % err) from err - con.state_machine.get_state(self.end_state) - self.result = True + reload_output = dialog.process(con.spawn, + timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=context) + self.result = reload_output.match_output + self.get_service_result() + except Exception as e: + if hasattr(con.device, 'clean') and hasattr(con.device.clean, 'device_recovery') and\ + con.device.clean.device_recovery.get('golden_image'): + con.log.exception(f"Reload failed to install with file: {getattr(con.device.clean, 'images', [None])[0]}") + con.log.info(f'Booting the device using golden_image.') + con.device.api.device_recovery_boot(golden_image=con.device.clean.device_recovery['golden_image']) + con.log.info('Successfully booted the device using golden_image.') + raise + elif raise_on_error: + raise + else: + con.log.exception(f'Reload failed: {e}') + self.result = False + if not con.connected: + con.disconnect() + for x in range(con.settings.RELOAD_RECONNECT_ATTEMPTS): + con.log.info('Waiting for {} seconds'.format(con.settings.RELOAD_WAIT / (x + 1))) + sleep(con.settings.RELOAD_WAIT / (x + 1)) + try: + con.log.info('Trying to connect... attempt #{}'.format(x + 1)) + con.connect() + except Exception: + con.log.exception('Connection to {} failed'.format(con.hostname)) + self.result = False + if con.is_connected: + self.result = True + break + else: + con.log.info('Waiting for boot messages to settle for {} seconds'.format( + self.post_reload_wait_time + )) + wait_time = timedelta(seconds=self.post_reload_wait_time) + settle_time = current_time = datetime.now() + while (current_time - settle_time) < wait_time: + if buffer_settled(con.spawn, self.post_reload_wait_time): + con.log.info('Buffer settled, accessing device..') + break + current_time = datetime.now() + if (current_time - start_time) > timeout_time: + con.log.info('Time out, trying to acces device..') + break + + # ! This line was added to resolve an issue with HA devices, but was + # ! found to cause further issues with other devices on reload + # TODO Need to find a better way to implement a fix for HA devices + # TODO that does not cause issues with other devices. Likely need to + # TODO modify the state machine and/or dialog processing. + # con.sendline() + try: + con.context = context + con.connection_provider.connect() + self.result = True + except Exception: + if raise_on_error: + raise + else: + con.log.exception('Connection to {} failed'.format(con.hostname)) + self.result = False + + con.settings.SYSLOG_WAIT = syslog_wait + + self.log_buffer.seek(0) + reload_output = self.log_buffer.read() + # clear buffer + self.log_buffer.truncate() + + self.connection.log.removeHandler(lb) + if return_output: - self.result = ReloadResult(self.result, reload_output.match_output.replace(reload_command, '', 1)) + self.result = ReloadResult(self.result, reload_output) + + if self.result: + con.log.info('--- Reload of device {} completed ---'.format(con.hostname)) + else: + con.log.info('--- Reload of device {} failed ---'.format(con.hostname)) + class Traceroute(BaseService): """ Service to issue traceroute response request to another network from device. @@ -1058,18 +1322,17 @@ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) self.start_state = 'enable' self.end_state = 'enable' - self.service_name = 'traceroute' self.timeout = 60 self.dialog = Dialog(trace_route_dialog_list) self.__dict__.update(kwargs) - def call_service(self, addr, command="traceroute", timeout = None, - error_pattern=None, **kwargs): + def call_service(self, addr, command="traceroute", timeout=None, error_pattern=None, **kwargs): con = self.connection con.log.debug("+++ traceroute +++") + traceroute_options = ['addr', 'proto', 'ingress', 'source', 'dscp', 'numeric', 'timeout', 'probe', 'minimum_ttl', 'maximum_ttl', - 'port', 'style', 'resolve_as_number' ] + 'port', 'style', 'resolve_as_number'] if error_pattern is None: self.error_pattern = con.settings.TRACEROUTE_ERROR_PATTERN @@ -1087,7 +1350,10 @@ def call_service(self, addr, command="traceroute", timeout = None, # src_route_addr keys. # The EAL backend requires all commands to be of string type. for key in kwargs: - trace_route_context[key] = str(kwargs[key]) + if key in traceroute_options: + trace_route_context[key] = str(kwargs[key]) + else: + con.log.warning("Unsupported traceroute option {}, ignoring".format(key)) # Validate Inputs if addr: @@ -1097,19 +1363,15 @@ def call_service(self, addr, command="traceroute", timeout = None, trace_route_context['addr'] = str(addr) else: raise SubCommandFailure("Address is not specified ") - + # Stringify the command in case it is an object. trace_route_str = str(command) dialog = self.service_dialog(service_dialog=self.dialog) spawn = self.get_spawn() - sm = self.get_sm() spawn.sendline(trace_route_str) try: - self.result = dialog.process( - spawn, context=trace_route_context, - timeout=timeout) - + self.result = dialog.process(spawn, context=trace_route_context, timeout=timeout) except TimeoutError: # Recover prompt and re-raise # Ctrl+shift+6 @@ -1122,8 +1384,7 @@ def call_service(self, addr, command="traceroute", timeout = None, self.result = self.result.match_output if self.result.rfind(self.connection.hostname): - self.result = self.result[ - :self.result.rfind(self.connection.hostname)] + self.result = self.result[:self.result.rfind(self.connection.hostname)] class Ping(BaseService): @@ -1147,7 +1408,6 @@ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) self.start_state = 'enable' self.end_state = 'enable' - self.service_name = 'ping' self.timeout = 60 self.dialog = Dialog(extended_ping_dialog_list) # Ping error Patterns @@ -1168,51 +1428,107 @@ def __init__(self, connection, context, **kwargs): self.__dict__.update(kwargs) - def call_service(self, addr, command="ping", timeout = None, **kwargs): + def call_service(self, addr, command="ping", timeout=None, **kwargs): # noqa: C901 con = self.connection con.log.debug("+++ ping +++") + + # Extended ping options + # If one of these is passed, set 'extd_ping' to 'y' automatically + ext_ping_options = [ + 'data_pat', + 'df_bit', + 'dscp', + 'exp', + 'extended_verbose', + 'force_exp_null_label', + 'ingress_int', + 'interface', + 'pad', + 'precedence', + 'record_hops', + 'reply_mode', + 'source', + 'src_route_addr', + 'src_route_type', + 'sweep_interval', + 'sweep_max', + 'sweep_min', + 'sweep_ping', + 'timestamp_count', + 'tos', + 'ttl', + 'udp', + 'validate_reply_data', + 'verbose', + ] + # Ping Options - ping_options = ['multicast', 'transport', 'mask', 'vcid', 'tunnel', - 'dest_start', 'dest_end', 'exp', 'pad', 'ttl', - 'reply_mode', 'dscp', 'proto', 'count', 'size', - 'verbose', 'interval', 'timeout_limit', - 'send_interval', 'vrf', 'src_route_type', - 'src_route_addr', 'extended_verbose', 'topo', - 'validate_reply_data', 'force_exp_null_label', - 'lsp_ping_trace_rev', 'oif', 'tos', 'data_pat', - 'int', 'udp', 'precedence', 'novell_type', - 'extended_timeout_limit', 'sweep_min', 'sweep_max', - 'sweep_interval', 'src_addr', 'df_bit', - 'ipv6_ext_headers', 'ipv6_hbh_headers', - 'ipv6_dst_headers', 'ping_packet_timeout', - 'sweep_ping', 'timestamp_count', 'record_hops', - 'ping_failures', 'extd_ping', 'addr' - ] + ping_options = [ + 'multicast', 'transport', 'mask', 'vcid', 'tunnel', + 'dest_start', 'dest_end', + 'proto', 'count', 'size', + 'interval', 'timeout_limit', + 'send_interval', 'vrf', 'topo', + 'lsp_ping_trace_rev', 'oif', + 'novell_type', 'extd_ping', + 'extended_timeout_limit', + 'ipv6_ext_headers', 'ipv6_hbh_headers', + 'ipv6_dst_headers', 'ping_packet_timeout', + 'ping_failures', 'addr' + ] + ext_ping_options # Default value setting timeout = timeout or self.timeout + # Prepare ping context + # set some default values ping_context = AttributeDict({}) for a in ping_options: - if a is "novell_type": + if a == "novell_type": ping_context[a] = "\r" - elif a is "sweep_ping": + elif a == "sweep_ping": ping_context[a] = "n" - elif a is 'extd_ping': + elif a == 'extd_ping': ping_context[a] = "n" else: ping_context[a] = "" + # old to new argument mapping + deprecated_arg_map = { + 'int': 'interface', + 'src_addr': 'source' + } # Read input values passed # Convert to string in case users pass in non-string types such as # integer for repeat_count or ipaddress for addr, src_addr or # src_route_addr keys. # The EAL backend requires all commands to be of string type. for key in kwargs: - ping_context[key] = str(kwargs[key]) + + # if one of the extended ping options is given, + # automatically set extd_ping to y. + # If extd_ping is explicitly set to 'n', + # it will be set by logic below + if key in ext_ping_options: + ping_context['extd_ping'] = 'y' + + if key in deprecated_arg_map: + con.log.warning( + 'ping service "{key}" argument is deprecated, ' + 'please use "{new_key}" instead'.format( + key=key, + new_key=deprecated_arg_map.get(key) + )) + old_key = key + key = deprecated_arg_map.get(key) + ping_context[key] = str(kwargs[old_key]) + else: + # this also sets extd_ping to 'n' + # if provided by user + ping_context[key] = str(kwargs[key]) # Validate Inputs - if ping_context['addr'] is "": + if ping_context['addr'] == "": if addr: # Do string conversion on addr, if specified, # in case the user passes in an ipaddress object instead of a @@ -1221,47 +1537,49 @@ def call_service(self, addr, command="ping", timeout = None, **kwargs): else: raise SubCommandFailure("Address is not specified ") - if ping_context['src_route_type'] is not "": + if ping_context['src_route_type'] != "": if ping_context['src_route_addr'] in "": raise SubCommandFailure("If src route type is set, " "then src route addr is mandatory \n") - elif ping_context['src_route_addr'] is not "": + elif ping_context['src_route_addr'] != "": raise SubCommandFailure("If src route addr is set, " "then src route type is mandatory \n") + # Stringify the command in case it is an object. ping_str = str(command) - if ping_context['topo'] is not "": + # If only the address is passed, ping it directly + if not kwargs: + ping_str += ' {}'.format(addr) + + if ping_context['topo'] != "": ping_str = ping_str + " topo " + ping_context['topo'] + handle = self.get_handle() spawn = self.get_spawn() - sm = self.get_sm() if ping_context['extd_ping'].lower().startswith('y'): - if self.connection.is_ha: - dialog = self.service_dialog(service_dialog=self.dialog, - handle=con.active) - else: - dialog = self.service_dialog(service_dialog=self.dialog) + dialog = self.service_dialog( + handle=handle, service_dialog=self.dialog) else: - if self.connection.is_ha: - dialog = self.service_dialog( - service_dialog=Dialog(ping_dialog_list), - handle=con.active) - else: - dialog = self.service_dialog( - service_dialog=Dialog(ping_dialog_list)) + dialog = self.service_dialog( + handle=handle, service_dialog=Dialog(ping_dialog_list)) spawn.sendline(ping_str) try: self.result = dialog.process( spawn, context=ping_context, timeout=timeout) except Exception as err: + # catch the prompt before raising an exception + # this uses 'any' state and not 'end_state' + # on purpose, this works best with real devices. + handle.state_machine.go_to('any', + handle.spawn, + context=self.context) raise SubCommandFailure("Ping failed", err) from err self.result = self.result.match_output if self.result.rfind(self.connection.hostname): - self.result = self.result[ - :self.result.rfind(self.connection.hostname)] + self.result = self.result[:self.result.rfind(self.connection.hostname)] class Copy(BaseService): @@ -1299,7 +1617,6 @@ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) self.start_state = 'enable' self.end_state = 'enable' - self.service_name = 'copy' self.timeout = 100 self.dialog = Dialog(copy_statement_list) self.copy_pat = CopyPatterns() @@ -1308,7 +1625,7 @@ def __init__(self, connection, context, **kwargs): if not hasattr(self, 'max_attempts'): self.max_attempts = self.connection.settings.MAX_COPY_ATTEMPTS - def call_service(self, reply=Dialog([]), *args, **kwargs): + def call_service(self, reply=Dialog([]), *args, **kwargs): # noqa: C901 con = self.connection # Inputs supported copy_options = ['source', 'dest', 'dest_file', 'source_file', @@ -1319,17 +1636,19 @@ def call_service(self, reply=Dialog([]), *args, **kwargs): # Default values copy_context = AttributeDict({}) for a in copy_options: - if a is "partition": + if a == "partition": copy_context[a] = 0 - elif a is "erase": + elif a == "erase": copy_context[a] = "n" - elif a is 'overwrite': - copy_context[a] = True - elif a is 'vrf': + elif a == 'overwrite': + # To Handle overwrite = False condition + overwrite = kwargs.get('overwrite', True) + copy_context[a] = overwrite + elif a == 'vrf': copy_context[a] = "Mgmt-intf" - elif a is 'timeout': + elif a == 'timeout': copy_context[a] = self.timeout - elif a is 'password': + elif a == 'password': password = kwargs.pop('password', None) if password: copy_context[a] = to_plaintext(password) @@ -1350,11 +1669,11 @@ def call_service(self, reply=Dialog([]), *args, **kwargs): self.max_attempts = kwargs['max_attempts'] # Validate input - if copy_context['source'] is "" or copy_context['dest'] is "": + if copy_context['source'] == "" or copy_context['dest'] == "": raise SubCommandFailure( "Source and Destination must be specified ") - if copy_context['source_file'] is "": + if copy_context['source_file'] == "": copy_context['source_file'] = copy_context['source'] remote_source = "" remote_dest = "" @@ -1365,38 +1684,32 @@ def call_service(self, reply=Dialog([]), *args, **kwargs): if copy_match: remote_dest = copy_match.group() - if remote_dest is not "" or remote_source is not "": + if remote_dest != "" or remote_source != "": match_server = "" src_server_match = re.search(self.copy_pat.addr_in_remote, copy_context['source']) dest_server_match = re.search(self.copy_pat.addr_in_remote, copy_context['dest']) if src_server_match or dest_server_match: - try: - match_server = src_server_match.group(2) - ipaddress.ip_address(match_server) - except Exception: - try: - match_server = dest_server_match.group(2) - ipaddress.ip_address(match_server) - except Exception: - match_server = "" - if copy_context['server'] is "": - if match_server is "": + try: + match_server = src_server_match.group(2) + ipaddress.ip_address(match_server) + except Exception: + try: + match_server = dest_server_match.group(2) + ipaddress.ip_address(match_server) + except Exception: + match_server = "" + if copy_context['server'] == "": + if match_server == "": raise SubCommandFailure( "Server address must be specified for remote copy") else: copy_context['server'] = match_server timeout = copy_context['timeout'] or self.timeout - # get spawn for ha/nan ha handle - if self.connection.is_ha: - dialog = self.service_dialog(handle=con.active, - service_dialog=self.dialog) - handle = con.active - spawn = con.active.spawn - else: - dialog = self.service_dialog(service_dialog=self.dialog) - handle = con - spawn = con.spawn + + handle = self.get_handle() + spawn = self.get_spawn() + dialog = self.service_dialog(handle=handle, service_dialog=self.dialog) dialog = reply + dialog @@ -1414,6 +1727,9 @@ def call_service(self, reply=Dialog([]), *args, **kwargs): for retry_num in range(self.max_attempts): spawn.sendline(copy_string) try: + if (sleep_time := kwargs.get('sleep_time')): + con.log.info(f"sleep for {sleep_time} seconds") + time.sleep(sleep_time) self.result = dialog.process(spawn, context=copy_context, timeout=timeout) @@ -1424,7 +1740,7 @@ def call_service(self, reply=Dialog([]), *args, **kwargs): handle.state_machine.go_to('any', handle.spawn, context=self.context) - except: + except Exception: pass if retry_num != (self.max_attempts - 1): # wait for a prompt before retry @@ -1446,7 +1762,7 @@ def call_service(self, reply=Dialog([]), *args, **kwargs): handle.state_machine.go_to('any', handle.spawn, context=self.context) - except: + except Exception: pass if retry_num != (self.max_attempts - 1): # wait for a prompt before retry @@ -1464,7 +1780,7 @@ def call_service(self, reply=Dialog([]), *args, **kwargs): handle.state_machine.go_to('any', handle.spawn, context=self.context) - except: + except Exception: pass raise SubCommandFailure("Copy failed", err) from err else: @@ -1472,8 +1788,7 @@ def call_service(self, reply=Dialog([]), *args, **kwargs): self.result = self.result.match_output if self.result.rfind(self.connection.hostname): - self.result = self.result[ - :self.result.rfind(self.connection.hostname)] + self.result = self.result[:self.result.rfind(self.connection.hostname)] class GetMode(BaseService): @@ -1492,12 +1807,10 @@ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) self.start_state = 'enable' self.end_state = 'enable' - self.service_name = 'get_mode' self.timeout = connection.settings.EXEC_TIMEOUT self.__dict__.update(kwargs) def call_service(self, - target='active', timeout=None, utils=utils, *args, @@ -1506,7 +1819,7 @@ def call_service(self, timeout = timeout or self.timeout try: self.result = utils.get_redundancy_details(self.connection, - timeout=timeout) + timeout=timeout) except Exception as err: raise SubCommandFailure("get_mode failed", err) from err @@ -1541,7 +1854,6 @@ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) self.start_state = 'enable' self.end_state = 'enable' - self.service_name = 'get_rp_state' self.timeout = connection.settings.EXEC_TIMEOUT self.__dict__.update(kwargs) @@ -1552,14 +1864,14 @@ def call_service(self, *args, **kwargs): """send the command on the right rp and return the output""" - handle = 'my' - if target is 'standby': - handle = 'peer' + handle = self.get_handle(target) + + red_handle = 'my' + if handle.alias == self.connection.standby.alias: + red_handle = 'peer' try: - self.result = utils.get_redundancy_details(self.connection, - timeout=timeout, - who=handle) + self.result = utils.get_redundancy_details(self.connection, timeout=timeout, who=red_handle) except Exception as err: raise SubCommandFailure("get_rp_state failed", err) from err @@ -1593,7 +1905,6 @@ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) self.start_state = 'enable' self.end_state = 'enable' - self.service_name = 'get_config' self.timeout = connection.settings.EXEC_TIMEOUT self.__dict__.update(kwargs) @@ -1632,7 +1943,6 @@ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) self.start_state = 'enable' self.end_state = 'enable' - self.service_name = 'sync_state' self.timeout = connection.settings.EXEC_TIMEOUT self.__dict__.update(kwargs) @@ -1650,8 +1960,7 @@ def call_service(self, # ToDo: Missing code to bring the device to stable state self.result = con.connection_provider.designate_handles() except Exception as err: - raise SubCommandFailure("Failed to bring the device to stable \ - state", err) from err + raise SubCommandFailure("Failed to bring the device to stable state") from err self.result = True def get_service_result(self): @@ -1693,12 +2002,11 @@ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) self.start_state = 'any' self.end_state = 'any' - self.service_name = 'execute' - self.timeout = connection.settings.EXEC_TIMEOUT self.__dict__.update(kwargs) self.dialog = Dialog(execution_statement_list) - self.matched_retries = connection.settings.EXECUTE_MATCHED_RETRIES - self.matched_retry_sleep = connection.settings.EXECUTE_MATCHED_RETRY_SLEEP + + def log_service_call(self): + pass def pre_service(self, *args, **kwargs): self.prompt_recovery = kwargs.get('prompt_recovery', False) @@ -1719,221 +2027,69 @@ def call_service(self, command, **kwargs): """send the command on the right rp and return the output""" # create an alias for connection. - con = self.connection - # timeout should not be in init because we don't want it - # to get constructed. User may change the exec timeout and expect it - # to take effect. - timeout = timeout or self.timeout - if allow_state_change is None: - allow_state_change = con.settings.EXEC_ALLOW_STATE_CHANGE + handle = self.get_handle(target) - if error_pattern is None: - self.error_pattern = con.settings.ERROR_PATTERN - else: - self.error_pattern = error_pattern + self.result = handle.execute(command, + reply=reply, + timeout=timeout, + error_pattern=error_pattern, + search_size=search_size, + allow_state_change=allow_state_change, + matched_retries=matched_retries, + matched_retry_sleep=matched_retry_sleep, + *args, + **kwargs) - if target is 'active': - handle = con.active - elif target is 'standby': - handle = con.standby - elif target is 'a': - handle = con.a - elif target is 'b': - handle = con.b - # user specified search buffer size - if search_size is not None: - handle.spawn.search_size = search_size - else: - handle.spawn.search_size = con.settings.SEARCH_SIZE +class HaConfigureService(Configure): + """DualRp configure service + Service to configure device with single or list of `commands`. - matched_retries = self.matched_retries \ - if matched_retries is None else matched_retries - matched_retry_sleep = self.matched_retry_sleep \ - if matched_retry_sleep is None else matched_retry_sleep + Config without config_command will take device to config mode. + Commands Should be list, if `config_command` are more than one. + reply option can be passed for the interactive config command. + Command can be executed on standby by pass in target as standby. - if not isinstance(reply, Dialog): - raise SubCommandFailure( - "dialog passed via 'reply' must be an instance of Dialog") + Arguments: + commands : list/single config command + reply: Addition Dialogs for interactive config commands. + timeout : Timeout value in sec, Default Value is 30 sec + target: Target RP where to execute service + lock_retries: retry times if config mode is locked, default is 0 + lock_retry_sleep: sleep between retries, default is 2 sec + bulk: send commands in one sendline or not, default is False - sm = handle.state_machine + Returns: + command output on Success, raise SubCommandFailure on failure - # service_dialog overrides the default execution dialogs - if 'service_dialog' in kwargs: - service_dialog = kwargs['service_dialog'] - if service_dialog is None: - service_dialog = Dialog([]) - if not isinstance(service_dialog, Dialog): - raise SubCommandFailure( - "dialog passed via 'service_dialog' must be an instance of Dialog") + Example: + .. code-block:: python - dialog = self.service_dialog( - service_dialog=service_dialog + reply, - handle=handle, - matched_retries=matched_retries, - matched_retry_sleep=matched_retry_sleep - ) - else: - dialog = self.dialog + self.service_dialog( - service_dialog=reply, - handle=handle, - matched_retries=matched_retries, - matched_retry_sleep=matched_retry_sleep - ) + output = rtr.configure() + output = rtr.configure('no logging console') + cmd =['hostname si-tvt-7200-28-41', 'no logging console'] + output = rtr.configure(cmd) + output = rtr.configure(cmd, target='standby') + """ + pass - # add default execution statements - custom_auth_stmt = custom_auth_statements(con.settings.LOGIN_PROMPT, - con.settings.PASSWORD_PROMPT) - if custom_auth_stmt: - dialog += Dialog(custom_auth_stmt) - # Add all known states to detect state changes. - for state in sm.states: - # The current state is already added by the service_dialog method - if state.name != sm.current_state: - if allow_state_change: - dialog.append(Statement( - pattern=state.pattern, - matched_retries=matched_retries, - matched_retry_sleep=matched_retry_sleep - )) - else: - dialog.append(Statement( - pattern=state.pattern, - action=exec_state_change_action, - args={'err_state': state, 'sm': sm}, - matched_retries=matched_retries, - matched_retry_sleep=matched_retry_sleep - )) +class HaConfigure(HaConfigureService): - if isinstance(command, str): - if len(command) == 0: - commands = [''] - else: - commands = command.splitlines() - elif isinstance(command, list): - commands = command - else: - raise ValueError('Command passed is not of type string or list (%s)' % type(command)) + def call_service(self, *args, **kwargs): + self.connection.log.warning('**** This service is deprecated. ' + + 'Please use "configure" service ****') + super().call_service(*args, **kwargs) - if con.settings.IGNORE_CHATTY_TERM_OUTPUT: - # clear buffer log messages - chatty_term_wait(handle.spawn, trim_buffer=True) - command_output = {} - for command in commands: - con.log.info("+++ %s: executing command '%s' +++" - % (self.connection.hostname, command)) - - handle.spawn.sendline(command) - try: - dialog_match = dialog.process( - handle.spawn, - timeout=timeout, - prompt_recovery=self.prompt_recovery, - context=con.context - ) - if dialog_match: - self.result = dialog_match.match_output - self.result = self.get_service_result() - except StateMachineError: - raise - except Exception as err: - raise SubCommandFailure("Command execution failed", err) from err - - sm.detect_state(handle.spawn) - - if self.result: - output = utils.truncate_trailing_prompt( - sm.get_state(sm.current_state), - self.result, - hostname=self.connection.hostname, - result_match=dialog_match, - ) - output = self.extra_output_process(output) - output = output.replace(command, "", 1) - output = re.sub(r"^\r?\r\n", "", output, 1) - output = output.rstrip() - - if command in command_output: - if isinstance(command_output[command], list): - command_output[command].append(output) - else: - command_output[command] = [command_output[command], output] - else: - command_output[command] = output - - if len(command_output) == 1: - self.result = list(command_output.values())[0] - else: - self.result = command_output - - # revert search size to default - handle.spawn.search_size = con.settings.SEARCH_SIZE - - if self.end_state != 'any': - sm.go_to(self.end_state, - handle.spawn, - prompt_recovery=self.prompt_recovery, - context=self.connection.context) - - def extra_output_process(self, output): - # remove backspace and ansi escape sequence from output - # scenario 1: it prevents correct command replacement, on linux terminals - # scenario 2: router with '\x1b[KSomething\x08 \x08\x08 \x08\x08 \x08\x08 \x08\x08\x1b[K' - return utils.remove_backspace_ansi_escape(output) - - -class HaConfigureService(Configure): - """DualRp configure service - Service to configure device with single or list of `commands`. - - Config without config_command will take device to config mode. - Commands Should be list, if `config_command` are more than one. - reply option can be passed for the interactive config command. - Command can be executed on standby by pass in target as standby. - - Arguments: - commands : list/single config command - reply: Addition Dialogs for interactive config commands. - timeout : Timeout value in sec, Default Value is 30 sec - target: Target RP where to execute service - lock_retries: retry times if config mode is locked, default is 0 - lock_retry_sleep: sleep between retries, default is 2 sec - bulk: send commands in one sendline or not, default is False - - Returns: - command output on Success, raise SubCommandFailure on failure - - Example: - .. code-block:: python - - output = rtr.configure() - output = rtr.configure('no logging console') - cmd =['hostname si-tvt-7200-28-41', 'no logging console'] - output = rtr.configure(cmd) - output = rtr.configure(cmd, target='standby') - """ - pass - - -class HaConfigure(HaConfigureService): - - def call_service(self, *args, **kwargs): - self.connection.log.warn('**** This service is deprecated. ' + - 'Please use "configure" service ****') - super().call_service(*args, **kwargs) - - -# TODO Add option to take additional dialog -class HAReloadService(BaseService): - """ Service to reload the device. +# TODO Add option to take additional dialog +class HAReloadService(BaseService): + """ Service to reload the device. Arguments: reload_command: reload command to be used. default "redundancy reload shelf" reload_creds: credential or list of credentials to use to respond to username/password prompts. - target: Target RP where to execute service reply: Additional Dialog( i.e patterns) to be handled timeout: Timeout value in sec, Default Value is 60 sec return_output: if True, return namedtuple with result and reload output @@ -1953,65 +2109,109 @@ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) self.start_state = 'enable' self.end_state = 'enable' - self.service_name = 'reload' self.timeout = connection.settings.HA_RELOAD_TIMEOUT - self.dialog = Dialog(ha_reload_statement_list) + self.dialog = Dialog(ha_reload_statement_list + default_statement_list) self.command = 'reload' + self.log_buffer = io.StringIO() self.__dict__.update(kwargs) - def call_service(self, command=None, - reload_command = None, + def call_service(self, # noqa: C901 + reload_command=None, + command=None, dialog=Dialog([]), + reply=Dialog([]), target='active', timeout=None, return_output=False, reload_creds=None, + target_standby_state='STANDBY HOT', + error_pattern = None, + append_error_pattern= None, *args, **kwargs): + + con = self.connection + + if error_pattern is None: + self.error_pattern = con.settings.ERROR_PATTERN + else: + self.error_pattern = error_pattern + + if not isinstance(self.error_pattern, list): + raise ValueError('error_pattern should be a list') + if append_error_pattern: + if not isinstance(append_error_pattern, list): + raise ValueError('append_error_pattern should be a list') + self.error_pattern += append_error_pattern + + lb = UniconStreamHandler(self.log_buffer) + lb.setFormatter(logging.Formatter(fmt=UNICON_LOG_FORMAT)) + self.connection.log.addHandler(lb) + + # logging the output to subconnections + for subcon in con.subconnections: + subcon.log.addHandler(lb) + + # Clear log buffer + self.log_buffer.seek(0) + self.log_buffer.truncate() + + if reply: + if dialog: + con.log.warning("**** Both 'reply' and 'dialog' were provided " + "to the reload service. Ignoring 'dialog'.") + dialog = reply + elif dialog: + warnings.warn('**** "dialog" parameter is deprecated. ' + 'Use "reply" instead. ****', + category=DeprecationWarning) + timeout = timeout or self.timeout if command: - con.log.warning("*** HA reload() service 'command' parameter \ -will be deprecated in next release. Please use 'reload_command' parameter ***") + con.log.warning("*** HA reload() service 'command' parameter " + "will be deprecated in next release. " + "Please use 'reload_command' parameter ***") if command and reload_command: - raise SubCommandFailure("Please use either 'command' or 'reload_command' parameter") + raise SubCommandFailure( + "Please use either 'command' or 'reload_command' parameter") command = command or reload_command or self.command # TODO counter value must be moved to settings counter = 0 - config_retry = 0 - fmt_str = "+++ reloading %s with reload_command %s and timeout is %s +++" - con.log.debug(fmt_str % (con.hostname, command, timeout)) - dialog = dialog + fmt_str = "+++ reloading %s with reload_command '%s' and timeout is %s +++" + con.log.info(fmt_str % (con.hostname, command, timeout)) dialog += self.dialog - dialog = self.service_dialog(handle=con.active, - service_dialog=dialog) - custom_auth_stmt = custom_auth_statements(con.settings.LOGIN_PROMPT, - con.settings.PASSWORD_PROMPT) - if custom_auth_stmt: - dialog += Dialog(custom_auth_stmt) - con.active.state_machine.go_to('enable', - self.connection.active.spawn, - prompt_recovery=self.prompt_recovery, - context=self.context) + custom_auth_stmt = custom_auth_statements(con.settings.LOGIN_PROMPT, con.settings.PASSWORD_PROMPT) if reload_creds: - context = self.context.copy() + context = con.active.context.copy() context.update(cred_list=reload_creds) + sby_context = con.standby.context.copy() + sby_context.update(cred_list=reload_creds) else: - context = self.context + context = con.active.context + sby_context = con.standby.context + + if custom_auth_stmt: + dialog += Dialog(custom_auth_stmt) # Issue reload command con.active.spawn.sendline(command) try: - reload_output=dialog.process(con.active.spawn, - context=context, - prompt_recovery=self.prompt_recovery, - timeout=timeout) + reload_output = dialog.process(con.active.spawn, + context=context, + prompt_recovery=self.prompt_recovery, + timeout=timeout) + self.result=reload_output.match_output + + self.get_service_result() + con.active.state_machine.go_to('any', con.active.spawn, prompt_recovery=self.prompt_recovery, - context=self.context) + timeout=con.connection_timeout, + context=context) # Bring standby to good state. con.log.info('Waiting for config sync to finish') @@ -2024,7 +2224,7 @@ def call_service(self, command=None, con.standby.state_machine.go_to( 'any', con.standby.spawn, - context=context, + context=sby_context, timeout=standby_wait_interval, prompt_recovery=self.prompt_recovery, dialog=con.connection_provider.get_connection_dialog() @@ -2033,54 +2233,89 @@ def call_service(self, command=None, except Exception as err: if round == standby_sync_try - 1: raise Exception( - 'Bringing standby to any state failed within {} sec' - .format(standby_wait_time)) from err + 'Bringing standby to any state failed within {} sec'.format(standby_wait_time)) from err + + # If standby is in rommon, use state machine to transition to disable state + if con.standby.state_machine.current_state == 'rommon': + con.log.info('Standby is in ROMMON state, transitioning to disable mode') + con.standby.state_machine.go_to( + 'disable', + con.standby.spawn, + context=sby_context, + timeout=timeout, + prompt_recovery=self.prompt_recovery, + dialog=con.connection_provider.get_connection_dialog() + ) except Exception as err: - raise SubCommandFailure("Reload failed : %s" % err) from err + if hasattr(con.device, 'clean') and hasattr(con.device.clean, 'device_recovery') and\ + con.device.clean.device_recovery.get('golden_image'): + con.log.error(f'Reload failed booting device using golden image: {con.device.clean.device_recovery["golden_image"]}') + con.device.api.device_recovery_boot(golden_image=con.device.clean.device_recovery['golden_image']) + con.log.info(f'Successfully booted the device using golden image.') + raise SubCommandFailure(f"Reload failed : {err}") # Re-designate handles before applying config. - self.connection.connection_provider.designate_handles() + # Roles could have switched as a result of the reload. + con.connection_provider.designate_handles() + con.connection_provider.unlock_standby() + + con.active.state_machine.go_to('enable', + con.active.spawn, + prompt_recovery=self.prompt_recovery, + context=context) # Issue init commands to disable console logging - exec_commands = self.connection.settings.HA_INIT_EXEC_COMMANDS + exec_commands = con.active.settings.HA_INIT_EXEC_COMMANDS for exec_command in exec_commands: con.execute(exec_command, prompt_recovery=self.prompt_recovery) - config_commands = self.connection.settings.HA_INIT_CONFIG_COMMANDS - while config_retry < \ - self.connection.settings.CONFIG_POST_RELOAD_MAX_RETRIES: - try: - con.configure(config_commands, timeout=60, prompt_recovery=self.prompt_recovery) - except Exception as err: - if re.search("Config mode cannot be entered", - str(err)): - sleep(self.connection.settings.\ - CONFIG_POST_RELOAD_RETRY_DELAY_SEC) - con.active.spawn.sendline() - config_retry += 1 - else: - config_retry = 21 + config_commands = con.active.settings.HA_INIT_CONFIG_COMMANDS + + config_lock_retries_ori = con.settings.CONFIG_LOCK_RETRIES + config_lock_retry_sleep_ori = con.settings.CONFIG_LOCK_RETRY_SLEEP + con.active.settings.CONFIG_LOCK_RETRY_SLEEP = con.active.settings.CONFIG_POST_RELOAD_RETRY_DELAY_SEC + con.active.settings.CONFIG_LOCK_RETRIES = con.active.settings.CONFIG_POST_RELOAD_MAX_RETRIES + + try: + con.configure(config_commands, + target='active', + prompt_recovery=self.prompt_recovery) + except Exception: + raise + finally: + con.settings.CONFIG_LOCK_RETRIES = config_lock_retries_ori + con.settings.CONFIG_LOCK_RETRY_SLEEP = config_lock_retry_sleep_ori # best effort for 'STANDBY HOT', consider argument for mandatory/ignore while counter < 31: try: # TODO need fix iosxr get_rp_state service rp_state = con.get_rp_state(target='standby', timeout=30) - if rp_state.find('STANDBY HOT') != -1: + if rp_state.find(target_standby_state) != -1: + con.log.info('Standby RP State: {}'.format(rp_state)) counter = 32 else: + con.log.info('Standby RP State: {}, waiting for {}'.format(rp_state, target_standby_state)) sleep(6) counter += 1 - except Exception: + except Exception as err: + con.log.error('Failed to get RP state: {}'.format(err)) sleep(6) counter += 1 - con.disconnect() - con.connect() - con.log.debug("+++ Reload Completed Successfully +++") + con.log.info("+++ Reload Completed Successfully +++") + self.log_buffer.seek(0) + reload_output = self.log_buffer.read() + # clear buffer + self.log_buffer.truncate() + + self.connection.log.removeHandler(lb) + for subcon in con.subconnections: + subcon.log.removeHandler(lb) + self.result = True if return_output: - self.result = ReloadResult(self.result, reload_output.match_output.replace(command, '', 1)) + self.result = ReloadResult(self.result, reload_output) class SwitchoverService(BaseService): @@ -2112,14 +2347,14 @@ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) self.start_state = 'enable' self.end_state = 'enable' - self.service_name = 'switchover' self.timeout = connection.settings.SWITCHOVER_TIMEOUT self.dialog = Dialog(switchover_statement_list) self.command = 'redundancy force-switchover' self.__dict__.update(kwargs) - def call_service(self, command=None, + def call_service(self, command=None, # noqa: C901 dialog=Dialog([]), + reply=Dialog([]), timeout=None, sync_standby=True, switchover_creds=None, @@ -2127,6 +2362,16 @@ def call_service(self, command=None, **kwargs): # create an alias for connection. con = self.connection + if reply: + if dialog: + con.log.warning("**** Both 'reply' and 'dialog' were provided " + "to the reload service. Ignoring 'dialog'.") + dialog = reply + elif dialog: + warnings.warn('**** "dialog" parameter is deprecated.' + ' Please use "reply" ****', + category=DeprecationWarning) + timeout = timeout or self.timeout command = command or self.command switchover_counter = con.settings.SWITCHOVER_COUNTER @@ -2142,20 +2387,20 @@ def call_service(self, command=None, # Save current active and standby handle details standby_start_cmd = con.standby.start - dialog = dialog dialog += self.dialog dialog = self.service_dialog(handle=con.active, service_dialog=dialog) - custom_auth_stmt = custom_auth_statements(con.settings.LOGIN_PROMPT, - con.settings.PASSWORD_PROMPT) + custom_auth_stmt = custom_auth_statements(con.settings.LOGIN_PROMPT, con.settings.PASSWORD_PROMPT) if custom_auth_stmt: dialog += Dialog(custom_auth_stmt) + # Use the standby credentials when processing because any + # authentication request is expected to come from the new active. if switchover_creds: - context = self.context.copy() + context = con.standby.context.copy() context.update(cred_list=switchover_creds) else: - context = self.context + context = con.standby.context # Issue switchover command con.active.spawn.sendline(command) @@ -2169,14 +2414,8 @@ def call_service(self, command=None, except SubCommandFailure as err: raise SubCommandFailure("Switchover Failed %s" % str(err)) from err - # Initialise Standby - try: - con.standby.spawn.sendline("\r") - con.standby.spawn.expect(".*") - con.swap_roles() - except Exception as err: - raise SubCommandFailure("Failed to initialise the standby", - err) from err + # swap roles after switchover + con.swap_roles() counter = 0 if not sync_standby: @@ -2207,7 +2446,7 @@ def call_service(self, command=None, con.active.state_machine.go_to( 'any', con.active.spawn, - context=self.context, + context=context, prompt_recovery=self.prompt_recovery, timeout=con.connection_timeout, dialog=con.connection_provider.get_connection_dialog() @@ -2215,7 +2454,7 @@ def call_service(self, command=None, con.active.state_machine.go_to( 'enable', con.active.spawn, - context=self.context, + context=context, prompt_recovery=self.prompt_recovery ) @@ -2224,23 +2463,24 @@ def call_service(self, command=None, for command in exec_commands: con.execute(command, prompt_recovery=self.prompt_recovery) config_commands = self.connection.settings.HA_INIT_CONFIG_COMMANDS - config_retry = 0 - while config_retry < 20: + con.configure(config_commands, prompt_recovery=self.prompt_recovery) + + # Try to determine state for to standby node, + # if first attempt fails, sendline and check again + for _ in range(2): + # Determine standby state try: - con.configure(config_commands, timeout=60, prompt_recovery=self.prompt_recovery) - except Exception as err: - if re.search("Config mode cannot be entered", - str(err)): - sleep(9) - con.active.spawn.sendline() - config_retry += 1 - else: - config_retry = 21 + con.standby.state_machine.go_to('any', + con.standby.spawn, + context=con.standby.context, + dialog=con.connection_provider.get_connection_dialog()) + break + except Exception: + con.log.error("Failed to bring standby rp to any state") + con.standby.spawn.sendline() + else: + raise Exception("Failed to bring standby rp to any state") - # Clear Standby buffer - con.standby.spawn.sendline("\r") - con.standby.spawn.expect(".*") - con.standby.state_machine.go_to('any', con.standby.spawn, context=con.context) con.enable(target='standby') # Verify switchover is Successful if con.active.start == standby_start_cmd: @@ -2257,7 +2497,7 @@ class ResetStandbyRP(BaseService): Arguments: command: command to reset standby, default is"redundancy reload peer" - dialog: Dialog which include list of Statements for + reply: Dialog which include list of Statements for additional dialogs prompted by standby reset command, in-case it is not in the current list. timeout: Timeout value in sec, Default Value is 500 sec @@ -2279,12 +2519,12 @@ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) self.start_state = 'enable' self.end_state = 'enable' - self.service_name = 'reset_standby_rp' self.timeout = connection.settings.HA_RELOAD_TIMEOUT self.dialog = Dialog(standby_reset_rp_statement_list) self.__dict__.update(kwargs) def pre_service(self, *args, **kwargs): + self.prompt_recovery = kwargs.get('prompt_recovery', False) if self.connection.is_connected: return elif self.connection.reconnect: @@ -2302,7 +2542,7 @@ def post_service(self, *args, **kwargs): self.connection.active.spawn, context=self.connection.context) - def call_service(self, command='redundancy reload peer', + def call_service(self, command='redundancy reload peer', # noqa: C901 reply=Dialog([]), timeout=None, *args, @@ -2315,19 +2555,24 @@ def call_service(self, command='redundancy reload peer', "reset_command %s and timeout is %s +++" % (con.hostname, command, timeout)) - # Check is switchover possible? + # Check is it possible to reset the standby? rp_state = con.get_rp_state(target='standby', timeout=100) - if rp_state.find('DISABLED') == -1: + + if re.search('DISABLED', rp_state): raise SubCommandFailure("No Standby found") + if 'standby_check' in kwargs and not re.search(kwargs['standby_check'], rp_state): + raise SubCommandFailure("Standby found but not in the expected state") + dialog = self.service_dialog(handle=con.active, - service_dialog=self.dialog) - # Issue switchover command + service_dialog=self.dialog+reply) + + # Issue standby reset command con.active.spawn.sendline(command) try: dialog.process(con.active.spawn, timeout=30, - context=con.context) + context=con.active.context) except TimeoutError: pass except SubCommandFailure as err: @@ -2336,6 +2581,7 @@ def call_service(self, command='redundancy reload peer', reset_counter = timeout / 10 counter = 0 + reloadGood = False while counter < reset_counter: try: rp_state = con.get_rp_state(target='standby', @@ -2345,6 +2591,17 @@ def call_service(self, command='redundancy reload peer', counter += 1 continue else: + # first need to insure reload happens + # no false positives + if not reloadGood: + if not re.search('DISABLED', rp_state): + sleep(2) + counter += 1 + continue + else: + reloadGood = True + counter = 0 + if re.search('STANDBY HOT', rp_state): counter = reset_counter + 1 else: @@ -2379,43 +2636,184 @@ class BashService(BaseService): rtr.bash_console(timeout=60).execute('ls') """ - - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.start_state = "enable" self.end_state = "enable" - self.service_name = "bash_console" self.bash_enabled = False - def call_service(self, **kwargs): - self.result = self.__class__.ContextMgr(connection = self.connection, - enable_bash = not self.bash_enabled, - **kwargs) + def pre_service(self, *args, **kwargs): + self.prompt_recovery = kwargs.get('prompt_recovery', False) + if not self.connection.is_connected: + if self.connection.reconnect: + self.connection.connect() + else: + raise ConnectionError("Connection is not established to device") + + if 'target' in kwargs: + handle = self.get_handle(kwargs['target']) + else: + handle = self.get_handle() + + handle.state_machine.go_to( + self.start_state, + handle.spawn, + context=self.connection.context, + prompt_recovery=self.prompt_recovery + ) + + def call_service(self, target=None, **kwargs): + enable_bash = kwargs.pop('enable_bash', False) + handle = self.get_handle(target) + self.result = self.__class__.ContextMgr( + connection=handle, + enable_bash=enable_bash and not self.bash_enabled, + end_state=self.end_state, + **kwargs) + # if bash wasn't enabled, it is now! - if not self.bash_enabled: + if enable_bash: self.bash_enabled = True + def post_service(self, *args, **kwargs): + # context manager will transition to end_state + # no need to do anything post service + pass + class ContextMgr(object): def __init__(self, connection, - enable_bash = False, - target='active', - timeout = None): + enable_bash=False, + end_state=None, + timeout=None, + **kwargs): self.conn = connection # Specific platforms has its own prompt - self.timeout = timeout self.enable_bash = enable_bash - self.target = target + self.end_state = end_state self.timeout = timeout or connection.settings.CONSOLE_TIMEOUT def __enter__(self): - raise NotImplementedError('No enter shell method supports in platform {}' - .format(self.conn.os)) + raise NotImplementedError('No enter shell method supports in platform {}'.format(self.conn.os)) def __exit__(self, exc_type, exc_value, exc_tb): self.conn.log.debug('--- detaching console ---') + sm = self.conn.state_machine + sm.go_to(self.end_state, self.conn.spawn) + + # do not suppress + return False + + def parse(self, *args, **kwargs): + abstract_args = kwargs.setdefault('abstract', {}) + device = getattr(self.conn, 'device', None) + if device: + abstract_args.update(dict( + os=[device.os, 'linux'], + platform=device.platform, + model=device.model, + pid=device.pid, + )) + return self.conn.device.parse(*args, **kwargs) + else: + self.conn.log.warning('No device object, parse method unavailable') + + def __getattr__(self, attr): + if attr in ('execute', 'sendline', 'send', 'expect'): + return getattr(self.conn, attr) + + raise AttributeError('%s object has no attribute %s' + % (self.__class__.__name__, attr)) + + +class AttachModuleService(BaseService): + """ Service to connect to a device module and execute commands. + + Arguments: + None + + Returns: + AttributeError: No attributes + + Examples: + .. code-block:: python + + rtr.attach(1, timeout=60).execute('show interface') + + with rtr.attach(1) as m: + m.execute('show interface') + m.execute(['show interface 1', 'show interface 2']) + # if we want to go to lc_shell state + with rtr.attach(1, debug=True) as m: + m.execute('show interface') + m.execute(['show interface 1', 'show interface 2']) + + + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.start_state = "module" + self.end_state = "enable" + self.service_name = "attach" + + def pre_service(self, module_num, *args, **kwargs): + """ Common pre_service procedure for all Services """ + self.prompt_recovery = kwargs.get('prompt_recovery', False) + if self.connection.is_connected: + return + elif self.connection.reconnect: + self.connection.connect() + else: + raise ConnectionError("Connection is not established to device") + self.context._module_num = module_num + + def call_service(self, module_num, debug=False, **kwargs): + self.result = self.__class__.ContextMgr(self.connection, + module_num, + debug, + context=self.context, + **kwargs) + + class ContextMgr(object): + def __init__(self, + connection, + module_num, + debug=False, + target='active', + context=None, + timeout=None): + self.conn = connection + # Specific platforms has its own prompt + self.timeout = timeout + self.target = target + self.context = context + self.debug = debug + self.timeout = timeout or connection.settings.CONSOLE_TIMEOUT + self.context._module_num = module_num + + def __enter__(self): + if self.conn.is_ha: + if self.target == 'standby': + conn = self.conn.standby + elif self.target == 'active': + conn = self.conn.active + else: + conn = self.conn + + if 'module' not in [s.name for s in conn.state_machine.states]: + raise NotImplementedError('Attach module state not implemented') + + self.conn.log.debug('+++ attaching module +++') + conn.state_machine.go_to('lc_shell' if self.debug else 'module', + conn.spawn, + context=self.context, + timeout=self.timeout) + + return self + + def __exit__(self, exc_type, exc_value, exc_tb): + self.conn.log.debug('--- detaching module ---') + if self.conn.is_ha: if self.target == 'standby': conn = self.conn.standby @@ -2436,3 +2834,268 @@ def __getattr__(self, attr): raise AttributeError('%s object has no attribute %s' % (self.__class__.__name__, attr)) + + +class Switchto(BaseService): + """ Switch to a certain CLI state that is known to the statemachine + """ + + def __init__(self, connection, context, **kwargs): + # Connection object will have all the received details + super().__init__(connection, context, **kwargs) + self.service_name = 'switchto' + self.timeout = connection.settings.EXEC_TIMEOUT + self.context = context + + def log_service_call(self): + pass + + def pre_service(self, to_state, *args, **kwargs): + + if not self.connection.connected: + self.connection.log.warning('Device is not connected, ignoring switchto') + return + + self.connection.log.info("+++ %s: %s +++" % (self.service_name, to_state)) + + def call_service(self, to_state, + timeout=None, + *args, **kwargs): + + if not self.connection.connected: + return + + con = self.connection + sm = self.get_sm() + + timeout = timeout if timeout is not None else self.timeout + + if isinstance(to_state, str): + to_state_list = [to_state] + elif isinstance(to_state, list): + to_state_list = to_state + else: + raise Exception('Invalid switchto to_state type: %s' % repr(to_state)) + + for to_state in to_state_list: + to_state = to_state.replace(' ', '_') + + valid_states = [x.name for x in sm.states] + if to_state not in valid_states: + con.log.warning('%s is not a valid state, ignoring switchto' % to_state) + return + + con.state_machine.go_to(to_state, con.spawn, + context=self.context, + hop_wise=True, + timeout=timeout) + + self.end_state = sm.current_state + + def post_service(self, *args, **kwargs): + pass + + +class GuestshellService(BaseService): + """Service to provide a Linux console. + + Arguments: + enable_guestshell: Enable the guestshell if not already enabled + timeout: Timeout for entering/exiting guestshell mode + retries: If enable_guestshell is True, number of retries + (waiting 5 seconds per retry) to successfully issue the + "guestshell enable" command, and also the number of retries to wait + for the guestshell to become activated afterward. + Default is 20 (100 seconds maximum) + + Example: + .. code-block:: python + + with rtr.guestshell(enable_guestshell=True, retries=10) as gs: + gs.execute("ifconfig") + + with rtr.guestshell() as gs: + gs.execute("ls") + gs.execute("pwd") + """ + + def __init__(self, connection, *args, **kwargs): + super().__init__(connection, *args, **kwargs) + self.start_state = "enable" + self.end_state = "enable" + + def call_service(self, **kwargs): + self.result = self.__class__.ContextMgr(connection=self.connection, + **kwargs) + + class ContextMgr(object): + def __init__(self, connection, + enable_guestshell=False, timeout=None, retries=None): + self.conn = connection + self.enable_guestshell = enable_guestshell + self.timeout = timeout or connection.settings.EXEC_TIMEOUT + self.retries = retries or connection.settings.GUESTSHELL_RETRIES + + def __enter__(self): + + if 'guestshell' not in [s.name for s in self.conn.state_machine.states]: + raise NotImplementedError('Guest shell state not implemented') + + if self.enable_guestshell: + self.conn.log.debug("+++ enabling guestshell +++") + + if self.conn.settings.GUESTSHELL_CONFIG_CMDS: + output = self.conn.execute(self.conn.settings.GUESTSHELL_CONFIG_VERIFY_CMDS) + if isinstance(output, dict): + output = '\n'.join(output.values()) + + if not re.search(self.conn.settings.GUESTSHELL_CONFIG_VERIFY_PATTERN, output): + self.conn.configure(self.conn.settings.GUESTSHELL_CONFIG_CMDS) + for _ in range(self.retries): + output = self.conn.execute(self.conn.settings.GUESTSHELL_CONFIG_VERIFY_CMDS) + if isinstance(output, dict): + output = '\n'.join(output.values()) + + if re.search(self.conn.settings.GUESTSHELL_CONFIG_VERIFY_PATTERN, output): + break + else: + sleep(self.conn.settings.GUESTSHELL_RETRY_SLEEP) + continue + else: + raise SubCommandFailure( + "Failed to enable guestshell after %d tries" + % self.retries) + + if self.conn.settings.GUESTSHELL_ENABLE_CMDS: + # "guestshell enable" may fail with a "please retry request" + # if the guestshell is already undergoing another transition, + # so we may potentially need to retry the command. + for _ in range(self.retries): + # Note: "guestshell enable" is an exec command not a config + output = self.conn.execute(self.conn.settings.GUESTSHELL_ENABLE_CMDS, + timeout=self.timeout) + if isinstance(output, dict): + output = '\n'.join(output.values()) + if not output or re.search("already enabled|enabled successfully", output): + break + elif "please retry request" in output: + sleep(self.conn.settings.GUESTSHELL_RETRY_SLEEP) + continue + else: + # Other output indicates some unexpected failure + raise SubCommandFailure( + "Failed to enable guestshell: %s" % output) + else: + raise SubCommandFailure( + "Failed to enable guestshell after %d tries" + % self.retries) + + if self.conn.settings.GUESTSHELL_ENABLE_VERIFY_CMDS: + # Okay, we successfully issued "guestshell enable". + # Now it may take some time for the guestshell to become + # fully activated (ready for use). + self.conn.log.debug("+++ waiting for guestshell activation +++") + for i in range(self.retries): + output = self.conn.execute(self.conn.settings.GUESTSHELL_ENABLE_VERIFY_CMDS, + timeout=self.timeout) + + if isinstance(output, dict): + output = '\n'.join(output.values()) + if re.search(self.conn.settings.GUESTSHELL_ENABLE_VERIFY_PATTERN, output): + # Success + break + elif "failed" in output.lower(): + # Terminal state, won't recover + raise SubCommandFailure( + "Failed to install/activate guestshell: %s" + % output) + else: + # Not yet ready + sleep(self.conn.settings.GUESTSHELL_RETRY_SLEEP) + continue + else: + raise SubCommandFailure( + "Guestshell failed to become activated after %d tries" + % self.retries) + + self.conn.log.debug('+++ entering guestshell +++') + conn = self.conn.active if self.conn.is_ha else self.conn + conn.state_machine.go_to('guestshell', + conn.spawn, + timeout=self.timeout, + context=self.conn.context) + + return self + + def __exit__(self, *args): + self.conn.log.debug('--- exiting guestshell ---') + conn = self.conn.active if self.conn.is_ha else self.conn + conn.state_machine.go_to('enable', + conn.spawn, + timeout=self.timeout, + context=self.conn.context) + + # do not suppress any errors that occurred + return False + + def __getattr__(self, attr): + if attr in ('execute', 'sendline', 'send', 'expect'): + return getattr(self.conn, attr) + + raise AttributeError('%s object has no attribute %s' + % (self.__class__.__name__, attr)) + +class ContextMgrBaseService(BaseService): + """ Base service to provide a context manager for device states. + Example: + .. code-block:: python + with device.service() as service: + service.execute('command') + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.service_name = "context" + self.context_state = "enable" + self.start_state = "enable" + self.end_state = "enable" + + def call_service(self, target=None, **kwargs): + self.result = self.__class__.ContextMgr( + connection=self.connection, + service=self, + **kwargs) + + class ContextMgr(object): + def __init__(self, connection, service=None, **kwargs): + self.conn = connection + self.service = service + + def __enter__(self): + self.conn.log.debug(f'Entering context for service {self.service.service_name}') + sm = self.conn.state_machine + sm.go_to(self.service.context_state, + self.conn.spawn, + context=self.conn.context) + return self + + def __exit__(self, exc_type, exc_value, exc_tb): + self.conn.log.debug(f'Exiting context for service {self.service.service_name}') + sm = self.conn.state_machine + sm.go_to(self.service.end_state, + self.conn.spawn, + context=self.conn.context) + + # do not suppress + return False + + def __getattr__(self, attr): + # check for connection methods + if hasattr(self.conn, attr): + return getattr(self.conn, attr) + # to support .parse() and other device methods + elif hasattr(self.conn.device, attr): + return getattr(self.conn.device, attr) + else: + raise AttributeError('Device %s and/or connection %s has no attribute %s' + % (self.conn.device, self.conn, attr)) + diff --git a/src/unicon/plugins/generic/service_patterns.py b/src/unicon/plugins/generic/service_patterns.py index 95f029fe..2b57b261 100644 --- a/src/unicon/plugins/generic/service_patterns.py +++ b/src/unicon/plugins/generic/service_patterns.py @@ -24,9 +24,14 @@ def __init__(self): self.secure_passwd_std = r'^.*Do you want to enforce secure password standard(\?)?\s*\(yes\/no\)(\s*\[[yn]\])?\:\s*' self.admin_password = r'^.*(Enter|Confirm) the password for .*admin' self.auto_provision = r'Abort( Power On)? Auto Provisioning .*:' - self.reload_confirm_ios = r'^.*Proceed( with reload)?\?\s*\[confirm\]' + self.reload_confirm_ios = r'^.*Proceed( with( quick)? reload)?\?\s*\[confirm\]' + self.reload_confirm_iosxe = r'^.*Do you wish to proceed with reload anyway\s*\[confirm\]\s*' self.reload_confirm = r'^.*Reload node\s*\?\s*\[no,yes\]\s?$' self.reload_confirm_nxos = r'^(.*)This command will reboot the system.\s*\(y\/n\)\?\s*\[n\]\s?$' + 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' + # Traceroute patterns class TraceroutePatterns(object): @@ -59,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?: $' @@ -76,7 +81,7 @@ def __init__(self): self.ipv6_precedence = r'^.*Precedence \[.+\]\s?: $' self.ipv6_dscp = r'^.*DSCP \[.+\]\s?: $' self.ipv6_hop = r'^.*Include hop by hop option\? \[.+\]\s?: $' - self.pv6_dest = r'^.*Include destination option\? \[.+\]\s?: $' + self.ipv6_dest = r'^.*Include destination option\? \[.+\]\s?: $' self.ipv6_extn_header = r'^.*Include extension headers\? \[.+\]\s?: $' self.ext_cmds_timeout = r'ADD TIMEOUT PATTERNS' # For IPV4 @@ -94,11 +99,11 @@ def __init__(self): self.verbomode = r'^.*Verbose mode\? \[.+\]\s?: $' self.ext_cmds_source = r'^.*Source .*address( or interface)?\s?: $' self.tos = r'^.*Type of service \[.+\]\s?: $' - self.validate = r'^.*Validate reply data\? \[.+\]\s?: $' + self.validate = r'^.*Validate reply data\?\s*\[.+\]:\s*$' self.data_pattern = r'^.*Data pattern \[.+\]\s?: $' self.dfbit_header = r'^.*Set DF bit in IP header(\?)? \[.+\]\s?: $' self.dscp = r'^.*DSCP .*\[.+\]\s?: $' - self.lsrtv = r'^.*Loose, Strict, Record, Timestamp, Verbose\s?\[.+\]\s?: $' + self.lsrtv = r'^.*Loose, Strict, Record, Timestamp, Verbose\s?\[(.+)\]\s?: $' self.qos = r'^.*Include global QOS option\? \[.+\]\s?: $' self.packet = r'^.*Pad packet\? \[.+\]\s?: $' # Range internal dialogs @@ -111,10 +116,14 @@ def __init__(self): self.others = r'^.*\[.+\]\s?: $' # extd_LSRTV patterns self.lsrtv_source = r'^.*Source route: $' - self.lsrtv_hot_count = r'^.*Number of hops \[.*\]: $' - self.lsrtv_timestamp_count = r'^.*Number of timestamps \[.*\]: $}' - self.lsrtv_noroom = r'^.*No room for that option$' - self.lsrtv_invalid_hop = r'^.*Invalid number of hops$' + self.lsrtv_hop_count = r'^.*Number of hops \[.*\]: $' + self.lsrtv_timestamp_count = r'^.*Number of timestamps \[.*\]: $' + self.lsrtv_noroom = r'^.*No room for that option' + self.lsrtv_invalid_hop = r'^.*Invalid number of hops' + self.lsrtv_one_allowed = r'^.*% Only one source route option allowed' + # Invalid commands + self.invalid_command = r'^.*% *Invalid.*' + class CopyPatterns(): def __init__(self): @@ -122,14 +131,14 @@ def __init__(self): self.copy_file = r'^.*(file to copy|Source file name|Source filename) *\[*.*\]*\?.*$' self.file_to_write = r'^file to write.*\[*.*\]*.*$' self.hostname = r'^.*((h|H)ost|(h|H)ostname)(.*?)\[.*\]\?( *)?$' - self.host = r'Address or name of remote host.*\?' - self.src_file = r'Name of file to copy\?' + self.host = r'Address or name of remote host.*\?\s*$' + self.src_file = r'Name of file to copy\?\s*$' self.dest_file = r'Destination filename.*$' self.dest_directory = r'Destination directory.*$' #Move this to NXOS group self.nx_hostname = r'^.*Enter hostname for the (tftp|ftp|scp) server:\s*$' self.partition = r'^.*Which partition\?.*$' - self.config = r'^.*Name of configuration file.*\[*.*\]*.*\?' + self.config = r'^.*Name of configuration file.*\[*.*\]*.*\?\s*$' self.writeto = r'^.*(name to write to|[Dd]estination file ?name).*\[.*\].*$' self.username = r'^.*username.*(\[.*\])?.*$' self.password = r'^.*[Pp]assword.*(\[.*\])?.*$' @@ -145,24 +154,25 @@ def __init__(self): self.copy_overwrite = r'^.*Do you want to over\s?write\?? (\(y\/n\)\?)?\[.*\].*$' self.copy_nx_vrf = r'^.*Enter vrf \(If no input,.*default.*\):\s*$' self.copy_proceed = r'^.*bytes.*proceed\?.*$' - self.tftp_addr =r'^.*Address.*$' + self.tftp_addr =r'^.*Address or name of remote host \[\]\?\s*$' self.copy_complete = r'^.*bank [0-9]+' - self.copy_error_message = r'fail|timed out|Timed out|Error|Login incorrect|denied|Problem' \ - r'|NOT|Invalid|No memory|Failed|mismatch|Bad|bogus|lose|abort' \ + self.copy_error_message = r'\bfail\b|timed out|Timed out|Error|Login incorrect|denied|Problem' \ + r'|NOT|Invalid|No memory|Failed(?! to generate persistent self-signed certificate)|mismatch|Bad|bogus|lose|abort' \ r'|Not |too big|exceeds|detected|[Nn]o route to host' \ r'|image is not allowed|Could not resolve|No such' - self.copy_retry_message = r'fail|[Tt]imed out|Error|Problem|NOT|Failed|Bad|bogus|lose|abort|Not |too big|exceeds|detected' - self.copy_continue = r'Are you sure you want to continue connecting (yes/no)?' + self.copy_retry_message = r'\bfail\b|[Tt]imed out|Error|Problem|NOT|Failed(?! to generate persistent self-signed certificate)|Bad|bogus|lose|abort|Not |too big|exceeds|detected' + self.copy_continue = r'Are you sure you want to continue connecting ((yes/no)|\((yes/no(/\[fingerprint\])?)?\))?' self.copy_other = r'^.*\[yes\/no\]\s*\?*\s*$' self.remote_param ='ftp:|tftp:|http:|rcp:|scp:' self.remote_in_dest = r'(ftp:|sftp:|tftp:|http:|rcp:|scp:)/*$' self.addr_in_remote = r'(ftp:|tftp:|http:|rcp:|scp:)\/*([\w\.\:]+)' + self.abort_copy = r'Abort Copy\? \[confirm\]\s*$' class HaReloadPatterns(UniconCorePatterns): def __init__(self): super().__init__() self.savenv = r'^.*System configuration has been modified\. Save.*$' - self.reload_proceed = r'^(.*)Proceed with reload\?\s*\[confirm\]$|^.*Escape character is.*\n' + self.reload_proceed = r'^(.*)Proceed with( quick)? reload\?\s*\[confirm\]\s*$' self.reload_entire_shelf = r'Reload the entire shelf\s*\[confirm\]' self.reload_this_shelf = r'Reload this shelf\s*\[confirm\]' self.default_prompts = r'(Router|Switch|ios|Switch-standby)(\\(boot\\))?(>|#)' @@ -179,7 +189,7 @@ def __init__(self): class SwitchoverPatterns: def __init__(self): self.save_config = r'^.*System configuration has been modified\.\s*Save\s?\?.*$' - self.build_config= r'Building configuration' + self.build_config = r'Building configuration' self.prompt_switchover = r'This will reload the active unit and force switchover to standby\[confirm\]' self.switchover_init = r'Preparing for switchover|LOGGER_FLUSHING|RELOAD|Reload' self.switchover_reason = r'^(.*)Reset Reason' @@ -188,6 +198,8 @@ def __init__(self): self.switchover_fail3 = r'% There is no STANDBY present\.?' self.switchover_fail4 = r'Failed to switchover' self.switchover_cmd_issued = r'Resetting ...(.*)' + self.switchover_proceed = r'^.*Proceed with switchover to standby RP\? \[confirm\]' + class ResetStandbyPatterns: def __init__(self): @@ -196,3 +208,5 @@ def __init__(self): self.reset_abort = r'Peer reload not performed' self.reload_proceed1 = r'System is running in SIMPLEX mode, reload anyway\?\s*\[confirm\]' + +reload_patterns = ReloadPatterns() diff --git a/src/unicon/plugins/generic/service_statements.py b/src/unicon/plugins/generic/service_statements.py index 739bf560..90b55f67 100644 --- a/src/unicon/plugins/generic/service_statements.py +++ b/src/unicon/plugins/generic/service_statements.py @@ -9,6 +9,7 @@ Module for defining all Services Statement, handlers(callback) and Statement list for service dialog would be defined here. """ + from time import sleep from unicon.eal.dialogs import Statement @@ -18,24 +19,22 @@ PingPatterns, TraceroutePatterns, CopyPatterns, HaReloadPatterns, \ SwitchoverPatterns, ResetStandbyPatterns -from .statements import GenericStatements +from .statements import GenericStatements, chatty_term_wait, update_context, wait_and_enter +from .service_patterns import reload_patterns from unicon.plugins.utils import (get_current_credential, common_cred_username_handler, common_cred_password_handler, ) -from unicon.utils import to_plaintext - generic_statements = GenericStatements() - # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# # Service handlers # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# def send_response(spawn, response=""): - sleep(0.5) + chatty_term_wait(spawn) spawn.sendline(response) @@ -49,15 +48,13 @@ def send_yes_callback(spawn): spawn.sendline("y") -def escape_char_callback(spawn): - sleep(0.5) - spawn.sendline() - def login_handler(spawn, context, session): """ handles login prompt """ credential = get_current_credential(context=context, session=session) if credential: + if credential != 'default': + spawn.log.info(f'Using {credential} credential set for login into device') common_cred_username_handler( spawn=spawn, context=context, credential=credential) else: @@ -108,6 +105,11 @@ def ping_loop_message_handler(): raise SubCommandFailure("Error while Executing ping command") +def ping_invalid_input_handler(spawn): + spawn.log.warning('Invalid ping input, skipping entry') + spawn.sendline() + + def ping_handler(spawn, context, send_key): if context.get(send_key): spawn.sendline(context[send_key]) @@ -119,6 +121,26 @@ def ping_handler_1(spawn, context, send_key): spawn.sendline(context[send_key]) +def lsrtv_handler(spawn, context): + selection = spawn.match.last_match.group(1) + if context.get('extended_verbose') and 'V' not in selection: + spawn.sendline('v') + return + if context.get('timestamp_count') and 'T' not in selection: + spawn.sendline('t') + return + if context.get('record_hops') and 'R' not in selection: + spawn.sendline('r') + return + if context.get('src_route_type', '').lower() == 'loose' and 'L' not in selection: + spawn.sendline('l') + return + if context.get('src_route_type', '').lower() == 'strict' and 'S' not in selection: + spawn.sendline('s') + return + spawn.sendline() + + def send_multicast(spawn, context): if context.get('multicast'): spawn.sendline(context['multicast']) @@ -146,6 +168,11 @@ def copy_handler_1(spawn, context, send_key): else: raise SubCommandFailure("%s is not specified" % context[send_key]) +def copy_overwrite_handler(spawn, context): + if context['overwrite'] == 'False': + spawn.send('n') + else: + spawn.send('y') def copy_error_handler(context, retry=False): if retry: @@ -157,21 +184,21 @@ def copy_error_handler(context, retry=False): def copy_partition_handler(spawn, context): - if context['partition'] is "0": + if context['partition'] == "0": spawn.sendline() else: spawn.sendline(context[partition]) def copy_dest_handler(spawn, context): - if context['dest_file'] is "": + if context['dest_file'] == "": spawn.sendline() else: spawn.sendline(context['dest_file']) def copy_dest_directory_handler(spawn, context): - if context['dest_directory'] is '': + if context['dest_directory'] == '': spawn.sendline() else: spawn.sendline(context['dest_directory']) @@ -182,111 +209,161 @@ def handle_poap_prompt(spawn, session): session.poap_flag = True spawn.sendline('y') + def switchover_failure(error): raise SubCommandFailure("Switchover Failed with error %s" % error) + def reset_failure(error): raise SubCommandFailure("reset_standby_rp Failed with error %s" % error) + +def connection_closed_handler(spawn): + spawn.close() + +def config_session_locked_handler(context): + context['config_session_locked'] = True + # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# # Reload Statements # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# -pat = ReloadPatterns() -save_env = Statement(pattern=pat.savenv, +save_env = Statement(pattern=reload_patterns.savenv, action=send_response, args={'response': 'n'}, loop_continue=True, continue_timer=False) -confirm_reset = Statement(pattern=pat.confirm_reset, +confirm_reset = Statement(pattern=reload_patterns.confirm_reset, action=send_response, args={'response': 'y'}, loop_continue=True, continue_timer=False) -reload_confirm = Statement(pattern=pat.reload_confirm, +reload_confirm = Statement(pattern=reload_patterns.reload_confirm, action=send_response, args={'response': 'yes'}, loop_continue=True, continue_timer=False) -reload_confirm_ios = Statement(pattern=pat.reload_confirm_ios, +reload_confirm_ios = Statement(pattern=reload_patterns.reload_confirm_ios, + action=send_response, args={'response': ''}, + loop_continue=True, + continue_timer=False) + +reload_confirm_iosxe = Statement(pattern=reload_patterns.reload_confirm_iosxe, action=send_response, args={'response': ''}, loop_continue=True, continue_timer=False) -useracess = Statement(pattern=pat.useracess, +useracess = Statement(pattern=reload_patterns.useracess, action=None, args=None, loop_continue=True, continue_timer=False) -press_enter = Statement(pattern=pat.press_enter, - action=send_response, args={'response': ''}, +press_enter = Statement(pattern=reload_patterns.press_enter, + action=wait_and_enter, loop_continue=False, continue_timer=False) -confirm_config = Statement(pattern=pat.confirm_config, +press_return = Statement(pattern=reload_patterns.press_return, + action=wait_and_enter, + loop_continue=False, + continue_timer=False) + +confirm_config = Statement(pattern=reload_patterns.confirm_config, action=send_response, args={'response': ''}, loop_continue=True, continue_timer=False) -setup_dialog = Statement(pattern=pat.setup_dialog, - action=send_response, args={'response': 'n'}, +setup_dialog = Statement(pattern=reload_patterns.setup_dialog, + action=send_response, args={'response': 'no'}, loop_continue=True, continue_timer=False) -auto_install_dialog = Statement(pattern=pat.autoinstall_dialog, +auto_install_dialog = Statement(pattern=reload_patterns.autoinstall_dialog, action=send_response, args={'response': 'y'}, loop_continue=True, continue_timer=False) -module_reload = Statement(pattern=pat.module_reload, +module_reload = Statement(pattern=reload_patterns.module_reload, action=send_response, args={'response': 'n'}, loop_continue=True, continue_timer=False) -save_module_cfg = Statement(pattern=pat.save_module_cfg, +save_module_cfg = Statement(pattern=reload_patterns.save_module_cfg, action=send_response, args={'response': 'n'}, loop_continue=True, continue_timer=False) -reboot_confirm = Statement(pattern=pat.reboot_confirm, +reboot_confirm = Statement(pattern=reload_patterns.reboot_confirm, action=send_response, args={'response': 'y'}, loop_continue=True, continue_timer=False) -secure_passwd_std = Statement(pattern=pat.secure_passwd_std, +secure_passwd_std = Statement(pattern=reload_patterns.secure_passwd_std, action=send_response, args={'response': 'n'}, loop_continue=True, continue_timer=False) -admin_password = Statement(pattern=pat.admin_password, +admin_password = Statement(pattern=reload_patterns.admin_password, action=send_admin_password, args=None, loop_continue=True, continue_timer=False) -auto_provision = Statement(pattern=pat.auto_provision, +auto_provision = Statement(pattern=reload_patterns.auto_provision, action=handle_poap_prompt, args=None, loop_continue=True, continue_timer=False) -login_stmt = Statement(pattern=pat.username, +login_stmt = Statement(pattern=reload_patterns.username, action=login_handler, args=None, loop_continue=True, continue_timer=False) -password_stmt = Statement(pattern=pat.password, +password_stmt = Statement(pattern=reload_patterns.password, action=password_handler, args=None, loop_continue=False, continue_timer=False) +connection_closed = Statement(pattern=reload_patterns.connection_closed, + action=update_context, + args={'console': False}, + loop_continue=False, + continue_timer=False) + +connection_closed_stmt = Statement(pattern=reload_patterns.connection_closed, + action=connection_closed_handler, + args=None, + loop_continue=False, + continue_timer=False) + +config_session_locked_stmt = Statement(pattern=reload_patterns.config_session_locked, + action=config_session_locked_handler, + args=None, + loop_continue=False, + continue_timer=False) + +eof_statement = Statement(pattern='__eof__', + action=connection_closed_handler, + args=None, + loop_continue=False, + continue_timer=False) + reload_statement_list = [save_env, confirm_reset, reload_confirm, - reload_confirm_ios, press_enter, useracess, + reload_confirm_ios, reload_confirm_iosxe, useracess, confirm_config, setup_dialog, auto_install_dialog, module_reload, save_module_cfg, reboot_confirm, secure_passwd_std, admin_password, auto_provision, - login_stmt, password_stmt] + generic_statements.password_ok_stmt, login_stmt, + generic_statements.enable_secret_stmt, + generic_statements.enter_your_selection_stmt, + generic_statements.syslog_msg_stmt, + # Below statements have loop_continue=False + password_stmt, press_enter, press_return, + connection_closed_stmt, eof_statement, + generic_statements.enter_your_encryption_selection_stmt + ] # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# # Ping Statements @@ -431,7 +508,7 @@ def reset_failure(error): loop_continue=True, continue_timer=False) -pv6_dest = Statement(pattern=pat.pv6_dest, +ipv6_dest = Statement(pattern=pat.ipv6_dest, action=ping_handler, args={'send_key': 'ipv6_dst_headers'}, loop_continue=False, @@ -455,7 +532,7 @@ def reset_failure(error): interface = Statement(pattern=pat.interface, action=ping_handler, - args={'send_key': 'int'}, + args={'send_key': 'interface'}, loop_continue=True, continue_timer=False) @@ -519,7 +596,7 @@ def reset_failure(error): ext_cmds_source = Statement(pattern=pat.ext_cmds_source, action=ping_handler, - args={'send_key': 'src_addr'}, + args={'send_key': 'source'}, loop_continue=True, continue_timer=False) @@ -565,6 +642,39 @@ def reset_failure(error): loop_continue=True, continue_timer=False) +lsrtv_stmt = Statement(pattern=pat.lsrtv, + action=lsrtv_handler, + loop_continue=True, + continue_timer=False) + +lsrtv_timestamp = Statement(pattern=pat.lsrtv_timestamp_count, + action=ping_handler, + args={'send_key': 'timestamp_count'}, + loop_continue=True, + continue_timer=False) + +lsrtv_hop_count = Statement(pattern=pat.lsrtv_hop_count, + action=ping_handler, + args={'send_key': 'record_hops'}, + loop_continue=True, + continue_timer=False) + +lsrtv_source = Statement(pattern=pat.lsrtv_source, + action=ping_handler, + args={'send_key': 'src_route_addr'}, + loop_continue=True, + continue_timer=False) + +lsrtv_one_allowed = Statement(pattern=pat.lsrtv_one_allowed, + action=ping_invalid_input_handler, + loop_continue=True, + continue_timer=False) + +lsrtv_noroom = Statement(pattern=pat.lsrtv_noroom, + action=ping_invalid_input_handler, + loop_continue=True, + continue_timer=False) + #################################################################### # Traceroute Statements #################################################################### @@ -625,10 +735,10 @@ def reset_failure(error): continue_timer=False) tr_port = Statement(pattern=tr_pat.port_number, - action=ping_handler, - args={'send_key': 'port'}, - loop_continue=True, - continue_timer=False) + action=ping_handler, + args={'send_key': 'port'}, + loop_continue=True, + continue_timer=False) tr_style = Statement(pattern=tr_pat.style, action=ping_handler, @@ -637,10 +747,10 @@ def reset_failure(error): continue_timer=False) tr_resolve_as_number = Statement(pattern=tr_pat.resolve_as_number, - action=ping_handler, - args={'send_key': 'resolve_as_number'}, - loop_continue=True, - continue_timer=False) + action=ping_handler, + args={'send_key': 'resolve_as_number'}, + loop_continue=True, + continue_timer=False) trace_route_dialog_list = [unkonwn_protocol, protocol, tr_target, tr_ingress, tr_source, tr_numeric, tr_dscp, tr_timeout, @@ -681,9 +791,20 @@ def reset_failure(error): loop_continue=True, continue_timer=False) -extended_ping_dialog_list = [unkonwn_protocol, protocol, transport, mask, - address, vcid, tunnel, repeat, size, verbose, +invalid_input = Statement(pattern=pat.invalid_command, + action=ping_invalid_input_handler, + args=None, + loop_continue=True, + continue_timer=False) + +extended_ping_dialog_list = [invalid_input, unkonwn_protocol, protocol, transport, + mask, address, vcid, tunnel, repeat, size, verbose, interval, packet_timeout, sending_interval, + # error patterns: + lsrtv_noroom, lsrtv_one_allowed, + # the error patterns need to come before lsrtv_stmt + lsrtv_stmt, + lsrtv_timestamp, lsrtv_hop_count, lsrtv_source, output_interface, novell_echo_type, vrf, ext_cmds, sweep_range, range_interval, range_max, range_min, dest_start, interface, dest_end, increment, @@ -694,16 +815,16 @@ def reset_failure(error): qos, packet, others] # TODO include ping_loop_message in dialog -ping_dialog_list = [unkonwn_protocol, protocol, transport, mask, +ping_dialog_list = [invalid_input, unkonwn_protocol, protocol, transport, mask, address, vcid, tunnel, repeat, size, verbose, interval, packet_timeout, sending_interval, output_interface, novell_echo_type, vrf, ext_cmds, sweep_range, range_interval, range_max, range_min, verbomode, others] -ping6_statement_list = [unkonwn_protocol, ipv6_source, ipv6_udp, ipv6_priority, - ipv6_verbose, ipv6_precedence, ipv6_dscp, ipv6_hop, - pv6_dest, ipv6_extn_header, protocol, transport, mask, - address, vcid, tunnel, repeat, size, verbose, interval, +ping6_statement_list = [invalid_input, unkonwn_protocol, ipv6_source, ipv6_udp, + ipv6_priority, ipv6_verbose, ipv6_precedence, ipv6_dscp, + ipv6_hop, ipv6_dest, ipv6_extn_header, protocol, transport, + mask, address, vcid, tunnel, repeat, size, verbose, interval, packet_timeout, sending_interval, output_interface, novell_echo_type, vrf, ext_cmds, sweep_range, range_interval, range_max, range_min, dest_start, @@ -847,8 +968,8 @@ def reset_failure(error): continue_timer=True) # Recheck this copy_overwrite = Statement(pattern=pat.copy_overwrite, - action=send_response, - args={'response': 'y'}, + action=copy_overwrite_handler, + args=None, loop_continue=True, continue_timer=True) @@ -913,6 +1034,12 @@ def reset_failure(error): loop_continue=True, continue_timer=False) +abort_copy_stmt = Statement(pattern=pat.abort_copy, + action=send_response, + args={'response': 'n'}, + loop_continue=True, + continue_timer=False) + copy_statement_list = [copy_retry_message, copy_error_message, source_filename, copy_file, src_file, hostname, dest_file, dest_directory, host, nx_hostname, partition, config, writeto, @@ -921,7 +1048,7 @@ def reset_failure(error): copy_confirm_yes, copy_reconfirm, copy_reconfirm, copy_progress, rcp_confirm, copy_overwrite, copy_nx_vrf, copy_proceed, tftp_addr, copy_continue, copy_complete, - copy_other] + copy_other, abort_copy_stmt] ############################################################################# @@ -982,12 +1109,11 @@ def reset_failure(error): loader_prompt = None rommon_prompt = None -ha_reload_statement_list = [save_env, sso_ready, press_enter, - reload_proceed, reload_entire_shelf, - reload_this_shelf, useracess, config_byte, - setup_dialog, auto_install_dialog, - login_notready, redundant, default_prompts, - auto_provision, login_stmt, password_stmt] +ha_reload_statement_list = [sso_ready, reload_proceed, reload_entire_shelf, + reload_this_shelf, config_byte, login_notready, + redundant, default_prompts + # no idea why we have default prompts... + ] + reload_statement_list ############################################################################# # Reset Standby Command Statement @@ -1088,19 +1214,29 @@ def reset_failure(error): loop_continue=False, continue_timer=False) +switchover_proceed = Statement( + pattern=pat.switchover_proceed, + action='sendline()', args=None, loop_continue=True, continue_timer=False +) + switchover_statement_list = [save_config, build_config, prompt_switchover, switchover_init, switchover_reason, switchover_fail1, switchover_fail2, switchover_fail3, switchover_fail4, - press_enter, login_stmt, password_stmt + press_enter, login_stmt, password_stmt, + generic_statements.password_ok_stmt, + generic_statements.syslog_msg_stmt, + switchover_proceed ] - - ############################################################ # Generic Execution statement list ############################################################# execution_statement_list = [generic_statements.confirm_prompt_y_n_stmt, generic_statements.confirm_prompt_stmt, - generic_statements.yes_no_stmt] + generic_statements.yes_no_stmt, + generic_statements.syslog_msg_stmt] + +configure_statement_list = [generic_statements.syslog_msg_stmt, + config_session_locked_stmt] diff --git a/src/unicon/plugins/generic/settings.py b/src/unicon/plugins/generic/settings.py index 10db6707..e0e84d6a 100644 --- a/src/unicon/plugins/generic/settings.py +++ b/src/unicon/plugins/generic/settings.py @@ -14,6 +14,8 @@ from unicon.plugins.generic.patterns import GenericPatterns genpat = GenericPatterns() + + class GenericSettings(Settings): """" Generic platform settings """ def __init__(self): @@ -28,6 +30,8 @@ def __init__(self): self.HA_INIT_CONFIG_COMMANDS = [ 'no logging console', 'line console 0', + 'exec-timeout 0', + 'line vty 0 4', 'exec-timeout 0' ] self.HA_STANDBY_UNLOCK_COMMANDS = [ @@ -39,28 +43,70 @@ def __init__(self): 'stty cols 200', 'stty rows 200' ] + self.ROMMON_INIT_COMMANDS = [] self.SWITCHOVER_COUNTER = 50 self.SWITCHOVER_TIMEOUT = 500 self.HA_RELOAD_TIMEOUT = 500 self.RELOAD_TIMEOUT = 300 self.RELOAD_WAIT = 240 + self.POST_RELOAD_WAIT = 60 + self.RELOAD_RECONNECT_ATTEMPTS = 3 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 + # and would not be saved by default + self.TEMP_ENABLE_SECRET = 'Secret12345!' + # Minimum length for enable secret password: + # if the password specified is shorter, + # use the TEMP_ENABLE_SECRET instead. + self.ENABLE_SECRET_MIN_LENGTH = 10 + + # for rommon boot, try to find image on flash + self.FIND_BOOT_IMAGE = True + self.BOOT_FILESYSTEM = 'bootflash:' + self.BOOT_FILE_REGEX = r'(\S+\.bin)' + + # Wait for the config prompt to appear + # before checking for the config prompt. + # This may need to be adjusted if the RTT between + # the execution host and lab device is high. + self.CONFIG_TRANSITION_WAIT = 0.2 + + # If learn_hostname is requested but no hostname was actually learned, + # substitute this default hostname when occurances of HOSTNAME_SUBST_PAT + # occur in state patterns. + self.DEFAULT_LEARNED_HOSTNAME = r'([^# \t\n\r\f\v\(\)]+)' + + # Pattern to avoid sending 'enter' after Escape character pattern is seen + self.ESCAPE_CHAR_PROMPT_PATTERN = r'.*(User Access Verification|sername:\s*$|assword:\s*$|login:\s*$|The highlighted entry will)' # When connecting to a device via telnet, how long (in seconds) # to pause before checking the spawn buffer - self.ESCAPE_CHAR_CHATTY_TERM_WAIT = 0.25 + self.ESCAPE_CHAR_CHATTY_TERM_WAIT = 0.5 # number of cycles to wait for if the terminal is still chatty - self.ESCAPE_CHAR_CHATTY_TERM_WAIT_RETRIES = 12 + self.ESCAPE_CHAR_CHATTY_TERM_WAIT_RETRIES = 6 # prompt wait delay - self.ESCAPE_CHAR_PROMPT_WAIT = 0.25 + self.ESCAPE_CHAR_PROMPT_WAIT = 1 # prompt wait retries - # (wait time: 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75 == total wait: 7.0s) + # (wait time: 0.5, 1, 1.5, 2, 2.5, 3, 3.5 == total wait: 14.0s) self.ESCAPE_CHAR_PROMPT_WAIT_RETRIES = 7 + # commands to get a prompt, default to "enter" + self.ESCAPE_CHAR_PROMPT_COMMANDS = ['\r'] + + # syslog message handling timers + self.SYSLOG_WAIT = 1 + # syslog wait time for reload service + self.RELOAD_SYSLOG_WAIT = 10 + # pattern to replace "more" string # command to continue for more_prompt_stmt # when changing MORE_REPLACE_PATTERN, please also change unicon/patterns.py more_prompt @@ -77,13 +123,41 @@ def __init__(self): self.CONFIG_POST_RELOAD_MAX_RETRIES = 20 self.CONFIG_POST_RELOAD_RETRY_DELAY_SEC = 9 + self.GUESTSHELL_RETRIES = 20 + self.GUESTSHELL_RETRY_SLEEP = 5 + + self.SHOW_REDUNDANCY_CMD = 'sh redundancy stat | inc my state' + self.REDUNDANCY_STATE_PATTERN = r'my state = (.*?)\s*$' + + self.ENABLE_TIMEOUT = 30 + # Default error pattern - self.ERROR_PATTERN=[] - self.CONFIGURE_ERROR_PATTERN = [] + self.ERROR_PATTERN = [r"% Invalid command at", + r"% Invalid input detected at", + r"% String is invalid, 'all' is not an allowed string at", + r"Incomplete command", + r'% Unrecognized host or address.', + r'Error: Could not open file .*', + r'Unable to deactivate Capture.', + ] + self.CONFIGURE_ERROR_PATTERN = [r"overlaps with", + r"% Class-map .* is being used", + r'% ?Insertion failed .*', + r'%Failed to add ace to access-list' + r'Insufficient bandwidth .*', + r'BGP is already running; AS is .*', + r'% Failed to commit one or more configuration items.*', + r'% Configuring IP routing on a LAN subinterface is only allowed if that ' + r'subinterface is already configured as part of an IEEE 802.10, IEEE 802.1Q, ' + r'or ISL vLAN.', + r'% OSPF: Please enable segment-routing globally', + r"% Invalid input detected at '^' marker", + r"%ERROR:" + ] # Number of times to retry for config mode by configure service. - self.CONFIG_LOCK_RETRIES = 0 - self.CONFIG_LOCK_RETRY_SLEEP = 2 + self.CONFIG_LOCK_RETRIES = 3 + self.CONFIG_LOCK_RETRY_SLEEP = 10 # for bulk configure self.BULK_CONFIG = False @@ -91,10 +165,18 @@ def __init__(self): self.BULK_CONFIG_CHUNK_LINES = 50 self.BULK_CONFIG_CHUNK_SLEEP = 0.5 - # for execute matched retry on state pattern + # for execute matched retry on statement pattern self.EXECUTE_MATCHED_RETRIES = 1 self.EXECUTE_MATCHED_RETRY_SLEEP = 0.05 + # for configure matched retry on statement pattern + self.CONFIGURE_MATCHED_RETRIES = 1 + self.CONFIGURE_MATCHED_RETRY_SLEEP = 0.05 + + # execute statement match retry for state change patterns + self.EXECUTE_STATE_CHANGE_MATCH_RETRIES = 1 + self.EXECUTE_STATE_CHANGE_MATCH_RETRY_SLEEP = 3 + # User defined login and password prompt pattern. self.LOGIN_PROMPT = None self.PASSWORD_PROMPT = None @@ -124,7 +206,108 @@ def __init__(self): 'bad context', 'Failed to resolve', '(U|u)nknown (H|h)ost'] -#TODO -#take addtional dialogs for all service -#move all commands to settings -# + # Overwite testbed tokens during token discovery + self.LEARN_DEVICE_TOKENS = False + self.OVERWRITE_TESTBED_TOKENS = False + + self.LEARN_OS_COMMANDS = [ + 'show version', + 'uname', + ] + + self.OS_MAPPING = { + 'nxos': { + 'os': ['Nexus Operating System'], + 'platform': { + 'aci': ['aci'], + 'mds': ['mds'], + 'n5k': ['n5k'], + 'n9k': ['n9k'], + 'nxosv': ['nxosv'], + }, + }, + 'iosxe': { + 'os': ['IOS( |-)XE Software'], + 'platform': { + 'cat3k': ['cat3k'], + 'cat9k': ['cat9k'], + 'csr1000v': ['csr1000v'], + 'sdwan': ['sdwan'], + 'nxosv': ['nxosv'], + }, + }, + 'iosxr': { + 'os': ['IOS XR Software'], + 'platform': { + 'asr9k': ['asr9k'], + 'iosxrv': ['iosxrv'], + 'iosxrv9k': ['iosxrv9k'], + 'moonshine': ['moonshine'], + 'ncs5k': ['ncs5k'], + 'spitfire': ['spitfire'], + }, + }, + 'ios': { + 'os': ['IOS Software'], + 'platform': { + 'ap': ['TBD'], + 'iol': ['TBD'], + 'iosv': ['TBD'], + 'pagent': ['TBD'], + }, + }, + 'junos': { + 'os': ['JUNOS Software'], + 'platform': { + 'vsrx': ['vsrx'], + }, + }, + 'linux': { + 'os': ['Linux'], + }, + 'aireos': { + 'os': ['aireos'], + }, + 'cheetah': { + 'os': ['cheetah'], + }, + 'ise': { + 'os': ['ise'], + }, + 'asa': { + 'os': ['asa'], + }, + 'nso': { + 'os': ['nso'], + }, + 'confd': { + 'os': ['confd'], + }, + 'vos': { + 'os': ['vos'], + }, + 'cimc': { + 'os': ['cimc'], + }, + 'fxos': { + 'os': ['fxos'], + }, + 'staros': { + 'os': ['staros'], + }, + 'aci': { + 'os': ['aci'], + }, + 'sdwan': { + 'os': ['sdwan'], + }, + 'sros': { + 'os': ['sros'], + }, + 'apic': { + 'os': ['apic'], + }, + 'windows': { + 'os': ['windows'], + }, + } diff --git a/src/unicon/plugins/generic/statemachine.py b/src/unicon/plugins/generic/statemachine.py index fa41520b..8114d02f 100644 --- a/src/unicon/plugins/generic/statemachine.py +++ b/src/unicon/plugins/generic/statemachine.py @@ -11,24 +11,91 @@ by majority of the platforms. It should also be used as starting point by further sub classing it. """ + +import re +from time import sleep + +from unicon.core.errors import StateMachineError, TimeoutError as UniconTimeoutError from unicon.plugins.generic.statements import GenericStatements from unicon.plugins.generic.patterns import GenericPatterns from unicon.statemachine import State, Path, StateMachine -from unicon.eal.dialogs import Dialog +from unicon.eal.dialogs import Dialog, Statement from .statements import (authentication_statement_list, - default_statement_list) + default_statement_list, + buffer_settled, + disable_enable_transition_statements) patterns = GenericPatterns() statements = GenericStatements() +def config_service_prompt_handler(spawn, config_pattern): + """ Check if we need to send the sevice config prompt command. + """ + if hasattr(spawn.settings, 'SERVICE_PROMPT_CONFIG_CMD') and spawn.settings.SERVICE_PROMPT_CONFIG_CMD: + spawn.log.debug('Waiting for config prompt') + # if the config prompt is seen, return + if re.search(config_pattern, spawn.buffer): + return + else: + # if no buffer changes for (config timout) seconds, check again + if buffer_settled(spawn, spawn.settings.CONFIG_TRANSITION_WAIT): + if re.search(config_pattern, spawn.buffer): + return + else: + spawn.log.debug('Config prompt not seen, enabling service prompt config') + spawn.sendline(spawn.settings.SERVICE_PROMPT_CONFIG_CMD) + + +def config_transition(statemachine, spawn, context): + ''' + Config may be locked, retry until max attempts or config state reached + ''' + wait_time = spawn.settings.CONFIG_LOCK_RETRY_SLEEP + max_attempts = spawn.settings.CONFIG_LOCK_RETRIES + dialog = Dialog([Statement(pattern=statemachine.get_state('enable').pattern, + loop_continue=False, + trim_buffer=True), + Statement(pattern=statemachine.get_state('config').pattern, + loop_continue=False, + trim_buffer=False), + statements.syslog_msg_stmt + ]) + if hasattr(statemachine, 'config_transition_statement_list'): + dialog += Dialog(statemachine.config_transition_statement_list) + + for attempt in range(max_attempts + 1): + spawn.sendline(statemachine.config_command) + try: + dialog.process(spawn, timeout=spawn.settings.CONFIG_TIMEOUT, context=context) + except UniconTimeoutError: + pass + + statemachine.detect_state(spawn) + if statemachine.current_state == 'config': + return + + if attempt < max_attempts: + spawn.log.warning('*** Could not enter config mode, waiting {} seconds. Retry attempt {}/{} ***'.format( + wait_time, attempt + 1, max_attempts)) + sleep(wait_time) + spawn.sendline() + statemachine.go_to('any', spawn) + if statemachine.current_state in ('config', 'exclusive'): + spawn.sendline() + return + + raise StateMachineError('Unable to transition to config mode') + + ############################################################# # State Machine Definition ############################################################# class GenericSingleRpStateMachine(StateMachine): + config_command = 'config term' """ Defines Generic StateMachine for singleRP @@ -53,15 +120,11 @@ def create(self): # Path Definition ########################################################## - enable_to_disable = Path(enable, disable, 'disable', None) - enable_to_config = Path(enable, config, 'config term', None) - # Dialog([ - # [r'\[terminal\]\?\s?$', 'sendline()', None, True, False] - # ])) + enable_to_disable = Path(enable, disable, 'disable', Dialog([statements.syslog_msg_stmt])) enable_to_rommon = Path(enable, rommon, 'reload', None) - disable_to_enable = Path(disable, enable, 'enable', - Dialog([statements.enable_password_stmt, statements.bad_password_stmt])) - config_to_enable = Path(config, enable, 'end', None) + enable_to_config = Path(enable, config, config_transition, Dialog([statements.syslog_msg_stmt])) + disable_to_enable = Path(disable, enable, 'enable', Dialog(disable_enable_transition_statements)) + config_to_enable = Path(config, enable, 'end', Dialog([statements.syslog_msg_stmt])) rommon_to_disable = Path(rommon, disable, 'boot', Dialog(authentication_statement_list)) @@ -78,8 +141,16 @@ def create(self): self.add_path(enable_to_disable) self.add_default_statements(default_statement_list) + standby_locked = State('standby_locked', patterns.standby_locked) -class GenericDualRpStateMachine(StateMachine): + self.add_state(standby_locked) + + def learn_os_state(self): + learn_os = State('learn_os', patterns.learn_os_prompt) + self.add_state(learn_os) + + +class GenericDualRpStateMachine(GenericSingleRpStateMachine): """ Defines Generic StateMachine for dualRP Statemachine keeps in track all the supported states @@ -90,47 +161,8 @@ class GenericDualRpStateMachine(StateMachine): def create(self): """creates the state machine""" - ########################################################## - # State Definition - ########################################################## - - enable = State('enable', patterns.enable_prompt) - disable = State('disable', patterns.disable_prompt) - config = State('config', patterns.config_prompt) - rommon = State('rommon', patterns.rommon_prompt) - # standby_enable = State('standby_enable', patterns.standby_enable_prompt) - # standby_disable = State('standby_disable', patterns.standby_disable_prompt) - standby_locked = State('standby_locked', patterns.standby_locked) + super().create() ########################################################## - # Path Definition + # State Definition ########################################################## - - enable_to_disable = Path(enable, disable, 'disable', None) - enable_to_config = Path(enable, config, 'config term', None) - enable_to_rommon = Path(enable, rommon, 'reload', None) - disable_to_enable = Path(disable, enable, 'enable', - Dialog([statements.enable_password_stmt, statements.bad_password_stmt])) - config_to_enable = Path(config, enable, 'end', None) - rommon_to_disable = Path(rommon, disable, 'boot', - Dialog(authentication_statement_list)) - - self.add_state(enable) - self.add_state(config) - self.add_state(disable) - self.add_state(rommon) - # self.add_state(standby_enable) - # self.add_state(standby_disable) - self.add_state(standby_locked) - - self.add_path(rommon_to_disable) - self.add_path(disable_to_enable) - self.add_path(enable_to_config) - self.add_path(enable_to_rommon) - self.add_path(config_to_enable) - self.add_path(enable_to_disable) - - self.add_default_statements(default_statement_list) - - -# TODO: state priorities to be reversed. diff --git a/src/unicon/plugins/generic/statements.py b/src/unicon/plugins/generic/statements.py index 63dad45a..28ae7da2 100644 --- a/src/unicon/plugins/generic/statements.py +++ b/src/unicon/plugins/generic/statements.py @@ -11,14 +11,21 @@ """ import re from time import sleep +from datetime import datetime, timedelta from unicon.eal.dialogs import Statement from unicon.eal.helpers import sendline from unicon.core.errors import UniconAuthenticationError +from unicon.core.errors import CredentialsExhaustedError +from unicon.core.errors import ConnectionError as UniconConnectionError from unicon.utils import Utils from unicon.plugins.generic.patterns import GenericPatterns -from unicon.plugins.utils import (get_current_credential, - common_cred_username_handler, common_cred_password_handler, ) +from unicon.plugins.utils import ( + get_current_credential, + _get_creds_to_try, + common_cred_username_handler, + common_cred_password_handler, +) from unicon.utils import to_plaintext from unicon.bases.routers.connection import ENABLE_CRED_NAME @@ -26,52 +33,139 @@ pat = GenericPatterns() utils = Utils() + ############################################################# # Callbacks ############################################################# -def connection_refused_handler(spawn): +def terminal_position_handler(spawn, session, context): + """ send terminal position (VT100) """ + spawn.send('\x1b[0;0R') + + +def connection_refused_handler(spawn, context): """ handles connection refused scenarios """ - raise Exception('Connection refused to device %s' % (str(spawn),)) + 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 + raise Exception('Connection refused to device %s' % (str(spawn))) + + +def connection_failure_handler(spawn): + raise Exception('received disconnect from router %s' % (str(spawn))) + +def permission_denied_handler(spawn): + raise UniconConnectionError( + 'Permission denied for device "%s"' % (str(spawn))) + +def syslog_stripper(spawn): + """Strip syslog from spawn buffer""" + spawn.buffer = re.sub(pat.syslog_message_pattern, '', spawn.buffer, flags=re.M).strip() + + +def buffer_wait(spawn, wait_time): + ''' Keep reading the buffer until wait_time. + + Args: + wait_time (float): wait time in seconds + Returns: + None + ''' + time_wait = timedelta(seconds=wait_time) + start_time = current_time = datetime.now() + while (current_time - start_time) < time_wait: + spawn.read_update_buffer() + current_time = datetime.now() -def connection_failure_handler(spawn, err): - raise Exception(err) +def buffer_settled(spawn, wait_time): + """Wait up to wait_time for the buffer to settle. + Args: + wait_time (float): wait time in seconds + Returns: + True/False -def chatty_term_wait(spawn, trim_buffer=False): - """ Wait a small amount of time for any chatter to cease from the device. + If the buffer is growing, return False immediately, + if the buffer did not grow during wait_time, + return True. """ + wait_time = timedelta(seconds=wait_time) + start_time = current_time = datetime.now() prev_buf_len = len(spawn.buffer) - for retry_number in range( - spawn.settings.ESCAPE_CHAR_CHATTY_TERM_WAIT_RETRIES): + while (current_time - start_time) < wait_time: + spawn.read_update_buffer() + cur_buf_len = len(spawn.buffer) - sleep(spawn.settings.ESCAPE_CHAR_CHATTY_TERM_WAIT) + if cur_buf_len > prev_buf_len: + return False - spawn.read_update_buffer() + current_time = datetime.now() + return True - cur_buf_len = len(spawn.buffer) - if prev_buf_len == cur_buf_len: +def syslog_wait_send_return(spawn, session): + """Handle syslog messages observed in the buffer. + + If a syslog messsage was seen, this handler is executed. + Read the buffer, if its growing, return. + + If the buffer is not growing, read updates up to SYSLOG_WAIT + and check if in that period the buffer stayed the same. + If so, the last message was a syslog message and we want + to send a return to get back the prompt. A return is sent + and the length of the buffer is stored, another return + is sent only if the buffer size changed the next time + this handler is called (i.e. another syslog message was received). + """ + buffer_len = session.get('buffer_len', 0) + if len(spawn.buffer) == buffer_len: + if not session.get('syslog_sent_cr', False) and \ + buffer_settled(spawn, spawn.settings.SYSLOG_WAIT): + spawn.sendline() + session['syslog_sent_cr'] = True + else: + session['syslog_sent_cr'] = False + session['buffer_len'] = len(spawn.buffer) + + +def chatty_term_wait(spawn, trim_buffer=False, wait_time=None): + """ Wait some time for any chatter to cease from the device. + """ + chatty_wait_time = wait_time or spawn.settings.ESCAPE_CHAR_CHATTY_TERM_WAIT + for retry_number in range(spawn.settings.ESCAPE_CHAR_CHATTY_TERM_WAIT_RETRIES): + + if buffer_settled(spawn, chatty_wait_time): break else: - prev_buf_len = cur_buf_len - if trim_buffer: - spawn.trim_buffer() + buffer_wait(spawn, chatty_wait_time * (retry_number + 1)) + + else: + spawn.log.warning('The buffer has not settled because the device is chatty. ' + 'You can try adjusting ESCAPE_CHAR_CHATTY_TERM_WAIT and ' + 'ESCAPE_CHAR_CHATTY_TERM_WAIT_RETRIES') + + if trim_buffer: + spawn.trim_buffer() def escape_char_callback(spawn): - """ Wait a small amount of time for terminal chatter to cease before - attempting to obtain prompt, do not attempt to obtain prompt if login message is seen. + """ Wait some time for terminal chatter to cease before attempting to obtain prompt, + do not attempt to obtain prompt if login message is seen. """ chatty_term_wait(spawn) - # Device is already asking for authentication - if re.search( - '.*(User Access Verification|sername:\s*$|assword:\s*$|login:\s*$)', - spawn.buffer): + # get from settings or fallback to default + escape_char_prompt = getattr(spawn.settings, 'ESCAPE_CHAR_PROMPT_PATTERN', + r'.*(User Access Verification|sername:\s*$|assword:\s*$|login:\s*$)') + # Device is already showing some kind of prompt + if re.search(escape_char_prompt, spawn.buffer): return auth_pat = '' @@ -92,22 +186,36 @@ def escape_char_callback(spawn): # store current know buffer known_buffer = len(spawn.buffer.strip()) + # list of commands to iterate through + cmds = spawn.settings.ESCAPE_CHAR_PROMPT_COMMANDS + iter_cmds = iter(cmds) + for retry_number in range(spawn.settings.ESCAPE_CHAR_PROMPT_WAIT_RETRIES): - # hit enter - spawn.sendline() - spawn.read_update_buffer() - # incremental sleep logic - sleep(spawn.settings.ESCAPE_CHAR_PROMPT_WAIT*(retry_number+1)) + # iterate through the commands + try: + cmd = next(iter_cmds) + except StopIteration: + iter_cmds = iter(cmds) + cmd = next(iter(iter_cmds)) - # did we get prompt after? - spawn.read_update_buffer() + # send command (typically "\r") + spawn.send(cmd) + + # incremental wait logic + buffer_wait(spawn, spawn.settings.ESCAPE_CHAR_PROMPT_WAIT * (retry_number + 1)) # check buffer if known_buffer != len(spawn.buffer.strip()): # we got new stuff - assume it's the the prompt, get out break + else: + spawn.log.warning('Device is not responding, it might be slow. ' + 'You can try adjusting the ESCAPE_CHAR_PROMPT_WAIT and ' + 'ESCAPE_CHAR_PROMPT_WAIT_RETRIES settings.') + + def ssh_continue_connecting(spawn): """ handles SSH new key prompt """ @@ -120,6 +228,8 @@ def login_handler(spawn, context, session): """ credential = get_current_credential(context=context, session=session) if credential: + if credential != 'default': + spawn.log.info(f'Using {credential} credential set for login into device') common_cred_username_handler( spawn=spawn, context=context, credential=credential) else: @@ -132,38 +242,133 @@ def user_access_verification(session): session['tacacs_login'] = 1 -def enable_password_handler(spawn, context, session): +def get_enable_credential_password(context): + """ Get the enable password from the credentials. + + 1. If there is a previous credential (the last credential used to respond to + a password prompt), use its enable_password member if it exists. + 2. Otherwise, if the user specified a list of credentials, pick the final one in the list and + use its enable_password member if it exists. + 3. Otherwise, if there is a default credential, use its enable_password member if it exists. + 4. Otherwise, use the well known "enable" credential, password member if it exists. + 5. Otherwise, use the default credential "password" member if it exists. + 6. Otherwise, raise error that no enable password could be found. + + """ credentials = context.get('credentials') - enable_credential = credentials[ENABLE_CRED_NAME] if credentials else None - if enable_credential: - try: - spawn.sendline(to_plaintext(enable_credential['password'])) - except KeyError as exc: - raise UniconAuthenticationError("No password has been defined " - "for credential {}.".format(ENABLE_CRED_NAME)) - else: - if 'password_attempts' not in session: - session['password_attempts'] = 1 + enable_credential_password = "" + login_creds = context.get('login_creds', []) + fallback_cred = context.get('default_cred_name', "") + if not login_creds: + login_creds = [fallback_cred] + if not isinstance(login_creds, list): + login_creds = [login_creds] + + # Pick the last item in the login_creds list to select the intended + # credential even if the device does not ask for a password on login + # and the given credential is not consumed. + final_credential = login_creds[-1] if login_creds else "" + if credentials: + enable_pw_checks = [ + (context.get('previous_credential', ""), 'enable_password'), + (final_credential, 'enable_password'), + (fallback_cred, 'enable_password'), + (ENABLE_CRED_NAME, 'password'), + (context.get('default_cred_name', ""), 'password'), + ] + for cred_name, key in enable_pw_checks: + if cred_name: + candidate_enable_pw = credentials.get(cred_name, {}).get(key) + if candidate_enable_pw is not None: + enable_credential_password = candidate_enable_pw + break else: - session['password_attempts'] += 1 - if session.password_attempts > spawn.settings.PASSWORD_ATTEMPTS: - raise UniconAuthenticationError('Too many enable password retries') + raise UniconAuthenticationError('{}: Could not find an enable credential.'. + format(context.get('hostname', ""))) + return to_plaintext(enable_credential_password) + + +def enable_password_handler(spawn, context, session): + if 'password_attempts' not in session: + session['password_attempts'] = 1 + else: + session['password_attempts'] += 1 + if session.password_attempts > spawn.settings.PASSWORD_ATTEMPTS: + raise UniconAuthenticationError('Too many enable password retries') + + enable_credential_password = get_enable_credential_password(context=context) + if enable_credential_password: + spawn.sendline(enable_credential_password) + else: spawn.sendline(context['enable_password']) +def set_new_password(spawn, context, session): + '''setting up the new password on the device. + + For setting up the password we need to do these 2 steps + to make sure we don't get CredentialsExhaustedError: + 1- remove the current_credential(this is the last credential used for login into device) + from session. + 2- remove the cred_iter(an iterable of login credentials) from session. + after removing these 2 we reset credentials and we could use the default password from the default credentials + for setting up the password on the device. + ''' + # remove the current credential from session + if session.get('current_credential'): + session.pop('current_credential') + # remove the cred_iter from session + if session.get('cred_iter'): + session.pop('cred_iter') + # calling the password handler for sending the passowrd. + password_handler(spawn, context, session ) + + +def enable_secret_handler(spawn, context, session): + if 'password_attempts' not in session: + session['password_attempts'] = 1 + else: + session['password_attempts'] += 1 + if session.password_attempts > spawn.settings.PASSWORD_ATTEMPTS: + raise UniconAuthenticationError('Too many enable password retries') + + enable_credential_password = get_enable_credential_password(context=context) + if enable_credential_password and len(enable_credential_password) >= \ + spawn.settings.ENABLE_SECRET_MIN_LENGTH: + spawn.sendline(enable_credential_password) + else: + spawn.log.warning('Using enable secret from TEMP_ENABLE_SECRET setting') + enable_secret = spawn.settings.TEMP_ENABLE_SECRET + context['setup_selection'] = 0 + spawn.sendline(enable_secret) + + +def setup_enter_selection(spawn, context): + selection = context.get('setup_selection') + if selection is not None: + if str(selection) == '0': + spawn.log.warning('Not saving setup configuration') + spawn.sendline(f'{selection}') + else: + spawn.sendline('2') + def ssh_tacacs_handler(spawn, context): result = False start_cmd = spawn.spawn_command - if re.search(context['username'] + r'@', start_cmd) \ - or re.search(r'-l\s*' + context['username'], start_cmd) \ - or re.search(context['username'] + r'@', spawn.buffer): - result = True + if context.get('username'): + if re.search(context['username'] + r'@', start_cmd) \ + or re.search(r'-l\s*' + context['username'], start_cmd) \ + or re.search(context['username'] + r'@', spawn.buffer): + result = True return result def password_handler(spawn, context, session): """ handles password prompt """ + if spawn.last_sent.startswith('enable'): + return enable_password_handler(spawn, context, session) + credential = get_current_credential(context=context, session=session) if credential: common_cred_password_handler( @@ -177,50 +382,140 @@ def password_handler(spawn, context, session): if session.password_attempts > spawn.settings.PASSWORD_ATTEMPTS: raise UniconAuthenticationError('Too many password retries') - if context['username'] == spawn.last_sent.rstrip() or \ - ssh_tacacs_handler(spawn, context): - spawn.sendline(context['tacacs_password']) + if context.get('username', '') == spawn.last_sent.rstrip() or ssh_tacacs_handler(spawn, context): + if (tacacs_password := context.get('tacacs_password')): + spawn.sendline(tacacs_password) + elif context.get('password'): + spawn.sendline(context['password']) else: spawn.sendline(context['line_password']) + cred_actions = context.get('cred_action', {}).get(credential, {}) + if cred_actions: + post_action = cred_actions.get('post', '') + action = re.match(r'(send|sendline)\((.*)\)', post_action) + if action: + method = action.group(1) + args = action.group(2) + spawn.log.info('Executing post credential command: {}'.format(post_action)) + getattr(spawn, method)(args) + elif credential and getattr(spawn.settings, 'SENDLINE_AFTER_CRED', None) == credential: + spawn.log.info("Sending return after credential '{}'".format(credential)) + spawn.sendline() + + +def passphrase_handler(spawn, context, session): + """ Handles SSH passphrase prompt """ + credential = get_current_credential(context=context, session=session) + try: + spawn.sendline(to_plaintext( + context['credentials'][credential]['passphrase'])) + except KeyError: + raise UniconAuthenticationError("No passphrase found " + "for credential {}.".format(credential)) + -def bad_password_handler(spawn): +def bad_password_handler(spawn, context, session): """ handles bad password prompt """ - raise UniconAuthenticationError('Bad Password sent to device %s' % (str(spawn),)) + # check if there is a fallback credential + if context['fallback_creds']: + spawn.log.info('Using fallback credentials for logging in to the device!') + # Update the session with fallback credentials + if not session.get('fallback_creds'): + session['fallback_creds'] = iter(context['fallback_creds']) + # this list keep track of the fallback credentials being used + session['cred_list'] = [] + try: + # update the current credential with the next fallback credential + session['current_credential'] = next(session['fallback_creds']) + spawn.log.info(f"Using {session['current_credential']} from fallback credential list.") + # update the list of fallback credentials + session['cred_list'].append(session['current_credential']) + except StopIteration: + raise CredentialsExhaustedError( + creds_tried= _get_creds_to_try(context) + (session['cred_list'])) + else: + raise UniconAuthenticationError('Bad Password sent to device %s' % (str(spawn),)) def incorrect_login_handler(spawn, context, session): + # In nxos device if the first attempt password prompt occur before + # username prompt, it will get Login incorrect error. + # Reset the cred_iter to try again + if 'incorrect_login_attempts' not in session: + session.pop('cred_iter', None) + credential = get_current_credential(context=context, session=session) - if credential: + if credential and 'incorrect_login_attempts' in session: # If credentials have been supplied, there are no login retries. # The user must supply appropriate credentials to ensure login - # does not fail. + # does not fail. Skip it for the first attempt + + # Attempt fallback credentials if available + if session['current_credential']: + return + raise UniconAuthenticationError( 'Login failure, either wrong username or password') + if 'incorrect_login_attempts' not in session: + session['incorrect_login_attempts'] = 1 + + # Let's give a chance for unicon to login with right credentials + # let's give three attempts + if session['incorrect_login_attempts'] <= 3: + session['incorrect_login_attempts'] = \ + session['incorrect_login_attempts'] + 1 else: - if 'incorrect_login_attempts' not in session: - session['incorrect_login_attempts'] = 1 - - # Let's give a change for unicon to login with right credentials - # let's give three attempts - if session['incorrect_login_attempts'] <=3: - session['incorrect_login_attempts'] = \ - session['incorrect_login_attempts'] + 1 - else: - raise UniconAuthenticationError( - 'Login failure, either wrong username or password') + raise UniconAuthenticationError( + 'Login failure, either wrong username or password') + +def no_password_handler(spawn, context, session): + """ handles no password prompt + """ + raise UniconAuthenticationError('No password set on this device') + +def sudo_password_handler(spawn, context, session): + """ Password handler for sudo command + """ + if 'sudo_attempts' not in session: + session['sudo_attempts'] = 1 + else: + raise UniconAuthenticationError('sudo failure') + + credentials = context.get('credentials') + if credentials: + try: + spawn.sendline( + to_plaintext(credentials['sudo']['password'])) + except KeyError: + raise UniconAuthenticationError("No password has been defined " + "for sudo credential.") + else: + raise UniconAuthenticationError("No credentials has been defined for sudo.") -def wait_and_enter(spawn): - sleep(0.5) # otherwise newline is sometimes lost? +def wait_and_enter(spawn, wait=0.5): + # wait and read the buffer + # this avoids issues where the 'sendline' + # is somehow lost + wait_time = timedelta(seconds=wait) + settle_time = current_time = datetime.now() + while (current_time - settle_time) < wait_time: + spawn.read_update_buffer() + current_time = datetime.now() spawn.sendline() def more_prompt_handler(spawn): output = utils.remove_backspace(spawn.match.match_output) all_more = re.findall(spawn.settings.MORE_REPLACE_PATTERN, output) - spawn.match.match_output = ''.join(output.rsplit(all_more[-1], 1)) + if all_more: + spawn.match.match_output = ''.join(output.rsplit(all_more[-1], 1)) + spawn.buffer = ''.join(spawn.buffer.rsplit(all_more[-1], 1)) + else: + spawn.match.match_output = output + spawn.buffer = utils.remove_backspace(spawn.buffer) spawn.send(spawn.settings.MORE_CONTINUE) @@ -229,22 +524,54 @@ def custom_auth_statements(login_pattern=None, password_pattern=None): stmt_list = [] if login_pattern: login_stmt = Statement(pattern=login_pattern, - action=login_handler, - args=None, - loop_continue=True, - continue_timer=False) + action=login_handler, + args=None, + loop_continue=True, + continue_timer=False) stmt_list.append(login_stmt) if password_pattern: password_stmt = Statement(pattern=password_pattern, - action=password_handler, - args=None, - loop_continue=True, - continue_timer=False) + action=password_handler, + args=None, + loop_continue=True, + continue_timer=False) stmt_list.append(password_stmt) if stmt_list: return stmt_list +def update_context(spawn, context, session, **kwargs): + context.update(kwargs) + + +def boot_timeout_handler(spawn, context, session): + '''Special handler for dialog timeouts that occur during boot. + Based on start_boot_time set in the rommon->disable + transition handler, determine if boot is taking too + long and raise an exception. + ''' + boot_timeout_time = timedelta(seconds=spawn.settings.BOOT_TIMEOUT) + boot_start_time = context.get('boot_start_time') + if boot_start_time: + current_time = datetime.now() + delta_time = current_time - boot_start_time + if delta_time > boot_timeout_time: + context.pop('boot_start_time', None) + raise TimeoutError('Boot timeout') + return True + else: + return False + + +boot_timeout_stmt = Statement( + pattern='__timeout__', + action=boot_timeout_handler, + args=None, + loop_continue=True, + continue_timer=False) + + + ############################################################# # Generic statements ############################################################# @@ -259,13 +586,18 @@ def __init__(self): ''' All generic Statements ''' + # This statement has retries to wait for other messages before + # calling the escape handler. self.escape_char_stmt = Statement(pattern=pat.escape_char, action=escape_char_callback, args=None, loop_continue=True, - continue_timer=False) + continue_timer=False, + matched_retries=1, + matched_retry_sleep=1) + self.press_return_stmt = Statement(pattern=pat.press_return, - action=sendline, args=None, + action=wait_and_enter, args=None, loop_continue=True, continue_timer=False) self.connection_refused_stmt = \ @@ -278,19 +610,24 @@ def __init__(self): self.bad_password_stmt = Statement(pattern=pat.bad_passwords, action=bad_password_handler, args=None, - loop_continue=False, + loop_continue=True, continue_timer=False) self.login_incorrect = Statement(pattern=pat.login_incorrect, - action=incorrect_login_handler, - args=None, - loop_continue=True, - continue_timer=False) + action=incorrect_login_handler, + args=None, + loop_continue=True, + continue_timer=False) + + self.no_password_set_stmt = Statement(pattern=pat.no_password_set, + action=no_password_handler, + args=None, + loop_continue=True, + continue_timer=False) self.disconnect_error_stmt = Statement(pattern=pat.disconnect_message, action=connection_failure_handler, - args={ - 'err': 'received disconnect from router'}, + args=None, loop_continue=False, continue_timer=False) self.login_stmt = Statement(pattern=pat.username, @@ -308,21 +645,33 @@ def __init__(self): args=None, loop_continue=True, continue_timer=False) - self.enable_password_stmt = Statement(pattern=pat.password, - action=enable_password_handler, - args=None, - loop_continue=True, - continue_timer=False) + self.new_password_stmt = Statement(pattern=pat.new_password, + action=set_new_password, + args=None, + loop_continue=True, + continue_timer=False) + self.enable_password_stmt = Statement(pattern=pat.enable_password, + action=enable_password_handler, + args=None, + loop_continue=True, + continue_timer=False) + self.enable_secret_stmt = Statement(pattern=pat.enable_secret, + action=enable_secret_handler, + args=None, + loop_continue=True, + continue_timer=False) self.password_ok_stmt = Statement(pattern=pat.password_ok, - action=sendline, - args=None, - loop_continue=True, - continue_timer=False) + action=escape_char_callback, + args=None, + loop_continue=True, + continue_timer=True, + trim_buffer=False) self.more_prompt_stmt = Statement(pattern=pat.more_prompt, action=more_prompt_handler, args=None, loop_continue=True, - continue_timer=False) + continue_timer=False, + trim_buffer=False) self.confirm_prompt_stmt = Statement(pattern=pat.confirm_prompt, action=sendline, args=None, @@ -340,22 +689,22 @@ def __init__(self): continue_timer=False) self.continue_connect_stmt = Statement(pattern=pat.continue_connect, - action=ssh_continue_connecting, - args=None, - loop_continue=True, - continue_timer=False) + action=ssh_continue_connecting, + args=None, + loop_continue=True, + continue_timer=False) self.hit_enter_stmt = Statement(pattern=pat.hit_enter, - action=wait_and_enter, - args=None, - loop_continue=True, - continue_timer=False) + action=wait_and_enter, + args=None, + loop_continue=True, + continue_timer=False) self.press_ctrlx_stmt = Statement(pattern=pat.press_ctrlx, - action=wait_and_enter, - args=None, - loop_continue=True, - continue_timer=False) + action=wait_and_enter, + args=None, + loop_continue=True, + continue_timer=False) self.init_conf_stmt = Statement(pattern=pat.setup_dialog, action='sendline(no)', @@ -364,16 +713,16 @@ def __init__(self): continue_timer=False) self.mgmt_setup_stmt = Statement(pattern=pat.enter_basic_mgmt_setup, - action='send(\x03)', # Ctrl-C - args=None, - loop_continue=True, - continue_timer=False) + action='send(\x03)', # Ctrl-C + args=None, + loop_continue=True, + continue_timer=False) self.clear_kerberos_no_realm = Statement(pattern=pat.kerberos_no_realm, - action=sendline, - args=None, - loop_continue=True, - continue_timer=False) + action=sendline, + args=None, + loop_continue=True, + continue_timer=False) self.connected_stmt = Statement(pattern=pat.connected, action=sendline, @@ -381,6 +730,69 @@ def __init__(self): loop_continue=True, continue_timer=False) + self.passphrase_stmt = Statement(pattern=pat.passphrase_prompt, + action=passphrase_handler, + args=None, + loop_continue=True, + continue_timer=False) + + self.sudo_stmt = Statement(pattern=pat.sudo_password_prompt, + action=sudo_password_handler, + args=None, + loop_continue=True, + continue_timer=False) + + self.syslog_msg_stmt = Statement(pattern=pat.syslog_message_pattern, + action=syslog_wait_send_return, + args=None, + loop_continue=True, + trim_buffer=False, + continue_timer=False) + + self.syslog_stripper_stmt = Statement(pattern=pat.syslog_message_pattern, + action=syslog_stripper, + args=None, + loop_continue=True, + trim_buffer=False, + continue_timer=False) + + self.enter_your_selection_stmt = Statement(pattern=pat.enter_your_selection_2, + action=setup_enter_selection, + args=None, + loop_continue=True, + continue_timer=True) + + self.press_any_key_stmt = Statement(pattern=pat.press_any_key, + action='sendline()', + args=None, + loop_continue=True, + continue_timer=False) + + self.permission_denied_stmt = Statement(pattern=pat.permission_denied, + action=permission_denied_handler, + args=None, + loop_continue=False, + continue_timer=False) + + self.terminal_position_stmt = Statement(pattern=pat.get_cursor_position, + action=terminal_position_handler, + args=None, + loop_continue=True, + continue_timer=False) + + self.enter_your_encryption_selection_stmt = Statement(pattern=pat.enter_your_encryption_selection_2, + action=setup_enter_selection, + args=None, + loop_continue=True, + continue_timer=True) + + self.tclsh_continue_stmt = Statement(pattern=pat.tclsh_continue, + action="sendline(})", + args=None, + loop_continue=True, + continue_timer=False) + + ############################################################# # Statement lists ############################################################# @@ -391,14 +803,20 @@ def __init__(self): # Initial connection Statements ############################################################# -pre_connection_statement_list = [generic_statements.escape_char_stmt, +pre_connection_statement_list = [# Ensure connection error statements are at the top to handle failures + # before other statements + generic_statements.connection_refused_stmt, + # other statements + generic_statements.escape_char_stmt, generic_statements.press_return_stmt, generic_statements.continue_connect_stmt, - generic_statements.connection_refused_stmt, generic_statements.disconnect_error_stmt, generic_statements.hit_enter_stmt, generic_statements.press_ctrlx_stmt, generic_statements.connected_stmt, + generic_statements.syslog_msg_stmt, + generic_statements.press_any_key_stmt, + generic_statements.permission_denied_stmt, ] ############################################################# @@ -407,10 +825,15 @@ def __init__(self): authentication_statement_list = [generic_statements.bad_password_stmt, generic_statements.login_incorrect, + generic_statements.no_password_set_stmt, generic_statements.login_stmt, generic_statements.useraccess_stmt, + generic_statements.new_password_stmt, generic_statements.password_stmt, - generic_statements.clear_kerberos_no_realm + generic_statements.clear_kerberos_no_realm, + generic_statements.password_ok_stmt, + generic_statements.passphrase_stmt, + generic_statements.enable_secret_stmt ] ############################################################# @@ -418,10 +841,11 @@ def __init__(self): ############################################################# initial_statement_list = [generic_statements.init_conf_stmt, - generic_statements.mgmt_setup_stmt - ] - -connection_statement_list = authentication_statement_list + initial_statement_list + pre_connection_statement_list + generic_statements.mgmt_setup_stmt, + generic_statements.enter_your_selection_stmt, + generic_statements.enter_your_encryption_selection_stmt, + generic_statements.tclsh_continue_stmt + ] ############################################################ @@ -429,3 +853,14 @@ def __init__(self): ############################################################# default_statement_list = [generic_statements.more_prompt_stmt] + +connection_statement_list = \ + default_statement_list + \ + authentication_statement_list + \ + initial_statement_list + \ + pre_connection_statement_list + +disable_enable_transition_statements = [generic_statements.password_stmt, + generic_statements.enable_password_stmt, + generic_statements.bad_password_stmt, + generic_statements.syslog_stripper_stmt] diff --git a/src/unicon/plugins/generic/utils.py b/src/unicon/plugins/generic/utils.py index e33b4468..31d2f636 100644 --- a/src/unicon/plugins/generic/utils.py +++ b/src/unicon/plugins/generic/utils.py @@ -32,59 +32,16 @@ def get_redundancy_details(self, connection, timeout=None, who='my'): redundancy_details['role'] = "standby" redundancy_details['state'] =\ show_red_out[show_red_out.find('-') + 1:].strip() + elif re.search("DISABLED|disabled", show_red_out): + redundancy_details['role'] = "disabled" + redundancy_details['state'] =\ + show_red_out[show_red_out.find('-') + 1:].strip() show_red_out = connection.execute( "show redundancy sta | inc Redundancy State") redundancy_details['mode'] =\ show_red_out[show_red_out.find("=") + 1:].strip() return redundancy_details - def retry_state_machine_go_to(self, - state_machine, - to_state, - spawn, - retries, - retry_sleep, - context=AttributeDict(), - dialog=None, - timeout=None, - hop_wise=False, - prompt_recovery=False): - for index in range(retries + 1): - try: - state_machine.go_to(to_state, - spawn, - context=context, - dialog=dialog, - timeout=timeout, - hop_wise=hop_wise, - prompt_recovery=prompt_recovery) - break - except Exception as err: - if index == retries: - raise SubCommandFailure(err, spawn.buffer) - time.sleep(retry_sleep) - - def retry_handle_state_machine_go_to(self, - handle, - to_state, - retries, - retry_sleep, - context=AttributeDict(), - dialog=None, - timeout=None, - hop_wise=False, - prompt_recovery=False): - self.retry_state_machine_go_to(handle.state_machine, - to_state, - handle.spawn, - retries, - retry_sleep, - context=context, - dialog=dialog, - timeout=timeout, - hop_wise=hop_wise, - prompt_recovery=prompt_recovery) - def flatten_splitlines_command(self, command): if isinstance(command, str): for item in command.splitlines(): diff --git a/src/unicon/plugins/hvrp/__init__.py b/src/unicon/plugins/hvrp/__init__.py new file mode 100644 index 00000000..011cdef5 --- /dev/null +++ b/src/unicon/plugins/hvrp/__init__.py @@ -0,0 +1,40 @@ +""" +Module: + unicon.plugins.hvrp +Authors: + Miguel Botia (mibotiaf@cisco.com), Leonardo Anez (leoanez@cisco.com) +Description: + This subpackage implements Huawei VRP devices +""" + +from unicon.bases.routers.connection import BaseSingleRpConnection +from unicon.plugins.hvrp.connection_provider import HvrpSingleRpConnectionProvider +from .statemachine import HvrpSingleRpStateMachine +from unicon.plugins.hvrp.settings import HvrpSettings +from unicon.plugins.generic import ServiceList, service_implementation as gsvc +from unicon.plugins.hvrp import service_implementation as svc + + +class HvrpServiceList(ServiceList): + def __init__(self): + super().__init__() + self.send = svc.Send + self.sendline = svc.Sendline + self.expect = svc.Expect + self.execute = svc.Execute + self.configure = svc.Configure + self.enable = svc.Enable + self.disable = svc.Disable + self.log_user = svc.LogUser + self.bash_console = svc.BashService + self.expect_log = gsvc.ExpectLogging + + +class HvrpSingleRpConnection(BaseSingleRpConnection): + os = 'hvrp' + platform = None + chassis_type = 'single_rp' + state_machine_class = HvrpSingleRpStateMachine + connection_provider_class = HvrpSingleRpConnectionProvider + subcommand_list = HvrpServiceList + settings = HvrpSettings() diff --git a/src/unicon/plugins/hvrp/connection_provider.py b/src/unicon/plugins/hvrp/connection_provider.py new file mode 100644 index 00000000..d06d48ea --- /dev/null +++ b/src/unicon/plugins/hvrp/connection_provider.py @@ -0,0 +1,53 @@ +""" +Module: + unicon.plugins.hvrp +Authors: + Miguel Botia (mibotiaf@cisco.com), Leonardo Anez (leoanez@cisco.com) +Description: + This Module implements two methods for conection and disconnection for HVRP devices. +""" + +from time import sleep +from unicon.bases.routers.connection_provider import \ + BaseSingleRpConnectionProvider +from unicon.eal.dialogs import Dialog +from .statements import connection_statement_list +from unicon.plugins.generic.statements import custom_auth_statements + + +class HvrpSingleRpConnectionProvider(BaseSingleRpConnectionProvider): + """ Implements Hvrp singleRP Connection Provider, + This class overrides the base class with the + additional dialogs and steps required for + connecting to any device via generic implementation + """ + + def __init__(self, *args, **kwargs): + """ Initializes the generic connection provider + """ + super().__init__(*args, **kwargs) + + def get_connection_dialog(self): + """ creates and returns a Dialog to handle all device prompts + appearing during initial connection to the device. + See statements.py for connnection statement lists + """ + con = self.connection + custom_auth_stmt = custom_auth_statements( + self.connection.settings.LOGIN_PROMPT, + self.connection.settings.PASSWORD_PROMPT) + return con.connect_reply \ + + Dialog(custom_auth_stmt + connection_statement_list + if custom_auth_stmt else connection_statement_list) + + def disconnect(self): + """ Logout and disconnect from the device + """ + con = self.connection + if con.connected: + con.log.info('Disconnecting...') + con.sendline('quit') + sleep(2) + con.expect('.*') + con.log.info('Closing connection...') + con.spawn.close() diff --git a/src/unicon/plugins/hvrp/patterns.py b/src/unicon/plugins/hvrp/patterns.py new file mode 100644 index 00000000..76ddc28d --- /dev/null +++ b/src/unicon/plugins/hvrp/patterns.py @@ -0,0 +1,42 @@ +""" +Module: + unicon.plugins.hvrp +Authors: + Miguel Botia (mibotiaf@cisco.com), Leonardo Anez (leoanez@cisco.com) +Description: + Module for defining all the Patterns required for the HVRP implementation. +""" + +from unicon.patterns import UniconCorePatterns + + +class HvrpPatterns(UniconCorePatterns): + + """ + Class defines all the patterns required + for Hvrp + """ + def __init__(self): + super().__init__() + self.username = r'^.*[Ll]ogin:' + self.password = r'^.*[Pp]assword:' + + # + self.enable_prompt = r'^(.*)\<%N.*\>$' + + # [~HOSTNAME] # two-stage config mode + # [HOSTNAME] # immediate config mode + self.config_prompt = r'^.*\[(?P~|\*)?%N.*\]' + + # Exit with uncommitted changes? [yes,no] (yes) + self.commit_changes_prompt = r'Exit with uncommitted changes? [yes,no] (yes)\s*' + + self.save_prompt = r'^(.*)Warning: All the configuration will be saved to the next startup configuration. Continue\? \[y\/n\]:' + + self.reboot_prompt = r'^(.*)System will reboot! Continue\? \[y\/n\]:' + + # Correct Password + self.password_ok = r'Password OK\s*' + + # Bad Password + self.bad_passwords = r'Permission denied.*' diff --git a/src/unicon/plugins/hvrp/service_implementation.py b/src/unicon/plugins/hvrp/service_implementation.py new file mode 100644 index 00000000..4d262a0e --- /dev/null +++ b/src/unicon/plugins/hvrp/service_implementation.py @@ -0,0 +1,43 @@ +""" +Module: + unicon.plugins.hvrp +Authors: + Miguel Botia (mibotiaf@cisco.com), Leonardo Anez (leoanez@cisco.com) +Description: + This subpackage implements services specific to HVRP. +""" + +from unicon.plugins.generic.service_implementation import BashService, \ + Send, Sendline, \ + Expect, Execute, \ + Configure ,\ + Enable, Disable, \ + LogUser + + +class Configure(Configure): + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.start_state = 'config' + self.end_state = 'enable' + self.service_name = 'config' + + def pre_service(self, *args, **kwargs): + super().pre_service(*args, **kwargs) + + # Check if device is operating in two-stage configuration mode. + # ============================================================= + spawn = self.get_spawn() + two_stage = spawn.match.last_match.groupdict().get('two_stage') + + # In the two-stage mode, if the user has modified configurations but has + # not submit the modification, the system prompt ~ is changed to *, + # prompting the user that the configurations are not submitted. After + # the user runs the commit command to submit the configurations, the + # system prompt * is restored to ~. + + if two_stage: + self.commit_cmd = 'commit' + else: + self.commit_cmd = '' diff --git a/src/unicon/plugins/hvrp/services.py b/src/unicon/plugins/hvrp/services.py new file mode 100644 index 00000000..e42aad8a --- /dev/null +++ b/src/unicon/plugins/hvrp/services.py @@ -0,0 +1,33 @@ +import logging + +from unicon.plugins.generic.service_implementation import Execute as GenericExec +from unicon.plugins.ios.iosv import IosvServiceList + +logger = logging.getLogger(__name__) + + +class Execute(GenericExec): + ''' + Demonstrating how to augment an existing service by updating its call + service method + ''' + + def call_service(self, *args, **kwargs): + # custom... code here + logger.info('execute service called') + + # call parent + super().call_service(*args, **kwargs) + + +class VrpServiceList(IosvServiceList): + ''' + class aggregating all service lists for this platform + ''' + + def __init__(self): + # use the parent services + super().__init__() + + # overwrite and add our own + self.execute = Execute diff --git a/src/unicon/plugins/hvrp/settings.py b/src/unicon/plugins/hvrp/settings.py new file mode 100644 index 00000000..848614d1 --- /dev/null +++ b/src/unicon/plugins/hvrp/settings.py @@ -0,0 +1,28 @@ +""" +Module: + unicon.plugins.hvrp +Authors: + Miguel Botia (mibotiaf@cisco.com), Leonardo Anez (leoanez@cisco.com) +Description: + This module defines the HVRP settings to setup the unicon environment required for generic based unicon connection. +""" + +from unicon.plugins.generic import GenericSettings + + +class HvrpSettings(GenericSettings): + """" Hvrp platform settings """ + + def __init__(self): + super().__init__() + self.HA_INIT_EXEC_COMMANDS = [ + 'screen-length 0 temporary', + 'undo terminal alarm', + 'undo terminal logging', + 'undo terminal debugging', + 'undo terminal monitor' + ] + + self.HA_INIT_CONFIG_COMMANDS = [] + self.ERROR_PATTERN.append("Error:.*") + self.CONFIGURE_ERROR_PATTERN.append(r'^Error:.*') diff --git a/src/unicon/plugins/hvrp/statemachine.py b/src/unicon/plugins/hvrp/statemachine.py new file mode 100644 index 00000000..ce50808d --- /dev/null +++ b/src/unicon/plugins/hvrp/statemachine.py @@ -0,0 +1,56 @@ +""" +Module: + unicon.plugins.hvrp +Authors: + Miguel Botia (mibotiaf@cisco.com), Leonardo Anez (leoanez@cisco.com) +Description: + This module implements a HVRP state machine. +""" + +from unicon.plugins.hvrp.patterns import HvrpPatterns +from unicon.statemachine import State, Path, StateMachine +from unicon.eal.dialogs import Dialog + +from unicon.plugins.hvrp.statements import default_statement_list + +patterns = HvrpPatterns() + + +class HvrpSingleRpStateMachine(StateMachine): + + """ + Defines Hvrp StateMachine for singleRP + Statemachine keeps in track all the supported states + for this platform, also have detail about moving from + one state to another + """ + + + def create(self): + """creates the hvrp state machine""" + + ########################################################## + # State Definition + ########################################################## + enable = State('enable', patterns.enable_prompt) + config = State('config', patterns.config_prompt) + + """creates the hvrp Paths machine""" + ########################################################## + # Path Definition + ########################################################## + config_dialog = Dialog([ + [patterns.commit_changes_prompt, 'sendline(yes)', None, True, False], + ]) + + enable_to_config = Path(enable, config, 'system-view', None) + config_to_enable = Path(config, enable, 'return', config_dialog) + + + # Add State and Path to State Machine + self.add_state(enable) + self.add_state(config) + + self.add_path(enable_to_config) + self.add_path(config_to_enable) + self.add_default_statements(default_statement_list) diff --git a/src/unicon/plugins/hvrp/statements.py b/src/unicon/plugins/hvrp/statements.py new file mode 100644 index 00000000..f8c3bb5e --- /dev/null +++ b/src/unicon/plugins/hvrp/statements.py @@ -0,0 +1,90 @@ +""" +Module: + unicon.plugins.hvrp +Authors: + Miguel Botia (mibotiaf@cisco.com), Leonardo Anez (leoanez@cisco.com) +Description: + Module for defining all the Statements and callback required for the + Current implementation +""" +from unicon.eal.dialogs import Statement +from unicon.plugins.hvrp.patterns import HvrpPatterns +from unicon.plugins.generic.statements import pre_connection_statement_list, \ + login_handler, user_access_verification, \ + password_handler, bad_password_handler, \ + incorrect_login_handler + +pat = HvrpPatterns() + + +############################################################# +# Hvrp statements +############################################################# + +class HvrpStatements(object): + """ + Class that defines All the Statements for Hvrp platform + implementation + """ + + def __init__(self): + ''' + All hvrp Statements + ''' + + self.bad_password_stmt = Statement(pattern=pat.bad_passwords, + action=bad_password_handler, + args=None, + loop_continue=False, + continue_timer=False) + + self.login_incorrect = Statement(pattern=pat.login_incorrect, + action=incorrect_login_handler, + args=None, + loop_continue=True, + continue_timer=False) + + self.login_stmt = Statement(pattern=pat.username, + action=login_handler, + args=None, + loop_continue=True, + continue_timer=False) + self.useraccess_stmt = Statement(pattern=pat.useracess, + action=user_access_verification, + args=None, + loop_continue=True, + continue_timer=False) + self.password_stmt = Statement(pattern=pat.password, + action=password_handler, + args=None, + loop_continue=True, + continue_timer=False) + self.save_config_notice = Statement(pattern=r'(\[y\/n\])', + action=lambda + spawn: spawn.sendline('y'), + args=None, + loop_continue=True, + continue_timer=False) + + +############################################################# +# Statement lists +############################################################# + +hvrp_statements = HvrpStatements() + +############################################################# +# Authentication Statements +############################################################# + +authentication_statement_list = [hvrp_statements.bad_password_stmt, + hvrp_statements.login_incorrect, + hvrp_statements.login_stmt, + hvrp_statements.useraccess_stmt, + hvrp_statements.password_stmt + ] + +connection_statement_list = authentication_statement_list + \ + pre_connection_statement_list + +default_statement_list = [hvrp_statements.save_config_notice] diff --git a/src/unicon/plugins/ios/ap/__init__.py b/src/unicon/plugins/ios/ap/__init__.py index aa5b1d53..9ca76210 100644 --- a/src/unicon/plugins/ios/ap/__init__.py +++ b/src/unicon/plugins/ios/ap/__init__.py @@ -17,7 +17,7 @@ def __init__(self): class ApSingleRpConnection(BaseSingleRpConnection): os = 'ios' - series = 'ap' + platform = 'ap' chassis_type = 'single_rp' state_machine_class = GenericSingleRpStateMachine connection_provider_class = GenericSingleRpConnectionProvider diff --git a/src/unicon/plugins/ios/ap/settings.py b/src/unicon/plugins/ios/ap/settings.py index 38307bd7..f31e5f69 100644 --- a/src/unicon/plugins/ios/ap/settings.py +++ b/src/unicon/plugins/ios/ap/settings.py @@ -11,5 +11,6 @@ def __init__(self): 'debug capwap console cli', 'terminal length 0', 'terminal width 0', - 'show version' + 'show version', + 'logging console disable' ] diff --git a/src/unicon/plugins/ios/iol/__init__.py b/src/unicon/plugins/ios/iol/__init__.py index 3c8460cf..3fe1b0cc 100644 --- a/src/unicon/plugins/ios/iol/__init__.py +++ b/src/unicon/plugins/ios/iol/__init__.py @@ -13,5 +13,5 @@ def __init__(self): class IosIolDualRPConnection(GenericDualRPConnection): os = 'ios' - series = 'iol' + platform = 'iol' subcommand_list = IosIolHAServiceList diff --git a/src/unicon/plugins/ios/iol/service_implementation.py b/src/unicon/plugins/ios/iol/service_implementation.py index af07b41c..dbe4d430 100644 --- a/src/unicon/plugins/ios/iol/service_implementation.py +++ b/src/unicon/plugins/ios/iol/service_implementation.py @@ -11,6 +11,5 @@ class IosIolSwitchoverService(HAReloadService): def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) - self.service_name = 'switchover' self.command = 'redundancy switch-activity force' self.dialog = Dialog(ios_iol_ha_reload_statement_list) diff --git a/src/unicon/plugins/ios/iosv/__init__.py b/src/unicon/plugins/ios/iosv/__init__.py index a934953a..97e5fb63 100644 --- a/src/unicon/plugins/ios/iosv/__init__.py +++ b/src/unicon/plugins/ios/iosv/__init__.py @@ -20,7 +20,7 @@ def __init__(self): class IosvSingleRpConnection(IosSingleRpConnection): os = 'ios' - series = 'iosv' + platform = 'iosv' chassis_type = 'single_rp' state_machine_class = IosvSingleRpStateMachine subcommand_list = IosvServiceList diff --git a/src/unicon/plugins/ios/pagent/__init__.py b/src/unicon/plugins/ios/pagent/__init__.py new file mode 100644 index 00000000..ed25be14 --- /dev/null +++ b/src/unicon/plugins/ios/pagent/__init__.py @@ -0,0 +1,21 @@ +__author__ = "Myles Dear " + + +from unicon.plugins.ios import IosServiceList, IosSingleRpConnection +from unicon.plugins.ios.settings import IosSettings +from .statemachine import IosPagentSingleRpStateMachine +from ..settings import IosSettings + + +class IosPagentServiceList(IosServiceList): + def __init__(self): + super().__init__() + + +class IosvSingleRpConnection(IosSingleRpConnection): + os = 'ios' + platform = 'pagent' + chassis_type = 'single_rp' + state_machine_class = IosPagentSingleRpStateMachine + subcommand_list = IosPagentServiceList + settings = IosSettings() diff --git a/src/unicon/plugins/ios/pagent/patterns.py b/src/unicon/plugins/ios/pagent/patterns.py new file mode 100644 index 00000000..e6271d50 --- /dev/null +++ b/src/unicon/plugins/ios/pagent/patterns.py @@ -0,0 +1,8 @@ + +from unicon.plugins.generic.patterns import GenericPatterns + +class IosPagentPatterns(GenericPatterns): + + def __init__(self): + super().__init__() + self.emu_prompt = r'^(.*?)([\w-]+)\(\w+:.*?\)#\s*$' diff --git a/src/unicon/plugins/ios/pagent/statemachine.py b/src/unicon/plugins/ios/pagent/statemachine.py new file mode 100644 index 00000000..d9d7cb5d --- /dev/null +++ b/src/unicon/plugins/ios/pagent/statemachine.py @@ -0,0 +1,36 @@ +__author__ = "Myles Dear " + +from unicon.plugins.generic.statemachine import GenericSingleRpStateMachine +from unicon.plugins.generic.statements import GenericStatements +from unicon.statemachine import State, Path, StateMachine +from unicon.eal.dialogs import Dialog +from .statements import IosPagentStatements +from .patterns import IosPagentPatterns + +statements = GenericStatements() +patterns = IosPagentPatterns() +ios_pagent_statements = IosPagentStatements() + + +class IosPagentSingleRpStateMachine(GenericSingleRpStateMachine): + + def create(self): + super().create() + + # Overload disable->enable path to account for Pagent key entry + self.remove_path('disable', 'enable') + enable = [state for state in self.states if state.name == 'enable'][0] + disable = [state for state in self.states \ + if state.name == 'disable'][0] + disable_to_enable = Path(disable, enable, 'enable', + Dialog([ + statements.password_stmt, + ios_pagent_statements.pagent_lic_stmt])) + self.add_path(disable_to_enable) + + emu = State('emu', pattern=patterns.emu_prompt) + + emu_to_enable = Path(emu, enable, 'end', None) + + self.add_state(emu) + self.add_path(emu_to_enable) diff --git a/src/unicon/plugins/ios/pagent/statements.py b/src/unicon/plugins/ios/pagent/statements.py new file mode 100644 index 00000000..f6e037ed --- /dev/null +++ b/src/unicon/plugins/ios/pagent/statements.py @@ -0,0 +1,43 @@ +__author__ = "Myles Dear " + +import re + + +from unicon.eal.dialogs import Statement +from unicon.plugins.generic.patterns import GenericPatterns + +patterns = GenericPatterns() + +def enter_license_handler(spawn, context): + mid = '' + m = re.search(r'Machine ID: (?P\d+)', spawn.match.match_output) + if m: + mid = m['mid'] + try: + spawn.sendline(context['pagent_key']) + spawn.expect(r'.*is valid.*done') + spawn.expect(r'.*> *$') + spawn.sendline('enable') + except KeyError: + raise Exception("Could not find Pagent key for Machine ID {}.".\ + format(mid)) + + +class IosPagentStatements(): + """ + Class that defines All the Statements for Pagent platform + implementation + """ + + def __init__(self): + ''' + All pagent Statements + ''' + self.pagent_lic_stmt = Statement( + pattern=patterns.enter_license, + action=enter_license_handler, + loop_continue=True, + continue_timer=False) + + + diff --git a/src/unicon/plugins/ios/settings.py b/src/unicon/plugins/ios/settings.py index b41fb831..b0be4b4f 100644 --- a/src/unicon/plugins/ios/settings.py +++ b/src/unicon/plugins/ios/settings.py @@ -14,4 +14,6 @@ def __init__(self): r'^%\s*[Ii]ncomplete (command|input)', r'^%\s*[Aa]mbiguous (command|input)' ] - + self.CONFIGURE_ERROR_PATTERN = [ + r'^%\s*[Ii]nvalid (command|input|number)' + ] diff --git a/src/unicon/plugins/iosxe/__init__.py b/src/unicon/plugins/iosxe/__init__.py index 923e2d8e..c41f9fc1 100644 --- a/src/unicon/plugins/iosxe/__init__.py +++ b/src/unicon/plugins/iosxe/__init__.py @@ -9,11 +9,10 @@ from unicon.bases.routers.connection import BaseSingleRpConnection from unicon.plugins.iosxe.statemachine import IosXESingleRpStateMachine from unicon.plugins.iosxe.statemachine import IosXEDualRpStateMachine -from unicon.plugins.generic import GenericSingleRpConnectionProvider,\ - GenericDualRPConnection +from unicon.plugins.iosxe.connection_provider import IosxeSingleRpConnectionProvider +from unicon.plugins.generic import GenericDualRPConnection from unicon.plugins.iosxe.settings import IosXESettings -from unicon.plugins.generic.service_implementation import Reload from unicon.plugins.iosxe import service_implementation as svc @@ -26,6 +25,13 @@ def __init__(self): self.ping = svc.Ping self.traceroute = svc.Traceroute self.bash_console = svc.BashService + self.copy = svc.Copy + self.reload = svc.Reload + self.rommon = svc.Rommon + self.tclsh = svc.Tclsh + self.maintenance_mode = svc.MaintenanceMode + self.config_syntax = svc.ConfigSyntax + self.enable = svc.Enable class HAIosXEServiceList(HAServiceList): @@ -39,21 +45,28 @@ def __init__(self): self.switchover = svc.HASwitchover self.ping = svc.Ping self.bash_console = svc.BashService + self.traceroute = svc.Traceroute + self.copy = svc.Copy + self.reset_standby_rp = svc.ResetStandbyRP + self.rommon = svc.HARommon + self.tclsh = svc.Tclsh + self.config_syntax = svc.ConfigSyntax + self.enable = svc.Enable class IosXESingleRpConnection(BaseSingleRpConnection): os = 'iosxe' - series = None + platform = None chassis_type = 'single_rp' state_machine_class = IosXESingleRpStateMachine - connection_provider_class = GenericSingleRpConnectionProvider + connection_provider_class = IosxeSingleRpConnectionProvider subcommand_list = IosXEServiceList settings = IosXESettings() class IosXEDualRPConnection(GenericDualRPConnection): os = 'iosxe' - series = None + platform = None chassis_type = 'dual_rp' subcommand_list = HAIosXEServiceList state_machine_class = IosXEDualRpStateMachine diff --git a/src/unicon/plugins/iosxe/c8kv/__init__.py b/src/unicon/plugins/iosxe/c8kv/__init__.py new file mode 100644 index 00000000..76000a97 --- /dev/null +++ b/src/unicon/plugins/iosxe/c8kv/__init__.py @@ -0,0 +1,15 @@ + +from unicon.plugins.iosxe import IosXEServiceList, IosXESingleRpConnection +from unicon.plugins.iosxe.c8kv.statemachine import IosXEC8kvSingleRpStateMachine +from unicon.internal.plugins.iosxe.connection_provider import InternalIosxeSingleRpConnectionProvider + +class IosXEC8kvServiceList(IosXEServiceList): + def __init__(self): + super().__init__() + +class IosXEC8kvSingleRpConnection(IosXESingleRpConnection): + platform = 'c8kv' + os = 'iosxe' + state_machine_class = IosXEC8kvSingleRpStateMachine + connection_provider_class = InternalIosxeSingleRpConnectionProvider + subcommand_list = IosXEC8kvServiceList diff --git a/src/unicon/plugins/iosxe/c8kv/statemachine.py b/src/unicon/plugins/iosxe/c8kv/statemachine.py new file mode 100644 index 00000000..3523d978 --- /dev/null +++ b/src/unicon/plugins/iosxe/c8kv/statemachine.py @@ -0,0 +1,77 @@ +""" +State machine definition for Cisco Catalyst 8000V (C8KV) virtual router. + +This module provides the custom state machine for C8KV devices, handling +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.plugins.generic.patterns import GenericPatterns +from unicon.plugins.iosxe.cat8k.service_statements import ( + reload_to_rommon_statement_list) + +generic_patterns = GenericPatterns() # Uses generic patterns to support GRUB prompt + +class IosXEC8kvSingleRpStateMachine(IosXESingleRpStateMachine): + """State machine for single RP Cisco Catalyst 8000V devices. + + This state machine extends IosXESingleRpStateMachine with C8KV-specific + behavior for GRUB boot mode and rommon recovery. Key changes: + + - Uses GenericPatterns.rommon_prompt which includes 'grub>' pattern + - Modified rommon->disable path to support GRUB command line booting + - Custom reload-to-rommon path for golden image recovery + """ + def create(self): + """Create and configure the C8KV state machine. + + This method extends the parent IosXE state machine with C8KV-specific + modifications: + + 1. Updates rommon state pattern to use generic_patterns.rommon_prompt + which includes support for GRUB prompt ('grub>') + 2. Removes default rommon->disable and enable->rommon paths + 3. Adds custom rommon->disable path using C8KV boot statements + 4. Adds custom enable->rommon path for reload operations + + The custom paths ensure proper handling of GRUB bootloader and + golden image recovery scenarios specific to C8KV virtual routers. + + Returns: + None + """ + super().create() + + # 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) \ No newline at end of file diff --git a/src/unicon/plugins/iosxe/c8kv/statements.py b/src/unicon/plugins/iosxe/c8kv/statements.py new file mode 100644 index 00000000..479b281f --- /dev/null +++ b/src/unicon/plugins/iosxe/c8kv/statements.py @@ -0,0 +1,58 @@ +import re +import time +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 + +log = logging.getLogger(__name__) +reload_patterns = IosXEReloadPatterns() +patterns = IosXEPatterns() + +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 + else: + raise Exception("Too many failed boot attempts have been detected.") + + +boot_from_rommon_stmt = Statement( + pattern=patterns.rommon_prompt, + action=boot_image, + 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 +] \ No newline at end of file diff --git a/src/unicon/plugins/iosxe/cat3k/__init__.py b/src/unicon/plugins/iosxe/cat3k/__init__.py index 31477362..3441f5ad 100644 --- a/src/unicon/plugins/iosxe/cat3k/__init__.py +++ b/src/unicon/plugins/iosxe/cat3k/__init__.py @@ -16,7 +16,7 @@ def __init__(self): class IosXECat3kSingleRpConnection(IosXESingleRpConnection): - series = 'cat3k' + platform = 'cat3k' os = 'iosxe' chassis_type = 'single_rp' state_machine_class = IosXECat3kSingleRpStateMachine diff --git a/src/unicon/plugins/iosxe/cat3k/patterns.py b/src/unicon/plugins/iosxe/cat3k/patterns.py index 22e2d637..2b45ac37 100644 --- a/src/unicon/plugins/iosxe/cat3k/patterns.py +++ b/src/unicon/plugins/iosxe/cat3k/patterns.py @@ -6,5 +6,4 @@ class IosXECat3kPatterns(IosXEPatterns): def __init__(self): super().__init__() - self.rommon_prompt = r'(.*)switch:\s?$' self.tcpdump = ".*listening on lfts.*$" diff --git a/src/unicon/plugins/iosxe/cat3k/service_implementation.py b/src/unicon/plugins/iosxe/cat3k/service_implementation.py index d97feb48..e73abe23 100644 --- a/src/unicon/plugins/iosxe/cat3k/service_implementation.py +++ b/src/unicon/plugins/iosxe/cat3k/service_implementation.py @@ -9,9 +9,10 @@ from unicon.plugins.generic.service_implementation import ReloadResult from unicon.eal.dialogs import Dialog from unicon.core.errors import SubCommandFailure -from .service_statements import boot_reached, tcpdump_continue from unicon.utils import AttributeDict +from ..statements import boot_from_rommon_stmt +from .service_statements import tcpdump_continue class Reload(BaseService): """Service to reload the device. @@ -42,19 +43,33 @@ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) self.start_state = 'enable' self.end_state = 'enable' - self.service_name = 'reload' self.timeout = connection.settings.RELOAD_TIMEOUT - self.dialog = Dialog(reload_statement_list) - self.dialog.append(boot_reached) + self.dialog = Dialog(reload_statement_list + [boot_from_rommon_stmt]) def call_service(self, reload_command='reload', dialog=Dialog([]), timeout=None, return_output=False, - *args, **kwargs): + error_pattern=None, + append_error_pattern=None, + *args, + **kwargs): con = self.connection timeout = timeout or self.timeout + + if error_pattern is None: + self.error_pattern = con.settings.ERROR_PATTERN + else: + self.error_pattern = error_pattern + + if not isinstance(self.error_pattern, list): + raise ValueError('error_pattern should be a list') + if append_error_pattern: + if not isinstance(append_error_pattern, list): + raise ValueError('append_error_pattern should be a list') + self.error_pattern += append_error_pattern + assert isinstance(dialog, Dialog), "dialog passed must be an instance of Dialog" dialog += self.dialog @@ -70,10 +85,17 @@ def call_service(self, dialog = self.service_dialog(service_dialog=dialog) con.spawn.sendline(reload_command) try: - reload_op=dialog.process(con.spawn, context=context, timeout=timeout, - prompt_recovery=self.prompt_recovery) - con.state_machine.go_to(['disable', 'enable'], con.spawn, + reload_op=dialog.process(con.spawn, + context=context, + timeout=timeout, + prompt_recovery=self.prompt_recovery) + + self.result = reload_op.match_output + self.get_service_result() + + con.state_machine.go_to('enable', con.spawn, context=context, + timeout=con.connection_timeout, prompt_recovery=self.prompt_recovery) except Exception as err: raise SubCommandFailure("Reload failed : {}".format(err)) @@ -135,7 +157,6 @@ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) self.start_state = 'shell' self.end_state = 'enable' - self.service_name = 'shellexec' self.timeout = connection.settings.EXEC_TIMEOUT self.transition_timeout = connection.settings.STATE_TRANSITION_TIMEOUT self.shell_enable = False @@ -205,7 +226,6 @@ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) self.start_state = 'rommon' self.end_state = 'rommon' - self.service_name = 'rommon' self.local_end_state = None self.timeout = connection.settings.EXEC_TIMEOUT diff --git a/src/unicon/plugins/iosxe/cat3k/service_statements.py b/src/unicon/plugins/iosxe/cat3k/service_statements.py index b3ff708a..b4606115 100644 --- a/src/unicon/plugins/iosxe/cat3k/service_statements.py +++ b/src/unicon/plugins/iosxe/cat3k/service_statements.py @@ -1,7 +1,5 @@ __author__ = "Giacomo Trifilo " -import re - from unicon.eal.dialogs import Statement from .patterns import IosXECat3kPatterns from .setting import IosXECat3kSettings @@ -10,37 +8,6 @@ settings = IosXECat3kSettings() -def boot_image(spawn, context, session): - if not context.get('boot_prompt_count'): - context['boot_prompt_count'] = 1 - if context.get('boot_prompt_count') < \ - settings.MAX_ALLOWABLE_CONSECUTIVE_BOOT_ATTEMPTS: - if "image_to_boot" in context: - cmd = "boot {}".format(context['image_to_boot']) - else: - spawn.sendline('dir flash:') - dir_listing = spawn.expect('.* bytes used').match_output - m = re.search(r'(\S+\.bin)[\r\n]', dir_listing) - if m: - boot_image = m.group(1) - cmd = "boot flash:{}".format(boot_image) - else: - cmd = "boot" - spawn.sendline(cmd) - context['boot_prompt_count'] += 1 - else: - raise Exception("Too many failed boot attempts have been detected.") - -boot_reached = Statement(pattern=patterns.rommon_prompt, - action=boot_image, - loop_continue=True, - continue_timer=False) - -access_shell = Statement(pattern=patterns.access_shell, - action=lambda spawn: spawn.sendline("y"), - loop_continue=True, - continue_timer=False) - tcpdump_continue = Statement(pattern=patterns.tcpdump, action=lambda spawn: spawn.sendline(""), loop_continue=False, diff --git a/src/unicon/plugins/iosxe/cat3k/setting.py b/src/unicon/plugins/iosxe/cat3k/setting.py index 8644dfae..3d47ae92 100644 --- a/src/unicon/plugins/iosxe/cat3k/setting.py +++ b/src/unicon/plugins/iosxe/cat3k/setting.py @@ -10,4 +10,5 @@ def __init__(self): self.RELOAD_TIMEOUT = 600 self.CONNECTION_TIMEOUT = 600 # Big timeout to handle transition rommon->enable self.STATE_TRANSITION_TIMEOUT = 30 - self.MAX_ALLOWABLE_CONSECUTIVE_BOOT_ATTEMPTS = 3 + + self.BOOT_FILESYSTEM = 'flash:' diff --git a/src/unicon/plugins/iosxe/cat3k/statemachine.py b/src/unicon/plugins/iosxe/cat3k/statemachine.py index be405dbc..4fb11d79 100644 --- a/src/unicon/plugins/iosxe/cat3k/statemachine.py +++ b/src/unicon/plugins/iosxe/cat3k/statemachine.py @@ -1,12 +1,9 @@ __author__ = "Giacomo Trifilo " from unicon.plugins.iosxe.statemachine import IosXESingleRpStateMachine -from unicon.plugins.generic.statements import connection_statement_list -from unicon.plugins.generic.service_statements import reload_statement_list from .patterns import IosXECat3kPatterns from unicon.statemachine import State, Path from unicon.eal.dialogs import Dialog -from .service_statements import access_shell, boot_reached patterns = IosXECat3kPatterns() @@ -14,33 +11,3 @@ class IosXECat3kSingleRpStateMachine(IosXESingleRpStateMachine): def create(self): super().create() - - self.remove_path('enable', 'rommon') - self.remove_path('rommon', 'disable') - self.remove_state('rommon') - # incase there is no previous shell state regiestered - try: - self.remove_path('shell', 'enable') - self.remove_path('enable', 'shell') - self.remove_state('shell') - except Exception: - pass - - rommon = State('rommon', patterns.rommon_prompt) - enable_to_rommon = Path(self.get_state('enable'), rommon, 'reload', - Dialog(reload_statement_list)) - rommon_to_disable = Path(rommon, self.get_state('disable'), '\r', - Dialog(connection_statement_list + [boot_reached])) - self.add_state(rommon) - self.add_path(enable_to_rommon) - self.add_path(rommon_to_disable) - - - shell = State('shell', patterns.shell_prompt) - enable_to_shell = Path(self.get_state('enable'), - shell, 'request platform software system shell', - Dialog([access_shell])) - shell_to_enable = Path(shell, self.get_state('enable'), 'exit', None) - self.add_state(shell) - self.add_path(enable_to_shell) - self.add_path(shell_to_enable) diff --git a/src/unicon/plugins/iosxe/cat3k/tests/test.py b/src/unicon/plugins/iosxe/cat3k/tests/test.py index 692722ee..1a787ca8 100644 --- a/src/unicon/plugins/iosxe/cat3k/tests/test.py +++ b/src/unicon/plugins/iosxe/cat3k/tests/test.py @@ -15,7 +15,7 @@ os = "iosxe" chassis_type = "single_rp" -series = "cat3k" +platform = "cat3k" class TestIosXE(unittest.TestCase): @@ -26,7 +26,7 @@ def setUpClass(cls): tacacs_password=password, start=[start], os=os, - series=series) + platform=platform) cls.con.connect() @classmethod diff --git a/src/unicon/plugins/iosxe/cat4k/__init__.py b/src/unicon/plugins/iosxe/cat4k/__init__.py new file mode 100644 index 00000000..2c0c9b9c --- /dev/null +++ b/src/unicon/plugins/iosxe/cat4k/__init__.py @@ -0,0 +1,29 @@ +""" CAT4K IOS-XE connection implementation. +""" + +from unicon.plugins.iosxe import IosXESingleRpConnection, IosXEDualRPConnection + +from .. import IosXEServiceList + +from .settings import IosXECat4kSettings +from . import service_implementation as svc +from .connection_provider import Cat4kDualRpConnectionProvider + + +class IosXECat4kServiceList(IosXEServiceList): + def __init__(self): + super().__init__() + self.execute= svc.Execute + self.config=svc.Configure + self.reload=svc.Reload + +class IosXECat4kSingleRpConnection(IosXESingleRpConnection): + platform = 'cat4k' + settings=IosXECat4kSettings() + +class IosXECat4kDualRPConnection(IosXEDualRPConnection): + platform = 'cat4k' + chassis_type= 'dual_rp' + connection_provider_class=Cat4kDualRpConnectionProvider + subcommand_list = IosXECat4kServiceList + settings=IosXECat4kSettings() \ No newline at end of file diff --git a/src/unicon/plugins/iosxe/cat4k/connection_provider.py b/src/unicon/plugins/iosxe/cat4k/connection_provider.py new file mode 100644 index 00000000..ffde48d8 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat4k/connection_provider.py @@ -0,0 +1,132 @@ +""" +Authors: + Omid Mehrabian: omehrabi@cisco.com +""" + + +from unicon.bases.routers.connection_provider import BaseDualRpConnectionProvider +from unicon.plugins.generic.statements import connection_statement_list +from concurrent.futures import ThreadPoolExecutor, wait as wait_futures, FIRST_COMPLETED +from unicon.eal.dialogs import Dialog + +class Cat4kDualRpConnectionProvider(BaseDualRpConnectionProvider): + """ Implements Stack Connection Provider, + This class overrides the base class with the + additional dialogs and steps required for + connecting to stack device + """ + def __init__(self, *args, **kwargs): + + """ Initializes the base connection provider + """ + super().__init__(*args, **kwargs) + + def unlock_standby(self, *args, **kwargs): + pass + def init_standby(self, *args, **kwargs): + pass + + def designate_handles(self, *args, **kwargs): + """ Identifies the Role of each handle and designates if it is active or + standby and bring the active RP to enable state """ + + con=self.connection + con.log.info('+++ designating handles +++') + subcons = list(con._subconnections.items()) + subcon1_alias, subcon1 = subcons[0] + subcon2_alias, subcon2 = subcons[1] + + # Try to go to enable mode on both connections + for subcon in [subcon1, subcon2]: + try: + subcon.state_machine.go_to( + 'enable', + subcon.spawn, + context=subcon.context, + ) + except Exception: + pass + con.log.debug('{} in state: {}'.format(subcon.alias, subcon.state_machine.current_state)) + + if subcon1.state_machine.current_state == 'enable': + target_alias = subcon1_alias + other_alias = subcon2_alias + elif subcon2.state_machine.current_state == 'enable': + target_alias = subcon2_alias + other_alias = subcon1_alias + + con._set_active_alias(target_alias) + con._set_standby_alias(other_alias) + con._handles_designated = True + + def establish_connection(self): + + """ Reads the device state and brings both RP to the right state + """ + con = self.connection + subconnections = con.subconnections + + for subconnection in subconnections: + learn_hostname = subconnection.learn_hostname and not \ + subconnection.learned_hostname + if learn_hostname: + subconnection.state_machine.learn_hostname = True + subconnection.state_machine.learn_pattern = \ + con.settings.DEFAULT_LEARNED_HOSTNAME + + for subconnection in subconnections: + context = subconnection.context + context.update(cred_list=context.get('login_creds')) + futures = [] + + + def detect_state(subcon, dialog= None): + subcon.sendline() + try: + subcon.state_machine.go_to( + 'any', + subcon.spawn, + context=subcon.context, + prompt_recovery=subcon.prompt_recovery, + timeout=subcon.connection_timeout, + dialog=Dialog(connection_statement_list) + ) + except Exception as e: + subcon.log.info(e) + subcon.log.debug('{} in state: {}'.format(subcon.alias, subcon.state_machine.current_state)) + + executer= ThreadPoolExecutor(max_workers = len(subconnections)) + for subcon in subconnections: + futures.append(executer.submit( + # Check current state + detect_state, + subcon=subcon, + dialog=self.get_connection_dialog())) + wait_futures(futures, timeout=3, return_when=FIRST_COMPLETED) + + for subconnection in subconnections: + context = subconnection.context + context.pop('cred_list', None) + + if learn_hostname: + # Use the learned hostname in %N substitutions from this point on. + learned_hostname = con.settings.DEFAULT_LEARNED_HOSTNAME + for subconnection in subconnections: + subcon_learned_hostname = subconnection._get_learned_hostname( + spawn=subconnection.spawn) + if subcon_learned_hostname != \ + con.settings.DEFAULT_LEARNED_HOSTNAME: + learned_hostname = subcon_learned_hostname + break + if learned_hostname == con.settings.DEFAULT_LEARNED_HOSTNAME: + con.log.warning( + 'Failed to learn the hostname. ' + 'Using the default hostname pattern {}. ' + 'This may lead to unstable behavior.'.\ + format(self.connection.settings.DEFAULT_LEARNED_HOSTNAME)) + + con.learned_hostname = learned_hostname + for subconnection in subconnections: + subconnection.learned_hostname = learned_hostname + subconnection.state_machine.learn_hostname = False + diff --git a/src/unicon/plugins/iosxe/cat4k/patterns.py b/src/unicon/plugins/iosxe/cat4k/patterns.py new file mode 100644 index 00000000..9a566665 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat4k/patterns.py @@ -0,0 +1,6 @@ +from unicon.plugins.iosxe.patterns import IosXEPatterns + +class IosXECat4kPatterns(IosXEPatterns): + def __init__(self): + super().__init__() + self.restart = r'^(.*)estarting system(.*)' diff --git a/src/unicon/plugins/iosxe/cat4k/service_implementation.py b/src/unicon/plugins/iosxe/cat4k/service_implementation.py new file mode 100644 index 00000000..18eab61c --- /dev/null +++ b/src/unicon/plugins/iosxe/cat4k/service_implementation.py @@ -0,0 +1,198 @@ + +import warnings + + +from time import sleep + +from unicon.core.errors import SubCommandFailure +from unicon.eal.dialogs import Dialog + +from unicon.plugins.generic.statements import ( + custom_auth_statements, + default_statement_list) + +from unicon.plugins.generic.service_statements import ha_reload_statement_list +from unicon.plugins.generic.service_implementation import HAReloadService as BaseService + +from unicon.plugins.generic.statements import connection_statement_list + +from .service_statements import change_rp + +from unicon.plugins.iosxe.service_implementation import \ + HAConfigure as XeConfigure, \ + HAExecute as XeExecute + +class Configure(XeConfigure): + + def __init__(self, *args, **kwargs): + + super().__init__(*args, **kwargs) + + def pre_service(self, *args, **kwargs): + + con=self.connection + if 'target' in kwargs: + if kwargs['target'] == 'standby': + con.active.log.info('Could not execute any command on standby for this device') + raise NotImplementedError + super().pre_service(*args, **kwargs) + +class Execute(XeExecute): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def pre_service(self, *args, **kwargs): + con=self.connection + if 'target' in kwargs: + if kwargs['target'] == 'standby': + con.active.log.info('could not execute any command on standby for the this device') + raise NotImplementedError + super().pre_service(*args, **kwargs) + + + +class Reload(BaseService): + """ Service to reload the device. + + Arguments: + reload_command: reload command to be used. default "redundancy reload shelf" + reload_creds: credential or list of credentials to use to respond to + username/password prompts. + reply: Additional Dialog( i.e patterns) to be handled + timeout: Timeout value in sec, Default Value is 60 sec + return_output: if True, return namedtuple with result and reload output + + Returns: + console True on Success, raises SubCommandFailure on failure. + + Example: + .. code-block:: python + + rtr.reload() + # If reload command is other than 'redundancy reload shelf' + rtr.reload(reload_command="reload location all", timeout=700) + """ + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.start_state = 'enable' + self.end_state = 'enable' + self.timeout = connection.settings.RELOAD_TIMEOUT + self.dialog = Dialog(ha_reload_statement_list + default_statement_list + [change_rp]) + self.command = 'reload' + self.__dict__.update(kwargs) + + def call_service(self, # noqa: C901 + reload_command=None, + dialog=Dialog([]), + reply=Dialog([]), + timeout=None, + reload_creds=None, + return_output=False, + *args, + **kwargs): + con = self.connection + if reply: + if dialog: + con.log.warning("**** Both 'reply' and 'dialog' were provided " + "to the reload service. Ignoring 'dialog'.") + dialog = reply + elif dialog: + warnings.warn('**** "dialog" parameter is deprecated. ' + 'Use "reply" instead. ****', + category=DeprecationWarning) + + timeout = timeout or self.timeout + + command = reload_command or self.command + + fmt_str = "+++ reloading %s with reload_command %s and timeout is %s +++" + con.log.info(fmt_str % (con.hostname, command, timeout)) + dialog += self.dialog + custom_auth_stmt = custom_auth_statements(con.settings.LOGIN_PROMPT, con.settings.PASSWORD_PROMPT) + + if reload_creds: + context = con.active.context.copy() + context.update(cred_list=reload_creds) + sby_context = con.standby.context.copy() + sby_context.update(cred_list=reload_creds) + else: + context = con.active.context + sby_context = con.standby.context + + if custom_auth_stmt: + dialog += Dialog(custom_auth_stmt) + + # Issue reload command + con.active.spawn.sendline(command) + try: + dialog.process(con.active.spawn, + context=context, + prompt_recovery=self.prompt_recovery, + timeout=timeout) + except Exception as e: + raise SubCommandFailure('Error during reload', e) from e + else: + con.active.state_machine._current_state= 'stby_locked' + # Bring standby to good state. + con.log.info('Waiting for config sync to finish') + if con.standby.state_machine.current_state == 'stby_locked': + con.standby.state_machine._current_state = 'generic' + standby_wait_time = con.settings.POST_HA_RELOAD_CONFIG_SYNC_WAIT + standby_wait_interval = 50 + standby_sync_try = standby_wait_time // standby_wait_interval + 1 + for round in range(standby_sync_try): + con.standby.spawn.sendline() + try: + con.standby.state_machine.go_to( + 'any', + con.standby.spawn, + context=sby_context, + timeout=standby_wait_interval, + prompt_recovery=self.prompt_recovery, + dialog=Dialog(connection_statement_list) + ) + break + except Exception as err: + if round == standby_sync_try - 1: + raise Exception( + 'Bringing standby to any state failed within {} sec'.format(standby_wait_time)) from err + + except Exception as err: + raise SubCommandFailure("Reload failed : %s" % err) from err + # Re-designate handles before applying config. + # Roles could have switched as a result of the reload. + con.connection_provider.designate_handles() + con.active.state_machine.go_to('enable', + con.active.spawn, + prompt_recovery=self.prompt_recovery, + context=context) + + # Issue init commands to disable console logging + exec_commands = con.active.settings.HA_INIT_EXEC_COMMANDS + for exec_command in exec_commands: + con.execute(exec_command, prompt_recovery=self.prompt_recovery) + config_commands = con.active.settings.HA_INIT_CONFIG_COMMANDS + + config_lock_retries_ori = con.settings.CONFIG_LOCK_RETRIES + config_lock_retry_sleep_ori = con.settings.CONFIG_LOCK_RETRY_SLEEP + con.active.settings.CONFIG_LOCK_RETRY_SLEEP = con.active.settings.CONFIG_POST_RELOAD_RETRY_DELAY_SEC + con.active.settings.CONFIG_LOCK_RETRIES = con.active.settings.CONFIG_POST_RELOAD_MAX_RETRIES + + try: + con.configure(config_commands, + target='active', + prompt_recovery=self.prompt_recovery) + except Exception: + raise + finally: + con.settings.CONFIG_LOCK_RETRIES = config_lock_retries_ori + con.settings.CONFIG_LOCK_RETRY_SLEEP = config_lock_retry_sleep_ori + + + + con.log.info("+++ Reload Completed Successfully +++") + self.result = True + + diff --git a/src/unicon/plugins/iosxe/cat4k/service_statements.py b/src/unicon/plugins/iosxe/cat4k/service_statements.py new file mode 100644 index 00000000..17d73133 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat4k/service_statements.py @@ -0,0 +1,12 @@ +from unicon.eal.dialogs import Statement +from .patterns import IosXECat4kPatterns +from .settings import IosXECat4kSettings + +patterns = IosXECat4kPatterns() +settings = IosXECat4kSettings() + + +change_rp = Statement(pattern=patterns.restart, + action=lambda spawn: spawn.close, + loop_continue=False, + continue_timer=False) diff --git a/src/unicon/plugins/iosxe/cat4k/settings.py b/src/unicon/plugins/iosxe/cat4k/settings.py new file mode 100644 index 00000000..3d83ba24 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat4k/settings.py @@ -0,0 +1,16 @@ +""" CAT4K IOS-XE Settings. """ + +from unicon.plugins.iosxe.settings import IosXESettings + +class IosXECat4kSettings(IosXESettings): + + def __init__(self): + super().__init__() + self.CONNECTION_TIMEOUT=10 + self.RELOAD_TIMEOUT = 300 + self.CONNECTION_TIMEOUT = 300 + # prompt wait delay + self.ESCAPE_CHAR_PROMPT_WAIT = 0.5 + # prompt wait retries + # (wait time: 0.5, 1, 1.5 == total wait: 3s) + self.ESCAPE_CHAR_PROMPT_WAIT_RETRIES = 3 diff --git a/src/unicon/plugins/iosxe/cat4k/statemachine.py b/src/unicon/plugins/iosxe/cat4k/statemachine.py new file mode 100644 index 00000000..f463043e --- /dev/null +++ b/src/unicon/plugins/iosxe/cat4k/statemachine.py @@ -0,0 +1,15 @@ +from typing import Pattern +from unicon.plugins.iosxe.statemachine import IosXEDualRpStateMachine +from .patterns import IosXECat4kPatterns +from unicon.statemachine import State, Path +from unicon.eal.dialogs import Dialog + +patterns = IosXECat4kPatterns() + + +class IosXEC4t3kDualRpStateMachine(IosXEDualRpStateMachine): + def create(self): + super().create() + + stby_lock = State('stby_locked', '' ) + self.add_state(stby_lock) \ No newline at end of file diff --git a/src/unicon/plugins/iosxe/cat8k/__init__.py b/src/unicon/plugins/iosxe/cat8k/__init__.py new file mode 100644 index 00000000..b27f8740 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat8k/__init__.py @@ -0,0 +1,25 @@ +""" cat8k IOS-XE connection implementation. +""" + +__author__ = "Lukas McClelland " + +from .. import IosXEServiceList +from .settings import IosXECat8kSettings +from . import service_implementation as svc +from .statemachine import IosXECat8kSingleRpStateMachine + +from unicon.plugins.iosxe import IosXESingleRpConnection + + +class IosXECat8kServiceList(IosXEServiceList): + def __init__(self): + super().__init__() + self.switchover = svc.SwitchoverService + self.reload = svc.Reload + + +class IosXECat8kSingleRpConnection(IosXESingleRpConnection): + platform = 'cat8k' + state_machine_class = IosXECat8kSingleRpStateMachine + subcommand_list = IosXECat8kServiceList + settings = IosXECat8kSettings() diff --git a/src/unicon/plugins/iosxe/cat8k/service_implementation.py b/src/unicon/plugins/iosxe/cat8k/service_implementation.py new file mode 100644 index 00000000..6e8c7af0 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat8k/service_implementation.py @@ -0,0 +1,233 @@ +__author__ = "Lukas McClelland " + +import io +import re +import logging +from time import sleep + +from unicon.eal.dialogs import Dialog +from unicon.core.errors import SubCommandFailure +from unicon.bases.routers.services import BaseService +from unicon.logs import UniconStreamHandler, UNICON_LOG_FORMAT +from unicon.plugins.generic.service_implementation import SwitchoverResult +from unicon.plugins.iosxe.cat8k.service_statements import switchover_statement_list +from unicon.plugins.generic.service_statements import ( + reload_statement_list, + ha_reload_statement_list) +from unicon.plugins.generic.service_implementation import ( + HAReloadService as GenericHAReloadService +) + +from ..service_implementation import Reload as XEReload +from ..statements import boot_from_rommon_stmt +class SwitchoverService(BaseService): + """ Service to switchover the device. + + Arguments: + command: command to do switchover. default + "redundancy force-switchover" + dialog: Dialog which include list of Statements for + additional dialogs prompted by switchover command, + in-case it is not in the current list. + timeout: Timeout value in sec, Default Value is 500 sec + + Returns: + True on Success, raise SubCommandFailure on failure. + + Example: + .. code-block:: python + + rtr.switchover() + # If switchover command is other than 'redundancy force-switchover' + rtr.switchover(command="command to invoke switchover",timeout=700) + """ + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.start_state = 'enable' + self.end_state = 'enable' + self.timeout = connection.settings.SWITCHOVER_TIMEOUT + self.dialog = Dialog(switchover_statement_list) + self.command = 'redundancy force-switchover' + self.log_buffer = io.StringIO() + lb = UniconStreamHandler(self.log_buffer) + lb.setFormatter(logging.Formatter(fmt=UNICON_LOG_FORMAT)) + self.connection.log.addHandler(lb) + self.__dict__.update(kwargs) + + def call_service(self, command=None, + reply=Dialog([]), + timeout=None, + sync_standby=True, + return_output=False, + *args, + **kwargs): + + # create an alias for connection. + con = self.connection + timeout = timeout or self.timeout + command = command or self.command + + if (reply is None) or (reply == []): + reply = Dialog([]) + elif not isinstance(reply, Dialog): + raise SubCommandFailure( + "dialog passed via 'reply' must be an instance of Dialog") + + reply += self.dialog + + # Clear log buffer + self.log_buffer.seek(0) + self.log_buffer.truncate() + + con.log.debug("+++ Issuing switchover on %s with " + "switchover_command %s and timeout is %s +++" + % (con.hostname, command, timeout)) + + # Check if switchover is possible by checking if "IOSXE_DUAL_IOS = 1" is + # in the output of 'sh romvar' + output = con.execute('show romvar') + if not re.search(r'IOSXE_DUAL_IOS\s*=\s*1', output): + raise SubCommandFailure( + "Switchover can't be issued if IOSXE_DUAL_IOS is not activated") + + + # Issue switchover command + con.spawn.sendline(command) + try: + reply.process(con.spawn, + timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=self.context) + except TimeoutError: + pass + except SubCommandFailure as err: + raise SubCommandFailure("Switchover Failed %s" % str(err)) from err + + con.log.info(f'Waiting {con.settings.POST_SWITCHOVER_WAIT} seconds') + sleep(con.settings.POST_SWITCHOVER_WAIT) + + con.spawn.sendline() + + con.connection_provider.connect() + self.result = True + + if not sync_standby: + con.log.info("Standby state check disabled on user request") + else: + con.log.info('Waiting for standby sync to finish') + standby_wait_time = con.settings.POST_HA_RELOAD_CONFIG_SYNC_WAIT + switchover_intervals = con.settings.SWITCHOVER_COUNTER + sleep_per_interval = standby_wait_time // switchover_intervals + 1 + for interval in range(switchover_intervals): + try: + output = con.execute('show platform') + except (SubCommandFailure, TimeoutError): + self.result = False + con.log.info( + "Encountered subcommand failure while trying to " + "execute 'show platform'. Waiting for %s seconds" + % sleep_per_interval) + sleep(sleep_per_interval) + continue + else: + if not re.search(r'R\d+/\d+\s+init,\s*standby.*', output): + break + elif interval * sleep_per_interval < standby_wait_time: + con.log.info( + 'Standby still initializing. Waiting for %s seconds' + % sleep_per_interval) + sleep(sleep_per_interval) + + if interval * sleep_per_interval >= standby_wait_time: + con.log.error( + 'Standby failed to complete initialization within ' + '{} seconds'.format(standby_wait_time)) + self.result = False + + self.log_buffer.seek(0) + switchover_output = self.log_buffer.read() + # clear buffer + self.log_buffer.truncate() + + if return_output: + self.result = SwitchoverResult( + result=self.result, + output=switchover_output) +class Reload(XEReload): + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.dialog = Dialog(reload_statement_list + [boot_from_rommon_stmt]) + + def pre_service(self, *args, **kwargs): + if "image_to_boot" in kwargs: + self.start_state = 'rommon' + if 'image_to_boot' in self.context: + self.context['orig_image_to_boot'] = self.context['image_to_boot'] + self.context["image_to_boot"] = kwargs["image_to_boot"] + self.connection.log.info("'image_to_boot' specified with reload, transitioning to 'rommon' state") + else: + if 'image' in kwargs: + self.context['image_to_boot'] = kwargs.get('image') + self.start_state = 'enable' + + super().pre_service(*args, **kwargs) + + def call_service(self, *args, **kwargs): + # assume the device is in rommon if image_to_boot is passed + # update reload command to use rommon boot syntax + if "image_to_boot" in kwargs: + self.context["image_to_boot"] = kwargs["image_to_boot"] + reload_command = "boot {}".format( + self.context['image_to_boot']).strip() + super().call_service(reload_command, *args, **kwargs) + self.context.pop("image_to_boot", None) + else: + super().call_service(*args, **kwargs) + + def post_service(self, *args, **kwargs): + if 'orig_image_to_boot' in self.context: + self.context['image_to_boot'] = self.context.pop('orig_image_to_boot') + super().post_service(*args, **kwargs) + + +class HAReloadService(GenericHAReloadService): + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.dialog = Dialog(ha_reload_statement_list + [boot_from_rommon_stmt]) + + def pre_service(self, *args, **kwargs): + if "image_to_boot" in kwargs: + self.start_state = 'rommon' + if 'image_to_boot' in self.context: + self.context['orig_image_to_boot'] = self.context['image_to_boot'] + self.context["image_to_boot"] = kwargs["image_to_boot"] + self.connection.active.context = self.context + self.connection.standby.context = self.context + self.connection.log.info("'image_to_boot' specified with reload, transitioning to 'rommon' state") + else: + if 'image' in kwargs: + self.context['image_to_boot'] = kwargs.get('image') + self.start_state = 'enable' + + super().pre_service(*args, **kwargs) + + def call_service(self, *args, **kwargs): + # assume the device is in rommon if image_to_boot is passed + # update reload command to use rommon boot syntax + if "image_to_boot" in kwargs: + reload_command = "boot {}".format( + self.context['image_to_boot']).strip() + super().call_service(reload_command, *args, **kwargs) + self.context.pop("image_to_boot", None) + else: + super().call_service(*args, **kwargs) + + def post_service(self, *args, **kwargs): + if 'orig_image_to_boot' in self.context: + self.context['image_to_boot'] = self.context.pop('orig_image_to_boot') + self.connection.active.context.pop('image_to_boot', None) + self.connection.standby.context.pop('image_to_boot', None) + super().post_service(*args, **kwargs) \ No newline at end of file diff --git a/src/unicon/plugins/iosxe/cat8k/service_patterns.py b/src/unicon/plugins/iosxe/cat8k/service_patterns.py new file mode 100644 index 00000000..51f9e420 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat8k/service_patterns.py @@ -0,0 +1,14 @@ +__author__ = "Lukas McClelland " + +from ..patterns import IosXEPatterns +class ReloadPatterns(IosXEPatterns): + + def __init__(self): + super().__init__() + self.boot_interrupt_prompt = r'Preparing to autoboot. \[Press Ctrl-C to interrupt\]' +class SwitchoverPatterns: + def __init__(self): + self.save_config = r'.*System configuration has been modified\.\s*Save\?\s*\[yes\/no\]:\s*$' + self.build_config= r'Building configuration' + self.prompt_switchover = r'Proceed with switchover to standby RP\? \[confirm\]\s*$' + self.switchover_complete = r'console active.\s+Press RETURN to get started!?' diff --git a/src/unicon/plugins/iosxe/cat8k/service_statements.py b/src/unicon/plugins/iosxe/cat8k/service_statements.py new file mode 100644 index 00000000..d132566d --- /dev/null +++ b/src/unicon/plugins/iosxe/cat8k/service_statements.py @@ -0,0 +1,57 @@ +__author__ = "Lukas McClelland " + +from unicon.eal.dialogs import Statement +from unicon.plugins.iosxe.cat8k.service_patterns import SwitchoverPatterns, ReloadPatterns +from unicon.plugins.generic.service_statements import ( + save_env, confirm_reset, reload_confirm, reload_confirm_ios) + + + +############################################################################# +# Switchover Command Statement +############################################################################# +pat = SwitchoverPatterns() + +save_config = Statement(pattern=pat.save_config, + action='sendline(yes)', + loop_continue=True, + continue_timer=True) + +build_config = Statement(pattern=pat.build_config, + action=None, + args=None, + loop_continue=True, + continue_timer=True) + +prompt_switchover = Statement(pattern=pat.prompt_switchover, + action='sendline()', + loop_continue=True, + continue_timer=True) + +switchover_complete = Statement(pattern=pat.switchover_complete, + action=None, + loop_continue=False, + continue_timer=False) + +switchover_statement_list = [save_config, + build_config, + prompt_switchover, + switchover_complete] +############################################################################# +# Reload Command Statement +############################################################################# +patterns = ReloadPatterns() + +boot_interrupt_stmt = Statement( + pattern=patterns.boot_interrupt_prompt, + action='send(\x03)', + args=None, + loop_continue=True, + continue_timer=False) + + +reload_to_rommon_statement_list = [save_env, + confirm_reset, + reload_confirm, + reload_confirm_ios, + boot_interrupt_stmt] diff --git a/src/unicon/plugins/iosxe/cat8k/settings.py b/src/unicon/plugins/iosxe/cat8k/settings.py new file mode 100644 index 00000000..8a9988cd --- /dev/null +++ b/src/unicon/plugins/iosxe/cat8k/settings.py @@ -0,0 +1,10 @@ +__author__ = "Lukas McClelland " + +from unicon.plugins.iosxe.settings import IosXESettings + + +class IosXECat8kSettings(IosXESettings): + + def __init__(self): + super().__init__() + self.POST_SWITCHOVER_WAIT = 30 diff --git a/src/unicon/plugins/iosxe/cat8k/statemachine.py b/src/unicon/plugins/iosxe/cat8k/statemachine.py new file mode 100644 index 00000000..77569678 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat8k/statemachine.py @@ -0,0 +1,60 @@ +__author__ = "Lukas McClelland " + +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 ..statements import boot_from_rommon_statement_list + +from .service_patterns import ReloadPatterns +from .service_statements import ( + reload_to_rommon_statement_list) + +patterns = ReloadPatterns() +class IosXECat8kSingleRpStateMachine(IosXESingleRpStateMachine): + def create(self): + super().create() + + rommon = self.get_state('rommon') + disable = self.get_state('disable') + enable = self.get_state('enable') + + rommon.pattern = patterns.rommon_prompt + + self.remove_path('rommon', 'disable') + self.remove_path('enable', 'rommon') + + rommon_to_disable = Path(rommon, disable, boot_from_rommon, Dialog( + boot_from_rommon_statement_list)) + enable_to_rommon = Path(enable, rommon, 'reload', Dialog( + reload_to_rommon_statement_list)) + + + self.add_path(rommon_to_disable) + self.add_path(enable_to_rommon) + + +class IosXECat8kDualRpStateMachine(IosXEDualRpStateMachine): + + def create(self): + super().create() + + rommon = self.get_state('rommon') + disable = self.get_state('disable') + enable = self.get_state('enable') + + rommon.pattern = patterns.rommon_prompt + + self.remove_path('rommon', 'disable') + self.remove_path('enable', 'rommon') + + rommon_to_disable = Path(rommon, disable, boot_from_rommon, Dialog( + boot_from_rommon_statement_list)) + enable_to_rommon = Path(enable, rommon, 'reload', Dialog( + reload_to_rommon_statement_list)) + + self.add_path(rommon_to_disable) + self.add_path(enable_to_rommon) diff --git a/src/unicon/plugins/iosxe/cat9k/__init__.py b/src/unicon/plugins/iosxe/cat9k/__init__.py new file mode 100644 index 00000000..b4498ac0 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/__init__.py @@ -0,0 +1,42 @@ +""" cat9k IOS-XE connection implementation. +""" + +__author__ = "Rob Trotter " + +from unicon.plugins.iosxe import ( + IosXESingleRpConnection, + IosXEDualRPConnection, + IosXEServiceList, + HAIosXEServiceList) + +from .statemachine import IosXECat9kSingleRpStateMachine, IosXECat9kDualRpStateMachine +from .settings import IosXECat9kSettings +from . import service_implementation as svc + + +class IosXECat9kServiceList(IosXEServiceList): + def __init__(self): + super().__init__() + self.reload = svc.Reload + self.rommon = svc.Rommon + + + +class IosxeCat9kHAServiceList(HAIosXEServiceList): + def __init__(self): + super().__init__() + self.reload = svc.HAReloadService + + +class IosXECat9kSingleRpConnection(IosXESingleRpConnection): + platform = 'cat9k' + state_machine_class = IosXECat9kSingleRpStateMachine + subcommand_list = IosXECat9kServiceList + settings = IosXECat9kSettings() + + +class IosXECat9kDualRPConnection(IosXEDualRPConnection): + platform = 'cat9k' + subcommand_list = IosxeCat9kHAServiceList + settings = IosXECat9kSettings() + state_machine_class = IosXECat9kDualRpStateMachine diff --git a/src/unicon/plugins/iosxe/cat9k/c9100ap/__init__.py b/src/unicon/plugins/iosxe/cat9k/c9100ap/__init__.py new file mode 100644 index 00000000..b5a44803 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/c9100ap/__init__.py @@ -0,0 +1,37 @@ +from unicon.bases.routers.connection import BaseSingleRpConnection +from unicon.plugins.generic.statemachine import GenericSingleRpStateMachine +from unicon.plugins.generic import ServiceList +from unicon.plugins.generic import GenericSingleRpConnectionProvider +from unicon.plugins.generic import service_implementation as gsvc + +from unicon.plugins.iosxe.cat9k.c9800 import IosXEc9800ServiceList, IosXEc9800SingleRpConnection + +from .settings import ApSettings +from . import service_implementation as svc +from .statemachine import IosXEEwcSingleRpStateMachine + + +class ApServiceList(ServiceList): + def __init__(self): + super().__init__() + self.execute = svc.Execute + self.send = gsvc.Send + self.sendline = gsvc.Sendline + self.expect = gsvc.Expect + self.enable = gsvc.Enable + self.disable = gsvc.Disable + self.reload = gsvc.Reload + self.log_user = gsvc.LogUser + self.bash_console = svc.IosXEEWCBashService + self.ap_shell = svc.EWCApShellService + + +class IosXEEwcSingleRpConnection(IosXEc9800SingleRpConnection): + os = 'iosxe' + platform = 'cat9k' + model = 'c9100ap' + chassis_type = 'single_rp' + subcommand_list = ApServiceList + state_machine_class = IosXEEwcSingleRpStateMachine + subcommand_list = ApServiceList + settings = ApSettings() diff --git a/src/unicon/plugins/iosxe/cat9k/c9100ap/patterns.py b/src/unicon/plugins/iosxe/cat9k/c9100ap/patterns.py new file mode 100644 index 00000000..63aac22d --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/c9100ap/patterns.py @@ -0,0 +1,39 @@ +"""Regex patterns relevant to the iosxe/ewc Unicon plugin + +Copyright (c) 2019-2020 by cisco Systems, Inc. +All rights reserved. +""" + +from unicon.plugins.iosxe.patterns import IosXEPatterns + + +class IosXEEWCGenericPatterns(IosXEPatterns): + def __init__(self): + super().__init__() + self.iosxe_glean_pattern = r'Cisco IOS XE Software' + self.ap_glean_pattern = r'Cisco AP Software' + + self.ap_disable_prompt = r'^(.*?)(?P\S+)>\s*$' + self.ap_enable_prompt = r'^(.*?)(?P[\w\.\d]+)(?!\(conf.*?\))?#\s*$' + + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# +# Bash Shell Patterns +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# +class IosXEEWCBashShellPatterns(IosXEEWCGenericPatterns): + def __init__(self): + super().__init__() + self.coral_hostname = 'Coral-mewlc' + self.coral_are_you_sure = r'^Are you sure you want to continue\?\s+\[y\/n\]\s+$' + self.coral_hostname_enable = r'^{}#\s?$'.format(self.coral_hostname) + + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# +# AP Shell Patterns +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# +class IosXEEWCAPShellPatterns(IosXEEWCGenericPatterns): + def __init__(self): + super().__init__() + self.ap_are_you_sure = r'^.*Are you sure you want to continue connecting.*$' + self.ap_password = r'^.* password:\s?$' + self.ap_enable = r'^Password:\s?$' diff --git a/src/unicon/plugins/iosxe/cat9k/c9100ap/service_implementation.py b/src/unicon/plugins/iosxe/cat9k/c9100ap/service_implementation.py new file mode 100644 index 00000000..a03e98e0 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/c9100ap/service_implementation.py @@ -0,0 +1,120 @@ +"""Implementation of services related to the iosxe/c9800/ewc Unicon plugin + +Copyright (c) 2019-2020 by cisco Systems, Inc. +All rights reserved. +""" + +from unicon.bases.routers.services import BaseService +from unicon.eal.dialogs import Dialog +from unicon.plugins.iosxe.service_implementation import BashService as IosXEBashService +from unicon.plugins.generic.service_implementation import \ + Execute as GenericExecute +from unicon.eal.dialogs import Dialog +from unicon.plugins.iosxe.service_statements import confirm + +from .patterns import IosXEEWCBashShellPatterns, IosXEEWCAPShellPatterns +from .service_statements import enter_bash_shell_statement_list +from .settings import IosXEEWCBashShellSettings, IosXEEWCAPShellSettings + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# +# Bash Shell service implementation +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# +bash_shell_settings = IosXEEWCBashShellSettings() +bash_shell_patterns = IosXEEWCBashShellPatterns() + + +class Execute(GenericExecute): + def call_service(self, command=None, reply=Dialog([]), timeout=None, *args, + **kwargs): + command = list() if command is None else command + super().call_service(command, + reply=reply + Dialog([confirm,]), + timeout=timeout, *args, **kwargs) + + +class IosXEEWCBashService(IosXEBashService): + + def pre_service(self, *args, **kwargs): + if kwargs.get('chassis'): + self.context['_chassis'] = kwargs.get('chassis') + + super().pre_service(self,args,kwargs) + class ContextMgr(IosXEBashService.ContextMgr): + + def __enter__(self): + conn = self.conn + conn.log.debug('+++ attaching iosxe ewc bash shell +++') + + conn.state_machine.hostname = bash_shell_patterns.coral_hostname + bash_shell_dialog = Dialog(enter_bash_shell_statement_list) + command = "request platform software system shell chassis {} R0".format( + conn.context.get('_chassis', '1')) + conn.sendline(command) + bash_shell_dialog.process(conn.spawn, conn.context, + timeout=bash_shell_settings.CONSOLE_TIMEOUT) + + for cmd in conn.settings.BASH_INIT_COMMANDS: + conn.execute(cmd, timeout=self.timeout) + return self + + def __exit__(self, exc_type, exc_value, exc_tb): + conn = self.conn + conn.log.debug('--- detaching console ---') + conn.sendline('exit') + return False # do not suppress + + def post_service(self, *args, **kwargs): + self.context.pop('_chassis', None) + + super().post_service(self,args,kwargs) + + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# +# AP Shell service implementation +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# +ap_shell_settings = IosXEEWCAPShellSettings() +ap_shell_patterns = IosXEEWCAPShellPatterns() + + +class EWCApShellService(BaseService): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.start_state = "enable" + self.end_state = "enable" + self.service_name = "ap_shell" + + def call_service(self, **kwargs): + self.result = self.__class__.ContextMgr(connection=self.connection, **kwargs) + + class ContextMgr(object): + def __init__(self, connection, **kwargs): + timeout = kwargs.get('timeout') + self.conn = connection + self.timeout = timeout or ap_shell_settings.EWC_AP_TIMEOUT + + def __enter__(self): + conn = self.conn + conn.log.debug('+++ attaching ap shell +++') + + conn.state_machine.go_to( + 'ap_enable', + spawn=conn.spawn, + context=conn.context) + + for command in ap_shell_settings.HA_INIT_EXEC_COMMANDS: + conn.execute(command, timeout=self.timeout) + return self + + def __exit__(self, *args, **kwargs): + conn = self.conn + conn.log.debug('--- detaching console ---') + conn.state_machine.go_to( + 'enable', + spawn=conn.spawn, + context=conn.context) + return False # do not suppress + + def __getattr__(self, attr): + return getattr(self.conn, attr) diff --git a/src/unicon/plugins/iosxe/cat9k/c9100ap/service_statements.py b/src/unicon/plugins/iosxe/cat9k/c9100ap/service_statements.py new file mode 100644 index 00000000..5f55b173 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/c9100ap/service_statements.py @@ -0,0 +1,62 @@ +"""Unicon eal Statements and callbacks relevant to the iosxe/c9800/ewc Unicon plugin + +Copyright (c) 2019-2020 by cisco Systems, Inc. +All rights reserved. +""" + +from unicon.eal.dialogs import Statement +from unicon.plugins.generic.statements import ssh_continue_connecting +from unicon.plugins.generic.service_statements import send_yes_callback +from .patterns import IosXEEWCBashShellPatterns, IosXEEWCAPShellPatterns + + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# +# Bash Shell Statements +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# +bash_patterns = IosXEEWCBashShellPatterns() + +bash_are_you_sure = Statement(pattern=bash_patterns.coral_are_you_sure, + action=send_yes_callback, + loop_continue=True, + continue_timer=False) + +bash_hostname_enable = Statement(pattern=bash_patterns.coral_hostname_enable, + action=None, + loop_continue=False, + continue_timer=False) + +enter_bash_shell_statement_list = [ + bash_are_you_sure, + bash_hostname_enable +] + + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# +# AP Shell Callbacks +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# + +def send_context_ap_enable(spawn, context, session): + credentials = context.get('credentials', {}) + ap_enable = credentials.get('ap', {}).get('enable_password', 'lab') + return spawn.sendline(ap_enable) + + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# +# AP Shell Statements +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# +ap_patterns = IosXEEWCAPShellPatterns() + +ap_are_you_sure = Statement(pattern=ap_patterns.ap_are_you_sure, + action=ssh_continue_connecting, + loop_continue=True, + continue_timer=True) + +ap_enable_stmt = Statement(pattern=ap_patterns.password, + action=send_context_ap_enable, + loop_continue=True, + continue_timer=True) + + +enter_ap_shell_statement_list = [ + ap_are_you_sure, +] diff --git a/src/unicon/plugins/iosxe/cat9k/c9100ap/settings.py b/src/unicon/plugins/iosxe/cat9k/c9100ap/settings.py new file mode 100644 index 00000000..d6fb8ba0 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/c9100ap/settings.py @@ -0,0 +1,41 @@ +"""Settings relevant to the iosxe/ewc Unicon plugin + +Copyright (c) 2019-2020 by cisco Systems, Inc. +All rights reserved. +""" + +from unicon.plugins.iosxe.settings import IosXESettings +from unicon.plugins.generic.settings import GenericSettings + + +class ApSettings(GenericSettings): + def __init__(self): + super().__init__() + + self.HA_INIT_EXEC_COMMANDS = [ + 'exec-timeout 0', + 'terminal length 0', + 'terminal width 0', + 'show version', + 'logging console disable', + ] + + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# +# Bash Shell Settings +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# +class IosXEEWCBashShellSettings(IosXESettings): + def __init__(self): + super().__init__() + + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# +# AP Shell Settings +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# +class IosXEEWCAPShellSettings(ApSettings): + + def __init__(self): + super().__init__() + self.EWC_SHORT_UNICON_SLEEP = 0.1 + self.EWC_ENTER_AP_SHELL_TIMEOUT = 20 + self.EWC_AP_TIMEOUT = self.CONSOLE_TIMEOUT + self.EWC_ENTER_AP_SHELL_TIMEOUT diff --git a/src/unicon/plugins/iosxe/cat9k/c9100ap/statemachine.py b/src/unicon/plugins/iosxe/cat9k/c9100ap/statemachine.py new file mode 100644 index 00000000..20c7575f --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/c9100ap/statemachine.py @@ -0,0 +1,136 @@ + +import re + +from unicon.plugins.iosxe.cat9k.c9800.statemachine import IosXEc9800SingleRpStateMachine +from unicon.statemachine import State, Path +from unicon.eal.dialogs import Dialog, Statement +from unicon.utils import AttributeDict +from unicon.core.errors import StateMachineError + +from unicon.plugins.generic.statements import GenericStatements, default_statement_list + +from .service_statements import enter_ap_shell_statement_list, ap_enable_stmt +from .patterns import IosXEEWCGenericPatterns +from .settings import IosXEEWCAPShellSettings + + +statements = GenericStatements() + +patterns = IosXEEWCGenericPatterns() +ap_shell_settings = IosXEEWCAPShellSettings() + + +def enable_to_ap_disable_transition(statemachine, spawn, context): + credentials = context.get('credentials') or {} + command = "wireless ewc-ap ap shell username {}".format( + credentials.get('ap', {}).get('username', '')) + spawn.sendline(command) + + + +class IosXEEwcSingleRpStateMachine(IosXEc9800SingleRpStateMachine): + + STATE_GLEAN = AttributeDict({ + 'disable': AttributeDict(dict( + command='show version | inc ^Cisco', + pattern=patterns.iosxe_glean_pattern)), + 'enable': AttributeDict(dict( + command='show version | inc ^Cisco', + pattern=patterns.iosxe_glean_pattern)), + 'ap_disable': AttributeDict(dict( + command='show version | inc ^Cisco', + pattern=patterns.ap_glean_pattern)), + 'ap_enable': AttributeDict(dict( + command='show version | inc ^Cisco', + pattern=patterns.ap_glean_pattern)) + }) + + def create(self): + super().create() + + enable = self.get_state('enable') + + ap_disable = State('ap_disable', pattern=patterns.ap_disable_prompt) + ap_enable = State('ap_enable', pattern=patterns.ap_enable_prompt) + + self.add_state(ap_disable) + self.add_state(ap_enable) + + ap_disable_to_ap_enable = Path(ap_disable, ap_enable, 'enable', Dialog([ + ap_enable_stmt, + statements.bad_password_stmt, + statements.syslog_stripper_stmt + ])) + + ap_disable_to_enable = Path(ap_disable, enable, 'exit', None) + ap_enable_to_enable = Path(ap_enable, enable, 'exit', None) + enable_to_ap_disable = Path(enable, ap_disable, enable_to_ap_disable_transition, + Dialog(enter_ap_shell_statement_list)) + + self.add_path(ap_disable_to_ap_enable) + self.add_path(ap_disable_to_enable) + self.add_path(ap_enable_to_enable) + self.add_path(enable_to_ap_disable) + + def detect_state(self, spawn, context=AttributeDict()): + """ Detect the device state and glean the actual state if multiple matches are found. + """ + state_matches = [] + result = spawn.match + if result: + prompt = result.match_output.splitlines()[-1] + for state in self.states: + if re.search(state.pattern, prompt): + state_matches.append(state) + + spawn.log.debug('statemachine detected state(s): {}'.format(state_matches)) + if len(state_matches) > 1: + # If the current state is in the detected states, assume we can keep the same state + # If not, try to glean the actual state + if self.current_state not in [s.name for s in state_matches]: + self.glean_state(spawn, state_matches) + elif len(state_matches) == 1: + self.update_cur_state(state_matches[0].name) + else: + spawn.sendline() + super().go_to('any', spawn, context) + + def glean_state(self, spawn, possible_states): + """ Try to figure out the state by sending commands and verifying the matches against known output. + """ + # Create list of commands to execute + glean_command_map = {} + state_patterns = [] + for state in possible_states: + state_patterns.append(state.pattern) + glean_data = self.STATE_GLEAN.get(state.name, None) + if glean_data: + if glean_data.command in glean_command_map: + glean_command_map[glean_data.command][glean_data.pattern] = state + else: + glean_command_map[glean_data.command] = {} + glean_command_map[glean_data.command][glean_data.pattern] = state + + if not glean_command_map: + raise StateMachineError('Unable to detect state, multiple states possible and no glean data available') + + # Execute each glean commnd and check for pattern match + for glean_cmd in glean_command_map: + glean_pattern_map = glean_command_map[glean_cmd] + dialog = Dialog(default_statement_list + [Statement(p) for p in state_patterns]) + + spawn.sendline(glean_cmd) + result = dialog.process(spawn) + if result: + output = result.match_output + for glean_pattern in glean_pattern_map: + if re.search(glean_pattern, output): + self.update_cur_state(glean_pattern_map[glean_pattern]) + return + + def go_to(self, to_state, spawn, **kwargs): + spawn.log.debug('statemachine goto: {} -> {}'.format(self.current_state, to_state)) + super().go_to(to_state, spawn, **kwargs) + if to_state == 'any' and self.current_state in self.STATE_GLEAN: + glean_states = [self.get_state(name) for name in self.STATE_GLEAN] + self.glean_state(spawn, glean_states) diff --git a/src/unicon/plugins/aci/apic/__init__.py b/src/unicon/plugins/iosxe/cat9k/c9350/__init__.py similarity index 100% rename from src/unicon/plugins/aci/apic/__init__.py rename to src/unicon/plugins/iosxe/cat9k/c9350/__init__.py diff --git a/src/unicon/plugins/iosxe/cat9k/c9350/stack/__init__.py b/src/unicon/plugins/iosxe/cat9k/c9350/stack/__init__.py new file mode 100644 index 00000000..6ccc01ca --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/c9350/stack/__init__.py @@ -0,0 +1,20 @@ +""" A Stackwise-virtual c9350 IOS-XE connection implementation. +""" + +from unicon.plugins.iosxe.stack import StackIosXEServiceList +from unicon.plugins.iosxe.stack import IosXEStackRPConnection, StackRpConnectionProvider +from . import service_implementation as svc + +class IosXEC9350StackServiceList(StackIosXEServiceList): + + def __init__(self): + super().__init__() + self.reload = svc.C9350StackReload + +class IosXEC9350StackRPConnection(IosXEStackRPConnection): + os = 'iosxe' + platform = 'cat9k' + model = 'c9350' + chassis_type = 'stack' + connection_provider_class = StackRpConnectionProvider + subcommand_list = IosXEC9350StackServiceList diff --git a/src/unicon/plugins/iosxe/cat9k/c9350/stack/service_implementation.py b/src/unicon/plugins/iosxe/cat9k/c9350/stack/service_implementation.py new file mode 100644 index 00000000..b09ee40e --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/c9350/stack/service_implementation.py @@ -0,0 +1,285 @@ +""" Stack based IOS-XE/cat9k/c9350 service implementations. """ +import io, logging +from time import sleep +from collections import namedtuple +from datetime import timedelta +from concurrent.futures import ThreadPoolExecutor, wait as wait_futures, ALL_COMPLETED + +from unicon.eal.dialogs import Dialog +from unicon.core.errors import SubCommandFailure +from unicon.bases.routers.services import BaseService + +from unicon.logs import UniconStreamHandler, UNICON_LOG_FORMAT + +from unicon.plugins.iosxe.stack.utils import StackUtils +from unicon.plugins.generic.statements import custom_auth_statements +from unicon.plugins.iosxe.stack.service_statements import (switch_prompt, + stack_reload_stmt_list, + stack_reload_stmt_list_1, + stack_switchover_stmt_list) + +utils = StackUtils() + +class C9350StackReload(BaseService): + """ Service to reload the stack device. + + Arguments: + reload_command: reload command to be used. default "redundancy reload shelf" + reply: Additional Dialog( i.e patterns) to be handled + timeout: Timeout value in sec, Default Value is 900 sec + image_to_boot: image to boot from rommon state + return_output: if True, return namedtuple with result and reload output + + Returns: + console True on Success, raises SubCommandFailure on failure. + + Example: + .. code-block:: python + + rtr.reload() + # If reload command is other than 'redundancy reload shelf' + rtr.reload(reload_command="reload location all", timeout=700) + """ + + def __init__(self, connection, context, *args, **kwargs): + super().__init__(connection, context, *args, **kwargs) + self.start_state = 'enable' + self.end_state = 'enable' + self.timeout = connection.settings.STACK_RELOAD_TIMEOUT + self.reload_command = "redundancy reload shelf" + self.log_buffer = io.StringIO() + self.dialog = Dialog(stack_reload_stmt_list) + + def call_service(self, + reload_command=None, + reply=Dialog([]), + timeout=None, + image_to_boot=None, + return_output=False, + member=None, + error_pattern = None, + append_error_pattern= None, + post_reload_wait_time=None, + *args, + **kwargs): + + self.result = False + if member: + reload_command = f'reload slot {member}' + + reload_cmd = reload_command or self.reload_command + timeout = timeout or self.timeout + conn = self.connection.active + + if error_pattern is None: + self.error_pattern = self.connection.settings.ERROR_PATTERN + else: + self.error_pattern = error_pattern + + if post_reload_wait_time is None: + self.post_reload_wait_time = self.connection.settings.POST_RELOAD_WAIT + else: + self.post_reload_wait_time = post_reload_wait_time + + if not isinstance(self.error_pattern, list): + raise ValueError('error_pattern should be a list') + if append_error_pattern: + if not isinstance(append_error_pattern, list): + raise ValueError('append_error_pattern should be a list') + self.error_pattern += append_error_pattern# Connecting to the log handler to capture the buffer output + + lb = UniconStreamHandler(self.log_buffer) + lb.setFormatter(logging.Formatter(fmt=UNICON_LOG_FORMAT)) + self.connection.log.addHandler(lb) + + # logging the output to subconnections + for subcon in self.connection.subconnections: + subcon.log.addHandler(lb) + + # Clear log buffer + self.log_buffer.seek(0) + self.log_buffer.truncate() + # update all subconnection context with image_to_boot + if image_to_boot: + for subconn in self.connection.subconnections: + subconn.context.image_to_boot = image_to_boot + + # Update the reload command to use the image_to_boot + self.context["image_to_boot"] = image_to_boot + reload_cmd = f"boot {image_to_boot.strip()}" + + reload_dialog = self.dialog + if reply: + reload_dialog = reply + reload_dialog + + custom_auth_stmt = custom_auth_statements(conn.settings.LOGIN_PROMPT, + conn.settings.PASSWORD_PROMPT) + if custom_auth_stmt: + reload_dialog += Dialog(custom_auth_stmt) + + reload_dialog += Dialog([switch_prompt]) + + conn.context['post_reload_wait_time'] = timedelta(seconds= self.post_reload_wait_time) + + conn.log.info('Processing on active rp %s-%s with timeout %s' % (conn.hostname, conn.alias, timeout)) + conn.sendline(reload_cmd) + + conn_list = self.connection.subconnections + reload_cmd_output = None + + def task(con): + for _ in range(3): + # The dialog handles the initial reload interaction for c9350 member consoles. + # It monitors for the prompt "Press RETURN to get started" and responds by sending a RETURN key. + # This action is repeated three times to cover the three subsequent "Press RETURN to get started" prompts on a single console. + # Once each console has been activated, the dialog concludes. + reload_cmd_output = reload_dialog.process(con.spawn, + timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=con.context) + self.result = reload_cmd_output.match_output + self.get_service_result() + + # If device entered rommon state, break out of the loop + if 'state' in con.context and con.context.state == 'rommon': + con.log.info(f"Device {con.alias} entered rommon state, breaking out of reload dialog loop") + break + + futures = [] + executor = ThreadPoolExecutor(max_workers=len(conn_list)) + for con in conn_list: + futures.append(executor.submit(task, con)) + + # Wait for all tasks to complete with timeout handling + future_results, not_completed = wait_futures( + futures, + timeout=timeout, + return_when=ALL_COMPLETED) + + if not_completed: + # Cancel any remaining futures + for future in not_completed: + future.cancel() + raise SubCommandFailure('Threading timeout') + + # Process completed futures + for future in future_results: + try: + result = future.result() + conn.log.info(f"Reload result: {result}") + + except Exception as e: + raise SubCommandFailure('Error during reload', e) from e + + if 'state' in conn.context and conn.context.state == 'rommon': + conn.log.info(f"Waiting {self.connection.settings.STACK_ROMMON_SLEEP} seconds for all peers to come to boot state ") + # If manual boot enabled wait for all peers to come to boot state. + sleep(self.connection.settings.STACK_ROMMON_SLEEP) + + conn.context.pop('state') + + def boot(con): + + # send boot command for each subconnection + utils.send_boot_cmd(con, timeout, self.prompt_recovery, reply) + + self.connection.log.info('Processing on rp %s-%s' % (con.hostname, con.alias)) + con.context['post_reload_timeout'] = timedelta(seconds= self.post_reload_wait_time) + # process boot up for each subconnection + + utils.boot_process(con, timeout, self.prompt_recovery, reload_dialog) + + futures = [] + executor = ThreadPoolExecutor(max_workers=len(conn_list)) + + for con in conn_list: + futures.append(executor.submit(boot, con)) + + # Wait for all tasks to complete with timeout handling + future_results, not_completed = wait_futures( + futures, + timeout=timeout, + return_when=ALL_COMPLETED) + + if not_completed: + # Cancel any remaining futures + for future in not_completed: + future.cancel() + raise SubCommandFailure('Threading timeout') + + # Process completed futures + for future in future_results: + try: + result = future.result() + conn.log.info(f"Reload result: {result}") + + except Exception as e: + raise SubCommandFailure('Error during reload', e) from e + + # After boot_process, bring each subconnection to enable state + conn.log.info("Bringing devices to enable state post rommon boot") + for con in conn_list: + try: + con.state_machine.go_to('enable', con.spawn, timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=con.context) + except Exception as e: + raise SubCommandFailure('Failed to bring device to enable mode.', e) from e + else: + try: + conn.log.info("Bring device to any state") + # bring device to enable mode + conn.state_machine.go_to('any', conn.spawn, timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=conn.context) + conn.state_machine.go_to('enable', conn.spawn, timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=conn.context) + except Exception as e: + raise SubCommandFailure('Failed to bring device to disable mode.', e) from e + # check active and standby rp is ready + self.connection.log.info('Wait for Standby RP to be ready.') + interval = self.connection.settings.RELOAD_POSTCHECK_INTERVAL + if utils.is_active_standby_ready(conn, timeout=timeout, interval=interval): + self.connection.log.info('Active and Standby RPs are ready.') + else: + self.connection.log.info('Timeout in %s secs. ' + 'Standby RP is not in Ready state. Reload failed' % timeout) + self.result = False + return + + if member: + if utils.is_all_member_ready(conn, timeout=timeout, interval=interval): + self.connection.log.info('All Members are ready.') + else: + self.connection.log.info(f'Timeout in {timeout} secs. ' + f'Member{member} is not in Ready state. Reload failed') + self.result = False + return + + self.connection.log.info('Sleeping for %s secs.' % \ + self.connection.settings.STACK_POST_RELOAD_SLEEP) + sleep(self.connection.settings.STACK_POST_RELOAD_SLEEP) + + self.connection.log.info('Disconnecting and reconnecting') + self.connection.disconnect() + self.connection.connect() + + self.connection.log.info("+++ Reload Completed Successfully +++") + + # Read the log buffer + self.log_buffer.seek(0) + reload_output = self.log_buffer.read() + # clear buffer + self.log_buffer.truncate() + + # Remove the handler + self.connection.log.removeHandler(lb) + for subcon in self.connection.subconnections: + subcon.log.removeHandler(lb) + + self.result = True + + if return_output: + Result = namedtuple('Result', ['result', 'output']) + self.result = Result(self.result, reload_output.replace(reload_cmd, '', 1)) diff --git a/src/unicon/plugins/aci/n9k/__init__.py b/src/unicon/plugins/iosxe/cat9k/c9500x/__init__.py similarity index 100% rename from src/unicon/plugins/aci/n9k/__init__.py rename to src/unicon/plugins/iosxe/cat9k/c9500x/__init__.py diff --git a/src/unicon/plugins/iosxe/cat9k/c9500x/stackwise_virtual/__init__.py b/src/unicon/plugins/iosxe/cat9k/c9500x/stackwise_virtual/__init__.py new file mode 100644 index 00000000..b59e13c0 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/c9500x/stackwise_virtual/__init__.py @@ -0,0 +1,26 @@ +""" A Stackwise-virtual C9500X IOS-XE connection implementation. +""" + +from unicon.plugins.iosxe.stack import StackIosXEServiceList +from unicon.plugins.iosxe.stack import IosXEStackRPConnection +from unicon.plugins.iosxe.cat9k.stackwise_virtual.connection_provider import StackwiseVirtualConnectionProvider + +from . import service_implementation as svc + + +class IosXEC9500xStackwiseVirtualServiceList(StackIosXEServiceList): + + def __init__(self): + super().__init__() + self.reload = svc.SVLStackReload + self.switchover = svc.SVLStackSwitchover + + +class IosXEC9500xStackwiseVirtualRPConnection(IosXEStackRPConnection): + os = 'iosxe' + platform = 'cat9k' + model = 'c9500' + submodel = 'c9500x' + chassis_type = 'stackwise_virtual' + connection_provider_class = StackwiseVirtualConnectionProvider + subcommand_list = IosXEC9500xStackwiseVirtualServiceList diff --git a/src/unicon/plugins/iosxe/cat9k/c9500x/stackwise_virtual/service_implementation.py b/src/unicon/plugins/iosxe/cat9k/c9500x/stackwise_virtual/service_implementation.py new file mode 100644 index 00000000..384c49e2 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/c9500x/stackwise_virtual/service_implementation.py @@ -0,0 +1,387 @@ +""" Stack based IOS-XE/cat9k/c9500X service implementations. """ +import io +import logging +from time import sleep +from collections import namedtuple +from datetime import timedelta +from concurrent.futures import ThreadPoolExecutor, wait as wait_futures, ALL_COMPLETED + +from unicon.eal.dialogs import Dialog +from unicon.core.errors import SubCommandFailure +from unicon.bases.routers.services import BaseService +from unicon.logs import UniconStreamHandler, UNICON_LOG_FORMAT + +from unicon.plugins.iosxe.stack.utils import StackUtils +from unicon.plugins.generic.statements import custom_auth_statements +from unicon.plugins.iosxe.stack.service_statements import (switch_prompt, + stack_reload_stmt_list, + stack_switchover_stmt_list) + +utils = StackUtils() + + +class SVLStackReload(BaseService): + """ Service to reload the SVL stack device. + + Arguments: + reload_command: reload command to be used. default "redundancy reload shelf" + reply: Additional Dialog( i.e patterns) to be handled + timeout: Timeout value in sec, Default Value is 900 sec + image_to_boot: image to boot from rommon state + return_output: if True, return namedtuple with result and reload output + + Returns: + console True on Success, raises SubCommandFailure on failure. + + Example: + .. code-block:: python + + rtr.reload() + # If reload command is other than 'redundancy reload shelf' + rtr.reload(reload_command="reload location all", timeout=700) + """ + + def __init__(self, connection, context, *args, **kwargs): + super().__init__(connection, context, *args, **kwargs) + self.start_state = 'enable' + self.end_state = 'enable' + self.timeout = connection.settings.STACK_RELOAD_TIMEOUT + self.reload_command = "redundancy reload shelf" + self.log_buffer = io.StringIO() + self.dialog = Dialog(stack_reload_stmt_list) + + def call_service(self, + reload_command=None, + reply=Dialog([]), + timeout=None, + image_to_boot=None, + return_output=False, + member=None, + error_pattern = None, + append_error_pattern= None, + post_reload_wait_time=None, + *args, + **kwargs): + + self.result = False + if member: + reload_command = f'reload slot {member}' + reload_cmd = reload_command or self.reload_command + timeout = timeout or self.timeout + conn = self.connection.active + + if error_pattern is None: + self.error_pattern = self.connection.settings.ERROR_PATTERN + else: + self.error_pattern = error_pattern + + if post_reload_wait_time is None: + self.post_reload_wait_time = self.connection.settings.POST_RELOAD_WAIT + else: + self.post_reload_wait_time = post_reload_wait_time + + if not isinstance(self.error_pattern, list): + raise ValueError('error_pattern should be a list') + if append_error_pattern: + if not isinstance(append_error_pattern, list): + raise ValueError('append_error_pattern should be a list') + self.error_pattern += append_error_pattern + + # Connecting to the log handler to capture the buffer output + lb = UniconStreamHandler(self.log_buffer) + lb.setFormatter(logging.Formatter(fmt=UNICON_LOG_FORMAT)) + self.connection.log.addHandler(lb) + + # logging the output to subconnections + for subcon in self.connection.subconnections: + subcon.log.addHandler(lb) + + # Clear log buffer + self.log_buffer.seek(0) + self.log_buffer.truncate() + + # update all subconnection context with image_to_boot + if image_to_boot: + for subconn in self.connection.subconnections: + subconn.context.image_to_boot = image_to_boot + reload_dialog = self.dialog + if reply: + reload_dialog = reply + reload_dialog + + custom_auth_stmt = custom_auth_statements(conn.settings.LOGIN_PROMPT, + conn.settings.PASSWORD_PROMPT) + if custom_auth_stmt: + reload_dialog += Dialog(custom_auth_stmt) + + reload_dialog += Dialog([switch_prompt]) + + conn.context['post_reload_wait_time'] = timedelta(seconds= self.post_reload_wait_time) + + conn.log.info('Processing on active rp %s-%s with timeout %s' % (conn.hostname, conn.alias, timeout)) + conn.sendline(reload_cmd) + + conn_list = self.connection.subconnections + reload_cmd_output = None + + def task(con): + + # The following multithreading logic is designed to manage + # all the subconnections within the stack. + # A loop has been implemented to handle the + # "Press RETURN to get started" prompt twice. Based on extensive + # testing during SVL reloads on 9500x devices, it was observed + # that the device is not fully ready after the first prompt. + # As a result, the logic accounts for this behavior by waiting for + # the second occurrence of the message, which is assumed to be the + # default behavior for these devices. + + for _ in range(2): + reload_cmd_output = reload_dialog.process(con.spawn, + timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=con.context) + self.result = reload_cmd_output.match_output + self.get_service_result() + + futures = [] + executor = ThreadPoolExecutor(max_workers=len(conn_list)) + + for con in conn_list: + futures.append(executor.submit(task, con)) + + # Log the output from threading + future_results = wait_futures(futures, timeout=timeout, return_when=ALL_COMPLETED) + + # Splitting it to done and not done specifically + # because future result is a tuple + + # Logs the completed output + done = list(future_results.done) + + # Logs the error traceback + not_done = list(future_results.not_done) + + for future in done + not_done: + try: + result = future.result() + conn.log.info(f"Reload result: {result}") + except Exception as e: + raise SubCommandFailure('Error during reload', e) from e + + if 'state' in conn.context and conn.context.state == 'rommon': + conn.log.info(f"Waiting {self.connection.settings.STACK_ROMMON_SLEEP} seconds for all peers to come to boot state") + # If manual boot enabled wait for all peers to come to boot state. + sleep(self.connection.settings.STACK_ROMMON_SLEEP) + + conn.context.pop('state') + + def boot(con): + + # send boot command for each subconnection + utils.send_boot_cmd(con, timeout, self.prompt_recovery, reply) + + self.connection.log.info('Processing on rp %s-%s' % (con.hostname, con.alias)) + con.context['post_reload_timeout'] = timedelta(seconds= self.post_reload_wait_time) + + # process boot up for each subconnection + # The following multithreading logic is designed to manage + # all the subconnections within the stack. + # A loop has been implemented to handle the + # "Press RETURN to get started" prompt twice. Based on extensive + # testing during SVL reloads on 9500x devices, it was observed + # that the device is not fully ready after the first prompt. + # As a result, the logic accounts for this behavior by waiting for + # the second occurrence of the message, which is assumed to be the + # default behavior for these devices. + for _ in range(2): + utils.boot_process(con, timeout, self.prompt_recovery, reload_dialog) + + futures = [] + executor = ThreadPoolExecutor(max_workers=len(conn_list)) + + for con in conn_list: + futures.append(executor.submit(boot, con)) + + # Log the output from threading + future_results = wait_futures(futures, timeout=timeout, return_when=ALL_COMPLETED) + + # Splitting it to done and not done specifically + # because future result is a tuple + + # Logs the completed output + done = list(future_results.done) + + # Logs the error traceback + not_done = list(future_results.not_done) + + for future in done + not_done: + try: + result = future.result() + conn.log.info(f"Reload result: {result}") + + except Exception as e: + raise SubCommandFailure('Error during reload', e) from e + else: + try: + # bring device to enable mode + conn.sendline() + conn.log.info("Bringing device to any state") + conn.state_machine.go_to('any', conn.spawn, timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=conn.context) + + except Exception as e: + raise SubCommandFailure('Failed to bring device to disable mode.', e) from e + + # check active and standby rp is ready + self.connection.log.info('Wait for Standby RP to be ready.') + interval = self.connection.settings.RELOAD_POSTCHECK_INTERVAL + if utils.is_active_standby_ready(conn, timeout=timeout, interval=interval): + self.connection.log.info('Active and Standby RPs are ready.') + else: + self.connection.log.info('Timeout in %s secs. ' + 'Standby RP is not in Ready state. Reload failed' % timeout) + self.result = False + return + + if member: + if utils.is_all_member_ready(conn, timeout=timeout, interval=interval): + self.connection.log.info('All Members are ready.') + else: + self.connection.log.info(f'Timeout in {timeout} secs. ' + f'Member{member} is not in Ready state. Reload failed') + self.result = False + return + + self.connection.log.info('Sleeping for %s secs.' % \ + self.connection.settings.STACK_POST_RELOAD_SLEEP) + sleep(self.connection.settings.STACK_POST_RELOAD_SLEEP) + + self.connection.log.info('Initialize the connection after reload') + self.connection.connection_provider.init_connection() + + self.connection.log.info("+++ Reload Completed Successfully +++") + + # Read the log buffer + self.log_buffer.seek(0) + reload_output = self.log_buffer.read() + # clear buffer + self.log_buffer.truncate() + + # Remove the handler + self.connection.log.removeHandler(lb) + for subcon in self.connection.subconnections: + subcon.log.removeHandler(lb) + + self.result = True + + if return_output: + Result = namedtuple('Result', ['result', 'output']) + self.result = Result(self.result, reload_output.replace(reload_cmd, '', 1)) + +class SVLStackSwitchover(BaseService): + """ Get Rp state + + Service to get the redundancy state of the device rp. + + Arguments: + target: Service target, by default active + + Returns: + Expected return values are ACTIVE, STANDBY, MEMBER + raise SubCommandFailure on failure. + + Example: + .. code-block:: python + + rtr.get_rp_state() + rtr.get_rp_state(target='standby') + """ + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.start_state = 'enable' + self.end_state = 'enable' + self.timeout = connection.settings.STACK_SWITCHOVER_TIMEOUT + self.command = "redundancy force-switchover" + self.dialog = Dialog(stack_switchover_stmt_list) + self.__dict__.update(kwargs) + + def call_service(self, command=None, + reply=Dialog([]), + timeout=None, + *args, **kwargs): + + switchover_cmd = command or self.command + timeout = timeout or self.timeout + conn = self.connection.active + + expected_active_sw = self.connection.standby.member_id + dialog = self.dialog + + if reply: + dialog = reply + self.dialog + + # added connection dialog in case switchover ask for username/password + connect_dialog = self.connection.connection_provider.get_connection_dialog() + dialog += connect_dialog + + conn.log.info('Processing on active rp %s-%s' % (conn.hostname, conn.alias)) + conn.sendline(switchover_cmd) + try: + # A loop has been implemented to handle the + # "Press RETURN to get started" prompt twice. Based on extensive + # testing during SVL reloads on 9500x devices, it was observed + # that the device is not fully ready after the first prompt. + # As a result, the logic accounts for this behavior by waiting for + # the second occurrence of the message, which is assumed to be the + # default behavior for these devices. + for _ in range(2): + match_object = dialog.process(conn.spawn, timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=conn.context) + except Exception as e: + raise SubCommandFailure('Error during switchover ', e) from e + + # try boot up original active rp with current active system + # image, if it moved to rommon state. + if 'state' in conn.context and conn.context.state == 'rommon': + try: + conn.state_machine.detect_state(conn.spawn, context=conn.context) + conn.state_machine.go_to('enable', conn.spawn, timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=conn.context, dialog=Dialog([switch_prompt])) + except Exception as e: + self.connection.log.warning('Fail to bring up original active rp from rommon state.', e) + finally: + conn.context.pop('state') + + # To ensure the stack is ready to accept the login + self.connection.log.info('Sleeping for %s secs.' % \ + self.connection.settings.POST_SWITCHOVER_SLEEP) + sleep(self.connection.settings.POST_SWITCHOVER_SLEEP) + + # check all members are ready + conn.state_machine.detect_state(conn.spawn, context=conn.context) + + interval = self.connection.settings.SWITCHOVER_POSTCHECK_INTERVAL + if utils.is_all_member_ready(conn, timeout=timeout, interval=interval): + self.connection.log.info('All members are ready.') + else: + self.connection.log.info('Timeout in %s secs. ' + 'Not all members are in Ready state.' % timeout) + self.result = False + return + + self.connection.log.info('Disconnecting and reconnecting') + self.connection.disconnect() + self.connection.connect() + + self.connection.log.info('Verifying active and standby switch State.') + if self.connection.active.member_id == expected_active_sw: + self.connection.log.info('Switchover successful') + self.result = True + else: + self.connection.log.info('Switchover failed') + self.result = False + diff --git a/src/unicon/plugins/iosxe/cat9k/c9610/__init__.py b/src/unicon/plugins/iosxe/cat9k/c9610/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/unicon/plugins/iosxe/cat9k/c9610/stackwise_virtual/__init__.py b/src/unicon/plugins/iosxe/cat9k/c9610/stackwise_virtual/__init__.py new file mode 100644 index 00000000..e5369ff3 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/c9610/stackwise_virtual/__init__.py @@ -0,0 +1,26 @@ +""" A Stackwise-virtual C9610 IOS-XE connection implementation. +""" + +from unicon.plugins.iosxe.stack import StackIosXEServiceList +from unicon.plugins.iosxe.stack import IosXEStackRPConnection +from unicon.plugins.iosxe.cat9k.stackwise_virtual.connection_provider import StackwiseVirtualConnectionProvider + +from unicon.plugins.iosxe.cat9k.c9500x.stackwise_virtual.service_implementation import SVLStackReload, SVLStackSwitchover + + +class IosXEC9610StackwiseVirtualServiceList(StackIosXEServiceList): + + def __init__(self): + super().__init__() + self.reload = SVLStackReload + self.switchover = SVLStackSwitchover + + +class IosXEC9610StackwiseVirtualRPConnection(IosXEStackRPConnection): + os = 'iosxe' + platform = 'cat9k' + model = 'c9610' + submodel = 'c9610' + chassis_type = 'stackwise_virtual' + connection_provider_class = StackwiseVirtualConnectionProvider + subcommand_list = IosXEC9610StackwiseVirtualServiceList diff --git a/src/unicon/plugins/iosxe/cat9k/c9800/__init__.py b/src/unicon/plugins/iosxe/cat9k/c9800/__init__.py new file mode 100644 index 00000000..6a44eb6c --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/c9800/__init__.py @@ -0,0 +1,31 @@ +""" C9800 connection implementation. +""" + +from unicon.plugins.iosxe import IosXESingleRpConnection, IosXEDualRPConnection + +from .. import IosXEServiceList + +from .statemachine import IosXEc9800SingleRpStateMachine +from .settings import IosXEc9800Settings +from .. import service_implementation as svc +from .service_implementation import Rommon + +class IosXEc9800ServiceList(IosXEServiceList): + def __init__(self): + super().__init__() + self.reload = svc.Reload + self.rommon = Rommon + + +class IosXEc9800SingleRpConnection(IosXESingleRpConnection): + platform = 'cat9k' + model = 'c9800' + state_machine_class = IosXEc9800SingleRpStateMachine + subcommand_list = IosXEc9800ServiceList + settings = IosXEc9800Settings() + + +class IosXEc9800DualRPConnection(IosXEDualRPConnection): + platform = 'cat9k' + model = 'c9800' + settings = IosXEc9800Settings() diff --git a/src/unicon/plugins/iosxe/cat9k/c9800/service_implementation.py b/src/unicon/plugins/iosxe/cat9k/c9800/service_implementation.py new file mode 100644 index 00000000..f1c51a2f --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/c9800/service_implementation.py @@ -0,0 +1,30 @@ + + +from unicon.eal.dialogs import Dialog +from unicon.core.errors import SubCommandFailure +from unicon.plugins.generic.service_implementation import Execute as GenericExecute + +class Rommon(GenericExecute): + """C9800-specific Rommon service. + Requires explicit config_register to be passed when invoked. + No Enable Break parsing (not in C9800 show boot output). + """ + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.start_state = 'rommon' + self.end_state = 'rommon' + self.service_name = 'rommon' + self.timeout = kwargs.get('reload_timeout', 600) + self.__dict__.update(kwargs) + + def pre_service(self, *args, **kwargs): + sm = self.get_sm() + con = self.connection + sm.go_to('enable', con.spawn, context=self.context) + confreg = kwargs.get('config_register', "0x0") + try: + con.configure(f'config-register {confreg}') + except Exception as err: + raise SubCommandFailure(f"Failed to configure config-register {confreg}", err) + + super().pre_service(*args, **kwargs) \ No newline at end of file diff --git a/src/unicon/plugins/iosxe/cat9k/c9800/settings.py b/src/unicon/plugins/iosxe/cat9k/c9800/settings.py new file mode 100644 index 00000000..6571aeaf --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/c9800/settings.py @@ -0,0 +1,8 @@ + +from unicon.plugins.iosxe.cat9k.settings import IosXECat9kSettings + + +class IosXEc9800Settings(IosXECat9kSettings): + + def __init__(self): + super().__init__() diff --git a/src/unicon/plugins/iosxe/cat9k/c9800/statemachine.py b/src/unicon/plugins/iosxe/cat9k/c9800/statemachine.py new file mode 100644 index 00000000..95f1d036 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/c9800/statemachine.py @@ -0,0 +1,8 @@ + +from unicon.plugins.iosxe.cat9k.statemachine import IosXECat9kSingleRpStateMachine + + +class IosXEc9800SingleRpStateMachine(IosXECat9kSingleRpStateMachine): + + def create(self): + super().create() diff --git a/src/unicon/plugins/iosxe/cat9k/c9800cl/__init__.py b/src/unicon/plugins/iosxe/cat9k/c9800cl/__init__.py new file mode 100644 index 00000000..a26df3e6 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/c9800cl/__init__.py @@ -0,0 +1,23 @@ + +from unicon.plugins.iosxe.cat9k.c9800 import IosXEc9800ServiceList, IosXEc9800SingleRpConnection, IosXEc9800DualRPConnection +from unicon.plugins.iosxe import service_implementation as svc + +class IosXEc9800CLServiceList(IosXEc9800ServiceList): + def __init__(self): + super().__init__() + self.rommon = svc.Rommon + + + +class IosXEc9800CLSingleRpConnection(IosXEc9800SingleRpConnection): + os = 'iosxe' + platform = 'cat9k' + model = 'c9800_cl' + subcommand_list = IosXEc9800CLServiceList + + +class IosXEc9800CLDualRpConnection(IosXEc9800DualRPConnection): + os = 'iosxe' + platform = 'cat9k' + model = 'c9800_cl' + subcommand_list = IosXEc9800CLServiceList diff --git a/src/unicon/plugins/iosxe/cat9k/patterns.py b/src/unicon/plugins/iosxe/cat9k/patterns.py new file mode 100644 index 00000000..709eaaf4 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/patterns.py @@ -0,0 +1,11 @@ + +from ..patterns import IosXEPatterns + + +class IosXECat9kPatterns(IosXEPatterns): + + def __init__(self): + super().__init__() + self.boot_interrupt_prompt = r'Preparing to autoboot. \[Press Ctrl-C to interrupt\]' + self.container_shell_prompt = r'^(.*?)\n(/(\S+)?)+\s+#\s*$' + self.container_ssh_prompt = r'^(.*?)(\w\w-){6,}.*?[\$#]\s*$' diff --git a/src/unicon/plugins/iosxe/cat9k/service_implementation.py b/src/unicon/plugins/iosxe/cat9k/service_implementation.py new file mode 100644 index 00000000..8bd383ee --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/service_implementation.py @@ -0,0 +1,167 @@ + + +import re + +from unicon.eal.dialogs import Dialog +from unicon.core.errors import SubCommandFailure +from unicon.plugins.generic.service_statements import ( + reload_statement_list, + ha_reload_statement_list) +from unicon.plugins.generic.service_implementation import ( + Execute as GenericExecute, + HAReloadService as GenericHAReloadService +) + +from ..service_implementation import Reload as XEReload +from ..statements import boot_from_rommon_stmt + + +class Reload(XEReload): + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + # Override the service dialog + self.dialog = Dialog(reload_statement_list + [boot_from_rommon_stmt]) + + def pre_service(self, *args, **kwargs): + if "image_to_boot" in kwargs: + self.start_state = 'rommon' + if 'image_to_boot' in self.context: + self.context['orig_image_to_boot'] = self.context['image_to_boot'] + self.context["image_to_boot"] = kwargs["image_to_boot"] + self.connection.log.info("'image_to_boot' specified with reload, transitioning to 'rommon' state") + else: + if 'image' in kwargs: + self.context['image_to_boot'] = kwargs.get('image') + self.start_state = 'enable' + + super().pre_service(*args, **kwargs) + + def call_service(self, *args, **kwargs): + # assume the device is in rommon if image_to_boot is passed + # update reload command to use rommon boot syntax + if "image_to_boot" in kwargs: + if 'rommon_vars' in kwargs and self.connection.state_machine.current_state == 'rommon': + self.connection.execute([f'set {k}={v}' for k, v in kwargs['rommon_vars'].items()]) + self.context["image_to_boot"] = kwargs["image_to_boot"] + reload_command = "boot {}".format( + self.context['image_to_boot']).strip() + super().call_service(reload_command, *args, **kwargs) + self.context.pop("image_to_boot", None) + else: + super().call_service(*args, **kwargs) + + def post_service(self, *args, **kwargs): + if 'orig_image_to_boot' in self.context: + self.context['image_to_boot'] = self.context.pop('orig_image_to_boot') + super().post_service(*args, **kwargs) + + +class HAReloadService(GenericHAReloadService): + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.dialog = Dialog(ha_reload_statement_list + [boot_from_rommon_stmt]) + + def pre_service(self, *args, **kwargs): + if "image_to_boot" in kwargs: + if 'rommon_vars' in kwargs and all(con.state_machine.current_state == 'rommon' for con in self.connection._subconnections): + for con in self.connection._subconnections: + con.execute([f'set {k}={v}' for k, v in kwargs['rommon_vars'].items()]) + self.start_state = 'rommon' + if 'image_to_boot' in self.context: + self.context['orig_image_to_boot'] = self.context['image_to_boot'] + self.context["image_to_boot"] = kwargs["image_to_boot"] + self.connection.active.context.update({ + "image_to_boot": self.context["image_to_boot"] + }) + self.connection.standby.context.update({ + "image_to_boot": self.context["image_to_boot"] + }) + self.connection.log.info("'image_to_boot' specified with reload, transitioning to 'rommon' state") + else: + if 'image' in kwargs: + self.context['image_to_boot'] = kwargs.get('image') + self.connection.active.context.update({ + "image_to_boot": self.context["image_to_boot"] + }) + self.connection.standby.context.update({ + "image_to_boot": self.context["image_to_boot"] + }) + self.start_state = 'enable' + + super().pre_service(*args, **kwargs) + + def call_service(self, *args, **kwargs): + # assume the device is in rommon if image_to_boot is passed + # update reload command to use rommon boot syntax + if "image_to_boot" in kwargs: + reload_command = "boot {}".format( + self.context['image_to_boot']).strip() + super().call_service(reload_command, *args, **kwargs) + self.context.pop("image_to_boot", None) + else: + super().call_service(*args, **kwargs) + + def post_service(self, *args, **kwargs): + if 'orig_image_to_boot' in self.context: + self.context['image_to_boot'] = self.context.pop('orig_image_to_boot') + self.connection.active.context.pop('image_to_boot', None) + self.connection.standby.context.pop('image_to_boot', None) + super().post_service(*args, **kwargs) + + +class Rommon(GenericExecute): + """ Brings device to the Rommon prompt and executes commands specified + """ + def __init__(self, connection, context, **kwargs): + # Connection object will have all the received details + super().__init__(connection, context, **kwargs) + self.start_state = 'rommon' + self.end_state = 'rommon' + self.service_name = 'rommon' + self.timeout = 600 + self.__dict__.update(kwargs) + + def pre_service(self, *args, **kwargs): + sm = self.get_sm() + con = self.connection + sm.go_to('enable', + con.spawn, + context=self.context) + boot_info = con.execute('show boot') + m = re.search(r'Enable Break = (yes|no|0|1)|ENABLE_BREAK variable (= yes|does not exist)', boot_info) + if m: + break_enabled = m.group() + if all(i not in break_enabled for i in ['yes', '1']): + con.configure('boot enable-break') + else: + raise SubCommandFailure('Could not determine if break is enabled, cannot transition to rommon') + super().pre_service(*args, **kwargs) + + +class HARommon(Rommon): + """ Brings device to the Rommon prompt and executes commands specified + """ + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + + def pre_service(self, *args, **kwargs): + con = self.connection + + # call pre_service to reload to rommon + super().pre_service(*args, **kwargs) + + # check connection states + subcon1, subcon2 = list(con._subconnections.values()) + + # Check current state + for subcon in [subcon1, subcon2]: + subcon.sendline() + subcon.state_machine.go_to( + 'any', + subcon.spawn, + context=subcon.context, + prompt_recovery=subcon.prompt_recovery, + timeout=subcon.connection_timeout, + ) diff --git a/src/unicon/plugins/iosxe/cat9k/settings.py b/src/unicon/plugins/iosxe/cat9k/settings.py new file mode 100644 index 00000000..005ee5e8 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/settings.py @@ -0,0 +1,15 @@ + +from unicon.plugins.iosxe.settings import IosXESettings + + +class IosXECat9kSettings(IosXESettings): + + def __init__(self): + super().__init__() + self.FIND_BOOT_IMAGE = False + self.BOOT_TIMEOUT = 420 + self.CONTAINER_EXIT_CMDS = ['exit\r', '\x03\x03\x03'] + + self.ROMMON_INIT_COMMANDS = [ + "set" + ] diff --git a/src/unicon/plugins/iosxe/cat9k/stackwise_virtual/__init__.py b/src/unicon/plugins/iosxe/cat9k/stackwise_virtual/__init__.py new file mode 100644 index 00000000..647fe27b --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/stackwise_virtual/__init__.py @@ -0,0 +1,24 @@ +""" A Stackwise-virtual IOS-XE connection implementation. +""" + +from unicon.plugins.iosxe.stack import StackIosXEServiceList +from unicon.plugins.iosxe.stack import IosXEStackRPConnection +from .connection_provider import StackwiseVirtualConnectionProvider + +from unicon.plugins.iosxe.stack.service_implementation import StackReload, StackSwitchover + + +class IosXECat9kStackwiseVirtualServiceList(StackIosXEServiceList): + + def __init__(self): + super().__init__() + self.reload = StackReload + self.switchover = StackSwitchover + + +class IosXECat9kStackwiseVirtualRPConnection(IosXEStackRPConnection): + os = 'iosxe' + platform = 'cat9k' + chassis_type = 'stackwise_virtual' + connection_provider_class = StackwiseVirtualConnectionProvider + subcommand_list = IosXECat9kStackwiseVirtualServiceList diff --git a/src/unicon/plugins/iosxe/cat9k/stackwise_virtual/connection_provider.py b/src/unicon/plugins/iosxe/cat9k/stackwise_virtual/connection_provider.py new file mode 100644 index 00000000..0a4dcd98 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/stackwise_virtual/connection_provider.py @@ -0,0 +1,126 @@ +""" +Authors: + pyATS TEAM (pyats-support@cisco.com, pyats-support-ext@cisco.com) +""" +import re + +from unicon.eal.dialogs import Dialog, Statement +from unicon.bases.routers.connection_provider import BaseStackRpConnectionProvider + +from genie.metaparser.util.exceptions import SchemaEmptyParserError + +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 + additional dialogs and steps required for + connecting to stack device + """ + def __init__(self, *args, **kwargs): + + """ Initializes the base connection provider + """ + super().__init__(*args, **kwargs) + + def designate_handles(self): + """ Identifies the Role of each handle and designates if + it is active or standby + """ + + con = self.connection + + con.log.info('+++ designating handles for SVL stack +++') + + subcons = list(con._subconnections.items()) + subcon1_alias, subcon1 = subcons[0] + subcon2_alias, subcon2 = subcons[1] + target_alias = None + 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 + con.log.debug('{} in state: {}'.format(subcon.alias, subcon.state_machine.current_state)) + + if subcon1.state_machine.current_state == 'enable': + target_con = subcon1 + target_alias = subcon1_alias + other_alias = subcon2_alias + elif subcon2.state_machine.current_state == 'enable': + target_con = subcon2 + target_alias = subcon2_alias + other_alias = subcon1_alias + + con._set_active_alias(target_alias) + con._set_standby_alias(other_alias) + con._handles_designated = True + + device = con.device + try: + # To check if the device is in SVL state + 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()] + + if "active" in roles and "standby" in roles: + # Only designate handle when in SVL state + # There are case when in non-SVL the device connection + # becomes active for both connection and there isn't a standby state + # it would have either active and member state or just active state + + # Verify the active and standby + target_con.spawn.sendline(target_con.spawn.settings.SHOW_REDUNDANCY_CMD) + output = target_con.spawn.expect( + target_con.state_machine.get_state('enable').pattern, + timeout=con.settings.EXEC_TIMEOUT).match_output + + state = re.findall(target_con.spawn.settings.REDUNDANCY_STATE_PATTERN, output, flags=re.M) + target_con.log.debug(f'{target_con.spawn} state: {state}') + if any('active' in s.lower() for s in state): + con._set_active_alias(target_alias) + con._set_standby_alias(other_alias) + elif any('standby' in s.lower() for s in state): + con._set_standby_alias(target_alias) + con._set_active_alias(other_alias) + else: + raise ConnectionError('unable to designate handles') + + except Exception: + con.log.exception("Failed to designate handle for SVL stack") + + def get_connection_dialog(self): + """ creates and returns a Dialog to handle all device prompts + appearing during initial connection to the device. + See generic/statements.py for connnection statement lists + """ + con = self.connection + custom_auth_stmt = custom_auth_statements( + self.connection.settings.LOGIN_PROMPT, + self.connection.settings.PASSWORD_PROMPT) + return con.connect_reply + \ + Dialog(custom_auth_stmt + connection_statement_list + if custom_auth_stmt else connection_statement_list) diff --git a/src/unicon/plugins/iosxe/cat9k/statemachine.py b/src/unicon/plugins/iosxe/cat9k/statemachine.py new file mode 100644 index 00000000..106f774f --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/statemachine.py @@ -0,0 +1,107 @@ + +from unicon.core.errors import StateMachineError +from unicon.plugins.iosxe.statemachine import ( + IosXESingleRpStateMachine, + IosXEDualRpStateMachine, + boot_from_rommon + ) +from unicon.plugins.generic.statements import GenericStatements +from unicon.statemachine import State, Path +from unicon.eal.dialogs import Dialog, Statement + +from ..statements import boot_from_rommon_statement_list + +from .patterns import IosXECat9kPatterns +from .statements import ( + reload_to_rommon_statement_list) + + +patterns = IosXECat9kPatterns() +statements = GenericStatements() + + +def container_to_enable_transition(statemachine, spawn, context): + ''' Exit from container back to enable mode + ''' + commands = spawn.settings.CONTAINER_EXIT_CMDS + + dialog = Dialog([Statement(pattern=statemachine.get_state('container_shell').pattern, + loop_continue=False, + trim_buffer=True), + Statement(pattern=statemachine.get_state('enable').pattern, + loop_continue=False, + trim_buffer=False), + statements.syslog_msg_stmt + ]) + + for cmd in commands: + spawn.send(cmd) + dialog.process(spawn, context=context) + statemachine.detect_state(spawn) + if statemachine.current_state == 'enable': + return + else: + raise StateMachineError('Unable to transition from container shell to enable mode') + + +class IosXECat9kSingleRpStateMachine(IosXESingleRpStateMachine): + def create(self): + super().create() + + container_shell = State('container_shell', patterns.container_shell_prompt) + container_ssh = State('container_ssh', patterns.container_ssh_prompt) + + rommon = self.get_state('rommon') + disable = self.get_state('disable') + enable = self.get_state('enable') + + self.add_state(container_shell) + self.add_state(container_ssh) + + rommon.pattern = patterns.rommon_prompt + + self.remove_path('rommon', 'disable') + self.remove_path('enable', 'rommon') + + rommon_to_disable = Path(rommon, disable, boot_from_rommon, Dialog( + boot_from_rommon_statement_list)) + enable_to_rommon = Path(enable, rommon, 'reload', Dialog( + reload_to_rommon_statement_list)) + + container_shell_to_enable = Path(container_shell, enable, container_to_enable_transition, None) + + self.add_path(rommon_to_disable) + self.add_path(enable_to_rommon) + self.add_path(container_shell_to_enable) + + +class IosXECat9kDualRpStateMachine(IosXEDualRpStateMachine): + + def create(self): + super().create() + + container_shell = State('container_shell', patterns.container_shell_prompt) + container_ssh = State('container_ssh', patterns.container_ssh_prompt) + + rommon = self.get_state('rommon') + disable = self.get_state('disable') + enable = self.get_state('enable') + + self.add_state(container_shell) + self.add_state(container_ssh) + + rommon.pattern = patterns.rommon_prompt + + self.remove_path('rommon', 'disable') + self.remove_path('enable', 'rommon') + + rommon_to_disable = Path(rommon, disable, boot_from_rommon, Dialog( + boot_from_rommon_statement_list)) + enable_to_rommon = Path(enable, rommon, 'reload', Dialog( + reload_to_rommon_statement_list)) + + container_shell_to_enable = Path(container_shell, enable, container_to_enable_transition, None) + + self.add_path(rommon_to_disable) + self.add_path(enable_to_rommon) + self.add_path(container_shell_to_enable) diff --git a/src/unicon/plugins/iosxe/cat9k/statements.py b/src/unicon/plugins/iosxe/cat9k/statements.py new file mode 100644 index 00000000..affe2972 --- /dev/null +++ b/src/unicon/plugins/iosxe/cat9k/statements.py @@ -0,0 +1,24 @@ + +from unicon.eal.dialogs import Statement +from unicon.plugins.generic.service_statements import ( + save_env, confirm_reset, reload_confirm, reload_confirm_ios, reload_confirm_iosxe) + +from .patterns import IosXECat9kPatterns + +patterns = IosXECat9kPatterns() + + +boot_interrupt_stmt = Statement( + pattern=patterns.boot_interrupt_prompt, + action='send(\x03)', + args=None, + loop_continue=True, + continue_timer=False) + + +reload_to_rommon_statement_list = [save_env, + confirm_reset, + reload_confirm, + reload_confirm_ios, + reload_confirm_iosxe, + boot_interrupt_stmt] diff --git a/src/unicon/plugins/iosxe/connection_provider.py b/src/unicon/plugins/iosxe/connection_provider.py new file mode 100644 index 00000000..3d4469d8 --- /dev/null +++ b/src/unicon/plugins/iosxe/connection_provider.py @@ -0,0 +1,83 @@ + +import re +from unicon.eal.dialogs import Dialog +from unicon.eal.dialogs import Statement +from unicon.core.errors import StateMachineError +from unicon.plugins.generic.connection_provider import GenericSingleRpConnectionProvider +from unicon.plugins.generic.patterns import GenericPatterns +from unicon.statemachine import State +from unicon.plugins.generic.statements import chatty_term_wait +from unicon.plugins.utils import get_device_mode + + +class IosxeSingleRpConnectionProvider(GenericSingleRpConnectionProvider): + """ Implements Iosxe singleRP Connection Provider, + This class overrides the base class with the + additional dialogs and steps required for + connecting to any device via generic implementation + """ + def __init__(self, *args, **kwargs): + + """ Initializes the generic connection provider + """ + super().__init__(*args, **kwargs) + + def learn_tokens(self): + con = self.connection + if (not con.learn_tokens or not con.settings.LEARN_DEVICE_TOKENS) and not con.operating_mode: + + # make sure device is in valid unicon state + con.sendline() + con.state_machine.go_to('any', + con.spawn, + context=con.context, + prompt_recovery=con.prompt_recovery) + + try: + con.state_machine.get_path(con.state_machine.current_state, 'enable') + except StateMachineError: + pass + else: + con.enable() + + # If the learn token is not enabled we need to see if the device is in Controller-Managed mode + # or it's in autonomous mode. If the device is in Controller-Managed mode, enable token discovery. + if get_device_mode(con) == 'Controller-Managed': + # The device is in Controller-Manged mode so we need to learn the abstraction tokens. + con.overwrite_testbed_tokens = True + con.learn_tokens = True + + # "operating_mode" attribute is added to the connection object to avoid getting in a loop + con.operating_mode = True + + # Add learn tokens state to state machine so it can use a looser + # prompt pattern to match. Required for at least some Linux prompts + if 'learn_tokens_state' not in [str(s) for s in con.state_machine.states]: + self.learn_tokens_state = State('learn_tokens_state', + GenericPatterns().learn_os_prompt) + con.state_machine.add_state(self.learn_tokens_state) + + # The first thing we need to is to send stop PnP discovery otherwise device will not execute any command. + con.spawn.sendline('pnpa service discovery stop') + + # The device may reload after the command we get the dialog statements from reload service and try to handle that + dialog = con.reload.dialog + dialog.append(Statement(con.state_machine.get_state('enable').pattern, action=None, + args=None, loop_continue=False, continue_timer=False)) + + dialog.process(con.spawn, + context=con.context, + timeout=con.settings.RELOAD_WAIT, + prompt_recovery=con.prompt_recovery) + + # The device may be chatty at this time we need to wait for + # it to to settle down. + chatty_wait_time = con.settings.CONTROLLER_MODE_CHATTY_WAIT_TIME + chatty_term_wait(con.spawn, trim_buffer=True, wait_time=chatty_wait_time) + con.sendline() + con.state_machine.go_to('any', + con.spawn, + context=con.context, + prompt_recovery=con.prompt_recovery) + super().learn_tokens() + diff --git a/src/unicon/plugins/iosxe/csr1000v/__init__.py b/src/unicon/plugins/iosxe/csr1000v/__init__.py index f193860c..27795bd4 100644 --- a/src/unicon/plugins/iosxe/csr1000v/__init__.py +++ b/src/unicon/plugins/iosxe/csr1000v/__init__.py @@ -11,7 +11,7 @@ class IosXECsr1000vServiceList(IosXEServiceList): class IosXECsr1000vSingleRpConnection(IosXESingleRpConnection): - series = 'csr1000v' + platform = 'csr1000v' state_machine_class = IosXECsr1000vSingleRpStateMachine subcommand_list = IosXECsr1000vServiceList settings = IosXECsr1000vSettings() diff --git a/src/unicon/plugins/iosxe/csr1000v/patterns.py b/src/unicon/plugins/iosxe/csr1000v/patterns.py index c1fc634a..e931d24d 100644 --- a/src/unicon/plugins/iosxe/csr1000v/patterns.py +++ b/src/unicon/plugins/iosxe/csr1000v/patterns.py @@ -6,9 +6,3 @@ class IosXECsr1000vPatterns(IosXEPatterns): def __init__(self): super().__init__() - - # Saw the following line in the CSR1000V log that led to a - # match failure, so relaxing the config_prompt. - # Router(config-line)#tion generated from file cdrom1:/ovf-env.xml - # Added cloud as pattern can be cloud-aws or cloud-azure under redundancy config - self.config_prompt = r'^(.*)\(.*(con|cfg|ipsec-profile|cloud)\S*\)#\s?$' diff --git a/src/unicon/plugins/iosxe/csr1000v/statemachine.py b/src/unicon/plugins/iosxe/csr1000v/statemachine.py index 2e92ec17..09dacae5 100644 --- a/src/unicon/plugins/iosxe/csr1000v/statemachine.py +++ b/src/unicon/plugins/iosxe/csr1000v/statemachine.py @@ -13,21 +13,3 @@ class IosXECsr1000vSingleRpStateMachine(IosXESingleRpStateMachine): def create(self): super().create() - self.remove_path('enable', 'rommon') - self.remove_path('rommon', 'disable') - self.remove_state('rommon') - - # Saw the following line in the CSR1000V log that led to a - # match failure, so relaxing the config_prompt. - # Router(config-line)#tion generated from file cdrom1:/ovf-env.xml - self.remove_path('enable', 'config') - self.remove_path('config', 'enable') - self.remove_state('config') - - config = State('config', patterns.config_prompt) - enable = [state for state in self.states if state.name == 'enable'][0] - enable_to_config = Path(enable, config, self.config_command, None) - config_to_enable = Path(config, enable, 'end', None) - self.add_state(config) - self.add_path(enable_to_config) - self.add_path(config_to_enable) diff --git a/src/unicon/plugins/iosxe/iec3400/__init__.py b/src/unicon/plugins/iosxe/iec3400/__init__.py new file mode 100644 index 00000000..a62e7b4d --- /dev/null +++ b/src/unicon/plugins/iosxe/iec3400/__init__.py @@ -0,0 +1,21 @@ + +from unicon.plugins.iosxe import IosXEServiceList, IosXESingleRpConnection + +from .settings import IosXEIec3400Settings +from . import service_implementation as svc +from .statemachine import IosXEIec3400SingleRpStateMachine + + +class IosXEIec3400ServiceList(IosXEServiceList): + def __init__(self): + super().__init__() + self.reload = svc.Reload + + +class IosXEIec3400SingleRpConnection(IosXESingleRpConnection): + os = 'iosxe' + platform = 'iec3400' + chassis_type = 'single_rp' + state_machine_class = IosXEIec3400SingleRpStateMachine + subcommand_list = IosXEIec3400ServiceList + settings = IosXEIec3400Settings() diff --git a/src/unicon/plugins/iosxe/iec3400/service_implementation.py b/src/unicon/plugins/iosxe/iec3400/service_implementation.py new file mode 100644 index 00000000..3ebd85f8 --- /dev/null +++ b/src/unicon/plugins/iosxe/iec3400/service_implementation.py @@ -0,0 +1,93 @@ + +from unicon.bases.routers.services import BaseService +from unicon.plugins.generic.service_implementation import ReloadResult +from unicon.eal.dialogs import Dialog +from unicon.core.errors import SubCommandFailure +from unicon.utils import AttributeDict + +from .service_statements import reload_statement_list + + +class Reload(BaseService): + """Service to reload the device. + + Arguments: + reload_command: reload command to be issued on device. + default reload_command is "reload" + dialog: Dialog which include list of Statements for + additional dialogs prompted by reload command, in-case + it is not in the current list. + timeout: Timeout value in sec, Default Value is 400 sec + image_to_boot: image to be used if the device stops in rommon mode + + Returns: + bool: True on success False otherwise + + Raises: + SubCommandFailure: on failure. + + Example: + .. code-block:: python + + uut.reload() + """ + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.start_state = 'enable' + self.end_state = 'enable' + self.timeout = connection.settings.RELOAD_TIMEOUT + self.dialog = Dialog(reload_statement_list) + + def call_service(self, + reload_command='reload', + dialog=Dialog([]), + timeout=None, + return_output=False, + error_pattern=None, + append_error_pattern=None, + *args, + **kwargs): + + con = self.connection + timeout = timeout or self.timeout + + if error_pattern is None: + self.error_pattern = con.settings.ERROR_PATTERN + else: + self.error_pattern = error_pattern + + if not isinstance(self.error_pattern, list): + raise ValueError('error_pattern should be a list') + if append_error_pattern: + if not isinstance(append_error_pattern, list): + raise ValueError('append_error_pattern should be a list') + self.error_pattern += append_error_pattern + sm = self.get_sm() + assert isinstance(dialog, + Dialog), "dialog passed must be an instance of Dialog" + dialog += self.dialog + + con.log.debug( + "+++ reloading {} with reload_command {} and timeout is {} +++" + .format(self.connection.hostname, reload_command, timeout)) + + context = AttributeDict(self.context) + dialog = self.service_dialog(service_dialog=dialog) + dialog += Dialog([[sm.get_state('disable').pattern]]) + con.spawn.sendline(reload_command) + try: + reload_op=dialog.process(con.spawn, context=context, timeout=timeout, + prompt_recovery=self.prompt_recovery) + sm.detect_state(con.spawn, context=context) + con.state_machine.go_to('enable', con.spawn, + context=context, + timeout=con.connection_timeout, + prompt_recovery=self.prompt_recovery) + except Exception as err: + raise SubCommandFailure("Reload failed : {}".format(err)) + + con.log.debug("+++ Reload Completed Successfully +++") + self.result = True + if return_output: + self.result = ReloadResult(self.result, reload_op.match_output.replace(reload_command, '', 1)) diff --git a/src/unicon/plugins/iosxe/iec3400/service_statements.py b/src/unicon/plugins/iosxe/iec3400/service_statements.py new file mode 100644 index 00000000..73f15087 --- /dev/null +++ b/src/unicon/plugins/iosxe/iec3400/service_statements.py @@ -0,0 +1,13 @@ + +from unicon.eal.dialogs import Statement + + +reload_proceed_stmt = Statement(pattern=r'.*Proceed with reload\?\[y/n]\s*$', + action='sendline(y)', + loop_continue=True, + continue_timer=False) + + +reload_statement_list = [ + reload_proceed_stmt +] diff --git a/src/unicon/plugins/iosxe/iec3400/settings.py b/src/unicon/plugins/iosxe/iec3400/settings.py new file mode 100644 index 00000000..3710bd0b --- /dev/null +++ b/src/unicon/plugins/iosxe/iec3400/settings.py @@ -0,0 +1,13 @@ + + +from unicon.plugins.iosxe.settings import IosXESettings + + +class IosXEIec3400Settings(IosXESettings): + + def __init__(self): + super().__init__() + self.RELOAD_TIMEOUT = 120 + + self.HA_INIT_EXEC_COMMANDS = [] + self.HA_INIT_CONFIG_COMMANDS = [] diff --git a/src/unicon/plugins/iosxe/iec3400/statemachine.py b/src/unicon/plugins/iosxe/iec3400/statemachine.py new file mode 100644 index 00000000..c0cbcc18 --- /dev/null +++ b/src/unicon/plugins/iosxe/iec3400/statemachine.py @@ -0,0 +1,14 @@ + +from unicon.plugins.generic.service_statements import generic_statements + +from unicon.plugins.iosxe.statemachine import IosXESingleRpStateMachine + + +class IosXEIec3400SingleRpStateMachine(IosXESingleRpStateMachine): + + def create(self): + super().create() + config_to_enable = self.get_path('config', 'enable') + config_to_enable.command = 'exit' + + self.add_default_statements([generic_statements.terminal_position_stmt]) diff --git a/src/unicon/plugins/iosxe/patterns.py b/src/unicon/plugins/iosxe/patterns.py index 6e5c5181..7ce5b8a5 100644 --- a/src/unicon/plugins/iosxe/patterns.py +++ b/src/unicon/plugins/iosxe/patterns.py @@ -5,25 +5,45 @@ from unicon.plugins.generic.patterns import GenericPatterns from unicon.plugins.generic.service_patterns import ReloadPatterns + class IosXEPatterns(GenericPatterns): + def __init__(self): super().__init__() - self.shell_prompt = r'^(.*?)\[%N.*\]\$\s?$' + self.shell_prompt = r'^(.*?)\[(%N|[Ss]witch|[Rr]outer|eWLC).*?\]\$\s?$' self.access_shell = \ r'^.*Are you sure you want to continue\? \[y/n\]\s?.*$' self.overwrite_previous = \ r'^.*Overwrite the previous NVRAM configuration\?\[confirm\].*$' self.are_you_sure = \ - r'^.*Are you sure you want to continue\? \(y\/n\)\[y\]:\s?$' + r'^.*Are you sure you want to continue\? \(y\/n\)\[y\]:?\s?$' self.delete_filename = r'^.*Delete filename \[.*\]\?\s*$' - self.confirm = r'^.*\[confirm\]\s*$' self.wish_continue = r'^.*Do you wish to continue\? \[yes\]:\s*$' self.want_continue = r'^.*Do you want to continue\? \[no\]:\s*$' + self.want_continue_confirm = r'.*Do you want to continue\?\s*\[confirm]\s*$' + self.want_continue_yes = r'.*Do you want to continue\?\s*\[y/n]\?\s*\[yes]:\s*$' self.disable_prompt = \ - r'^(.*?)(Router|Switch|ios|switch|%N)(\(standby\))?(-stby)?(\(boot\))?>\s?$' + r'^(?!.*?grub)(.*?)(\(unlicensed\))?(wlc|WLC|Router|RouterRP|Switch|ios|switch|%N)([0-9])?(\(recovery-mode\))?(\(rp-rec-mode\))?(\(standby\))?(-stby)?(-standby)?(\(boot\))?(?\s?$' self.enable_prompt = \ - r'^(.*?)(Router|Switch|ios|switch|%N)(\(standby\))?(-stby)?(\(boot\))?#\s?$' + r'^(.*?)(\(unlicensed\))?(wlc|WLC|eWLC|Router|RouterRP|Switch|ios|switch|%N)([a-zA-Z0-9-]*)(\(recovery-mode\))?(\(rp-rec-mode\))?(\(standby\))?(-stby)?(-standby)?(\(boot\))?#[\s\x07]*$' + self.maintenance_mode_prompt = \ + r'^(.*?)(\(unlicensed\))?(wlc|WLC|Router|RouterRP|Switch|ios|switch|%N)([0-9])?(\(standby\))?(-stby)?(-standby)?(\(boot\))?\(maint-mode\)#[\s\x07]*$' self.press_enter = ReloadPatterns().press_enter + self.config_prompt = r'^(.*)\((?!.*pki-hexmode).*(con|cfg|ipsec-profile|ca-trustpoint|ca-certificate-map|cs-server|ca-profile|gkm-local-server|cloud|host-list|config-gkm-group|gkm-sa-ipsec|gdoi-coop-ks-config|wsma|enforce-rule|DDNS|ca-trustpool|cert-trustpool)\S*\)#\s?$' + + + self.config_pki_prompt = r'^(.*)\(config-pki-hexmode\)#\s?$' + self.are_you_sure_ywtdt = r'Are you sure you want to do this\? \[yes/no\]:\s*$' + self.do_you_want_to = r'^.*Do you want to remove the above files\? \[y\/n]\s*$' + self.confirm_uncommited_changes = r'Uncommitted changes found, commit them\? \[yes\/no\/CANCEL\]\s*$' + self.proceed_confirm = r'^.*Proceed\? \[yes,no\]\s*$' + # Don't use hostname in tclsh prompt, hostname may be truncated + self.tclsh_prompt = r'^(.*?)\(tcl.*?\)#[\s\x07]*$' + self.macro_prompt = r'^(.*?)(\{\.\.\}|then.else.fi)\s*>\s*$' + self.unable_to_create = r'^(.*?)Unable to create.*$' + self.acm_prompt = r'^(.*?)\(acm.*?\)#[\s\x07]*$' + self.syntax_prompt = r'^(.*?)\(syntax.*?\)#[\s\x07]*$' + self.rules_prompt = r'^(.*?)\(rules.*?\)#[\s\x07]*$' class IosXEReloadPatterns(ReloadPatterns): @@ -37,10 +57,16 @@ def __init__(self): self.useracess = r'^.*User Access Verification' self.setup_dialog = r'^.*(initial|basic) configuration dialog.*\s?' self.autoinstall_dialog = r'^(.*)Would you like to terminate autoinstall\? ?\[yes\]: $' - self.default_prompts = r'^(.*?)(Router|Switch|ios|switch|.*)(\(standby\))?(\(boot\))?(>|#)' + self.default_prompts = r'^(.*?)(wlc|WLC|Router|RouterRP|Switch|ios|switch|.*)([0-9])?(\(standby\))?(\(boot\))?(>|#)' self.telnet_prompt = r'^.*telnet>\s?' self.please_reset = r'^(.*)Please reset' + self.grub_prompt = r'.*The highlighted entry will be (booted|executed) automatically in .*?(\x1b\S+)?\s+' # The uniclean package expects these patterns to be here. self.enable_prompt = IosXEPatterns().enable_prompt self.disable_prompt = IosXEPatterns().disable_prompt + +class FactoryResetPatterns: + def __init__(self): + self.factory_reset_confirm = r'factory reset operation is irreversible for all operations\. Are you sure\? \[confirm\]' + self.are_you_sure_confirm = r'Are you sure you want to continue\? \[confirm\]' diff --git a/src/unicon/plugins/iosxe/quad/__init__.py b/src/unicon/plugins/iosxe/quad/__init__.py new file mode 100644 index 00000000..ca774925 --- /dev/null +++ b/src/unicon/plugins/iosxe/quad/__init__.py @@ -0,0 +1,28 @@ +""" IOS-XE Quad connection implementation """ +from unicon.bases.routers.connection import BaseQuadRpConnection +from unicon.bases.routers.connection_provider import BaseQuadRpConnectionProvider + +from unicon.plugins.iosxe import HAIosXEServiceList + +from .settings import IosXEQuadSettings +from .statemachine import IosXEQuadStateMachine +from .service_implementation import QuadGetRPState, QuadSwitchover, QuadReload + + +class IosXEQuadServiceList(HAIosXEServiceList): + + def __init__(self): + super().__init__() + self.get_rp_state = QuadGetRPState + self.switchover = QuadSwitchover + self.reload = QuadReload + + +class IosXEQuadRPConnection(BaseQuadRpConnection): + os = 'iosxe' + platform = None + chassis_type = 'quad' + subcommand_list = IosXEQuadServiceList + state_machine_class = IosXEQuadStateMachine + connection_provider_class = BaseQuadRpConnectionProvider + settings = IosXEQuadSettings() diff --git a/src/unicon/plugins/iosxe/quad/patterns.py b/src/unicon/plugins/iosxe/quad/patterns.py new file mode 100644 index 00000000..a92db6de --- /dev/null +++ b/src/unicon/plugins/iosxe/quad/patterns.py @@ -0,0 +1,13 @@ +""" IOS-XE Quad Patterns """ +from unicon.plugins.iosxe.patterns import IosXEPatterns + + +class IosXEQuadPatterns(IosXEPatterns): + def __init__(self): + super().__init__() + + self.rpr_state = r'RPR Mode: Remote supervisor is already active' + self.unlock_state = r'RPR Mode: Remote Supervisor is no longer active' + self.autoboot =r'Preparing to autoboot.+\[Press Ctrl-C to interrupt\]' + self.ica = r'RPR Mode:.+Will boot as in-chassis active' + self.proceed_switchover = r'^.*Proceed with switchover to standby RP\? \[confirm\]' diff --git a/src/unicon/plugins/iosxe/quad/service_implementation.py b/src/unicon/plugins/iosxe/quad/service_implementation.py new file mode 100644 index 00000000..10c89e37 --- /dev/null +++ b/src/unicon/plugins/iosxe/quad/service_implementation.py @@ -0,0 +1,329 @@ +""" IOS-XE Quad service implementations. """ +from time import sleep, time +from collections import namedtuple + +from unicon.eal.dialogs import Dialog +from unicon.core.errors import SubCommandFailure +from unicon.bases.routers.services import BaseService + +from unicon.plugins.generic.statements import custom_auth_statements + +from .utils import QuadUtils +from .service_statements import quad_switchover_stmt_list, quad_reload_stmt_list + +utils = QuadUtils() + +class QuadGetRPState(BaseService): + """ Get Rp state + + Service to get the redundancy state of the device rp. + + Arguments: + target: Service target, by default active + + Returns: + Expected return values are ACTIVE, STANDBY, MEMBER, IN_CHASSIS_STANDBY + raise SubCommandFailure on failure. + + Example: + .. code-block:: python + + rtr.get_rp_state() + rtr.get_rp_state(target='standby') + """ + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.start_state = 'enable' + self.end_state = 'enable' + self.timeout = connection.settings.EXEC_TIMEOUT + self.__dict__.update(kwargs) + + def pre_service(self, *args, **kwargs): + if 'target' in kwargs: + handle = self.get_handle(kwargs['target']) + if handle.state_machine.current_state == 'rpr': + return + + super().pre_service(*args, **kwargs) + + def post_service(self, *args, **kwargs): + if 'target' in kwargs: + handle = self.get_handle(kwargs['target']) + if handle.state_machine.current_state == 'rpr': + return + + super().post_service(*args, **kwargs) + + def call_service(self, + target='active', + timeout=None, + utils=utils, + *args, + **kwargs): + """send the command on the right rp and return the output""" + handle = self.get_handle(target) + timeout = timeout or self.timeout + if handle.state_machine.current_state == 'rpr': + self.result = {'role': 'IN_CHASSIS_STANDBY'} + return + + try: + info_dict = utils.get_redundancy_details(handle, timeout=timeout) + except Exception as err: + raise SubCommandFailure("get_rp_state failed", err) from err + + self.result = info_dict.get(str(handle.member_id)) + + def get_service_result(self): + if 'role' in self.result: + return self.result['role'].upper() + else: + return "None" + + +class QuadSwitchover(BaseService): + """ Quad switchover service + + Service for Quad device switchover. + + Arguments: + command ('str'): Switchover command to execute, + by default "redundancy force-switchover" + reply ('Dialog'): Extra switchover dialogs, by default None + timeout ('int'): Switchover timeout value, by default 600 secs + sync_standby ('bool'): Whether to sync up standby RP, by default True + + Returns: + result ('bool'): True/False + raise SubCommandFailure on failure. + + Example: + .. code-block:: python + + rtr.switchover() + rtr.switchover(timeout=900) + """ + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.start_state = 'enable' + self.end_state = 'enable' + self.timeout = connection.settings.QUAD_SWITCHOVER_TIMEOUT + self.command = "redundancy force-switchover" + self.dialog = Dialog(quad_switchover_stmt_list) + self.__dict__.update(kwargs) + + def call_service(self, command=None, + reply=Dialog([]), + timeout=None, + sync_standby=True, + *args, **kwargs): + + self.result = False + switchover_cmd = command or self.command + timeout = timeout or self.timeout + conn = self.connection.active + + dialog = self.dialog + + if reply: + dialog = reply + self.dialog + custom_auth_stmt = custom_auth_statements( + conn.settings.LOGIN_PROMPT, + conn.settings.PASSWORD_PROMPT) + if custom_auth_stmt: + dialog += Dialog(custom_auth_stmt) + + self.connection.log.info('Processing on original Global Active rp ' + '%s-%s' % (conn.hostname, conn.alias)) + conn.sendline(switchover_cmd) + try: + active_output = dialog.process(conn.spawn, timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=conn.context) + except Exception as e: + raise SubCommandFailure('Error during switchover ', e) from e + + # check if active rp changed to rpr state and update state machine + if 'state' in conn.context and conn.context.state == 'rpr': + conn.state_machine.detect_state(conn.spawn) + conn.context.pop('state') + + self.connection.log.info('Processing on new Global Active rp ' + '%s-%s' % (conn.hostname, self.connection.standby.alias)) + + if utils.is_active_ready(self.connection.standby): + self.connection.log.info('Standby RP changed to active role') + # Reassign roles for each rp + # standby -> active + # active ics -> standby + # standby ics -> active ics + # active -> standby ics + self.reassign_roles(conn) + else: + raise SubCommandFailure('Failed to bring standby rp to active role') + + if not sync_standby: + self.connection.log.info("Standby state check disabled on user request") + self.connection.log.info('Switchover successful') + self.result = True + else: + new_active = self.connection.active + new_standby = self.connection.standby + self.connection.log.info('Waiting for new standby RP to be STANDBY HOT') + + start_time = time() + while (time() - start_time) < timeout: + if utils.is_peer_standby_hot(new_active): + self.connection.log.info('Standby RP is in STANDBY HOT state.') + break + else: + self.connection.log.info('Sleeping for %s secs.' % \ + self.connection.settings.QUAD_SWITCHOVER_SLEEP) + sleep(self.connection.settings.QUAD_SWITCHOVER_SLEEP) + else: + self.connection.log.info('Timeout in %s secs. ' + 'Standby RP is not in STANDBY HOT state. Switchover failed' % timeout) + self.result = False + return + + new_active.execute('show module') + + self.connection.log.info('Processing on new Global Standby rp ' + '%s-%s' % (conn.hostname, new_standby.alias)) + new_standby.spawn.sendline() + try: + new_standby.state_machine.go_to( + 'any', new_standby.spawn, context=new_standby.context) + new_standby.state_machine.detect_state(new_standby.spawn) + new_standby.enable() + except Exception as e: + raise SubCommandFailure('Error while bringing standby rp ' + 'to enable state', e) from e + + self.connection.log.info('Switchover sucessful') + self.result = True + + + def reassign_roles(self, active_con): + ''' reassign roles for each rp + standby -> active, active ics -> standby, + standby ics -> active ics, active -> standby ics + ''' + self.connection.log.info("Reassign roles for each rp") + self.connection._set_active_alias(self.connection.standby.alias) + self.connection._set_standby_alias(self.connection.active_ics.alias) + self.connection._set_active_ics_alias(self.connection.standby_ics.alias) + self.connection._set_standby_ics_alias(active_con.alias) + + def post_service(self, *args, **kwargs): + pass + + +class QuadReload(BaseService): + """ Service to reload the Quad device. + + Arguments: + reload_command: reload command to be used. default "redundancy reload shelf" + reply: Additional Dialog( i.e patterns) to be handled + timeout: Timeout value in sec, Default Value is 60 sec + image_to_boot: image to boot from rommon state + return_output: if True, return namedtuple with result and reload output + + Returns: + console True on Success, raises SubCommandFailure on failure. + + Example: + .. code-block:: python + + rtr.reload() + # If reload command is other than 'reload' + rtr.reload(reload_command="reload location all", timeout=700) + """ + + def __init__(self, connection, context, *args, **kwargs): + super().__init__(connection, context, *args, **kwargs) + self.start_state = 'enable' + self.end_state = 'enable' + self.timeout = connection.settings.QUAD_RELOAD_TIMEOUT + self.reload_command = "reload" + self.dialog = Dialog(quad_reload_stmt_list) + self.__dict__.update(kwargs) + + def call_service(self, + reload_command=None, + reply=Dialog([]), + timeout=None, + return_output=False, + error_pattern=None, + append_error_pattern=None, + *args, + **kwargs): + + self.result = False + reload_cmd = reload_command or self.reload_command + timeout = timeout or self.timeout + conn = self.connection.active + + if error_pattern is None: + self.error_pattern = conn.settings.ERROR_PATTERN + else: + self.error_pattern = error_pattern + + if not isinstance(self.error_pattern, list): + raise ValueError('error_pattern should be a list') + if append_error_pattern: + if not isinstance(append_error_pattern, list): + raise ValueError('append_error_pattern should be a list') + self.error_pattern += append_error_pattern + + reload_dialog = self.dialog + if reply: + reload_dialog = reply + reload_dialog + + custom_auth_stmt = custom_auth_statements(conn.settings.LOGIN_PROMPT, + conn.settings.PASSWORD_PROMPT) + if custom_auth_stmt: + reload_dialog += Dialog(custom_auth_stmt) + + self.connection.log.info('Processing on rp %s-%s' % + (conn.hostname, conn.alias)) + conn.sendline(reload_cmd) + try: + reload_output = reload_dialog.process(conn.spawn, timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=conn.context) + self.result=reload_output.match_output + self.get_service_result() + except Exception as e: + raise SubCommandFailure('Error during reload', e) from e + + try: + # check other rp if they reach to stable state + for subconn in self.connection.subconnections: + if subconn.alias != conn.alias: + self.connection.log.info('Processing on rp %s-%s' % + (conn.hostname, subconn.alias)) + subconn.spawn.sendline() + reload_peer_output = reload_dialog.process( + subconn.spawn, timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=subconn.context) + except Exception as e: + raise SubCommandFailure('Reload failed.', e) from e + + self.connection.log.info('Sleeping for %s secs.' % \ + self.connection.settings.QUAD_RELOAD_SLEEP) + sleep(self.connection.settings.QUAD_RELOAD_SLEEP) + + self.connection.log.info('Disconnecting and reconnecting') + self.connection.disconnect() + self.connection.connect() + + self.connection.log.info("+++ Reload Completed Successfully +++") + self.result = True + + if return_output: + Result = namedtuple('Result', ['result', 'output']) + self.result = Result(self.result, reload_output.match_output.replace(reload_cmd, '', 1)) diff --git a/src/unicon/plugins/iosxe/quad/service_statements.py b/src/unicon/plugins/iosxe/quad/service_statements.py new file mode 100644 index 00000000..f6315715 --- /dev/null +++ b/src/unicon/plugins/iosxe/quad/service_statements.py @@ -0,0 +1,40 @@ +""" Generic IOS-XE Quad Service Statements """ + +from unicon.eal.dialogs import Statement +from unicon.plugins.generic.service_statements import (reload_statement_list, + switchover_statement_list) + +from .patterns import IosXEQuadPatterns + +patterns = IosXEQuadPatterns() + +def update_rpr_state(spawn, context, state): + context['state'] = state + +# proceed_switchover +proceed_sw = Statement(pattern=patterns.proceed_switchover, + action='sendline()', + loop_continue=True, + continue_timer=False) +# rpr_state +rpr_state = Statement(pattern=patterns.rpr_state, + action=update_rpr_state, + args={'state': 'rpr'}, + trim_buffer=False, + loop_continue=False, + continue_timer=False) +# press_enter +press_enter = Statement(pattern=patterns.press_enter, + action='sendline()', + loop_continue=False, + continue_timer=False) + +quad_switchover_stmt_list = list(switchover_statement_list) +quad_switchover_stmt_list.insert(0, proceed_sw) +quad_switchover_stmt_list.insert(0, rpr_state) +quad_switchover_stmt_list.insert(0, press_enter) + + +quad_reload_stmt_list = list(reload_statement_list) +quad_reload_stmt_list.insert(0, rpr_state) +quad_reload_stmt_list.insert(0, press_enter) \ No newline at end of file diff --git a/src/unicon/plugins/iosxe/quad/settings.py b/src/unicon/plugins/iosxe/quad/settings.py new file mode 100644 index 00000000..d0ca104a --- /dev/null +++ b/src/unicon/plugins/iosxe/quad/settings.py @@ -0,0 +1,21 @@ +""" IOS-XE Quad Settings """ + +from unicon.plugins.iosxe.settings import IosXESettings + +class IosXEQuadSettings(IosXESettings): + + def __init__(self): + super().__init__() + + # Quad detect rpr timeout + self.DETECT_RPR_TIMEOUT = 1 + + # Quad switchover timeout + self.QUAD_SWITCHOVER_TIMEOUT = 600 + # Secs to sleep after switchover + self.QUAD_SWITCHOVER_SLEEP = 30 + + # Quad reload timeout + self.QUAD_RELOAD_TIMEOUT = 600 + # Secs to sleep after reload + self.QUAD_RELOAD_SLEEP = 60 diff --git a/src/unicon/plugins/iosxe/quad/statemachine.py b/src/unicon/plugins/iosxe/quad/statemachine.py new file mode 100644 index 00000000..cf69361a --- /dev/null +++ b/src/unicon/plugins/iosxe/quad/statemachine.py @@ -0,0 +1,16 @@ +""" IOS-XE Quad State Machine """ +from unicon.statemachine import State +from unicon.plugins.iosxe.statemachine import IosXEDualRpStateMachine + +from .patterns import IosXEQuadPatterns + +patterns = IosXEQuadPatterns() + +class IosXEQuadStateMachine(IosXEDualRpStateMachine): + + def create(self): + super().create() + + # Add RPR state + rpr = State('rpr', patterns.rpr_state) + self.add_state(rpr) diff --git a/src/unicon/plugins/iosxe/quad/utils.py b/src/unicon/plugins/iosxe/quad/utils.py new file mode 100644 index 00000000..690a86d2 --- /dev/null +++ b/src/unicon/plugins/iosxe/quad/utils.py @@ -0,0 +1,27 @@ +""" Quad Utilities """ + +import re + +from unicon.plugins.iosxe.stack.utils import StackUtils + + +class QuadUtils(StackUtils): + + def is_peer_standby_hot(self, connection, timeout=None): + """ Check whether peer rp is in STANDBY HOT state + + Args: + connection (`obj`): connection object + timeout (`int`): execute timeout + Returns: + result (`bool`): True if peer in STANDBY HOT state, else False + """ + timeout = timeout or connection.settings.EXEC_TIMEOUT + + output = connection.execute("show redundancy states | in peer", + timeout=timeout) + + if 'STANDBY HOT' in output: + return True + else: + return False diff --git a/src/unicon/plugins/iosxe/sdwan/__init__.py b/src/unicon/plugins/iosxe/sdwan/__init__.py index d9608bf4..f247a3f9 100644 --- a/src/unicon/plugins/iosxe/sdwan/__init__.py +++ b/src/unicon/plugins/iosxe/sdwan/__init__.py @@ -1,6 +1,6 @@ -from unicon.plugins.iosxe import IosXESingleRpConnection, IosXEServiceList -from unicon.plugins.iosxe.sdwan.statemachine import SDWANSingleRpStateMachine +from unicon.plugins.iosxe import IosXESingleRpConnection, IosXEServiceList, IosXEDualRPConnection +from unicon.plugins.iosxe.sdwan.statemachine import SDWANSingleRpStateMachine, SDWANDualRpStateMachine from unicon.plugins.iosxe.sdwan import service_implementation as svc from unicon.plugins.iosxe.sdwan.settings import SDWANSettings @@ -11,7 +11,14 @@ def __init__(self): class SDWANSingleRpConnection(IosXESingleRpConnection): os = 'iosxe' - series = 'sdwan' + platform = 'sdwan' state_machine_class = SDWANSingleRpStateMachine subcommand_list = SDWANServiceList settings = SDWANSettings() + +class SDWANDualRpConnection(IosXEDualRPConnection): + os = 'iosxe' + platform = 'sdwan' + state_machine_class = SDWANDualRpStateMachine + subcommand_list = SDWANServiceList + settings = SDWANSettings() diff --git a/src/unicon/plugins/iosxe/sdwan/service_implementation.py b/src/unicon/plugins/iosxe/sdwan/service_implementation.py index fced0a83..34640c17 100644 --- a/src/unicon/plugins/iosxe/sdwan/service_implementation.py +++ b/src/unicon/plugins/iosxe/sdwan/service_implementation.py @@ -5,4 +5,3 @@ class SDWANConfigure(Configure): def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) self.commit_cmd = "commit" - diff --git a/src/unicon/plugins/iosxe/sdwan/statemachine.py b/src/unicon/plugins/iosxe/sdwan/statemachine.py index a9bfd167..f02b5ca7 100644 --- a/src/unicon/plugins/iosxe/sdwan/statemachine.py +++ b/src/unicon/plugins/iosxe/sdwan/statemachine.py @@ -1,12 +1,26 @@ -from unicon.plugins.iosxe.statemachine import IosXESingleRpStateMachine +from unicon.plugins.iosxe.statemachine import IosXESingleRpStateMachine, IosXEDualRpStateMachine from unicon.eal.dialogs import Dialog, Statement +from ..patterns import IosXEPatterns + +patterns = IosXEPatterns() + class SDWANSingleRpStateMachine(IosXESingleRpStateMachine): config_command = 'config-transaction' def create(self): super().create() - self.get_path('config', 'enable').dialog = Dialog([ - Statement(pattern=r'Uncommitted changes found, commit them\? \[yes\/no\/CANCEL\]', - action='sendline(no)', loop_continue=True)]) + self.get_path('config', 'enable').dialog += Dialog([ + Statement(pattern=patterns.confirm_uncommited_changes, + action='sendline(no)', loop_continue=True) + ]) +class SDWANDualRpStateMachine(IosXEDualRpStateMachine): + config_command = 'config-transaction' + + def create(self): + super().create() + self.get_path('config', 'enable').dialog += Dialog([ + Statement(pattern=patterns.confirm_uncommited_changes, + action='sendline(no)', loop_continue=True) + ]) diff --git a/src/unicon/plugins/iosxe/service_implementation.py b/src/unicon/plugins/iosxe/service_implementation.py index 42cf471f..63758745 100644 --- a/src/unicon/plugins/iosxe/service_implementation.py +++ b/src/unicon/plugins/iosxe/service_implementation.py @@ -2,151 +2,517 @@ __author__ = "Myles Dear" - +import re from unicon.eal.dialogs import Dialog +from unicon.core.errors import SubCommandFailure +from unicon.bases.routers.services import BaseService + -from unicon.plugins.generic.service_implementation import \ - Configure as GenericConfigure, \ - Execute as GenericExecute,\ - Ping as GenericPing,\ - HaConfigureService as GenericHAConfigure,\ - HaExecService as GenericHAExecute,\ - HAReloadService as GenericHAReload,\ - SwitchoverService as GenericHASwitchover, \ - Traceroute as GenericTraceroute +from unicon.plugins.generic.service_implementation import ( + Configure as GenericConfigure, + Execute as GenericExecute, + Ping as GenericPing, + HaConfigureService as GenericHAConfigure, + HaExecService as GenericHAExecute, + HAReloadService as GenericHAReload, + SwitchoverService as GenericHASwitchover, + Traceroute as GenericTraceroute, + Copy as GenericCopy, + ResetStandbyRP as GenericResetStandbyRP, + Reload as GenericReload, + Enable as GenericEnable, + ContextMgrBaseService) -from .service_statements import overwrite_previous, are_you_sure, \ - delete_filename, confirm, wish_continue, want_continue +from .service_statements import execute_statement_list, configure_statement_list, confirm -from unicon.plugins.generic.service_implementation import BashService +from .statements import grub_prompt_stmt, boot_from_rommon_stmt, terminal_position_stmt + +from unicon.plugins.generic.utils import GenericUtils +from unicon.plugins.generic.service_implementation import BashService as GenericBashService # Simplex Services # ---------------- class Configure(GenericConfigure): - def call_service(self, command=[], reply=Dialog([]), timeout=None, *args, - **kwargs): - super().call_service(command, reply=reply + Dialog([are_you_sure]), - timeout=timeout, *args, **kwargs) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.dialog += Dialog(configure_statement_list) + + class ConfigUtils(GenericUtils): + def truncate_trailing_prompt(self, con_state, + result, + hostname=None, + result_match=None): + host_idx = result.rfind(hostname) + if host_idx != -1: + result = result[:host_idx] + else: + if result_match and len(result_match.last_match.groups()) > 2: + idx = result.rfind(result_match.last_match.group(2)) + if idx: + result = result[:idx] + return result + + self.utils = ConfigUtils() + + def pre_service(self, *args, **kwargs): + + self.acm_configlet = kwargs.pop('acm_configlet', None) + self.syntax_configlet = kwargs.pop('syntax_configlet', None) + self.config_syntax_check = kwargs.pop('config_syntax_check', False) + self.rules = kwargs.pop('rules', False) + self.prompt_recovery = kwargs.get('prompt_recovery', True) + + if self.acm_configlet: + self.connection.state_machine.go_to('acm', self.connection.spawn,context={'acm_configlet': self.acm_configlet}) + self.start_state = 'acm' + self.end_state = 'enable' + + elif self.rules: + if self.connection.connected: + self.connection.state_machine.go_to('rules', self.connection.spawn) + 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}) + self.start_state = 'syntax' + self.end_state = 'syntax' + + else: + super().pre_service(*args, **kwargs) + + def post_service(self, *args, **kwargs): + if self.acm_configlet: + self.connection.state_machine.go_to('enable', self.connection.spawn) + elif self.rules: + self.connection.state_machine.go_to('enable', self.connection.spawn) + else: + super().post_service(*args, **kwargs) class Config(Configure): - def call_service(self, command=[], reply=Dialog([]), timeout=None, *args, - **kwargs): - self.connection.log.warn('**** This service is deprecated. ' + - 'Please use "configure" service ****') - super().call_service(command, reply=reply + Dialog([are_you_sure, - wish_continue]), - timeout=timeout, *args, **kwargs) + pass + + +class ConfigSyntax(Configure): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.start_state = 'syntax' + self.end_state = 'enable' class Execute(GenericExecute): def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) - self.dialog += Dialog([overwrite_previous, - delete_filename, - confirm, - want_continue]) + self.dialog += Dialog(execute_statement_list) + class Traceroute(GenericTraceroute): - def call_service(self, addr, command="traceroute", vrf=None, timeout = None, + def call_service(self, addr, command="traceroute", vrf=None, timeout=None, error_pattern=None, **kwargs): if 'vrf' not in command and vrf: - command = command.replace('traceroute', 'traceroute vrf {}'. - format(str(vrf))) - super().call_service(addr=addr, command=command, - error_pattern=error_pattern, timeout=timeout, **kwargs) + command = command.replace('traceroute', 'traceroute vrf {}'.format(str(vrf))) + super().call_service(addr=addr, command=command, error_pattern=error_pattern, timeout=timeout, **kwargs) + class Ping(GenericPing): def call_service(self, addr, command="", *, vrf=None, **kwargs): - command = command if command else \ - "ping vrf {vrf}".format(vrf=vrf) if vrf else "ping" + command = command if command else "ping vrf {vrf}".format(vrf=vrf) if vrf else "ping" super().call_service(addr=addr, command=command, **kwargs) + +class Copy(GenericCopy): + def call_service(self, reply=Dialog([]), vrf=None, *args, **kwargs): + if vrf is not None: + kwargs['extra_options'] = kwargs.setdefault('extra_options', '') + ' vrf {}'.format(vrf) + super().call_service(reply=reply, *args, **kwargs) + + # HA Services # ----------- class HAConfigure(GenericHAConfigure): - def call_service(self, command=[], reply=Dialog([]), timeout=None, *args, - **kwargs): - super().call_service(command, reply=reply + Dialog([are_you_sure]), - timeout=timeout, *args, **kwargs) + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.dialog += Dialog(configure_statement_list) + + def pre_service(self, *args, **kwargs): + self.acm_configlet = kwargs.pop('acm_configlet', None) + self.syntax_configlet = kwargs.pop('syntax_configlet', None) + self.rules = kwargs.pop('rules', False) + self.prompt_recovery = kwargs.get('prompt_recovery', True) + + if self.acm_configlet: + self.connection.state_machine.go_to('acm', self.connection.spawn,context={'acm_configlet': self.acm_configlet}) + 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}) + self.start_state = 'syntax' + self.end_state = 'syntax' + elif self.rules: + if self.connection.connected: + self.connection.state_machine.go_to('rules', self.connection.spawn) + self.start_state = 'rules' + self.end_state = 'rules' + else: + super().pre_service(*args, **kwargs) + + def post_service(self, *args, **kwargs): + if self.acm_configlet: + self.connection.state_machine.go_to('enable', self.connection.spawn) + elif self.rules: + self.connection.state_machine.go_to('enable', self.connection.spawn) + else: + super().post_service(*args, **kwargs) class HAConfig(HAConfigure): - def call_service(self, command=[], reply=Dialog([]), timeout=None, *args, - **kwargs): - self.connection.log.warn('**** This service is deprecated. ' + - 'Please use "configure" service ****') - super().call_service(command, reply=reply + Dialog([are_you_sure, - wish_continue]), - timeout=timeout, *args, **kwargs) + pass class HAExecute(GenericHAExecute): - def call_service(self, command=[], reply=Dialog([]), timeout=None, *args, - **kwargs): - super().call_service(command, - reply=reply + Dialog([overwrite_previous, - delete_filename, - confirm, - want_continue]), - timeout=timeout, *args, **kwargs) + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.dialog += Dialog(execute_statement_list) class HAReload(GenericHAReload): - # Non-stacked platforms such as ASR and ISR do not use the same - # reload command as the generic implementation (whose reload command - # covers stackable platforms only). - def call_service(self, command=[], reload_command=[], reply=Dialog([]), timeout=None, *args, - **kwargs): + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + + def pre_service(self, *args, **kwargs): + self.prompt_recovery = self.connection.prompt_recovery + if 'prompt_recovery' in kwargs: + self.prompt_recovery = kwargs.get('prompt_recovery') + self.context.pop('boot_prompt_count', None) + sm = self.get_sm() + if sm.current_state != 'rommon': + return super().pre_service(*args, **kwargs) + + # Non-stacked platforms such as ASR and ISR do not use the same + # reload command as the generic implementation (whose reload command + # covers stackable platforms only). + def call_service(self, command=[], reload_command=[], reply=Dialog([]), timeout=None, *args, **kwargs): + sm = self.get_sm() + + self.context["image_to_boot"] = \ + kwargs.get("image_to_boot", kwargs.get('image', '')) + + # boot_cmd is used by the boot_image handler, see statements.py + if sm.current_state == 'rommon' and reload_command: + self.connection.active.context['boot_cmd'] = reload_command + if command: - super().call_service(command or "reload", + super().call_service(command or "reload", reply=reply, timeout=timeout, *args, **kwargs) else: - super().call_service(reload_command=reload_command or "reload", + super().call_service(reload_command=reload_command or "reload", reply=reply, timeout=timeout, *args, **kwargs) + class HASwitchover(GenericHASwitchover): - def call_service(self, command=[], dialog=Dialog([]), timeout=None, *args, + def call_service(self, command=[], reply=Dialog([]), timeout=None, *args, **kwargs): - super().call_service(command, - dialog = dialog + Dialog([confirm, ]), - timeout=timeout, *args, **kwargs) + super().call_service(command, reply=reply + Dialog([confirm]), timeout=timeout, *args, **kwargs) + +class BashService(GenericBashService): -class BashService(BashService): + def pre_service(self, *args, **kwargs): + handle = self.get_handle(kwargs.get('target')) + if kwargs.get('switch'): + handle.context['_switch'] = kwargs.get('switch') + else: + handle.context.pop('_switch', None) + if kwargs.get('rp'): + handle.context['_rp'] = kwargs.get('rp') + else: + handle.context.pop('_rp', None) + if kwargs.get('chassis'): + handle.context['_chassis'] = kwargs.get('chassis') + else: + handle.context.pop('_chassis', None) + if kwargs.get('disable_selinux') is not None: + handle.context['_disable_selinux'] = kwargs.get('disable_selinux') + elif hasattr(self, 'disable_selinux'): + handle.context['_disable_selinux'] = self.disable_selinux + else: + handle.context.pop('_disable_selinux', None) + super().pre_service(*args, **kwargs) - class ContextMgr(BashService.ContextMgr): - def __init__(self, connection, - enable_bash = False, - target='active', - timeout = None): + class ContextMgr(GenericBashService.ContextMgr): + def __init__(self, connection, enable_bash=False, timeout=None, **kwargs): super().__init__(connection=connection, enable_bash=enable_bash, - target = target, - timeout=timeout) + timeout=timeout, + **kwargs) + self.terminal_position_dialog = Dialog([terminal_position_stmt]) def __enter__(self): + + if self.conn.context.get('_disable_selinux'): + try: + self.conn.execute('set platform software selinux permissive') + except SubCommandFailure: + pass + self.conn.log.debug('+++ attaching bash shell +++') # enter shell prompt + self.conn.state_machine.go_to( + 'shell', + self.conn.spawn, + timeout=self.timeout, + context=self.conn.context, + dialog=self.terminal_position_dialog,) - if self.conn.is_ha: - conn = self.conn - if self.target == 'standby': - conn.state_machine = self.conn.standby.state_machine - conn.spawn = self.conn.standby.spawn - elif self.target == 'active': - conn.state_machine = self.conn.active.state_machine - conn.spawn = self.conn.active.spawn - else: - conn = self.conn + for cmd in self.conn.settings.BASH_INIT_COMMANDS: + self.conn.execute( + cmd, timeout=self.timeout) - conn.state_machine.go_to('shell', conn.spawn, - timeout = self.timeout) + return self - for cmd in conn.settings.BASH_INIT_COMMANDS: - conn.execute(cmd, timeout = self.timeout, target=self.target) + def __exit__(self, type, value, traceback): + res = super().__exit__(type, value, traceback) - return self + if self.conn.context.get('_disable_selinux'): + try: + self.conn.execute('set platform software selinux default') + except SubCommandFailure: + pass + + return res + +class ResetStandbyRP(GenericResetStandbyRP): + """ Service to reset the standby rp. + + Arguments: + + command: command to reset standby, default is"redundancy reload peer" + dialog: Dialog which include list of Statements for + additional dialogs prompted by standby reset command, + in-case it is not in the current list. + timeout: Timeout value in sec, Default Value is 500 sec + + Returns: + True on Success, raise SubCommandFailure on failure. + + Example: + .. code-block:: python + + rtr.reset_standby_rp() + # If command is other than 'redundancy reload peer' + rtr.reset_standby_rp(command="command which will reset standby rp", + timeout=600) + + """ + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.prompt_recovery = connection.prompt_recovery + + def call_service(self, command='redundancy reload peer', + reply=Dialog([]), + timeout=None, + *args, + **kwargs): + super().call_service(command=command, + reply=reply, + timeout=timeout, + standby_check='STANDBY HOT', + *args, + **kwargs) + + +class Reload(GenericReload): + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + # Add the grub prompt statement + self.dialog += Dialog([grub_prompt_stmt, boot_from_rommon_stmt]) + + def pre_service(self, *args, **kwargs): + self.prompt_recovery = self.connection.prompt_recovery + if 'prompt_recovery' in kwargs: + self.prompt_recovery = kwargs.get('prompt_recovery') + self.context.pop('boot_prompt_count', None) + sm = self.get_sm() + if sm.current_state != 'rommon': + return super().pre_service(*args, **kwargs) + + def call_service(self, + reload_command='reload', + dialog=Dialog([]), + reply=Dialog([]), + timeout=None, + return_output=False, + reload_creds=None, + grub_boot_image=None, + post_reload_wait_time=None, + *args, **kwargs): + sm = self.get_sm() + + # update the context with the boot_image + self.context.update({'grub_boot_image': grub_boot_image}) + + self.context["image_to_boot"] = \ + kwargs.get("image_to_boot", kwargs.get('image', '')) + + # boot_cmd is used by the boot_image handler, see statements.py + if sm.current_state == 'rommon' and reload_command != 'reload': + self.context['boot_cmd'] = reload_command + + super().call_service( + reload_command=reload_command, + dialog=dialog, + reply=reply, + timeout=timeout, + return_output=return_output, + reload_creds=reload_creds, + post_reload_wait_time=post_reload_wait_time, + *args, **kwargs) + + self.context.pop("image_to_boot", None) + self.context.pop("grub_boot_image", None) + + +class Rommon(GenericExecute): + """ Brings device to the Rommon prompt and executes commands specified + """ + def __init__(self, connection, context, **kwargs): + # Connection object will have all the received details + super().__init__(connection, context, **kwargs) + self.start_state = 'rommon' + self.end_state = 'rommon' + self.service_name = 'rommon' + self.__dict__.update(kwargs) + + def log_service_call(self): + via = self.handle.via + alias = self.handle.alias if hasattr(self.handle, 'alias') and self.handle.alias != 'cli' else None + self.handle.alias + if alias and via: + self.connection.log.info( + "+++ %s with via '%s' and alias '%s': %s +++" % + (self.connection.hostname if + (self.connection.hostname != + self.connection.settings.DEFAULT_LEARNED_HOSTNAME) else "", + via, alias, self.service_name)) + elif via: + self.connection.log.info( + "+++ %s with via '%s': %s +++" % + (self.connection.hostname if + (self.connection.hostname != + self.connection.settings.DEFAULT_LEARNED_HOSTNAME) else "", + via, self.service_name)) + else: + self.connection.log.info( + "+++ %s: %s +++" % + (self.connection.hostname if + (self.connection.hostname != self.connection.settings.DEFAULT_LEARNED_HOSTNAME) else "", + self.service_name)) + + def pre_service(self, *args, **kwargs): + self.timeout = kwargs.get('reload_timeout', 600) + sm = self.get_sm() + con = self.connection + sm.go_to('enable', + con.spawn, + context=self.context) + confreg = kwargs.get('config_register', "0x0") + con.configure('config-register {}'.format(confreg)) + super().pre_service(*args, **kwargs) + + +class HARommon(Rommon): + """ Brings device to the Rommon prompt and executes commands specified + """ + def __init__(self, connection, context, **kwargs): + # Connection object will have all the received details + super().__init__(connection, context, **kwargs) + self.start_state = 'rommon' + self.end_state = 'rommon' + self.service_name = 'rommon' + self.__dict__.update(kwargs) + + def pre_service(self, *args, **kwargs): + con = self.connection + + # call pre_service to reload to rommon + super().pre_service(*args, **kwargs) + + # check connection states + subcon1, subcon2 = list(con._subconnections.values()) + + # Check current state + for subcon in [subcon1, subcon2]: + subcon.sendline() + subcon.state_machine.go_to( + 'any', + subcon.spawn, + context=subcon.context, + prompt_recovery=subcon.prompt_recovery, + timeout=subcon.connection_timeout, + ) + con.log.debug('{} in state: {}'.format(subcon.alias, subcon.state_machine.current_state)) + + + +class Tclsh(Execute): + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.start_state = 'tclsh' + self.end_state = 'tclsh' + 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): + super().__init__(connection, context, **kwargs) + self.context_state = 'maintenance' + self.service_name = 'maintenance' + self.start_state = "enable" + self.end_state = "enable" + self.__dict__.update(kwargs) + + +class Enable(GenericEnable): + + def call_service(self, target=None, command='', *args, **kwargs): + super().call_service(target=target, command=command, *args, **kwargs) + + handle = self.get_handle(target) + spawn = self.get_spawn(target) + timeout = kwargs.get('timeout', None) or handle.settings.ENABLE_TIMEOUT + + # explicit enable + handle.sendline(command or 'enable') + dialog = self.service_dialog(service_dialog=self.dialog) + dialog.process(spawn, timeout=timeout, + context=handle.context, + prompt_recovery=self.prompt_recovery) diff --git a/src/unicon/plugins/iosxe/service_statements.py b/src/unicon/plugins/iosxe/service_statements.py index cc01d33b..05cdfa95 100644 --- a/src/unicon/plugins/iosxe/service_statements.py +++ b/src/unicon/plugins/iosxe/service_statements.py @@ -2,47 +2,56 @@ __author__ = "Myles Dear " - from unicon.eal.dialogs import Statement -from .patterns import IosXEPatterns +from .patterns import IosXEPatterns, FactoryResetPatterns +from unicon.plugins.generic.statements import chatty_term_wait + +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# +# Service handlers +# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++# + + +def send_response(spawn, response=""): + chatty_term_wait(spawn) + spawn.sendline(response) + patterns = IosXEPatterns() -# loop_continue is set to `True` to ensure the dialog does not end up -# prematurely terminating, which can mess up things like executing the -# "write memory" command. + overwrite_previous = Statement(pattern=patterns.overwrite_previous, action='sendline()', loop_continue=True, continue_timer=False) - delete_filename = Statement(pattern=patterns.delete_filename, action='sendline()', loop_continue=True, continue_timer=False) -# loop_continue is set to `True` to ensure the dialog does not end up -# prematurely terminating, which can mess up things like uniclean -# successive file deletion. -confirm = Statement(pattern=patterns.confirm, +confirm = Statement(pattern=patterns.confirm_prompt, action='sendline()', loop_continue=True, continue_timer=False) are_you_sure = Statement(pattern=patterns.are_you_sure, action='sendline(y)', - loop_continue=False, + loop_continue=True, continue_timer=False) +are_you_sure_ywtdt = Statement(pattern=patterns.are_you_sure_ywtdt, + action='sendline(yes)', + loop_continue=True, + continue_timer=False) + wish_continue = Statement(pattern=patterns.wish_continue, action='sendline(yes)', - loop_continue=False, + loop_continue=True, continue_timer=False) want_continue = Statement(pattern=patterns.want_continue, action='sendline(yes)', - loop_continue=False, + loop_continue=True, continue_timer=False) press_enter = Statement(pattern=patterns.press_enter, @@ -50,3 +59,50 @@ loop_continue=True, continue_timer=False) +do_you_want_to = Statement(pattern=patterns.do_you_want_to, + action='sendline(y)', + loop_continue=True, + continue_timer=False) + +proceed_confirm_stmt = Statement(pattern=patterns.proceed_confirm, + action='sendline(yes)', + loop_continue=True, + continue_timer=False) + +macro_prompt = Statement(pattern=patterns.macro_prompt, + loop_continue=False) + + +configure_statement_list = [ + are_you_sure, + wish_continue, + confirm, + want_continue, + are_you_sure_ywtdt, + proceed_confirm_stmt, + macro_prompt +] + +execute_statement_list = [ + overwrite_previous, + delete_filename, + confirm, + want_continue, + do_you_want_to +] + +############################################################################# +# Factory Reset Command Statement +############################################################################# +pat = FactoryResetPatterns() + + +factory_reset_confirm = Statement(pattern=pat.factory_reset_confirm, + action=send_response, args={'response': ''}, + loop_continue=True, + continue_timer=False) + +are_you_sure_confirm = Statement(pattern=pat.are_you_sure_confirm, + action=send_response, args={'response': ''}, + loop_continue=True, + continue_timer=False) \ No newline at end of file diff --git a/src/unicon/plugins/iosxe/settings.py b/src/unicon/plugins/iosxe/settings.py index 301eec98..485d2059 100644 --- a/src/unicon/plugins/iosxe/settings.py +++ b/src/unicon/plugins/iosxe/settings.py @@ -12,11 +12,62 @@ def __init__(self): # A single cycle of retries wasn't enough to recover an iosxe device # just rebooted after a "write erase". self.PROMPT_RECOVERY_RETRIES = 2 + self.PROMPT_RECOVERY_COMMANDS = ['\r', '\x1e', '\x03'] + self.ERROR_PATTERN = [ - r'^%\s*[Ii]nvalid (command|input)', + r'^\s*%\s*[Ii]nvalid (command|input)', r'^%\s*[Ii]ncomplete (command|input)', - r'^%\s*[Aa]mbiguous (command|input)' + r'^%\s*[Aa]mbiguous (command|input)', + r'% Bad IP address or host name', + r'% Unknown command or computer name, or unable to find computer address' + ] + self.CONFIGURE_ERROR_PATTERN = [ + r'^%\s*[Ii]nvalid (command|input|number|address)', + r'routing table \S+ does not exist', + r'^%\s*SR feature is not configured yet, please enable Segment-routing first.', + r'^%\s*\S+ overlaps with \S+', + r'^\S+ / \S+ is an [Ii]nvalid network\.', + r'^%\S+ is linked to a VRF. Enable \S+ on that VRF first.', + r'% VRF \S+ not configured', + r'% Incomplete command.', + r'%CLNS: System ID (\S+) must not change when defining additional area addresses', + r'% Specify remote-as or peer-group commands first', + r'% Policy commands not allowed without an address family', + r'% Color set already. Deconfigure first', + r'Invalid policy name, \S+ does not exist', + r'% Deletion of RD in progress; wait for it to complete', + r'% VLAN \[\d+\] already in use', + r'% VNI \d+ is either already in use or exceeds the maximum allowable VNIs.' ] self.EXECUTE_MATCHED_RETRIES = 1 self.EXECUTE_MATCHED_RETRY_SLEEP = 0.1 + + self.RELOAD_WAIT = 300 + + # wait time for buffer to settle down + self.CONTROLLER_MODE_CHATTY_WAIT_TIME = 5 + + self.CONFIG_LOCK_RETRY_SLEEP = 30 + self.CONFIG_LOCK_RETRIES = 10 + + self.POST_BOOT_TIMEOUT = 300 + self.BOOT_POSTCHECK_INTERVAL = 30 + + self.SERVICE_PROMPT_CONFIG_CMD = 'service prompt config' + self.CONFIG_PROMPT_WAIT = 2 + + self.GUESTSHELL_CONFIG_CMDS = ['iox', 'app-hosting appid guestshell', 'app-vnic management guest-interface 0'] + self.GUESTSHELL_CONFIG_VERIFY_CMDS = ['show iox-service', 'show app-hosting list'] + self.GUESTSHELL_CONFIG_VERIFY_PATTERN = r'guestshell\s+RUNNING' + self.GUESTSHELL_ENABLE_CMDS = 'guestshell enable' + self.GUESTSHELL_ENABLE_VERIFY_CMDS = [] + self.GUESTSHELL_ENABLE_VERIFY_PATTERN = r'' + + # Regex to match the entries on the grub boot screen + self.GRUB_REGEX_PATTERN = r'(?:\x1b\[7m)?\x1b\[\d;3H.*? ' + + self.MAINTENANCE_MODE_WAIT_TIME = 30 # 30 seconds + self.MAINTENANCE_MODE_TIMEOUT = 60*40 # 40 minutes + self.MAINTENANCE_START_COMMAND = 'start maintenance' + self.MAINTENANCE_STOP_COMMAND = 'stop maintenance' diff --git a/src/unicon/plugins/iosxe/stack/__init__.py b/src/unicon/plugins/iosxe/stack/__init__.py new file mode 100644 index 00000000..48d55347 --- /dev/null +++ b/src/unicon/plugins/iosxe/stack/__init__.py @@ -0,0 +1,35 @@ +""" A Stack IOS-XE connection implementation. +""" + +from unicon.plugins.generic import HAServiceList +from unicon.plugins.iosxe import service_implementation as svc +from unicon.bases.routers.connection import BaseStackRpConnection + +from .settings import IosXEStackSettings +from .statemachine import StackIosXEStateMachine +from .connection_provider import StackRpConnectionProvider +from .service_implementation import StackGetRPState, StackSwitchover, StackReload, StackRommon, StackEnable, StackResetStandbyRP + +class StackIosXEServiceList(HAServiceList): + def __init__(self): + super().__init__() + self.ping = svc.Ping + self.config = svc.HAConfig + self.configure = svc.HAConfigure + self.execute = svc.HAExecute + self.reload = StackReload + self.switchover = StackSwitchover + self.get_rp_state = StackGetRPState + self.rommon = StackRommon + self.enable = StackEnable + self.reset_standby_rp = StackResetStandbyRP + + +class IosXEStackRPConnection(BaseStackRpConnection): + os = 'iosxe' + platform = None + chassis_type = 'stack' + subcommand_list = StackIosXEServiceList + state_machine_class = StackIosXEStateMachine + connection_provider_class = StackRpConnectionProvider + settings = IosXEStackSettings() diff --git a/src/unicon/plugins/iosxe/stack/connection_provider.py b/src/unicon/plugins/iosxe/stack/connection_provider.py new file mode 100644 index 00000000..5d78dc5a --- /dev/null +++ b/src/unicon/plugins/iosxe/stack/connection_provider.py @@ -0,0 +1,35 @@ +""" +Authors: + pyATS TEAM (pyats-support@cisco.com, pyats-support-ext@cisco.com) +""" + +from unicon.eal.dialogs import Dialog +from unicon.bases.routers.connection_provider import BaseStackRpConnectionProvider + +from unicon.plugins.generic.statements import connection_statement_list, custom_auth_statements + + +class StackRpConnectionProvider(BaseStackRpConnectionProvider): + """ Implements Stack Connection Provider, + This class overrides the base class with the + additional dialogs and steps required for + connecting to stack device + """ + def __init__(self, *args, **kwargs): + + """ Initializes the base connection provider + """ + super().__init__(*args, **kwargs) + + def get_connection_dialog(self): + """ creates and returns a Dialog to handle all device prompts + appearing during initial connection to the device. + See generic/statements.py for connnection statement lists + """ + con = self.connection + custom_auth_stmt = custom_auth_statements( + self.connection.settings.LOGIN_PROMPT, + self.connection.settings.PASSWORD_PROMPT) + return con.connect_reply + \ + Dialog(custom_auth_stmt + connection_statement_list + if custom_auth_stmt else connection_statement_list) diff --git a/src/unicon/plugins/iosxe/stack/exception.py b/src/unicon/plugins/iosxe/stack/exception.py new file mode 100644 index 00000000..7f837873 --- /dev/null +++ b/src/unicon/plugins/iosxe/stack/exception.py @@ -0,0 +1,3 @@ +class StackException(Exception): + ''' base class ''' + pass diff --git a/src/unicon/plugins/iosxe/stack/patterns.py b/src/unicon/plugins/iosxe/stack/patterns.py new file mode 100644 index 00000000..0575c685 --- /dev/null +++ b/src/unicon/plugins/iosxe/stack/patterns.py @@ -0,0 +1,9 @@ +""" IOS-XE Stack Patterns """ +from unicon.plugins.iosxe.patterns import IosXEPatterns + + +class StackIosXEPatterns(IosXEPatterns): + def __init__(self): + super().__init__() + self.rommon_prompt = r'(.*)switch:\s?$' + self.tcpdump = ".*listening on lfts.*$" \ No newline at end of file diff --git a/src/unicon/plugins/iosxe/stack/service_implementation.py b/src/unicon/plugins/iosxe/stack/service_implementation.py new file mode 100644 index 00000000..52ef3557 --- /dev/null +++ b/src/unicon/plugins/iosxe/stack/service_implementation.py @@ -0,0 +1,714 @@ +""" Stack based IOS-XE service implementations. """ +import io +import logging +from time import sleep, time +from collections import namedtuple +from datetime import datetime, timedelta +import re +from concurrent.futures import ThreadPoolExecutor, wait as wait_futures, ALL_COMPLETED + +from unicon.eal.dialogs import Dialog +from unicon.core.errors import SubCommandFailure +from unicon.bases.routers.services import BaseService +from unicon.logs import UniconStreamHandler, UNICON_LOG_FORMAT + +from .utils import StackUtils +from unicon.plugins.generic.statements import custom_auth_statements, buffer_settled +from unicon.plugins.generic.service_statements import standby_reset_rp_statement_list +from .service_statements import (switch_prompt, + stack_reload_stmt_list, + stack_reload_stmt_list_1, + stack_switchover_stmt_list, stack_factory_reset_stmt_list) +from unicon.plugins.generic.service_implementation import Enable as GenericEnable, Execute as GenericExecute + +utils = StackUtils() + +class StackGetRPState(BaseService): + """ Get Rp state + + Service to get the redundancy state of the device rp. + + Arguments: + target: Service target, by default active + + Returns: + Expected return values are ACTIVE, STANDBY, MEMBER + raise SubCommandFailure on failure. + + Example: + .. code-block:: python + + rtr.get_rp_state() + rtr.get_rp_state(target='standby') + """ + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.start_state = 'enable' + self.end_state = 'enable' + self.timeout = connection.settings.EXEC_TIMEOUT + self.__dict__.update(kwargs) + + def call_service(self, + target='active', + timeout=None, + utils=utils, + *args, + **kwargs): + """send the command on the right rp and return the output""" + handle = self.get_handle(target) + timeout = timeout or self.timeout + try: + info_dict = utils.get_redundancy_details(handle, timeout=timeout) + except Exception as err: + raise SubCommandFailure("get_rp_state failed", err) from err + + self.result = info_dict.get(str(handle.member_id)) + + def get_service_result(self): + if 'role' in self.result: + return self.result['role'].upper() + else: + return "None" + + +class StackSwitchover(BaseService): + """ Get Rp state + + Service to get the redundancy state of the device rp. + + Arguments: + target: Service target, by default active + + Returns: + Expected return values are ACTIVE, STANDBY, MEMBER + raise SubCommandFailure on failure. + + Example: + .. code-block:: python + + rtr.get_rp_state() + rtr.get_rp_state(target='standby') + """ + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.start_state = 'enable' + self.end_state = 'enable' + self.timeout = connection.settings.STACK_SWITCHOVER_TIMEOUT + self.command = "redundancy force-switchover" + self.dialog = Dialog(stack_switchover_stmt_list) + self.__dict__.update(kwargs) + + def call_service(self, command=None, + reply=Dialog([]), + timeout=None, + *args, **kwargs): + + switchover_cmd = command or self.command + timeout = timeout or self.timeout + conn = self.connection.active + + expected_active_sw = self.connection.standby.member_id + dialog = self.dialog + + if reply: + dialog = reply + self.dialog + + # added connection dialog in case switchover ask for username/password + connect_dialog = self.connection.connection_provider.get_connection_dialog() + dialog += connect_dialog + + conn.log.info('Processing on active rp %s-%s' % (conn.hostname, conn.alias)) + conn.sendline(switchover_cmd) + try: + match_object = dialog.process(conn.spawn, timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=conn.context) + except Exception as e: + raise SubCommandFailure('Error during switchover ', e) from e + + # try boot up original active rp with current active system + # image, if it moved to rommon state. + if 'state' in conn.context and conn.context.state == 'rommon': + try: + conn.state_machine.detect_state(conn.spawn, context=conn.context) + conn.state_machine.go_to('enable', conn.spawn, timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=conn.context, dialog=Dialog([switch_prompt])) + except Exception as e: + self.connection.log.warning('Fail to bring up original active rp from rommon state.', e) + finally: + conn.context.pop('state') + + # To ensure the stack is ready to accept the login + self.connection.log.info('Sleeping for %s secs.' % \ + self.connection.settings.POST_SWITCHOVER_SLEEP) + sleep(self.connection.settings.POST_SWITCHOVER_SLEEP) + + # check all members are ready + conn.state_machine.detect_state(conn.spawn, context=conn.context) + + interval = self.connection.settings.SWITCHOVER_POSTCHECK_INTERVAL + if utils.is_all_member_ready(conn, timeout=timeout, interval=interval): + self.connection.log.info('All members are ready.') + else: + self.connection.log.info('Timeout in %s secs. ' + 'Not all members are in Ready state.' % timeout) + self.result = False + return + + self.connection.log.info('Disconnecting and reconnecting') + self.connection.disconnect() + self.connection.connect() + + self.connection.log.info('Verifying active and standby switch State.') + if self.connection.active.member_id == expected_active_sw: + self.connection.log.info('Switchover successful') + self.result = True + else: + self.connection.log.info('Switchover failed') + self.result = False + + +class StackReload(BaseService): + """ Service to reload the stack device. + + Arguments: + reload_command: reload command to be used. default "redundancy reload shelf" + reply: Additional Dialog( i.e patterns) to be handled + timeout: Timeout value in sec, Default Value is 900 sec + image_to_boot: image to boot from rommon state + return_output: if True, return namedtuple with result and reload output + + Returns: + console True on Success, raises SubCommandFailure on failure. + + Example: + .. code-block:: python + + rtr.reload() + # If reload command is other than 'redundancy reload shelf' + rtr.reload(reload_command="reload location all", timeout=700) + """ + + def __init__(self, connection, context, *args, **kwargs): + super().__init__(connection, context, *args, **kwargs) + self.start_state = 'enable' + self.end_state = 'enable' + self.timeout = connection.settings.STACK_RELOAD_TIMEOUT + self.reload_command = "redundancy reload shelf" + self.log_buffer = io.StringIO() + self.dialog = Dialog(stack_reload_stmt_list) + + def call_service(self, + reload_command=None, + reply=Dialog([]), + timeout=None, + image_to_boot=None, + return_output=False, + member=None, + error_pattern = None, + append_error_pattern= None, + post_reload_wait_time=None, + *args, + **kwargs): + + self.result = False + if member: + reload_command = f'reload slot {member}' + + reload_cmd = reload_command or self.reload_command + timeout = timeout or self.timeout + conn = self.connection.active + + if error_pattern is None: + self.error_pattern = self.connection.settings.ERROR_PATTERN + else: + self.error_pattern = error_pattern + + if post_reload_wait_time is None: + self.post_reload_wait_time = self.connection.settings.POST_RELOAD_WAIT + else: + self.post_reload_wait_time = post_reload_wait_time + + if not isinstance(self.error_pattern, list): + raise ValueError('error_pattern should be a list') + if append_error_pattern: + if not isinstance(append_error_pattern, list): + raise ValueError('append_error_pattern should be a list') + self.error_pattern += append_error_pattern + + # Connecting to the log handler to capture the buffer output + lb = UniconStreamHandler(self.log_buffer) + lb.setFormatter(logging.Formatter(fmt=UNICON_LOG_FORMAT)) + self.connection.log.addHandler(lb) + + # logging the output to subconnections + for subcon in self.connection.subconnections: + subcon.log.addHandler(lb) + + # Clear log buffer + self.log_buffer.seek(0) + self.log_buffer.truncate() + + # update all subconnection context with image_to_boot + if image_to_boot: + for subconn in self.connection.subconnections: + subconn.context.image_to_boot = image_to_boot + + # Update the reload command to use the image_to_boot + self.context["image_to_boot"] = image_to_boot + reload_cmd = f"boot {image_to_boot.strip()}" + + reload_dialog = self.dialog + if reply: + reload_dialog = reply + reload_dialog + + custom_auth_stmt = custom_auth_statements(conn.settings.LOGIN_PROMPT, + conn.settings.PASSWORD_PROMPT) + if custom_auth_stmt: + reload_dialog += Dialog(custom_auth_stmt) + + reload_dialog += Dialog([switch_prompt]) + + conn.context['post_reload_wait_time'] = timedelta(seconds= self.post_reload_wait_time) + + conn.log.info('Processing on active rp %s-%s with timeout %s' % (conn.hostname, conn.alias, timeout)) + conn.sendline(reload_cmd) + + conn_list = self.connection.subconnections + + reload_cmd_output = None + + reload_dialog2 = Dialog(stack_reload_stmt_list_1) + + def task(con): + + # The purpose of this dialog is to manage the initial interaction + # with the device during the reload process. The dialog handles + # the startup sequence until the device either displays the ROMMON + # prompt or "All switches in the stack have been discovered. + # Accelerating discovery" message indicating readiness. At this + # point, the dialog exits to proceed with subsequent operations. + reload_cmd_output = reload_dialog2.process(con.spawn, + timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=con.context) + + # A sendline command is necessary when the device is configured + # for manual boot or when device is in enable/disable state + # during the member reload to ensure the subsequent dialog can + # proceed seamlessly. + con.sendline() + + # The dialog process outlined below manages the + # "Press RETURN to get started" prompt for each subconnection. + # This ensures that the reload process is completed successfully + # across all connections. + reload_cmd_output2 = reload_dialog.process(con.spawn, + timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=con.context) + + self.result = reload_cmd_output.match_output + reload_cmd_output2.match_output + self.get_service_result() + + futures = [] + executor = ThreadPoolExecutor(max_workers=len(conn_list)) + + for con in conn_list: + futures.append(executor.submit(task, con)) + + # Log the output from threading + future_results = wait_futures(futures, timeout=timeout, return_when=ALL_COMPLETED) + + # Splitting it to done and not done specifically + # because future result is a tuple + + # Logs the completed output + done = list(future_results.done) + + # Logs the error traceback + not_done = list(future_results.not_done) + + for future in done + not_done: + try: + result = future.result() + conn.log.info(f"Reload result: {result}") + + except Exception as e: + raise SubCommandFailure('Error during reload', e) from e + + if 'state' in conn.context and conn.context.state == 'rommon': + conn.log.info(f"Waiting {self.connection.settings.STACK_ROMMON_SLEEP} seconds for all peers to come to boot state ") + # If manual boot enabled wait for all peers to come to boot state. + sleep(self.connection.settings.STACK_ROMMON_SLEEP) + + conn.context.pop('state') + + def boot(con): + + # send boot command for each subconnection + utils.send_boot_cmd(con, timeout, self.prompt_recovery, reply) + + self.connection.log.info('Processing on rp %s-%s' % (con.hostname, con.alias)) + con.context['post_reload_timeout'] = timedelta(seconds= self.post_reload_wait_time) + # process boot up for each subconnection + utils.boot_process(con, timeout, self.prompt_recovery, reload_dialog) + + futures = [] + executor = ThreadPoolExecutor(max_workers=len(conn_list)) + + for con in conn_list: + futures.append(executor.submit(boot, con)) + + # Log the output from threading + future_results = wait_futures(futures, timeout=timeout, return_when=ALL_COMPLETED) + + # Splitting it to done and not done specifically + # because future result is a tuple + + # Logs the completed output + done = list(future_results.done) + + # Logs the error traceback + not_done = list(future_results.not_done) + + for future in done + not_done: + try: + result = future.result() + conn.log.info(f"Reload result: {result}") + + except Exception as e: + raise SubCommandFailure('Error during reload', e) from e + else: + try: + conn.log.info("Bring device to any state") + # bring device to enable mode + conn.state_machine.go_to('any', conn.spawn, timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=conn.context) + conn.state_machine.go_to('enable', conn.spawn, timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=conn.context) + except Exception as e: + raise SubCommandFailure('Failed to bring device to disable mode.', e) from e + # check active and standby rp is ready + self.connection.log.info('Wait for Standby RP to be ready.') + interval = self.connection.settings.RELOAD_POSTCHECK_INTERVAL + if utils.is_active_standby_ready(conn, timeout=timeout, interval=interval): + self.connection.log.info('Active and Standby RPs are ready.') + else: + self.connection.log.info('Timeout in %s secs. ' + 'Standby RP is not in Ready state. Reload failed' % timeout) + self.result = False + return + + if member: + if utils.is_all_member_ready(conn, timeout=timeout, interval=interval): + self.connection.log.info('All Members are ready.') + else: + self.connection.log.info(f'Timeout in {timeout} secs. ' + f'Member{member} is not in Ready state. Reload failed') + self.result = False + return + + self.connection.log.info('Sleeping for %s secs.' % \ + self.connection.settings.STACK_POST_RELOAD_SLEEP) + sleep(self.connection.settings.STACK_POST_RELOAD_SLEEP) + + self.connection.log.info('Disconnecting and reconnecting') + self.connection.disconnect() + self.connection.connect() + + self.connection.log.info("+++ Reload Completed Successfully +++") + + # Read the log buffer + self.log_buffer.seek(0) + reload_output = self.log_buffer.read() + # clear buffer + self.log_buffer.truncate() + + # Remove the handler + self.connection.log.removeHandler(lb) + for subcon in self.connection.subconnections: + subcon.log.removeHandler(lb) + + self.result = True + + if return_output: + Result = namedtuple('Result', ['result', 'output']) + self.result = Result(self.result, reload_output.replace(reload_cmd, '', 1)) + +class StackRommon(GenericExecute): + """ Brings device to the Rommon prompt and executes commands specified + """ + def __init__(self, connection, context, **kwargs): + # Connection object will have all the received details + super().__init__(connection, context, **kwargs) + self.start_state = 'rommon' + self.end_state = 'rommon' + self.service_name = 'rommon' + self.dialog = Dialog(stack_reload_stmt_list) + self.timeout = 200 + self.__dict__.update(kwargs) + + def pre_service(self, reload_command=None, timeout=None, *args, **kwargs): + con = self.connection + sm = self.get_sm() + con = self.connection + sm.go_to('enable', + con.spawn, + context=self.context) + boot_info = con.execute('show boot') + m = re.search(r'Enable Break = (yes|no)', boot_info) + if m: + break_enabled = m.group(1) + if break_enabled == 'no': + con.configure('boot enable-break') + else: + raise SubCommandFailure('Could not determine if break is enabled, cannot transition to rommon') + + if reload_command: + reload_dialog = self.dialog + reload_dialog += Dialog([switch_prompt] + stack_factory_reset_stmt_list) + timeout = timeout or self.timeout + con.sendline(reload_command) + try: + reload_cmd_output = reload_dialog.process(con.spawn, + timeout=timeout, + prompt_recovery=con.prompt_recovery, + context=con.context) + self.result=reload_cmd_output.match_output + self.get_service_result() + except Exception as e: + raise SubCommandFailure('Error during reload', e) from e + sleep(self.connection.settings.STACK_ROMMON_SLEEP) + + for subconn in con._subconnections.values(): + subconn.sendline() + subconn.state_machine.go_to( + 'any', + subconn.spawn, + context=subconn.context, + prompt_recovery=subconn.prompt_recovery, + timeout=subconn.settings.STACK_SWITCHOVER_TIMEOUT, + ) + self.connection.log.debug('{} in state: {}'.format(subconn.alias, subconn.state_machine.current_state)) + + super().pre_service(*args, **kwargs) + + # send boot command for each subconnection + for subconn in con._subconnections.values(): + subconn.sendline() + subconn.state_machine.go_to( + 'any', + subconn.spawn, + context=subconn.context, + prompt_recovery=subconn.prompt_recovery, + timeout=subconn.connection_timeout, + ) + self.connection.log.debug('{} in state: {}'.format(subconn.alias, subconn.state_machine.current_state)) + + +class StackEnable(GenericEnable): + """ Brings device to enable + + Service to change the device mode to enable from any state. + Brings the standby handle to enable state, if standby is passed as input. + + Arguments: + target= Target connection, Defaults to active + + Returns: + True on Success, raise SubCommandFailure on failure + + Example: + .. code-block:: python + + rtr.enable() + rtr.enable(target='standby') + """ + + def __init__(self, connection, context, **kwargs): + # Connection object will have all the received details + super().__init__(connection, context, **kwargs) + + def pre_service(self, *args, **kwargs): + super().pre_service(*args, **kwargs) + + def call_service(self, target=None, command='', *args, **kwargs): + if target is not None: + super().call_service(target, command, *args, **kwargs) + else: + subconnections = self.connection._subconnections + timeout = self.connection.settings.STACK_BOOT_TIMEOUT + for subconn in subconnections.values(): + subconn.sendline() + subconn.state_machine.go_to( + 'any', + subconn.spawn, + context=subconn.context, + prompt_recovery=subconn.prompt_recovery, + timeout=subconn.connection_timeout, + ) + + for subconn_name, subconn in subconnections.items(): + if subconn.state_machine.current_state != 'enable': + if kwargs.get('timeout', None) is None and subconn.state_machine.current_state == 'rommon': + kwargs['timeout'] = timeout + super().call_service(target=subconn_name, command=command, *args, **kwargs) + + self.result = True + +class StackResetStandbyRP(BaseService): + """ Service to reset the standby rp. + + Arguments: + + command: command to reset standby, default is"redundancy reload peer" + reply: Dialog which include list of Statements for + additional dialogs prompted by standby reset command, + in-case it is not in the current list. + timeout: Timeout value in sec, Default Value is 500 sec + delay_before_check: Delay in secs before checking stack readiness. Default is 20 secs + + Returns: + True on Success, raise SubCommandFailure on failure. + + Example: + .. code-block:: python + + rtr.reset_standby_rp() + # If command is other than 'redundancy reload peer' + rtr.reset_standby_rp(command="command which will reset standby rp", + timeout=600) + + """ + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.start_state = 'enable' + self.end_state = 'enable' + self.timeout = connection.settings.HA_RELOAD_TIMEOUT + self.dialog = Dialog(standby_reset_rp_statement_list) + self.__dict__.update(kwargs) + + def pre_service(self, *args, **kwargs): + self.prompt_recovery = kwargs.get('prompt_recovery', False) + if self.connection.is_connected: + return + elif self.connection.reconnect: + self.connection.connect() + else: + raise ConnectionError("Connection is not established to device") + state_machine = self.connection.active.state_machine + state_machine.go_to(self.start_state, + self.connection.active.spawn, + context=self.connection.context) + + def post_service(self, *args, **kwargs): + state_machine = self.connection.active.state_machine + state_machine.go_to(self.end_state, + self.connection.active.spawn, + context=self.connection.context) + + def call_service(self, command='redundancy reload peer', # noqa: C901 + reply=Dialog([]), + timeout=None, + delay_before_check=20, + *args, + **kwargs): + # create an alias for connection. + con = self.connection + timeout = timeout or self.timeout + # resetting the standby rp for + con.log.debug("+++ Issuing reset on %s with " + "reset_command %s and timeout is %s +++" + % (con.hostname, command, timeout)) + + # Check is it possible to reset the standby? + rp_state = con.get_rp_state(target='standby', timeout=100) + + if re.search('DISABLED', rp_state): + raise SubCommandFailure("No Standby found") + + if 'standby_check' in kwargs and not re.search(kwargs['standby_check'], rp_state): + raise SubCommandFailure("Standby found but not in the expected state") + + dialog = self.service_dialog(handle=con.active, + service_dialog=self.dialog+reply) + + # Issue standby reset command + con.active.spawn.sendline(command) + try: + dialog.process(con.active.spawn, + timeout=30, + context=con.active.context) + except TimeoutError: + pass + except SubCommandFailure as err: + raise SubCommandFailure("Failed to reset standby rp %s" % str(err)) from err + + con.log.info(f'Sleep {delay_before_check} seconds before checking standby readiness') + sleep(delay_before_check) + # get current time before checking stack readiness + start_time = time() + if not utils.is_all_member_ready(con, timeout=timeout): + self.result = False + else: + # make sure standby reload/reset has been done + # get current time + end_time = time() + # calculate the time taken to reload the standby + time_taken = end_time - start_time + con.log.info("Time taken to reload Standby RP: %s seconds" % time_taken) + if time_taken < 60: + raise SubCommandFailure("Reload time is too short. Standby RP reload/reset did not happen") + + # check mac address of 0000.0000.0000 which is the temporary value before standby is really ready + con.log.info('Make sure no invalid mac address 0000.0000.0000') + + def _check_invalid_mac(con): + ''' Check if there is any invalid mac address 0000.0000.0000 + Return True if no invalid mac address found + ''' + parsed = utils.get_redundancy_details(con) + for sw in parsed: + if parsed[sw]['mac'] == '0000.0000.0000': + return True + return False + + from genie.utils.timeout import Timeout + exec_timeout = Timeout(timeout, 15) + found_invalid_mac = False + while exec_timeout.iterate(): + con.log.info('Make sure no invalid mac address 0000.0000.0000') + if not _check_invalid_mac(con): + con.log.info('Did not find invalid mac as 0000.0000.0000') + found_invalid_mac = False + break + else: + con.log.warning('Found 0000.0000.0000 mac address') + found_invalid_mac = True + exec_timeout.sleep() + continue + else: + if found_invalid_mac: + raise SubCommandFailure('Found 0000.0000.0000 mac address. Stack is not really ready') + else: + con.log.info('Did not find invalid mac as 0000.0000.0000. Stack is ready') + + con.log.info("Successfully reloaded Standby RP") + con.log.info("Reconnecting to the device, to make sure console is ready") + + try: + con.disconnect() + con.connect() + except Exception as err: + raise SubCommandFailure("Failed to reconnect to the device: %s" % str(err)) from err + + con.log.info("Successfully reloaded Standby RP") + + self.result = True diff --git a/src/unicon/plugins/iosxe/stack/service_patterns.py b/src/unicon/plugins/iosxe/stack/service_patterns.py new file mode 100644 index 00000000..d41e4251 --- /dev/null +++ b/src/unicon/plugins/iosxe/stack/service_patterns.py @@ -0,0 +1,29 @@ +""" IOS-XE Stack Service Patterns """ +from unicon.plugins.generic.service_patterns import SwitchoverPatterns, ReloadPatterns +from unicon.plugins.iosxe.patterns import IosXEPatterns + +class StackIosXESwitchoverPatterns(SwitchoverPatterns): + def __init__(self): + super().__init__() + self.save_config = r'^.*System configuration has been modified. Save\? \[yes\/no\]' + self.useracess = r'^.*User Access Verification' + self.cisco_commit_changes_prompt = r'^(.*)Uncommitted changes found.*' + self.terminal_state = r'.* Terminal state reached for \(SSO\).*' + self.gen_rsh_key = r'.* Generating 1024 bit RSA keys .*' + self.auto_provision = r'^.*Abort( Power On)? Auto Provisioning .*:' + self.secure_passwd_std = r'^.*Do you want to enforce secure password standard(\?)? \(yes\/no\)( \[[yn]\])?\: ?' + self.switchover_fail5 = r'Failed to switchover|Switchover aborted' + self.press_return = r'Press RETURN to get started.*' + self.enable_prompt = IosXEPatterns().enable_prompt + self.disable_prompt = IosXEPatterns().disable_prompt + self.rommon_prompt = r'(.*)switch:\s?$' + self.fastreload_iosxeswitch = r'^.*Proceed with fast reload\? \[confirm\]' + + +class StackIosXEReloadPatterns(ReloadPatterns): + def __init__(self): + super().__init__() + self.reload_entire_shelf = r'^.*?Reload the entire shelf \[confirm\]' + self.reload_fast = r'^.*Proceed with reload fast\? \[confirm\]' + self.accelarating_discovery = r'^.*All switches in the stack have been discovered. Accelerating discovery' + self.proceed_prompt = r'^(.*?)Do you want to proceed\? \[y/n\]\s*$' \ No newline at end of file diff --git a/src/unicon/plugins/iosxe/stack/service_statements.py b/src/unicon/plugins/iosxe/stack/service_statements.py new file mode 100644 index 00000000..f5d6f8c6 --- /dev/null +++ b/src/unicon/plugins/iosxe/stack/service_statements.py @@ -0,0 +1,172 @@ +""" Generic IOS-XE Stack Service Statements """ +from unicon.eal.dialogs import Statement + +from unicon.plugins.generic.service_statements import (reload_statement_list, + save_env, + reload_confirm_ios, + reload_confirm_iosxe, + reload_entire_shelf, + reload_this_shelf, + send_response) + +from unicon.plugins.iosxe.service_statements import (factory_reset_confirm, + are_you_sure_confirm) +from .service_patterns import (StackIosXESwitchoverPatterns, + StackIosXEReloadPatterns) + + +def update_curr_state(spawn, context, state): + context['state'] = state + + +def switchover_failed(spawn, context): + context['switchover_failed'] = True + + +def boot_from_rommon(sm, spawn, context): + cmd = "boot {}".format(context['image_to_boot']) \ + if "image_to_boot" in context else "boot" + spawn.sendline(cmd) + + +def send_boot_cmd(spawn, context): + cmd = "boot {}".format(context['image_to_boot']) \ + if "image_to_boot" in context else "boot" + spawn.sendline(cmd) + + +# switchover service statements +switchover_pat = StackIosXESwitchoverPatterns() + +save_config = Statement(pattern=switchover_pat.save_config, + action='sendline(yes)', + loop_continue=True, continue_timer=False) + +proceed_sw = Statement(pattern=switchover_pat.switchover_proceed, + action='sendline()', + loop_continue=True, continue_timer=False) + +commit_changes = Statement(pattern=switchover_pat.cisco_commit_changes_prompt, + action='sendline(yes)', + loop_continue=True, continue_timer=False) + +term_state = Statement(pattern=switchover_pat.terminal_state, + action='sendline(\r)', + loop_continue=True, continue_timer=False) + +gen_rsh_key = Statement(pattern=switchover_pat.gen_rsh_key, + action='sendline()', + loop_continue=True, continue_timer=False) + +auto_pro = Statement(pattern=switchover_pat.auto_provision, + action='sendline(yes)', + loop_continue=True, continue_timer=False) + +secure_passwd = Statement(pattern=switchover_pat.secure_passwd_std, + action='sendline(no)', + loop_continue=True, continue_timer=False) + +build_config = Statement(pattern=switchover_pat.build_config, + action=None, + loop_continue=True, continue_timer=False) + +sw_init = Statement(pattern=switchover_pat.switchover_init, + action=None, + loop_continue=True, + continue_timer=False) + +user_acc = Statement(pattern=switchover_pat.useracess, + action=None, + args=None, + loop_continue=True, + continue_timer=False) + +switch_prompt = Statement(pattern=switchover_pat.rommon_prompt, + action=update_curr_state, + args={'state': 'rommon'}, + loop_continue=False, + continue_timer=False) +fastreload_iosxeswitch = Statement(pattern=switchover_pat.fastreload_iosxeswitch, + action='sendline()', + loop_continue=True, continue_timer=False) + +en_state = Statement(pattern=switchover_pat.enable_prompt, + action=update_curr_state, + args={'state': 'enable'}, + loop_continue=False, + continue_timer=False) + +dis_state = Statement(pattern=switchover_pat.disable_prompt, + action=update_curr_state, + args={'state': 'disable'}, + loop_continue=False, + continue_timer=False) + +found_return = Statement(pattern=switchover_pat.press_return, + args=None, + loop_continue=False, + continue_timer=False) + +switchover_fail_pattern = '|'.join([switchover_pat.switchover_fail1, + switchover_pat.switchover_fail2, + switchover_pat.switchover_fail3, + switchover_pat.switchover_fail4, + switchover_pat.switchover_fail5]) + +switchover_fail = Statement(pattern=switchover_fail_pattern, + action=switchover_failed, args=None, + loop_continue=False, continue_timer=False) + +stack_switchover_stmt_list = [save_config, proceed_sw, commit_changes, + term_state, gen_rsh_key, auto_pro, secure_passwd, + build_config, sw_init, user_acc, switch_prompt, + found_return, switchover_fail, dis_state] +# reload service statements +reload_pat = StackIosXEReloadPatterns() + +reload_shelf = Statement(pattern=reload_pat.reload_entire_shelf, + action='sendline()', + loop_continue=True, + continue_timer=False) + +reload_fast = Statement(pattern=reload_pat.reload_fast, + action='sendline()', + loop_continue=True, + continue_timer=False) + +accelarating_discovery = Statement(pattern=reload_pat.accelarating_discovery, + action=send_response, + args=None, + loop_continue=False, + continue_timer=False) + +proceed_prompt_stmt = Statement(pattern=reload_pat.proceed_prompt, + action='sendline(y)', + args=None, + loop_continue=True, + continue_timer=False) + +stack_reload_stmt_list_1 = [save_env, reload_confirm_ios, reload_confirm_iosxe, + reload_entire_shelf, reload_this_shelf, + # Below statements have loop_continue=False + # enable and disable state is needed by dialog + # processor during member reload to process the + # device state during reload + en_state, dis_state, + switch_prompt, + accelarating_discovery, fastreload_iosxeswitch, + proceed_prompt_stmt] + +stack_reload_stmt_list = list(reload_statement_list) + +# The enable and disable states are needed when using `reload slot N` +stack_reload_stmt_list.extend([en_state, dis_state]) +stack_reload_stmt_list.insert(0, reload_shelf) +stack_reload_stmt_list.insert(0, reload_fast) + + +stack_factory_reset_stmt_list = [factory_reset_confirm, are_you_sure_confirm] + +send_boot = Statement(pattern=switchover_pat.rommon_prompt, + action=send_boot_cmd, loop_continue=False, + continue_timer=False) diff --git a/src/unicon/plugins/iosxe/stack/settings.py b/src/unicon/plugins/iosxe/stack/settings.py new file mode 100644 index 00000000..403bda71 --- /dev/null +++ b/src/unicon/plugins/iosxe/stack/settings.py @@ -0,0 +1,30 @@ +""" Stack IOS-XE Settings. """ + +from unicon.plugins.iosxe.settings import IosXESettings + +class IosXEStackSettings(IosXESettings): + + def __init__(self): + super().__init__() + + # Switchover service timeout + self.STACK_SWITCHOVER_TIMEOUT = 600 + # Switchover postcheck interval + self.SWITCHOVER_POSTCHECK_INTERVAL = 30 + self.POST_SWITCHOVER_SLEEP = 90 + + # Secs to sleep before reconnect device + self.STACK_POST_RELOAD_SLEEP = 30 + # Secs to sleep before booting device + self.STACK_ROMMON_SLEEP = 20 + # Stack reload timeout + self.STACK_RELOAD_TIMEOUT = 900 + # Reload postcheck interval + self.RELOAD_POSTCHECK_INTERVAL = 30 + # Timeout for boot + self.STACK_BOOT_TIMEOUT = 1000 + + self.CONFIGURE_ALLOW_STATE_CHANGE = True + + # Secs to sleep after booting the device + self.STACK_ENABLE_SLEEP = 100 \ No newline at end of file diff --git a/src/unicon/plugins/iosxe/stack/statemachine.py b/src/unicon/plugins/iosxe/stack/statemachine.py new file mode 100644 index 00000000..32a3e923 --- /dev/null +++ b/src/unicon/plugins/iosxe/stack/statemachine.py @@ -0,0 +1,28 @@ +""" Stack IOS-XE state machine """ +from unicon.plugins.iosxe.statemachine import IosXESingleRpStateMachine +from unicon.plugins.generic.statements import connection_statement_list +from unicon.plugins.generic.service_statements import reload_statement_list +from .patterns import StackIosXEPatterns +from unicon.statemachine import State, Path +from unicon.eal.dialogs import Dialog +from .service_statements import boot_from_rommon + +patterns = StackIosXEPatterns() + + +class StackIosXEStateMachine(IosXESingleRpStateMachine): + def create(self): + super().create() + + self.remove_path('enable', 'rommon') + self.remove_path('rommon', 'disable') + self.remove_state('rommon') + + rommon = State('rommon', patterns.rommon_prompt) + enable_to_rommon = Path(self.get_state('enable'), rommon, 'reload', + Dialog(reload_statement_list)) + rommon_to_disable = Path(rommon, self.get_state('disable'), boot_from_rommon, + Dialog(connection_statement_list)) + self.add_state(rommon) + self.add_path(enable_to_rommon) + self.add_path(rommon_to_disable) diff --git a/src/unicon/plugins/iosxe/stack/utils.py b/src/unicon/plugins/iosxe/stack/utils.py new file mode 100644 index 00000000..3c935cf9 --- /dev/null +++ b/src/unicon/plugins/iosxe/stack/utils.py @@ -0,0 +1,206 @@ +""" Stack utilities. """ + +import re +import logging +from time import sleep, time + +from unicon.eal.dialogs import Dialog +from unicon.utils import Utils, AttributeDict + +from .service_statements import send_boot + +logger = logging.getLogger(__name__) + + +class StackUtils(Utils): + + def get_redundancy_details(self, connection, timeout=None): + """ Get redundancy details from stack device + + Args: + connection (`obj`): connection object + timeout (`int`): execute timeout + Returns: + redundancy_details (`dict`): redundancy details of all peers + eg: + {'1': {'mac': 'bcc4.9346.7880', + 'role': 'Member', + 'state': 'Ready', + 'sw_num': '1'}, + '2': {'mac': 'bcc4.9346.9180', + 'role': 'Standby', + 'state': 'Ready', + 'sw_num': '2'}, + '3': {'mac': 'bcc4.9346.7280', + 'role': 'Active', + 'state': 'Ready', + 'sw_num': '3'}} + """ + timeout = timeout or connection.settings.EXEC_TIMEOUT + redundancy_details = AttributeDict() + + # 1 Member bcc4.9346.7880 1 V01 Ready + # *2 Active bcc4.9346.9180 3 V04 Ready + # 4 Standby d8b1.9009.bf80 1 V01 HA sync in progress + p = re.compile(r'^(\*)?(?P\d+)\s+(?PMember|Active|Standby)\s+' + r'(?P[\w\.]+)\s+\d+\s+\w+\s+(?P[\S\s]+)$') + + output = connection.execute("show switch", timeout=timeout) + + for line in output.splitlines(): + m = p.search(line.strip()) + if m: + group = m.groupdict() + redundancy_details.update({group['sw_num']: group}) + + return redundancy_details + + + def send_boot_cmd(self, connection, timeout, prompt_recovery, dialog=Dialog([])): + """ Send the boot command when device come to Rommon mode + + Args: + connection (`obj`): connection object + timeout (`int`): execute timeout + prompt_recovery (`bool`): prompt_recovery flag + dialog (`Dialog`): dialog to process + Returns: + None + """ + connection.spawn.sendline() + dialog = dialog + Dialog([send_boot]) + dialog.process(connection.spawn, timeout=timeout, + prompt_recovery=prompt_recovery, + context=connection.context) + + + def boot_process(self, connection, timeout, prompt_recovery, dialog=Dialog([])): + """ Boot up the device and bring it to disable mode + + Args: + connection (`obj`): connection object + timeout (`int`): execute timeout + prompt_recovery (`bool`): prompt_recovery flag + dialog (`Dialog`): dialog to process + Returns: + None + """ + connection.spawn.sendline() + + # reload dialog is expected to passed here + dialog.process(connection.spawn, timeout=timeout, + prompt_recovery=prompt_recovery, + context=connection.context) + + connection.state_machine.go_to('any', connection.spawn, timeout=timeout, + prompt_recovery=prompt_recovery, + context=connection.context) + connection.state_machine.go_to('disable', connection.spawn, timeout=timeout, + prompt_recovery=prompt_recovery, + context=connection.context) + + + def is_active_standby_ready(self, connection, timeout=120, interval=30): + """ Check whether active and standby rp are in ready state + + Args: + connection (`obj`): connection object + timeout (`int`): timeout value, default is 120 secs + interval (`int`): check interval, default is 30 secs + Returns: + result (`bool`): True if both in ready state, else False + """ + active = standby = '' + start_time = time() + + while (time() - start_time) < timeout: + details = self.get_redundancy_details(connection) + for sw_num, info in details.items(): + if info['role'] == 'Active': + active = info.get('state') + elif info['role'] == 'Standby': + standby = info.get('state') + + if active == 'Ready' and standby == 'Ready': + return True + + # Not ready sleep and retry + connection.log.info('Sleeping for %s secs.' % interval) + sleep(interval) + continue + + return False + + + def is_active_ready(self, connection): + """ Check whether active rp is in ready state + + Args: + connection (`obj`): connection object + Returns: + result (`bool`): True if in ready state, else False + """ + active = '' + details = self.get_redundancy_details(connection) + for sw_num, info in details.items(): + if info['role'] == 'Active': + active = info.get('state') + + return active == 'Ready' + + + def is_all_member_ready(self, connection, timeout=270, interval=30): + """ Check whether all rp are in ready state + including active, standby and members + + Args: + connection (`obj`): connection object + timeout (`int`): timeout value, default is 120 secs + interval (`int`): check interval, default is 30 secs + Returns: + result (`bool`): True if all members are in ready state + else False + """ + ready = active = standby = False + start_time = time() + + while (time() - start_time) < timeout: + details = self.get_redundancy_details(connection) + for sw_num, info in details.items(): + state = info.get('state') + if state != 'Ready': + ready = False + break + if info['role'] == 'Active': + active = True + if info['role'] == 'Standby': + standby = True + else: + ready = True + + if ready and active and standby: + return True + # Not ready sleep and retry + connection.log.info('Sleeping for %s secs.' % interval) + sleep(interval) + continue + + return False + + + def get_standby_rp_sn(self, connection): + """ Get the standby rp switch number + + Args: + connection (`obj`): connection object + Returns: + standby (`int`): switch number for standby + """ + standby = None + details = self.get_redundancy_details(connection) + for sw_num, info in details.items(): + role = info.get('role') + if role == 'Standby': + standby = int(sw_num) + + return standby diff --git a/src/unicon/plugins/iosxe/statemachine.py b/src/unicon/plugins/iosxe/statemachine.py index 2de961f4..3d119839 100644 --- a/src/unicon/plugins/iosxe/statemachine.py +++ b/src/unicon/plugins/iosxe/statemachine.py @@ -2,23 +2,104 @@ __author__ = "Myles Dear " -from unicon.plugins.generic.statemachine import GenericSingleRpStateMachine +import re +from datetime import datetime +from unicon.plugins.generic.statemachine import (GenericSingleRpStateMachine, config_transition, + config_service_prompt_handler) from unicon.plugins.generic.statements import (connection_statement_list, - default_statement_list) + default_statement_list, wait_and_enter) from unicon.plugins.generic.service_statements import reload_statement_list -from unicon.plugins.generic.statements import GenericStatements +from unicon.plugins.generic.statements import GenericStatements, buffer_settled from unicon.statemachine import State, Path, StateMachine -from unicon.eal.dialogs import Dialog +from unicon.eal.dialogs import Dialog, Statement from .patterns import IosXEPatterns -from .statements import boot_from_rommon_statement_list +from .statements import ( + boot_image, boot_timeout_stmt, + boot_from_rommon_statement_list) patterns = IosXEPatterns() statements = GenericStatements() +def enable_bash_console_transition(statemachine, spawn, context): + ''' Transition from enable mode to bash_console + + Optional arguments are set by bash_console() (switch and rp). + ''' + switch = context.get('_switch') + rp = context.get('_rp') + chassis = context.get('_chassis') + cmd = 'request platform software system shell' + if switch: + cmd += f' switch {switch}' + if rp: + cmd += f' rp {rp}' + if chassis: + cmd += f' chassis {chassis}' + spawn.sendline(cmd) + + +def boot_from_rommon(statemachine, spawn, context): + context['boot_start_time'] = datetime.now() + context['boot_prompt_count'] = 1 + boot_image(spawn, context, None) + + +def send_break(statemachine, spawn, context): + spawn.send('\x03') + + +def enable_to_maintenance_transition(statemachine, spawn, context): + + dialog = Dialog([ + [patterns.want_continue_confirm, 'sendline()', None, True, False], + [patterns.enable_prompt, wait_and_enter, + {'wait': spawn.settings.MAINTENANCE_MODE_WAIT_TIME}, True, False], + [patterns.maintenance_mode_prompt, None, None, False, False], + [patterns.unable_to_create, 'sendline()', None, True, False] + ]) + + spawn.sendline(spawn.settings.MAINTENANCE_START_COMMAND) + dialog.process(spawn, timeout=spawn.settings.MAINTENANCE_MODE_TIMEOUT) + + spawn.sendline() + +def enable_to_acm_transition(state_machine, spawn, context): + configlet_name = context.get('acm_configlet', '') + spawn.sendline(f'acm configlet create {configlet_name}') + +def enable_to_syntax_transition(state_machine, spawn, context): + configlet_name = context.get('syntax_configlet', '') + spawn.sendline(f'syntax configlet create {configlet_name}') + +def maintenance_to_enable_transition(statemachine, spawn, context): + + dialog = Dialog([ + [patterns.want_continue_yes, 'sendline(yes)', None, True, False], + [patterns.maintenance_mode_prompt, wait_and_enter, + {'wait': spawn.settings.MAINTENANCE_MODE_WAIT_TIME}, True, False], + [patterns.enable_prompt, None, None, False, False], + [patterns.unable_to_create, 'sendline()', None, True, False] + ]) + + spawn.sendline(spawn.settings.MAINTENANCE_STOP_COMMAND) + dialog.process(spawn, timeout=spawn.settings.MAINTENANCE_MODE_TIMEOUT) + + spawn.sendline() + class IosXESingleRpStateMachine(GenericSingleRpStateMachine): config_command = 'config term' + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.config_transition_statement_list = [ + Statement(pattern=patterns.config_start, + action=config_service_prompt_handler, + args={'config_pattern': self.get_state('config').pattern}, + loop_continue=True, + trim_buffer=False) + ] + def create(self): super().create() @@ -32,7 +113,7 @@ def create(self): self.remove_state('config') self.remove_state('enable') self.remove_state('disable') - # incase there is no previous shell state regiestered + # incase there is no previous shell state registered try: self.remove_state('shell') self.remove_path('shell', 'enable') @@ -44,30 +125,86 @@ def create(self): enable = State('enable', patterns.enable_prompt) config = State('config', patterns.config_prompt) shell = State('shell', patterns.shell_prompt) + guestshell = State('guestshell', patterns.guestshell_prompt) + rommon = State('rommon', patterns.rommon_prompt) + tclsh = State('tclsh', patterns.tclsh_prompt) + acm = State('acm', patterns.acm_prompt) + syntax = State('syntax', patterns.syntax_prompt) + rules = State('rules', patterns.rules_prompt) + macro = State('macro', patterns.macro_prompt) + maintenance = State('maintenance', patterns.maintenance_mode_prompt) + config_pki_hexmode = State('config_pki_hexmode', patterns.config_pki_prompt) - disable_to_enable = Path(disable, enable, 'enable', - Dialog([statements.enable_password_stmt, statements.bad_password_stmt])) + disable_to_enable = Path(disable, enable, 'enable', Dialog([ + statements.password_stmt, + statements.enable_password_stmt, + statements.no_password_set_stmt, + statements.bad_password_stmt, + statements.syslog_stripper_stmt + ])) enable_to_disable = Path(enable, disable, 'disable', None) - enable_to_config = Path(enable, config, self.config_command, None) - config_to_enable = Path(config, enable, 'end', None) + enable_to_config = Path(enable, config, config_transition, Dialog([statements.syslog_msg_stmt])) + config_to_enable = Path(config, enable, 'end', Dialog([statements.syslog_msg_stmt])) + + enable_to_guestshell = Path(enable, guestshell, 'guestshell run bash', None) + guestshell_to_enable = Path(guestshell, enable, 'exit', None) + + enable_to_tclsh = Path(enable, tclsh, 'tclsh', None) + tclsh_to_enable = Path(tclsh, enable, 'exit', None) + + enable_to_acm = Path(enable, acm, enable_to_acm_transition, None) + acm_to_enable = Path(acm, enable, 'end', None) + + enable_to_syntax = Path(enable, syntax, 'config check syntax', None) + syntax_to_enable = Path(syntax, enable, 'end', None) + enable_to_rules = Path(enable, rules, 'acm rules', None) + rules_to_enable = Path(rules, enable, 'end', None) + + macro_to_config = Path(macro, config, send_break, None) + + enable_to_maintanance = Path(enable, maintenance, enable_to_maintenance_transition, None) + maintenance_to_enable = Path(maintenance, enable, maintenance_to_enable_transition, None) + + config_pki_hexmode_to_config = Path(config_pki_hexmode, config, 'quit', None) self.add_state(disable) self.add_state(enable) self.add_state(config) + self.add_state(guestshell) + self.add_state(tclsh) + self.add_state(acm) + self.add_state(syntax) + self.add_state(rules) + self.add_state(macro) + self.add_state(maintenance) + self.add_state(config_pki_hexmode) self.add_path(disable_to_enable) self.add_path(enable_to_disable) self.add_path(enable_to_config) self.add_path(config_to_enable) + self.add_path(enable_to_guestshell) + self.add_path(guestshell_to_enable) + self.add_path(enable_to_tclsh) + self.add_path(tclsh_to_enable) + self.add_path(enable_to_acm) + self.add_path(acm_to_enable) + self.add_path(enable_to_syntax) + self.add_path(syntax_to_enable) + self.add_path(enable_to_rules) + self.add_path(rules_to_enable) + self.add_path(macro_to_config) + self.add_path(enable_to_maintanance) + self.add_path(maintenance_to_enable) + self.add_path(config_pki_hexmode_to_config) + enable_to_rommon = Path(enable, rommon, 'reload', Dialog( + connection_statement_list + reload_statement_list)) + + rommon_to_disable = Path(rommon, disable, boot_from_rommon, Dialog( + boot_from_rommon_statement_list)) - rommon = State('rommon', patterns.rommon_prompt) - enable_to_rommon = Path(enable, rommon, 'reload', - Dialog(reload_statement_list)) - rommon_to_disable = \ - Path(rommon, disable, '\r', Dialog( - connection_statement_list + boot_from_rommon_statement_list)) self.add_state(rommon) self.add_path(enable_to_rommon) self.add_path(rommon_to_disable) @@ -75,7 +212,7 @@ def create(self): # Adding SHELL state to IOSXE platform. shell_dialog = Dialog([[patterns.access_shell, 'sendline(y)', None, True, False]]) - enable_to_shell = Path(enable, shell, 'request platform software system shell', shell_dialog) + enable_to_shell = Path(enable, shell, enable_bash_console_transition, shell_dialog) shell_to_enable = Path(shell, enable, 'exit', None) # Add State and Path to State Machine @@ -85,6 +222,8 @@ def create(self): class IosXEDualRpStateMachine(StateMachine): + config_command = 'config term' + def create(self): # States disable = State('disable', patterns.disable_prompt) @@ -93,27 +232,65 @@ def create(self): standby_locked = State('standby_locked', patterns.standby_locked) rommon = State('rommon', patterns.rommon_prompt) shell = State('shell', patterns.shell_prompt) + tclsh = State('tclsh', patterns.tclsh_prompt) + acm = State('acm', patterns.acm_prompt) + syntax = State('syntax', patterns.syntax_prompt) + rules = State('rules', patterns.rules_prompt) + macro = State('macro', patterns.macro_prompt) + + def update_cur_state(sm, state): + sm._current_state = state # Paths - disable_to_enable = Path(disable, enable, 'enable', - Dialog([statements.enable_password_stmt, statements.bad_password_stmt])) - enable_to_disable = Path(enable, disable, 'disable', None) + disable_to_enable = Path(disable, enable, 'enable', Dialog([ + statements.enable_password_stmt, + statements.bad_password_stmt, + statements.syslog_stripper_stmt, + Statement( + pattern=patterns.standby_locked, + action=update_cur_state, + args={ + 'sm': self, + 'state': 'standby_locked' + }, + loop_continue=False) + ])) + + enable_to_disable = Path(enable, disable, 'disable', Dialog([statements.syslog_msg_stmt])) - enable_to_config = Path(enable, config, 'config term', None) + enable_to_config = Path(enable, config, config_transition, Dialog([statements.syslog_msg_stmt])) - config_to_enable = Path(config, enable, 'end', None) + config_to_enable = Path(config, enable, 'end', Dialog([statements.syslog_msg_stmt])) - enable_to_rommon = Path(enable, rommon, 'reload', - Dialog(reload_statement_list)) + enable_to_rommon = Path(enable, rommon, 'reload', Dialog( + connection_statement_list + reload_statement_list)) rommon_to_disable = \ - Path(rommon, disable, '\r', Dialog( - connection_statement_list + boot_from_rommon_statement_list)) + Path(rommon, disable, boot_from_rommon, Dialog( + boot_from_rommon_statement_list)) + + enable_to_tclsh = Path(enable, tclsh, 'tclsh', None) + tclsh_to_enable = Path(tclsh, enable, 'exit', None) + + enable_to_acm = Path(enable, acm, enable_to_acm_transition, None) + acm_to_enable = Path(acm, enable, 'end', None) + + enable_to_syntax = Path(enable, syntax, enable_to_syntax_transition, None) + syntax_to_enable = Path(syntax, enable, 'end', None) + enable_to_rules = Path(enable, rules, 'acm rules', None) + rules_to_enable = Path(rules, enable, 'end', None) + + macro_to_config = Path(macro, config, send_break, None) self.add_state(disable) self.add_state(enable) self.add_state(config) self.add_state(rommon) + self.add_state(tclsh) + self.add_state(acm) + self.add_state(syntax) + self.add_state(rules) + self.add_state(macro) # Ensure that a locked standby is properly detected. # This is done by ensuring this is the last state added @@ -126,11 +303,20 @@ def create(self): self.add_path(config_to_enable) self.add_path(enable_to_rommon) self.add_path(rommon_to_disable) + self.add_path(enable_to_tclsh) + self.add_path(tclsh_to_enable) + self.add_path(enable_to_acm) + self.add_path(acm_to_enable) + self.add_path(enable_to_syntax) + self.add_path(syntax_to_enable) + self.add_path(enable_to_rules) + self.add_path(rules_to_enable) + self.add_path(macro_to_config) # Adding SHELL state to IOSXE platform. shell_dialog = Dialog([[patterns.access_shell, 'sendline(y)', None, True, False]]) - enable_to_shell = Path(enable, shell, 'request platform software system shell', shell_dialog) + enable_to_shell = Path(enable, shell, enable_bash_console_transition, shell_dialog) shell_to_enable = Path(shell, enable, 'exit', None) # Add State and Path to State Machine diff --git a/src/unicon/plugins/iosxe/statements.py b/src/unicon/plugins/iosxe/statements.py index 2ed129bc..9a95668c 100644 --- a/src/unicon/plugins/iosxe/statements.py +++ b/src/unicon/plugins/iosxe/statements.py @@ -1,12 +1,24 @@ +import re +import time +import logging +from functools import wraps +from datetime import datetime, timedelta from unicon.eal.dialogs import Statement - from unicon.plugins.generic.service_statements import\ admin_password as admin_password_stmt +from unicon.plugins.generic.statements import ( + connection_statement_list, + boot_timeout_stmt, + terminal_position_handler, +) + +from .patterns import IosXEReloadPatterns, IosXEPatterns -from .patterns import IosXEReloadPatterns +log = logging.getLogger(__name__) +reload_patterns = IosXEReloadPatterns() +patterns = IosXEPatterns() -p = IosXEReloadPatterns() def please_reset_handler(spawn, session): """ Handles the router asking to be reset before booting. """ @@ -60,40 +72,187 @@ def rommon_prompt_handler(spawn, session, context): # "Please reset" message was detected. # Set the configuration register to 0x0 (boot to rommon) and reset. # This is recommended in the platform documentation: - # http://www.cisco.com/c/en/us/support/docs/routers/4000-series-integrated-services-routers/200678-Troubleshoot-Cisco-4000-Series-ISR-Stuck.pdf + # http://www.cisco.com/c/en/us/support/docs/routers/4000-platform-integrated-services-routers/200678-Troubleshoot-Cisco-4000-platform-ISR-Stuck.pdf spawn.send("confreg 0x0\r") session['rommon_count'] = 1 + +def grub_prompt_handler(spawn, session, context): + """ handles the grub menu during boot process + """ + + # if grub prompt already handled, return + if session.get('grub_handler'): + return + else: + session['grub_handler'] = True + # Below is used by the boot_timeout statement + # with the `BOOT_TIMEOUT` setting. + context['boot_start_time'] = datetime.now() + context['boot_prompt_count'] = 1 + + grub_boot_image = context.get('grub_boot_image') + # if no grub_boot_image, do nothing + if not grub_boot_image: + return + + spawn.log.info("Finding an entry that includes the string '{}'". + format(grub_boot_image)) + + # Regex to match grub screen boot entries on cat9kv + lines = re.findall(spawn.settings.GRUB_REGEX_PATTERN, spawn.buffer) + + spawn.log.debug(f'Grub lines: {lines}') + + selected_line = None + desired_line = None + + # Get index for selected_line and desired_line + for index, line in enumerate(lines): + # \x1b[7m is reverse video (inverted colors) + if '*' in line or '\x1b[7m' in line: + selected_line = index + if grub_boot_image in line: + desired_line = index + + if selected_line is None or desired_line is None: + raise Exception("Cannot figure out which image to select! " + "Debug info:\n" + "selected_line: {}\n" + "desired_line: {}\n" + "lines: {}" + .format(selected_line, desired_line, lines)) + + spawn.log.info("Selecting the entry '{}' now.".format(lines[desired_line])) + + num_lines_to_move = desired_line - selected_line + + spawn.log.debug(f'Lines to move: {num_lines_to_move}') + + keys = { + 'down': '\x1B[B', + 'up': '\x1B[A' + } + # If positive we want to move down the list. + # If negative we want to move up the list. + if num_lines_to_move >= 0: + key = 'down' + else: + key = 'up' + + for _ in range(abs(num_lines_to_move)): + spawn.send(keys.get(key)) + time.sleep(0.5) + + spawn.sendline() + time.sleep(0.5) + + +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 context.get('image_to_boot', '').strip(): + 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 + else: + raise Exception("Too many failed boot attempts have been detected.") + + +boot_from_rommon_stmt = Statement( + pattern=patterns.rommon_prompt, + action=boot_image, + args=None, + loop_continue=True, + continue_timer=False) + +terminal_position_stmt = Statement( + pattern=patterns.get_cursor_position, + action=terminal_position_handler, + args=None, + loop_continue=True, + continue_timer=False, +) + # Statement covering when a device asks us to reset it. please_reset_stmt = \ - Statement(pattern=p.please_reset, + Statement(pattern=reload_patterns.please_reset, action=please_reset_handler, args=None, loop_continue=True, continue_timer=False) -rommon_boot_stmt = \ - Statement(pattern=p.rommon_prompt, - action=rommon_prompt_handler, +grub_prompt_stmt = \ + Statement(pattern=reload_patterns.grub_prompt, + action=grub_prompt_handler, args=None, loop_continue=True, continue_timer=False) setup_dialog_stmt = \ - Statement(pattern=p.setup_dialog, + Statement(pattern=reload_patterns.setup_dialog, action='sendline(no)', args=None, loop_continue=True, continue_timer=False) auto_install_stmt = \ - Statement(pattern=p.autoinstall_dialog, + Statement(pattern=reload_patterns.autoinstall_dialog, action='sendline(yes)', args=None, loop_continue=True, continue_timer=False) +# This list is extended later, see below boot_from_rommon_statement_list = [ - please_reset_stmt, rommon_boot_stmt, admin_password_stmt, + please_reset_stmt, admin_password_stmt, setup_dialog_stmt, auto_install_stmt, + boot_timeout_stmt, grub_prompt_stmt ] + + +def boot_finished_deco(func): + '''Decorator function that wraps dialog statements + for rommon to disable state transition to pop the + boot_start_time after boot is (supposedly) finished. + + Used with boot_from_rommon_statement_list (see below) + ''' + + @wraps(func) + def wrapper(spawn, session, context, **kwargs): + args = [a for a in [spawn, session, context] if a] + if context: + context.pop('boot_start_time', None) + return func(*args, **kwargs) + return wrapper + + +# Create list of statements for rommon to disable, i.e. device boot +# If the boot is completed because we hit a statement with +# loop_continue = False, use the wrapper to pop the start time +# from the context dict. +boot_from_rommon_statement_list += connection_statement_list.copy() +for stmt in boot_from_rommon_statement_list: + if stmt.pattern in [reload_patterns.press_return] or stmt.loop_continue is False: + stmt.action = boot_finished_deco(stmt.action) diff --git a/src/unicon/plugins/iosxr/__init__.py b/src/unicon/plugins/iosxr/__init__.py index e2e229bb..4f0a273a 100755 --- a/src/unicon/plugins/iosxr/__init__.py +++ b/src/unicon/plugins/iosxr/__init__.py @@ -17,6 +17,7 @@ def __init__(self): super().__init__() self.execute = svc.Execute self.configure = svc.Configure + self.configure_exclusive = svc.ConfigureExclusive self.admin_execute = svc.AdminExecute self.admin_configure = svc.AdminConfigure self.attach_console = svc.AttachModuleConsole @@ -25,13 +26,17 @@ def __init__(self): self.admin_attach_console = svc.AdminAttachModuleConsole self.admin_bash_console = svc.AdminBashService self.ping = IosXePing + self.reload = svc.Reload + self.monitor = svc.Monitor + class IOSXRHAServiceList(HAServiceList): """ Generic dual rp services. """ def __init__(self): super().__init__() self.execute = svc.HAExecute - self.configure= svc.HaConfigureService + self.reload = svc.HaReload + self.configure = svc.HaConfigureService self.admin_execute = svc.HaAdminExecute self.admin_configure = svc.HaAdminConfigure self.switchover = svc.Switchover @@ -40,21 +45,22 @@ def __init__(self): self.admin_console = svc.AdminService self.admin_attach_console = svc.AdminAttachModuleConsole self.admin_bash_console = svc.AdminBashService - + self.get_rp_state = svc.GetRPState + self.monitor = svc.Monitor class IOSXRSingleRpConnection(BaseSingleRpConnection): os = 'iosxr' - series = None + platform = None chassis_type = 'single_rp' state_machine_class = IOSXRSingleRpStateMachine - connection_provider_class = IOSXRSingleRpConnectionProvider + connection_provider_class = IOSXRSingleRpConnectionProvider subcommand_list = IOSXRServiceList settings = IOSXRSettings() class IOSXRDualRpConnection(BaseDualRpConnection): os = 'iosxr' - series = None + platform = None chassis_type = 'dual_rp' state_machine_class = IOSXRDualRpStateMachine connection_provider_class = IOSXRDualRpConnectionProvider diff --git a/src/unicon/plugins/iosxr/asr9k/__init__.py b/src/unicon/plugins/iosxr/asr9k/__init__.py index 71b7cfdb..5ab29d6f 100755 --- a/src/unicon/plugins/iosxr/asr9k/__init__.py +++ b/src/unicon/plugins/iosxr/asr9k/__init__.py @@ -1,35 +1,46 @@ __author__ = "Myles Dear " -from unicon.bases.routers.connection import BaseSingleRpConnection -from unicon.bases.routers.connection import BaseDualRpConnection - -from unicon.plugins.iosxr.asr9k.statemachine import IOSXRASR9KSingleRpStateMachine -from unicon.plugins.iosxr.asr9k.statemachine import IOSXRASR9KDualRpStateMachine -from unicon.plugins.iosxr.__init__ import IOSXRServiceList -from unicon.plugins.iosxr.__init__ import IOSXRHAServiceList - -from unicon.plugins.iosxr.connection_provider \ - import IOSXRSingleRpConnectionProvider -from unicon.plugins.iosxr.connection_provider \ - import IOSXRDualRpConnectionProvider +from unicon.plugins.iosxr import (IOSXRSingleRpConnection, + IOSXRDualRpConnection) + +from unicon.plugins.iosxr.asr9k.statemachine import (IOSXRASR9KSingleRpStateMachine, + IOSXRASR9KDualRpStateMachine) +from unicon.plugins.iosxr import (IOSXRServiceList, + IOSXRHAServiceList) + +from unicon.plugins.iosxr.connection_provider import (IOSXRSingleRpConnectionProvider, + IOSXRDualRpConnectionProvider) +from . import service_implementation as asr9k_svc from unicon.plugins.iosxr.asr9k.settings import IOSXRAsr9kSettings -class IOSXRASR9KSingleRpConnection(BaseSingleRpConnection): +class IOSXRASR9KServiceList(IOSXRServiceList): + def __init__(self): + super().__init__() + self.reload = asr9k_svc.Reload + + +class IOSXRASR9KHAServiceList(IOSXRHAServiceList): + def __init__(self): + super().__init__() + self.reload = asr9k_svc.HAReload + + +class IOSXRASR9KSingleRpConnection(IOSXRSingleRpConnection): os = 'iosxr' - series = 'asr9k' + platform = 'asr9k' chassis_type = 'single_rp' state_machine_class = IOSXRASR9KSingleRpStateMachine connection_provider_class = IOSXRSingleRpConnectionProvider - subcommand_list = IOSXRServiceList + subcommand_list = IOSXRASR9KServiceList settings = IOSXRAsr9kSettings() -class IOSXRASR9KDualRpConnection(BaseDualRpConnection): +class IOSXRASR9KDualRpConnection(IOSXRDualRpConnection): os = 'iosxr' - series = 'asr9k' + platform = 'asr9k' chassis_type = 'dual_rp' state_machine_class = IOSXRASR9KDualRpStateMachine connection_provider_class = IOSXRDualRpConnectionProvider - subcommand_list = IOSXRHAServiceList + subcommand_list = IOSXRASR9KHAServiceList settings = IOSXRAsr9kSettings() diff --git a/src/unicon/plugins/iosxr/asr9k/service_implementation.py b/src/unicon/plugins/iosxr/asr9k/service_implementation.py new file mode 100644 index 00000000..9297afb4 --- /dev/null +++ b/src/unicon/plugins/iosxr/asr9k/service_implementation.py @@ -0,0 +1,371 @@ +""" +Module: + unicon.plugins.iosxr.asr9k.service_implementation + +Description: + This module ASR9K specific service implementation + +""" +__author__ = "Takashi Higashimura " + +import re +from time import sleep +from datetime import datetime, timedelta + +from unicon.bases.routers.services import BaseService +from unicon.core.errors import SubCommandFailure, TimeoutError +from unicon.eal.dialogs import Dialog +from unicon.plugins.generic.statements import buffer_settled + + +from .service_statements import reload_statement_list, reload_statement_list_vty + + +class Reload(BaseService): + """Service to reload the device. + + Arguments: + reload_command: reload command to be issued. default is "reload" + dialog: Dialog which include list of Statements for + additional dialogs prompted by reload command, in-case + it is not in the current list. + timeout: Timeout value in sec, Default Value is 300 sec + reload_creds: name or list of names of credential(s) to use if + username or password is prompted for during reload. + + Returns: + True on Success, raise SubCommandFailure on failure + + Example :: + .. code-block:: python + + rtr.reload() + # If reload command is other than 'reload' + rtr.reload(reload_command="reload location all", timeout=400) + + """ + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.start_state = 'enable' + self.end_state = 'enable' + self.service_name = 'reload' + self.timeout = connection.settings.RELOAD_TIMEOUT + self.dialog = Dialog(reload_statement_list) + self.__dict__.update(kwargs) + + def call_service(self, + reload_command='reload', + dialog=Dialog([]), + timeout=None, + reload_creds=None, + error_pattern=None, + append_error_pattern=None, + raise_on_error=True, + *args, **kwargs): + con = self.connection + self.context = con.context + timeout = timeout or self.timeout + + if error_pattern is None: + self.error_pattern = con.settings.ERROR_PATTERN + else: + self.error_pattern = error_pattern + + if not isinstance(self.error_pattern, list): + raise ValueError('error_pattern should be a list') + if append_error_pattern: + if not isinstance(append_error_pattern, list): + raise ValueError('append_error_pattern should be a list') + self.error_pattern += append_error_pattern + + fmt_msg = "+++ reloading %s " \ + " with reload_command %s " \ + "and timeout is %s +++" + con.log.debug(fmt_msg % (self.connection.hostname, + reload_command, + timeout)) + + con.state_machine.go_to(self.start_state, + con.spawn, + prompt_recovery=self.prompt_recovery, + context=self.context) + + if not isinstance(dialog, Dialog): + raise SubCommandFailure( + "dialog passed must be an instance of Dialog") + + show_terminal = con.execute('show terminal') + line_type = re.search(r"Line .*, Type \"(\w+)\"", show_terminal) + if line_type and line_type.groups(): + line_type = line_type.group(1) + + if reload_creds: + context = self.context.copy() + context.update(cred_list=reload_creds) + else: + context = self.context + + if line_type == 'Console': + dialog += self.dialog + con.spawn.sendline(reload_command) + try: + self.result = dialog.process(con.spawn, + timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=context) + if self.result: + self.result = self.result.match_output + self.get_service_result() + except Exception as err: + raise SubCommandFailure("Reload failed %s" % err) + + output = self.result + output = output.replace(reload_command, "", 1) + # only strip first newline and leave formatting intact + output = re.sub(r"^\r?\r\n", "", output, 1) + output = output.rstrip() + + # Bring standby to good state. + con.log.info('Reconnecting to device after reload') + wait_time = timedelta(seconds=con.settings.POST_RELOAD_WAIT) + settle_time = current_time = datetime.now() + con.disconnect() + while (current_time - settle_time) < wait_time: + try: + con.connect() + except Exception as e: + current_time = datetime.now() + if (current_time - settle_time) < wait_time: + con.log.info('Could not connect to device. Try again!') + continue + else: + if raise_on_error: + raise + else: + con.log.exception('Connection to {} failed'.format(con.hostname)) + self.result = False + else: + con.log.info('Connected to device after reload') + break + else: + con.log.warning('Did not detect a console session, will try to reconnect...') + dialog = Dialog(reload_statement_list_vty) + con.spawn.sendline(reload_command) + output = "" + self.result = dialog.process(con.spawn, + timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=self.context) + if self.result: + output += self.result.match_output + try: + m = con.spawn.expect('.+', timeout=10) + if m: + output += m.match_output + except TimeoutError: + pass + con.log.warning('Disconnecting...') + con.disconnect() + for x in range(con.settings.RELOAD_ATTEMPTS): + con.log.warning('Waiting for {} seconds'.format(con.settings.RELOAD_WAIT)) + sleep(con.settings.RELOAD_WAIT) + con.log.warning('Trying to connect... attempt #{}'.format(x+1)) + try: + output += con.connect() + except: + con.log.warning('Connection failed') + if con.is_connected: + break + + if not con.is_connected: + raise SubCommandFailure('Reload failed - could not reconnect') + + self.result = output + + +class HAReload(BaseService): + """Service to reload the device. + + Arguments: + reload_command: reload command to be issued. default is "reload" + dialog: Dialog which include list of Statements for + additional dialogs prompted by reload command, in-case + it is not in the current list. + timeout: Timeout value in sec, Default Value is 300 sec + reload_creds: name or list of names of credential(s) to use if + username or password is prompted for during reload. + + Returns: + True on Success, raise SubCommandFailure on failure + + Example :: + .. code-block:: python + + rtr.reload() + # If reload command is other than 'reload' + rtr.reload(reload_command="reload location all", timeout=400) + + """ + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.start_state = 'enable' + self.end_state = 'enable' + self.service_name = 'reload' + self.timeout = connection.settings.RELOAD_TIMEOUT + self.dialog = Dialog(reload_statement_list) + self.__dict__.update(kwargs) + + def call_service(self, + reload_command='reload', + dialog=Dialog([]), + target='active', + timeout=None, + reload_creds=None, + error_pattern=None, + append_error_pattern=None, + raise_on_error=True, + *args, **kwargs): + + con = self.connection + self.context = con.active.context + timeout = timeout or self.timeout + + if error_pattern is None: + self.error_pattern = con.settings.ERROR_PATTERN + else: + self.error_pattern = error_pattern + + if not isinstance(self.error_pattern, list): + raise ValueError('error_pattern should be a list') + if append_error_pattern: + if not isinstance(append_error_pattern, list): + raise ValueError('append_error_pattern should be a list') + self.error_pattern += append_error_pattern + + fmt_msg = "+++ reloading %s " \ + " with reload_command %s " \ + "and timeout is %s +++" + con.log.debug(fmt_msg % (self.connection.hostname, + reload_command, + timeout)) + + con.active.state_machine.go_to(self.start_state, + con.active.spawn, + prompt_recovery=self.prompt_recovery, + context=self.context) + + if not isinstance(dialog, Dialog): + raise SubCommandFailure( + "dialog passed must be an instance of Dialog") + + show_terminal = con.execute('show terminal') + line_type = re.search(r"Line .*, Type \"(\w+)\"", show_terminal) + if line_type and line_type.groups(): + line_type = line_type.group(1) + + if reload_creds: + context = self.context.copy() + context.update(cred_list=reload_creds) + else: + context = self.context + + if line_type == 'Console': + dialog += self.dialog + con.active.spawn.sendline(reload_command) + try: + try: + self.result = dialog.process(con.active.spawn, + prompt_recovery=self.prompt_recovery, + context=context) + if self.result: + self.result = self.result.match_output + self.get_service_result() + + except Exception: + self.result = con.active.spawn.buffer + if 'is in standby' in self.result: + con.log.info('Timed out due to active/standby interchanged. Reconnecting...') + else: + con.log.info('Timed out. timeout might need to be increased. Reconnecting...') + con.disconnect() + original_connection_timeout = con.settings.CONNECTION_TIMEOUT + con.settings.CONNECTION_TIMEOUT = timeout + + con.log.info(f"Connecting to the {self.connection.hostname} within {con.settings.CONNECTION_TIMEOUT} seconds") + reconnect_attempts = con.settings.RELOAD_RECONNECT_ATTEMPTS + + for x in range(reconnect_attempts): + + con.log.info('Waiting for {} seconds'.format(con.settings.CONNECTION_TIMEOUT / reconnect_attempts)) + sleep(con.settings.CONNECTION_TIMEOUT / reconnect_attempts) + try: + con.log.info('Trying to connect... attempt #{}'.format(x + 1)) + con.connect() + break + except: + con.log.info(f'Reconnecting to the device') + continue + else: + con.log.exception(f'Could not connect to the device post reload. \ + Waited for {con.settings.CONNECTION_TIMEOUT} seconds') + + con.settings.CONNECTION_TIMEOUT = original_connection_timeout + # Bring standby to good state. + con.log.info('Reconnecting to device after reload') + wait_time = timedelta(seconds=con.settings.POST_RELOAD_WAIT) + settle_time = current_time = datetime.now() + con.disconnect() + while (current_time - settle_time) < wait_time: + try: + con.connect() + except Exception as e: + current_time = datetime.now() + if (current_time - settle_time) < wait_time: + con.log.info('Could not connect to device. Try again!') + continue + else: + if raise_on_error: + raise + else: + con.log.exception('Connection to {} failed'.format(con.hostname)) + self.result = False + else: + con.log.info('Connected to device after reload') + break + + con.log.info('Waiting for config sync to finish') + standby_wait_time = con.settings.POST_HA_RELOAD_CONFIG_SYNC_WAIT + standby_wait_interval = 50 + standby_sync_try = standby_wait_time // standby_wait_interval + 1 + for round in range(standby_sync_try): + con.standby.spawn.sendline() + try: + con.standby.state_machine.go_to( + 'any', + con.standby.spawn, + context=context, + timeout=standby_wait_interval, + prompt_recovery=self.prompt_recovery, + dialog=con.connection_provider.get_connection_dialog() + ) + break + except Exception as err: + if round == standby_sync_try - 1: + raise Exception( + 'Bringing standby to any state failed within {} sec' + .format(standby_wait_time)) from err + except Exception as err: + raise SubCommandFailure("Reload failed %s" % err) + + output = self.result + else: + raise Exception("Console is not used.") + + if self.result: + con.log.info('--- Reload of device {} completed ---'.format(con.hostname)) + else: + con.log.info('--- Reload of device {} failed ---'.format(con.hostname)) + + self.result = output diff --git a/src/unicon/plugins/iosxr/asr9k/service_patterns.py b/src/unicon/plugins/iosxr/asr9k/service_patterns.py new file mode 100644 index 00000000..6df1be37 --- /dev/null +++ b/src/unicon/plugins/iosxr/asr9k/service_patterns.py @@ -0,0 +1,9 @@ +__author__ = "Takashi Higashimura " + +from unicon.plugins.iosxr.service_patterns import IOSXRReloadPatterns + +class IOSXRASR9KReloadPatterns(IOSXRReloadPatterns): + def __init__(self): + super().__init__() + self.system_config_completed = r"^(.*?)SYSTEM CONFIGURATION COMPLETED" + self.reloading_node = r"^(.*?)Reloading node .*" diff --git a/src/unicon/plugins/iosxr/asr9k/service_statements.py b/src/unicon/plugins/iosxr/asr9k/service_statements.py new file mode 100644 index 00000000..4171d9f6 --- /dev/null +++ b/src/unicon/plugins/iosxr/asr9k/service_statements.py @@ -0,0 +1,69 @@ +__author__ = "Takashi Higashimura " + +from unicon.eal.dialogs import Statement + +from unicon.plugins.generic.service_statements import (save_env, + confirm_reset, reload_confirm, + reload_confirm_ios, useracess, + confirm_config, setup_dialog, + auto_install_dialog, module_reload, + save_module_cfg, reboot_confirm, + secure_passwd_std, admin_password, + auto_provision, login_stmt, + send_response, password_handler) +from unicon.plugins.iosxr.service_statements import confirm_module_reload_stmt + +from .service_patterns import IOSXRASR9KReloadPatterns + + +pat = IOSXRASR9KReloadPatterns() + + +press_enter = Statement(pattern=pat.press_enter, + action=send_response, args={'response': ''}, + loop_continue=True, + continue_timer=False) + +config_completed = Statement(pattern=pat.system_config_completed, + action=send_response, args={'response': ''}, + loop_continue=False, + continue_timer=False) + +password_stmt = Statement(pattern=pat.password, + action=password_handler, + args=None, + loop_continue=True, + continue_timer=False) + +reloading_node_stmt = Statement(pattern=pat.reloading_node, + action=None, + args=None, + loop_continue=False, + continue_timer=False) + + +reload_statement_list = [save_env, + confirm_reset, + reload_confirm, + reload_confirm_ios, + useracess, + confirm_config, + setup_dialog, + auto_install_dialog, + module_reload, + save_module_cfg, + reboot_confirm, + secure_passwd_std, + admin_password, + auto_provision, + login_stmt, + password_stmt, + press_enter, + confirm_module_reload_stmt, + config_completed, # loop_continue=False + ] + +reload_statement_list_vty = [reload_confirm, + reload_confirm_ios, + reloading_node_stmt # loop_continue=False + ] diff --git a/src/unicon/plugins/iosxr/asr9k/settings.py b/src/unicon/plugins/iosxr/asr9k/settings.py index 711f70bd..291ae979 100644 --- a/src/unicon/plugins/iosxr/asr9k/settings.py +++ b/src/unicon/plugins/iosxr/asr9k/settings.py @@ -6,3 +6,7 @@ class IOSXRAsr9kSettings(IOSXRSettings): def __init__(self): super().__init__() self.STANDBY_STATE_REGEX = r'Standby node .* is (.*)' + + # number of retries to reconnect after reloading + self.RELOAD_RECONNECT_ATTEMPTS = 3 + self.POST_RELOAD_WAIT = 500 diff --git a/src/unicon/plugins/iosxr/connection_provider.py b/src/unicon/plugins/iosxr/connection_provider.py index 8a05c84d..b7b000b8 100755 --- a/src/unicon/plugins/iosxr/connection_provider.py +++ b/src/unicon/plugins/iosxr/connection_provider.py @@ -4,23 +4,26 @@ from random import randint +from unicon.eal.dialogs import Dialog +from unicon.statemachine import State + +from unicon.core.errors import TimeoutError from unicon.bases.routers.connection_provider \ import BaseSingleRpConnectionProvider, BaseDualRpConnectionProvider -from unicon.plugins.generic.statements import pre_connection_statement_list -from unicon.plugins.iosxr.statements import authentication_statement_list + from unicon.plugins.generic.statements import custom_auth_statements -from unicon.core.errors import TimeoutError +from unicon.plugins.generic.statements import pre_connection_statement_list +from unicon.plugins.generic.patterns import GenericPatterns + + from unicon.plugins.iosxr.patterns import IOSXRPatterns from unicon.plugins.iosxr.errors import RpNotRunningError -from unicon.eal.dialogs import Dialog -from unicon.bases.routers.connection_provider \ - import BaseDualRpConnectionProvider +from unicon.plugins.iosxr.statements import authentication_statement_list patterns = IOSXRPatterns() - class IOSXRSingleRpConnectionProvider(BaseSingleRpConnectionProvider): def __init__(self, *args, **kwargs): @@ -67,10 +70,7 @@ def set_init_commands(self): if con.init_config_commands is not None: self.init_config_commands = con.init_config_commands else: - hostname_command = [] - if con.hostname != None and con.hostname != '': - hostname_command = ['hostname ' + con.hostname] - self.init_config_commands = hostname_command + con.settings.IOSXR_INIT_CONFIG_COMMANDS + self.init_config_commands = con.settings.IOSXR_INIT_CONFIG_COMMANDS def get_connection_dialog(self): con = self.connection @@ -87,24 +87,32 @@ def designate_handles(self): """ Identifies the Role of each handle and designates if it is active or standby and bring the active RP to enable state """ con = self.connection - if con.a.state_machine.current_state is 'standby_locked': - target_rp = 'b' - other_rp = 'a' - elif con.b.state_machine.current_state is 'standby_locked': - target_rp = 'a' - other_rp = 'b' + subcons = list(con._subconnections.items()) + subcon1_alias, subcon1 = subcons[0] + subcon2_alias, subcon2 = subcons[1] + if subcon1.state_machine.current_state == 'standby_locked': + target_con = subcon2 + other_con = subcon1 + target_alias = subcon2_alias + other_alias = subcon1_alias + elif subcon2.state_machine.current_state == 'standby_locked': + target_con = subcon1 + other_con = subcon2 + target_alias = subcon1_alias + other_alias = subcon2_alias else: con.log.info("None of the RPs are currently in standby locked state") - target_rp = 'a' - other_rp = 'b' - target_handle = getattr(con, target_rp) - other_handle = getattr(con, other_rp) - target_handle.role = 'active' - other_handle.role = 'standby' - target_handle.state_machine.go_to('enable', - target_handle.spawn, - context=con.context, - timeout=con.connection_timeout, + target_con = subcon2 + other_con = subcon1 + target_alias = subcon2_alias + other_alias = subcon1_alias + + con._set_active_alias(target_alias) + con._set_standby_alias(other_alias) + target_con.state_machine.go_to('enable', + target_con.spawn, + context=target_con.context, + timeout=target_con.connection_timeout, dialog=self.get_connection_dialog(), ) con._handles_designated = True @@ -112,14 +120,28 @@ def designate_handles(self): def connect(self): """ Connects, initializes and designates handle """ con = self.connection - con.log.info('+++ connection to %s +++' % str(self.connection.a.spawn)) - con.log.info('+++ connection to %s +++' % str(self.connection.b.spawn)) + + for subconnection in con.subconnections: + con.log.info('+++ connection to %s +++' % str(subconnection.spawn)) + if con.learn_tokens or con.settings.LEARN_DEVICE_TOKENS: + # Add learn tokens state to state machine so it can use a looser + # prompt pattern to match. Required for at least some Linux prompts + for subconnection in con.subconnections: + if 'learn_tokens_state' not in \ + [str(s) for s in subconnection.state_machine.states]: + self.learn_tokens_state = State('learn_tokens_state', + GenericPatterns().learn_os_prompt) + subconnection.state_machine.add_state(self.learn_tokens_state) self.establish_connection() - con.log.info('+++ designating handles +++') - self.designate_handles() - con.log.info('+++ initializing active handle +++') - self.init_active() - self.connection._is_connected = True + # Maintain initial state + if not con.mit: + con.log.info('+++ designating handles +++') + self.designate_handles() + + # Run initial exec/configure commands on the active, which is + # supposed to disable console logging. + con.log.info('+++ initializing active handle +++') + self.init_active() class IOSXRVirtualConnectionProviderLaunchWaiter(object): diff --git a/src/unicon/plugins/iosxr/iosxrv/__init__.py b/src/unicon/plugins/iosxr/iosxrv/__init__.py index 080b87ce..bb32fd4e 100755 --- a/src/unicon/plugins/iosxr/iosxrv/__init__.py +++ b/src/unicon/plugins/iosxr/iosxrv/__init__.py @@ -5,8 +5,8 @@ from unicon.plugins.iosxr.iosxrv.statemachine import IOSXRVSingleRpStateMachine from unicon.plugins.iosxr.iosxrv.statemachine import IOSXRVDualRpStateMachine -from unicon.plugins.iosxr.__init__ import IOSXRServiceList -from unicon.plugins.iosxr.__init__ import IOSXRHAServiceList +from unicon.plugins.iosxr import IOSXRServiceList +from unicon.plugins.iosxr import IOSXRHAServiceList from unicon.plugins.iosxr.iosxrv.connection_provider \ import IOSXRVSingleRpConnectionProvider @@ -16,7 +16,7 @@ class IOSXRVSingleRpConnection(BaseSingleRpConnection): os = 'iosxr' - series = 'iosxrv' + platform = 'iosxrv' chassis_type = 'single_rp' state_machine_class = IOSXRVSingleRpStateMachine connection_provider_class = IOSXRVSingleRpConnectionProvider @@ -25,7 +25,7 @@ class IOSXRVSingleRpConnection(BaseSingleRpConnection): class IOSXRVDualRpConnection(BaseDualRpConnection): os = 'iosxr' - series = 'iosxrv' + platform = 'iosxrv' chassis_type = 'dual_rp' state_machine_class = IOSXRVDualRpStateMachine connection_provider_class = IOSXRVDualRpConnectionProvider diff --git a/src/unicon/plugins/iosxr/iosxrv/statemachine.py b/src/unicon/plugins/iosxr/iosxrv/statemachine.py index 2de80f1b..a64b42ad 100755 --- a/src/unicon/plugins/iosxr/iosxrv/statemachine.py +++ b/src/unicon/plugins/iosxr/iosxrv/statemachine.py @@ -3,7 +3,7 @@ from unicon.plugins.iosxr.statemachine import IOSXRSingleRpStateMachine from unicon.plugins.iosxr.iosxrv.patterns import IOSXRVPatterns -from unicon.plugins.iosxr.statements import IOSXRStatements +from unicon.plugins.iosxr.statements import IOSXRStatements, handle_failed_config from unicon.statemachine import State, Path from unicon.eal.dialogs import Statement, Dialog @@ -31,8 +31,7 @@ def create(self): config_dialog = Dialog([ [patterns.commit_changes_prompt, 'sendline(yes)', None, True, False], [patterns.commit_replace_prompt, 'sendline(yes)', None, True, False], - [patterns.configuration_failed_message, - self.handle_failed_config, None, True, False] + [patterns.configuration_failed_message, handle_failed_config, None, True, False] ]) enable_to_config = Path(enable, config, 'configure terminal', None) diff --git a/src/unicon/plugins/iosxr/iosxrv9k/__init__.py b/src/unicon/plugins/iosxr/iosxrv9k/__init__.py index feaadde7..cc4f5a70 100755 --- a/src/unicon/plugins/iosxr/iosxrv9k/__init__.py +++ b/src/unicon/plugins/iosxr/iosxrv9k/__init__.py @@ -2,13 +2,13 @@ from unicon.plugins.iosxr.iosxrv9k.settings import IOSXRV9KSettings from unicon.plugins.iosxr.statemachine import IOSXRSingleRpStateMachine -from unicon.plugins.iosxr.__init__ import IOSXRServiceList +from unicon.plugins.iosxr import IOSXRServiceList from unicon.plugins.iosxr.iosxrv9k.connection_provider import IOSXRV9KSingleRpConnectionProvider from unicon.bases.routers.connection import BaseSingleRpConnection class IOSXRV9KSingleRpConnection(BaseSingleRpConnection): os = 'iosxr' - series = 'iosxrv9k' + platform = 'iosxrv9k' chassis_type = 'single_rp' state_machine_class = IOSXRSingleRpStateMachine connection_provider_class = IOSXRV9KSingleRpConnectionProvider diff --git a/src/unicon/plugins/iosxr/iosxrv9k/connection_provider.py b/src/unicon/plugins/iosxr/iosxrv9k/connection_provider.py index 37170660..f1f6c240 100755 --- a/src/unicon/plugins/iosxr/iosxrv9k/connection_provider.py +++ b/src/unicon/plugins/iosxr/iosxrv9k/connection_provider.py @@ -54,7 +54,6 @@ def init_handle(self): after bringing to enable state """ con = self.connection - con._is_connected = True con.state_machine.go_to('enable', self.connection.spawn, context=self.connection.context, @@ -65,6 +64,7 @@ def init_handle(self): def establish_connection(self): con = self.connection settings = con.settings + learn_hostname = con.learn_hostname self.wait_for_launch_complete( initial_discovery_wait_sec = \ @@ -72,7 +72,9 @@ def establish_connection(self): initial_wait_sec = settings.INITIAL_LAUNCH_WAIT_SEC, post_prompt_wait_sec = settings.POST_PROMPT_WAIT_SEC, connection = con, log=con.log, hostname=con.hostname, - checkpoint_pattern=patterns.logout_prompt) + checkpoint_pattern=patterns.logout_prompt, + learn_hostname=learn_hostname + ) super().establish_connection() """ diff --git a/src/unicon/plugins/iosxr/moonshine/__init__.py b/src/unicon/plugins/iosxr/moonshine/__init__.py index 72474143..f566d6b3 100755 --- a/src/unicon/plugins/iosxr/moonshine/__init__.py +++ b/src/unicon/plugins/iosxr/moonshine/__init__.py @@ -2,13 +2,13 @@ from unicon.plugins.iosxr.moonshine.settings import MoonshineSettings from unicon.plugins.iosxr.moonshine.statemachine import MoonshineSingleRpStateMachine, MoonshineDualRpStateMachine -from unicon.plugins.iosxr.__init__ import IOSXRServiceList, IOSXRHAServiceList, IOSXRSingleRpConnection, IOSXRDualRpConnection +from unicon.plugins.iosxr import IOSXRServiceList, IOSXRHAServiceList, IOSXRSingleRpConnection, IOSXRDualRpConnection from unicon.plugins.iosxr.moonshine.connection_provider import MoonshineSingleRpConnectionProvider, MoonshineDualRpConnectionProvider from unicon.plugins.iosxr.moonshine.pty_backend import MoonshineSpawn class MoonshineSingleRpConnection(IOSXRSingleRpConnection): os = 'iosxr' - series = 'moonshine' + platform = 'moonshine' chassis_type = 'single_rp' state_machine_class = MoonshineSingleRpStateMachine connection_provider_class = MoonshineSingleRpConnectionProvider @@ -21,9 +21,10 @@ def setup_connection(self): # Spawn a connection to the device self.spawn = MoonshineSpawn(self.parse_spawn_command(self.start[0]), + target='{}'.format(self.hostname), + hostname=self.hostname, settings=self.settings, - log=self.log, - logfile=self.logfile) + logger=self.log) # Instantiate connection provider self.connection_provider = self.connection_provider_class(self) @@ -31,7 +32,7 @@ def setup_connection(self): class MoonshineDualRpConnection(IOSXRDualRpConnection): os = 'iosxr' - series = 'moonshine' + platform = 'moonshine' chassis_type = 'dual_rp' state_machine_class = MoonshineDualRpStateMachine connection_provider_class = MoonshineDualRpConnectionProvider @@ -44,14 +45,16 @@ def setup_connection(self): """ # Spawn each handle - self.a.spawn = MoonshineSpawn(self.parse_spawn_command(self.a.start), + self.a.spawn = MoonshineSpawn(self.parse_spawn_command(self.a.start[0]), + target='{}.a'.format(self.hostname), + hostname=self.hostname, settings=self.settings, - log=self.log, - logfile=self.logfile) - self.b.spawn = MoonshineSpawn(self.parse_spawn_command(self.b.start), + logger=self.log) + self.b.spawn = MoonshineSpawn(self.parse_spawn_command(self.b.start[0]), + target='{}.b'.format(self.hostname), + hostname=self.hostname, settings=self.settings, - log=self.log, - logfile=self.logfile) + logger=self.log) # Instantiate connection provider self.connection_provider = self.connection_provider_class(self) diff --git a/src/unicon/plugins/iosxr/moonshine/connection_provider.py b/src/unicon/plugins/iosxr/moonshine/connection_provider.py index cbb0556e..6d5046cf 100755 --- a/src/unicon/plugins/iosxr/moonshine/connection_provider.py +++ b/src/unicon/plugins/iosxr/moonshine/connection_provider.py @@ -10,6 +10,7 @@ from unicon.plugins.iosxr.moonshine.patterns import MoonshinePatterns from unicon.plugins.iosxr.errors import RpNotRunningError from unicon.eal.dialogs import Dialog +from unicon.plugins.generic.connection_provider import GenericDualRpConnectionProvider patterns = MoonshinePatterns() @@ -37,7 +38,6 @@ def init_handle(self): """ Executes the init commands on the device after bringing it to enable state """ con = self.connection - con._is_connected = True con.state_machine.go_to('enable', self.connection.spawn, context=self.connection.context, @@ -45,7 +45,11 @@ def init_handle(self): self.execute_init_commands() -class MoonshineDualRpConnectionProvider(IOSXRDualRpConnectionProvider): +class MoonshineDualRpConnectionProvider(GenericDualRpConnectionProvider): + # This class inherits from GenericDualRpConnectionProvider instead + # of IOSXRDualRpConnectionProvider because we want to use the + # generic `designate_handles` method. + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -65,3 +69,8 @@ def set_init_commands(self): hostname_command = ['hostname ' + con.hostname] self.init_config_commands = hostname_command + con.settings.MOONSHINE_INIT_CONFIG_COMMANDS + def unlock_standby(self): + pass + + def assign_ha_mode(self): + pass diff --git a/src/unicon/plugins/iosxr/moonshine/patterns.py b/src/unicon/plugins/iosxr/moonshine/patterns.py index f0e418c4..97e63f45 100755 --- a/src/unicon/plugins/iosxr/moonshine/patterns.py +++ b/src/unicon/plugins/iosxr/moonshine/patterns.py @@ -6,6 +6,6 @@ class MoonshinePatterns(IOSXRPatterns): def __init__(self): super().__init__() - self.shell_prompt = r'^(.*)%N.[0-9][1-9]*/[0-9][1-9]*/CPU[0-9][1-9]*\.*[0|1]*/*\s?#.*$' + self.shell_prompt = r'^(.*)%N.[0-9][1-9]*/[0-9][1-9]*/CPU[0-9][1-9]*\.*[0|1]*(\x1b\S+)?/*\s?[#\$].*$' self.enable_prompt = r'^(.*)RP/[0-9][1-9]*/[0-9][1-9]*/CPU[0-9][1-9]*:[a-zA-Z0-9_.{}+-]+#.*$' self.config_prompt = r'^(.*)RP/[0-9][1-9]*/[0-9][1-9]*/CPU[0-9][1-9]*:[a-zA-Z0-9_.{}+-]+\(config.*\)#.*$' diff --git a/src/unicon/plugins/iosxr/moonshine/pty_backend.py b/src/unicon/plugins/iosxr/moonshine/pty_backend.py index 763e178b..7108a9e2 100644 --- a/src/unicon/plugins/iosxr/moonshine/pty_backend.py +++ b/src/unicon/plugins/iosxr/moonshine/pty_backend.py @@ -1,7 +1,5 @@ import os -from unicon.eal.utils import send_message_logging from unicon.eal.backend.pty_backend import Spawn -from unicon import logs class MoonshineSpawn(Spawn): def send(self, command, *args, **kwargs): @@ -9,10 +7,9 @@ def send(self, command, *args, **kwargs): if not isinstance(command, str): command = str(command) if self.is_writable(): - if logs.IS_EXPECT_LOG_ENABLED: - message = "Expect Sending :: " + repr(command) - log_info = {'color': 'blue'} - send_message_logging(message, log_info) + msg = ">>> Unicon Sending :: {}".format(repr(command)) + self.log.debug(msg) + self.last_sent = command ret = os.write(self.fd, str.encode(command)) return ret diff --git a/src/unicon/plugins/iosxr/moonshine/statemachine.py b/src/unicon/plugins/iosxr/moonshine/statemachine.py index 48a07979..3e2952e1 100755 --- a/src/unicon/plugins/iosxr/moonshine/statemachine.py +++ b/src/unicon/plugins/iosxr/moonshine/statemachine.py @@ -1,6 +1,7 @@ __author__ = "Isobel Ormiston " from unicon.plugins.iosxr.statemachine import IOSXRSingleRpStateMachine +from unicon.plugins.iosxr.statements import handle_failed_config from unicon.plugins.iosxr.moonshine.patterns import MoonshinePatterns from unicon.plugins.iosxr.moonshine.statements import MoonshineStatements from unicon.statemachine import State, Path @@ -29,8 +30,7 @@ def create(self): config_dialog = Dialog([ [patterns.commit_changes_prompt, 'sendline(yes)', None, True, False], [patterns.commit_replace_prompt, 'sendline(yes)', None, True, False], - [patterns.configuration_failed_message, self.handle_failed_config, - None, True, False] + [patterns.configuration_failed_message, handle_failed_config, None, True, False] ]) shell_to_enable = Path(shell, enable, 'exec', None) diff --git a/src/unicon/plugins/iosxr/moonshine/tests/config_test.py b/src/unicon/plugins/iosxr/moonshine/tests/config_test.py index 573fe5d9..484e745a 100755 --- a/src/unicon/plugins/iosxr/moonshine/tests/config_test.py +++ b/src/unicon/plugins/iosxr/moonshine/tests/config_test.py @@ -1,7 +1,7 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from pyats import aetest -from pyats.kleenex import BringUp +from pyats.bringup import BringUp import re, logging # NOTE: uut1 device must be Moonshine for this test to work. diff --git a/src/unicon/plugins/iosxr/moonshine/tests/standalone_ping_test.py b/src/unicon/plugins/iosxr/moonshine/tests/standalone_ping_test.py index a72549e5..3cc8c4ad 100755 --- a/src/unicon/plugins/iosxr/moonshine/tests/standalone_ping_test.py +++ b/src/unicon/plugins/iosxr/moonshine/tests/standalone_ping_test.py @@ -1,7 +1,7 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from pyats import aetest -from pyats.kleenex import BringUp +from pyats.bringup import BringUp class common_setup(aetest.CommonSetup): @aetest.subsection diff --git a/src/unicon/plugins/iosxr/ncs5k/__init__.py b/src/unicon/plugins/iosxr/ncs5k/__init__.py index d74f7118..fd6c200a 100644 --- a/src/unicon/plugins/iosxr/ncs5k/__init__.py +++ b/src/unicon/plugins/iosxr/ncs5k/__init__.py @@ -13,8 +13,6 @@ from .settings import NCS5KSettings - - class Ncs5kServiceList(IOSXRServiceList): def __init__(self): super().__init__() @@ -24,13 +22,12 @@ class Ncs5kHAServiceList(IOSXRHAServiceList): """ Generic dual rp services. """ def __init__(self): super().__init__() - self.reload = ncs_svc.Reload - + self.reload = ncs_svc.HAReload class Ncs5kSingleRpConnection(IOSXRSingleRpConnection): os = 'iosxr' - series = 'ncs5k' + platform = 'ncs5k' chassis_type = 'single_rp' state_machine_class = IOSXRSingleRpStateMachine connection_provider_class = IOSXRSingleRpConnectionProvider @@ -40,7 +37,7 @@ class Ncs5kSingleRpConnection(IOSXRSingleRpConnection): class Ncs5kDualRpConnection(IOSXRDualRpConnection): os = 'iosxr' - series = 'ncs5k' + platform = 'ncs5k' chassis_type = 'dual_rp' state_machine_class = IOSXRDualRpStateMachine connection_provider_class = IOSXRDualRpConnectionProvider diff --git a/src/unicon/plugins/iosxr/ncs5k/service_implementation.py b/src/unicon/plugins/iosxr/ncs5k/service_implementation.py index 66cbe51a..5949a151 100644 --- a/src/unicon/plugins/iosxr/ncs5k/service_implementation.py +++ b/src/unicon/plugins/iosxr/ncs5k/service_implementation.py @@ -10,10 +10,12 @@ import re from time import sleep +from datetime import datetime, timedelta from unicon.bases.routers.services import BaseService from unicon.core.errors import SubCommandFailure, TimeoutError from unicon.eal.dialogs import Dialog +from unicon.plugins.generic.statements import buffer_settled from .service_statements import reload_statement_list, reload_statement_list_vty @@ -46,7 +48,6 @@ def __init__(self, connection, context, **kwargs): super().__init__(connection, context, **kwargs) self.start_state = 'enable' self.end_state = 'enable' - self.service_name = 'reload' self.timeout = connection.settings.RELOAD_TIMEOUT self.dialog = Dialog(reload_statement_list) self.__dict__.update(kwargs) @@ -56,9 +57,28 @@ def call_service(self, dialog=Dialog([]), timeout=None, reload_creds=None, + error_pattern = None, + append_error_pattern= None, + raise_on_error=True, *args, **kwargs): + con = self.connection + self.context = con.context timeout = timeout or self.timeout + start_time = current_time = datetime.now() + timeout_time = timedelta(seconds=timeout) + + if error_pattern is None: + self.error_pattern = con.settings.ERROR_PATTERN + else: + self.error_pattern = error_pattern + + if not isinstance(self.error_pattern, list): + raise ValueError('error_pattern should be a list') + if append_error_pattern: + if not isinstance(append_error_pattern, list): + raise ValueError('append_error_pattern should be a list') + self.error_pattern += append_error_pattern fmt_msg = "+++ reloading %s " \ " with reload_command %s " \ @@ -97,10 +117,7 @@ def call_service(self, context=context) if self.result: self.result = self.result.match_output - con.state_machine.go_to('any', - con.spawn, - prompt_recovery=self.prompt_recovery, - context=self.context) + self.get_service_result() except Exception as err: raise SubCommandFailure("Reload failed %s" % err) @@ -109,6 +126,29 @@ def call_service(self, # only strip first newline and leave formatting intact output = re.sub(r"^\r?\r\n", "", output, 1) output = output.rstrip() + + # Bring standby to good state. + con.log.info('Reconnecting to device after reload') + wait_time = timedelta(seconds=con.settings.POST_RELOAD_WAIT) + settle_time = current_time = datetime.now() + con.disconnect() + while (current_time - settle_time) < wait_time: + try: + con.connect() + except Exception as e: + current_time = datetime.now() + if (current_time - settle_time) < wait_time: + con.log.info('Could not connect to device. Try again!') + continue + else: + if raise_on_error: + raise + else: + con.log.exception('Connection to {} failed'.format(con.hostname)) + self.result = False + else: + con.log.info('Connected to device after reload') + break else: con.log.warning('Did not detect a console session, will try to reconnect...') dialog = Dialog(reload_statement_list_vty) @@ -128,7 +168,7 @@ def call_service(self, pass con.log.warning('Disconnecting...') con.disconnect() - for x in range(3): + for x in range(con.settings.RELOAD_RECONNECT_ATTEMPTS): con.log.warning('Waiting for {} seconds'.format(con.settings.RELOAD_WAIT)) sleep(con.settings.RELOAD_WAIT) con.log.warning('Trying to connect... attempt #{}'.format(x+1)) @@ -136,10 +176,181 @@ def call_service(self, output += con.connect() except: con.log.warning('Connection failed') - if con.connected: + if con.is_connected: break - if not con.connected: + if not con.is_connected: raise SubCommandFailure('Reload failed - could not reconnect') self.result = output + +class HAReload(BaseService): + """Service to reload the device. + + Arguments: + reload_command: reload command to be issued. default is "reload" + dialog: Dialog which include list of Statements for + additional dialogs prompted by reload command, in-case + it is not in the current list. + timeout: Timeout value in sec, Default Value is 300 sec + reload_creds: name or list of names of credential(s) to use if + username or password is prompted for during reload. + + Returns: + True on Success, raise SubCommandFailure on failure + + Example :: + .. code-block:: python + + rtr.reload() + # If reload command is other than 'reload' + rtr.reload(reload_command="reload location all", timeout=400) + + """ + + def __init__(self, connection, context, **kwargs): + super().__init__(connection, context, **kwargs) + self.start_state = 'enable' + self.end_state = 'enable' + self.service_name = 'reload' + self.timeout = connection.settings.RELOAD_TIMEOUT + self.dialog = Dialog(reload_statement_list) + self.__dict__.update(kwargs) + + def call_service(self, + reload_command='reload', + dialog=Dialog([]), + target='active', + timeout=None, + reload_creds=None, + error_pattern = None, + append_error_pattern= None, + raise_on_error=True, + *args, **kwargs): + + con = self.connection + self.context = con.active.context + timeout = timeout or self.timeout + start_time = current_time = datetime.now() + timeout_time = timedelta(seconds=timeout) + + if error_pattern is None: + self.error_pattern = con.settings.ERROR_PATTERN + else: + self.error_pattern = error_pattern + + if not isinstance(self.error_pattern, list): + raise ValueError('error_pattern should be a list') + if append_error_pattern: + if not isinstance(append_error_pattern, list): + raise ValueError('append_error_pattern should be a list') + self.error_pattern += append_error_pattern + + fmt_msg = "+++ reloading %s " \ + " with reload_command %s " \ + "and timeout is %s +++" + con.log.debug(fmt_msg % (self.connection.hostname, + reload_command, + timeout)) + + con.active.state_machine.go_to(self.start_state, + con.active.spawn, + prompt_recovery=self.prompt_recovery, + context=self.context) + + if not isinstance(dialog, Dialog): + raise SubCommandFailure( + "dialog passed must be an instance of Dialog") + + show_terminal = con.execute('show terminal') + line_type = re.search(r"Line .*, Type \"(\w+)\"", show_terminal) + if line_type and line_type.groups(): + line_type = line_type.group(1) + + if reload_creds: + context = self.context.copy() + context.update(cred_list=reload_creds) + else: + context = self.context + + if line_type == 'Console': + dialog += self.dialog + con.active.spawn.sendline(reload_command) + try: + try: + self.result = dialog.process(con.active.spawn, + timeout=timeout, + prompt_recovery=self.prompt_recovery, + context=context) + if self.result: + self.result = self.result.match_output + self.get_service_result() + except Exception: + self.result = con.active.spawn.buffer + if 'is in standby' in self.result: + con.log.info('Timed out due to active/standby interchanged. Reconnecting...') + else: + con.log.info('Timed out. timeout might need to be increased. Reconnecting...') + con.disconnect() + original_connection_timeout = con.settings.CONNECTION_TIMEOUT + con.settings.CONNECTION_TIMEOUT = timeout + con.connect() + con.settings.CONNECTION_TIMEOUT = original_connection_timeout + # Bring standby to good state. + con.log.info('Reconnecting to device after reload') + wait_time = timedelta(seconds=con.settings.POST_RELOAD_WAIT) + settle_time = current_time = datetime.now() + con.disconnect() + while (current_time - settle_time) < wait_time: + try: + con.connect() + except Exception as e: + current_time = datetime.now() + if (current_time - settle_time) < wait_time: + con.log.info('Could not connect to device. Try again!') + continue + else: + if raise_on_error: + raise + else: + con.log.exception('Connection to {} failed'.format(con.hostname)) + self.result = False + else: + con.log.info('Connected to device after reload') + break + # Bring standby to good state. + con.log.info('Waiting for config sync to finish') + standby_wait_time = con.settings.POST_HA_RELOAD_CONFIG_SYNC_WAIT + standby_wait_interval = 50 + standby_sync_try = standby_wait_time // standby_wait_interval + 1 + for round in range(standby_sync_try): + con.standby.spawn.sendline() + try: + con.standby.state_machine.go_to( + 'any', + con.standby.spawn, + context=context, + timeout=standby_wait_interval, + prompt_recovery=self.prompt_recovery, + dialog=con.connection_provider.get_connection_dialog() + ) + break + except Exception as err: + if round == standby_sync_try - 1: + raise Exception( + 'Bringing standby to any state failed within {} sec' + .format(standby_wait_time)) from err + + except Exception as err: + raise SubCommandFailure("Reload failed %s" % err) + + output = self.result + + else: + raise Exception("Console is not used.") + if self.result: + con.log.info('--- Reload of device {} completed ---'.format(con.hostname)) + else: + con.log.info('--- Reload of device {} failed ---'.format(con.hostname)) + + self.result = output diff --git a/src/unicon/plugins/iosxr/ncs5k/service_statements.py b/src/unicon/plugins/iosxr/ncs5k/service_statements.py index e83cf2c9..8f69d4a8 100644 --- a/src/unicon/plugins/iosxr/ncs5k/service_statements.py +++ b/src/unicon/plugins/iosxr/ncs5k/service_statements.py @@ -11,6 +11,7 @@ secure_passwd_std, admin_password, auto_provision, login_stmt, send_response, password_handler) +from unicon.plugins.iosxr.service_statements import confirm_module_reload_stmt from .service_patterns import Ncs5kReloadPatterns @@ -58,6 +59,7 @@ login_stmt, password_stmt, press_enter, + confirm_module_reload_stmt, config_completed, # loop_continue=False ] diff --git a/src/unicon/plugins/iosxr/ncs5k/settings.py b/src/unicon/plugins/iosxr/ncs5k/settings.py index 3a31ed7e..0eea58c6 100644 --- a/src/unicon/plugins/iosxr/ncs5k/settings.py +++ b/src/unicon/plugins/iosxr/ncs5k/settings.py @@ -11,3 +11,8 @@ def __init__(self): # prompt wait retries self.ESCAPE_CHAR_PROMPT_WAIT_RETRIES = 3 + + # number of retries to reconnect after reloading + self.RELOAD_RECONNECT_ATTEMPTS = 3 + + self.STANDBY_STATE_REGEX = r'Standby node .* is (.*)' diff --git a/src/unicon/plugins/iosxr/patterns.py b/src/unicon/plugins/iosxr/patterns.py index 0e6eb514..8cb08414 100755 --- a/src/unicon/plugins/iosxr/patterns.py +++ b/src/unicon/plugins/iosxr/patterns.py @@ -8,11 +8,19 @@ class IOSXRPatterns(GenericPatterns): def __init__(self): super().__init__() - self.enable_prompt = r'^(.*?)RP/\d+/\S+/\S+\d+:(%N|ios|xr)\s?#\s?$' + self.enable_prompt = r'^(.*?)RP/\w+(/\S+)?/\S+\d+:(%N|ios|xr)\s?#\s?$' + + # [xr-vm_node0_RP1_CPU0:~]$ + # [xr-vm_node0_RSP1_CPU0:~]$ + # [xr-vm_nodeD0_CB0_CPU0:~]$ + # [node0_RP1_CPU0:~]$ + # # << this is a prompt, not a comment + self.run_prompt = r'^(.*?)(?:\[(xr-vm_)?nodeD?\d_(?:(?:RS?P|CB)[01]|[\d+])_CPU\d:(.*?)\]\s?\$\s?|[\r\n]+\s?#\s?)$' + # don't use hostname match in config prompt - hostname may be truncated # see CSCve48115 and CSCve51502 - self.run_prompt = r'^(.*?)(?:\[xr-vm_.*:([\s\S]+)?\]\s?\$\s?|[\r\n]+\s?#\s?)$' self.config_prompt = r'^(.*?)RP/\S+\(config.*\)\s?#\s?$' + self.exclusive_prompt = r'^(.*?)RP/\S+\(config.*\)#\s?$' self.telnet_prompt = r'^.*Escape character is.*' self.username_prompt = r'^.*([Uu]sername|[Ll]ogin):\s*$' self.password_prompt = r'^.*[Pp]assword:\s?$' @@ -21,10 +29,31 @@ def __init__(self): self.logout_prompt = r'^.*Press RETURN to get started\..*' self.commit_replace_prompt = r'Do you wish to proceed?.*$' self.admin_prompt = r'^(.*?)(?:sysadmin-vm:0_(.*)\s?#\s?$|RP/\S+\(admin\)\s?#\s?)$' - self.admin_conf_prompt = r'^(.*?)(?:sysadmin-vm:0_(.*)\(config.*\)\s?#\s?|RP/\S+\(admin-config\)\s?#\s?)$' + self.admin_conf_prompt = r'^(.*?)(?:sysadmin-vm:0_(.*)\(config.*\)\s?#\s?|RP/\S+\(admin-config(\S+)?\)\s?#\s?)$' self.admin_run_prompt = r'^(.*?)(?:\[sysadmin-vm:0_.*:([\s\S]+)?\]\s?\$\s?|[\r\n]+\s?#\s?)$' + # [host:0_RP0:~]$ + # [ios:~]$ + self.admin_host_prompt = r'^(.*?)(?:\[(host|ios):.*?\]\s?\$\s?)$' self.unreachable_prompt = r'apples are green but oranges are red' self.configuration_failed_message = r'^.*Please issue \'show configuration failed \[inheritance\].*[\r\n]*' self.standby_prompt = r'^.*This \(D\)RP Node is not ready or active for login \/configuration.*' self.rp_extract_status = r'^\d+\s+(\w+)\s+\-?\d+.*$' self.confirm_y_prompt = r"\[confirm( with only 'y' or 'n')?\]\s*\[y/n\].*$" + self.reload_module_prompt = r"^(.*)?Reload hardware module ? \[no,yes\].*$" + self.proceed_config_mode = r'Would you like to proceed in configuration mode\? \[no\]:\s*$' + + # when changing more_prompt, please also change plugins/iosxr/settings.py MORE_REPLACE_PATTERN + # ESC[7m--More--ESC[27m + # ESC[7m(END)ESC[27m + self.more_prompt = r'^.*(--\s?[Mm]ore\s?--|\(END\)).*$' + + # Brief='b', Detail='d', Protocol(IPv4/IPv6)='r' + # Brief='b', Detail='d', Protocol(IPv4/IPv6)='r'\x1b[K\r\n\x1b[K\r\n + # (General='g', IPv4 Uni='4u', IPv4 Multi='4m', IPv6 Uni='6u', IPv6 Multi='6m') + # This pattern does not end with $ on purpose as the prompt is part of the 'live' output + # and the output is updated frequently + self.monitor_prompt = r"^(.*?)(Brief='b', Detail='d', Protocol\(IPv4/IPv6\)='r'|\(General='g', IPv4 Uni='4u', IPv4 Multi='4m', IPv6 Uni='6u', IPv6 Multi='6m'\))(\x1b\S+[\r\n]+)*" + # r1 Monitor Time: 00:00:06 SysUptime: 15:48:49 + self.monitor_time_regex = r'(?P\S+).*?Monitor Time: (?P