From 5eb3ac1a76908b5f75df79e4aa02eb2c93231f29 Mon Sep 17 00:00:00 2001 From: Jeff Kala <48843785+jeffkala@users.noreply.github.com> Date: Thu, 27 Oct 2022 12:53:17 -0500 Subject: [PATCH 01/13] reprep 0.20.3 (#260) --- .github/workflows/ci.yml | 4 +- poetry.lock | 168 +++++++++++++++++---------------------- pyproject.toml | 6 +- tasks.py | 2 +- 4 files changed, 80 insertions(+), 100 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4466db2..16009661 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,7 +83,7 @@ jobs: strategy: fail-fast: true matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9"] # "3.10" add back after pyeapi new release. runs-on: "ubuntu-20.04" env: PYTHON_VER: "${{ matrix.python-version }}" @@ -156,7 +156,7 @@ jobs: strategy: fail-fast: true matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9"] # "3.10" add back after pyeapi new release. runs-on: "ubuntu-20.04" env: PYTHON_VER: "${{ matrix.python-version }}" diff --git a/poetry.lock b/poetry.lock index a035169f..d3b8371b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -133,11 +133,11 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" -version = "0.4.5" +version = "0.4.6" description = "Cross-platform colored terminal text." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" [[package]] name = "cryptography" @@ -160,15 +160,26 @@ test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0 [[package]] name = "dill" -version = "0.3.5.1" +version = "0.3.6" description = "serialize all of python" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" +python-versions = ">=3.7" [package.extras] graph = ["objgraph (>=1.7.2)"] +[[package]] +name = "exceptiongroup" +version = "1.0.0" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "f5-icontrol-rest" version = "1.3.13" @@ -253,7 +264,7 @@ typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\"" [[package]] name = "griffe" -version = "0.22.2" +version = "0.23.0" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." category = "dev" optional = false @@ -358,11 +369,11 @@ yamlordereddictloader = "*" [[package]] name = "lazy-object-proxy" -version = "1.7.1" +version = "1.8.0" description = "A fast and thorough lazy object proxy." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "lxml" @@ -469,11 +480,11 @@ pymdown-extensions = ">=9.4" [[package]] name = "mkdocs-material-extensions" -version = "1.0.3" -description = "Extension pack for Python Markdown." +version = "1.1" +description = "Extension pack for Python Markdown and MkDocs Material." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "mkdocs-version-annotations" @@ -629,7 +640,7 @@ python-versions = ">=3.7" [[package]] name = "pbr" -version = "5.10.0" +version = "5.11.0" description = "Python Build Reasonableness" category = "dev" optional = false @@ -662,14 +673,6 @@ importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - [[package]] name = "pycodestyle" version = "2.7.0" @@ -702,12 +705,11 @@ toml = ["toml"] [[package]] name = "pyeapi" -version = "0.8.4rc0" +version = "0.8.4" description = "Python Client for eAPI" category = "main" optional = false python-versions = "*" -develop = false [package.dependencies] netaddr = "*" @@ -716,12 +718,6 @@ netaddr = "*" dev = ["check-manifest", "pep8", "pyflakes", "twine"] test = ["coverage", "mock"] -[package.source] -type = "git" -url = "https://github.com/arista-eosplus/pyeapi.git" -reference = "develop" -resolved_reference = "236503162d1aa3ecc953678ec05380f1f605be02" - [[package]] name = "pyflakes" version = "2.3.1" @@ -764,7 +760,7 @@ testutil = ["gitpython (>3)"] [[package]] name = "pymdown-extensions" -version = "9.6" +version = "9.7" description = "Extension pack for Python Markdown." category = "dev" optional = false @@ -825,7 +821,7 @@ cp2110 = ["hidapi"] [[package]] name = "pytest" -version = "7.1.3" +version = "7.2.0" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -834,12 +830,12 @@ python-versions = ">=3.7" [package.dependencies] attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -tomli = ">=1.0.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] @@ -958,7 +954,7 @@ python-versions = "*" [[package]] name = "stevedore" -version = "3.5.1" +version = "3.5.2" description = "Manage dynamic plugins for Python applications" category = "dev" optional = false @@ -1096,7 +1092,7 @@ pyyaml = "*" [[package]] name = "zipp" -version = "3.9.0" +version = "3.10.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false @@ -1109,7 +1105,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "26b716d7c610fcc02f7563c0b6aca28f8b71bced544d33dae6a3438e638deb0f" +content-hash = "421e618173a8bc9b8467691de14cdd06a6eadef1d1e20e8e1e9555a3047f1146" [metadata.files] astroid = [ @@ -1253,8 +1249,8 @@ click = [ {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] cryptography = [ {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f"}, @@ -1285,8 +1281,12 @@ cryptography = [ {file = "cryptography-38.0.1.tar.gz", hash = "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7"}, ] dill = [ - {file = "dill-0.3.5.1-py2.py3-none-any.whl", hash = "sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302"}, - {file = "dill-0.3.5.1.tar.gz", hash = "sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86"}, + {file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"}, + {file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"}, +] +exceptiongroup = [ + {file = "exceptiongroup-1.0.0-py3-none-any.whl", hash = "sha256:2ac84b496be68464a2da60da518af3785fff8b7ec0d090a581604bc870bdee41"}, + {file = "exceptiongroup-1.0.0.tar.gz", hash = "sha256:affbabf13fb6e98988c38d9c5650e701569fe3c1de3233cfb61c5f33774690ad"}, ] f5-icontrol-rest = [ {file = "f5-icontrol-rest-1.3.13.tar.gz", hash = "sha256:49fffd999fb4971d6754beb0e066051175db9d9baeb8a76fca6c801dacc89359"}, @@ -1314,8 +1314,8 @@ gitpython = [ {file = "GitPython-3.1.29.tar.gz", hash = "sha256:cc36bfc4a3f913e66805a28e84703e419d9c264c1077e537b54f0e1af85dbefd"}, ] griffe = [ - {file = "griffe-0.22.2-py3-none-any.whl", hash = "sha256:cea5415ac6a92f4a22638e3f1f2e661402bac09fb8e8266936d67185a7e0d0fb"}, - {file = "griffe-0.22.2.tar.gz", hash = "sha256:1408e336a4155392bbd81eed9f2f44bf144e71b9c664e905630affe83bbc088e"}, + {file = "griffe-0.23.0-py3-none-any.whl", hash = "sha256:cfca5f523808109da3f8cfaa46e325fa2e5bef51120d1146e908c121b56475f0"}, + {file = "griffe-0.23.0.tar.gz", hash = "sha256:a639e2968c8e27f56ebcc57f869a03cea7ac7e7f5684bd2429c665f761c4e7bd"}, ] idna = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, @@ -1346,43 +1346,25 @@ junos-eznc = [ {file = "junos_eznc-2.6.5-py2.py3-none-any.whl", hash = "sha256:0036512d2dfc0e875a0698092eb05fa03e394ed6aa3b0350ce051ef765773d8f"}, ] lazy-object-proxy = [ - {file = "lazy-object-proxy-1.7.1.tar.gz", hash = "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-win32.whl", hash = "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win32.whl", hash = "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win32.whl", hash = "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-win32.whl", hash = "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-win32.whl", hash = "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, - {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, + {file = "lazy-object-proxy-1.8.0.tar.gz", hash = "sha256:c219a00245af0f6fa4e95901ed28044544f50152840c5b6a3e7b2568db34d156"}, + {file = "lazy_object_proxy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4fd031589121ad46e293629b39604031d354043bb5cdf83da4e93c2d7f3389fe"}, + {file = "lazy_object_proxy-1.8.0-cp310-cp310-win32.whl", hash = "sha256:b70d6e7a332eb0217e7872a73926ad4fdc14f846e85ad6749ad111084e76df25"}, + {file = "lazy_object_proxy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:eb329f8d8145379bf5dbe722182410fe8863d186e51bf034d2075eb8d85ee25b"}, + {file = "lazy_object_proxy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4e2d9f764f1befd8bdc97673261b8bb888764dfdbd7a4d8f55e4fbcabb8c3fb7"}, + {file = "lazy_object_proxy-1.8.0-cp311-cp311-win32.whl", hash = "sha256:e20bfa6db17a39c706d24f82df8352488d2943a3b7ce7d4c22579cb89ca8896e"}, + {file = "lazy_object_proxy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:14010b49a2f56ec4943b6cf925f597b534ee2fe1f0738c84b3bce0c1a11ff10d"}, + {file = "lazy_object_proxy-1.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6850e4aeca6d0df35bb06e05c8b934ff7c533734eb51d0ceb2d63696f1e6030c"}, + {file = "lazy_object_proxy-1.8.0-cp37-cp37m-win32.whl", hash = "sha256:5b51d6f3bfeb289dfd4e95de2ecd464cd51982fe6f00e2be1d0bf94864d58acd"}, + {file = "lazy_object_proxy-1.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6f593f26c470a379cf7f5bc6db6b5f1722353e7bf937b8d0d0b3fba911998858"}, + {file = "lazy_object_proxy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c1c7c0433154bb7c54185714c6929acc0ba04ee1b167314a779b9025517eada"}, + {file = "lazy_object_proxy-1.8.0-cp38-cp38-win32.whl", hash = "sha256:d176f392dbbdaacccf15919c77f526edf11a34aece58b55ab58539807b85436f"}, + {file = "lazy_object_proxy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:afcaa24e48bb23b3be31e329deb3f1858f1f1df86aea3d70cb5c8578bfe5261c"}, + {file = "lazy_object_proxy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:71d9ae8a82203511a6f60ca5a1b9f8ad201cac0fc75038b2dc5fa519589c9288"}, + {file = "lazy_object_proxy-1.8.0-cp39-cp39-win32.whl", hash = "sha256:8f6ce2118a90efa7f62dd38c7dbfffd42f468b180287b748626293bf12ed468f"}, + {file = "lazy_object_proxy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:eac3a9a5ef13b332c059772fd40b4b1c3d45a3a2b05e33a361dee48e54a4dad0"}, + {file = "lazy_object_proxy-1.8.0-pp37-pypy37_pp73-any.whl", hash = "sha256:ae032743794fba4d171b5b67310d69176287b5bf82a21f588282406a79498891"}, + {file = "lazy_object_proxy-1.8.0-pp38-pypy38_pp73-any.whl", hash = "sha256:7e1561626c49cb394268edd00501b289053a652ed762c58e1081224c8d881cec"}, + {file = "lazy_object_proxy-1.8.0-pp39-pypy39_pp73-any.whl", hash = "sha256:ce58b2b3734c73e68f0e30e4e725264d4d6be95818ec0a0be4bb6bf9a7e79aa8"}, ] lxml = [ {file = "lxml-4.9.1-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:98cafc618614d72b02185ac583c6f7796202062c41d2eeecdf07820bad3295ed"}, @@ -1523,8 +1505,8 @@ mkdocs-material = [ {file = "mkdocs_material-8.3.9-py2.py3-none-any.whl", hash = "sha256:263f2721f3abe533b61f7c8bed435a0462620912742c919821ac2d698b4bfe67"}, ] mkdocs-material-extensions = [ - {file = "mkdocs-material-extensions-1.0.3.tar.gz", hash = "sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2"}, - {file = "mkdocs_material_extensions-1.0.3-py3-none-any.whl", hash = "sha256:a82b70e533ce060b2a5d9eb2bc2e1be201cf61f901f93704b4acf6e3d5983a44"}, + {file = "mkdocs_material_extensions-1.1-py3-none-any.whl", hash = "sha256:bcc2e5fc70c0ec50e59703ee6e639d87c7e664c0c441c014ea84461a90f1e902"}, + {file = "mkdocs_material_extensions-1.1.tar.gz", hash = "sha256:96ca979dae66d65c2099eefe189b49d5ac62f76afb59c38e069ffc7cf3c131ec"}, ] mkdocs-version-annotations = [ {file = "mkdocs-version-annotations-1.0.0.tar.gz", hash = "sha256:6786024b37d27b330fda240b76ebec8e7ce48bd5a9d7a66e99804559d088dffa"}, @@ -1574,8 +1556,8 @@ pathspec = [ {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"}, ] pbr = [ - {file = "pbr-5.10.0-py2.py3-none-any.whl", hash = "sha256:da3e18aac0a3c003e9eea1a81bd23e5a3a75d745670dcf736317b7d966887fdf"}, - {file = "pbr-5.10.0.tar.gz", hash = "sha256:cfcc4ff8e698256fc17ea3ff796478b050852585aa5bae79ecd05b2ab7b39b9a"}, + {file = "pbr-5.11.0-py2.py3-none-any.whl", hash = "sha256:db2317ff07c84c4c63648c9064a79fe9d9f5c7ce85a9099d4b6258b3db83225a"}, + {file = "pbr-5.11.0.tar.gz", hash = "sha256:b97bc6695b2aff02144133c2e7399d5885223d42b7912ffaec2ca3898e673bfe"}, ] platformdirs = [ {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, @@ -1585,10 +1567,6 @@ pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] pycodestyle = [ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, @@ -1601,7 +1579,9 @@ pydocstyle = [ {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, ] -pyeapi = [] +pyeapi = [ + {file = "pyeapi-0.8.4.tar.gz", hash = "sha256:c33ad1eadd8ebac75f63488df9412081ce0b024c9e1da12a37196a5c60427c54"}, +] pyflakes = [ {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, @@ -1615,8 +1595,8 @@ pylint = [ {file = "pylint-2.13.9.tar.gz", hash = "sha256:095567c96e19e6f57b5b907e67d265ff535e588fe26b12b5ebe1fc5645b2c731"}, ] pymdown-extensions = [ - {file = "pymdown_extensions-9.6-py3-none-any.whl", hash = "sha256:1e36490adc7bfcef1fdb21bb0306e93af99cff8ec2db199bd17e3bf009768c11"}, - {file = "pymdown_extensions-9.6.tar.gz", hash = "sha256:b956b806439bbff10f726103a941266beb03fbe99f897c7d5e774d7170339ad9"}, + {file = "pymdown_extensions-9.7-py3-none-any.whl", hash = "sha256:767d07d9dead0f52f5135545c01f4ed627f9a7918ee86c646d893e24c59db87d"}, + {file = "pymdown_extensions-9.7.tar.gz", hash = "sha256:651b0107bc9ee790aedea3673cb88832c0af27d2569cf45c2de06f1d65292e96"}, ] pynacl = [ {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, @@ -1642,8 +1622,8 @@ pyserial = [ {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"}, ] pytest = [ - {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, - {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, + {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, + {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, ] python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, @@ -1724,8 +1704,8 @@ snowballstemmer = [ {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] stevedore = [ - {file = "stevedore-3.5.1-py3-none-any.whl", hash = "sha256:df36e6c003264de286d6e589994552d3254052e7fc6a117753d87c471f06de2a"}, - {file = "stevedore-3.5.1.tar.gz", hash = "sha256:1fecadf3d7805b940227f10e6a0140b202c9a24ba5c60cb539159046dc11e8d7"}, + {file = "stevedore-3.5.2-py3-none-any.whl", hash = "sha256:fa2630e3d0ad3e22d4914aff2501445815b9a4467a6edc49387c667a38faf5bf"}, + {file = "stevedore-3.5.2.tar.gz", hash = "sha256:cf99f41fc0d5a4f185ca4d3d42b03be9011b0a1ec1a4ea1a282be1b4b306dcc2"}, ] tenacity = [ {file = "tenacity-8.1.0-py3-none-any.whl", hash = "sha256:35525cd47f82830069f0d6b73f7eb83bc5b73ee2fff0437952cedf98b27653ac"}, @@ -1882,6 +1862,6 @@ yamlordereddictloader = [ {file = "yamlordereddictloader-0.4.0.tar.gz", hash = "sha256:7f30f0b99ea3f877f7cb340c570921fa9d639b7f69cba18be051e27f8de2080e"}, ] zipp = [ - {file = "zipp-3.9.0-py3-none-any.whl", hash = "sha256:972cfa31bc2fedd3fa838a51e9bc7e64b7fb725a8c00e7431554311f180e9980"}, - {file = "zipp-3.9.0.tar.gz", hash = "sha256:3a7af91c3db40ec72dd9d154ae18e008c69efe8ca88dde4f9a731bb82fe2f9eb"}, + {file = "zipp-3.10.0-py3-none-any.whl", hash = "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1"}, + {file = "zipp-3.10.0.tar.gz", hash = "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8"}, ] diff --git a/pyproject.toml b/pyproject.toml index 335c0f26..fac1ef73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ classifiers = [ "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", + # "Programming Language :: Python :: 3.10", ] include = [ "LICENSE", @@ -30,8 +30,8 @@ importlib-metadata = "4.13.0" f5-sdk = "^3.0.21" junos-eznc = "^2.6" netmiko = "^4.0" -# develop branch because these lines are required for py3.10 to work. https://github.com/arista-eosplus/pyeapi/blob/236503162d1aa3ecc953678ec05380f1f605be02/pyeapi/api/abstract.py#L44 -pyeapi = {git = "https://github.com/arista-eosplus/pyeapi.git", rev = "develop"} +# pyeapi doesn't support py3.10 yet in a release, and pypi doesn't allow direct dependencies. py3.10 to work. https://github.com/arista-eosplus/pyeapi/blob/236503162d1aa3ecc953678ec05380f1f605be02/pyeapi/api/abstract.py#L44 +pyeapi = "^0.8.4" pynxos = "^0.0.5" requests = "^2.28" scp = "^0.14" diff --git a/tasks.py b/tasks.py index cde3b90d..80df5dd4 100644 --- a/tasks.py +++ b/tasks.py @@ -31,7 +31,7 @@ def is_truthy(arg): TOOL_CONFIG = PYPROJECT_CONFIG["tool"]["poetry"] # Can be set to a separate Python version to be used for launching or building image -PYTHON_VER = os.getenv("PYTHON_VER", "3.10") +PYTHON_VER = os.getenv("PYTHON_VER", "3.8") # Name of the docker image/image IMAGE_NAME = os.getenv("IMAGE_NAME", TOOL_CONFIG["name"]) # Tag for the image From 47e6db99eea1df71c4f40f0409a506dd131bd3b7 Mon Sep 17 00:00:00 2001 From: Armen Martirosyan Date: Sun, 30 Oct 2022 21:28:02 -0700 Subject: [PATCH 02/13] Fixes issue #213 --- pyntc/devices/ios_device.py | 58 ++++++++-- tasks.py | 4 +- .../device_mocks/ios/dir_flash:.txt | 2 +- tests/unit/test_devices/test_ios_device.py | 106 +++++++++++++----- 4 files changed, 126 insertions(+), 44 deletions(-) diff --git a/pyntc/devices/ios_device.py b/pyntc/devices/ios_device.py index 06fd5c2a..805c7d8e 100644 --- a/pyntc/devices/ios_device.py +++ b/pyntc/devices/ios_device.py @@ -258,8 +258,7 @@ def boot_options(self): except CommandError: # Default to running config value show_boot_out = self.show("show run | inc boot") - boot_path_regex = r"boot\s+system\s+(?:\S+\s+|)(\S+)\s*$" - + boot_path_regex = r"boot\ssystem\s\S+(?::+|\s)(\S+.bin)" match = re.search(boot_path_regex, show_boot_out, re.MULTILINE) if match: boot_path_tuple = match.groups() @@ -710,7 +709,10 @@ def install_os(self, image_name, install_mode=False, install_mode_delay_factor=2 self.show(command, delay_factor=install_mode_delay_factor) except IOError: pass - + except CommandError: + command = f"request platform software package install switch all file {self._get_file_system()}{image_name} auto-copy" + self.show(command, delay_factor=install_mode_delay_factor) + self.reboot() else: self.set_boot_options(image_name, **vendor_specifics) self.reboot() @@ -721,7 +723,7 @@ def install_os(self, image_name, install_mode=False, install_mode_delay_factor=2 # Set FastCLI back to originally set when using install mode if install_mode: self.fast_cli = current_fast_cli - + image_name = INSTALL_MODE_FILE_NAME # Verify the OS level if not self._image_booted(image_name): raise OSInstallError(hostname=self.hostname, desired_boot=image_name) @@ -964,20 +966,54 @@ def set_boot_options(self, image_name, **vendor_specifics): CommandError: Error if setting new image as boot variable fails. """ file_system = vendor_specifics.get("file_system") + command = "boot system flash" if file_system is None: file_system = self._get_file_system() file_system_files = self.show("dir {0}".format(file_system)) if re.search(image_name, file_system_files) is None: raise NTCFileNotFoundError(hostname=self.hostname, file=image_name, dir=file_system) - - try: - command = "boot system {0}/{1}".format(file_system, image_name) - self.config(["no boot system", command]) - except CommandListError: # TODO: Update to CommandError when deprecating config_list - file_system = file_system.replace(":", "") - command = "boot system {0} {1}".format(file_system, image_name) + if image_name == "packages.conf": + command = "boot system {0}{1}".format(file_system, image_name) self.config(["no boot system", command]) + else: + show_boot_sys = self.show("show run | include boot system") + # Sample: + # boot system flash:/c3560-advipservicesk9-mz.122-44.SE.bin + # boot system flash0:/c3560-advipservicesk9-mz.122-44.SE.bin + if re.search(r"boot\ssystem\s\S+\:\/\S+", show_boot_sys): + command = "boot system {0}/{1}".format(file_system, image_name) + self.config(["no boot system", command]) + # Sample: + # boot system flash:c3560-advipservicesk9-mz.122-44.SE.bin + # boot system flash0:c3560-advipservicesk9-mz.122-44.SE.bin + elif re.search(r"boot\ssystem\s\S+\:\S+", show_boot_sys): + command = "boot system {0}{1}".format(file_system, image_name) + self.config(["no boot system", command]) + # Sample: + # boot system flash flash:c3560-advipservicesk9-mz.122-44.SE.bin + # boot system flash flash0:c3560-advipservicesk9-mz.122-44.SE.bin + # boot system flash bootflash:c3560-advipservicesk9-mz.122-44.SE.bin + elif re.search( + r"boot\ssystem\s\S+\s\S+:\S+", show_boot_sys + ): # TODO: Update to CommandError when deprecating config_list + command = "boot system flash {0}{1}".format(file_system, image_name) + self.config(["no boot system", command]) + # Sample: + # boot system flash c3560-advipservicesk9-mz.122-44.SE.bin + elif re.search( + r"boot\ssystem\sflash\s\S+", show_boot_sys + ): # TODO: Update to CommandError when deprecating config_list + file_system = file_system.replace(":", "") + command = "boot system {0} {1}".format(file_system, image_name) + self.config(["no boot system", command]) + else: + raise CommandError( + command=command, + message="Unable to determine the boot system configuration syntax. Current config is {0}".format( + show_boot_sys + ), + ) self.save() new_boot_options = self.boot_options["sys"] diff --git a/tasks.py b/tasks.py index 80df5dd4..9cd7d718 100644 --- a/tasks.py +++ b/tasks.py @@ -109,9 +109,9 @@ def rebuild(context): @task(help={"local": "Run locally or within the Docker container"}) -def pytest(context, local=INVOKE_LOCAL): +def pytest(context, local=INVOKE_LOCAL, args=""): """Run pytest test cases.""" - exec_cmd = "pytest" + exec_cmd = f"pytest {args}" run_cmd(context, exec_cmd, local) diff --git a/tests/unit/test_devices/device_mocks/ios/dir_flash:.txt b/tests/unit/test_devices/device_mocks/ios/dir_flash:.txt index 33d8621e..f009a37a 100644 --- a/tests/unit/test_devices/device_mocks/ios/dir_flash:.txt +++ b/tests/unit/test_devices/device_mocks/ios/dir_flash:.txt @@ -1,3 +1,3 @@ - -rw- 15183868 c3560-advipservicesk9-mz.122-44.SE + -rw- 15183868 c3560-advipservicesk9-mz.122-44.SE.bin 16777216 bytes total (1592488 bytes free) \ No newline at end of file diff --git a/tests/unit/test_devices/test_ios_device.py b/tests/unit/test_devices/test_ios_device.py index 343ae006..cb0259db 100644 --- a/tests/unit/test_devices/test_ios_device.py +++ b/tests/unit/test_devices/test_ios_device.py @@ -10,7 +10,7 @@ from pyntc.devices import ios_device as ios_module -BOOT_IMAGE = "c3560-advipservicesk9-mz.122-44.SE" +BOOT_IMAGE = "c3560-advipservicesk9-mz.122-44.SE.bin" BOOT_OPTIONS_PATH = "pyntc.devices.ios_device.IOSDevice.boot_options" DEVICE_FACTS = { "version": "15.1(3)T4", @@ -224,9 +224,8 @@ def test_boot_options_show_bootvar(self, mock_boot): def test_boot_options_show_run(self, mock_boot): results = [ ios_module.CommandError("show bootvar", "fail"), - ios_module.CommandError("show bootvar", "fail"), - f"boot system flash bootflash:/{BOOT_IMAGE}", - "Directory of bootflash:/", + ios_module.CommandError("show boot", "fail"), + "boot system flash:c3560-advipservicesk9-mz.122-44.SE.bin", ] self.device.native.send_command.side_effect = results boot_options = self.device.boot_options @@ -818,11 +817,11 @@ def test_send_command_timing(ios_native_send_command_timing): @mock.patch.object(IOSDevice, "config") @mock.patch.object(IOSDevice, "boot_options", new_callable=mock.PropertyMock) @mock.patch.object(IOSDevice, "save") -def test_set_boot_options(mock_save, mock_boot_options, mock_config, mock_file_system, ios_show): - device = ios_show(["dir_flash:.txt"]) +def test_set_boot_options_pass_standard(mock_save, mock_boot_options, mock_config, mock_file_system, ios_show): + device = ios_show(["dir_flash:.txt", "boot system flash:c3560-advipservicesk9-mz.122-44.SE.bin"]) mock_boot_options.return_value = {"sys": BOOT_IMAGE} device.set_boot_options(BOOT_IMAGE) - mock_config.assert_called_with(["no boot system", f"boot system flash:/{BOOT_IMAGE}"]) + mock_config.assert_called_with(["no boot system", f"boot system flash:{BOOT_IMAGE}"]) mock_file_system.assert_called_once() mock_config.assert_called_once() mock_save.assert_called_once() @@ -833,8 +832,8 @@ def test_set_boot_options(mock_save, mock_boot_options, mock_config, mock_file_s @mock.patch.object(IOSDevice, "config") @mock.patch.object(IOSDevice, "boot_options", new_callable=mock.PropertyMock) @mock.patch.object(IOSDevice, "save") -def test_set_boot_options_pass_file_system(mock_save, mock_boot_options, mock_config, mock_file_system, ios_show): - device = ios_show(["dir_flash:.txt"]) +def test_set_boot_options_pass_backslash(mock_save, mock_boot_options, mock_config, mock_file_system, ios_show): + device = ios_show(["dir_flash:.txt", "boot system flash:/c3560-advipservicesk9-mz.122-44.SE.bin"]) mock_boot_options.return_value = {"sys": BOOT_IMAGE} device.set_boot_options(BOOT_IMAGE, file_system="flash:") mock_config.assert_called_with(["no boot system", f"boot system flash:/{BOOT_IMAGE}"]) @@ -844,28 +843,76 @@ def test_set_boot_options_pass_file_system(mock_save, mock_boot_options, mock_co mock_boot_options.assert_called_once() +@mock.patch.object(IOSDevice, "_get_file_system") @mock.patch.object(IOSDevice, "config") @mock.patch.object(IOSDevice, "boot_options", new_callable=mock.PropertyMock) @mock.patch.object(IOSDevice, "save") -def test_set_boot_options_with_spaces(mock_save, mock_boot_options, mock_config, ios_show): - device = ios_show(["dir_flash:.txt"]) - mock_config.side_effect = [ - ios_module.CommandListError( - ["no boot system", "invalid boot command"], - "invalid boot command", - r"% Invalid command", - ), - "valid boot command", - ] +def test_set_boot_options_pass_double_flash(mock_save, mock_boot_options, mock_config, mock_file_system, ios_show): + device = ios_show(["dir_flash:.txt", "boot system flash flash:c3560-advipservicesk9-mz.122-44.SE.bin"]) mock_boot_options.return_value = {"sys": BOOT_IMAGE} device.set_boot_options(BOOT_IMAGE, file_system="flash:") - mock_config.assert_has_calls( - [ - mock.call(["no boot system", f"boot system flash:/{BOOT_IMAGE}"]), - mock.call(["no boot system", f"boot system flash {BOOT_IMAGE}"]), - ] - ) + mock_config.assert_called_with(["no boot system", f"boot system flash flash:{BOOT_IMAGE}"]) + mock_file_system.assert_not_called() + mock_config.assert_called_once() mock_save.assert_called_once() + mock_boot_options.assert_called_once() + + +@mock.patch.object(IOSDevice, "_get_file_system") +@mock.patch.object(IOSDevice, "config") +@mock.patch.object(IOSDevice, "boot_options", new_callable=mock.PropertyMock) +@mock.patch.object(IOSDevice, "save") +def test_set_boot_options_pass_with_space(mock_save, mock_boot_options, mock_config, mock_file_system, ios_show): + device = ios_show(["dir_flash:.txt", "boot system flash c3560-advipservicesk9-mz.122-44.SE.bin"]) + mock_boot_options.return_value = {"sys": BOOT_IMAGE} + device.set_boot_options(BOOT_IMAGE, file_system="flash:") + mock_config.assert_called_with(["no boot system", f"boot system flash {BOOT_IMAGE}"]) + mock_file_system.assert_not_called() + mock_config.assert_called_once() + mock_save.assert_called_once() + mock_boot_options.assert_called_once() + + +@mock.patch.object(IOSDevice, "_get_file_system") +@mock.patch.object(IOSDevice, "config") +@mock.patch.object(IOSDevice, "boot_options", new_callable=mock.PropertyMock) +@mock.patch.object(IOSDevice, "save") +def test_set_boot_options_raise_commanderror(mock_save, mock_boot_options, mock_config, mock_file_system, ios_show): + device = ios_show(["dir_flash:.txt", "boot flash:c3560-advipservicesk9-mz.122-44.SE.bin"]) + with pytest.raises(ios_module.CommandError) as err: + device.set_boot_options(BOOT_IMAGE, file_system="flash:") + + assert ( + err.value.message + == "Command boot system flash was not successful: Unable to determine the boot system configuration syntax. Current config is {0}".format( + "boot flash:c3560-advipservicesk9-mz.122-44.SE.bin" + ) + ) + mock_config.assert_not_called() + + +# @mock.patch.object(IOSDevice, "config") +# @mock.patch.object(IOSDevice, "boot_options", new_callable=mock.PropertyMock) +# @mock.patch.object(IOSDevice, "save") +# def test_set_boot_options_with_spaces(mock_save, mock_boot_options, mock_config, ios_show): +# device = ios_show(["dir_flash:.txt", ""]) +# mock_config.side_effect = [ +# ios_module.CommandListError( +# ["no boot system", "invalid boot command"], +# "invalid boot command", +# r"% Invalid command", +# ), +# "valid boot command", +# ] +# mock_boot_options.return_value = {"sys": BOOT_IMAGE} +# device.set_boot_options(BOOT_IMAGE, file_system="flash:") +# mock_config.assert_has_calls( +# [ +# mock.call(["no boot system", f"boot system flash:/{BOOT_IMAGE}"]), +# mock.call(["no boot system", f"boot system flash {BOOT_IMAGE}"]), +# ] +# ) +# mock_save.assert_called_once() @mock.patch.object(IOSDevice, "hostname", new_callable=mock.PropertyMock) @@ -887,11 +934,11 @@ def test_set_boot_options_no_file(mock_hostname, ios_show): def test_set_boot_options_bad_boot(mock_save, mock_config, mock_boot_options, ios_show): bad_image = "bad_image.bin" mock_boot_options.return_value = {"sys": bad_image} - device = ios_show(["dir_flash:.txt"]) + device = ios_show(["dir_flash:.txt", "boot system flash:c3560-advipservicesk9-mz.122-44.SE.bin"]) with pytest.raises(ios_module.CommandError) as err: device.set_boot_options(BOOT_IMAGE, file_system="flash:") - assert err.value.command == f"boot system flash:/{BOOT_IMAGE}" + assert err.value.command == f"boot system flash:{BOOT_IMAGE}" assert err.value.cli_error_msg == f"Setting boot command did not yield expected results, found {bad_image}" @@ -973,7 +1020,7 @@ def test_install_os_install_mode_failed( with pytest.raises(ios_module.OSInstallError) as err: ios_device.install_os(image_name, install_mode=True) - assert err.value.message == "ntc-rtr01 was unable to boot into cat9k_iosxe.16.12.04.SPA.bin" + assert err.value.message == "ntc-rtr01 was unable to boot into packages.conf" # Check the results mock_set_boot_options.assert_called_with("packages.conf") @@ -1097,7 +1144,7 @@ def test_install_os_install_mode_from_everest_failed( with pytest.raises(ios_module.OSInstallError) as err: ios_device.install_os(image_name, install_mode=True) - assert err.value.message == "ntc-rtr01 was unable to boot into cat9k_iosxe.16.12.04.SPA.bin" + assert err.value.message == "ntc-rtr01 was unable to boot into packages.conf" # Test the results mock_set_boot_options.assert_called_with("packages.conf") @@ -1254,7 +1301,6 @@ def test_show_list(ios_native_send_command): def test_vlans(mock_show_vlan, mock_model, ios_show): mock_model.return_value = "WS-3750" device = ios_show(["show_vlan.txt"]) - print(device) mock_show_vlan.return_value = [{"vlan_id": "1"}, {"vlan_id": "2"}, {"vlan_id": "3"}, {"vlan_id": "4"}] assert device.vlans == ["1", "2", "3", "4"] From 19ed875d06a08fcef107e28703513568c147c3e6 Mon Sep 17 00:00:00 2001 From: Antonio Balmaseda <68010787+balmasea@users.noreply.github.com> Date: Fri, 16 Dec 2022 16:15:07 +0100 Subject: [PATCH 03/13] Bugfix #263 (#264) * skipping exception raising when upgrade is in install mode and activate command is being sent * typo * allowing set boot to continue when package.conf does not exist * reverting boolean usage * black * adding test for packages.conf file * black linting * adding missin slash --- pyntc/devices/ios_device.py | 2 +- tests/unit/test_devices/test_ios_device.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pyntc/devices/ios_device.py b/pyntc/devices/ios_device.py index 805c7d8e..123a9da1 100644 --- a/pyntc/devices/ios_device.py +++ b/pyntc/devices/ios_device.py @@ -971,7 +971,7 @@ def set_boot_options(self, image_name, **vendor_specifics): file_system = self._get_file_system() file_system_files = self.show("dir {0}".format(file_system)) - if re.search(image_name, file_system_files) is None: + if image_name != INSTALL_MODE_FILE_NAME and re.search(image_name, file_system_files) is None: raise NTCFileNotFoundError(hostname=self.hostname, file=image_name, dir=file_system) if image_name == "packages.conf": command = "boot system {0}{1}".format(file_system, image_name) diff --git a/tests/unit/test_devices/test_ios_device.py b/tests/unit/test_devices/test_ios_device.py index cb0259db..6b72a313 100644 --- a/tests/unit/test_devices/test_ios_device.py +++ b/tests/unit/test_devices/test_ios_device.py @@ -942,6 +942,23 @@ def test_set_boot_options_bad_boot(mock_save, mock_config, mock_boot_options, io assert err.value.cli_error_msg == f"Setting boot command did not yield expected results, found {bad_image}" +@mock.patch.object(IOSDevice, "_get_file_system") +@mock.patch.object(IOSDevice, "config") +@mock.patch.object(IOSDevice, "boot_options", new_callable=mock.PropertyMock) +@mock.patch.object(IOSDevice, "save") +def test_set_boot_options_image_packages_conf_file( + mock_save, mock_boot_options, mock_config, mock_file_system, ios_show +): + device = ios_show(["dir_flash:.txt"]) + mock_boot_options.return_value = {"sys": ios_module.INSTALL_MODE_FILE_NAME} + device.set_boot_options(ios_module.INSTALL_MODE_FILE_NAME, file_system="flash:/") + mock_config.assert_called_with(["no boot system", f"boot system flash:/{ios_module.INSTALL_MODE_FILE_NAME}"]) + mock_file_system.assert_not_called() + mock_config.assert_called_once() + mock_save.assert_called_once() + mock_boot_options.assert_called_once() + + # # TESTS FOR IOS INSTALL MODE METHOD # From 6f2a9a5719f1da28e06d734617c888263852d55e Mon Sep 17 00:00:00 2001 From: Jeff Kala <48843785+jeffkala@users.noreply.github.com> Date: Tue, 20 Dec 2022 07:44:22 -0700 Subject: [PATCH 04/13] deprecate _list methods in asa and ios (#269) --- pyntc/devices/asa_device.py | 79 +++++++++++----------- pyntc/devices/ios_device.py | 37 +++++----- tests/unit/test_devices/test_asa_device.py | 6 +- 3 files changed, 58 insertions(+), 64 deletions(-) diff --git a/pyntc/devices/asa_device.py b/pyntc/devices/asa_device.py index cdfcd45d..ae2f98d2 100644 --- a/pyntc/devices/asa_device.py +++ b/pyntc/devices/asa_device.py @@ -354,35 +354,37 @@ def close(self): self._connected = False def config(self, command): - """ - Send single command to device. + """Send configuration commands to a device. Args: - command (str): command to be sent to device. + commands (str, list): String with single command, or list with multiple commands. + + Raises: + CommandListError: Message stating which command failed and the response from the device. """ self._enter_config() - self._send_command(command) + if isinstance(command, list): + entered_commands = [] + for command_instance in command: + entered_commands.append(command_instance) + try: + self._send_command(command_instance) + except CommandError as e: + raise CommandListError(entered_commands, command_instance, e.cli_error_msg) + else: + self._send_command(command) self.native.exit_config_mode() def config_list(self, commands): - """ - Send list of commands to device. + """Send configuration commands in list format to a device. - Args: - commands (list): list of commands to be set to device. + DEPRECATED - Use the `config` method. - Raises: - CommandListError: Message stating which command failed and the response from the device. + Args: + commands (list): List with multiple commands. """ - self._enter_config() - entered_commands = [] - for command in commands: - entered_commands.append(command) - try: - self._send_command(command) - except CommandError as e: - raise CommandListError(entered_commands, command, e.cli_error_msg) - self.native.exit_config_mode() + warnings.warn("config_list() is deprecated; use config().", DeprecationWarning) + self.config(commands) @property def connected_interface(self) -> str: @@ -1002,7 +1004,7 @@ def set_boot_options(self, image_name, **vendor_specifics): current_images = current_boot.splitlines() commands_to_exec = ["no {0}".format(image) for image in current_images] commands_to_exec.append("boot system {0}/{1}".format(file_system, image_name)) - self.config_list(commands_to_exec) + self.config(commands_to_exec) self.save() if self.boot_options["sys"] != image_name: @@ -1023,33 +1025,28 @@ def show(self, command, expect_string=None): str: Output from running command on device. """ self.enable() + if isinstance(command, list): + responses = [] + entered_commands = [] + for command_instance in command: + entered_commands.append(command_instance) + try: + responses.append(self._send_command(command_instance)) + except CommandError as e: + raise CommandListError(entered_commands, command_instance, e.cli_error_msg) + return responses return self._send_command(command, expect_string=expect_string) def show_list(self, commands): - """ - Send list of commands to device. - - Args: - commands (list): Commands to be sent to device. + """Send show commands in list format to a device. - Raises: - CommandListError: Failure running command on device. + DEPRECATED - Use the `show` method. - Returns: - list: Output from each command sent. + Args: + commands (list): List with multiple commands. """ - self.enable() - - responses = [] - entered_commands = [] - for command in commands: - entered_commands.append(command) - try: - responses.append(self._send_command(command)) - except CommandError as e: - raise CommandListError(entered_commands, command, e.cli_error_msg) - - return responses + warnings.warn("show_list() is deprecated; use show().", DeprecationWarning) + return self.show(commands) @property def startup_config(self): diff --git a/pyntc/devices/ios_device.py b/pyntc/devices/ios_device.py index 123a9da1..10dabca8 100644 --- a/pyntc/devices/ios_device.py +++ b/pyntc/devices/ios_device.py @@ -1034,32 +1034,29 @@ def show(self, command, expect_string=None, **netmiko_args): str: Output of command. """ self.enable() + if isinstance(command, list): + responses = [] + entered_commands = [] + for command_instance in command: + entered_commands.append(command_instance) + try: + responses.append(self._send_command(command_instance)) + except CommandError as e: + raise CommandListError(entered_commands, command_instance, e.cli_error_msg) + + return responses return self._send_command(command, expect_string=expect_string, **netmiko_args) def show_list(self, commands): - """Run a list of commands on device. + """Send show commands in list format to a device. - Args: - commands (list): List of commands to run on device. - - Raises: - CommandListError: Error if one of the commands is not able to be ran on the device. + DEPRECATED - Use the `show` method. - Returns: - list: Responses from each command ran on device. + Args: + commands (list): List with multiple commands. """ - self.enable() - - responses = [] - entered_commands = [] - for command in commands: - entered_commands.append(command) - try: - responses.append(self._send_command(command)) - except CommandError as e: - raise CommandListError(entered_commands, command, e.cli_error_msg) - - return responses + warnings.warn("show_list() is deprecated; use show().", DeprecationWarning) + return self.show(commands) @property def startup_config(self): diff --git a/tests/unit/test_devices/test_asa_device.py b/tests/unit/test_devices/test_asa_device.py index e1e0086a..4ce3f791 100644 --- a/tests/unit/test_devices/test_asa_device.py +++ b/tests/unit/test_devices/test_asa_device.py @@ -62,7 +62,7 @@ def test_boot_options_none(self, mock_boot): assert boot_options["sys"] is None @mock.patch.object(ASADevice, "_get_file_system", return_value="disk0:") - @mock.patch.object(ASADevice, "config_list", return_value=None) + @mock.patch.object(ASADevice, "config", return_value=None) def test_set_boot_options(self, mock_cl, mock_fs): with mock.patch(BOOT_OPTIONS_PATH, new_callable=mock.PropertyMock) as mock_boot: mock_boot.return_value = {"sys": BOOT_IMAGE} @@ -70,7 +70,7 @@ def test_set_boot_options(self, mock_cl, mock_fs): mock_cl.assert_called_with([f"boot system disk0:/{BOOT_IMAGE}"]) @mock.patch.object(ASADevice, "_get_file_system", return_value="disk0:") - @mock.patch.object(ASADevice, "config_list", return_value=None) + @mock.patch.object(ASADevice, "config", return_value=None) def test_set_boot_options_dir(self, mock_cl, mock_fs): with mock.patch(BOOT_OPTIONS_PATH, new_callable=mock.PropertyMock) as mock_boot: mock_boot.return_value = {"sys": BOOT_IMAGE} @@ -84,7 +84,7 @@ def test_set_boot_options_no_file(self, mock_fs): self.device.set_boot_options("bad_image.bin") @mock.patch.object(ASADevice, "_get_file_system", return_value="disk0:") - @mock.patch.object(ASADevice, "config_list", return_value=None) + @mock.patch.object(ASADevice, "config", return_value=None) def test_set_boot_options_bad_boot(self, mock_cl, mock_fs): with mock.patch(BOOT_OPTIONS_PATH, new_callable=mock.PropertyMock) as mock_boot: mock_boot.return_value = {"sys": "bad_image.bin"} From 999581ded7fe8a3b2ccd81b37ecd820b945f37af Mon Sep 17 00:00:00 2001 From: Antonio Balmaseda <68010787+balmasea@users.noreply.github.com> Date: Tue, 20 Dec 2022 15:48:57 +0100 Subject: [PATCH 05/13] Feature/verifying reload has happened (#268) * including test to verify nine minutes string in uptime * checking if reload has happened in the last ten minutes and retrying 10 times * adding test for install mode with reload not happening * checking has_reload_happened_recently in test * black * flake8 * making wait_for_device_reboot smarter for ios * resetting uptime string to none when it has not reloaded --- pyntc/devices/ios_device.py | 10 +++- tests/unit/test_devices/test_ios_device.py | 63 ++++++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/pyntc/devices/ios_device.py b/pyntc/devices/ios_device.py index 10dabca8..0e20160c 100644 --- a/pyntc/devices/ios_device.py +++ b/pyntc/devices/ios_device.py @@ -222,13 +222,19 @@ def _wait_for_device_reboot(self, timeout=3600): while time.time() - start < timeout: try: self.open() - self.show("show version") - return + if self._has_reload_happened_recently(): + return except: # noqa E722 # nosec pass raise RebootTimeoutError(hostname=self.hostname, wait_time=timeout) + def _has_reload_happened_recently(self): + if re.search(r"^00:00:0\d:*", self.uptime_string) is None: + self._uptime_string = None + return False + return True + def backup_running_config(self, filename): """Backup running configuration to filename specified. diff --git a/tests/unit/test_devices/test_ios_device.py b/tests/unit/test_devices/test_ios_device.py index 6b72a313..1f9c6178 100644 --- a/tests/unit/test_devices/test_ios_device.py +++ b/tests/unit/test_devices/test_ios_device.py @@ -1,6 +1,7 @@ import unittest import mock import os +import time import pytest @@ -21,6 +22,15 @@ "serial": "", "config_register": "0x2102", } +RECENT_UPTIME_DEVICE_FACTS = { + "version": "15.1(3)T4", + "hostname": "rtr2811", + "uptime": "9 minutes", + "running_image": "c2800nm-adventerprisek9_ivs_li-mz.151-3.T4.bin", + "hardware": "2811", + "serial": "", + "config_register": "0x2102", +} SHOW_BOOT_VARIABLE = ( "Current Boot Variables:\n" "BOOT variable = flash:/cat3k_caa-universalk9.16.11.03a.SPA.bin;\n\n" @@ -32,6 +42,7 @@ "iPXE Timeout = 0" ) + SHOW_BOOT_PATH_LIST = ( f"BOOT path-list : {BOOT_IMAGE}\n" "Config file : flash:/config.text\n" @@ -258,6 +269,14 @@ def test_uptime_string(self, mock_raw_version_data): mock_raw_version_data.return_value = DEVICE_FACTS uptime_string = self.device.uptime_string assert uptime_string == "04:18:59:00" + assert self.device._has_reload_happened_recently() is False + + @mock.patch.object(IOSDevice, "_raw_version_data", autospec=True) + def test_uptime_nine_minutes_string(self, mock_raw_version_data): + mock_raw_version_data.return_value = RECENT_UPTIME_DEVICE_FACTS + uptime_string = self.device.uptime_string + assert uptime_string == "00:00:09:00" + assert self.device._has_reload_happened_recently() is True def test_vendor(self): vendor = self.device.vendor @@ -1257,6 +1276,50 @@ def test_install_os_install_mode_fast_cli_state( assert ios_device.fast_cli == fast_cli_setting +@mock.patch.object(IOSDevice, "_has_reload_happened_recently") +@mock.patch.object(IOSDevice, "os_version", new_callable=mock.PropertyMock) +@mock.patch.object(IOSDevice, "_image_booted") +@mock.patch.object(IOSDevice, "set_boot_options") +@mock.patch.object(IOSDevice, "show") +@mock.patch.object(IOSDevice, "_get_file_system") +@mock.patch.object(IOSDevice, "reboot") +@mock.patch.object(IOSDevice, "fast_cli", new_callable=mock.PropertyMock) +@mock.patch.object(time, "sleep") +def test_install_os_install_mode_with_retries( + mock_sleep, + mock_fast_cli, + mock_reboot, + mock_get_file_system, + mock_show, + mock_set_boot_options, + mock_image_booted, + mock_os_version, + mock_has_reload_happened_recently, + ios_device, +): + image_name = "cat9k_iosxe.16.12.04.SPA.bin" + file_system = "flash:" + mock_get_file_system.return_value = file_system + mock_os_version.return_value = "16.12.03a" + mock_has_reload_happened_recently.side_effect = [False, False, True] + mock_image_booted.side_effect = [False, True] + mock_sleep.return_value = None + # Call the install os function + actual = ios_device.install_os(image_name, install_mode=True) + + # Check the results + mock_set_boot_options.assert_called_with("packages.conf") + mock_show.assert_called_with( + f"install add file {file_system}{image_name} activate commit prompt-level none", delay_factor=20 + ) + mock_reboot.assert_not_called() + mock_os_version.assert_called() + mock_image_booted.assert_called() + assert actual is True + # Assert that fast_cli value was retrieved, set to Fals, and set back to original value + assert mock_fast_cli.call_count == 3 + + def test_show(ios_send_command): command = "show_ip_arp" device = ios_send_command([f"{command}.txt"]) From 6c740a0ea7eda63fd8a45418e1602acf3d483e98 Mon Sep 17 00:00:00 2001 From: Jeff Kala <48843785+jeffkala@users.noreply.github.com> Date: Fri, 31 Mar 2023 15:01:13 -0600 Subject: [PATCH 06/13] Add proper logging for all devices(#271) * draft log * add logging utility function * add logging to aireos device * add logging aireos, asa, f3, eos * resync with develop * add all logging * fix all tests and linting * convert all logs to non f strings * Update pyntc/log.py Co-authored-by: Jacob McGill <9847006+jmcgill298@users.noreply.github.com> * Update pyntc/log.py Co-authored-by: Jacob McGill <9847006+jmcgill298@users.noreply.github.com> * Update pyntc/log.py Co-authored-by: Jacob McGill <9847006+jmcgill298@users.noreply.github.com> * Update pyntc/log.py Co-authored-by: Jacob McGill <9847006+jmcgill298@users.noreply.github.com> * Update pyntc/devices/aireos_device.py Co-authored-by: Jacob McGill <9847006+jmcgill298@users.noreply.github.com> * Update pyntc/devices/aireos_device.py Co-authored-by: Jacob McGill <9847006+jmcgill298@users.noreply.github.com> * Update pyntc/devices/aireos_device.py Co-authored-by: Jacob McGill <9847006+jmcgill298@users.noreply.github.com> * Update pyntc/devices/aireos_device.py Co-authored-by: Jacob McGill <9847006+jmcgill298@users.noreply.github.com> * Update pyntc/devices/aireos_device.py Co-authored-by: Jacob McGill <9847006+jmcgill298@users.noreply.github.com> * Update pyntc/devices/aireos_device.py Co-authored-by: Jacob McGill <9847006+jmcgill298@users.noreply.github.com> * add nxos latest * get all tests passing with new logging * fix yamllint --------- Co-authored-by: drx Co-authored-by: drx Co-authored-by: Jacob McGill <9847006+jmcgill298@users.noreply.github.com> --- .github/workflows/ci.yml | 4 +- poetry.lock | 1 - pyntc/devices/aireos_device.py | 185 ++++++++++++++++-- pyntc/devices/asa_device.py | 113 +++++++++-- pyntc/devices/eos_device.py | 109 ++++++++--- pyntc/devices/f5_device.py | 71 ++++++- pyntc/devices/ios_device.py | 101 +++++++++- pyntc/devices/iosxewlc_device.py | 10 + pyntc/devices/nxos_device.py | 62 +++++- pyntc/log.py | 78 ++++++++ tests/fixtures/.ntc.conf | 2 + tests/unit/test_devices/test_aireos_device.py | 33 ++-- tests/unit/test_devices/test_asa_device.py | 24 ++- tests/unit/test_devices/test_ios_device.py | 16 +- 14 files changed, 698 insertions(+), 111 deletions(-) create mode 100644 pyntc/log.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16009661..f483fdd5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -188,8 +188,8 @@ jobs: - name: "Run Tests" run: "poetry run invoke pytest" needs: - # Remove everything but pylint once pylint is passing. - # - "pylint" + # Remove everything but pylint once pylint is passing. + # - "pylint" - "bandit" - "pydocstyle" - "flake8" diff --git a/poetry.lock b/poetry.lock index d3b8371b..28376bf2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -559,7 +559,6 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] lxml = ">=3.3.0" paramiko = ">=1.15.0" -setuptools = ">0.6" six = "*" [[package]] diff --git a/pyntc/devices/aireos_device.py b/pyntc/devices/aireos_device.py index 7147af58..b7f67150 100644 --- a/pyntc/devices/aireos_device.py +++ b/pyntc/devices/aireos_device.py @@ -1,12 +1,13 @@ """Module for using a Cisco WLC/AIREOS device over SSH.""" # pylint: disable=too-many-lines +import json import os import re import signal import time -import warnings from netmiko import ConnectHandler +from pyntc import log from pyntc.errors import ( CommandError, CommandListError, @@ -90,6 +91,7 @@ def __init__( # nosec # pylint: disable=too-many-arguments self.delay_factor = kwargs.get("delay_factor", 1) self._connected = False self.open(confirm_active=confirm_active) + log.init(host=host) def _ap_images_match_expected(self, image_option, image, ap_boot_options=None): """ @@ -119,8 +121,7 @@ def _ap_images_match_expected(self, image_option, image, ap_boot_options=None): return all(boot_option[image_option] == image for boot_option in ap_boot_options.values()) - @staticmethod - def _check_command_output_for_errors(command, command_response): + def _check_command_output_for_errors(self, command, command_response): """ Check response from device to see if an error was reported. @@ -142,12 +143,14 @@ def _check_command_output_for_errors(command, command_response): >>> """ if "Incorrect usage" in command_response or "Error:" in command_response: + log.error("Host %s: Error in %s with response: %s", self.host, command, command_response) raise CommandError(command, command_response) def _enter_config(self): """Enter into config mode.""" self.enable() self.native.config_mode() + log.debug("Host %s: Device entered config mode.", self.host) def _image_booted(self, image_name, **vendor_specifics): """ @@ -169,8 +172,10 @@ def _image_booted(self, image_name, **vendor_specifics): sysinfo = self.show("show sysinfo") booted_image = re.search(re_version, sysinfo, re.M) if booted_image.group(1) == image_name: + log.info("Host %s: Image %s is booted.", self.host, image_name) return True + log.warning("Host %s: Image %s not booted successfully.", self.host, image_name) return False def _send_command(self, command, expect_string=None, **kwargs): @@ -205,8 +210,10 @@ def _send_command(self, command, expect_string=None, **kwargs): response = self.native.send_command(command, expect_string=expect_string, **kwargs) if "Incorrect usage" in response or "Error:" in response: + log.error("Host %s: Error in %s with response: %s", self.host, command, response) raise CommandError(command, response) + log.info("Host %s: Command %s was executed successfully with response: %s", self.host, command, response) return response def _uptime_components(self): @@ -236,6 +243,9 @@ def _uptime_components(self): hours = int(match_hours.group(1)) if match_hours else 0 minutes = int(match_minutes.group(1)) if match_minutes else 0 + log.debug( + "Host %s: The device has been up for %s days, %s hours, and %s minutes", self.host, days, hours, minutes + ) return days, hours, minutes def _wait_for_ap_image_download(self, timeout=3600): @@ -281,16 +291,37 @@ def _wait_for_ap_image_download(self, timeout=3600): failed = ap_image_stats["failed"] # TODO: When adding logging, send log message of current stats if unsupported or failed: + log.error( + "Host %s: Failed transferring image to AP\nUnsupported: %s\nFailed: %s\n", + self.host, + unsupported, + failed, + ) raise FileTransferError( "Failed transferring image to AP\n" f"Unsupported: {unsupported}\n" f"Failed: {failed}\n" ) elapsed_time = time.time() - start if elapsed_time > timeout: + log.error( + "Host %s: Failed waiting for AP image to be transferred to all devices:\n Total: %s\nDownloaded: %s", + self.host, + ap_count, + downloaded, + ) raise FileTransferError( "Failed waiting for AP image to be transferred to all devices:\n" f"Total: {ap_count}\nDownloaded: {downloaded}" ) + log.debug( + "Host %s:" + "End of waiting time for AP image to be transferred to all devices:\n" + "Total: %s\nDownloaded: %s", + self.host, + ap_count, + downloaded, + ) + def _wait_for_device_reboot(self, timeout=3600): """ Wait for the device to finish reboot process and become accessible. @@ -313,11 +344,13 @@ def _wait_for_device_reboot(self, timeout=3600): while time.time() - start < timeout: try: self.open() + log.debug("Host %s: Device rebooted.", self.host) return except: # noqa E722 # nosec # pylint: disable=bare-except pass # TODO: Get proper hostname parameter + log.error("Host %s: Device timed out while rebooting.", self.host) raise RebootTimeoutError(hostname=self.host, wait_time=timeout) def _wait_for_peer_to_form(self, redundancy_state, timeout=300): @@ -346,8 +379,14 @@ def _wait_for_peer_to_form(self, redundancy_state, timeout=300): while time.time() - start < timeout: current_state = self.peer_redundancy_state if current_state == redundancy_state: + log.debug("Host %s: Redundancy state %s formed properly.", self.host, redundancy_state) return + log.error( + "Host %s: Redundancy state did not form properly to desired state: %s from current state: {current_state}", + self.host, + redundancy_state, + ) raise PeerFailedToFormError(hostname=self.host, desired_state=redundancy_state, current_state=current_state) @property @@ -385,6 +424,7 @@ def ap_boot_options(self): } for ap in ap_boot_options } + log.debug("Host %s: Boot options: {boot_options_by_ap}", self.host, boot_options_by_ap) return boot_options_by_ap @property @@ -411,12 +451,14 @@ def ap_image_stats(self): downloaded = RE_AP_IMAGE_DOWNLOADED.search(ap_images).group(1) unsupported = RE_AP_IMAGE_UNSUPPORTED.search(ap_images).group(1) failed = RE_AP_IMAGE_FAILED.search(ap_images).group(1) - return { + stats = { "count": int(count), "downloaded": int(downloaded), "unsupported": int(unsupported), "failed": int(failed), } + log.debug("Host %s: Image stats {json.dumps(stats, indent=4)}", self.host) + return stats def backup_running_config(self, filename): """ @@ -466,6 +508,8 @@ def boot_options(self): result["sys"] = None else: result = {"sys": None} + + log.debug("Host %s: Boot options %s", self.host, result) return result def checkpoint(self, filename): @@ -486,6 +530,7 @@ def close(self): if self.connected: self.native.disconnect() self._connected = False + log.debug("Host %s: Connection closed.", self.host) def config(self, command, **netmiko_args): r""" @@ -518,7 +563,8 @@ def config(self, command, **netmiko_args): # TODO: Remove this when deprecating config_list method original_command_is_str = isinstance(command, str) - if original_command_is_str: # TODO: switch to isinstance(command, str) when removing above + # TODO: switch to isinstance(command, str) when removing above + if original_command_is_str: command = [command] original_exit_config_setting = netmiko_args.get("exit_config_mode") @@ -537,12 +583,21 @@ def config(self, command, **netmiko_args): command_responses.append(command_response) self._check_command_output_for_errors(cmd, command_response) except TypeError as err: - raise TypeError(f"Netmiko Driver's {err.args[0]}") from err + log.error("Host %s: Netmiko Driver's %s", self.host, err.args[0]) + raise TypeError(f"Netmiko Driver's {err.args[0]}") # TODO: Remove this when deprecating config_list method except CommandError as err: if not original_command_is_str: - raise CommandListError(entered_commands, cmd, err.cli_error_msg) from err - raise err + log.error( + "Host %s: Commands %s returned the error %s", + self.host, + entered_commands, + err.cli_error_msg, + ) + raise CommandListError(entered_commands, cmd, err.cli_error_msg) + else: + log.error("Host %s: Commands %s returned the error %s", self.host, entered_commands, err.cli_error_msg) + raise err # Don't let exception prevent exiting config mode finally: # Ignore None or invalid args passed for exit_config_mode @@ -553,6 +608,12 @@ def config(self, command, **netmiko_args): if original_command_is_str: return command_responses[0] + log.info( + "Host %s: List of config commands %s received responses %s.", + self.host, + command, + command_response, + ) return command_responses def config_list(self, commands, **netmiko_args): # noqa: D401 @@ -581,7 +642,7 @@ def config_list(self, commands, **netmiko_args): # noqa: D401 >>> device.config_list(["interface hostname virtual wlc1.site.com", "config interface vlan airway 20"]) >>> """ - warnings.warn("config_list() is deprecated; use config.", DeprecationWarning) + log.warning("config_list() is deprecated; use config.") return self.config(commands, **netmiko_args) def confirm_is_active(self): @@ -615,6 +676,12 @@ def confirm_is_active(self): redundancy_state = self.redundancy_state peer_redundancy_state = self.peer_redundancy_state self.close() + log.error( + "Host %s: Device not active error where redundancy state %s and peer redundancy state %s", + self.host, + redundancy_state, + peer_redundancy_state, + ) raise DeviceNotActiveError(self.host, redundancy_state, peer_redundancy_state) return True @@ -627,10 +694,12 @@ def connected(self): Returns: bool: True if the device is connected, else False. """ + log.debug("Host %s: Connection status %s.", self.host, self._connected) return self._connected @connected.setter def connected(self, value): + log.debug("Host %s: Device connected is %s.", self.host, value) self._connected = value def disable_wlans(self, wlan_ids): @@ -674,8 +743,16 @@ def disable_wlans(self, wlan_ids): post_disabled_wlans = self.disabled_wlans if not wlans_to_validate.issubset(post_disabled_wlans): desired_wlans = wlans_to_validate.union(disabled_wlans) + log.error( + "Host %s: WLANS not disabled, with desired WLANs %s and post disabled WLANs %s", + self.host, + desired_wlans, + post_disabled_wlans, + ) raise WLANDisableError(self.host, desired_wlans, post_disabled_wlans) + log.info("Host %s: All WLANs with IDs %s were disabled.", self.host, disabled_wlans) + @property def disabled_wlans(self): # noqa: D403 """ @@ -700,6 +777,7 @@ def disabled_wlans(self): # noqa: D403 >>> """ disabled_wlans = [wlan_id for wlan_id, wlan_data in self.wlans.items() if wlan_data["status"] == "Disabled"] + log.info("Host %s: Disabled WLAN IDs: %s", self.host, disabled_wlans) return disabled_wlans def enable(self): @@ -716,6 +794,8 @@ def enable(self): if self.native.check_config_mode(): self.native.exit_config_mode() + log.debug("Host %s: Device enabled.", self.host) + def enable_wlans(self, wlan_ids): """ Enable all given WLAN IDs. @@ -758,6 +838,12 @@ def enable_wlans(self, wlan_ids): post_enabled_wlans = self.enabled_wlans if not wlans_to_validate.issubset(post_enabled_wlans): desired_wlans = wlans_to_validate.union(enabled_wlans) + log.error( + "Host %s: WLANS not enabled,\nwith desired WLANs %s and post enabled WLANs %s", + self.host, + desired_wlans, + post_enabled_wlans, + ) raise WLANEnableError(self.host, desired_wlans, post_enabled_wlans) @property @@ -784,6 +870,7 @@ def enabled_wlans(self): # noqa: D403 >>> """ enabled_wlans = [wlan_id for wlan_id, wlan_data in self.wlans.items() if wlan_data["status"] == "Enabled"] + log.info("Host %s: List of enabled WLAN IDs: %s", self.host, enabled_wlans) return enabled_wlans def facts(self): @@ -859,6 +946,12 @@ def file_copy( ] ) except CommandListError as error: + log.error( + "Host %s: File transfer error %s\n\n%s", + self.host, + FileTransferError.default_message, + error.message, + ) raise FileTransferError(error.message) from error try: @@ -866,13 +959,22 @@ def file_copy( if "Are you sure you want to start? (y/N)" in response: response = self.show("y", auto_find_prompt=False, delay_factor=delay_factor) except CommandError as error: + log.error( + "Host %s: File transfer error %s\n\n%s", + self.host, + FileTransferError.default_message, + error.message, + ) raise FileTransferError(message=f"{FileTransferError.default_message}\n\n{error.message}") from error except: # noqa E722 + log.error("Host %s: File transfer error %s", self.host, FileTransferError) raise FileTransferError if "File transfer is successful" not in response: - raise FileTransferError(message=f"Did not find expected success message in response, found:\n{response}") + log.error("Host %s: Did not find expected success message in response, found:\n%s", self.host, response) + raise FileTransferError + log.info("Host %s: File transferred successfully.", self.host) return True def file_copy_remote_exists(self, src, dest=None, **kwargs): @@ -938,14 +1040,25 @@ def install_os(self, image_name, controller="both", save_config=True, disable_wl if disable_wlans is not None: self.enable_wlans(disable_wlans) if not self._image_booted(image_name): + log.error("Host %s: OS install error for image %s", self.host, image_name) raise OSInstallError(hostname=self.host, desired_boot=image_name) try: self._wait_for_peer_to_form(peer_redundancy) except PeerFailedToFormError: - raise OSInstallError(hostname=f"{self.host}-standby", desired_boot=f"{image_name}-{peer_redundancy}") + log.error( + "Host %s: Peer failed to form error for image %s and peer redundancy %s", + self.host, + image_name, + peer_redundancy, + ) + raise OSInstallError( + hostname=f"Host {self.host}: {self.host}-standby", desired_boot=f"{image_name}-{peer_redundancy}" + ) + log.info("Host %s: OS image %s installed successfully.", self.host, image_name) return True + log.info("Host %s: OS image %s not installed.", self.host, image_name) return False def is_active(self): @@ -1013,6 +1126,8 @@ def open(self, confirm_active=True): if confirm_active: self.confirm_is_active() + log.debug("Host %s: Connection to controller was opened successfully.", self.host) + @property def peer_redundancy_state(self): """ @@ -1031,11 +1146,14 @@ def peer_redundancy_state(self): try: show_redundancy = self.show("show redundancy summary") except CommandError: + log.error("Host %s: Peer redundancy state command error.", self.host) return None re_peer_redundancy_state = RE_PEER_REDUNDANCY_STATE.search(show_redundancy) peer_redundancy_state = re_peer_redundancy_state.group(1).lower() if peer_redundancy_state == "n/a": peer_redundancy_state = "disabled" + + log.debug("Host %s: Peer redundancy state: %s.", self.host, peer_redundancy_state) return peer_redundancy_state def reboot(self, timer=0, controller="self", save_config=True, **kwargs): @@ -1056,10 +1174,11 @@ def reboot(self, timer=0, controller="self", save_config=True, **kwargs): >>> """ if kwargs.get("confirm"): - warnings.warn("Passing 'confirm' to reboot method is deprecated.", DeprecationWarning) + log.warning("Passing 'confirm' to reboot method is deprecated.") def handler(signum, frame): - raise RebootSignal("Interrupting after reload") + log.error("Host %s: Interrupting after reload.", self.host) + raise RebootSignal signal.signal(signal.SIGALRM, handler) @@ -1089,6 +1208,8 @@ def handler(signum, frame): signal.alarm(0) + log.info("Host %s: Device rebooted.", self.host) + @property def redundancy_mode(self): """ @@ -1105,6 +1226,7 @@ def redundancy_mode(self): """ ha = self.show("show redundancy summary") ha_mode = re.search(r"^\s*Redundancy\s+Mode\s*=\s*(.+?)\s*$", ha, re.M) + log.debug("Host %s: Redundancy mode: {ha_mode.group(1).lower()}", self.host, ha_mode.group(1).lower()) return ha_mode.group(1).lower() @property @@ -1125,11 +1247,13 @@ def redundancy_state(self): try: show_redundancy = self.show("show redundancy summary") except CommandError: + log.error("Host %s: Redundancy state command error.", self.host) return None re_redundancy_state = RE_REDUNDANCY_STATE.search(show_redundancy) redundancy_state = re_redundancy_state.group(1).lower() if redundancy_state == "n/a": redundancy_state = "disabled" + log.debug("Host %s: Redundancy state %s.", self.host, redundancy_state) return redundancy_state def rollback(self): @@ -1164,6 +1288,7 @@ def save(self): >>> """ self.native.save_config() + log.debug("Host %s: Configuration saved.", self.host) return True def set_boot_options(self, image_name, **vendor_specifics): @@ -1197,13 +1322,15 @@ def set_boot_options(self, image_name, **vendor_specifics): elif self.boot_options["backup"] == image_name: boot_command = "boot backup" else: + log.error("Host %s: File not found error for image %s.", self.host, image_name) raise NTCFileNotFoundError(image_name, "'show boot'", self.host) self.config(boot_command) self.save() if not self.boot_options["sys"] == image_name: + log.error("Host %s: Setting boot command did not yield expected results", self.host) raise CommandError( command=boot_command, - message="Setting boot command did not yield expected results", + message="Host {self.host}: Setting boot command did not yield expected results", ) def show(self, command, expect_string=None, **netmiko_args): @@ -1241,7 +1368,8 @@ def show(self, command, expect_string=None, **netmiko_args): # TODO: Remove this when deprecating config_list method original_command_is_str = isinstance(command, str) - if original_command_is_str: # TODO: switch to isinstance(command, str) when removing above + # TODO: switch to isinstance(command, str) when removing above + if original_command_is_str: command = [command] entered_commands = [] @@ -1256,18 +1384,36 @@ def show(self, command, expect_string=None, **netmiko_args): command_responses.append(command_response) self._check_command_output_for_errors(cmd, command_response) except TypeError as err: + log.error("Host %s: Netmiko Driver's %s", self.host, err.args[0]) raise TypeError(f"Netmiko Driver's {err.args[0]}") # TODO: Remove this when deprecating config_list method except CommandError as err: if not original_command_is_str: + log.error( + "Host %s: Command error for commands %s with message %s.", + self.host, + entered_commands, + err.cli_error_msg, + ) raise CommandListError(entered_commands, cmd, err.cli_error_msg) else: + log.error( + "Host %s: Command error for commands %s with message %s.", + self.host, + entered_commands, + err, + ) raise err # TODO: Remove this when deprecating config_list method if original_command_is_str: return command_responses[0] + log.debug( + "Host %s: Successfully executed show commands with responses %s.", + self.host, + command_responses, + ) return command_responses def show_list(self, commands, **netmiko_args): # noqa: D401 @@ -1299,7 +1445,7 @@ def show_list(self, commands, **netmiko_args): # noqa: D401 Backup Boot Image................................ 8.5.110.0 >>> """ - warnings.warn("show_list() is deprecated; use show.", DeprecationWarning) + log.warning("show_list() is deprecated; use show.") return self.show(commands, **netmiko_args) @property @@ -1368,7 +1514,8 @@ def transfer_image_to_ap(self, image, timeout=None): download_image = option break if download_image is None: - raise FileTransferError(f"Unable to find {image} on {self.host}") + log.error("Host %s: Unable to find %s on host.", self.host, image) + raise FileTransferError self.config(f"ap image predownload {option} all") self._wait_for_ap_image_download() @@ -1382,8 +1529,10 @@ def transfer_image_to_ap(self, image, timeout=None): time.sleep(1) if not self._ap_images_match_expected("primary", image): + log.error("Host %s: Unable to set all APs to use %s", self.host, image) raise FileTransferError(f"Unable to set all APs to use {image}") + log.info("Host %s: All images transferred to AP connected to WLC.", self.host) return changed @property @@ -1404,6 +1553,7 @@ def uptime(self): hours += days * 24 minutes += hours * 60 seconds = minutes * 60 + log.debug("Host %s: Device has been up for %d seconds", self.host, seconds) return seconds @property @@ -1449,6 +1599,7 @@ def wlans(self): show_wlan_summary_out = self.show("show wlan summary") re_wlans = RE_WLANS.finditer(show_wlan_summary_out) wlans = {int(wlan.group("wlan_id")): dict(zip(wlan_keys, wlan.groups()[1:])) for wlan in re_wlans} + log.debug("Host %s: Device WLANs %s.", self.host, json.dumps(wlans, indent=4)) return wlans diff --git a/pyntc/devices/asa_device.py b/pyntc/devices/asa_device.py index ae2f98d2..0337d236 100644 --- a/pyntc/devices/asa_device.py +++ b/pyntc/devices/asa_device.py @@ -4,13 +4,13 @@ import re import signal import time -import warnings from collections import Counter from ipaddress import ip_address, IPv4Address, IPv4Interface, IPv6Address, IPv6Interface from typing import Dict, Iterable, List, Optional, Union from netmiko import ConnectHandler from netmiko.cisco import CiscoAsaFileTransfer, CiscoAsaSSH +from pyntc import log from pyntc.errors import ( CommandError, CommandListError, @@ -60,14 +60,17 @@ def __init__(self, host: str, username: str, password: str, secret="", port=22, self._connected = False self.open() self._peer_device: Optional[ASADevice] = None + log.init(host=host) def _enable(self): - warnings.warn("_enable() is deprecated; use enable().", DeprecationWarning) + log.warning("_enable() is deprecated; use enable().") self.enable() + log.debug("Host %s: Device enabled", self.host) def _enter_config(self): self.enable() self.native.config_mode() + log.debug("Host %s: Device entered config mode.", self.host) def _file_copy(self, src: str, dest: str, file_system: str) -> None: self.enable() @@ -80,24 +83,34 @@ def _file_copy(self, src: str, dest: str, file_system: str) -> None: fc.transfer_file() # Allow EOFErrors to be caught and only raise an error if the file is not actually on the device except EOFError: + log.error("Host %s: EOF error.", self.host) self.open() except Exception: + log.error("Host %s: File transfer error %s", self.host, FileTransferError.default_message) raise FileTransferError finally: + log.error("Host %s: An error occurred when transferring file %s.", self.host, src) fc.close_scp_chan() if not self.file_copy_remote_exists(src, dest, file_system): - raise FileTransferError( - message="Attempted file copy, but could not validate file existed after transfer" + log.error( + "Host %s: Attempted file copy, but could not validate file %s existed after transfer.", + self.host, + src, ) + raise FileTransferError + + log.info("Host %s: File transferred successfully.", self.host) def _file_copy_instance( self, src: str, dest: Optional[str] = None, file_system: str = "flash:" ) -> CiscoAsaFileTransfer: + log.info("Host %s: _file_copy_instance dest %s.", self.host, dest) if dest is None: dest = os.path.basename(src) fc = CiscoAsaFileTransfer(self.native, src, dest, file_system=file_system) + log.debug("Host %s: File copy instance %s.", self.host, fc) return fc def _get_file_system(self): @@ -115,8 +128,10 @@ def _get_file_system(self): except AttributeError: # TODO: Get proper hostname - + log.error("Host %s: File system not found with command 'dir'.", self.host) raise FileSystemNotFoundError(hostname=self.host, command="dir") + + log.debug("Host %s: File system %s.", self.host, file_system) return file_system def _get_ipv4_addresses(self, host: str) -> Dict[str, List[IPv4Address]]: @@ -145,7 +160,11 @@ def _get_ipv4_addresses(self, host: str) -> Dict[str, List[IPv4Address]]: show_ip_address = self.show(command) re_ip_addresses = RE_SHOW_IP_ADDRESS.findall(show_ip_address) - return {interface: [IPv4Interface(f"{address}/{netmask}")] for interface, address, netmask in re_ip_addresses} + results = { + interface: [IPv4Interface(f"{address}/{netmask}")] for interface, address, netmask in re_ip_addresses + } + log.debug("Host %s: ip interfaces %s", self.host) + return results def _get_ipv6_addresses(self, host: str) -> Dict[str, List[IPv6Address]]: """ @@ -193,27 +212,34 @@ def _get_ipv6_addresses(self, host: str) -> Dict[str, List[IPv6Address]]: if ipv6_addresses: results[interface] = ipv6_addresses + log.debug("Host %s: ip interfaces %s", self.host, results) return results def _image_booted(self, image_name, **vendor_specifics): version_data = self.show("show version") if re.search(image_name, version_data): + log.info("Host %s: Image %s booted successfully.", self.host, image_name) return True + log.info("Host %s: Image %s not booted successfully.", self.host, image_name) return False def _interfaces_detailed_list(self): ip_int = self.show("show interface") ip_int_data = get_structured_data("cisco_asa_show_interface.template", ip_int) + log.debug("Host %s: interfaces detailed list %s.", self.host, ip_int_data) return ip_int_data def _raw_version_data(self): show_version_out = self.show("show version") try: version_data = get_structured_data("cisco_asa_show_version.template", show_version_out)[0] + + log.debug("Host %s: version data %s.", self.host, version_data) return version_data except IndexError: + log.error("Host %s: index error.", self.host) return {} def _send_command(self, command, expect_string=None): @@ -223,14 +249,17 @@ def _send_command(self, command, expect_string=None): response = self.native.send_command(command, expect_string=expect_string) if "% " in response or "Error:" in response: + log.error("Host %s: Error in %s with response: %s", self.host, command, response) raise CommandError(command, response) + log.info("Host %s: Command %s was executed successfully with response: %s", self.host, command, response) return response def _show_vlan(self): show_vlan_out = self.show("show vlan") show_vlan_data = get_structured_data("cisco_ios_show_vlan.template", show_vlan_out) + log.debug("Host %s: Successfully executed command 'show vlan' with responses %s.", self.host, show_vlan_data) return show_vlan_data def _uptime_components(self, uptime_full_string): @@ -262,11 +291,13 @@ def _wait_for_device_reboot(self, timeout=3600): while time.time() - start < timeout: try: self.open() + log.debug("Host %s: Device rebooted.", self.host) return except: # noqa E722 # nosec pass # TODO: Get proper hostname parameter + log.error("Host %s: Device timed out while rebooting.", self.host) raise RebootTimeoutError(hostname=self.host, wait_time=timeout) def _wait_for_peer_reboot(self, acceptable_states: Iterable[str], timeout: int = 3600) -> None: @@ -298,6 +329,12 @@ def _wait_for_peer_reboot(self, acceptable_states: Iterable[str], timeout: int = start = time.time() while time.time() - start < timeout: if self.peer_redundancy_state == "failed": + log.error( + "Host %s: Redundancy state for device %s did not form properly to desired state: %s.", + self.host, + self.host, + self.peer_redundancy_state, + ) break while time.time() - start < timeout: @@ -306,6 +343,7 @@ def _wait_for_peer_reboot(self, acceptable_states: Iterable[str], timeout: int = time.sleep(1) # TODO: Get proper hostname parameter + log.error("Host %s: reboot timeout error with timeout %s.", self.host, timeout) raise RebootTimeoutError(hostname=f"{self.host}-peer", wait_time=timeout) def backup_running_config(self, filename): @@ -318,6 +356,8 @@ def backup_running_config(self, filename): with open(filename, "w") as f: f.write(self.running_config) + log.debug("Host %s: Running config backed up to %s.", self.host, self.running_config) + @property def boot_options(self): """ @@ -336,6 +376,7 @@ def boot_options(self): else: boot_image = None + log.debug("Host %s: the boot options are %s", self.host, dict(sys=boot_image)) return dict(sys=boot_image) def checkpoint(self, checkpoint_file): @@ -345,6 +386,7 @@ def checkpoint(self, checkpoint_file): Args: checkpoint_file (str): Saves a checkpoint file with the name provided to the function. """ + log.debug("Host %s: checkpoint is %s.", self.host, checkpoint_file) self.save(filename=checkpoint_file) def close(self): @@ -352,6 +394,7 @@ def close(self): if self._connected: self.native.disconnect() self._connected = False + log.debug("Host %s: Connection closed.", self.host) def config(self, command): """Send configuration commands to a device. @@ -374,6 +417,7 @@ def config(self, command): else: self._send_command(command) self.native.exit_config_mode() + log.info("Host %s: Device configured with command %s.", self.host, command) def config_list(self, commands): """Send configuration commands in list format to a device. @@ -383,7 +427,7 @@ def config_list(self, commands): Args: commands (list): List with multiple commands. """ - warnings.warn("config_list() is deprecated; use config().", DeprecationWarning) + log.warning("config_list() is deprecated; use config().") self.config(commands) @property @@ -408,7 +452,9 @@ def connected_interface(self) -> str: connected_interface = interface break - return connected_interface # TODO: Raise custom exception for when connected_interface is not discovered + # TODO: Raise custom exception for when connected_interface is not discovered + log.debug("Host %s: Interface connected to %s is %s.", self.host, address, connected_interface) + return connected_interface def enable(self): """Ensure device is in enable mode. @@ -423,6 +469,8 @@ def enable(self): if self.native.check_config_mode(): self.native.exit_config_mode() + log.debug("Host %s: Device enabled.", self.host) + def enable_scp(self) -> None: """ Enable SCP on device by configuring "ssh scopy enable". @@ -449,12 +497,16 @@ def enable_scp(self) -> None: device = self.peer_device if not device.is_active(): - raise FileTransferError("Unable to establish a connection with the active device") + log.error("Host %s: Unable to establish a connection with the active device", self.host) + raise FileTransferError try: device.config("ssh scopy enable") except CommandError: - raise FileTransferError("Unable to enable scopy on the device") + log.error("Host %s: Unable to enable scopy on the device", self.host) + raise FileTransferError + + log.info("Host %s: ssh copy enabled.", self.host) device.save() @property @@ -504,6 +556,10 @@ def file_copy( if peer: self.peer_device._file_copy(src, dest, file_system) + # logging removed because it messes up unit test mock_basename.assert_not_called() + # for tests test_file_copy_no_peer_pass_args, test_file_copy_include_peer + # log.info("Host %s: File %s transferred successfully.") + # TODO: Make this an internal method since exposing file_copy should be sufficient def file_copy_remote_exists(self, src, dest=None, file_system=None): """ @@ -531,7 +587,10 @@ def file_copy_remote_exists(self, src, dest=None, file_system=None): fc = self._file_copy_instance(src, dest, file_system=file_system) if fc.check_file_exists() and fc.compare_md5(): + log.debug("Host %s: File %s already exists on remote.", self.host, src) return True + + log.debug("Host %s: File %s does not already exist on remote.", self.host, src) return False def install_os(self, image_name, **vendor_specifics): @@ -553,10 +612,13 @@ def install_os(self, image_name, **vendor_specifics): self.reboot() self._wait_for_device_reboot(timeout=timeout) if not self._image_booted(image_name): + log.error("Host %s: OS install error for image %s", self.host, image_name) raise OSInstallError(hostname=self.facts.get("hostname"), desired_boot=image_name) + log.info("Host %s: OS image %s installed successfully.", self.host, image_name) return True + log.info("Host %s: OS image %s not installed.", self.host, image_name) return False @property @@ -583,8 +645,10 @@ def ip_address(self) -> Union[IPv4Address, IPv6Address]: ip = ip_address(self.host) except ValueError: # Assume hostname was used, and retrieve resolved IP from paramiko transport + log.error("Host %s: value error for ip address used to establish connection.", self.host) ip = ip_address(self.native.remote_conn.transport.getpeername()[0]) + log.debug("Host %s: ip address used to establish connection %s.", self.host, ip) return ip @property @@ -601,6 +665,7 @@ def ipv4_addresses(self) -> Dict[str, List[IPv4Address]]: {'outside': [IPv4Interface('10.132.8.6/24')], 'inside': [IPv4Interface('10.1.1.2/23')]} >>> """ + log.debug("Host %s: ipv4 addresses of the devices interfaces %s.", self.host, self._get_ipv4_addresses("self")) return self._get_ipv4_addresses("self") @property @@ -617,6 +682,7 @@ def ipv6_addresses(self) -> Dict[str, List[IPv6Address]]: {'outside': [IPv6Interface('fe80::5200:ff:fe0a:1')]} >>> """ + log.debug("Host %s: ipv6 addresses of the devices interfaces %s.", self.host, self._get_ipv6_addresses("self")) return self._get_ipv6_addresses("self") @property @@ -641,6 +707,7 @@ def ip_protocol(self) -> str: """ protocol = f"ipv{self.ip_address.version}" + log.debug("Host %s: IP protocol for paramiko is %s.", self.host) return protocol def is_active(self): @@ -679,6 +746,8 @@ def open(self): ) self._connected = True + log.debug("Host %s: Connection to controller was opened successfully.", self.host) + @property def peer_device(self) -> "ASADevice": """ @@ -694,6 +763,7 @@ def peer_device(self) -> "ASADevice": else: self._peer_device.open() + log.debug("Host %s: Peer device %s.", self.host, self._peer_device) return self._peer_device @property @@ -721,6 +791,7 @@ def peer_ip_address(self) -> Union[IPv4Address, IPv6Address]: peer_ip_addresses = peer_ip_property[self.connected_interface] for address in peer_ip_addresses: if self_address in address.network: + log.debug("Host %s: Peer IP %s.", self.host, address.ip) return address.ip @property @@ -777,6 +848,7 @@ def peer_redundancy_state(self): try: show_failover = self.show("show failover") except CommandError: + log.error("Host %s: Peer redundancy state command error.", self.host) return None if "Failover On" in show_failover: @@ -794,6 +866,7 @@ def peer_redundancy_state(self): else: peer_redundancy_state = "disabled" + log.debug("Host %s: Peer redundancy state: %s.", self.host, peer_redundancy_state) return peer_redundancy_state.lower() def reboot(self, timer=0, **kwargs): @@ -812,10 +885,11 @@ def reboot(self, timer=0, **kwargs): >>> """ if kwargs.get("confirm"): - warnings.warn("Passing 'confirm' to reboot method is deprecated.", DeprecationWarning) + log.warning("Passing 'confirm' to reboot method is deprecated.") def handler(signum, frame): - raise RebootSignal("Interrupting after reload") + log.error("Host %s: Interrupting after reload.", self.host) + raise RebootSignal signal.signal(signal.SIGALRM, handler) signal.alarm(10) @@ -835,6 +909,8 @@ def handler(signum, frame): signal.alarm(0) + log.info("Host %s: Device rebooted.", self.host) + def reboot_standby(self, acceptable_states: Optional[Iterable[str]] = None, timeout: Optional[int] = None) -> None: """ Reload the standby device from the active device. @@ -868,6 +944,8 @@ def reboot_standby(self, acceptable_states: Optional[Iterable[str]] = None, time self.show("failover reload-standby") self._wait_for_peer_reboot(**kwargs) + log.debug("Host %s: reboot standby with timeout %s.", self.host, timeout) + @property def redundancy_mode(self): """ @@ -890,6 +968,7 @@ def redundancy_mode(self): show_failover_first_line = show_failover.splitlines()[0].strip() redundancy_mode = show_failover_first_line.lower().lstrip("failover") + log.debug("Host %s: Redundancy mode: %s", self.host, redundancy_mode.lstrip()) return redundancy_mode.lstrip() @property @@ -914,6 +993,7 @@ def redundancy_state(self): try: show_failover = self.show("show failover") except CommandError: + log.error("Host %s: Redundancy state command error.", self.host) return None if "Failover On" in show_failover: @@ -931,6 +1011,7 @@ def redundancy_state(self): else: redundancy_state = "disabled" + log.debug("Host %s: Redundancy state %s.", self.host, redundancy_state.lower()) return redundancy_state.lower() def rollback(self, rollback_to): @@ -974,6 +1055,7 @@ def save(self, filename="startup-config"): self.native.send_command_timing("\n", delay_factor=2) # Confirm that we have a valid prompt again before returning. self.native.find_prompt() + log.debug("Host %s: Configuration saved.", self.host) return True def set_boot_options(self, image_name, **vendor_specifics): @@ -994,6 +1076,7 @@ def set_boot_options(self, image_name, **vendor_specifics): file_system_files = self.show("dir {0}".format(file_system)) if re.search(image_name, file_system_files) is None: + log.error("Host %s: File not found error for image %s.", self.host, image_name) raise NTCFileNotFoundError( # TODO: Update to use hostname hostname=self.host, @@ -1008,11 +1091,14 @@ def set_boot_options(self, image_name, **vendor_specifics): self.save() if self.boot_options["sys"] != image_name: + log.error("Host %s: Setting boot command did not yield expected results", self.host) raise CommandError( command="boot system {0}/{1}".format(file_system, image_name), message="Setting boot command did not yield expected results", ) + log.info("Host %s: boot options have been set to %s", self.host, image_name) + def show(self, command, expect_string=None): """ Send command to device. @@ -1025,6 +1111,7 @@ def show(self, command, expect_string=None): str: Output from running command on device. """ self.enable() + log.debug("Host %s: Successfully executed command 'show' with responses.", self.host) if isinstance(command, list): responses = [] entered_commands = [] @@ -1045,7 +1132,7 @@ def show_list(self, commands): Args: commands (list): List with multiple commands. """ - warnings.warn("show_list() is deprecated; use show().", DeprecationWarning) + log.warning("show_list() is deprecated; use show().") return self.show(commands) @property diff --git a/pyntc/devices/eos_device.py b/pyntc/devices/eos_device.py index 66e5de29..32189c5d 100644 --- a/pyntc/devices/eos_device.py +++ b/pyntc/devices/eos_device.py @@ -1,30 +1,28 @@ """Module for using an Arista EOS device over the eAPI.""" +import os import re import time -import os -import warnings +from netmiko import ConnectHandler, FileTransfer from pyeapi import connect as eos_connect from pyeapi.client import Node as EOSNative from pyeapi.eapilib import CommandError as EOSCommandError -from netmiko import ConnectHandler -from netmiko import FileTransfer - -from pyntc.utils import convert_list_by_key -from .system_features.vlans.eos_vlans import EOSVlans -from .base_device import BaseDevice, RollbackError, RebootTimerError, fix_docs +from pyntc import log from pyntc.errors import ( - NTCError, CommandError, - OSInstallError, CommandListError, + FileSystemNotFoundError, FileTransferError, - RebootTimeoutError, + NTCError, NTCFileNotFoundError, - FileSystemNotFoundError, + OSInstallError, + RebootTimeoutError, ) +from pyntc.utils import convert_list_by_key +from .base_device import BaseDevice, fix_docs, RebootTimerError, RollbackError +from .system_features.vlans.eos_vlans import EOSVlans BASIC_FACTS_KM = {"model": "modelName", "os_version": "internalVersion", "serial_number": "serialNumber"} INTERFACES_KM = { @@ -72,12 +70,14 @@ def __init__(self, host, username, password, transport="http", port=None, timeou self.native = EOSNative(self.connection) # _connected indicates Netmiko ssh connection self._connected = False + log.init(host=host) def _file_copy_instance(self, src, dest=None, file_system="flash:"): if dest is None: dest = os.path.basename(src) fc = FileTransfer(self.native_ssh, src, dest, file_system=file_system) + log.debug("Host %s: File copy instance %s.", self.host, fc) return fc def _get_file_system(self): @@ -93,13 +93,16 @@ def _get_file_system(self): try: file_system = re.match(r"\s*.*?(\S+:)", raw_data).group(1) except AttributeError: + log.error("Host %s: Attribute error with command 'dir'.", self.host) raise FileSystemNotFoundError(hostname=self.hostname, command="dir") + log.debug("Host %s: File system %s.", self.host, file_system) return file_system def _image_booted(self, image_name, **vendor_specifics): version_data = self.show("show boot", raw_text=True) if re.search(image_name, version_data): + log.info("Host %s: Image %s booted successfully.", self.host, image_name) return True return False @@ -112,7 +115,11 @@ def _interfaces_status_list(self): interface_dictionary["interface"] = key interfaces_list.append(interface_dictionary) - return convert_list_by_key(interfaces_list, INTERFACES_KM, fill_in=True, whitelist=["interface"]) + interface_status_list = convert_list_by_key( + interfaces_list, INTERFACES_KM, fill_in=True, whitelist=["interface"] + ) + log.debug("Host %s: interfaces detailed list %s.", self.host, interface_status_list) + return interface_status_list def _parse_response(self, response, raw_text): if raw_text: @@ -139,10 +146,12 @@ def _wait_for_device_reboot(self, timeout=3600): while time.time() - start < timeout: try: self.show("show hostname") + log.debug("Host %s: Device rebooted.", self.host) return except: # noqa E722 # nosec pass + log.error("Host %s: Device timed out while rebooting.", self.host) raise RebootTimeoutError(hostname=self.hostname, wait_time=timeout) def backup_running_config(self, filename): @@ -155,6 +164,8 @@ def backup_running_config(self, filename): with open(filename, "w") as f: f.write(self.running_config) + log.debug("Host %s: Running config backed up to %s.", self.host, self.running_config) + @property def boot_options(self): """Get current running software. @@ -164,18 +175,25 @@ def boot_options(self): """ image = self.show("show boot-config")["softwareImage"] image = image.replace("flash:", "") + log.debug("Host %s: the boot options are %s", self.host, dict(sys=image)) return dict(sys=image) def checkpoint(self, checkpoint_file): - """Create a checkpoint file of the running config. + """Copy running config checkpoint. Args: - checkpoint_file (str): Name of the checkpoint file. + checkpoint_file (str): Checkpoint file name. """ + log.debug("Host %s: checkpoint is %s.", self.host, checkpoint_file) self.show("copy running-config %s" % checkpoint_file) def close(self): - """Not implemented. Just ``passes``.""" + """Create a checkpoint file of the running config. + + Args: + checkpoint_file (str): Name of the checkpoint file. + """ + log.info("Host %s: Device configured.", self.host) pass def config(self, commands): @@ -190,8 +208,12 @@ def config(self, commands): """ try: self.native.config(commands) + log.info("Host %s: Device configured with commands %s.", self.host, commands) except EOSCommandError as e: if isinstance(commands, str): + log.error( + "Host %s: Command error with commands: %s and error message %s", self.host, commands, e.message + ) raise CommandError(commands, e.message) raise CommandListError(commands, e.commands[len(e.commands) - 1], e.message) @@ -203,7 +225,7 @@ def config_list(self, commands): Args: commands (list): List with multiple commands. """ - warnings.warn("config_list() is deprecated; use config().", DeprecationWarning) + log.warning("config_list() is deprecated; use config().") self.config(commands) def enable(self): @@ -219,6 +241,8 @@ def enable(self): if self.native_ssh.check_config_mode(): self.native_ssh.exit_config_mode() + log.debug("Host %s: Device enabled", self.host) + @property def uptime(self): """ @@ -231,6 +255,7 @@ def uptime(self): sh_version_output = self.show("show version") self._uptime = int(time.time() - sh_version_output["bootupTimestamp"]) + log.debug("Host %s: Uptime %s", self.host, self._uptime) return self._uptime @property @@ -257,6 +282,7 @@ def hostname(self): sh_hostname_output = self.show("show hostname") self._hostname = sh_hostname_output["hostname"] + log.debug("Host %s: Hostname %s", self.host, self._hostname) return self._hostname @property @@ -270,6 +296,7 @@ def interfaces(self): iface_detailed_list = self._interfaces_status_list() self._interfaces = sorted(list(x["interface"] for x in iface_detailed_list)) + log.debug("Host %s: Interfaces %s", self.host, self._interfaces) return self._interfaces @property @@ -283,6 +310,7 @@ def vlans(self): vlans = EOSVlans(self) self._vlans = vlans.get_list() + log.debug("Host %s: Vlans %s", self.host, self._vlans) return self._vlans @property @@ -296,6 +324,7 @@ def fqdn(self): sh_hostname_output = self.show("show hostname") self._fqdn = sh_hostname_output["fqdn"] + log.debug("Host %s: FQDN %s", self.host, self._fqdn) return self._fqdn @property @@ -309,6 +338,7 @@ def model(self): sh_version_output = self.show("show version") self._model = sh_version_output["modelName"] + log.debug("Host %s: Model %s", self.host, self._model) return self._model @property @@ -322,6 +352,7 @@ def os_version(self): sh_version_output = self.show("show version") self._os_version = sh_version_output["internalVersion"] + log.debug("Host %s: OS version %s", self.host, self._os_version) return self._os_version @property @@ -335,6 +366,7 @@ def serial_number(self): sh_version_output = self.show("show version") self._serial_number = sh_version_output["serialNumber"] + log.debug("Host %s: Serial number %s", self.host, self._serial_number) return self._serial_number def file_copy(self, src, dest=None, file_system=None): @@ -361,15 +393,20 @@ def file_copy(self, src, dest=None, file_system=None): fc.enable_scp() fc.establish_scp_conn() fc.transfer_file() + log.info("Host %s: File %s transferred successfully.", self.host, src) except: # noqa E722 + log.error("Host %s: File transfer error %s", self.host, FileTransferError.default_message) raise FileTransferError finally: fc.close_scp_chan() if not self.file_copy_remote_exists(src, dest, file_system): - raise FileTransferError( - message="Attempted file copy, but could not validate file existed after transfer" + log.error( + "Host %s: Attempted file copy, but could not validate file existed after transfer %s", + self.host, + FileTransferError.default_message, ) + raise FileTransferError # TODO: Make this an internal method since exposing file_copy should be sufficient def file_copy_remote_exists(self, src, dest=None, file_system=None): @@ -389,8 +426,10 @@ def file_copy_remote_exists(self, src, dest=None, file_system=None): fc = self._file_copy_instance(src, dest, file_system=file_system) if fc.check_file_exists() and fc.compare_md5(): + log.debug("Host %s: File %s already exists on remote.", self.host, src) return True + log.debug("Host %s: File %s does not already exist on remote.", self.host, src) return False def install_os(self, image_name, **vendor_specifics): @@ -411,10 +450,13 @@ def install_os(self, image_name, **vendor_specifics): self.reboot() self._wait_for_device_reboot(timeout=timeout) if not self._image_booted(image_name): + log.error("Host %s: OS install error for image %s", self.host, image_name) raise OSInstallError(hostname=self.hostname, desired_boot=image_name) + log.info("Host %s: OS image %s installed successfully.", self.host, image_name) return True + log.info("Host %s: OS image %s not installed.", self.host, image_name) return False def open(self): @@ -438,6 +480,8 @@ def open(self): ) self._connected = True + log.debug("Host %s: Connection to controller was opened successfully.", self.host) + def reboot(self, timer=0, **kwargs): """ Reload the controller or controller pair. @@ -455,12 +499,14 @@ def reboot(self, timer=0, **kwargs): """ if kwargs.get("confirm"): - warnings.warn("Passing 'confirm' to reboot method is deprecated.", DeprecationWarning) + log.warning("Passing 'confirm' to reboot method is deprecated.") if timer != 0: + log.error("Host %s: Reboot time error.", self.host) raise RebootTimerError(self.device_type) self.show("reload now") + log.info("Host %s: Device rebooted.", self.host) def rollback(self, rollback_to): """Rollback device configuration. @@ -473,27 +519,28 @@ def rollback(self, rollback_to): """ try: self.show("configure replace %s force" % rollback_to) + log.info("Host %s: Rollback to %s.", self.host, rollback_to) except (CommandError, CommandListError): + log.error("Host %s: Rollback unsuccessful. %s may not exist.", self.host, rollback_to) raise RollbackError("Rollback unsuccessful. %s may not exist." % rollback_to) @property def running_config(self): - """Show running configuration. + """Return running config. Returns: str: Running configuration. """ + log.debug("Host %s: Show running config.", self.host) return self.show("show running-config", raw_text=True) def save(self, filename="startup-config"): - """Copy running configuration to startup configuration. - - Args: - filename (str, optional): Name where you want running configuration to save. Defaults to "startup-config". + """Show running configuration. Returns: - bool: True when succesfull. + str: Running configuration. """ + log.debug("Host %s: Copy running config with name %s.", self.host, filename) self.show("copy running-config %s" % filename) return True @@ -513,15 +560,19 @@ def set_boot_options(self, image_name, **vendor_specifics): file_system_files = self.show("dir {0}".format(file_system), raw_text=True) if re.search(image_name, file_system_files) is None: + log.error("Host %s: File not found error for image %s.", self.host, image_name) raise NTCFileNotFoundError(hostname=self.hostname, file=image_name, dir=file_system) self.show("install source {0}{1}".format(file_system, image_name)) if self.boot_options["sys"] != image_name: + log.error("Host %s: Setting boot command did not yield expected results", self.host) raise CommandError( command="install source {0}".format(image_name), message="Setting install source did not yield expected results", ) + log.info("Host %s: boot options have been set to %s", self.host, image_name) + def show(self, commands, raw_text=False): """Send configuration commands to a device. @@ -546,10 +597,13 @@ def show(self, commands, raw_text=False): response_list = self._parse_response(response, raw_text=raw_text) if original_commands_is_str: return response_list[0] + log.debug("Host %s: Successfully executed command 'show' with responses %s.", self.host, response_list) return response_list except EOSCommandError as e: if original_commands_is_str: + log.error("Host %s: Command error for command %s with message %s.", self.host, commands, e.message) raise CommandError(e.commands, e.message) + log.error("Host %s: Command list error for commands %s with message %s.", self.host, commands, e.message) raise CommandListError(commands, e.commands[len(e.commands) - 1], e.message) def show_list(self, commands): @@ -560,7 +614,7 @@ def show_list(self, commands): Args: commands (list): List with multiple commands. """ - warnings.warn("show_list() is deprecated; use show().", DeprecationWarning) + log.warning("show_list() is deprecated; use show().") self.show(commands) @property @@ -570,6 +624,7 @@ def startup_config(self): Returns: str: Startup configuration. """ + log.debug("Host %s: show startup-config", self.host) return self.show("show startup-config", raw_text=True) diff --git a/pyntc/devices/f5_device.py b/pyntc/devices/f5_device.py index f272d4d8..b257cf6f 100644 --- a/pyntc/devices/f5_device.py +++ b/pyntc/devices/f5_device.py @@ -8,8 +8,9 @@ import requests from f5.bigip import ManagementRoot +from pyntc import log +from pyntc.errors import FileTransferError, NotEnoughFreeSpaceError, NTCFileNotFoundError, OSInstallError -from pyntc.errors import OSInstallError, FileTransferError, NTCFileNotFoundError, NotEnoughFreeSpaceError from .base_device import BaseDevice @@ -29,6 +30,7 @@ def __init__(self, host, username, password, **kwargs): # noqa: D403 super().__init__(host, username, password, device_type="f5_tmos_icontrol") self.api_handler = ManagementRoot(self.host, self.username, self.password) + log.init(host=host) def _check_free_space(self, min_space=0): """Check for minimum space on the device. @@ -46,8 +48,11 @@ def _check_free_space(self, min_space=0): elif free_space >= min_space: return elif free_space < min_space: + log.error("Host %s: Not enough free space for min space requirement %s.", self.host, min_space) raise NotEnoughFreeSpaceError(hostname=self.host, min_space=min_space) + log.debug("Host %s: Free space %s is sufficient.", self.host, free_space) + def _check_md5sum(self, filename, checksum): """Check is md5sum is correct. @@ -61,8 +66,10 @@ def _check_md5sum(self, filename, checksum): md5sum = self._file_copy_remote_md5(filename) if checksum == md5sum: + log.debug("Host %s: Checksums match.", self.host) return True else: + log.debug("Host %s: Checksums do not match.", self.host) return False @staticmethod @@ -98,6 +105,7 @@ def _get_active_volume(self): for _volume in volumes: if hasattr(_volume, "active") and _volume.active is True: current_volume = _volume.name + log.debug("Host %s: Active volume name is %s.", self.host, current_volume) return current_volume def _get_free_space(self): @@ -120,41 +128,53 @@ def _get_free_space(self): if match: free_space = float(match.group(1)) + log.debug("Host %s: Free space is %s GB.", self.host, free_space) return free_space def _get_images(self): images = self.api_handler.tm.sys.software.images.get_collection() + log.debug("Host %s: List of images %s.", self.host, images) return images def _get_interfaces_list(self): interfaces = self.soap_handler.Networking.Interfaces.get_list() + log.debug("Host %s: List of interfaces %s.", self.host, interfaces) return interfaces def _get_model(self): - return self.soap_handler.System.SystemInfo.get_marketing_name() + model = self.soap_handler.System.SystemInfo.get_marketing_name() + log.debug("Host %s: Model name %s.", self.host, model) + return model def _get_serial_number(self): system_information = self.soap_handler.System.SystemInfo.get_system_information() chassis_serial = system_information.get("chassis_serial") + log.debug("Host %s: Serial number %s.", self.host, chassis_serial) return chassis_serial def _get_uptime(self): - return self.soap_handler.System.SystemInfo.get_uptime() + uptime = self.soap_handler.System.SystemInfo.get_uptime() + log.debug("Host %s: Uptime %s.", self.host, uptime) + return uptime def _get_version(self): - return self.soap_handler.System.SystemInfo.get_version() + version = self.soap_handler.System.SystemInfo.get_version() + log.debug("Host %s: Version %s.", self.host, version) + return version def _get_vlans(self): rd_list = self.soap_handler.Networking.RouteDomainV2.get_list() rd_vlan_list = self.soap_handler.Networking.RouteDomainV2.get_vlan(rd_list) + log.debug("Host %s: List of vlans %s.", self.host, rd_vlan_list) return rd_vlan_list def _get_volumes(self): volumes = self.api_handler.tm.sys.software.volumes.get_collection() + log.debug("Host %s: List of volumes %s.", self.host, volumes) return volumes def _image_booted(self, image_name, **vendor_specifics): @@ -167,6 +187,7 @@ def _image_booted(self, image_name, **vendor_specifics): bool: True if booted volume is equal to active volume. Otherwise, false. """ volume = vendor_specifics.get("volume") + log.debug("Host %s: Checking if image %s has been booted.", self.host, image_name) return True if self._get_active_volume() == volume else False def _image_exists(self, image_name): @@ -186,8 +207,10 @@ def _image_exists(self, image_name): return None if image_name in all_images: + log.debug("Host %s: Image %s exists.", self.host, image_name) return True else: + log.debug("Host %s: Image %s does not exist.", self.host, image_name) return False def _image_install(self, image_name, volume): @@ -206,6 +229,8 @@ def _image_install(self, image_name, volume): self.api_handler.tm.sys.software.images.exec_cmd("install", name=image_name, volume=volume, options=options) + log.info("Host %s: Image %s is installed.", self.host, image_name) + def _image_match(self, image_name, checksum): """Check if image name matches the checksum. @@ -219,8 +244,10 @@ def _image_match(self, image_name, checksum): if self._image_exists(image_name): image = os.path.join("/shared/images", image_name) if self._check_md5sum(image, checksum): + log.debug("Host %s: Image %s matches the checksum.", self.host, image_name) return True + log.debug("Host %s: Image %s does not match the checksum.", self.host, image_name) return False def _reboot_to_volume(self, volume_name=None): @@ -236,9 +263,12 @@ def _reboot_to_volume(self, volume_name=None): # This is a workaround by issuing reboot command from bash directly. self.api_handler.tm.util.bash.exec_cmd("run", utilCmdArgs='-c "reboot"') + log.debug("Host %s: Activation to volume %s.", self.host, volume_name) + def _reconnect(self): """Reconnect to the device.""" self.api_handler = ManagementRoot(self.host, self.username, self.password) + log.debug("Host %s: Reconnect to device.", self.host) def _upload_image(self, image_filepath): """Upload an iso image to the device. @@ -273,6 +303,8 @@ def _upload_image(self, image_filepath): start += len(payload) + log.info("Host %s: Image %s uploaded to %s.", self.host, image_filename, image_filepath) + @staticmethod def _uptime_to_string(uptime): """Change uptime to a string. @@ -304,6 +336,7 @@ def _volume_exists(self, volume_name): """ result = self.api_handler.tm.sys.software.volumes.volume.exists(name=volume_name) + log.debug("Host %s: Checking if volume exists.", self.host) return result def _wait_for_device_reboot(self, volume_name, timeout=600): @@ -326,8 +359,11 @@ def _wait_for_device_reboot(self, volume_name, timeout=600): volume = self.api_handler.tm.sys.software.volumes.volume.load(name=volume_name) if hasattr(volume, "active") and volume.active is True: return True + log.debug("Host %s: Reboot successfull.", self.host) except Exception: # noqa E722 # nosec + log.error("Host %s: Error while rebooting.", self.host) pass + log.debug("Host %s: Reboot not successfull.", self.host) return False def _wait_for_image_installed(self, image_name, volume, timeout=1800): @@ -349,10 +385,12 @@ def _wait_for_image_installed(self, image_name, volume, timeout=1800): # of .version attribute in first seconds of their live. try: if self.image_installed(image_name=image_name, volume=volume): + log.info("Host %s: Image %s installed on volume %s.", self.host, image_name, volume) return except: # noqa E722 # nosec pass + log.error("Host %s: OS install error with image %s and volume %s.", self.host, image_name, volume) raise OSInstallError(hostname=self.hostname, desired_boot=volume) def backup_running_config(self, filename): @@ -375,6 +413,7 @@ def boot_options(self): """ active_volume = self._get_active_volume() + log.debug("Host %s: Active volume name %s.", self.host, active_volume) return {"active_volume": active_volume} def checkpoint(self, filename): @@ -413,6 +452,7 @@ def uptime(self): if self._uptime is None: self._uptime = self._get_uptime() + log.debug("Host %s: Uptime %s.", self.host, self._uptime) return self._uptime @property @@ -528,9 +568,14 @@ def file_copy(self, src, dest=None, **kwargs): self._check_free_space(min_space=6) self._upload_image(image_filepath=src) if not self.file_copy_remote_exists(src, dest, **kwargs): - raise FileTransferError( - message="Attempted file copy, but could not validate file existed after transfer" + log.error( + "Host %s: Attempted file copy, but could not validate file existed after transfer for file %s.", + self.host, + src, ) + raise FileTransferError + + log.info("Host %s: File %s copied successfully.", self.host, src) # TODO: Make this an internal method since exposing file_copy should be sufficient def file_copy_remote_exists(self, src, dest=None, **kwargs): @@ -547,14 +592,17 @@ def file_copy_remote_exists(self, src, dest=None, **kwargs): bool: True if image specified exists on device. Otherwise, false. """ if dest and not dest.startswith("/shared/images"): + log.error("Host %s: Support only for images - destination is always /shared/images.", self.host) raise NotImplementedError("Support only for images - destination is always /shared/images") local_md5sum = self._file_copy_local_md5(filepath=src) file_basename = os.path.basename(src) if not self._image_match(image_name=file_basename, checksum=local_md5sum): + log.debug("Host %s: File %s does not already exist on remote.", self.host, src) return False else: + log.debug("Host %s: File %s already exists on remote.", self.host) return True def image_installed(self, image_name, volume): @@ -591,8 +639,10 @@ def image_installed(self, image_name, volume): and _volume.basebuild == image.build # noqa W503 and _volume.status == "complete" # noqa W503 ): + log.debug("Host %s: Image %s installed on volume %s.", self.host, image_name, volume) return True + log.debug("Host %s: Image %s not installed on volume %s.", self.host, image_name, volume) return False def install_os(self, image_name, **vendor_specifics): @@ -611,12 +661,15 @@ def install_os(self, image_name, **vendor_specifics): if not self.image_installed(image_name, volume): self._check_free_space(min_space=6) if not self._image_exists(image_name): + log.error("Host %s: File not found for image %s and volume %s.", self.host, image_name, volume) raise NTCFileNotFoundError(hostname=self.hostname, file=image_name, dir="/shared/images") self._image_install(image_name=image_name, volume=volume) self._wait_for_image_installed(image_name=image_name, volume=volume) + log.info("Host %s: Image %s installed on volume %s.", self.host, image_name, volume) return True + log.info("Host %s: Image %s not installed on volume %s.", self.host, image_name, volume) return False def open(self): @@ -650,7 +703,9 @@ def reboot(self, timer=0, volume=None, **kwargs): self._reboot_to_volume(volume_name=volume_name) if not self._wait_for_device_reboot(volume_name=volume): - raise RuntimeError("Reboot to volume {} failed".format(volume)) + log.error("Host %s: Reboot to volume %s failed.", self.host, volume) + raise RuntimeError + log.debug("Host %s: Reboot to volume %s succeeded.", self.host, volume) def rollback(self, checkpoint_file): """Rollback to checkpoint configurtion file. @@ -694,9 +749,11 @@ def set_boot_options(self, image_name, **vendor_specifics): volume = vendor_specifics.get("volume") self._check_free_space(min_space=6) if not self._image_exists(image_name): + log.error("Host %s: File not found for image %s and volume %s.", self.host, image_name, volume) raise NTCFileNotFoundError(hostname=self.hostname, file=image_name, dir="/shared/images") self._image_install(image_name=image_name, volume=volume) self._wait_for_image_installed(image_name=image_name, volume=volume) + log.info("Host %s: Image %s installed to volume %s.", self.host, image_name, volume) def show(self, command, raw_text=False): """Run cli command on device. diff --git a/pyntc/devices/ios_device.py b/pyntc/devices/ios_device.py index 0e20160c..a8a9cffe 100644 --- a/pyntc/devices/ios_device.py +++ b/pyntc/devices/ios_device.py @@ -4,9 +4,9 @@ import re import signal import time -import warnings from netmiko import ConnectHandler, FileTransfer +from pyntc import log from pyntc.errors import ( CommandError, CommandListError, @@ -68,6 +68,7 @@ def __init__( # nosec self._fast_cli = fast_cli self._connected = False self.open(confirm_active=confirm_active) + log.init(host=host) def _check_command_output_for_errors(self, command, command_response): """ @@ -94,18 +95,21 @@ def _check_command_output_for_errors(self, command, command_response): raise CommandError(command, command_response) def _enable(self): - warnings.warn("_enable() is deprecated; use enable().", DeprecationWarning) + log.warning("_enable() is deprecated; use enable().") self.enable() + log.debug("Host %s: Device enabled", self.host) def _enter_config(self): self.enable() self.native.config_mode() + log.debug("Host %s: Device entered config mode.", self.host) def _file_copy_instance(self, src, dest=None, file_system="flash:"): if dest is None: dest = os.path.basename(src) fc = FileTransfer(self.native, src, dest, file_system=file_system) + log.debug("Host %s: File copy instance %s.", self.host, fc) return fc def _get_file_system(self): @@ -126,19 +130,23 @@ def _get_file_system(self): raw_data = self.show("dir") try: file_system = re.match(r"\s*.*?(\S+:)", raw_data).group(1) + log.debug("Host %s: File system %s.", self.host, file_system) return file_system except AttributeError: # Allow to continue through the loop continue + log.error("host %s: File system not found with command 'dir'.") raise FileSystemNotFoundError(hostname=self.hostname, command="dir") # Get the version of the image that is booted into on the device def _image_booted(self, image_name, image_pattern=r".*\.(\d+\.\d+\.\w+)\.SPA.+", **vendor_specifics): version_data = self.show("show version") if re.search(image_name, version_data): + log.info("Host %s: Image %s booted successfully.", self.host, image_name) return True + log.info("Host %s: Image %s not booted successfully.", self.host, image_name) # Test for version number in the text, used on install mode devices that use packages.conf try: version_number = re.search(image_pattern, image_name).group(1) @@ -155,6 +163,7 @@ def _interfaces_detailed_list(self): ip_int_br_out = self.show("show ip int br") ip_int_br_data = get_structured_data("cisco_ios_show_ip_int_brief.template", ip_int_br_out) + log.debug("Host %s: interfaces detailed list %s.", self.host, ip_int_br_data) return ip_int_br_data def _is_catalyst(self): @@ -165,8 +174,10 @@ def _raw_version_data(self): try: version_data = get_structured_data("cisco_ios_show_version.template", show_version_out)[0] except IndexError: + log.error("Host %s: index error.", self.host) return {} + log.debug("Host %s: version data %s.", self.host, version_data) return version_data def _send_command(self, command, expect_string=None, **kwargs): @@ -183,14 +194,17 @@ def _send_command(self, command, expect_string=None, **kwargs): response = self.native.send_command(**command_args) if "% " in response or "Error:" in response: + log.error("Host %s: Error in %s with response: %s", self.host, command, response) raise CommandError(command, response) + log.info("Host %s: Command %s was executed successfully with response: %s", self.host, command, response) return response def _show_vlan(self): show_vlan_out = self.show("show vlan") show_vlan_data = get_structured_data("cisco_ios_show_vlan.template", show_vlan_out) + log.debug("Host %s: Successfully executed command 'show vlan' with responses %s.", self.host, show_vlan_data) return show_vlan_data def _uptime_components(self, uptime_full_string): @@ -222,11 +236,14 @@ def _wait_for_device_reboot(self, timeout=3600): while time.time() - start < timeout: try: self.open() + self.show("show version") + log.debug("Host %s: Device rebooted.", self.host) if self._has_reload_happened_recently(): return except: # noqa E722 # nosec pass + log.error("Host %s: Device timed out while rebooting.", self.host) raise RebootTimeoutError(hostname=self.hostname, wait_time=timeout) def _has_reload_happened_recently(self): @@ -265,6 +282,8 @@ def boot_options(self): # Default to running config value show_boot_out = self.show("show run | inc boot") boot_path_regex = r"boot\ssystem\s\S+(?::+|\s)(\S+.bin)" + log.error("Host %s: Command error 'show boot'.", self.host) + match = re.search(boot_path_regex, show_boot_out, re.MULTILINE) if match: boot_path_tuple = match.groups() @@ -279,6 +298,7 @@ def boot_options(self): else: boot_image = None + log.debug("Host %s: the boot options are {dict(sys=boot_image)}", self.host) return {"sys": boot_image} def checkpoint(self, checkpoint_file): @@ -287,6 +307,7 @@ def checkpoint(self, checkpoint_file): Args: checkpoint_file (str): Name of checkpoint file. """ + log.debug("Host %s: checkpoint is %s.", self.host, checkpoint_file) self.save(filename=checkpoint_file) def close(self): @@ -294,6 +315,7 @@ def close(self): if self.connected: self.native.disconnect() self._connected = False + log.debug("Host %s: Connection closed.", self.host) def config(self, command, **netmiko_args): r""" @@ -329,7 +351,8 @@ def config(self, command, **netmiko_args): # TODO: Remove this when deprecating config_list method original_command_is_str = isinstance(command, str) - if original_command_is_str: # TODO: switch to isinstance(command, str) when removing above + # TODO: switch to isinstance(command, str) when removing above + if original_command_is_str: command = [command] original_exit_config_setting = netmiko_args.get("exit_config_mode") @@ -348,10 +371,17 @@ def config(self, command, **netmiko_args): command_responses.append(command_response) self._check_command_output_for_errors(cmd, command_response) except TypeError as err: + log.error("Host %s: Netmiko Driver's %s", self.host, err.args[0]) raise TypeError(f"Netmiko Driver's {err.args[0]}") # TODO: Remove this when deprecating config_list method except CommandError as err: if not original_command_is_str: + log.error( + "Host %s: Command error with commands: %s and error message %s", + self.host, + entered_commands, + err.cli_error_msg, + ) raise CommandListError(entered_commands, cmd, err.cli_error_msg) else: raise err @@ -365,6 +395,7 @@ def config(self, command, **netmiko_args): if original_command_is_str: return command_responses[0] + log.info("Host %s: Device configured with command responses %s.", self.host, command_responses) return command_responses def config_list(self, commands, **netmiko_args): # noqa: D401 @@ -394,7 +425,7 @@ def config_list(self, commands, **netmiko_args): # noqa: D401 ['host(config)#interface Gig0/1\nhost(config-if)#, 'description x-connect\nhost(config-if)#'] >>> """ - warnings.warn("config_list() is deprecated; use config.", DeprecationWarning) + log.warning("config_list() is deprecated; use config.") return self.config(commands, **netmiko_args) def confirm_is_active(self): @@ -428,8 +459,15 @@ def confirm_is_active(self): redundancy_state = self.redundancy_state peer_redundancy_state = self.peer_redundancy_state self.close() + log.error( + "Host %s: Device not active error with redundancy state %s and peer redundancy state %s", + self.host, + redundancy_state, + peer_redundancy_state, + ) raise DeviceNotActiveError(self.host, redundancy_state, peer_redundancy_state) + log.debug("Host %s: Device is active.", self.host) return True @property @@ -459,6 +497,8 @@ def enable(self): if self.native.check_config_mode(): self.native.exit_config_mode() + log.debug("Host %s: Device enabled.", self.host) + @property def uptime(self): """Get uptime from device. @@ -471,6 +511,7 @@ def uptime(self): uptime_full_string = version_data["uptime"] self._uptime = self._uptime_to_seconds(uptime_full_string) + log.debug("Host %s: Uptime %s", self.host, self._uptime) return self._uptime @property @@ -498,6 +539,7 @@ def hostname(self): if self._hostname is None: self._hostname = version_data["hostname"] + log.debug("Host %s: Hostname {self._hostname}", self.host) return self._hostname @property @@ -511,6 +553,7 @@ def interfaces(self): if self._interfaces is None: self._interfaces = list(x["intf"] for x in self._interfaces_detailed_list()) + log.debug("Host %s: Interfaces %s", self.host, self._interfaces) return self._interfaces @property @@ -527,6 +570,7 @@ def vlans(self): else: self._vlans = [] + log.debug("Host %s: Vlans %s", self.host, self._vlans) return self._vlans @property @@ -539,6 +583,7 @@ def fqdn(self): if self._fqdn is None: self._fqdn = "N/A" + log.debug("Host %s: FQDN %s", self.host, self._fqdn) return self._fqdn @property @@ -552,6 +597,7 @@ def model(self): if self._model is None: self._model = version_data["hardware"] + log.debug("Host %s: Model %s", self.host, self._model) return self._model @property @@ -565,6 +611,7 @@ def os_version(self): if self._os_version is None: self._os_version = version_data["version"] + log.debug("Host %s: OS version %s", self.host, self._os_version) return self._os_version @property @@ -578,6 +625,7 @@ def serial_number(self): if self._serial_number is None: self._serial_number = version_data["serial"] + log.debug("Host %s: Serial number %s", self.host, self._serial_number) return self._serial_number @property @@ -591,6 +639,7 @@ def config_register(self): version_data = self._raw_version_data() self._config_register = version_data["config_register"] + log.debug("Host %s: Config register %s", self.host, self._config_register) return self._config_register @property @@ -633,11 +682,15 @@ def file_copy(self, src, dest=None, file_system=None): fc.enable_scp() fc.establish_scp_conn() fc.transfer_file() + log.info("Host %s: File %s transferred successfully.", self.host, src) except OSError as error: # compare hashes if not fc.compare_md5(): + log.error("Host %s: Socket closed error %s", self.host, error) raise SocketClosedError(message=error) + log.error("Host %s: OS error %s", self.host, error) except: # noqa E722 + log.error("Host %s: File transfer error %s", self.host, FileTransferError.default_message) raise FileTransferError finally: fc.close_scp_chan() @@ -646,9 +699,12 @@ def file_copy(self, src, dest=None, file_system=None): self.open() if not self.file_copy_remote_exists(src, dest, file_system): - raise FileTransferError( - message="Attempted file copy, but could not validate file existed after transfer" + log.error( + "Host %s: Attempted file copy, but could not validate file existed after transfer %s", + self.host, + FileTransferError.default_message, ) + raise FileTransferError # TODO: Make this an internal method since exposing file_copy should be sufficient def file_copy_remote_exists(self, src, dest=None, file_system=None): @@ -668,7 +724,10 @@ def file_copy_remote_exists(self, src, dest=None, file_system=None): fc = self._file_copy_instance(src, dest, file_system=file_system) if fc.check_file_exists() and fc.compare_md5(): + log.debug("Host %s: File %s already exists on remote.", self.host, src) return True + + log.debug("Host %s: File %s does not already exist on remote.", self.host, src) return False def install_os(self, image_name, install_mode=False, install_mode_delay_factor=20, **vendor_specifics): @@ -714,6 +773,7 @@ def install_os(self, image_name, install_mode=False, install_mode_delay_factor=2 try: self.show(command, delay_factor=install_mode_delay_factor) except IOError: + log.error("Host %s: IO error for image %s", self.host, image_name) pass except CommandError: command = f"request platform software package install switch all file {self._get_file_system()}{image_name} auto-copy" @@ -732,10 +792,13 @@ def install_os(self, image_name, install_mode=False, install_mode_delay_factor=2 image_name = INSTALL_MODE_FILE_NAME # Verify the OS level if not self._image_booted(image_name): + log.error("Host %s: OS install error for image %s", self.host, image_name) raise OSInstallError(hostname=self.hostname, desired_boot=image_name) + log.info("Host %s: OS image %s installed successfully.", self.host, image_name) return True + log.info("Host %s: OS image %s not installed.", self.host, image_name) return False def is_active(self): @@ -803,6 +866,8 @@ def open(self, confirm_active=True): if confirm_active: self.confirm_is_active() + log.debug("Host %s: Connection to controller was opened successfully.", self.host) + @property def peer_redundancy_state(self): """ @@ -821,6 +886,7 @@ def peer_redundancy_state(self): try: show_redundancy = self.show("show redundancy") except CommandError: + log.error("Host %s: Command error for command 'show redundancy'.", self.host) return None re_show_redundancy = RE_SHOW_REDUNDANCY.match(show_redundancy.lstrip()) processor_redundancy_info = re_show_redundancy.group("other") @@ -829,6 +895,8 @@ def peer_redundancy_state(self): processor_redundancy_state = re_redundancy_state.group(1).lower() else: processor_redundancy_state = "disabled" + + log.debug("Host %s: Processor redundancy state %s.", self.host, processor_redundancy_state) return processor_redundancy_state def reboot(self, timer=0, **kwargs): @@ -843,10 +911,11 @@ def reboot(self, timer=0, **kwargs): ReloadTimeoutError: When the device is still unreachable after the timeout period. """ if kwargs.get("confirm"): - warnings.warn("Passing 'confirm' to reboot method is deprecated.", DeprecationWarning) + log.warning("Passing 'confirm' to reboot method is deprecated.") def handler(signum, frame): - raise RebootSignal("Interrupting after reload") + log.error("Host %s: Reboot signal error interrupting after reload.", self.host) + raise RebootSignal signal.signal(signal.SIGALRM, handler) signal.alarm(10) @@ -862,9 +931,11 @@ def handler(signum, frame): self.native.send_command_timing("\n") except RebootSignal: + log.error("Host %s: Reboot signal error.", self.host) signal.alarm(0) signal.alarm(0) + log.info("Host %s: Device rebooted.", self.host) # else: # print("Need to confirm reboot with confirm=True") @@ -886,11 +957,13 @@ def redundancy_mode(self): try: show_redundancy = self.show("show redundancy") except CommandError: + log.error("Host %s: Command error for command 'show redundancy'.", self.host) return "n/a" re_show_redundancy = RE_SHOW_REDUNDANCY.match(show_redundancy.lstrip()) redundancy_info = re_show_redundancy.group("info") re_redundancy_mode = RE_REDUNDANCY_OPERATION_MODE.search(redundancy_info) redundancy_mode = re_redundancy_mode.group(1).lower() + log.debug("Host %s: Redundancy mode is %s.", self.host, redundancy_mode) return redundancy_mode @property @@ -911,11 +984,14 @@ def redundancy_state(self): try: show_redundancy = self.show("show redundancy") except CommandError: + log.error("Host %s: Command error for command 'show redundancy'.", self.host) return None re_show_redundancy = RE_SHOW_REDUNDANCY.match(show_redundancy.lstrip()) processor_redundancy_info = re_show_redundancy.group("self") re_redundancy_state = RE_REDUNDANCY_STATE.search(processor_redundancy_info) processor_redundancy_state = re_redundancy_state.group(1).lower() + + log.debug("Host %s: Redundancy state is %s.", self.host, processor_redundancy_state) return processor_redundancy_state def rollback(self, rollback_to): @@ -929,7 +1005,9 @@ def rollback(self, rollback_to): """ try: self.show("configure replace %s%s force" % (self._get_file_system(), rollback_to)) + log.info("Host %s: Rollback to %s.", self.host, rollback_to) except CommandError: + log.error("Host %s: Rollback unsuccessful. %s may not exist.", self.host, rollback_to) raise RollbackError("Rollback unsuccessful. %s may not exist." % rollback_to) @property @@ -939,6 +1017,7 @@ def running_config(self): Returns: str: Output of ``show running-config``. """ + log.debug("Host %s: Show running config.", self.host) return self.show("show running-config") def save(self, filename="startup-config"): @@ -959,6 +1038,7 @@ def save(self, filename="startup-config"): self.native.send_command_timing("\n", delay_factor=2) # Confirm that we have a valid prompt again before returning. self.native.find_prompt() + log.debug("Host %s: Copy running config with name %s.", self.host, filename) return True def set_boot_options(self, image_name, **vendor_specifics): @@ -978,6 +1058,7 @@ def set_boot_options(self, image_name, **vendor_specifics): file_system_files = self.show("dir {0}".format(file_system)) if image_name != INSTALL_MODE_FILE_NAME and re.search(image_name, file_system_files) is None: + log.error("Host %s: File not found error for image %s.", self.host, image_name) raise NTCFileNotFoundError(hostname=self.hostname, file=image_name, dir=file_system) if image_name == "packages.conf": command = "boot system {0}{1}".format(file_system, image_name) @@ -1024,6 +1105,7 @@ def set_boot_options(self, image_name, **vendor_specifics): self.save() new_boot_options = self.boot_options["sys"] if new_boot_options != image_name: + log.error("Host %s: Setting boot command did not yield expected results", self.host) raise CommandError( command=command, message="Setting boot command did not yield expected results, found {0}".format(new_boot_options), @@ -1061,7 +1143,7 @@ def show_list(self, commands): Args: commands (list): List with multiple commands. """ - warnings.warn("show_list() is deprecated; use show().", DeprecationWarning) + log.warning("show_list() is deprecated; use show().") return self.show(commands) @property @@ -1071,6 +1153,7 @@ def startup_config(self): Returns: str: Startup configuration from device. """ + log.debug("Host %s: Successfully executed command 'show startup-config'.", self.host) return self.show("show startup-config") diff --git a/pyntc/devices/iosxewlc_device.py b/pyntc/devices/iosxewlc_device.py index 90d070ec..62c8ced5 100644 --- a/pyntc/devices/iosxewlc_device.py +++ b/pyntc/devices/iosxewlc_device.py @@ -7,6 +7,7 @@ OSInstallError, WaitingRebootTimeoutError, ) +from pyntc import log INSTALL_MODE_FILE_NAME = "packages.conf" @@ -14,6 +15,8 @@ class IOSXEWLCDevice(IOSDevice): """Cisco IOSXE WLC Device Implementation.""" + log.init() + def _wait_for_device_start_reboot(self, timeout=600): start = time.time() while time.time() - start < timeout: @@ -23,6 +26,7 @@ def _wait_for_device_start_reboot(self, timeout=600): except Exception: # noqa E722 # nosec return + log.error("Host %s: Wait reboot timeout error with timeout %s", self.host, timeout) raise WaitingRebootTimeoutError(hostname=self.hostname, wait_time=timeout) def _wait_for_device_reboot(self, timeout=5400): @@ -31,10 +35,12 @@ def _wait_for_device_reboot(self, timeout=5400): try: self.open() self.show("show version") + log.debug("Host %s: Device rebooted.", self.host) return except Exception: # noqa E722 # nosec pass + log.error("Host %s: Device timed out while rebooting.", self.host) raise RebootTimeoutError(hostname=self.hostname, wait_time=timeout) def install_os(self, image_name, install_mode_delay_factor=20, **vendor_specifics): @@ -81,10 +87,13 @@ def install_os(self, image_name, install_mode_delay_factor=20, **vendor_specific # Verify the OS level if not self._image_booted(image_name): + log.error("Host %s: OS install error for image %s", self.host, image_name) raise OSInstallError(hostname=self.hostname, desired_boot=image_name) + log.info("Host %s: OS image %s installed successfully.", self.host, image_name) return True + log.info("Host %s: OS image %s not installed.", self.host, image_name) return False def show(self, command, expect_string=None, **netmiko_args): @@ -98,4 +107,5 @@ def show(self, command, expect_string=None, **netmiko_args): str: Output of command. """ self.enable() + log.debug("Host %s: Successfully executed command 'show'.", self.host) return self._send_command(command, expect_string=expect_string, **netmiko_args) diff --git a/pyntc/devices/nxos_device.py b/pyntc/devices/nxos_device.py index 1782706d..0c3fe599 100644 --- a/pyntc/devices/nxos_device.py +++ b/pyntc/devices/nxos_device.py @@ -2,8 +2,8 @@ import os import re import time -import warnings +from pyntc import log from pyntc.errors import ( CommandError, CommandListError, @@ -16,7 +16,7 @@ from pynxos.errors import CLIError from pynxos.features.file_copy import FileTransferError as NXOSFileTransferError -from .base_device import BaseDevice, RebootTimerError, RollbackError, fix_docs +from .base_device import BaseDevice, fix_docs, RebootTimerError, RollbackError @fix_docs @@ -40,12 +40,15 @@ def __init__(self, host, username, password, transport="http", timeout=30, port= self.transport = transport self.timeout = timeout self.native = NXOSNative(host, username, password, transport=transport, timeout=timeout, port=port, **kwargs) + log.init(host=host) def _image_booted(self, image_name, **vendor_specifics): version_data = self.show("show version", raw_text=True) if re.search(image_name, version_data): + log.info("Host %s: Image %s booted successfully.", self.host, image_name) return True + log.info("Host %s: Image %s not booted successfully.", self.host, image_name) return False def _wait_for_device_reboot(self, timeout=600): @@ -54,10 +57,12 @@ def _wait_for_device_reboot(self, timeout=600): try: self.refresh_facts() if self.uptime < 180: + log.debug("Host %s: Device rebooted.", self.host) return except: # noqa E722 # nosec pass + log.error("Host %s: Device timed out while rebooting.", self.host) raise RebootTimeoutError(hostname=self.hostname, wait_time=timeout) def backup_running_config(self, filename): @@ -67,6 +72,7 @@ def backup_running_config(self, filename): filename (str): Name of backup file. """ self.native.backup_running_config(filename) + log.debug("Host %s: Running config backed up.", self.host) @property def boot_options(self): @@ -75,7 +81,9 @@ def boot_options(self): Returns: dict: e.g . {"kick": "router_kick.img", "sys": "router_sys.img"} """ - return self.native.get_boot_options() + boot_options = self.native.get_boot_options() + log.debug("Host %s: the boot options are %s", self.host, boot_options) + return boot_options def checkpoint(self, filename): """Save a checkpoint of the running configuration to the device. @@ -83,6 +91,7 @@ def checkpoint(self, filename): Args: filename (str): The filename to save the checkpoint on the remote device. """ + log.debug("Host %s: checkpoint is %s.", self.host, filename) return self.native.checkpoint(filename) def close(self): # noqa: D401 @@ -100,7 +109,9 @@ def config(self, command): """ try: self.native.config(command) + log.info("Host %s: Device configured with command %s.", self.host, command) except CLIError as e: + log.error("Host %s: Command error with commands: %s and error message %s", self.host, command, str(e)) raise CommandError(command, str(e)) def config_list(self, commands): @@ -114,7 +125,9 @@ def config_list(self, commands): """ try: self.native.config_list(commands) + log.info("Host %s: Configured with commands: %s", self.host, commands) except CLIError as e: + log.error("Host %s: Command error with commands: %s and error message %s", self.host, commands, str(e)) raise CommandListError(commands, e.command, str(e)) @property @@ -127,6 +140,7 @@ def uptime(self): if self._uptime is None: self._uptime = self.native.facts.get("uptime") + log.debug("Host %s: Uptime %s", self.host, self._uptime) return self._uptime @property @@ -139,6 +153,7 @@ def hostname(self): if self._hostname is None: self._hostname = self.native.facts.get("hostname") + log.debug("Host %s: Hostname %s", self.host, self._hostname) return self._hostname @property @@ -151,6 +166,7 @@ def interfaces(self): if self._interfaces is None: self._interfaces = self.native.facts.get("interfaces") + log.debug("Host %s: Interfaces %s", self.host, self._interfaces) return self._interfaces @property @@ -163,6 +179,7 @@ def vlans(self): if self._vlans is None: self._vlans = self.native.facts.get("vlans") + log.debug("Host %s: Vlans %s", self.host, self._vlans) return self._vlans @property @@ -175,6 +192,7 @@ def fqdn(self): if self._fqdn is None: self._fqdn = self.native.facts.get("fqdn") + log.debug("Host %s: FQDN %s", self.host, self._fqdn) return self._fqdn @property @@ -187,6 +205,7 @@ def model(self): if self._model is None: self._model = self.native.facts.get("model") + log.debug("Host %s: Model %s", self.host, self._model) return self._model @property @@ -199,6 +218,7 @@ def os_version(self): if self._os_version is None: self._os_version = self.native.facts.get("os_version") + log.debug("Host %s: OS version %s", self.host, self._os_version) return self._os_version @property @@ -211,6 +231,7 @@ def serial_number(self): if self._serial_number is None: self._serial_number = self.native.facts.get("serial_number") + log.debug("Host %s: Serial number %s", self.host, self._serial_number) return self._serial_number def file_copy(self, src, dest=None, file_system="bootflash:"): @@ -228,13 +249,17 @@ def file_copy(self, src, dest=None, file_system="bootflash:"): dest = dest or os.path.basename(src) try: file_copy = self.native.file_copy(src, dest, file_system=file_system) + log.info("Host %s: File %s transferred successfully.", self.host, src) if not self.file_copy_remote_exists(src, dest, file_system): - raise FileTransferError( - message="Attempted file copy, but could not validate file existed after transfer" + log.error( + "Host %s: Attempted file copy, but could not validate file existed after transfer %s", + self.host, + FileTransferError.default_message, ) + raise FileTransferError return file_copy except NXOSFileTransferError as e: - print(str(e)) + log.error("Host %s: NXOS file transfer error %s", self.host, str(e)) raise FileTransferError # TODO: Make this an internal method since exposing file_copy should be sufficient @@ -250,6 +275,12 @@ def file_copy_remote_exists(self, src, dest=None, file_system="bootflash:"): bool: True if the remote file exists. Otherwise, false. """ dest = dest or os.path.basename(src) + log.debug( + "Host %s: File %s exists on remote %s.", + self.host, + src, + self.native.file_copy_remote_exists(src, dest, file_system=file_system), + ) return self.native.file_copy_remote_exists(src, dest, file_system=file_system) def install_os(self, image_name, **vendor_specifics): @@ -269,11 +300,14 @@ def install_os(self, image_name, **vendor_specifics): self.set_boot_options(image_name, **vendor_specifics) self._wait_for_device_reboot(timeout=timeout) if not self._image_booted(image_name): + log.error("Host %s: OS install error for image %s", self.host, image_name) raise OSInstallError(hostname=self.facts.get("hostname"), desired_boot=image_name) self.save() + log.info("Host %s: OS image %s installed successfully.", self.host, image_name) return True + log.info("Host %s: OS image %s not installed.", self.host, image_name) return False def open(self): # noqa: D401 @@ -296,12 +330,14 @@ def reboot(self, timer=0, **kwargs): >> """ if kwargs.get("confirm"): - warnings.warn("Passing 'confirm' to reboot method is deprecated.", DeprecationWarning) + log.warning("Passing 'confirm' to reboot method is deprecated.", DeprecationWarning) if timer != 0: + log.error("Host %s: Reboot time error for device type %s.", self.host, self.device_type) raise RebootTimerError(self.device_type) self.native.reboot(confirm=True) + log.info("Host %s: Device rebooted.", self.host) def rollback(self, filename): """Rollback configuration to specified file. @@ -314,7 +350,9 @@ def rollback(self, filename): """ try: self.native.rollback(filename) + log.info("Host %s: Rollback to %s.", self.host, filename) except CLIError: + log.error("Host %s: Rollback unsuccessful. %s may not exist.", self.host, filename) raise RollbackError("Rollback unsuccessful, %s may not exist." % filename) @property @@ -324,6 +362,7 @@ def running_config(self): Returns: str: Running configuration of device. """ + log.debug("Host %s: Show running config.", self.host) return self.native.running_config def save(self, filename="startup-config"): @@ -335,6 +374,7 @@ def save(self, filename="startup-config"): Returns: bool: True if configuration is saved. """ + log.debug("Host %s: Copy running config with name %s.", self.host, filename) return self.native.save(filename=filename) def set_boot_options(self, image_name, kickstart=None, **vendor_specifics): @@ -353,10 +393,12 @@ def set_boot_options(self, image_name, kickstart=None, **vendor_specifics): file_system_files = self.show("dir {0}".format(file_system), raw_text=True) if re.search(image_name, file_system_files) is None: + log.error("Host %s: File not found error for image %s.", self.host, image_name) raise NTCFileNotFoundError(hostname=self.hostname, file=image_name, dir=file_system) if kickstart is not None: if re.search(kickstart, file_system_files) is None: + log.error("Host %s: File not found error for image %s.", self.host, image_name) raise NTCFileNotFoundError(hostname=self.hostname, file=kickstart, dir=file_system) kickstart = file_system + kickstart @@ -373,6 +415,7 @@ def set_boot_options(self, image_name, kickstart=None, **vendor_specifics): upgrade_result = self.native.set_boot_options(image_name, kickstart=kickstart) self.native.timeout = 30 + log.info("Host %s: boot options have been set to %s", self.host, upgrade_result) return upgrade_result def set_timeout(self, timeout): @@ -381,6 +424,7 @@ def set_timeout(self, timeout): Args: timeout (int): Timeout value. """ + log.debug("Host %s: Timeout set to %s.", self.host, timeout) self.native.timeout = timeout def show(self, command, raw_text=False): @@ -397,8 +441,10 @@ def show(self, command, raw_text=False): str: Results of the command ran. """ try: + log.debug("Host %s: Successfully executed command 'show'.", self.host) return self.native.show(command, raw_text=raw_text) except CLIError as e: + log.error("Host %s: Command error %s.", self.host, str(e)) raise CommandError(command, str(e)) def show_list(self, commands, raw_text=False): @@ -415,8 +461,10 @@ def show_list(self, commands, raw_text=False): list: Outputs of all the commands ran on the device. """ try: + log.debug("Host %s: Successfully executed command 'show' with commands %s.", self.host, commands) return self.native.show_list(commands, raw_text=raw_text) except CLIError as e: + log.error("Host %s: Command error for command %s with message %s.", self.host, e.command, str(e)) raise CommandListError(commands, e.command, str(e)) @property diff --git a/pyntc/log.py b/pyntc/log.py new file mode 100644 index 00000000..fdecb586 --- /dev/null +++ b/pyntc/log.py @@ -0,0 +1,78 @@ +"""Logging utilities for Pyntc.""" +import os +import logging + +from logging.handlers import RotatingFileHandler + + +APP = "pyntc" +""" Application name, used as the logging root. """ + +FORMAT = "%(asctime)s [%(levelname)s] %(name)s: %(message)s" +""" Logging format to use. """ + +DEBUG_FORMAT = "%(asctime)s [%(levelname)s] [%(module)s] [%(funcName)s] %(name)s: %(message)s" +""" Logging format used when debug output is enabled. """ + + +def get_log(name=None): + """Get log namespace and creates logger and rotating file handler. + + Args: + name (str, optional): Sublogger name. Defaults to None. + + Returns: + logger: Return a logger instance in the :data:`APP` namespace. + """ + logger_name = f"{APP}.{name}" if name else APP + # file handler + handler = RotatingFileHandler(f"{logger_name}.log", maxBytes=2000) + logger = logging.getLogger(logger_name) + logger.addHandler(handler) + + return logger + + +def init(**kwargs): + """Initialize logging using sensible defaults. + + If keyword arguments are passed to this function, they will be passed + directly to the :func:`logging.basicConfig` call in turn. + + Args: + **kwargs: Arguments to pass for logging configuration + + + """ + debug = os.environ.get("PYNTC_DEBUG", None) + log_format = DEBUG_FORMAT if debug else FORMAT + + log_level = getattr(logging, os.environ.get("PYNTC_LOG_LEVEL", "info").upper()) + log_level = logging.DEBUG if debug else log_level + + kwargs.setdefault("format", log_format) + kwargs.setdefault("level", log_level) + + logging.basicConfig(**kwargs) + # info is defined at the end of the file + info("Logging initialized for host %s.", kwargs.get("host")) + + +def logger(level): + """Wrap around logger methods. + + Args: + level (str): defines the log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) + + Returns: + string: Returns logger. type of string. + """ + return getattr(get_log(), level) + + +critical = logger("critical") +error = logger("error") +warning = logger("warning") +info = logger("info") +debug = logger("debug") +exception = logger("exception") diff --git a/tests/fixtures/.ntc.conf b/tests/fixtures/.ntc.conf index 08490fa6..62cf2ff0 100644 --- a/tests/fixtures/.ntc.conf +++ b/tests/fixtures/.ntc.conf @@ -2,9 +2,11 @@ username: cisco password: !cisco123! transport: http +log_level: INFO [arista_eos_eapi:spine1] host: 176.126.90.158 username: admin password: arista transport: http +log_level: WARNING diff --git a/tests/unit/test_devices/test_aireos_device.py b/tests/unit/test_devices/test_aireos_device.py index 38dbe739..3cf085ef 100644 --- a/tests/unit/test_devices/test_aireos_device.py +++ b/tests/unit/test_devices/test_aireos_device.py @@ -1,10 +1,9 @@ import json - -import pytest from unittest import mock -from pyntc.devices import AIREOSDevice +import pytest from pyntc.devices import aireos_device as aireos_module +from pyntc.devices import AIREOSDevice @pytest.mark.parametrize( @@ -245,7 +244,8 @@ def test_config_pass_string(mock_enter_config, mock_check_for_errors, aireos_con device = aireos_config([""]) result = device.config(command) - assert isinstance(result, str) # TODO: Change to list when deprecating config_list + # TODO: Change to list when deprecating config_list + assert isinstance(result, str) mock_enter_config.assert_called_once() mock_check_for_errors.assert_called_with(command, result) mock_check_for_errors.assert_called_once() @@ -909,7 +909,10 @@ def test_install_os_error_peer( device = aireos_image_booted([False, True]) with pytest.raises(aireos_module.OSInstallError) as boot_error: device.install_os(aireos_boot_image) - assert boot_error.value.message == f"{device.host}-standby was unable to boot into {aireos_boot_image}-standby hot" + assert ( + boot_error.value.message + == f"Host {device.host}: {device.host}-standby was unable to boot into {aireos_boot_image}-standby hot" + ) device._image_booted.assert_has_calls([mock.call(aireos_boot_image)] * 2) @@ -1508,7 +1511,9 @@ def test_transfer_image_to_ap_already_transferred_secondary( @mock.patch.object(AIREOSDevice, "boot_options", new_callable=mock.PropertyMock) @mock.patch.object(AIREOSDevice, "_ap_images_match_expected") @mock.patch.object(AIREOSDevice, "ap_boot_options", new_callable=mock.PropertyMock) +@mock.patch("pyntc.devices.aireos_device.log.error") def test_transfer_image_to_ap_already_transferred_secondary_fail( + mock_log, mock_ap_boot_options, mock_ap_image_matches_expected, mock_boot_options, @@ -1519,13 +1524,14 @@ def test_transfer_image_to_ap_already_transferred_secondary_fail( aireos_boot_image, ): mock_ap_image_matches_expected.side_effect = [False, False, True, True, True, True, False] - with pytest.raises(aireos_module.FileTransferError) as fte: + with pytest.raises(aireos_module.FileTransferError): aireos_device.transfer_image_to_ap(aireos_boot_image) assert len(mock_ap_image_matches_expected.mock_calls) == 7 mock_config.assert_has_calls([mock.call("ap image swap all")] * 3) mock_wait.assert_not_called() mock_boot_options.assert_not_called() - assert fte.value.message == f"Unable to set all APs to use {aireos_boot_image}" + + mock_log.assert_called_once_with("Host %s: Unable to set all APs to use %s", "host", "8.2.170.0") @mock.patch.object(AIREOSDevice, "config") @@ -1581,7 +1587,9 @@ def test_transfer_image_to_ap_transfer_secondary( @mock.patch.object(AIREOSDevice, "boot_options", new_callable=mock.PropertyMock) @mock.patch.object(AIREOSDevice, "_ap_images_match_expected") @mock.patch.object(AIREOSDevice, "ap_boot_options", new_callable=mock.PropertyMock) +@mock.patch("pyntc.devices.aireos_device.log.error") def test_transfer_image_to_ap_transfer_secondary_fail( + mock_log, mock_ap_boot_options, mock_ap_image_matches_expected, mock_boot_options, @@ -1593,13 +1601,14 @@ def test_transfer_image_to_ap_transfer_secondary_fail( ): mock_boot_options.return_value = {"primary": None, "backup": aireos_boot_image} mock_ap_image_matches_expected.side_effect = [False, False, False, True, True, True, False] - with pytest.raises(aireos_module.FileTransferError) as fte: + with pytest.raises(aireos_module.FileTransferError): aireos_device.transfer_image_to_ap(aireos_boot_image) assert len(mock_ap_image_matches_expected.mock_calls) == 7 mock_config.assert_has_calls([mock.call("ap image predownload backup all")] + [mock.call("ap image swap all")] * 3) mock_wait.assert_called() mock_boot_options.assert_has_calls([mock.call(), mock.call()]) - assert fte.value.message == f"Unable to set all APs to use {aireos_boot_image}" + + mock_log.assert_called_once_with("Host %s: Unable to set all APs to use %s", "host", "8.2.170.0") @mock.patch.object(AIREOSDevice, "config") @@ -1629,7 +1638,9 @@ def test_transfer_image_to_ap_transfer_fail_swap_at_first_try( @mock.patch.object(AIREOSDevice, "boot_options", new_callable=mock.PropertyMock) @mock.patch.object(AIREOSDevice, "_ap_images_match_expected") @mock.patch.object(AIREOSDevice, "ap_boot_options", new_callable=mock.PropertyMock) +@mock.patch("pyntc.devices.aireos_device.log.error") def test_transfer_image_does_not_exist( + mock_log, mock_ap_boot_options, mock_ap_image_matches_expected, mock_boot_options, @@ -1641,13 +1652,13 @@ def test_transfer_image_does_not_exist( ): mock_boot_options.return_value = {"primary": None, "backup": None} mock_ap_image_matches_expected.return_value = False - with pytest.raises(aireos_module.FileTransferError) as fte: + with pytest.raises(aireos_module.FileTransferError): aireos_device.transfer_image_to_ap(aireos_boot_image) assert len(mock_ap_image_matches_expected.mock_calls) == 3 mock_config.assert_not_called() mock_wait.assert_not_called() mock_boot_options.assert_has_calls([mock.call(), mock.call()]) - assert fte.value.message == f"Unable to find {aireos_boot_image} on {aireos_device.host}" + mock_log.assert_called_once_with("Host %s: Unable to find %s on host.", "host", "8.2.170.0") @mock.patch.object(AIREOSDevice, "_uptime_components") diff --git a/tests/unit/test_devices/test_asa_device.py b/tests/unit/test_devices/test_asa_device.py index 4ce3f791..7d8adde8 100644 --- a/tests/unit/test_devices/test_asa_device.py +++ b/tests/unit/test_devices/test_asa_device.py @@ -359,7 +359,9 @@ def test_file_copy_transfer_file_error( @mock.patch("pyntc.devices.asa_device.CiscoAsaFileTransfer", spec_set=asa_module.CiscoAsaFileTransfer) @mock.patch.object(ASADevice, "_file_copy_instance") @mock.patch.object(ASADevice, "open") -def test_file_copy_transfer_file_does_not_transfer( +@mock.patch("pyntc.devices.asa_device.log.error") +def test__file_copy_transfer_file_does_not_transfer( + mock_log, mock_open, mock_file_copy_instance, mock_cisco_asa_file_transfer, @@ -369,14 +371,16 @@ def test_file_copy_transfer_file_does_not_transfer( ): args = ("a.txt", "a.txt", "flash:") mock_file_copy_instance.return_value = mock_cisco_asa_file_transfer - with pytest.raises(asa_module.FileTransferError) as err: + with pytest.raises(asa_module.FileTransferError): asa_device._file_copy(*args) mock_enable.assert_called() mock_file_copy_remote_exists.assert_has_calls([mock.call(*args)] * 2) mock_cisco_asa_file_transfer.establish_scp_conn.assert_called_once() mock_cisco_asa_file_transfer.transfer_file.assert_called_once() mock_cisco_asa_file_transfer.close_scp_chan.assert_called_once() - assert err.value.message == "Attempted file copy, but could not validate file existed after transfer" + mock_log.assert_called_with( + "Host %s: Attempted file copy, but could not validate file %s existed after transfer.", "host", "a.txt" + ) @pytest.mark.parametrize("host,command_prefix", (("self", ""), ("peer", "failover exec mate ")), ids=("self", "peer")) @@ -482,11 +486,12 @@ def test_enable_scp_standby_device(mock_save, mock_config, mock_peer_device, moc @mock.patch.object(ASADevice, "peer_device", new_callable=mock.PropertyMock) @mock.patch.object(ASADevice, "config") @mock.patch.object(ASADevice, "save") -def test_enable_scp_device_not_active(mock_save, mock_config, mock_peer_device, mock_is_active, asa_device): +@mock.patch("pyntc.devices.asa_device.log.error") +def test_enable_scp_device_not_active(mock_log, mock_save, mock_config, mock_peer_device, mock_is_active, asa_device): mock_peer_device.return_value = asa_device - with pytest.raises(asa_module.FileTransferError) as err: + with pytest.raises(asa_module.FileTransferError): asa_device.enable_scp() - assert err.value.message == "Unable to establish a connection with the active device" + mock_log.assert_called_once_with("Host %s: Unable to establish a connection with the active device", "host") mock_is_active.assert_has_calls([mock.call()] * 2) mock_peer_device.assert_called() mock_config.assert_not_called() @@ -499,10 +504,11 @@ def test_enable_scp_device_not_active(mock_save, mock_config, mock_peer_device, ASADevice, "config", side_effect=[asa_module.CommandError(command="ssh scopy enable", message="Error")] ) @mock.patch.object(ASADevice, "save") -def test_enable_scp_enable_fail(mock_save, mock_config, mock_peer_device, mock_is_active, asa_device): - with pytest.raises(asa_module.FileTransferError) as err: +@mock.patch("pyntc.devices.asa_device.log.error") +def test_enable_scp_enable_fail(mock_log, mock_save, mock_config, mock_peer_device, mock_is_active, asa_device): + with pytest.raises(asa_module.FileTransferError): asa_device.enable_scp() - assert err.value.message == "Unable to enable scopy on the device" + mock_log.assert_called_once_with("Host %s: Unable to enable scopy on the device", "host") mock_config.assert_called_with("ssh scopy enable") mock_save.assert_not_called() diff --git a/tests/unit/test_devices/test_ios_device.py b/tests/unit/test_devices/test_ios_device.py index 1f9c6178..db4783e5 100644 --- a/tests/unit/test_devices/test_ios_device.py +++ b/tests/unit/test_devices/test_ios_device.py @@ -1,15 +1,14 @@ -import unittest -import mock import os import time +import unittest +import mock import pytest - -from .device_mocks.ios import send_command, send_command_expect -from pyntc.devices.base_device import RollbackError -from pyntc.devices import IOSDevice from pyntc.devices import ios_device as ios_module +from pyntc.devices import IOSDevice +from pyntc.devices.base_device import RollbackError +from .device_mocks.ios import send_command, send_command_expect BOOT_IMAGE = "c3560-advipservicesk9-mz.122-44.SE.bin" BOOT_OPTIONS_PATH = "pyntc.devices.ios_device.IOSDevice.boot_options" @@ -1309,14 +1308,15 @@ def test_install_os_install_mode_with_retries( # Check the results mock_set_boot_options.assert_called_with("packages.conf") - mock_show.assert_called_with( + mock_show.assert_any_call( f"install add file {file_system}{image_name} activate commit prompt-level none", delay_factor=20 ) + mock_show.assert_any_call("show version") mock_reboot.assert_not_called() mock_os_version.assert_called() mock_image_booted.assert_called() assert actual is True - # Assert that fast_cli value was retrieved, set to Fals, and set back to original value + # Assert that fast_cli value was retrieved, set to False, and set back to original value assert mock_fast_cli.call_count == 3 From c9c1a9d3e965b1998103ca59e0fd2244fbd8eac1 Mon Sep 17 00:00:00 2001 From: Jeff Kala <48843785+jeffkala@users.noreply.github.com> Date: Fri, 31 Mar 2023 15:13:10 -0600 Subject: [PATCH 07/13] add asa and aireos properties part 1 (#270) --- pyntc/devices/aireos_device.py | 9 ++ pyntc/devices/asa_device.py | 113 +++++++++++++++++- .../cisco_asa_show_interface.template | 60 +++++++--- .../device_mocks/aireos/show_sysinfo_full.txt | 43 +++++++ .../device_mocks/asa/show_version.txt | 51 ++++++++ tests/unit/test_devices/test_aireos_device.py | 5 + tests/unit/test_devices/test_asa_device.py | 70 +++++++++++ 7 files changed, 333 insertions(+), 18 deletions(-) create mode 100644 tests/unit/test_devices/device_mocks/aireos/show_sysinfo_full.txt create mode 100644 tests/unit/test_devices/device_mocks/asa/show_version.txt diff --git a/pyntc/devices/aireos_device.py b/pyntc/devices/aireos_device.py index b7f67150..9f3ab35b 100644 --- a/pyntc/devices/aireos_device.py +++ b/pyntc/devices/aireos_device.py @@ -991,6 +991,15 @@ def file_copy_remote_exists(self, src, dest=None, **kwargs): """ raise NotImplementedError + @property + def hostname(self): + """Retrieve hostname from sysinfo.""" + sysinfo = self.show("show sysinfo") + re_hostname = r"^System\s+Name\.+\s*(.+?)\s*$" + hostname = re.search(re_hostname, sysinfo, re.M) + hostname_string = hostname.group(1) + return hostname_string + def install_os(self, image_name, controller="both", save_config=True, disable_wlans=None, **vendor_specifics): """ Install an operating system on the controller. diff --git a/pyntc/devices/asa_device.py b/pyntc/devices/asa_device.py index 0337d236..b27f05a8 100644 --- a/pyntc/devices/asa_device.py +++ b/pyntc/devices/asa_device.py @@ -10,6 +10,7 @@ from netmiko import ConnectHandler from netmiko.cisco import CiscoAsaFileTransfer, CiscoAsaSSH + from pyntc import log from pyntc.errors import ( CommandError, @@ -257,15 +258,14 @@ def _send_command(self, command, expect_string=None): def _show_vlan(self): show_vlan_out = self.show("show vlan") - show_vlan_data = get_structured_data("cisco_ios_show_vlan.template", show_vlan_out) - log.debug("Host %s: Successfully executed command 'show vlan' with responses %s.", self.host, show_vlan_data) - return show_vlan_data + log.debug("Host %s: Successfully executed command 'show vlan' with responses %s.", self.host, show_vlan_out) + return show_vlan_out.split(",") def _uptime_components(self, uptime_full_string): match_days = re.search(r"(\d+) days?", uptime_full_string) match_hours = re.search(r"(\d+) hours?", uptime_full_string) - match_minutes = re.search(r"(\d+) minutes?", uptime_full_string) + match_minutes = re.search(r"(\d+) mins?", uptime_full_string) days = int(match_days.group(1)) if match_days else 0 hours = int(match_hours.group(1)) if match_hours else 0 @@ -1144,6 +1144,111 @@ def startup_config(self): """ return self.show("show startup-config") + @property + def uptime(self): + """Get uptime from device. + + Returns: + int: Uptime in seconds. + """ + if self._uptime is None: + version_data = self._raw_version_data() + uptime_full_string = version_data["uptime"] + self._uptime = self._uptime_to_seconds(uptime_full_string) + + return self._uptime + + @property + def uptime_string(self): + """Get uptime in format dd:hh:mm. + + Returns: + str: Uptime of device. + """ + if self._uptime_string is None: + version_data = self._raw_version_data() + uptime_full_string = version_data["uptime"] + self._uptime_string = self._uptime_to_string(uptime_full_string) + + return self._uptime_string + + @property + def hostname(self): + """Get hostname of device. + + Returns: + str: Hostname of device. + """ + version_data = self._raw_version_data() + if self._hostname is None: + self._hostname = version_data["hostname"] + + return self._hostname + + @property + def interfaces(self): + """ + Get list of interfaces on device. + + Returns: + list: List of interfaces on device. + """ + if self._interfaces is None: + self._interfaces = list(x["interface"] for x in self._interfaces_detailed_list()) + + return self._interfaces + + @property + def model(self): + """Get the device model. + + Returns: + str: Device model. + """ + version_data = self._raw_version_data() + if self._model is None: + self._model = version_data["hardware"] + + return self._model + + @property + def os_version(self): + """Get os version on device. + + Returns: + str: OS version on device. + """ + version_data = self._raw_version_data() + if self._os_version is None: + self._os_version = version_data["version"] + + return self._os_version + + @property + def serial_number(self): + """Get serial number of device. + + Returns: + str: Serial number of device. + """ + version_data = self._raw_version_data() + if self._serial_number is None: + self._serial_number = version_data["serial"] + + return self._serial_number + + @property + def vlans(self): + """Get vlan ids from device. + + Returns: + list: List of vlans + """ + if self._vlans is None: + self._vlans = self._show_vlan() + + return self._vlans + class RebootSignal(NTCError): """Not implemented.""" diff --git a/pyntc/utils/templates/cisco_asa_show_interface.template b/pyntc/utils/templates/cisco_asa_show_interface.template index 6083fcf4..065170d5 100644 --- a/pyntc/utils/templates/cisco_asa_show_interface.template +++ b/pyntc/utils/templates/cisco_asa_show_interface.template @@ -1,6 +1,6 @@ Value Required INTERFACE (\S+) Value INTERFACE_ZONE (.+?) -Value LINK_STATUS (\w+) +Value LINK_STATUS (.+?) Value PROTOCOL_STATUS (.*) Value HARDWARE_TYPE ([\w ]+) Value BANDWIDTH (\d+\s+\w+) @@ -10,6 +10,7 @@ Value SPEED (\d+\w+\s\w+) Value DESCRIPTION (.*) Value ADDRESS ([a-zA-Z0-9]+.[a-zA-Z0-9]+.[a-zA-Z0-9]+) Value MTU (\d+) +Value VLAN (\d+) Value IP_ADDRESS (\d+\.\d+\.\d+\.\d+) Value NET_MASK (\d+\.\d+\.\d+\.\d+) Value ONEMIN_IN_PPS (\d+) @@ -24,16 +25,47 @@ Value FIVEMIN_OUT_RATE (\d+) Value FIVEMIN_DROP_RATE (\d+) Start - ^.*Interface ${INTERFACE} "${INTERFACE_ZONE}", is ${LINK_STATUS}.*protocol is ${PROTOCOL_STATUS} - ^\s+Hardware is ${HARDWARE_TYPE} -> Continue - ^.*BW ${BANDWIDTH}.*DLY ${DELAY} - ^.*\(${DUPLEX}.*Auto-Speed\(${SPEED}\) - ^.*Description: ${DESCRIPTION} - ^.*MAC address ${ADDRESS}.*MTU ${MTU} - ^.*IP address ${IP_ADDRESS}, .*subnet mask ${NET_MASK} - ^.*1 minute input rate ${ONEMIN_IN_PPS} pkts/sec,\s+${ONEMIN_IN_RATE} bytes/sec - ^.*1 minute output rate ${ONEMIN_OUT_PPS} pkts/sec,\s+${ONEMIN_OUT_RATE} bytes/sec - ^.*1 minute drop rate, ${ONEMIN_DROP_RATE} - ^.*5 minute input rate ${FIVEMIN_IN_PPS} pkts/sec,\s+${FIVEMIN_IN_RATE} bytes/sec - ^.*5 minute output rate ${FIVEMIN_OUT_PPS} pkts/sec,\s+${FIVEMIN_OUT_RATE} bytes/sec - ^.*5 minute drop rate, ${FIVEMIN_DROP_RATE} -> Record \ No newline at end of file + ^.*Interface\s+ -> Continue.Record + ^.*Interface\s+${INTERFACE}\s+"${INTERFACE_ZONE}",\s+is\s+${LINK_STATUS},.*protocol\s+is\s+${PROTOCOL_STATUS} + ^.*Interface\s+${INTERFACE}.*is\s+${LINK_STATUS},.*protocol\s+is\s+${PROTOCOL_STATUS} + ^\s+Hardware\s+is\s+${HARDWARE_TYPE} -> Continue + ^.*BW\s+${BANDWIDTH},\s+DLY\s+${DELAY} + ^.*\(${DUPLEX}\),\s+Auto-Speed\(${SPEED}\) + ^.*\(${DUPLEX}\),\s+\d+\s+Mbps\(${SPEED}\) + ^.*Duplex,\s+Auto-Speed + ^.*Description:\s+${DESCRIPTION} + ^.*VLAN\s+identifier\s+${VLAN} + ^.*MAC\s+address\s+${ADDRESS},\s+MTU\s+${MTU} + ^.*MAC\s+address\s+${ADDRESS},\s+MTU\s+not\s+set + ^.*IP\s+address\s+${IP_ADDRESS},\s+subnet\s+mask\s+${NET_MASK} + ^.*1\s+minute\s+input\s+rate\s+${ONEMIN_IN_PPS}\s+pkts/sec,\s+${ONEMIN_IN_RATE}\s+bytes/sec + ^.*1\s+minute\s+output\s+rate\s+${ONEMIN_OUT_PPS}\s+pkts/sec,\s+${ONEMIN_OUT_RATE}\s+bytes/sec + ^.*1\s+minute\s+drop\s+rate,\s+${ONEMIN_DROP_RATE} + ^.*5\s+minute\s+input\s+rate\s+${FIVEMIN_IN_PPS}\s+pkts/sec,\s+${FIVEMIN_IN_RATE}\s+bytes/sec + ^.*5\s+minute\s+output\s+rate\s+${FIVEMIN_OUT_PPS}\s+pkts/sec,\s+${FIVEMIN_OUT_RATE}\s+bytes/sec + ^.*5\s+minute\s+drop\s+rate,\s+${FIVEMIN_DROP_RATE} + ^.*Input\s+flow\s+control\s+is\s+unsupported,\s+output\s+flow\s+control\s+is\s+off + ^.*\d+\s+packets\s+input,\s+\d+\s+bytes,\s+\d+\s+no\s+buffer + ^.*Received\s+\d+\s+broadcasts,\s+\d+\s+runts,\s+\d+\s+giants + ^.*\d+\s+input\s+errors,\s+\d+\s+CRC,\s+\d+\s+frame,\s+\d+\s+overrun,\s+\d+\s+ignored,\s+\d+\s+abort + ^.*\d+\s+pause\s+input,\s+\d+\s+resume\s+input + ^.*\d+\s+(L2|output)\s+decode\s+drops + ^.*\d+\s+packets\s+output,\s+\d+\s+bytes,\s+\d+\s+underruns + ^.*\d+\s+pause\s+output,\s+\d+\s+resume\s+output + ^.*\d+\s+output\s+errors,\s+\d+\s+collisions,\s+\d+\s+interface\s+resets + ^.*\d+\s+late\s+collisions,\s+\d+\s+deferred + ^.*\d+\s+input\s+reset\s+drops,\s+\d+\s+output\s+reset\s+drops + ^.*input\s+queue\s+\(blocks\s+free\s+curr\/low\):\s+hardware\s+\(\d+\/\d+\) + ^.*output\s+queue\s+\(blocks\s+free\s+curr\/low\):\s+hardware\s+\(\d+\/\d+\) + ^.*Traffic\s+Statistics\s+for\s+".+?": + ^.*\d+\s+packets\s+input,\s+\d+\s+bytes + ^.*\d+\s+packets\s+output,\s+\d+\s+bytes + ^.*\d+\s+packets\s+dropped + ^.*Management-only\sinterface\.\s+Blocked\s+\d+\s+through-the-device\s+packets + ^.*Input\s+flow\s+control\s+is\s+unsupported,\s+output\s+flow\s+control\s+is\s+unsupported + ^.*Available\s+but\s+not\s+configured\s+via\s+nameif + ^.*IP\s+address\s+unassigned + ^\s*\d+\s+lost\s+carrier,\s+\d+\s+no\s+carrier + ^\s*(input|output)\s+queue\s+\(curr\/max\s+packets\):\s+hardware\s+\(\d+\/\d+\)\s+software\s+\(\d+\/\d+\) + ^.*input\s+queue\s+\(blocks\s+free\s+curr\/low\):\s+hardware\s+\(\d+\/\d+\) + ^\s*$$ diff --git a/tests/unit/test_devices/device_mocks/aireos/show_sysinfo_full.txt b/tests/unit/test_devices/device_mocks/aireos/show_sysinfo_full.txt new file mode 100644 index 00000000..7aaca224 --- /dev/null +++ b/tests/unit/test_devices/device_mocks/aireos/show_sysinfo_full.txt @@ -0,0 +1,43 @@ +Manufacturer's Name.............................. Cisco Systems Inc. +Product Name..................................... Cisco Controller +Product Version.................................. 8.2.110.0 +RTOS Version..................................... 8.2.110.0 +Bootloader Version............................... 8.0.100.0 +Emergency Image Version.......................... 8.0.100.0 + +Build Type....................................... DATA + WPS + +System Name...................................... PYNTCHOST +System Location.................................. USA +System Contact................................... SN:E228240;ASSET:LSMTCc1 +System ObjectID.................................. 1.3.6.1.4.1.9.1.1615 +Redundancy Mode.................................. Disabled +IP Address....................................... 10.10.10.10 +IPv6 Address..................................... :: +System Up Time................................... 328 days 7 hrs 54 mins 49 secs +System Timezone Location......................... (GMT) London, Lisbon, Dublin, Edinburgh +System Stats Realtime Interval................... 5 +System Stats Normal Interval..................... 180 + +Configured Country............................... US - United States +Operating Environment............................ Commercial (10 to 35 C) +Internal Temp Alarm Limits....................... 10 to 38 C +Internal Temperature............................. +18 C +Fan Status....................................... OK + + RAID Volume Status +Drive 0.......................................... Good +Drive 1.......................................... Good + +State of 802.11b Network......................... Enabled +State of 802.11a Network......................... Enabled +Number of WLANs.................................. 1 +Number of Active Clients......................... 0 + +Burned-in MAC Address............................ AA:AA:AA:AA:AA:AA +Power Supply 1................................... Present, OK +Power Supply 2................................... Present, OK +Maximum number of APs supported.................. 6000 +System Nas-Id.................................... +WLC MIC Certificate Types........................ SHA1/SHA2 +Licensing Type................................... RTU \ No newline at end of file diff --git a/tests/unit/test_devices/device_mocks/asa/show_version.txt b/tests/unit/test_devices/device_mocks/asa/show_version.txt new file mode 100644 index 00000000..e18328c4 --- /dev/null +++ b/tests/unit/test_devices/device_mocks/asa/show_version.txt @@ -0,0 +1,51 @@ +Cisco Adaptive Security Appliance Software Version 9.16(2) +SSP Operating System Version 2.10(1.162) +Device Manager Version 7.16(1) + +Compiled on Tue 17-Aug-21 16:32 GMT by builders +System image file is "boot:/asa9162-smp-k8.bin" +Config file at boot was "startup-config" + +ciscoasa up 2 hours 12 mins + +Hardware: ASAv, 2048 MB RAM, CPU Lynnfield 3695 MHz, +Internal ATA Compact Flash, 8192MB +Slot 1: ATA Compact Flash, 8192MB +BIOS Flash Firmware Hub @ 0x1, 0KB + + + 0: Ext: Management0/0 : address is 5254.0009.d178, irq 10 + 1: Ext: GigabitEthernet0/0 : address is 5254.000a.06f3, irq 11 + 2: Int: Internal-Data0/0 : address is 0000.0100.0001, irq 0 + +License mode: Smart Licensing +ASAv Platform License State: Unlicensed +No active entitlement: no feature tier and no throughput level configured +Firewall throughput limited to 100 Kbps + +Licensed features for this platform: +Maximum VLANs : 50 +Inside Hosts : Unlimited +Failover : Active/Active +Encryption-DES : Enabled +Encryption-3DES-AES : Enabled +Security Contexts : 2 +Carrier : Disabled +AnyConnect Premium Peers : 2 +AnyConnect Essentials : Disabled +Other VPN Peers : 250 +Total VPN Peers : 250 +AnyConnect for Mobile : Disabled +AnyConnect for Cisco VPN Phone : Disabled +Advanced Endpoint Assessment : Disabled +Shared License : Disabled +Total TLS Proxy Sessions : 2 +Botnet Traffic Filter : Enabled +Cluster : Disabled + +Serial Number: 9ANMLKTFC5B + +Image type : Release +Key version : A + +Configuration last modified by cisco at 17:47:47.639 UTC Thu Dec 22 2022 diff --git a/tests/unit/test_devices/test_aireos_device.py b/tests/unit/test_devices/test_aireos_device.py index 3cf085ef..60ce6324 100644 --- a/tests/unit/test_devices/test_aireos_device.py +++ b/tests/unit/test_devices/test_aireos_device.py @@ -122,6 +122,11 @@ def test_uptime_components(aireos_show): assert minutes == 20 +def test_hostname(aireos_show): + device = aireos_show(["show_sysinfo_full.txt"]) + assert device.hostname == "PYNTCHOST" + + @mock.patch.object(AIREOSDevice, "ap_image_stats", new_callable=mock.PropertyMock) def test_wait_for_ap_image_download(mock_ap_image_stats, aireos_device): mock_ap_image_stats.side_effect = [ diff --git a/tests/unit/test_devices/test_asa_device.py b/tests/unit/test_devices/test_asa_device.py index 7d8adde8..c53399d0 100644 --- a/tests/unit/test_devices/test_asa_device.py +++ b/tests/unit/test_devices/test_asa_device.py @@ -16,6 +16,25 @@ FAILED = "failed" NOT_DETECTED = "not detected" COLD_STANDBY = "cold standby" +VERSION_DATA = { + "version": "9.16(2)", + "device_mgr_version": "7.16(1)", + "image": "boot:/asa9162-smp-k8.bin", + "hostname": "ciscoasa", + "uptime": "2 hours 7 mins", + "hardware": "ASAv, 2048 MB RAM, CPU Lynnfield 3695 MHz", + "model": "", + "flash": "8192MB", + "interfaces": ["Management0/0", "GigabitEthernet0/0", "Internal-Data0/0"], + "license_mode": "Smart Licensing", + "license_state": "Unlicensed", + "max_intf": "", + "max_vlans": "50", + "failover": "Active/Active", + "cluster": "Disabled", + "serial": "9ANMLKTFC5B", + "last_mod": "cisco at 17:41:15.359 UTC Thu Dec 22 2022", +} class TestASADevice: @@ -825,3 +844,54 @@ def test_send_command_error(asa_send_command_timing): with pytest.raises(asa_module.CommandError): device._send_command(command) device.native.send_command_timing.assert_called() + + +@mock.patch.object(ASADevice, "_raw_version_data", autospec=True) +def test_uptime(mock_raw_version_data, asa_device): + mock_raw_version_data.return_value = VERSION_DATA + uptime = asa_device.uptime + assert uptime == 7620 + + +@mock.patch.object(ASADevice, "_raw_version_data", autospec=True) +def test_uptime_string(mock_raw_version_data, asa_device): + mock_raw_version_data.return_value = VERSION_DATA + uptime_string = asa_device.uptime_string + assert uptime_string == "00:02:07:00" + + +@mock.patch.object(ASADevice, "_raw_version_data", autospec=True) +def test_model(mock_raw_version_data, asa_device): + mock_raw_version_data.return_value = VERSION_DATA + model = asa_device.model + assert model == "ASAv, 2048 MB RAM, CPU Lynnfield 3695 MHz" + + +@mock.patch.object(ASADevice, "_raw_version_data", autospec=True) +def test_os_version(mock_raw_version_data, asa_device): + mock_raw_version_data.return_value = VERSION_DATA + os_version = asa_device.os_version + assert os_version == "9.16(2)" + + +@mock.patch.object(ASADevice, "_raw_version_data", autospec=True) +def test_serial_number(mock_raw_version_data, asa_device): + mock_raw_version_data.return_value = VERSION_DATA + sn = asa_device.serial_number + assert sn == "9ANMLKTFC5B" + + +@mock.patch.object(ASADevice, "_interfaces_detailed_list", autospec=True) +def test_interfaces(mock_get_intf_list, asa_device): + expected = [{"interface": "Management0/0"}, {"interface": "GigabitEthernet0/0"}] + mock_get_intf_list.return_value = expected + interfaces = asa_device.interfaces + assert interfaces == ["Management0/0", "GigabitEthernet0/0"] + + +@mock.patch.object(ASADevice, "_show_vlan", autospec=True) +def test_vlan(mock_get_vlans, asa_device): + expected = [10, 20] + mock_get_vlans.return_value = expected + vlans = asa_device.vlans + assert vlans == [10, 20] From 5ca390678dc9119af3ceca9db9290b8507d322d3 Mon Sep 17 00:00:00 2001 From: Jeff Kala <48843785+jeffkala@users.noreply.github.com> Date: Wed, 5 Apr 2023 11:02:06 -0600 Subject: [PATCH 08/13] Remove ABC, deprecate and remove show_list and config_list methods, add fact properties (#275) * deprecate list methods * remove ABC and use standard python inheritance * clean up commented out code * bump version to alpha and prep changelog --- docs/admin/release_notes/version_1_0.md | 10 +++ mkdocs.yml | 1 + pyntc/devices/aireos_device.py | 62 +------------------ pyntc/devices/asa_device.py | 22 ------- pyntc/devices/base_device.py | 62 ------------------- pyntc/devices/eos_device.py | 23 +------ pyntc/devices/f5_device.py | 3 +- pyntc/devices/ios_device.py | 42 +------------ pyntc/devices/jnpr_device.py | 33 ++-------- pyntc/devices/nxos_device.py | 62 +++++++------------ pyproject.toml | 2 +- tests/unit/test_devices/test_aireos_device.py | 13 ++-- tests/unit/test_devices/test_asa_device.py | 9 +-- tests/unit/test_devices/test_eos_device.py | 15 ++--- tests/unit/test_devices/test_ios_device.py | 9 +-- tests/unit/test_devices/test_jnpr_device.py | 14 ++--- tests/unit/test_devices/test_nxos_device.py | 14 ++--- 17 files changed, 81 insertions(+), 315 deletions(-) create mode 100644 docs/admin/release_notes/version_1_0.md diff --git a/docs/admin/release_notes/version_1_0.md b/docs/admin/release_notes/version_1_0.md new file mode 100644 index 00000000..8b8fe3d3 --- /dev/null +++ b/docs/admin/release_notes/version_1_0.md @@ -0,0 +1,10 @@ +# v1.0 Release Notes + +## [1.0.0a0] 04-2023 + +### Added + +### Changed + +### Deprecated + diff --git a/mkdocs.yml b/mkdocs.yml index 05ab9ac6..5594aec5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -112,6 +112,7 @@ nav: - v0.18: "admin/release_notes/version_0_18.md" - v0.19: "admin/release_notes/version_0_19.md" - v0.20: "admin/release_notes/version_0_20.md" + - v1.0: "admin/release_notes/version_1_0.md" - Developer Guide: - Extending the Library: "dev/extending.md" - Contributing to the Library: "dev/contributing.md" diff --git a/pyntc/devices/aireos_device.py b/pyntc/devices/aireos_device.py index 9f3ab35b..002cc268 100644 --- a/pyntc/devices/aireos_device.py +++ b/pyntc/devices/aireos_device.py @@ -7,6 +7,7 @@ import time from netmiko import ConnectHandler + from pyntc import log from pyntc.errors import ( CommandError, @@ -616,35 +617,6 @@ def config(self, command, **netmiko_args): ) return command_responses - def config_list(self, commands, **netmiko_args): # noqa: D401 - """ - DEPRECATED - Use the `config` method. - - Send config commands to device. - - By default, entering and exiting config mode is handled automatically. - To disable entering and exiting config mode, pass `enter_config_mode` and `exit_config_mode` in ``**netmiko_args``. - This supports all arguments supported by Netmiko's `send_config_set` method using ``netmiko_args``. - - Args: - commands (list): The commands to send to the device. - **netmiko_args: Any argument supported by ``netmiko.base_connection.BaseConnection.send_config_set``. - - Returns: - list: Each command's input and ouput from sending the command in ``commands``. - - Raises: - TypeError: When sending an argument in ``**netmiko_args`` that is not supported. - CommandListError: When one of the commands reports an error on the device. - - Example: - >>> device = AIREOSDevice(**connection_args) - >>> device.config_list(["interface hostname virtual wlc1.site.com", "config interface vlan airway 20"]) - >>> - """ - log.warning("config_list() is deprecated; use config.") - return self.config(commands, **netmiko_args) - def confirm_is_active(self): """ Confirm that the device is either standalone or the active device in a high availability cluster. @@ -1425,38 +1397,6 @@ def show(self, command, expect_string=None, **netmiko_args): ) return command_responses - def show_list(self, commands, **netmiko_args): # noqa: D401 - """ - DEPRECATED - Use the `show` method. - - Send operational commands to the device. - - Args: - commands (list): The list of commands to send to the device. - **netmiko_args: Any argument supported by ``netmiko.ConnectHandler.send_command``. - - Returns: - list: The data returned from the device for all commands. - - Raises: - TypeError: When sending an argument in ``**netmiko_args`` that is not supported. - CommandListError: When the returned data indicates one of the commands failed. - - Example: - >>> device = AIREOSDevice(**connection_args) - >>> command_data = device._send_command(["show sysinfo", "show boot"]) - >>> print(command_data[0]) - Product Version.....8.2.170.0 - System Up Time......3 days 2 hrs 20 mins 30 sec - ... - >>> print(command_data[1]) - Primary Boot Image............................... 8.2.170.0 (default) (active) - Backup Boot Image................................ 8.5.110.0 - >>> - """ - log.warning("show_list() is deprecated; use show.") - return self.show(commands, **netmiko_args) - @property def startup_config(self): """ diff --git a/pyntc/devices/asa_device.py b/pyntc/devices/asa_device.py index b27f05a8..4091a2f2 100644 --- a/pyntc/devices/asa_device.py +++ b/pyntc/devices/asa_device.py @@ -419,17 +419,6 @@ def config(self, command): self.native.exit_config_mode() log.info("Host %s: Device configured with command %s.", self.host, command) - def config_list(self, commands): - """Send configuration commands in list format to a device. - - DEPRECATED - Use the `config` method. - - Args: - commands (list): List with multiple commands. - """ - log.warning("config_list() is deprecated; use config().") - self.config(commands) - @property def connected_interface(self) -> str: """ @@ -1124,17 +1113,6 @@ def show(self, command, expect_string=None): return responses return self._send_command(command, expect_string=expect_string) - def show_list(self, commands): - """Send show commands in list format to a device. - - DEPRECATED - Use the `show` method. - - Args: - commands (list): List with multiple commands. - """ - log.warning("show_list() is deprecated; use show().") - return self.show(commands) - @property def startup_config(self): """ diff --git a/pyntc/devices/base_device.py b/pyntc/devices/base_device.py index 2fdc1ba3..f05ef836 100644 --- a/pyntc/devices/base_device.py +++ b/pyntc/devices/base_device.py @@ -1,6 +1,5 @@ """The module contains the base class that all device classes must inherit from.""" -import abc import importlib import warnings @@ -27,8 +26,6 @@ def fix_docs(cls): class BaseDevice: # pylint: disable=too-many-instance-attributes,too-many-public-methods """Base Device ABC.""" - __metaclass__ = abc.ABCMeta - def __init__( self, host, username, password, device_type=None, **kwargs ): # noqa: D403 # pylint: disable=unused-argument @@ -67,10 +64,6 @@ def _image_booted(self, image_name, **vendor_specifics): """ raise NotImplementedError - #################### - # ABSTRACT METHODS # - #################### - @abc.abstractmethod def backup_running_config(self, filename): """Save a local copy of the running config. @@ -80,7 +73,6 @@ def backup_running_config(self, filename): raise NotImplementedError @property - @abc.abstractmethod def boot_options(self): """Get current boot variables. @@ -91,7 +83,6 @@ def boot_options(self): """ raise NotImplementedError - @abc.abstractmethod def checkpoint(self, filename): """Save a checkpoint of the running configuration to the device. @@ -100,12 +91,10 @@ def checkpoint(self, filename): """ raise NotImplementedError - @abc.abstractmethod def close(self): """Close the connection to the device.""" raise NotImplementedError - @abc.abstractmethod def config(self, command): """Send a configuration command. @@ -117,20 +106,7 @@ def config(self, command): """ raise NotImplementedError - @abc.abstractmethod - def config_list(self, commands): - """Send a list of configuration commands. - - Args: - commands (list): A list of commands to send to the device. - - Raises: - CommandListError: If there is a problem with one of the commands in the list. - """ - raise NotImplementedError - @property - @abc.abstractmethod def uptime(self): """Uptime integer property, part of device facts. @@ -140,7 +116,6 @@ def uptime(self): raise NotImplementedError @property - @abc.abstractmethod def os_version(self): """Operating System string property, part of device facts. @@ -150,7 +125,6 @@ def os_version(self): raise NotImplementedError @property - @abc.abstractmethod def interfaces(self): """Interfaces list of strings property, part of device facts. @@ -160,7 +134,6 @@ def interfaces(self): raise NotImplementedError @property - @abc.abstractmethod def hostname(self): """Host name string property, part of device facts. @@ -170,7 +143,6 @@ def hostname(self): raise NotImplementedError @property - @abc.abstractmethod def fqdn(self): """ Get FQDN of the device. @@ -181,7 +153,6 @@ def fqdn(self): raise NotImplementedError @property - @abc.abstractmethod def uptime_string(self): """Uptime string string property, part of device facts. @@ -191,7 +162,6 @@ def uptime_string(self): raise NotImplementedError @property - @abc.abstractmethod def serial_number(self): """ Get serial number of the device. @@ -202,7 +172,6 @@ def serial_number(self): raise NotImplementedError @property - @abc.abstractmethod def model(self): """Model string property, part of device facts. @@ -212,7 +181,6 @@ def model(self): raise NotImplementedError @property - @abc.abstractmethod def vlans(self): """Vlans lost of strings property, part of device facts. @@ -242,7 +210,6 @@ def facts(self): # noqa 401 if self.device_type == "cisco_ios_ssh": facts[self.device_type] = {"config_register": self.config_register} # pylint: disable=no-member - @abc.abstractmethod def file_copy(self, src, dest=None, **kwargs): """Send a local file to the device. @@ -259,7 +226,6 @@ def file_copy(self, src, dest=None, **kwargs): """ raise NotImplementedError - @abc.abstractmethod def file_copy_remote_exists(self, src, dest=None, **kwargs): """Check if a remote file exists. @@ -281,7 +247,6 @@ def file_copy_remote_exists(self, src, dest=None, **kwargs): True if the remote file exists, False if it doesn't. """ - @abc.abstractmethod def install_os(self, image_name, **vendor_specifics): """Install the OS from specified image_name. @@ -314,12 +279,10 @@ def install_os(self, image_name, **vendor_specifics): """ raise NotImplementedError - @abc.abstractmethod def open(self): """Open a connection to the device.""" raise NotImplementedError - @abc.abstractmethod def reboot(self, timer=0): """Reboot the device. @@ -329,7 +292,6 @@ def reboot(self, timer=0): """ raise NotImplementedError - @abc.abstractmethod def rollback(self, checkpoint_file): """Rollback to a checkpoint file. @@ -339,12 +301,10 @@ def rollback(self, checkpoint_file): raise NotImplementedError @property - @abc.abstractmethod def running_config(self): """Return the running configuration of the device.""" raise NotImplementedError - @abc.abstractmethod def save(self, filename=None): """Save a device's running configuration. @@ -355,7 +315,6 @@ def save(self, filename=None): """ raise NotImplementedError - @abc.abstractmethod def set_boot_options(self, image_name, **vendor_specifics): """Set boot variables like system image and kickstart image. @@ -376,7 +335,6 @@ def set_boot_options(self, image_name, **vendor_specifics): """ raise NotImplementedError - @abc.abstractmethod def show(self, command, raw_text=False): """Send a non-configuration command. @@ -391,31 +349,11 @@ def show(self, command, raw_text=False): """ raise NotImplementedError - @abc.abstractmethod - def show_list(self, commands, raw_text=False): - """Send a list of non-configuration commands. - - Args: - commands (list): A list of commands to send to the device. - - Keyword Args: - raw_text (bool): Whether to return raw text or structured data. - - Returns: - A list of outputs for each show command - """ - raise NotImplementedError - @property - @abc.abstractmethod def startup_config(self): """Return the startup configuration of the device.""" raise NotImplementedError - ################################# - # Inherited implemented methods # - ################################# - def feature(self, feature_name): """Return a feature class based on the ``feature_name`` for the appropriate subclassed device type.""" try: diff --git a/pyntc/devices/eos_device.py b/pyntc/devices/eos_device.py index 32189c5d..4efab307 100644 --- a/pyntc/devices/eos_device.py +++ b/pyntc/devices/eos_device.py @@ -8,6 +8,7 @@ from pyeapi import connect as eos_connect from pyeapi.client import Node as EOSNative from pyeapi.eapilib import CommandError as EOSCommandError + from pyntc import log from pyntc.errors import ( CommandError, @@ -217,17 +218,6 @@ def config(self, commands): raise CommandError(commands, e.message) raise CommandListError(commands, e.commands[len(e.commands) - 1], e.message) - def config_list(self, commands): - """Send configuration commands in list format to a device. - - DEPRECATED - Use the `config` method. - - Args: - commands (list): List with multiple commands. - """ - log.warning("config_list() is deprecated; use config().") - self.config(commands) - def enable(self): """Ensure device is in enable mode. @@ -606,17 +596,6 @@ def show(self, commands, raw_text=False): log.error("Host %s: Command list error for commands %s with message %s.", self.host, commands, e.message) raise CommandListError(commands, e.commands[len(e.commands) - 1], e.message) - def show_list(self, commands): - """Send show commands in list format to a device. - - DEPRECATED - Use the `show` method. - - Args: - commands (list): List with multiple commands. - """ - log.warning("show_list() is deprecated; use show().") - self.show(commands) - @property def startup_config(self): """Get startup configuration. diff --git a/pyntc/devices/f5_device.py b/pyntc/devices/f5_device.py index b257cf6f..7fa64a50 100644 --- a/pyntc/devices/f5_device.py +++ b/pyntc/devices/f5_device.py @@ -8,6 +8,7 @@ import requests from f5.bigip import ManagementRoot + from pyntc import log from pyntc.errors import FileTransferError, NotEnoughFreeSpaceError, NTCFileNotFoundError, OSInstallError @@ -708,7 +709,7 @@ def reboot(self, timer=0, volume=None, **kwargs): log.debug("Host %s: Reboot to volume %s succeeded.", self.host, volume) def rollback(self, checkpoint_file): - """Rollback to checkpoint configurtion file. + """Rollback to checkpoint configuration file. Args: checkpoint_file (str): Name of checkpoint file. diff --git a/pyntc/devices/ios_device.py b/pyntc/devices/ios_device.py index a8a9cffe..21b9dfa3 100644 --- a/pyntc/devices/ios_device.py +++ b/pyntc/devices/ios_device.py @@ -6,6 +6,7 @@ import time from netmiko import ConnectHandler, FileTransfer + from pyntc import log from pyntc.errors import ( CommandError, @@ -398,36 +399,6 @@ def config(self, command, **netmiko_args): log.info("Host %s: Device configured with command responses %s.", self.host, command_responses) return command_responses - def config_list(self, commands, **netmiko_args): # noqa: D401 - r""" - DEPRECATED - Use the `config` method. - - Send config commands to device. - - By default, entering and exiting config mode is handled automatically. - To disable entering and exiting config mode, pass `enter_config_mode` and `exit_config_mode` in ``**netmiko_args``. - This supports all arguments supported by Netmiko's `send_config_set` method using ``netmiko_args``. - - Args: - commands (list): The commands to send to the device. - **netmiko_args: Any argument supported by ``netmiko.ConnectHandler.send_config_set``. - - Returns: - list: Each command's input and output from sending the command in ``commands``. - - Raises: - TypeError: When sending an argument in ``**netmiko_args`` that is not supported. - CommandListError: When one of the commands reports an error on the device. - - Example: - >>> device = IOSDevice(**connection_args) - >>> device.config_list(["interface Gig0/1", "description x-connect"]) - ['host(config)#interface Gig0/1\nhost(config-if)#, 'description x-connect\nhost(config-if)#'] - >>> - """ - log.warning("config_list() is deprecated; use config.") - return self.config(commands, **netmiko_args) - def confirm_is_active(self): """ Confirm that the device is either standalone or the active device in a high availability cluster. @@ -1135,17 +1106,6 @@ def show(self, command, expect_string=None, **netmiko_args): return responses return self._send_command(command, expect_string=expect_string, **netmiko_args) - def show_list(self, commands): - """Send show commands in list format to a device. - - DEPRECATED - Use the `show` method. - - Args: - commands (list): List with multiple commands. - """ - log.warning("show_list() is deprecated; use show().") - return self.show(commands) - @property def startup_config(self): """Get startup configuration. diff --git a/pyntc/devices/jnpr_device.py b/pyntc/devices/jnpr_device.py index 17048ee8..65b369f8 100644 --- a/pyntc/devices/jnpr_device.py +++ b/pyntc/devices/jnpr_device.py @@ -13,6 +13,7 @@ from jnpr.junos.utils.fs import FS as JunosNativeFS from jnpr.junos.utils.scp import SCP from jnpr.junos.utils.sw import SW as JunosNativeSW + from pyntc.errors import CommandError, CommandListError, FileTransferError, RebootTimeoutError from .base_device import BaseDevice, fix_docs @@ -144,12 +145,12 @@ def config(self, commands, format="set"): """Send configuration commands to a device. Args: - commands (str, list): String with single command, or list with multiple commands. + commands (str, list): String with single command, or list with multiple commands. Raises: - ConfigLoadError: Issue with loading the command. - CommandError: Issue with the command provided, if its a single command, passed in as a string. - CommandListError: Issue with a command in the list provided. + ConfigLoadError: Issue with loading the command. + CommandError: Issue with the command provided, if its a single command, passed in as a string. + CommandListError: Issue with a command in the list provided. """ if isinstance(commands, str): try: @@ -166,18 +167,6 @@ def config(self, commands, format="set"): except ConfigLoadError as e: raise CommandListError(commands, command, e.message) - def config_list(self, commands, format="set"): - """Send configuration commands in list format to a device. - - DEPRECATED - Use the `config` method. - - Args: - commands (list): List with multiple commands. - format (str): The Junos format the commands are in. - """ - warnings.warn("config_list() is deprecated; use config().", DeprecationWarning) - self.config(commands, format=format) - @property def connected(self): """Get connection status of device. @@ -463,18 +452,6 @@ def show(self, commands): return responses[0] return responses - def show_list(self, commands, raw_text=True): - """Send show commands in list format to a device. - - DEPRECATED - Use the `show` method. - - Args: - commands (list): List with multiple commands. - raw_text (bool): Return raw text or structured text. - """ - warnings.warn("show_list() is deprecated; use show().", DeprecationWarning) - return self.show(commands) - @property def startup_config(self): """Get startup configuration. diff --git a/pyntc/devices/nxos_device.py b/pyntc/devices/nxos_device.py index 0c3fe599..df4d1a72 100644 --- a/pyntc/devices/nxos_device.py +++ b/pyntc/devices/nxos_device.py @@ -3,6 +3,10 @@ import re import time +from pynxos.device import Device as NXOSNative +from pynxos.errors import CLIError +from pynxos.features.file_copy import FileTransferError as NXOSFileTransferError + from pyntc import log from pyntc.errors import ( CommandError, @@ -12,9 +16,6 @@ OSInstallError, RebootTimeoutError, ) -from pynxos.device import Device as NXOSNative -from pynxos.errors import CLIError -from pynxos.features.file_copy import FileTransferError as NXOSFileTransferError from .base_device import BaseDevice, fix_docs, RebootTimerError, RollbackError @@ -102,11 +103,18 @@ def config(self, command): """Send configuration command. Args: - command (str): command to be sent to the device. + command (str, list): command to be sent to the device. Raises: CommandError: Error if command is not succesfully ran on device. """ + if isinstance(command, list): + try: + self.native.config_list(command) + log.info("Host %s: Configured with commands: %s", self.host, command) + except CLIError as e: + log.error("Host %s: Command error with commands: %s and error message %s", self.host, command, str(e)) + raise CommandListError(command, e.command, str(e)) try: self.native.config(command) log.info("Host %s: Device configured with command %s.", self.host, command) @@ -114,22 +122,6 @@ def config(self, command): log.error("Host %s: Command error with commands: %s and error message %s", self.host, command, str(e)) raise CommandError(command, str(e)) - def config_list(self, commands): - """Send a list of configuration commands. - - Args: - commands (list): A list of commands. - - Raises: - CommandListError: Error if any of the commands in the list fail running on the device. - """ - try: - self.native.config_list(commands) - log.info("Host %s: Configured with commands: %s", self.host, commands) - except CLIError as e: - log.error("Host %s: Command error with commands: %s and error message %s", self.host, commands, str(e)) - raise CommandListError(commands, e.command, str(e)) - @property def uptime(self): """Get uptime of the device in seconds. @@ -161,7 +153,7 @@ def interfaces(self): """Get list of interfaces. Returns: - list: List of interfaces. + list: List of interfaces. """ if self._interfaces is None: self._interfaces = self.native.facts.get("interfaces") @@ -440,6 +432,14 @@ def show(self, command, raw_text=False): Returns: str: Results of the command ran. """ + log.debug("Host %s: Successfully executed command 'show' with responses.", self.host) + if isinstance(command, list): + try: + log.debug("Host %s: Successfully executed command 'show' with commands %s.", self.host, command) + return self.native.show_list(command, raw_text=raw_text) + except CLIError as e: + log.error("Host %s: Command error for command %s with message %s.", self.host, e.command, str(e)) + raise CommandListError(command, e.command, str(e)) try: log.debug("Host %s: Successfully executed command 'show'.", self.host) return self.native.show(command, raw_text=raw_text) @@ -447,26 +447,6 @@ def show(self, command, raw_text=False): log.error("Host %s: Command error %s.", self.host, str(e)) raise CommandError(command, str(e)) - def show_list(self, commands, raw_text=False): - """Send a list of non-configuration commands. - - Args: - commands (str): A list of commands to be sent to the device. - raw_text (bool, optional): Whether to return raw text or structured data. Defaults to False. - - Raises: - CommandListError: Error message stating which command failed. - - Returns: - list: Outputs of all the commands ran on the device. - """ - try: - log.debug("Host %s: Successfully executed command 'show' with commands %s.", self.host, commands) - return self.native.show_list(commands, raw_text=raw_text) - except CLIError as e: - log.error("Host %s: Command error for command %s with message %s.", self.host, e.command, str(e)) - raise CommandListError(commands, e.command, str(e)) - @property def startup_config(self): """Get startup configuration. diff --git a/pyproject.toml b/pyproject.toml index fac1ef73..40e54a36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pyntc" -version = "0.20.3" +version = "1.0.0a0" description = "SDK to simplify common workflows for Network Devices." authors = ["Network to Code, LLC "] readme = "README.md" diff --git a/tests/unit/test_devices/test_aireos_device.py b/tests/unit/test_devices/test_aireos_device.py index 60ce6324..059db59a 100644 --- a/tests/unit/test_devices/test_aireos_device.py +++ b/tests/unit/test_devices/test_aireos_device.py @@ -2,6 +2,7 @@ from unittest import mock import pytest + from pyntc.devices import aireos_device as aireos_module from pyntc.devices import AIREOSDevice @@ -378,14 +379,14 @@ def test_config_pass_invalid_list_command(mock_enter_config, mock_check_for_erro @mock.patch.object(AIREOSDevice, "config") def test_config_list(mock_config, aireos_device): config_commands = ["a", "b"] - aireos_device.config_list(config_commands) + aireos_device.config(config_commands) mock_config.assert_called_with(config_commands) @mock.patch.object(AIREOSDevice, "config") def test_config_list_pass_netmiko_args(mock_config, aireos_device): config_commands = ["a", "b"] - aireos_device.config_list(config_commands, strip_prompt=True) + aireos_device.config(config_commands, strip_prompt=True) mock_config.assert_called_with(config_commands, strip_prompt=True) @@ -1427,7 +1428,7 @@ def test_show_pass_invalid_list_command(mock_check_for_errors, aireos_send_comma @mock.patch.object(AIREOSDevice, "show") def test_show_list(mock_show, aireos_device): commands = ["a", "b"] - aireos_device.show_list(commands) + aireos_device.show(commands) mock_show.assert_called_with(commands) @@ -1435,14 +1436,14 @@ def test_show_list(mock_show, aireos_device): def test_show_list_pass_netmiko_args(mock_show, aireos_device): commands = ["a", "b"] netmiko_args = {"auto_find_prompt": False} - aireos_device.show_list(commands, **netmiko_args) + aireos_device.show(commands, **netmiko_args) mock_show.assert_called_with(commands, auto_find_prompt=False) @mock.patch.object(AIREOSDevice, "show") def test_show_list_pass_expect_string(mock_show, aireos_device): commands = ["a", "b"] - aireos_device.show_list(commands, expect_string="Continue?") + aireos_device.show(commands, expect_string="Continue?") mock_show.assert_called_with(commands, expect_string="Continue?") @@ -1450,7 +1451,7 @@ def test_show_list_pass_expect_string(mock_show, aireos_device): def test_show_list_pass_netmiko_args_and_expect_string(mock_show, aireos_device): commands = ["a", "b"] netmiko_args = {"auto_find_prompt": False} - aireos_device.show_list(commands, expect_string="Continue?", **netmiko_args) + aireos_device.show(commands, expect_string="Continue?", **netmiko_args) mock_show.assert_called_with(commands, expect_string="Continue?", auto_find_prompt=False) diff --git a/tests/unit/test_devices/test_asa_device.py b/tests/unit/test_devices/test_asa_device.py index c53399d0..2fe40328 100644 --- a/tests/unit/test_devices/test_asa_device.py +++ b/tests/unit/test_devices/test_asa_device.py @@ -3,6 +3,7 @@ from unittest import mock import pytest + from pyntc.devices import asa_device as asa_module from pyntc.devices import ASADevice @@ -180,7 +181,7 @@ def test_bad_config(asa_device): def test_config_list(asa_device): commands = ["crypto key generate rsa modulus 2048", "aaa authentication ssh console LOCAL"] - asa_device.config_list(commands) + asa_device.config(commands) for cmd in commands: asa_device.native.send_command_timing.assert_any_call(cmd) @@ -193,7 +194,7 @@ def test_bad_config_list(asa_device): asa_device.native.send_command_timing.side_effect = results with pytest.raises(asa_module.CommandListError, match=commands[1]): - asa_device.config_list(commands) + asa_device.config(commands) def test_show(asa_device): @@ -219,7 +220,7 @@ def test_show_list(asa_device): results = ["console 0", "security-level meh"] asa_device.native.send_command_timing.side_effect = results - result = asa_device.show_list(commands) + result = asa_device.show(commands) assert isinstance(result, list) assert "console" in result[0] assert "security-level" in result[1] @@ -235,7 +236,7 @@ def test_bad_show_list(asa_device): asa_device.native.send_command_timing.side_effect = results with pytest.raises(asa_module.CommandListError, match="show badcommand"): - asa_device.show_list(commands) + asa_device.show(commands) def test_save(asa_device): diff --git a/tests/unit/test_devices/test_eos_device.py b/tests/unit/test_devices/test_eos_device.py index 443e0fa1..fad0e06c 100644 --- a/tests/unit/test_devices/test_eos_device.py +++ b/tests/unit/test_devices/test_eos_device.py @@ -1,17 +1,18 @@ +import os +import time import unittest + import mock -import os import pytest -import time -from .device_mocks.eos import enable, config -from .device_mocks.eos import send_command, send_command_expect from pyntc.devices import EOSDevice -from pyntc.devices.base_device import RollbackError, RebootTimerError +from pyntc.devices.base_device import RebootTimerError, RollbackError from pyntc.devices.eos_device import FileTransferError from pyntc.devices.system_features.vlans.eos_vlans import EOSVlans from pyntc.errors import CommandError, CommandListError +from .device_mocks.eos import config, enable, send_command, send_command_expect + class TestEOSDevice(unittest.TestCase): @mock.patch("pyeapi.client.Node", autospec=True) @@ -44,7 +45,7 @@ def test_config_pass_list(self): @mock.patch.object(EOSDevice, "config") def test_config_list(self, mock_config): commands = ["interface Eth1", "no shutdown"] - self.device.config_list(commands) + self.device.config(commands) self.device.config.assert_called_with(commands) def test_bad_config_pass_string(self): @@ -149,7 +150,7 @@ def test_show_raw_text(self, mock_parse): @mock.patch.object(EOSDevice, "show") def test_show_list(self, mock_config): commands = ["show hostname", "show clock"] - self.device.show_list(commands) + self.device.show(commands) self.device.show.assert_called_with(commands) def test_save(self): diff --git a/tests/unit/test_devices/test_ios_device.py b/tests/unit/test_devices/test_ios_device.py index db4783e5..2f9ebcd9 100644 --- a/tests/unit/test_devices/test_ios_device.py +++ b/tests/unit/test_devices/test_ios_device.py @@ -4,6 +4,7 @@ import mock import pytest + from pyntc.devices import ios_device as ios_module from pyntc.devices import IOSDevice from pyntc.devices.base_device import RollbackError @@ -90,7 +91,7 @@ def test_bad_show_list(self): self.device.native.send_command.side_effect = results with self.assertRaisesRegex(ios_module.CommandListError, "show badcommand"): - self.device.show_list(commands) + self.device.show(commands) def test_save(self): result = self.device.save() @@ -544,14 +545,14 @@ def test_config_pass_invalid_list_command(mock_enter_config, mock_check_for_erro @mock.patch.object(IOSDevice, "config") def test_config_list(mock_config, ios_device): config_commands = ["a", "b"] - ios_device.config_list(config_commands) + ios_device.config(config_commands) mock_config.assert_called_with(config_commands) @mock.patch.object(IOSDevice, "config") def test_config_list_pass_netmiko_args(mock_config, ios_device): config_commands = ["a", "b"] - ios_device.config_list(config_commands, strip_prompt=True) + ios_device.config(config_commands, strip_prompt=True) mock_config.assert_called_with(config_commands, strip_prompt=True) @@ -1370,7 +1371,7 @@ def test_show_netmiko_args(ios_send_command): def test_show_list(ios_native_send_command): commands = ["show_version", "show_ip_arp"] device = ios_native_send_command([f"{commands[0]}.txt", f"{commands[1]}"]) - device.show_list(commands) + device.show(commands) device.native.send_command.assert_has_calls( [mock.call(command_string="show_version"), mock.call(command_string="show_ip_arp")] ) diff --git a/tests/unit/test_devices/test_jnpr_device.py b/tests/unit/test_devices/test_jnpr_device.py index 256aa115..d55c7147 100644 --- a/tests/unit/test_devices/test_jnpr_device.py +++ b/tests/unit/test_devices/test_jnpr_device.py @@ -1,16 +1,14 @@ +import os import unittest +from tempfile import NamedTemporaryFile + import mock -import os import pytest - -from tempfile import NamedTemporaryFile +from jnpr.junos.exception import ConfigLoadError from pyntc.devices import JunosDevice from pyntc.errors import CommandError, CommandListError -from jnpr.junos.exception import ConfigLoadError - - DEVICE_FACTS = { "domain": "ntc.com", "hostname": "vmx3", @@ -78,7 +76,7 @@ def test_config_pass_list(self): def test_config_list(self, mock_config): commands = ["set interfaces lo0", "set snmp community jason"] - self.device.config_list(commands, format="set") + self.device.config(commands, format="set") self.device.config.assert_called_with(commands, format="set") def test_bad_config_pass_string(self): @@ -164,7 +162,7 @@ def test_bad_show_non_show_pass_list(self): def test_show_list(self, mock_show): commands = ["show vlans", "show snmp v3"] - self.device.show_list(commands) + self.device.show(commands) self.device.show.assert_called_with(commands) @mock.patch("pyntc.devices.jnpr_device.SCP", autospec=True) diff --git a/tests/unit/test_devices/test_nxos_device.py b/tests/unit/test_devices/test_nxos_device.py index 3f494cd1..5e0aad5e 100644 --- a/tests/unit/test_devices/test_nxos_device.py +++ b/tests/unit/test_devices/test_nxos_device.py @@ -1,13 +1,13 @@ import unittest -import mock +import mock from pynxos.errors import CLIError -from .device_mocks.nxos import show, show_list +from pyntc.devices.base_device import RebootTimerError, RollbackError from pyntc.devices.nxos_device import NXOSDevice -from pyntc.devices.base_device import RollbackError, RebootTimerError from pyntc.errors import CommandError, CommandListError, FileTransferError, NTCFileNotFoundError +from .device_mocks.nxos import show, show_list BOOT_IMAGE = "n9000-dk9.9.2.1.bin" KICKSTART_IMAGE = "n9000-kickstart.9.2.1.bin" @@ -53,7 +53,7 @@ def test_bad_config(self): def test_config_list(self): commands = ["interface eth 1/1", "no shutdown"] - result = self.device.config_list(commands) + result = self.device.config(commands) self.assertIsNone(result) self.device.native.config_list.assert_called_with(commands) @@ -63,7 +63,7 @@ def test_bad_config_list(self): self.device.native.config_list.side_effect = CLIError(commands[1], "Invalid command.") with self.assertRaisesRegex(CommandListError, commands[1]): - self.device.config_list(commands) + self.device.config(commands) def test_show(self): command = "show cdp neighbors" @@ -90,7 +90,7 @@ def test_show_raw_text(self): def test_show_list(self): commands = ["show hostname", "show clock"] - result = self.device.show_list(commands) + result = self.device.show(commands) self.assertIsInstance(result, list) self.assertIn("hostname", result[0]) @@ -101,7 +101,7 @@ def test_show_list(self): def test_bad_show_list(self): commands = ["show badcommand", "show clock"] with self.assertRaisesRegex(CommandListError, "show badcommand"): - self.device.show_list(commands) + self.device.show(commands) def test_save(self): result = self.device.save() From 0910b54eac67811e15be5895fa679508083afb4f Mon Sep 17 00:00:00 2001 From: Jeff Kala <48843785+jeffkala@users.noreply.github.com> Date: Thu, 6 Apr 2023 07:55:04 -0600 Subject: [PATCH 09/13] Update nxos_device.py fix missing else branch --- pyntc/devices/nxos_device.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pyntc/devices/nxos_device.py b/pyntc/devices/nxos_device.py index df4d1a72..9caa0179 100644 --- a/pyntc/devices/nxos_device.py +++ b/pyntc/devices/nxos_device.py @@ -115,12 +115,13 @@ def config(self, command): except CLIError as e: log.error("Host %s: Command error with commands: %s and error message %s", self.host, command, str(e)) raise CommandListError(command, e.command, str(e)) - try: - self.native.config(command) - log.info("Host %s: Device configured with command %s.", self.host, command) - except CLIError as e: - log.error("Host %s: Command error with commands: %s and error message %s", self.host, command, str(e)) - raise CommandError(command, str(e)) + else: + try: + self.native.config(command) + log.info("Host %s: Device configured with command %s.", self.host, command) + except CLIError as e: + log.error("Host %s: Command error with commands: %s and error message %s", self.host, command, str(e)) + raise CommandError(command, str(e)) @property def uptime(self): From 77c0d4390d735a0c9c655d713b1af6b8506ef483 Mon Sep 17 00:00:00 2001 From: Jeff Kala <48843785+jeffkala@users.noreply.github.com> Date: Fri, 7 Apr 2023 10:16:16 -0600 Subject: [PATCH 10/13] More Prep for 1.0 Release (#280) * another round of fixes and deprecations to prep 1.0 * standardize reboot method * fix wait for reload on nxos, move to show_list for reboot nxos --- pyntc/devices/aireos_device.py | 49 ++++++++----------- pyntc/devices/asa_device.py | 39 ++++++--------- pyntc/devices/base_device.py | 13 +++-- pyntc/devices/eos_device.py | 17 +++---- pyntc/devices/f5_device.py | 7 ++- pyntc/devices/ios_device.py | 44 +++++++---------- pyntc/devices/iosxewlc_device.py | 8 +-- pyntc/devices/jnpr_device.py | 14 +++--- pyntc/devices/nxos_device.py | 34 +++++++------ .../device_mocks/nxos/show/reload | 1 + .../device_mocks/nxos/show/terminal_dont-ask | 1 + tests/unit/test_devices/test_aireos_device.py | 29 ++++------- tests/unit/test_devices/test_asa_device.py | 5 -- tests/unit/test_devices/test_eos_device.py | 6 +-- tests/unit/test_devices/test_ios_device.py | 4 -- tests/unit/test_devices/test_jnpr_device.py | 4 -- tests/unit/test_devices/test_nxos_device.py | 9 ++-- 17 files changed, 118 insertions(+), 166 deletions(-) create mode 100644 tests/unit/test_devices/device_mocks/nxos/show/reload create mode 100644 tests/unit/test_devices/device_mocks/nxos/show/terminal_dont-ask diff --git a/pyntc/devices/aireos_device.py b/pyntc/devices/aireos_device.py index 002cc268..7294bdda 100644 --- a/pyntc/devices/aireos_device.py +++ b/pyntc/devices/aireos_device.py @@ -3,12 +3,13 @@ import json import os import re -import signal import time from netmiko import ConnectHandler +from netmiko.exceptions import ReadTimeout from pyntc import log +from pyntc.devices.base_device import BaseDevice, fix_docs from pyntc.errors import ( CommandError, CommandListError, @@ -23,8 +24,6 @@ WLANEnableError, ) -from .base_device import BaseDevice, fix_docs - RE_FILENAME_FIND_VERSION = re.compile(r"^.+?(?P\d+(?:-|_)\d+(?:-|_)\d+(?:-|_)\d+)\.", re.M) RE_AP_IMAGE_COUNT = re.compile(r"^[Tt]otal\s+number\s+of\s+APs\.+\s+(?P\d+)\s*$", re.M) RE_AP_IMAGE_DOWNLOADED = re.compile(r"^\s*[Cc]ompleted\s+[Pp]redownloading\.+\s+(?P\d+)\s*$", re.M) @@ -1137,12 +1136,12 @@ def peer_redundancy_state(self): log.debug("Host %s: Peer redundancy state: %s.", self.host, peer_redundancy_state) return peer_redundancy_state - def reboot(self, timer=0, controller="self", save_config=True, **kwargs): + def reboot(self, wait_for_reload=False, controller="self", save_config=True, **kwargs): """ Reload the controller or controller pair. Args: - timer (int): The time to wait before reloading. + wait_for_reload: Whether or not reboot method should also run _wait_for_device_reboot(). Defaults to False. controller (str): Which controller(s) to reboot (only applies to HA pairs). save_config (bool): Whether the configuration should be saved before reload. @@ -1157,39 +1156,33 @@ def reboot(self, timer=0, controller="self", save_config=True, **kwargs): if kwargs.get("confirm"): log.warning("Passing 'confirm' to reboot method is deprecated.") - def handler(signum, frame): - log.error("Host %s: Interrupting after reload.", self.host) - raise RebootSignal - - signal.signal(signal.SIGALRM, handler) - if self.redundancy_mode != "sso disabled": reboot_command = f"reset system {controller}" else: reboot_command = "reset system" - if timer: - reboot_command += f" in {timer}" - if save_config: self.save() - signal.alarm(20) try: response = self.native.send_command_timing(reboot_command) - if "save" in response: - if not save_config: - response = self.native.send_command_timing("n") - else: - response = self.native.send_command_timing("y") - if "reset" in response: - self.native.send_command_timing("y") - except RebootSignal: - signal.alarm(0) - - signal.alarm(0) - - log.info("Host %s: Device rebooted.", self.host) + try: + if "save" in response: + if not save_config: + response = self.native.send_command_timing("n") + else: + response = self.native.send_command_timing("y") + if "reset" in response: + self.native.send_command_timing("y") + except ReadTimeout as expected_exception: + log.info("Host %s: Device rebooted.", self.host) + log.info("Hit expected exception during reload: %s", expected_exception.__class__) + if wait_for_reload: + time.sleep(10) + self._wait_for_device_reboot() + except Exception as err: + log.error(err) + log.error(err.__class__) @property def redundancy_mode(self): diff --git a/pyntc/devices/asa_device.py b/pyntc/devices/asa_device.py index 4091a2f2..f3352a22 100644 --- a/pyntc/devices/asa_device.py +++ b/pyntc/devices/asa_device.py @@ -2,7 +2,6 @@ import os import re -import signal import time from collections import Counter from ipaddress import ip_address, IPv4Address, IPv4Interface, IPv6Address, IPv6Interface @@ -10,8 +9,10 @@ from netmiko import ConnectHandler from netmiko.cisco import CiscoAsaFileTransfer, CiscoAsaSSH +from netmiko.exceptions import ReadTimeout from pyntc import log +from pyntc.devices.base_device import BaseDevice, fix_docs from pyntc.errors import ( CommandError, CommandListError, @@ -24,8 +25,6 @@ ) from pyntc.utils import get_structured_data -from .base_device import BaseDevice, fix_docs - RE_SHOW_FAILOVER_GROUPS = re.compile(r"Group\s+\d+\s+State:\s+(.+?)\s*$", re.M) RE_SHOW_FAILOVER_STATE = re.compile(r"(?:Primary|Secondary)\s+-\s+(.+?)\s*$", re.M) RE_SHOW_IP_ADDRESS = re.compile(r"^\S+\s+(\S+)\s+((?:\d+.){3}\d+)\s+((?:\d+.){3}\d+)", re.M) @@ -858,12 +857,12 @@ def peer_redundancy_state(self): log.debug("Host %s: Peer redundancy state: %s.", self.host, peer_redundancy_state) return peer_redundancy_state.lower() - def reboot(self, timer=0, **kwargs): + def reboot(self, wait_for_reload=False, **kwargs): """ Reload the controller or controller pair. Args: - timer (int, optional): The time to wait before reloading. Defaults to 0. + wait_for_reload: Whether or not reboot method should also run _wait_for_device_reboot(). Defaults to False. Raises: RebootTimeoutError: When the device is still unreachable after the timeout period. @@ -876,29 +875,23 @@ def reboot(self, timer=0, **kwargs): if kwargs.get("confirm"): log.warning("Passing 'confirm' to reboot method is deprecated.") - def handler(signum, frame): - log.error("Host %s: Interrupting after reload.", self.host) - raise RebootSignal - - signal.signal(signal.SIGALRM, handler) - signal.alarm(10) - try: - if timer > 0: - first_response = self.show("reload in %d" % timer) - else: - first_response = self.show("reload") + first_response = self.show("reload") if "System configuration" in first_response: self.native.send_command_timing("no") - self.native.send_command_timing("\n") - except RebootSignal: - signal.alarm(0) - - signal.alarm(0) - - log.info("Host %s: Device rebooted.", self.host) + try: + self.native.send_command_timing("\n", read_timeout=10) + except ReadTimeout as expected_exception: + log.info("Host %s: Device rebooted.", self.host) + log.info("Hit expected exception during reload: %s", expected_exception.__class__) + if wait_for_reload: + time.sleep(10) + self._wait_for_device_reboot() + except Exception as err: + log.error(err) + log.error(err.__class__) def reboot_standby(self, acceptable_states: Optional[Iterable[str]] = None, timeout: Optional[int] = None) -> None: """ diff --git a/pyntc/devices/base_device.py b/pyntc/devices/base_device.py index f05ef836..6c496d6c 100644 --- a/pyntc/devices/base_device.py +++ b/pyntc/devices/base_device.py @@ -283,12 +283,17 @@ def open(self): """Open a connection to the device.""" raise NotImplementedError - def reboot(self, timer=0): - """Reboot the device. + def reboot(self, wait_for_reload=False): + """Reload a device. Args: - confirm(bool): if False, this method has no effect. - timer(int): number of seconds to wait before rebooting. + wait_for_reload: Whether or not reboot method should also run _wait_for_device_reboot(). Defaults to False. + + Raises: + RebootTimeoutError: When the device is still unreachable after the timeout period. + + Raises: + NotImplementedError: _description_ """ raise NotImplementedError diff --git a/pyntc/devices/eos_device.py b/pyntc/devices/eos_device.py index 4efab307..2f3407f8 100644 --- a/pyntc/devices/eos_device.py +++ b/pyntc/devices/eos_device.py @@ -10,6 +10,8 @@ from pyeapi.eapilib import CommandError as EOSCommandError from pyntc import log +from pyntc.devices.base_device import BaseDevice, fix_docs, RollbackError +from pyntc.devices.system_features.vlans.eos_vlans import EOSVlans from pyntc.errors import ( CommandError, CommandListError, @@ -22,9 +24,6 @@ ) from pyntc.utils import convert_list_by_key -from .base_device import BaseDevice, fix_docs, RebootTimerError, RollbackError -from .system_features.vlans.eos_vlans import EOSVlans - BASIC_FACTS_KM = {"model": "modelName", "os_version": "internalVersion", "serial_number": "serialNumber"} INTERFACES_KM = { "speed": "bandwidth", @@ -472,12 +471,12 @@ def open(self): log.debug("Host %s: Connection to controller was opened successfully.", self.host) - def reboot(self, timer=0, **kwargs): + def reboot(self, wait_for_reload=False, **kwargs): """ Reload the controller or controller pair. Args: - timer (int, optional): The time to wait before reloading. Defaults to 0. + wait_for_reload: Whether or not reboot method should also run _wait_for_device_reboot(). Defaults to False. Raises: RebootTimeoutError: When the device is still unreachable after the timeout period. @@ -491,12 +490,10 @@ def reboot(self, timer=0, **kwargs): if kwargs.get("confirm"): log.warning("Passing 'confirm' to reboot method is deprecated.") - if timer != 0: - log.error("Host %s: Reboot time error.", self.host) - raise RebootTimerError(self.device_type) - self.show("reload now") log.info("Host %s: Device rebooted.", self.host) + if wait_for_reload: + self._wait_for_device_reboot() def rollback(self, rollback_to): """Rollback device configuration. @@ -505,7 +502,7 @@ def rollback(self, rollback_to): rollback_to (str): Name of file to revert configuration to. Raises: - RollbackError: When rollback is unsuccesful. + RollbackError: When rollback is unsuccessful. """ try: self.show("configure replace %s force" % rollback_to) diff --git a/pyntc/devices/f5_device.py b/pyntc/devices/f5_device.py index 7fa64a50..b3eb8cf8 100644 --- a/pyntc/devices/f5_device.py +++ b/pyntc/devices/f5_device.py @@ -10,10 +10,9 @@ from f5.bigip import ManagementRoot from pyntc import log +from pyntc.devices.base_device import BaseDevice from pyntc.errors import FileTransferError, NotEnoughFreeSpaceError, NTCFileNotFoundError, OSInstallError -from .base_device import BaseDevice - class F5Device(BaseDevice): """F5 LTM Device Implementation.""" @@ -677,13 +676,13 @@ def open(self): """Implement ``pass``.""" pass - def reboot(self, timer=0, volume=None, **kwargs): + def reboot(self, wait_for_reload=False, volume=None, **kwargs): """ Reload the controller or controller pair. Args: - timer (int, optional): The time to wait before reloading. Defaults to 0. volume (str, optional): Active volume to reboot. Defaults to None. + wait_for_reload: Whether or not reboot method should also run _wait_for_device_reboot(). Defaults to False. Raises: RuntimeError: If device is unreachable after timeout period, raise an error. diff --git a/pyntc/devices/ios_device.py b/pyntc/devices/ios_device.py index 21b9dfa3..77c13521 100644 --- a/pyntc/devices/ios_device.py +++ b/pyntc/devices/ios_device.py @@ -2,12 +2,13 @@ import os import re -import signal import time from netmiko import ConnectHandler, FileTransfer +from netmiko.exceptions import ReadTimeout from pyntc import log +from pyntc.devices.base_device import BaseDevice, fix_docs, RollbackError from pyntc.errors import ( CommandError, CommandListError, @@ -22,8 +23,6 @@ ) from pyntc.utils import get_structured_data -from .base_device import BaseDevice, fix_docs, RollbackError - BASIC_FACTS_KM = {"model": "hardware", "os_version": "version", "serial_number": "serial", "hostname": "hostname"} RE_SHOW_REDUNDANCY = re.compile( r"^Redundant\s+System\s+Information\s*:\s*\n^-.+-\s*\n(?P.+?)\n" @@ -238,7 +237,7 @@ def _wait_for_device_reboot(self, timeout=3600): try: self.open() self.show("show version") - log.debug("Host %s: Device rebooted.", self.host) + log.debug("Host %s: Device reboot Completed.", self.host) if self._has_reload_happened_recently(): return except: # noqa E722 # nosec @@ -755,6 +754,7 @@ def install_os(self, image_name, install_mode=False, install_mode_delay_factor=2 self.reboot() # Wait for the reboot to finish + # TODO: This was moved into reboot method as well, should cause issues running again, but should be removed in future versions. self._wait_for_device_reboot(timeout=timeout) # Set FastCLI back to originally set when using install mode @@ -870,13 +870,13 @@ def peer_redundancy_state(self): log.debug("Host %s: Processor redundancy state %s.", self.host, processor_redundancy_state) return processor_redundancy_state - def reboot(self, timer=0, **kwargs): + def reboot(self, wait_for_reload=False, **kwargs): """Reboot device. Reload the controller or controller pair. Args: - timer (int): The time to wait before reloading. + wait_for_reload: Whether or not reboot method should also run _wait_for_device_reboot(). Defaults to False. Raises: ReloadTimeoutError: When the device is still unreachable after the timeout period. @@ -884,31 +884,23 @@ def reboot(self, timer=0, **kwargs): if kwargs.get("confirm"): log.warning("Passing 'confirm' to reboot method is deprecated.") - def handler(signum, frame): - log.error("Host %s: Reboot signal error interrupting after reload.", self.host) - raise RebootSignal - - signal.signal(signal.SIGALRM, handler) - signal.alarm(10) - try: - if timer > 0: - first_response = self.native.send_command_timing("reload in %d" % timer) - else: - first_response = self.native.send_command_timing("reload") + first_response = self.native.send_command_timing("reload") if "System configuration" in first_response: self.native.send_command_timing("no") - self.native.send_command_timing("\n") - except RebootSignal: - log.error("Host %s: Reboot signal error.", self.host) - signal.alarm(0) - - signal.alarm(0) - log.info("Host %s: Device rebooted.", self.host) - # else: - # print("Need to confirm reboot with confirm=True") + try: + self.native.send_command_timing("\n", read_timeout=10) + except ReadTimeout as expected_exception: + log.info("Host %s: Device rebooted.", self.host) + log.info("Hit expected exception during reload: %s", expected_exception.__class__) + if wait_for_reload: + time.sleep(10) + self._wait_for_device_reboot() + except Exception as err: + log.error(err) + log.error(err.__class__) @property def redundancy_mode(self): diff --git a/pyntc/devices/iosxewlc_device.py b/pyntc/devices/iosxewlc_device.py index 62c8ced5..c24015d2 100644 --- a/pyntc/devices/iosxewlc_device.py +++ b/pyntc/devices/iosxewlc_device.py @@ -1,13 +1,9 @@ """Module for using a Cisco IOSXE WLC device over SSH.""" import time -from .ios_device import IOSDevice -from pyntc.errors import ( - RebootTimeoutError, - OSInstallError, - WaitingRebootTimeoutError, -) from pyntc import log +from pyntc.devices.ios_device import IOSDevice +from pyntc.errors import OSInstallError, RebootTimeoutError, WaitingRebootTimeoutError INSTALL_MODE_FILE_NAME = "packages.conf" diff --git a/pyntc/devices/jnpr_device.py b/pyntc/devices/jnpr_device.py index 65b369f8..46c006aa 100644 --- a/pyntc/devices/jnpr_device.py +++ b/pyntc/devices/jnpr_device.py @@ -14,11 +14,10 @@ from jnpr.junos.utils.scp import SCP from jnpr.junos.utils.sw import SW as JunosNativeSW +from pyntc.devices.base_device import BaseDevice, fix_docs +from pyntc.devices.tables.jnpr.loopback import LoopbackTable from pyntc.errors import CommandError, CommandListError, FileTransferError, RebootTimeoutError -from .base_device import BaseDevice, fix_docs -from .tables.jnpr.loopback import LoopbackTable - @fix_docs class JunosDevice(BaseDevice): @@ -342,12 +341,12 @@ def open(self): if not self.connected: self.native.open() - def reboot(self, timer=0, **kwargs): + def reboot(self, wait_for_reload=False, **kwargs): """ Reload the controller or controller pair. Args: - timer (int, optional): The time to wait before reloading. Defaults to 0. + wait_for_reload: Whether or not reboot method should also run _wait_for_device_reboot(). Defaults to False. Example: >>> device = JunosDevice(**connection_args) @@ -358,7 +357,10 @@ def reboot(self, timer=0, **kwargs): warnings.warn("Passing 'confirm' to reboot method is deprecated.", DeprecationWarning) self.sw = JunosNativeSW(self.native) - self.sw.reboot(in_min=timer) + self.sw.reboot(in_min=0) + if wait_for_reload: + time.sleep(10) + self._wait_for_device_reboot() def rollback(self, filename): """Rollback to a specific configuration file. diff --git a/pyntc/devices/nxos_device.py b/pyntc/devices/nxos_device.py index 9caa0179..d9379c48 100644 --- a/pyntc/devices/nxos_device.py +++ b/pyntc/devices/nxos_device.py @@ -6,8 +6,10 @@ from pynxos.device import Device as NXOSNative from pynxos.errors import CLIError from pynxos.features.file_copy import FileTransferError as NXOSFileTransferError +from requests.exceptions import ReadTimeout from pyntc import log +from pyntc.devices.base_device import BaseDevice, fix_docs, RollbackError from pyntc.errors import ( CommandError, CommandListError, @@ -17,8 +19,6 @@ RebootTimeoutError, ) -from .base_device import BaseDevice, fix_docs, RebootTimerError, RollbackError - @fix_docs class NXOSDevice(BaseDevice): @@ -52,14 +52,13 @@ def _image_booted(self, image_name, **vendor_specifics): log.info("Host %s: Image %s not booted successfully.", self.host, image_name) return False - def _wait_for_device_reboot(self, timeout=600): + def _wait_for_device_reboot(self, timeout=3600): start = time.time() while time.time() - start < timeout: try: - self.refresh_facts() - if self.uptime < 180: - log.debug("Host %s: Device rebooted.", self.host) - return + self.native.show("show hostname") + log.debug("Host %s: Device rebooted.", self.host) + return except: # noqa E722 # nosec pass @@ -288,6 +287,7 @@ def install_os(self, image_name, **vendor_specifics): Returns: bool: True if new image is boot option on device. Otherwise, false. """ + self.native.show("terminal dont-ask") timeout = vendor_specifics.get("timeout", 3600) if not self._image_booted(image_name): self.set_boot_options(image_name, **vendor_specifics) @@ -307,12 +307,12 @@ def open(self): # noqa: D401 """Implements ``pass``.""" pass - def reboot(self, timer=0, **kwargs): + def reboot(self, wait_for_reload=False, **kwargs): """ Reload the controller or controller pair. Args: - timer (int, optional): The time to wait before reloading. Defaults to 0. + wait_for_reload: Whether or not reboot method should also run _wait_for_device_reboot(). Defaults to False. Raises: RebootTimerError: When the device is still unreachable after the timeout period. @@ -324,12 +324,16 @@ def reboot(self, timer=0, **kwargs): """ if kwargs.get("confirm"): log.warning("Passing 'confirm' to reboot method is deprecated.", DeprecationWarning) - - if timer != 0: - log.error("Host %s: Reboot time error for device type %s.", self.host, self.device_type) - raise RebootTimerError(self.device_type) - - self.native.reboot(confirm=True) + try: + self.native.show_list(["terminal dont-ask", "reload"]) + # The native reboot is not always properly disabling confirmation. Above is more consistent. + # self.native.reboot(confirm=True) + except ReadTimeout as expected_exception: + log.info("Host %s: Device rebooted.", self.host) + log.info("Hit expected exception during reload: %s", expected_exception.__class__) + if wait_for_reload: + time.sleep(10) + self._wait_for_device_reboot() log.info("Host %s: Device rebooted.", self.host) def rollback(self, filename): diff --git a/tests/unit/test_devices/device_mocks/nxos/show/reload b/tests/unit/test_devices/device_mocks/nxos/show/reload new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/tests/unit/test_devices/device_mocks/nxos/show/reload @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/tests/unit/test_devices/device_mocks/nxos/show/terminal_dont-ask b/tests/unit/test_devices/device_mocks/nxos/show/terminal_dont-ask new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/tests/unit/test_devices/device_mocks/nxos/show/terminal_dont-ask @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/tests/unit/test_devices/test_aireos_device.py b/tests/unit/test_devices/test_aireos_device.py index 059db59a..9dfdde67 100644 --- a/tests/unit/test_devices/test_aireos_device.py +++ b/tests/unit/test_devices/test_aireos_device.py @@ -1173,9 +1173,8 @@ def test_re_redundancy_state(filename, expected, aireos_mock_path): assert actual == expected -@mock.patch("pyntc.devices.aireos_device.RebootSignal") @mock.patch.object(AIREOSDevice, "save") -def test_reboot_confirm(mock_save, mock_reboot, aireos_send_command_timing, aireos_redundancy_mode_path): +def test_reboot_confirm(mock_save, aireos_send_command_timing, aireos_redundancy_mode_path): device = aireos_send_command_timing(["reset_system_confirm.txt", "reset_system_restart.txt"]) with mock.patch(aireos_redundancy_mode_path, new_callable=mock.PropertyMock) as redundnacy_mode: redundnacy_mode.return_value = "sso enabled" @@ -1184,9 +1183,8 @@ def test_reboot_confirm(mock_save, mock_reboot, aireos_send_command_timing, aire mock_save.assert_called() -@mock.patch("pyntc.devices.aireos_device.RebootSignal") @mock.patch.object(AIREOSDevice, "save") -def test_reboot_confirm_deprecation(mock_save, mock_reboot, aireos_send_command_timing, aireos_redundancy_mode_path): +def test_reboot_confirm_deprecation(mock_save, aireos_send_command_timing, aireos_redundancy_mode_path): device = aireos_send_command_timing(["reset_system_confirm.txt", "reset_system_restart.txt"]) with mock.patch(aireos_redundancy_mode_path, new_callable=mock.PropertyMock) as redundnacy_mode: redundnacy_mode.return_value = "sso enabled" @@ -1195,24 +1193,20 @@ def test_reboot_confirm_deprecation(mock_save, mock_reboot, aireos_send_command_ mock_save.assert_called() -@mock.patch("pyntc.devices.aireos_device.RebootSignal") @mock.patch.object(AIREOSDevice, "save") -def test_reboot_confirm_args(mock_save, mock_reboot, aireos_send_command_timing, aireos_redundancy_mode_path): +def test_reboot_confirm_args(mock_save, aireos_send_command_timing, aireos_redundancy_mode_path): device = aireos_send_command_timing( ["reset_system_save.txt", "reset_system_confirm.txt", "reset_system_restart.txt"] ) with mock.patch(aireos_redundancy_mode_path, new_callable=mock.PropertyMock) as redundnacy_mode: redundnacy_mode.return_value = "sso enabled" - device.reboot(timer="00:00:10", controller="both", save_config=False) - device.native.send_command_timing.assert_has_calls( - [mock.call("reset system both in 00:00:10"), mock.call("n"), mock.call("y")] - ) + device.reboot(controller="both", save_config=False) + device.native.send_command_timing.assert_has_calls([mock.call("reset system both"), mock.call("n"), mock.call("y")]) mock_save.assert_not_called() -@mock.patch("pyntc.devices.aireos_device.RebootSignal") @mock.patch.object(AIREOSDevice, "save") -def test_reboot_confirm_standalone(mock_save, mock_reboot, aireos_send_command_timing, aireos_redundancy_mode_path): +def test_reboot_confirm_standalone(mock_save, aireos_send_command_timing, aireos_redundancy_mode_path): device = aireos_send_command_timing(["reset_system_confirm.txt", "reset_system_restart.txt"]) with mock.patch(aireos_redundancy_mode_path, new_callable=mock.PropertyMock) as redundnacy_mode: redundnacy_mode.return_value = "sso disabled" @@ -1221,20 +1215,15 @@ def test_reboot_confirm_standalone(mock_save, mock_reboot, aireos_send_command_t mock_save.assert_called() -@mock.patch("pyntc.devices.aireos_device.RebootSignal") @mock.patch.object(AIREOSDevice, "save") -def test_reboot_confirm_standalone_args( - mock_save, mock_reboot, aireos_send_command_timing, aireos_redundancy_mode_path -): +def test_reboot_confirm_standalone_args(mock_save, aireos_send_command_timing, aireos_redundancy_mode_path): device = aireos_send_command_timing( ["reset_system_save.txt", "reset_system_confirm.txt", "reset_system_restart.txt"] ) with mock.patch(aireos_redundancy_mode_path, new_callable=mock.PropertyMock) as redundnacy_mode: redundnacy_mode.return_value = "sso disabled" - device.reboot(timer="00:00:10", controller="both", save_config=False) - device.native.send_command_timing.assert_has_calls( - [mock.call("reset system in 00:00:10"), mock.call("n"), mock.call("y")] - ) + device.reboot(controller="both", save_config=False) + device.native.send_command_timing.assert_has_calls([mock.call("reset system"), mock.call("n"), mock.call("y")]) mock_save.assert_not_called() diff --git a/tests/unit/test_devices/test_asa_device.py b/tests/unit/test_devices/test_asa_device.py index 2fe40328..870f209c 100644 --- a/tests/unit/test_devices/test_asa_device.py +++ b/tests/unit/test_devices/test_asa_device.py @@ -251,11 +251,6 @@ def test_reboot(asa_device): asa_device.native.send_command_timing.assert_any_call("reload") -def test_reboot_with_timer(asa_device): - asa_device.reboot(timer=5) - asa_device.native.send_command_timing.assert_any_call("reload in 5") - - def test_reboot_confirm_deprecated(asa_device): asa_device.reboot(confirm=True) asa_device.native.send_command_timing.assert_any_call("reload") diff --git a/tests/unit/test_devices/test_eos_device.py b/tests/unit/test_devices/test_eos_device.py index fad0e06c..5aa82099 100644 --- a/tests/unit/test_devices/test_eos_device.py +++ b/tests/unit/test_devices/test_eos_device.py @@ -6,7 +6,7 @@ import pytest from pyntc.devices import EOSDevice -from pyntc.devices.base_device import RebootTimerError, RollbackError +from pyntc.devices.base_device import RollbackError from pyntc.devices.eos_device import FileTransferError from pyntc.devices.system_features.vlans.eos_vlans import EOSVlans from pyntc.errors import CommandError, CommandListError @@ -281,10 +281,6 @@ def test_reboot(self): self.device.reboot() self.device.native.enable.assert_called_with(["reload now"], encoding="json") - def test_reboot_with_timer(self): - with self.assertRaises(RebootTimerError): - self.device.reboot(timer=3) - def test_boot_options(self): boot_options = self.device.boot_options self.assertEqual(boot_options, {"sys": "EOS.swi"}) diff --git a/tests/unit/test_devices/test_ios_device.py b/tests/unit/test_devices/test_ios_device.py index 2f9ebcd9..a6d4663b 100644 --- a/tests/unit/test_devices/test_ios_device.py +++ b/tests/unit/test_devices/test_ios_device.py @@ -219,10 +219,6 @@ def test_reboot(self): self.device.reboot() self.device.native.send_command_timing.assert_any_call("reload") - def test_reboot_with_timer(self): - self.device.reboot(timer=5) - self.device.native.send_command_timing.assert_any_call("reload in 5") - @mock.patch.object(IOSDevice, "_get_file_system", return_value="bootflash:") def test_boot_options_show_bootvar(self, mock_boot): self.device.native.send_command.side_effect = None diff --git a/tests/unit/test_devices/test_jnpr_device.py b/tests/unit/test_devices/test_jnpr_device.py index d55c7147..57f0c363 100644 --- a/tests/unit/test_devices/test_jnpr_device.py +++ b/tests/unit/test_devices/test_jnpr_device.py @@ -215,10 +215,6 @@ def test_reboot(self): self.device.reboot() self.device.sw.reboot.assert_called_with(in_min=0) - def test_reboot_timer(self): - self.device.reboot(timer=2) - self.device.sw.reboot.assert_called_with(in_min=2) - @mock.patch("pyntc.devices.jnpr_device.JunosDevice.running_config", new_callable=mock.PropertyMock) def test_backup_running_config(self, mock_run): filename = "local_running_config" diff --git a/tests/unit/test_devices/test_nxos_device.py b/tests/unit/test_devices/test_nxos_device.py index 5e0aad5e..dd1ef4a3 100644 --- a/tests/unit/test_devices/test_nxos_device.py +++ b/tests/unit/test_devices/test_nxos_device.py @@ -3,7 +3,7 @@ import mock from pynxos.errors import CLIError -from pyntc.devices.base_device import RebootTimerError, RollbackError +from pyntc.devices.base_device import RollbackError from pyntc.devices.nxos_device import NXOSDevice from pyntc.errors import CommandError, CommandListError, FileTransferError, NTCFileNotFoundError @@ -153,11 +153,8 @@ def test_file_copy_fail(self, mock_fcre): def test_reboot(self): self.device.reboot() - self.device.native.reboot.assert_called_with(confirm=True) - - def test_reboot_with_timer(self): - with self.assertRaises(RebootTimerError): - self.device.reboot(timer=3) + self.device.native.show_list.assert_called_with(["terminal dont-ask", "reload"]) + # self.device.native.reboot.assert_called_with(confirm=True) def test_boot_options(self): expected = {"sys": "my_sys", "boot": "my_boot"} From 30445b8f49207171646358914dfd2a5a657848cb Mon Sep 17 00:00:00 2001 From: Josh VanDeraa Date: Fri, 7 Apr 2023 15:27:49 -0500 Subject: [PATCH 11/13] Pylint updates (#282) * reprep 0.20.3 (#260) (#261) * Updates errors and aireos files for pylint. * ASA pylint updates. * EOS pylint updates. * F5 pylint. * IOS updates. * xe wlc pylint updates. * Juniper pylint. * nxos pylint * Final lints. * Formatting * Fix my mistakes. * Test updates from linting. --------- Co-authored-by: Jeff Kala <48843785+jeffkala@users.noreply.github.com> --- pyntc/devices/aireos_device.py | 12 +-- pyntc/devices/asa_device.py | 57 +++++++------ pyntc/devices/eos_device.py | 80 +++++++++---------- pyntc/devices/f5_device.py | 66 ++++++++------- pyntc/devices/ios_device.py | 57 ++++++------- pyntc/devices/iosxewlc_device.py | 4 +- pyntc/devices/jnpr_device.py | 50 ++++++------ pyntc/devices/nxos_device.py | 28 ++++--- pyntc/devices/system_features/base_feature.py | 2 +- .../system_features/vlans/base_vlans.py | 4 +- pyntc/errors.py | 41 +++++----- pyntc/utils/converters.py | 8 +- pyntc/utils/templates/__init__.py | 2 +- pyproject.toml | 17 +++- tests/unit/test_devices/test_jnpr_device.py | 8 +- 15 files changed, 228 insertions(+), 208 deletions(-) diff --git a/pyntc/devices/aireos_device.py b/pyntc/devices/aireos_device.py index 7294bdda..31fc8f6b 100644 --- a/pyntc/devices/aireos_device.py +++ b/pyntc/devices/aireos_device.py @@ -1086,7 +1086,7 @@ def open(self, confirm_active=True): if self.connected: try: self.native.find_prompt() - except: # noqa E722 + except: # noqa E722 pylint: disable=bare-except self._connected = False if not self.connected: @@ -1198,8 +1198,8 @@ def redundancy_mode(self): 'sso enabled' >>> """ - ha = self.show("show redundancy summary") - ha_mode = re.search(r"^\s*Redundancy\s+Mode\s*=\s*(.+?)\s*$", ha, re.M) + high_availability = self.show("show redundancy summary") + ha_mode = re.search(r"^\s*Redundancy\s+Mode\s*=\s*(.+?)\s*$", high_availability, re.M) log.debug("Host %s: Redundancy mode: {ha_mode.group(1).lower()}", self.host, ha_mode.group(1).lower()) return ha_mode.group(1).lower() @@ -1400,13 +1400,13 @@ def startup_config(self): """ raise NotImplementedError - def transfer_image_to_ap(self, image, timeout=None): + def transfer_image_to_ap(self, image): """ Transfer ``image`` file to all APs connected to the WLC. Args: image (str): The image that should be sent to the APs. - timeout (int): The max time to wait for all APs to download the image. + timeout (int): Removed, The max time to wait for all APs to download the image. Returns: bool: True if AP images are transferred or swapped, False otherwise. @@ -1513,7 +1513,7 @@ def uptime_string(self): >>> """ days, hours, minutes = self._uptime_components() - return "%02d:%02d:%02d:00" % (days, hours, minutes) + return f"{days:02d}:{hours:02d}:{minutes:02d}:00" @property def wlans(self): diff --git a/pyntc/devices/asa_device.py b/pyntc/devices/asa_device.py index f3352a22..a69b4a01 100644 --- a/pyntc/devices/asa_device.py +++ b/pyntc/devices/asa_device.py @@ -76,11 +76,11 @@ def _file_copy(self, src: str, dest: str, file_system: str) -> None: self.enable() if not self.file_copy_remote_exists(src, dest, file_system): - fc: CiscoAsaFileTransfer = self._file_copy_instance(src, dest, file_system) + file_copy: CiscoAsaFileTransfer = self._file_copy_instance(src, dest, file_system) try: - fc.establish_scp_conn() - fc.transfer_file() + file_copy.establish_scp_conn() + file_copy.transfer_file() # Allow EOFErrors to be caught and only raise an error if the file is not actually on the device except EOFError: log.error("Host %s: EOF error.", self.host) @@ -90,7 +90,7 @@ def _file_copy(self, src: str, dest: str, file_system: str) -> None: raise FileTransferError finally: log.error("Host %s: An error occurred when transferring file %s.", self.host, src) - fc.close_scp_chan() + file_copy.close_scp_chan() if not self.file_copy_remote_exists(src, dest, file_system): log.error( @@ -109,9 +109,9 @@ def _file_copy_instance( if dest is None: dest = os.path.basename(src) - fc = CiscoAsaFileTransfer(self.native, src, dest, file_system=file_system) - log.debug("Host %s: File copy instance %s.", self.host, fc) - return fc + file_copy = CiscoAsaFileTransfer(self.native, src, dest, file_system=file_system) + log.debug("Host %s: File copy instance %s.", self.host, file_copy) + return file_copy def _get_file_system(self): """Determine the default file system or directory for device. @@ -261,7 +261,7 @@ def _show_vlan(self): log.debug("Host %s: Successfully executed command 'show vlan' with responses %s.", self.host, show_vlan_out) return show_vlan_out.split(",") - def _uptime_components(self, uptime_full_string): + def _uptime_components(self, uptime_full_string): # pylint: disable=no-self-use match_days = re.search(r"(\d+) days?", uptime_full_string) match_hours = re.search(r"(\d+) hours?", uptime_full_string) match_minutes = re.search(r"(\d+) mins?", uptime_full_string) @@ -283,7 +283,7 @@ def _uptime_to_seconds(self, uptime_full_string): def _uptime_to_string(self, uptime_full_string): days, hours, minutes = self._uptime_components(uptime_full_string) - return "%02d:%02d:%02d:00" % (days, hours, minutes) + return f"{days:02d}:{hours:02d}:{minutes:02d}:00" def _wait_for_device_reboot(self, timeout=3600): start = time.time() @@ -292,7 +292,7 @@ def _wait_for_device_reboot(self, timeout=3600): self.open() log.debug("Host %s: Device rebooted.", self.host) return - except: # noqa E722 # nosec + except: # noqa E722 # nosec # pylint: disable=bare-except pass # TODO: Get proper hostname parameter @@ -352,8 +352,8 @@ def backup_running_config(self, filename): Args: filename (str): Name of backup file. """ - with open(filename, "w") as f: - f.write(self.running_config) + with open(filename, "w", encoding="utf-8") as file_name: + file_name.write(self.running_config) log.debug("Host %s: Running config backed up to %s.", self.host, self.running_config) @@ -498,7 +498,7 @@ def enable_scp(self) -> None: device.save() @property - def facts(self): + def facts(self): # pylint: disable=invalid-overridden-method """Implement this once facts re-factor is done.""" return {} @@ -542,7 +542,7 @@ def file_copy( self.enable_scp() self._file_copy(src, dest, file_system) if peer: - self.peer_device._file_copy(src, dest, file_system) + self.peer_device._file_copy(src, dest, file_system) # pylint: disable=protected-access # logging removed because it messes up unit test mock_basename.assert_not_called() # for tests test_file_copy_no_peer_pass_args, test_file_copy_include_peer @@ -573,8 +573,8 @@ def file_copy_remote_exists(self, src, dest=None, file_system=None): if file_system is None: file_system = self._get_file_system() - fc = self._file_copy_instance(src, dest, file_system=file_system) - if fc.check_file_exists() and fc.compare_md5(): + file_copy = self._file_copy_instance(src, dest, file_system=file_system) + if file_copy.check_file_exists() and file_copy.compare_md5(): log.debug("Host %s: File %s already exists on remote.", self.host, src) return True @@ -630,14 +630,13 @@ def ip_address(self) -> Union[IPv4Address, IPv6Address]: >>> """ try: - ip = ip_address(self.host) + ip_add = ip_address(self.host) except ValueError: # Assume hostname was used, and retrieve resolved IP from paramiko transport log.error("Host %s: value error for ip address used to establish connection.", self.host) - ip = ip_address(self.native.remote_conn.transport.getpeername()[0]) - - log.debug("Host %s: ip address used to establish connection %s.", self.host, ip) - return ip + ip_add = ip_address(self.native.remote_conn.transport.getpeername()[0]) + log.debug("Host %s: ip address used to establish connection %s.", self.host, ip_add) + return ip_add @property def ipv4_addresses(self) -> Dict[str, List[IPv4Address]]: @@ -718,7 +717,7 @@ def open(self): if self._connected: try: self.native.find_prompt() - except: # noqa E722 + except: # noqa E722 pylint: disable=bare-except self._connected = False if not self._connected: @@ -1028,7 +1027,7 @@ def save(self, filename="startup-config"): Returns: bool: True if configuration saved succesfully. """ - command = "copy running-config %s" % filename + command = f"copy running-config {filename}" # Changed to send_command_timing to not require a direct prompt return. self.native.send_command_timing(command) # If the user has enabled 'file prompt quiet' which dose not require any confirmation or feedback. @@ -1056,26 +1055,26 @@ def set_boot_options(self, image_name, **vendor_specifics): if file_system is None: file_system = self._get_file_system() - file_system_files = self.show("dir {0}".format(file_system)) + file_system_files = self.show(f"dir {file_system}") if re.search(image_name, file_system_files) is None: log.error("Host %s: File not found error for image %s.", self.host, image_name) raise NTCFileNotFoundError( # TODO: Update to use hostname hostname=self.host, file=image_name, - dir=file_system, + directory=file_system, ) current_images = current_boot.splitlines() - commands_to_exec = ["no {0}".format(image) for image in current_images] - commands_to_exec.append("boot system {0}/{1}".format(file_system, image_name)) + commands_to_exec = [f"no {image}" for image in current_images] + commands_to_exec.append(f"boot system {file_system}/{image_name}") self.config(commands_to_exec) self.save() if self.boot_options["sys"] != image_name: log.error("Host %s: Setting boot command did not yield expected results", self.host) raise CommandError( - command="boot system {0}/{1}".format(file_system, image_name), + command=f"boot system {file_system}/{image_name}", message="Setting boot command did not yield expected results", ) @@ -1224,4 +1223,4 @@ def vlans(self): class RebootSignal(NTCError): """Not implemented.""" - pass + pass # pylint: disable=unnecessary-pass diff --git a/pyntc/devices/eos_device.py b/pyntc/devices/eos_device.py index 2f3407f8..ae9c816a 100644 --- a/pyntc/devices/eos_device.py +++ b/pyntc/devices/eos_device.py @@ -24,6 +24,7 @@ ) from pyntc.utils import convert_list_by_key + BASIC_FACTS_KM = {"model": "modelName", "os_version": "internalVersion", "serial_number": "serialNumber"} INTERFACES_KM = { "speed": "bandwidth", @@ -76,9 +77,9 @@ def _file_copy_instance(self, src, dest=None, file_system="flash:"): if dest is None: dest = os.path.basename(src) - fc = FileTransfer(self.native_ssh, src, dest, file_system=file_system) - log.debug("Host %s: File copy instance %s.", self.host, fc) - return fc + file_copy = FileTransfer(self.native_ssh, src, dest, file_system=file_system) + log.debug("Host %s: File copy instance %s.", self.host, file_copy) + return file_copy def _get_file_system(self): """Determine the default file system or directory for device. @@ -121,13 +122,13 @@ def _interfaces_status_list(self): log.debug("Host %s: interfaces detailed list %s.", self.host, interface_status_list) return interface_status_list - def _parse_response(self, response, raw_text): + def _parse_response(self, response, raw_text): # pylint: disable=no-self-use if raw_text: return list(x["result"]["output"] for x in response) - else: - return list(x["result"] for x in response) - def _uptime_to_string(self, uptime): + return list(x["result"] for x in response) + + def _uptime_to_string(self, uptime): # pylint: disable=no-self-use days = uptime / (24 * 60 * 60) uptime = uptime % (24 * 60 * 60) @@ -139,7 +140,7 @@ def _uptime_to_string(self, uptime): seconds = uptime - return "%02d:%02d:%02d:%02d" % (days, hours, mins, seconds) + return f"{days:02d}:{hours:02d}:{mins:02d}:{seconds:02d}" def _wait_for_device_reboot(self, timeout=3600): start = time.time() @@ -148,7 +149,7 @@ def _wait_for_device_reboot(self, timeout=3600): self.show("show hostname") log.debug("Host %s: Device rebooted.", self.host) return - except: # noqa E722 # nosec + except: # noqa E722 # nosec # pylint: disable=bare-except pass log.error("Host %s: Device timed out while rebooting.", self.host) @@ -161,8 +162,8 @@ def backup_running_config(self, filename): Args: filename (str): The name of the file that will be saved. """ - with open(filename, "w") as f: - f.write(self.running_config) + with open(filename, "w", encoding="utf-8") as file_name: + file_name.write(self.running_config) log.debug("Host %s: Running config backed up to %s.", self.host, self.running_config) @@ -185,16 +186,11 @@ def checkpoint(self, checkpoint_file): checkpoint_file (str): Checkpoint file name. """ log.debug("Host %s: checkpoint is %s.", self.host, checkpoint_file) - self.show("copy running-config %s" % checkpoint_file) + self.show(f"copy running-config {checkpoint_file}") def close(self): - """Create a checkpoint file of the running config. - - Args: - checkpoint_file (str): Name of the checkpoint file. - """ - log.info("Host %s: Device configured.", self.host) - pass + """Not implemented. Just ``passes``.""" + pass # pylint: disable=unnecessary-pass def config(self, commands): """Send configuration commands to a device. @@ -376,18 +372,18 @@ def file_copy(self, src, dest=None, file_system=None): file_system = self._get_file_system() if not self.file_copy_remote_exists(src, dest, file_system): - fc = self._file_copy_instance(src, dest, file_system=file_system) + file_copy = self._file_copy_instance(src, dest, file_system=file_system) try: - fc.enable_scp() - fc.establish_scp_conn() - fc.transfer_file() + file_copy.enable_scp() + file_copy.establish_scp_conn() + file_copy.transfer_file() log.info("Host %s: File %s transferred successfully.", self.host, src) except: # noqa E722 log.error("Host %s: File transfer error %s", self.host, FileTransferError.default_message) raise FileTransferError finally: - fc.close_scp_chan() + file_copy.close_scp_chan() if not self.file_copy_remote_exists(src, dest, file_system): log.error( @@ -413,8 +409,8 @@ def file_copy_remote_exists(self, src, dest=None, file_system=None): if file_system is None: file_system = self._get_file_system() - fc = self._file_copy_instance(src, dest, file_system=file_system) - if fc.check_file_exists() and fc.compare_md5(): + filecopy = self._file_copy_instance(src, dest, file_system=file_system) + if filecopy.check_file_exists() and filecopy.compare_md5(): log.debug("Host %s: File %s already exists on remote.", self.host, src) return True @@ -452,12 +448,12 @@ def open(self): """Open ssh connection with Netmiko ConnectHandler to be used with FileTransfer.""" if self._connected: try: - self.native_ssh.find_prompt() - except Exception: + self.native_ssh.find_prompt() # pylint: disable=access-member-before-definition + except Exception: # pylint: disable=broad-except self._connected = False if not self._connected: - self.native_ssh = ConnectHandler( + self.native_ssh = ConnectHandler( # pylint: disable=attribute-defined-outside-init device_type="arista_eos", ip=self.host, username=self.username, @@ -505,11 +501,11 @@ def rollback(self, rollback_to): RollbackError: When rollback is unsuccessful. """ try: - self.show("configure replace %s force" % rollback_to) + self.show(f"configure replace {rollback_to} force") log.info("Host %s: Rollback to %s.", self.host, rollback_to) except (CommandError, CommandListError): log.error("Host %s: Rollback unsuccessful. %s may not exist.", self.host, rollback_to) - raise RollbackError("Rollback unsuccessful. %s may not exist." % rollback_to) + raise RollbackError(f"Rollback unsuccessful. {rollback_to} may not exist.") @property def running_config(self): @@ -528,7 +524,7 @@ def save(self, filename="startup-config"): str: Running configuration. """ log.debug("Host %s: Copy running config with name %s.", self.host, filename) - self.show("copy running-config %s" % filename) + self.show(f"copy running-config {filename}") return True def set_boot_options(self, image_name, **vendor_specifics): @@ -545,16 +541,16 @@ def set_boot_options(self, image_name, **vendor_specifics): if file_system is None: file_system = self._get_file_system() - file_system_files = self.show("dir {0}".format(file_system), raw_text=True) + file_system_files = self.show(f"dir {file_system}", raw_text=True) if re.search(image_name, file_system_files) is None: log.error("Host %s: File not found error for image %s.", self.host, image_name) - raise NTCFileNotFoundError(hostname=self.hostname, file=image_name, dir=file_system) + raise NTCFileNotFoundError(hostname=self.hostname, file=image_name, directory=file_system) - self.show("install source {0}{1}".format(file_system, image_name)) + self.show(f"install source {file_system}{image_name}") if self.boot_options["sys"] != image_name: log.error("Host %s: Setting boot command did not yield expected results", self.host) raise CommandError( - command="install source {0}".format(image_name), + command=f"install source {image_name}", message="Setting install source did not yield expected results", ) @@ -586,12 +582,12 @@ def show(self, commands, raw_text=False): return response_list[0] log.debug("Host %s: Successfully executed command 'show' with responses %s.", self.host, response_list) return response_list - except EOSCommandError as e: + except EOSCommandError as err: if original_commands_is_str: - log.error("Host %s: Command error for command %s with message %s.", self.host, commands, e.message) - raise CommandError(e.commands, e.message) - log.error("Host %s: Command list error for commands %s with message %s.", self.host, commands, e.message) - raise CommandListError(commands, e.commands[len(e.commands) - 1], e.message) + log.error("Host %s: Command error for command %s with message %s.", self.host, commands, err.message) + raise CommandError(err.commands, err.message) + log.error("Host %s: Command list error for commands %s with message %s.", self.host, commands, err.message) + raise CommandListError(commands, err.commands[len(err.commands) - 1], err.message) @property def startup_config(self): @@ -607,4 +603,4 @@ def startup_config(self): class RebootSignal(NTCError): """Error for sending reboot signal.""" - pass + pass # pylint: disable=unnecessary-pass diff --git a/pyntc/devices/f5_device.py b/pyntc/devices/f5_device.py index b3eb8cf8..8e0dc9e1 100644 --- a/pyntc/devices/f5_device.py +++ b/pyntc/devices/f5_device.py @@ -13,6 +13,8 @@ from pyntc.devices.base_device import BaseDevice from pyntc.errors import FileTransferError, NotEnoughFreeSpaceError, NTCFileNotFoundError, OSInstallError +# TODO: Check in on soap_handler in the F5Device, many instances of no-member. Is this broken? + class F5Device(BaseDevice): """F5 LTM Device Implementation.""" @@ -45,7 +47,8 @@ def _check_free_space(self, min_space=0): if not free_space: raise ValueError("Could not get free space") - elif free_space >= min_space: + + if free_space >= min_space: return elif free_space < min_space: log.error("Host %s: Not enough free space for min space requirement %s.", self.host, min_space) @@ -78,17 +81,17 @@ def _file_copy_local_file_exists(filepath): def _file_copy_local_md5(self, filepath, blocksize=2**20): if self._file_copy_local_file_exists(filepath): - m = hashlib.md5() # nosec - with open(filepath, "rb") as f: - buf = f.read(blocksize) + md5_check = hashlib.md5() # nosec + with open(filepath, "rb") as file_name: + buf = file_name.read(blocksize) while buf: - m.update(buf) - buf = f.read(blocksize) - return m.hexdigest() + md5_check.update(buf) + buf = file_name.read(blocksize) + return md5_check.hexdigest() def _file_copy_remote_md5(self, filepath): md5sum_result = None - md5sum_output = self.api_handler.tm.util.bash.exec_cmd("run", utilCmdArgs='-c "md5sum {}"'.format(filepath)) + md5sum_output = self.api_handler.tm.util.bash.exec_cmd("run", utilCmdArgs=f'-c "md5sum {filepath}"') if md5sum_output: md5sum_result = md5sum_output.commandResult md5sum_result = md5sum_result.split()[0] @@ -138,35 +141,35 @@ def _get_images(self): return images def _get_interfaces_list(self): - interfaces = self.soap_handler.Networking.Interfaces.get_list() + interfaces = self.soap_handler.Networking.Interfaces.get_list() # pylint: disable=no-member log.debug("Host %s: List of interfaces %s.", self.host, interfaces) return interfaces def _get_model(self): - model = self.soap_handler.System.SystemInfo.get_marketing_name() + model = self.soap_handler.System.SystemInfo.get_marketing_name() # pylint: disable=no-member log.debug("Host %s: Model name %s.", self.host, model) return model def _get_serial_number(self): - system_information = self.soap_handler.System.SystemInfo.get_system_information() + system_information = self.soap_handler.System.SystemInfo.get_system_information() # pylint: disable=no-member chassis_serial = system_information.get("chassis_serial") log.debug("Host %s: Serial number %s.", self.host, chassis_serial) return chassis_serial def _get_uptime(self): - uptime = self.soap_handler.System.SystemInfo.get_uptime() + uptime = self.soap_handler.System.SystemInfo.get_uptime() # pylint: disable=no-member log.debug("Host %s: Uptime %s.", self.host, uptime) return uptime def _get_version(self): - version = self.soap_handler.System.SystemInfo.get_version() + version = self.soap_handler.System.SystemInfo.get_version() # pylint: disable=no-member log.debug("Host %s: Version %s.", self.host, version) return version def _get_vlans(self): - rd_list = self.soap_handler.Networking.RouteDomainV2.get_list() - rd_vlan_list = self.soap_handler.Networking.RouteDomainV2.get_vlan(rd_list) + rd_list = self.soap_handler.Networking.RouteDomainV2.get_list() # pylint: disable=no-member + rd_vlan_list = self.soap_handler.Networking.RouteDomainV2.get_vlan(rd_list) # pylint: disable=no-member log.debug("Host %s: List of vlans %s.", self.host, rd_vlan_list) return rd_vlan_list @@ -277,13 +280,11 @@ def _upload_image(self, image_filepath): image_filepath (str): Name of file. """ image_filename = os.path.basename(image_filepath) - _URI = "https://{hostname}/mgmt/cm/autodeploy/software-image-uploads/{filename}".format( - hostname=self.host, filename=image_filename - ) + upload_uri = f"https://{self.host}/mgmt/cm/autodeploy/software-image-uploads/{image_filename}" chunk_size = 512 * 1024 size = os.path.getsize(image_filepath) headers = {"Content-Type": "application/octet-stream"} - requests.packages.urllib3.disable_warnings() + requests.packages.urllib3.disable_warnings() # pylint: disable=no-member start = 0 with open(image_filepath, "rb") as fileobj: @@ -295,10 +296,14 @@ def _upload_image(self, image_filepath): end = fileobj.tell() if end < chunk_size: end = size - content_range = "{}-{}/{}".format(start, end - 1, size) + content_range = f"{start}-{end - 1}/{size}" headers["Content-Range"] = content_range requests.post( - _URI, auth=(self.username, self.password), data=payload, headers=headers, verify=False # nosec + upload_uri, + auth=(self.username, self.password), + data=payload, + headers=headers, + verify=False, # nosec ) start += len(payload) @@ -359,8 +364,9 @@ def _wait_for_device_reboot(self, volume_name, timeout=600): volume = self.api_handler.tm.sys.software.volumes.volume.load(name=volume_name) if hasattr(volume, "active") and volume.active is True: return True + log.debug("Host %s: Reboot successfull.", self.host) - except Exception: # noqa E722 # nosec + except Exception: # noqa E722 # nosec # pylint: disable=broad-except log.error("Host %s: Error while rebooting.", self.host) pass log.debug("Host %s: Reboot not successfull.", self.host) @@ -387,7 +393,7 @@ def _wait_for_image_installed(self, image_name, volume, timeout=1800): if self.image_installed(image_name=image_name, volume=volume): log.info("Host %s: Image %s installed on volume %s.", self.host, image_name, volume) return - except: # noqa E722 # nosec + except: # noqa E722 # nosec # pylint: disable=bare-except pass log.error("Host %s: OS install error with image %s and volume %s.", self.host, image_name, volume) @@ -429,7 +435,7 @@ def checkpoint(self, filename): def close(self): """Implement ``pass``.""" - pass + pass # pylint: disable=unnecessary-pass def config(self, command): """Send command to device. @@ -662,7 +668,7 @@ def install_os(self, image_name, **vendor_specifics): self._check_free_space(min_space=6) if not self._image_exists(image_name): log.error("Host %s: File not found for image %s and volume %s.", self.host, image_name, volume) - raise NTCFileNotFoundError(hostname=self.hostname, file=image_name, dir="/shared/images") + raise NTCFileNotFoundError(hostname=self.hostname, file=image_name, directory="/shared/images") self._image_install(image_name=image_name, volume=volume) self._wait_for_image_installed(image_name=image_name, volume=volume) @@ -674,7 +680,7 @@ def install_os(self, image_name, **vendor_specifics): def open(self): """Implement ``pass``.""" - pass + pass # pylint: disable=unnecessary-pass def reboot(self, wait_for_reload=False, volume=None, **kwargs): """ @@ -704,7 +710,7 @@ def reboot(self, wait_for_reload=False, volume=None, **kwargs): if not self._wait_for_device_reboot(volume_name=volume): log.error("Host %s: Reboot to volume %s failed.", self.host, volume) - raise RuntimeError + raise RuntimeError(f"Reboot to volume {volume} failed") log.debug("Host %s: Reboot to volume %s succeeded.", self.host, volume) def rollback(self, checkpoint_file): @@ -718,7 +724,7 @@ def rollback(self, checkpoint_file): """ raise NotImplementedError - def running_config(self): + def running_config(self): # pylint: disable=invalid-overridden-method """Get running configuration. Raises: @@ -750,7 +756,7 @@ def set_boot_options(self, image_name, **vendor_specifics): self._check_free_space(min_space=6) if not self._image_exists(image_name): log.error("Host %s: File not found for image %s and volume %s.", self.host, image_name, volume) - raise NTCFileNotFoundError(hostname=self.hostname, file=image_name, dir="/shared/images") + raise NTCFileNotFoundError(hostname=self.hostname, file=image_name, directory="/shared/images") self._image_install(image_name=image_name, volume=volume) self._wait_for_image_installed(image_name=image_name, volume=volume) log.info("Host %s: Image %s installed to volume %s.", self.host, image_name, volume) @@ -767,7 +773,7 @@ def show(self, command, raw_text=False): """ raise NotImplementedError - def startup_config(self): + def startup_config(self): # pylint: disable=invalid-overridden-method """Get startup configuration. Raises: diff --git a/pyntc/devices/ios_device.py b/pyntc/devices/ios_device.py index 77c13521..d277acb7 100644 --- a/pyntc/devices/ios_device.py +++ b/pyntc/devices/ios_device.py @@ -70,7 +70,7 @@ def __init__( # nosec self.open(confirm_active=confirm_active) log.init(host=host) - def _check_command_output_for_errors(self, command, command_response): + def _check_command_output_for_errors(self, command, command_response): # pylint: disable=no-self-use """ Check response from device to see if an error was reported. @@ -108,9 +108,9 @@ def _file_copy_instance(self, src, dest=None, file_system="flash:"): if dest is None: dest = os.path.basename(src) - fc = FileTransfer(self.native, src, dest, file_system=file_system) - log.debug("Host %s: File copy instance %s.", self.host, fc) - return fc + file_copy = FileTransfer(self.native, src, dest, file_system=file_system) + log.debug("Host %s: File copy instance %s.", self.host, file_copy) + return file_copy def _get_file_system(self): """Determine the default file system or directory for device. @@ -207,7 +207,7 @@ def _show_vlan(self): log.debug("Host %s: Successfully executed command 'show vlan' with responses %s.", self.host, show_vlan_data) return show_vlan_data - def _uptime_components(self, uptime_full_string): + def _uptime_components(self, uptime_full_string): # pylint: disable=no-self-use match_days = re.search(r"(\d+) days?", uptime_full_string) match_hours = re.search(r"(\d+) hours?", uptime_full_string) match_minutes = re.search(r"(\d+) minutes?", uptime_full_string) @@ -229,7 +229,7 @@ def _uptime_to_seconds(self, uptime_full_string): def _uptime_to_string(self, uptime_full_string): days, hours, minutes = self._uptime_components(uptime_full_string) - return "%02d:%02d:%02d:00" % (days, hours, minutes) + return f"{days:02d}:{hours:02d}:{minutes:02d}:00" def _wait_for_device_reboot(self, timeout=3600): start = time.time() @@ -240,7 +240,7 @@ def _wait_for_device_reboot(self, timeout=3600): log.debug("Host %s: Device reboot Completed.", self.host) if self._has_reload_happened_recently(): return - except: # noqa E722 # nosec + except: # noqa E722 # nosec # pylint: disable=bare-except pass log.error("Host %s: Device timed out while rebooting.", self.host) @@ -258,8 +258,8 @@ def backup_running_config(self, filename): Args: filename (str): Filename to save running configuration to. """ - with open(filename, "w") as f: - f.write(self.running_config) + with open(filename, "w", encoding="utf-8") as file_name: + file_name.write(self.running_config) @property def boot_options(self): @@ -383,8 +383,7 @@ def config(self, command, **netmiko_args): err.cli_error_msg, ) raise CommandListError(entered_commands, cmd, err.cli_error_msg) - else: - raise err + raise err # Don't let exception prevent exiting config mode finally: # Ignore None or invalid args passed for exit_config_mode @@ -644,18 +643,18 @@ def file_copy(self, src, dest=None, file_system=None): file_system = self._get_file_system() if not self.file_copy_remote_exists(src, dest, file_system): - fc = self._file_copy_instance(src, dest, file_system=file_system) + file_copy = self._file_copy_instance(src, dest, file_system=file_system) # if not self.fc.verify_space_available(): # raise FileTransferError('Not enough space available.') try: - fc.enable_scp() - fc.establish_scp_conn() - fc.transfer_file() + file_copy.enable_scp() + file_copy.establish_scp_conn() + file_copy.transfer_file() log.info("Host %s: File %s transferred successfully.", self.host, src) except OSError as error: # compare hashes - if not fc.compare_md5(): + if not file_copy.compare_md5(): log.error("Host %s: Socket closed error %s", self.host, error) raise SocketClosedError(message=error) log.error("Host %s: OS error %s", self.host, error) @@ -663,7 +662,7 @@ def file_copy(self, src, dest=None, file_system=None): log.error("Host %s: File transfer error %s", self.host, FileTransferError.default_message) raise FileTransferError finally: - fc.close_scp_chan() + file_copy.close_scp_chan() # Ensure connection to device is still open after long transfers self.open() @@ -692,8 +691,8 @@ def file_copy_remote_exists(self, src, dest=None, file_system=None): if file_system is None: file_system = self._get_file_system() - fc = self._file_copy_instance(src, dest, file_system=file_system) - if fc.check_file_exists() and fc.compare_md5(): + file_copy = self._file_copy_instance(src, dest, file_system=file_system) + if file_copy.check_file_exists() and file_copy.compare_md5(): log.debug("Host %s: File %s already exists on remote.", self.host, src) return True @@ -817,7 +816,7 @@ def open(self, confirm_active=True): if self.connected: try: self.native.find_prompt() - except: # noqa E722 + except: # noqa E722 # pylint: disable=bare-except self._connected = False if not self.connected: @@ -967,11 +966,11 @@ def rollback(self, rollback_to): RollbackError: Error if unable to rollback to configuration. """ try: - self.show("configure replace %s%s force" % (self._get_file_system(), rollback_to)) + self.show(f"configure replace {self._get_file_system()}{rollback_to} force") log.info("Host %s: Rollback to %s.", self.host, rollback_to) except CommandError: log.error("Host %s: Rollback unsuccessful. %s may not exist.", self.host, rollback_to) - raise RollbackError("Rollback unsuccessful. %s may not exist." % rollback_to) + raise RollbackError(f"Rollback unsuccessful. {rollback_to} may not exist.") @property def running_config(self): @@ -992,7 +991,7 @@ def save(self, filename="startup-config"): Returns: bool: True if save is succesfull. """ - command = "copy running-config %s" % filename + command = f"copy running-config {filename}" # Changed to send_command_timing to not require a direct prompt return. self.native.send_command_timing(command) # If the user has enabled 'file prompt quiet' which dose not require any confirmation or feedback. @@ -1019,12 +1018,12 @@ def set_boot_options(self, image_name, **vendor_specifics): if file_system is None: file_system = self._get_file_system() - file_system_files = self.show("dir {0}".format(file_system)) + file_system_files = self.show(f"dir {file_system}") if image_name != INSTALL_MODE_FILE_NAME and re.search(image_name, file_system_files) is None: log.error("Host %s: File not found error for image %s.", self.host, image_name) - raise NTCFileNotFoundError(hostname=self.hostname, file=image_name, dir=file_system) + raise NTCFileNotFoundError(hostname=self.hostname, file=image_name, directory=file_system) if image_name == "packages.conf": - command = "boot system {0}{1}".format(file_system, image_name) + command = f"boot system {file_system}{image_name}" self.config(["no boot system", command]) else: show_boot_sys = self.show("show run | include boot system") @@ -1071,7 +1070,7 @@ def set_boot_options(self, image_name, **vendor_specifics): log.error("Host %s: Setting boot command did not yield expected results", self.host) raise CommandError( command=command, - message="Setting boot command did not yield expected results, found {0}".format(new_boot_options), + message=f"Setting boot command did not yield expected results, found {new_boot_options}", ) def show(self, command, expect_string=None, **netmiko_args): @@ -1110,4 +1109,6 @@ def startup_config(self): class RebootSignal(NTCError): # noqa: D101 - pass + """RebootSignal.""" + + pass # pylint: disable=unnecessary-pass diff --git a/pyntc/devices/iosxewlc_device.py b/pyntc/devices/iosxewlc_device.py index c24015d2..0e4b472f 100644 --- a/pyntc/devices/iosxewlc_device.py +++ b/pyntc/devices/iosxewlc_device.py @@ -19,7 +19,7 @@ def _wait_for_device_start_reboot(self, timeout=600): try: self.open() self.show("show version") - except Exception: # noqa E722 # nosec + except Exception: # noqa E722 # nosec # pylint: disable=broad-except return log.error("Host %s: Wait reboot timeout error with timeout %s", self.host, timeout) @@ -33,7 +33,7 @@ def _wait_for_device_reboot(self, timeout=5400): self.show("show version") log.debug("Host %s: Device rebooted.", self.host) return - except Exception: # noqa E722 # nosec + except Exception: # noqa E722 # nosec # pylint: disable=broad-except pass log.error("Host %s: Device timed out while rebooting.", self.host) diff --git a/pyntc/devices/jnpr_device.py b/pyntc/devices/jnpr_device.py index 46c006aa..42a60b0f 100644 --- a/pyntc/devices/jnpr_device.py +++ b/pyntc/devices/jnpr_device.py @@ -8,7 +8,7 @@ from jnpr.junos import Device as JunosNativeDevice from jnpr.junos.exception import ConfigLoadError -from jnpr.junos.op.ethport import EthPortTable +from jnpr.junos.op.ethport import EthPortTable # pylint: disable=no-name-in-module from jnpr.junos.utils.config import Config as JunosNativeConfig from jnpr.junos.utils.fs import FS as JunosNativeFS from jnpr.junos.utils.scp import SCP @@ -37,22 +37,22 @@ def __init__(self, host, username, password, *args, **kwargs): # noqa: D403 self.native = JunosNativeDevice(*args, host=host, user=username, passwd=password, **kwargs) self.open() - self.cu = JunosNativeConfig(self.native) - self.fs = JunosNativeFS(self.native) - self.sw = JunosNativeSW(self.native) + self.cu = JunosNativeConfig(self.native) # pylint: disable=invalid-name + self.fs = JunosNativeFS(self.native) # pylint: disable=invalid-name + self.sw = JunosNativeSW(self.native) # pylint: disable=invalid-name - def _file_copy_local_file_exists(self, filepath): + def _file_copy_local_file_exists(self, filepath): # pylint: disable=no-self-use return os.path.isfile(filepath) def _file_copy_local_md5(self, filepath, blocksize=2**20): if self._file_copy_local_file_exists(filepath): - m = hashlib.md5() # nosec - with open(filepath, "rb") as f: - buf = f.read(blocksize) + md5_hash = hashlib.md5() # nosec + with open(filepath, "rb") as file_name: + buf = file_name.read(blocksize) while buf: - m.update(buf) - buf = f.read(blocksize) - return m.hexdigest() + md5_hash.update(buf) + buf = file_name.read(blocksize) + return md5_hash.hexdigest() def _file_copy_remote_md5(self, filename): return self.fs.checksum(filename) @@ -72,7 +72,7 @@ def _get_interfaces(self): def _image_booted(self, image_name, **vendor_specifics): raise NotImplementedError - def _uptime_components(self, uptime_full_string): + def _uptime_components(self, uptime_full_string): # pylint: disable=no-self-use match_days = re.search(r"(\d+) days?", uptime_full_string) match_hours = re.search(r"(\d+) hours?", uptime_full_string) match_minutes = re.search(r"(\d+) minutes?", uptime_full_string) @@ -96,7 +96,7 @@ def _uptime_to_seconds(self, uptime_full_string): def _uptime_to_string(self, uptime_full_string): days, hours, minutes, seconds = self._uptime_components(uptime_full_string) - return "%02d:%02d:%02d:%02d" % (days, hours, minutes, seconds) + return f"{days:02d}:{hours:02d}:{minutes:02d}:{seconds:02d}" def _wait_for_device_reboot(self, timeout=3600): start = time.time() @@ -104,7 +104,7 @@ def _wait_for_device_reboot(self, timeout=3600): try: self.open() return - except: # noqa E722 # nosec + except: # noqa E722 # nosec # pylint: disable=bare-except pass raise RebootTimeoutError(hostname=self.hostname, wait_time=timeout) @@ -115,8 +115,8 @@ def backup_running_config(self, filename): Args: filename (str): Name used for backup file. """ - with open(filename, "w") as f: - f.write(self.running_config) + with open(filename, "w", encoding="utf-8") as file_name: + file_name.write(self.running_config) @property def boot_options(self): @@ -140,7 +140,7 @@ def close(self): if self.connected: self.native.close() - def config(self, commands, format="set"): + def config(self, commands, format_type="set"): """Send configuration commands to a device. Args: @@ -153,18 +153,18 @@ def config(self, commands, format="set"): """ if isinstance(commands, str): try: - self.cu.load(commands, format=format) + self.cu.load(commands, format_type=format_type) self.cu.commit() - except ConfigLoadError as e: - raise CommandError(commands, e.message) + except ConfigLoadError as err: + raise CommandError(commands, err.message) else: try: for command in commands: - self.cu.load(command, format=format) + self.cu.load(command, format_type=format_type) self.cu.commit() - except ConfigLoadError as e: - raise CommandListError(commands, command, e.message) + except ConfigLoadError as err: + raise CommandListError(commands, command, err.message) @property def connected(self): @@ -370,7 +370,7 @@ def rollback(self, filename): """ self.native.timeout = 60 - temp_file = NamedTemporaryFile() + temp_file = NamedTemporaryFile() # pylint: disable=consider-using-with with SCP(self.native) as scp: scp.get(filename, local_path=temp_file.name) @@ -407,7 +407,7 @@ def save(self, filename=None): self.cu.commit() return - temp_file = NamedTemporaryFile() + temp_file = NamedTemporaryFile() # pylint: disable=consider-using-with temp_file.write(self.show("show config")) temp_file.flush() diff --git a/pyntc/devices/nxos_device.py b/pyntc/devices/nxos_device.py index d9379c48..bb2d873b 100644 --- a/pyntc/devices/nxos_device.py +++ b/pyntc/devices/nxos_device.py @@ -6,6 +6,7 @@ from pynxos.device import Device as NXOSNative from pynxos.errors import CLIError from pynxos.features.file_copy import FileTransferError as NXOSFileTransferError + from requests.exceptions import ReadTimeout from pyntc import log @@ -59,7 +60,7 @@ def _wait_for_device_reboot(self, timeout=3600): self.native.show("show hostname") log.debug("Host %s: Device rebooted.", self.host) return - except: # noqa E722 # nosec + except: # noqa E722 # nosec # pylint: disable=bare-except pass log.error("Host %s: Device timed out while rebooting.", self.host) @@ -96,7 +97,7 @@ def checkpoint(self, filename): def close(self): # noqa: D401 """Implements ``pass``.""" - pass + pass # pylint: disable=unnecessary-pass def config(self, command): """Send configuration command. @@ -240,7 +241,9 @@ def file_copy(self, src, dest=None, file_system="bootflash:"): if not self.file_copy_remote_exists(src, dest, file_system): dest = dest or os.path.basename(src) try: - file_copy = self.native.file_copy(src, dest, file_system=file_system) + file_copy = self.native.file_copy( + src, dest, file_system=file_system + ) # pylint: disable=assignment-from-no-return log.info("Host %s: File %s transferred successfully.", self.host, src) if not self.file_copy_remote_exists(src, dest, file_system): log.error( @@ -250,8 +253,9 @@ def file_copy(self, src, dest=None, file_system="bootflash:"): ) raise FileTransferError return file_copy - except NXOSFileTransferError as e: - log.error("Host %s: NXOS file transfer error %s", self.host, str(e)) + + except NXOSFileTransferError as err: + log.error("Host %s: NXOS file transfer error %s", self.host, str(err)) raise FileTransferError # TODO: Make this an internal method since exposing file_copy should be sufficient @@ -305,7 +309,7 @@ def install_os(self, image_name, **vendor_specifics): def open(self): # noqa: D401 """Implements ``pass``.""" - pass + pass # pylint: disable=unnecessary-pass def reboot(self, wait_for_reload=False, **kwargs): """ @@ -350,7 +354,7 @@ def rollback(self, filename): log.info("Host %s: Rollback to %s.", self.host, filename) except CLIError: log.error("Host %s: Rollback unsuccessful. %s may not exist.", self.host, filename) - raise RollbackError("Rollback unsuccessful, %s may not exist." % filename) + raise RollbackError(f"Rollback unsuccessful, {filename} may not exist.") @property def running_config(self): @@ -388,15 +392,15 @@ def set_boot_options(self, image_name, kickstart=None, **vendor_specifics): if file_system is None: file_system = "bootflash:" - file_system_files = self.show("dir {0}".format(file_system), raw_text=True) + file_system_files = self.show(f"dir {file_system}", raw_text=True) if re.search(image_name, file_system_files) is None: log.error("Host %s: File not found error for image %s.", self.host, image_name) - raise NTCFileNotFoundError(hostname=self.hostname, file=image_name, dir=file_system) + raise NTCFileNotFoundError(hostname=self.hostname, file=image_name, directory=file_system) if kickstart is not None: if re.search(kickstart, file_system_files) is None: log.error("Host %s: File not found error for image %s.", self.host, image_name) - raise NTCFileNotFoundError(hostname=self.hostname, file=kickstart, dir=file_system) + raise NTCFileNotFoundError(hostname=self.hostname, file=kickstart, directory=file_system) kickstart = file_system + kickstart @@ -409,7 +413,9 @@ def set_boot_options(self, image_name, kickstart=None, **vendor_specifics): if native_timeout < 300: self.native.timeout = 300 - upgrade_result = self.native.set_boot_options(image_name, kickstart=kickstart) + upgrade_result = self.native.set_boot_options( + image_name, kickstart=kickstart + ) # pylint: disable=assignment-from-no-return self.native.timeout = 30 log.info("Host %s: boot options have been set to %s", self.host, upgrade_result) diff --git a/pyntc/devices/system_features/base_feature.py b/pyntc/devices/system_features/base_feature.py index 8d2eb6a2..6784e232 100644 --- a/pyntc/devices/system_features/base_feature.py +++ b/pyntc/devices/system_features/base_feature.py @@ -1,7 +1,7 @@ """Define base feature set.""" -class BaseFeature(object): +class BaseFeature: """Base feature sets.""" def config(self, vlan_id, **params): diff --git a/pyntc/devices/system_features/vlans/base_vlans.py b/pyntc/devices/system_features/vlans/base_vlans.py index fad926f0..d9e2e400 100644 --- a/pyntc/devices/system_features/vlans/base_vlans.py +++ b/pyntc/devices/system_features/vlans/base_vlans.py @@ -14,7 +14,7 @@ def vlan_not_in_range_error(vlan_id, lower=1, upper=4094): class BaseVlans(BaseFeature): """Subclass for base vlan feature.""" - pass + pass # pylint: disable=unnecessary-pass class VlanNotInRangeError(NTCError): @@ -31,4 +31,4 @@ def __init__(self, lower, upper): lower (int): lower vlan range. upper (int): upper vlan range. """ - super().__init__("Vlan Id must be in range %s-%s" % (lower, upper)) + super().__init__(f"Vlan Id must be in range {lower}-{upper}") diff --git a/pyntc/errors.py b/pyntc/errors.py index 4e385059..c249e0ff 100644 --- a/pyntc/errors.py +++ b/pyntc/errors.py @@ -5,7 +5,7 @@ class NTCError(Exception): """Generic Error class for PyNTC.""" - def __init__(self, message): + def __init__(self, message): # pylint: disable=super-init-not-called """ Generic Error class for PyNTC. @@ -16,7 +16,7 @@ def __init__(self, message): def __repr__(self): """Representation of NTC error object.""" - return "%s: \n%s" % (self.__class__.__name__, self.message) + return f"{self.__class__.__name__}: \n{self.message}" __str__ = __repr__ @@ -31,7 +31,7 @@ def __init__(self, vendor): Args: vendor (str): The name of the deice's vendor to present in the error. """ - message = "%s is not a supported vendor." % vendor + message = f"{vendor} is not a supported vendor." super().__init__(message) @@ -46,7 +46,7 @@ def __init__(self, name, filename): name (str): The hostname that failed the lookup. filename (str): The name of the file used for inventory. """ - message = "Name %s not found in %s. The file may not exist." % (name, filename) + message = f"Name {name} not found in {filename}. The file may not exist." super().__init__(message) @@ -60,7 +60,7 @@ def __init__(self, filename): Args: filename (str): The name of the file used for config settings. """ - message = "NTC Configuration file %s could not be found." % filename + message = f"NTC Configuration file {filename} could not be found." super().__init__(message) @@ -77,7 +77,7 @@ def __init__(self, command, message): """ self.command = command self.cli_error_msg = message - message = "Command %s was not successful: %s" % (command, message) + message = f"Command {command} was not successful: {message}" super().__init__(message) @@ -96,10 +96,10 @@ def __init__(self, commands, command, message): warnings.warn("This will raise CommandError in the future", FutureWarning) self.commands = commands self.command = command - message = "\nCommand %s failed with message: %s" % (command, message) + message = f"\nCommand {command} failed with message: {message}" message += "\nCommand List: \n" - for command in commands: - message += "\t%s\n" % command + for command in commands: # pylint: disable=redefined-argument-from-local + message += f"\t{command}\n" super().__init__(message) @@ -135,7 +135,7 @@ def __init__(self, feature, device_type): TODO: Remove this Exception when VLAN feature is removed. """ - message = "%s feature not found for %s device type." % (feature, device_type) + message = f"{feature} feature not found for {device_type} device type." super().__init__(message) @@ -150,7 +150,7 @@ def __init__(self, hostname, command): hostname (str): The hostname of the device that failed. command (str): The command used to detect the default file system. """ - message = 'Unable to parse "{0}" command to identify the default file system on {1}.'.format(command, hostname) + message = f'Unable to parse "{command}" command to identify the default file system on {hostname}.' super().__init__(message) @@ -183,7 +183,7 @@ def __init__(self, hostname, wait_time): hostname (str): The hostname of the device that did not boot back up. wait_time (int): The amount of time waiting before considering the reboot failed. """ - message = "Unable to reconnect to {0} after {1} seconds".format(hostname, wait_time) + message = f"Unable to reconnect to {hostname} after {wait_time} seconds" super().__init__(message) @@ -198,9 +198,7 @@ def __init__(self, hostname, wait_time): hostname (str): The hostname of the device that did not boot back up. wait_time (int): The amount of time waiting before considering the reboot failed. """ - message = "{0} has not rebooted in {1} seconds after issuing install mode upgrade command".format( - hostname, wait_time - ) + message = f"{hostname} has not rebooted in {wait_time} seconds after issuing install mode upgrade command" super().__init__(message) @@ -215,7 +213,7 @@ def __init__(self, hostname, min_space): hostname (str): The hostname of the device that did not boot back up. min_space (str): The minimum amount of space required to transfer the file. """ - message = "{0} does not meet the minimum disk space requirements of {1}".format(hostname, min_space) + message = f"{hostname} does not meet the minimum disk space requirements of {min_space}" super().__init__(message) @@ -230,7 +228,7 @@ def __init__(self, hostname, desired_boot): hostname (str): The hostname of the device that failed to install OS. desired_boot (str): The OS that was attempted to be installed. """ - message = "{0} was unable to boot into {1}".format(hostname, desired_boot) + message = f"{hostname} was unable to boot into {desired_boot}" super().__init__(message) @@ -256,18 +254,17 @@ def __init__(self, hostname, desired_state, current_state): class NTCFileNotFoundError(NTCError): """Error for not being able to find a file on a device.""" - def __init__(self, hostname, file, dir): - """ - Error for not being able to find a file on a device. + def __init__(self, hostname, file, directory): + """Error for not being able to find a file on a device. Args: hostname (str): The hostname of the device that did not have the ``file``. file (str): The name of the file that could not be found. - dir (str): The directory on the network device where the file was searched for. + directory (str): The directory on the network device where the file was searched for. TODO: Rename ``dir`` arg as that is a reserved name in python. """ - message = "{0} was not found in {1} on {2}".format(file, dir, hostname) + message = f"{file} was not found in {directory} on {hostname}" super().__init__(message) diff --git a/pyntc/utils/converters.py b/pyntc/utils/converters.py index f3e6ee7d..c729a1e9 100644 --- a/pyntc/utils/converters.py +++ b/pyntc/utils/converters.py @@ -1,7 +1,9 @@ """Provides methods for manipulating and converting data.""" -def convert_dict_by_key(original, key_map, fill_in=False, whitelist=[], blacklist=[]): +def convert_dict_by_key( + original, key_map, fill_in=False, whitelist=[], blacklist=[] +): # pylint: disable=dangerous-default-value """Use a key map to convert a dictionary to desired keys. Args: @@ -41,7 +43,9 @@ def convert_dict_by_key(original, key_map, fill_in=False, whitelist=[], blacklis return converted -def convert_list_by_key(original_list, key_map, fill_in=False, whitelist=[], blacklist=[]): +def convert_list_by_key( + original_list, key_map, fill_in=False, whitelist=[], blacklist=[] +): # pylint: disable=dangerous-default-value """Apply a list conversion for all items in original_list. Args: diff --git a/pyntc/utils/templates/__init__.py b/pyntc/utils/templates/__init__.py index 638bd54b..90eefacd 100644 --- a/pyntc/utils/templates/__init__.py +++ b/pyntc/utils/templates/__init__.py @@ -17,7 +17,7 @@ def get_structured_data(template_name, rawtxt): list: A dict per entry returned by TextFSM. """ template_file = get_template(template_name) - with open(template_file) as template: + with open(template_file, encoding="utf-8") as template: fsm = textfsm.TextFSM(template) table = fsm.ParseText(rawtxt) diff --git a/pyproject.toml b/pyproject.toml index 40e54a36..daa5f55d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,7 +93,7 @@ exclude = ''' ''' [tool.pylint.master] -ignore=".venv" +ignore=[".venv", "tests"] [tool.pylint.basic] # No docstrings required for private methods (Pylint default), or for test_ functions, or for inner Meta classes. @@ -103,9 +103,20 @@ no-docstring-rgx="^(_|test_|Meta$)" # Line length is enforced by Black, so pylint doesn't need to check it. # Pylint and Black disagree about how to format multi-line arrays; Black wins. disable = """, - line-too-long, + abstract-method, + arguments-differ, + arguments-renamed, + attribute-defined-outside-init, bad-continuation, - consider-iterating-dictionary + consider-iterating-dictionary, + duplicate-code, + inconsistent-return-statements, + line-too-long, + raise-missing-from, + too-many-arguments, + too-many-instance-attributes, + too-many-lines, + too-many-public-methods, """ [tool.pylint.miscellaneous] diff --git a/tests/unit/test_devices/test_jnpr_device.py b/tests/unit/test_devices/test_jnpr_device.py index 57f0c363..c5700bb4 100644 --- a/tests/unit/test_devices/test_jnpr_device.py +++ b/tests/unit/test_devices/test_jnpr_device.py @@ -61,7 +61,7 @@ def test_config_pass_string(self): result = self.device.config(command) self.assertIsNone(result) - self.device.cu.load.assert_called_with(command, format="set") + self.device.cu.load.assert_called_with(command, format_type="set") self.device.cu.commit.assert_called_with() def test_config_pass_list(self): @@ -69,15 +69,15 @@ def test_config_pass_list(self): result = self.device.config(commands) self.assertIsNone(result) - self.device.cu.load.assert_has_calls(mock.call(command, format="set") for command in commands) + self.device.cu.load.assert_has_calls(mock.call(command, format_type="set") for command in commands) self.device.cu.commit.assert_called_with() @mock.patch.object(JunosDevice, "config") def test_config_list(self, mock_config): commands = ["set interfaces lo0", "set snmp community jason"] - self.device.config(commands, format="set") - self.device.config.assert_called_with(commands, format="set") + self.device.config(commands, format_type="set") + self.device.config.assert_called_with(commands, format_type="set") def test_bad_config_pass_string(self): command = "asdf poknw" From 526dd7aff0b3770952def060cc4c6e7ced6e2bb1 Mon Sep 17 00:00:00 2001 From: Jeff Kala Date: Fri, 7 Apr 2023 15:02:25 -0600 Subject: [PATCH 12/13] prep for 1.0.0 release --- .github/workflows/ci.yml | 83 ++++++++++++------------- docs/admin/release_notes/version_1_0.md | 13 +++- docs/user/lib_getting_started.md | 14 ++--- pyntc/__init__.py | 2 +- pyproject.toml | 2 +- 5 files changed, 60 insertions(+), 54 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f483fdd5..42b82e75 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,44 +114,43 @@ jobs: - "pydocstyle" - "flake8" - "yamllint" - # TODO: Re-enable after initial pylint issue is completed. https://github.com/networktocode/pyntc/issues/249 - # pylint: - # runs-on: "ubuntu-20.04" - # strategy: - # fail-fast: true - # matrix: - # python-version: ["3.7"] - # env: - # PYTHON_VER: "${{ matrix.python-version }}" - # steps: - # - name: "Check out repository code" - # uses: "actions/checkout@v2" - # - name: "Setup environment" - # uses: "networktocode/gh-action-setup-poetry-environment@v2" - # - name: "Get image version" - # run: "echo IMAGE_VER=`poetry version -s`-py${{ matrix.python-version }} >> $GITHUB_ENV" - # - name: "Set up Docker Buildx" - # id: "buildx" - # uses: "docker/setup-buildx-action@v1" - # - name: "Load the image from cache" - # uses: "docker/build-push-action@v2" - # with: - # builder: "${{ steps.buildx.outputs.name }}" - # context: "./" - # push: false - # load: true - # tags: "${{ env.IMAGE_NAME }}:${{ env.IMAGE_VER }}" - # file: "./Dockerfile" - # cache-from: "type=gha,scope=${{ env.IMAGE_NAME }}-${{ env.IMAGE_VER }}-py${{ matrix.python-version }}" - # cache-to: "type=gha,scope=${{ env.IMAGE_NAME }}-${{ env.IMAGE_VER }}-py${{ matrix.python-version }}" - # build-args: | - # PYTHON_VER=${{ env.PYTHON_VER }} - # - name: "Debug: Show docker images" - # run: "docker image ls" - # - name: "Linting: Pylint" - # run: "poetry run invoke pylint" - # needs: - # - "build" + pylint: + runs-on: "ubuntu-20.04" + strategy: + fail-fast: true + matrix: + python-version: ["3.9"] + env: + PYTHON_VER: "${{ matrix.python-version }}" + steps: + - name: "Check out repository code" + uses: "actions/checkout@v2" + - name: "Setup environment" + uses: "networktocode/gh-action-setup-poetry-environment@v2" + - name: "Get image version" + run: "echo IMAGE_VER=`poetry version -s`-py${{ matrix.python-version }} >> $GITHUB_ENV" + - name: "Set up Docker Buildx" + id: "buildx" + uses: "docker/setup-buildx-action@v1" + - name: "Load the image from cache" + uses: "docker/build-push-action@v2" + with: + builder: "${{ steps.buildx.outputs.name }}" + context: "./" + push: false + load: true + tags: "${{ env.IMAGE_NAME }}:${{ env.IMAGE_VER }}" + file: "./Dockerfile" + cache-from: "type=gha,scope=${{ env.IMAGE_NAME }}-${{ env.IMAGE_VER }}-py${{ matrix.python-version }}" + cache-to: "type=gha,scope=${{ env.IMAGE_NAME }}-${{ env.IMAGE_VER }}-py${{ matrix.python-version }}" + build-args: | + PYTHON_VER=${{ env.PYTHON_VER }} + - name: "Debug: Show docker images" + run: "docker image ls" + - name: "Linting: Pylint" + run: "poetry run invoke pylint" + needs: + - "build" pytest: strategy: fail-fast: true @@ -188,12 +187,8 @@ jobs: - name: "Run Tests" run: "poetry run invoke pytest" needs: - # Remove everything but pylint once pylint is passing. - # - "pylint" - - "bandit" - - "pydocstyle" - - "flake8" - - "yamllint" + - "pylint" + publish_gh: name: "Publish to GitHub" runs-on: "ubuntu-20.04" diff --git a/docs/admin/release_notes/version_1_0.md b/docs/admin/release_notes/version_1_0.md index 8b8fe3d3..c301d56e 100644 --- a/docs/admin/release_notes/version_1_0.md +++ b/docs/admin/release_notes/version_1_0.md @@ -1,10 +1,21 @@ # v1.0 Release Notes -## [1.0.0a0] 04-2023 +## [1.0.0] 04-2023 ### Added +- [270](https://github.com/networktocode/pyntc/pull/270) Add additional properties to ASA and AIREOS. +- [271](https://github.com/networktocode/pyntc/pull/271) Add default logging for all devices and overall library. +- [280](https://github.com/networktocode/pyntc/pull/280) Add the `wait_for_reload` argument from `reboot` method throughout library. Defaults to False to keep current code backward compatible, If set to True the reboot method waits for the device to finish the reboot before returning. + ### Changed +- [280](https://github.com/networktocode/pyntc/pull/280) Changed from relative imports to absolute imports. +- [282](https://github.com/networktocode/pyntc/pull/282) Update full code base to pass pylint and other static code checkers. ### Deprecated +- [269](https://github.com/networktocode/pyntc/pull/269) Remove `show_list` and `config_list` methods asa and ios. Add default functionality to `show` and `config` to handle str and list. +- [275](https://github.com/networktocode/pyntc/pull/275) Remove python ABC (abstract base classes) as they were not required. +- [275](https://github.com/networktocode/pyntc/pull/275) Remove `show_list` and `config_list` methods for the rest of device drivers. Add default functionality to `show` and `config` to handle str and list. +- [280](https://github.com/networktocode/pyntc/pull/280) Remove the use of `signal` modules within Cisco drivers. This will allow for reboots to be able to be handled within threads. +- [280](https://github.com/networktocode/pyntc/pull/280) Remove the `timer` argument from `reboot` method throughout library. Compatibility matrix on which versions, vendors support it became to much to maintain. diff --git a/docs/user/lib_getting_started.md b/docs/user/lib_getting_started.md index a67c602a..f164d661 100644 --- a/docs/user/lib_getting_started.md +++ b/docs/user/lib_getting_started.md @@ -173,12 +173,12 @@ On an IOS device: ### Sending Multiple Commands -- `show_list` method +- `show` method ```python >>> cmds = ['show hostname', 'show run int Eth2/1'] ->>> data = nxs1.show_list(cmds, raw_text=True) +>>> data = nxs1.show(cmds) ``` ```python @@ -197,7 +197,7 @@ interface Ethernet2/1 ### Config Commands -- Use `config` and `config_list` +- Use `config` ```python >>> csr1.config('hostname testname') @@ -205,7 +205,7 @@ interface Ethernet2/1 ``` ```python ->>> csr1.config_list(['interface Gi3', 'shutdown']) +>>> csr1.config(['interface Gi3', 'shutdown']) >>> ``` @@ -283,11 +283,11 @@ Backup current running configuration and store it locally Reboot target device Parameters: - - `timer=0` by default - - `confirm=False` by default + - `wait_for_reload=False` by default. If `True` function waits for device to recover from reboot before returning. + ```python ->>> csr1.reboot(confirm=True) +>>> csr1.reboot(wait_for_reload=False) >>> ``` diff --git a/pyntc/__init__.py b/pyntc/__init__.py index 110ca0fd..c3607697 100644 --- a/pyntc/__init__.py +++ b/pyntc/__init__.py @@ -1,4 +1,4 @@ -"""Kickoff functions for getting instancs of device objects.""" +"""Kickoff functions for getting instance of device objects.""" import os import warnings diff --git a/pyproject.toml b/pyproject.toml index daa5f55d..4951c764 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pyntc" -version = "1.0.0a0" +version = "1.0.0" description = "SDK to simplify common workflows for Network Devices." authors = ["Network to Code, LLC "] readme = "README.md" From 8095ccb14cf4388a82ebb0910eab839558842366 Mon Sep 17 00:00:00 2001 From: Jeff Kala Date: Fri, 7 Apr 2023 15:21:58 -0600 Subject: [PATCH 13/13] revert turning on pylint checks until another PR of fixes in introduced --- .github/workflows/ci.yml | 83 +++++++++++++------------ docs/admin/install.md | 2 +- docs/admin/release_notes/version_1_0.md | 2 +- 3 files changed, 46 insertions(+), 41 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 42b82e75..f483fdd5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,43 +114,44 @@ jobs: - "pydocstyle" - "flake8" - "yamllint" - pylint: - runs-on: "ubuntu-20.04" - strategy: - fail-fast: true - matrix: - python-version: ["3.9"] - env: - PYTHON_VER: "${{ matrix.python-version }}" - steps: - - name: "Check out repository code" - uses: "actions/checkout@v2" - - name: "Setup environment" - uses: "networktocode/gh-action-setup-poetry-environment@v2" - - name: "Get image version" - run: "echo IMAGE_VER=`poetry version -s`-py${{ matrix.python-version }} >> $GITHUB_ENV" - - name: "Set up Docker Buildx" - id: "buildx" - uses: "docker/setup-buildx-action@v1" - - name: "Load the image from cache" - uses: "docker/build-push-action@v2" - with: - builder: "${{ steps.buildx.outputs.name }}" - context: "./" - push: false - load: true - tags: "${{ env.IMAGE_NAME }}:${{ env.IMAGE_VER }}" - file: "./Dockerfile" - cache-from: "type=gha,scope=${{ env.IMAGE_NAME }}-${{ env.IMAGE_VER }}-py${{ matrix.python-version }}" - cache-to: "type=gha,scope=${{ env.IMAGE_NAME }}-${{ env.IMAGE_VER }}-py${{ matrix.python-version }}" - build-args: | - PYTHON_VER=${{ env.PYTHON_VER }} - - name: "Debug: Show docker images" - run: "docker image ls" - - name: "Linting: Pylint" - run: "poetry run invoke pylint" - needs: - - "build" + # TODO: Re-enable after initial pylint issue is completed. https://github.com/networktocode/pyntc/issues/249 + # pylint: + # runs-on: "ubuntu-20.04" + # strategy: + # fail-fast: true + # matrix: + # python-version: ["3.7"] + # env: + # PYTHON_VER: "${{ matrix.python-version }}" + # steps: + # - name: "Check out repository code" + # uses: "actions/checkout@v2" + # - name: "Setup environment" + # uses: "networktocode/gh-action-setup-poetry-environment@v2" + # - name: "Get image version" + # run: "echo IMAGE_VER=`poetry version -s`-py${{ matrix.python-version }} >> $GITHUB_ENV" + # - name: "Set up Docker Buildx" + # id: "buildx" + # uses: "docker/setup-buildx-action@v1" + # - name: "Load the image from cache" + # uses: "docker/build-push-action@v2" + # with: + # builder: "${{ steps.buildx.outputs.name }}" + # context: "./" + # push: false + # load: true + # tags: "${{ env.IMAGE_NAME }}:${{ env.IMAGE_VER }}" + # file: "./Dockerfile" + # cache-from: "type=gha,scope=${{ env.IMAGE_NAME }}-${{ env.IMAGE_VER }}-py${{ matrix.python-version }}" + # cache-to: "type=gha,scope=${{ env.IMAGE_NAME }}-${{ env.IMAGE_VER }}-py${{ matrix.python-version }}" + # build-args: | + # PYTHON_VER=${{ env.PYTHON_VER }} + # - name: "Debug: Show docker images" + # run: "docker image ls" + # - name: "Linting: Pylint" + # run: "poetry run invoke pylint" + # needs: + # - "build" pytest: strategy: fail-fast: true @@ -187,8 +188,12 @@ jobs: - name: "Run Tests" run: "poetry run invoke pytest" needs: - - "pylint" - + # Remove everything but pylint once pylint is passing. + # - "pylint" + - "bandit" + - "pydocstyle" + - "flake8" + - "yamllint" publish_gh: name: "Publish to GitHub" runs-on: "ubuntu-20.04" diff --git a/docs/admin/install.md b/docs/admin/install.md index cc9ec153..6bb1d6dc 100644 --- a/docs/admin/install.md +++ b/docs/admin/install.md @@ -18,5 +18,5 @@ poetry install Option 3: Install from a GitHub branch, such as develop as shown below. ```bash -$ pip install git+https://github.com/networktocode/netutils.git@develop +$ pip install git+https://github.com/networktocode/pyntc.git@develop ``` diff --git a/docs/admin/release_notes/version_1_0.md b/docs/admin/release_notes/version_1_0.md index c301d56e..3140be0d 100644 --- a/docs/admin/release_notes/version_1_0.md +++ b/docs/admin/release_notes/version_1_0.md @@ -10,7 +10,7 @@ ### Changed - [280](https://github.com/networktocode/pyntc/pull/280) Changed from relative imports to absolute imports. -- [282](https://github.com/networktocode/pyntc/pull/282) Update full code base to pass pylint and other static code checkers. +- [282](https://github.com/networktocode/pyntc/pull/282) Update initial pass at pylint. ### Deprecated