├── tests ├── __init__.py ├── live │ ├── __init__.py │ ├── test_objects.py │ ├── test_userid.py │ ├── conftest.py │ ├── testlib.py │ └── test_device_config.py ├── test_classic_objects.py ├── test_firewall.py ├── test_panorama.py ├── test_vsys_xpaths.py ├── test_userid.py ├── test_params.py ├── test_device_profile_xpaths.py ├── test_predefined.py └── test_PanOScomp.py ├── docs ├── _static │ └── .gitkeep ├── history.rst ├── readme.rst ├── contributing.rst ├── requirements.txt ├── module-errors.rst ├── module-userid.rst ├── module-objects.rst ├── module-updater.rst ├── module-base.rst ├── module-predefined.rst ├── module-ha.rst ├── module-device.rst ├── module-network.rst ├── module-plugins.rst ├── module-firewall.rst ├── module-panorama.rst ├── module-policies.rst ├── reference.rst ├── index.rst ├── configtree.rst ├── examples.rst ├── moduleref.py ├── configtree.py ├── make.bat └── Makefile ├── examples ├── __init__.py ├── README.md ├── log_forwarding_profile.py ├── prisma_access_show_remote_net_per_tenant.py ├── prisma_access_show_jobs_status.py ├── prisma_access_list_RN_regions_bw.py ├── upgrade.py ├── userid.py ├── prisma_access_create_remote_network.py ├── dyn_address_group.py ├── bulk_address_objects.py ├── ensure_security_rule.py └── bulk_subinterfaces.py ├── .bandit ├── .hound.yml ├── HISTORY.rst ├── .isort.cfg ├── requirements.txt ├── tox.ini ├── MANIFEST.in ├── .editorconfig ├── .readthedocs.yaml ├── LICENSE ├── .vscode └── settings.json ├── .flake8 ├── mypy.ini ├── .releaserc.json ├── .devcontainer └── devcontainer.json ├── .github ├── set-version.sh └── workflows │ └── ci.yml ├── .gitignore ├── Makefile ├── pyproject.toml ├── CONTRIBUTING.rst ├── panos └── errors.py ├── README.md └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/live/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/history.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../HISTORY.rst -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | .. mdinclude:: ../README.md 2 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = "btorres-gil" 2 | -------------------------------------------------------------------------------- /.bandit: -------------------------------------------------------------------------------- 1 | [bandit] 2 | targets: panos/ 3 | skips: B314,B405 4 | -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | python: 2 | enabled: true 3 | config_file: .flake8.ini 4 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mistune<2.0.0 2 | m2r==0.3.1 3 | sphinx_rtd_theme 4 | pan-python==0.17.0 5 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | Release Notes 4 | ============= 5 | 6 | Release notes (changelogs) are available on GitHub: https://github.com/PaloAltoNetworks/pan-os-python/releases -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | multi_line_output = 3 3 | include_trailing_comma = true 4 | force_grid_wrap = 0 5 | use_parentheses = true 6 | line_length = 88 7 | known_first_party = panos 8 | known_third_party = pan 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pan-python==0.17.0 \ 2 | --hash=sha256:9c074ea2f69a63996a6fefe8935d60dca61660e14715ac19d257ea9b1c41c6e2 \ 3 | --hash=sha256:f4674e40763c46d5933244b3059a57884e4e28205ef6d0f9ce2dc2013e3db010 4 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27 3 | 4 | [testenv] 5 | setenv = 6 | PYTHONPATH = {toxinidir}:{toxinidir}/panos 7 | commands = python setup.py test {posargs} 8 | deps = 9 | -r{toxinidir}/requirements.txt 10 | -------------------------------------------------------------------------------- /docs/module-errors.rst: -------------------------------------------------------------------------------- 1 | Module: errors 2 | ============== 3 | 4 | Inheritance diagram 5 | ------------------- 6 | 7 | .. inheritance-diagram:: panos.errors 8 | :parts: 1 9 | 10 | Class Reference 11 | --------------- 12 | 13 | .. automodule:: panos.errors 14 | :members: 15 | -------------------------------------------------------------------------------- /docs/module-userid.rst: -------------------------------------------------------------------------------- 1 | Module: userid 2 | ============== 3 | 4 | Inheritance diagram 5 | ------------------- 6 | 7 | .. inheritance-diagram:: panos.userid 8 | :parts: 1 9 | 10 | Class Reference 11 | --------------- 12 | 13 | .. automodule:: panos.userid 14 | :members: 15 | -------------------------------------------------------------------------------- /docs/module-objects.rst: -------------------------------------------------------------------------------- 1 | Module: objects 2 | =============== 3 | 4 | Inheritance diagram 5 | ------------------- 6 | 7 | .. inheritance-diagram:: panos.objects 8 | :parts: 1 9 | 10 | Class Reference 11 | --------------- 12 | 13 | .. automodule:: panos.objects 14 | :members: 15 | -------------------------------------------------------------------------------- /docs/module-updater.rst: -------------------------------------------------------------------------------- 1 | Module: updater 2 | =============== 3 | 4 | Inheritance diagram 5 | ------------------- 6 | 7 | .. inheritance-diagram:: panos.updater 8 | :parts: 1 9 | 10 | Class Reference 11 | --------------- 12 | 13 | .. automodule:: panos.updater 14 | :members: 15 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include HISTORY.rst 4 | include LICENSE 5 | include README.rst 6 | 7 | recursive-include tests * 8 | recursive-exclude * __pycache__ 9 | recursive-exclude * *.py[co] 10 | 11 | recursive-include docs *.rst conf.py Makefile make.bat -------------------------------------------------------------------------------- /docs/module-base.rst: -------------------------------------------------------------------------------- 1 | Module: base 2 | ============ 3 | 4 | Inheritance diagram 5 | ------------------- 6 | 7 | .. inheritance-diagram:: panos.base 8 | :parts: 1 9 | 10 | Class Reference 11 | --------------- 12 | 13 | .. automodule:: panos.base 14 | :members: PanObject 15 | :no-index: -------------------------------------------------------------------------------- /docs/module-predefined.rst: -------------------------------------------------------------------------------- 1 | Module: predefined 2 | ================== 3 | 4 | Inheritance diagram 5 | ------------------- 6 | 7 | .. inheritance-diagram:: panos.predefined 8 | :parts: 1 9 | 10 | Class Reference 11 | --------------- 12 | 13 | .. automodule:: panos.predefined 14 | :members: 15 | -------------------------------------------------------------------------------- /docs/module-ha.rst: -------------------------------------------------------------------------------- 1 | Module: ha 2 | ========== 3 | 4 | Inheritance diagram 5 | ------------------- 6 | 7 | .. inheritance-diagram:: panos.ha 8 | :parts: 1 9 | 10 | Configuration tree diagram 11 | -------------------------- 12 | 13 | .. graphviz:: _diagrams/panos.ha.dot 14 | 15 | Class Reference 16 | --------------- 17 | 18 | .. automodule:: panos.ha 19 | :members: -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*.py] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /docs/module-device.rst: -------------------------------------------------------------------------------- 1 | Module: device 2 | ============== 3 | 4 | Inheritance diagram 5 | ------------------- 6 | 7 | .. inheritance-diagram:: panos.device 8 | :parts: 1 9 | 10 | Configuration tree diagram 11 | -------------------------- 12 | 13 | .. graphviz:: _diagrams/panos.device.dot 14 | 15 | Class Reference 16 | --------------- 17 | 18 | .. automodule:: panos.device 19 | :members: 20 | -------------------------------------------------------------------------------- /docs/module-network.rst: -------------------------------------------------------------------------------- 1 | Module: network 2 | =============== 3 | 4 | Inheritance diagram 5 | ------------------- 6 | 7 | .. inheritance-diagram:: panos.network 8 | :parts: 1 9 | 10 | Configuration tree diagram 11 | -------------------------- 12 | 13 | .. graphviz:: _diagrams/panos.network.dot 14 | 15 | Class Reference 16 | --------------- 17 | 18 | .. automodule:: panos.network 19 | :members: 20 | -------------------------------------------------------------------------------- /docs/module-plugins.rst: -------------------------------------------------------------------------------- 1 | Module: plugins 2 | =============== 3 | 4 | Inheritance diagram 5 | ------------------- 6 | 7 | .. inheritance-diagram:: panos.plugins 8 | :parts: 1 9 | 10 | Configuration tree diagram 11 | -------------------------- 12 | 13 | .. graphviz:: _diagrams/panos.plugins.dot 14 | 15 | Class Reference 16 | --------------- 17 | 18 | .. automodule:: panos.plugins 19 | :members: 20 | -------------------------------------------------------------------------------- /docs/module-firewall.rst: -------------------------------------------------------------------------------- 1 | Module: firewall 2 | ================ 3 | 4 | Inheritance diagram 5 | ------------------- 6 | 7 | .. inheritance-diagram:: panos.firewall 8 | :parts: 1 9 | 10 | Configuration tree diagram 11 | -------------------------- 12 | 13 | .. graphviz:: _diagrams/panos.firewall.dot 14 | 15 | Class Reference 16 | --------------- 17 | 18 | .. automodule:: panos.firewall 19 | :members: 20 | -------------------------------------------------------------------------------- /docs/module-panorama.rst: -------------------------------------------------------------------------------- 1 | Module: panorama 2 | ================ 3 | 4 | Inheritance diagram 5 | ------------------- 6 | 7 | .. inheritance-diagram:: panos.panorama 8 | :parts: 1 9 | 10 | Configuration tree diagram 11 | -------------------------- 12 | 13 | .. graphviz:: _diagrams/panos.panorama.dot 14 | 15 | Class Reference 16 | --------------- 17 | 18 | .. automodule:: panos.panorama 19 | :members: 20 | -------------------------------------------------------------------------------- /docs/module-policies.rst: -------------------------------------------------------------------------------- 1 | Module: policies 2 | ================ 3 | 4 | Inheritance diagram 5 | ------------------- 6 | 7 | .. inheritance-diagram:: panos.policies 8 | :parts: 1 9 | 10 | Configuration tree diagram 11 | -------------------------- 12 | 13 | .. graphviz:: _diagrams/panos.policies.dot 14 | 15 | Class Reference 16 | --------------- 17 | 18 | .. automodule:: panos.policies 19 | :members: 20 | -------------------------------------------------------------------------------- /docs/reference.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | useful-methods 8 | configtree 9 | 10 | .. toctree:: 11 | :maxdepth: 1 12 | 13 | module-base 14 | module-device 15 | module-errors 16 | module-firewall 17 | module-ha 18 | module-network 19 | module-objects 20 | module-panorama 21 | module-plugins 22 | module-policies 23 | module-predefined 24 | module-updater 25 | module-userid 26 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. complexity documentation master file, created by 2 | sphinx-quickstart on Tue Jul 9 22:26:36 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Palo Alto Networks PAN-OS SDK for Python 7 | ======================================== 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | readme 15 | getting-started 16 | howto 17 | examples 18 | reference 19 | history 20 | contributing 21 | 22 | Indices and tables 23 | ================== 24 | 25 | * :ref:`genindex` 26 | * :ref:`modindex` 27 | * :ref:`search` 28 | -------------------------------------------------------------------------------- /tests/test_classic_objects.py: -------------------------------------------------------------------------------- 1 | """ Tests specifically for classic objects. 2 | 3 | Note: All tests in this file are for classic objects. These are to try and 4 | make sure that the fix for classic objects with a self.NAME == None still 5 | work properly. 6 | 7 | """ 8 | 9 | 10 | from panos import device 11 | 12 | 13 | def test_system_settings_with_positional_arg_sets_hostname(): 14 | ss = device.SystemSettings("foobar") 15 | 16 | assert ss.hostname == "foobar" 17 | 18 | 19 | def test_system_settings_parsing(): 20 | ss = device.SystemSettings(hostname="foobar") 21 | ss2 = device.SystemSettings() 22 | 23 | ss2.refresh(xml=ss.element()) 24 | 25 | assert ss.equal(ss2) 26 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need. 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.7" 13 | apt_packages: 14 | - graphviz 15 | 16 | # Build documentation in the docs/ directory with Sphinx 17 | sphinx: 18 | configuration: docs/conf.py 19 | 20 | # We recommend specifying your dependencies to enable reproducible builds: 21 | # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 22 | python: 23 | install: 24 | - requirements: docs/requirements.txt 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2014-2020, Palo Alto Networks Inc. 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/__pycache__": true 4 | }, 5 | "editor.codeActionsOnSave": { 6 | "source.organizeImports": "explicit" 7 | }, 8 | "python.testing.unittestEnabled": false, 9 | "python.testing.pytestEnabled": true, 10 | "python.testing.pytestArgs": [ 11 | "--no-cov" 12 | ], 13 | "editor.formatOnSave": true, 14 | "cSpell.words": [ 15 | "Vlan", 16 | "etree", 17 | "nosuffix", 18 | "onlink", 19 | "pandevice", 20 | "pan-os-python", 21 | "refreshall" 22 | ], 23 | "black-formatter.importStrategy": "fromEnvironment", 24 | "bandit.importStrategy": "fromEnvironment", 25 | "bandit.args": [ 26 | "-r", 27 | "--ini .bandit" 28 | ], 29 | "flake8.importStrategy": "fromEnvironment", 30 | "flake8.args": ["--config=.flake8"] 31 | } -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | select = C,E,F,W,B,D,B950 3 | ignore = E203,E501,W503,D203,D102,D103,D107,D400 4 | # E203,E501,W503: per recommendation from black 5 | # D203: Not compatible with pep257 6 | # D102,D103: Hopefully remove someday, too many public functions to document for now 7 | # D107: __init__ docstring is redundant, already require class docstring 8 | # D400: Period at end of every docstring, maybe change some day, not high priority 9 | max-line-length = 88 10 | # max-complexity = 20 11 | no-isort-config = true 12 | use-varnames-strict-mode = true 13 | exclude = 14 | # No need to traverse our git directory 15 | .git, 16 | # There's no value in checking cache directories 17 | __pycache__, 18 | # The conf file is mostly autogenerated, ignore it 19 | docs/conf.py, 20 | # The old directory contains Flake8 2.0 21 | old, 22 | # This contains our built documentation 23 | build, 24 | # This contains builds of flake8 that we don't want to check 25 | dist, 26 | # The tox directory 27 | .tox 28 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | # Global options: 2 | 3 | [mypy] 4 | ignore_missing_imports = True 5 | ; follow_imports = silent 6 | show_column_numbers = True 7 | warn_unused_configs = False 8 | disallow_subclassing_any = False 9 | disallow_any_generics = False 10 | disallow_untyped_calls = False 11 | disallow_untyped_defs = False 12 | disallow_incomplete_defs = False 13 | check_untyped_defs = True 14 | disallow_untyped_decorators = False 15 | no_implicit_optional = False 16 | warn_redundant_casts = False 17 | warn_unused_ignores = False 18 | warn_return_any = False 19 | 20 | # Per-module options: 21 | 22 | ## In this section, add overrides for specific files, modules, or functions 23 | ## that don't yet have type annotations. For example: 24 | ## [mypy-older_module.older_file] 25 | ## disallow_untyped_defs = False 26 | 27 | [mypy-panos.updater] 28 | disallow_subclassing_any = True 29 | disallow_any_generics = True 30 | disallow_untyped_defs = False 31 | disallow_incomplete_defs = True 32 | check_untyped_defs = True 33 | disallow_untyped_decorators = True 34 | no_implicit_optional = True 35 | warn_unused_ignores = True 36 | warn_return_any = True -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "conventionalcommits", 3 | "plugins": [ 4 | "@semantic-release/commit-analyzer", 5 | "@semantic-release/release-notes-generator", 6 | [ 7 | "@semantic-release/exec", 8 | { 9 | "prepareCmd": "poetry run .github/set-version.sh ${nextRelease.version}", 10 | "publishCmd": "poetry publish --build" 11 | } 12 | ], 13 | [ 14 | "@semantic-release/git", 15 | { 16 | "assets": [ 17 | "pyproject.toml", 18 | "setup.py", 19 | "README.rst", 20 | "panos/__init__.py" 21 | ], 22 | "message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}" 23 | } 24 | ], 25 | [ 26 | "@semantic-release/github", 27 | { 28 | "successComment": ":tada: This ${issue.pull_request ? 'PR is included' : 'issue has been resolved'} in version ${nextRelease.version} :tada:\n\nThe release is available on [PyPI](https://pypi.org/project/pan-os-python/) and [GitHub release]()\n\n> Posted by [semantic-release](https://github.com/semantic-release/semantic-release) bot" 29 | } 30 | ] 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/python 3 | { 4 | "name": "Python 3", 5 | "image": "mcr.microsoft.com/devcontainers/python:1-3.9-bullseye", 6 | "features": { 7 | "ghcr.io/devcontainers-contrib/features/poetry:2": { 8 | "version": "latest" 9 | }, 10 | "ghcr.io/dhoeric/features/google-cloud-cli:1": {} 11 | }, 12 | "postCreateCommand": "poetry install", 13 | "customizations": { 14 | "vscode": { 15 | "extensions": [ 16 | // Python 17 | "ms-python.python", 18 | "ms-python.vscode-pylance", 19 | "ms-python.black-formatter", 20 | "ms-python.flake8", 21 | "matangover.mypy", 22 | "nwgh.bandit", 23 | "KevinRose.vsc-python-indent", 24 | // RestructuredText 25 | "lextudio.restructuredtext", 26 | "trond-snekvik.simple-rst", 27 | // Helpers 28 | "tamasfe.even-better-toml", 29 | "njpwerner.autodocstring", 30 | "aaron-bond.better-comments", 31 | // Tools 32 | "github.vscode-github-actions", 33 | "GitHub.copilot", 34 | "ms-toolsai.jupyter", 35 | // VIM 36 | "vscodevim.vim" 37 | ] 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /.github/set-version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | SCRIPT_BASE="$(cd "$( dirname "$0")" && pwd )" 4 | ROOT=${SCRIPT_BASE}/.. 5 | 6 | # Exit immediatly if any command exits with a non-zero status 7 | set -e 8 | 9 | # Usage 10 | print_usage() { 11 | echo "Set the app/add-on version" 12 | echo "" 13 | echo "Usage:" 14 | echo " set-version.sh " 15 | echo "" 16 | } 17 | 18 | # if less than one arguments supplied, display usage 19 | if [ $# -lt 1 ] 20 | then 21 | print_usage 22 | exit 1 23 | fi 24 | 25 | # check whether user had supplied -h or --help . If yes display usage 26 | if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then 27 | print_usage 28 | exit 0 29 | fi 30 | 31 | NEW_VERSION=$(echo "$1" | sed -e 's/-beta\./.b/' | sed -e 's/-alpha\./.a/') 32 | 33 | # Set version in pyproject.toml 34 | grep -E '^version = ".+"$' "$ROOT/pyproject.toml" >/dev/null 35 | sed -i.bak -E "s/^version = \".+\"$/version = \"$NEW_VERSION\"/" "$ROOT/pyproject.toml" && rm "$ROOT/pyproject.toml.bak" 36 | 37 | # Set version in __init__.py 38 | grep -E '^__version__ = ".+"$' "$ROOT/panos/__init__.py" >/dev/null 39 | sed -i.bak -E "s/^__version__ = \".+\"$/__version__ = \"$NEW_VERSION\"/" "$ROOT/panos/__init__.py" && rm "$ROOT/panos/__init__.py.bak" 40 | 41 | # Generate setup.py from pyproject.toml 42 | make sync-deps -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | #vscode file containing some env variables 10 | launch.json 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | # IPython Notebook 63 | .ipynb_checkpoints 64 | 65 | # pyenv 66 | .python-version 67 | 68 | # dotenv 69 | .env 70 | 71 | # virtualenv 72 | .venv/ 73 | venv/ 74 | ENV/ 75 | .envrc 76 | 77 | # PyCharm / IntelliJ 78 | .idea 79 | 80 | # Configtree diagram generated by sphinx 81 | docs/_diagrams 82 | 83 | # vim swap files 84 | *.swp 85 | -------------------------------------------------------------------------------- /docs/configtree.rst: -------------------------------------------------------------------------------- 1 | .. _classtree: 2 | 3 | Configuration tree diagrams 4 | =========================== 5 | 6 | These diagrams illustrates the possible tree structures for a Firewall/Panorama configuration. 7 | 8 | The tree diagrams are broken out into partial diagrams by module or function for better readability. 9 | The nodes are color coded by the module they are in according to the legend. 10 | 11 | Module Legend 12 | ------------- 13 | 14 | .. graphviz:: _diagrams/legend.dot 15 | 16 | .. _panoramatree: 17 | 18 | Panorama 19 | -------- 20 | 21 | A Panorama object can contain a DeviceGroup or Firewall, each of which 22 | can contain configuration objects. (see :ref:`firewalltree` below for objects that 23 | can be added to the Firewall object) 24 | 25 | .. graphviz:: _diagrams/panos.panorama.dot 26 | 27 | .. _firewalltree: 28 | 29 | Firewall 30 | -------- 31 | 32 | .. graphviz:: _diagrams/panos.firewall.dot 33 | 34 | .. _devicetree: 35 | 36 | Device 37 | ------ 38 | 39 | .. graphviz:: _diagrams/panos.device.dot 40 | 41 | .. _hatree: 42 | 43 | HA 44 | -- 45 | 46 | .. graphviz:: _diagrams/panos.ha.dot 47 | 48 | .. _networktree: 49 | 50 | Network 51 | ------- 52 | 53 | .. graphviz:: _diagrams/panos.network.dot 54 | 55 | .. _policytree: 56 | 57 | Policies 58 | -------- 59 | 60 | .. graphviz:: _diagrams/panos.policies.dot 61 | 62 | Plugins 63 | ------- 64 | 65 | .. graphviz:: _diagrams/panos.plugins.dot 66 | 67 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | These scripts are full examples that can be run on the CLI. For example: 5 | 6 | ```shell 7 | $ python dyn_address_group.py "10.0.1.1" "admin" "password" "1.2.3.4" "quarantine" 8 | ``` 9 | 10 | See the top of each script for usage instructions. 11 | 12 | **upgrade.py** 13 | 14 | Upgrades a Palo Alto Networks firewall or Panorama to the specified version. It 15 | takes care of all intermediate upgrades and reboots. 16 | 17 | **userid.py** 18 | 19 | Update User-ID by adding or removing a user-to-ip mapping on the firewall 20 | 21 | **dyn_address_group.py** 22 | 23 | Tag/untag ip addresses for Dynamic Address Groups on a firewall 24 | 25 | **ensure_security_rule.py** 26 | 27 | Ensure that specified security rule is on the firewall. Prints all the security 28 | rules connected to the firewall, then checks to make sure that the desired rule 29 | is present. If it is there, then the script ends. If not, it is created, and 30 | then a commit is performed. 31 | 32 | **log_forwarding_profile.py** 33 | 34 | Ensure that all security rules have the same log forwarding profile assigned. 35 | 36 | This script checks if any rules are missing the specified log forwarding profile 37 | and applies the profile if it is missing. This is done with as few API calls as 38 | possible. 39 | 40 | **bulk_address_objects.py** 41 | 42 | Use bulk operations to create / delete hundreds of firewall Address Objects. 43 | 44 | **bulk_subinterfaces.py** 45 | 46 | Use bulk operations to create / delete hundreds of firewall interfaces. -------------------------------------------------------------------------------- /tests/test_firewall.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, Palo Alto Networks 2 | # 3 | # Permission to use, copy, modify, and/or distribute this software for any 4 | # purpose with or without fee is hereby granted, provided that the above 5 | # copyright notice and this permission notice appear in all copies. 6 | # 7 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | 15 | import unittest 16 | 17 | import panos 18 | import panos.firewall 19 | 20 | 21 | class TestFirewall(unittest.TestCase): 22 | def test_id_returns_serial(self): 23 | expected = "serial#" 24 | 25 | fw = panos.firewall.Firewall( 26 | serial=expected, 27 | ) 28 | 29 | ret_val = fw.id 30 | 31 | self.assertEqual(expected, ret_val) 32 | 33 | def test_id_returns_hostname(self): 34 | expected = "hostName" 35 | 36 | fw = panos.firewall.Firewall( 37 | hostname=expected, 38 | ) 39 | 40 | ret_val = fw.id 41 | 42 | self.assertEqual(expected, ret_val) 43 | 44 | def test_id_returns_no_id(self): 45 | expected = "" 46 | 47 | fw = panos.firewall.Firewall() 48 | 49 | ret_val = fw.id 50 | 51 | self.assertEqual(expected, ret_val) 52 | 53 | 54 | if __name__ == "__main__": 55 | unittest.main() 56 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean-pyc clean-build docs clean local-setup 2 | 3 | help: 4 | @echo "clean - remove all build, test, coverage and Python artifacts" 5 | @echo "clean-build - remove build artifacts" 6 | @echo "clean-pyc - remove Python file artifacts" 7 | @echo "clean-test - remove test and coverage artifacts" 8 | @echo "lint - check style with flake8" 9 | @echo "bandit - check security with bandit" 10 | @echo "format - reformat code with black and isort" 11 | @echo "check-format - check code format/style with black and isort" 12 | @echo "test - run tests quickly with the default Python" 13 | @echo "test-all - run tests on every Python version with tox" 14 | @echo "coverage - check code coverage quickly with the default Python" 15 | @echo "docs - generate Sphinx HTML documentation, including API docs" 16 | @echo "release - package and upload a release" 17 | @echo "dist - package" 18 | @echo "sync-deps - save dependencies to requirements.txt" 19 | @echo "local-setup - sets up a linux or macos local directory for contribution by installing poetry and requirements" 20 | 21 | clean: clean-build clean-pyc clean-test clean-docs 22 | 23 | clean-build: 24 | rm -fr build/ 25 | rm -fr dist/ 26 | rm -fr *.egg-info 27 | 28 | clean-docs: 29 | rm -fr docs/_build/ 30 | rm -fr docs/_diagrams/ 31 | 32 | clean-pyc: 33 | find . -name '*.pyc' -exec rm -f {} + 34 | find . -name '*.pyo' -exec rm -f {} + 35 | find . -name '*~' -exec rm -f {} + 36 | find . -name '__pycache__' -exec rm -fr {} + 37 | 38 | clean-test: 39 | rm -fr .tox/ 40 | rm -f .coverage 41 | rm -fr htmlcov/ 42 | rm -fr .pytest_cache 43 | 44 | lint: 45 | flake8 panos tests 46 | 47 | bandit: 48 | bandit -r --ini .bandit 49 | 50 | format: 51 | isort --atomic panos 52 | black . 53 | 54 | check-format: 55 | isort --atomic --check-only panos 56 | black --check --diff . 57 | 58 | test: 59 | pytest 60 | 61 | test-simple: 62 | pytest --disable-warnings 63 | 64 | test-all: 65 | tox 66 | 67 | coverage: 68 | pytest --cov=panos 69 | 70 | docs: clean-docs 71 | $(MAKE) -C docs html 72 | open docs/_build/html/index.html 73 | 74 | release: clean 75 | python setup.py sdist upload 76 | python setup.py bdist_wheel upload 77 | 78 | dist: clean 79 | python setup.py sdist 80 | python setup.py bdist_wheel 81 | ls -l dist 82 | 83 | sync-deps: 84 | poetry export -f requirements.txt > requirements.txt 85 | 86 | local-setup: 87 | ifeq ($(wildcard ~/.local/bin/poetry),) 88 | @echo "installing poetry" 89 | curl -sSL https://install.python-poetry.org | python3 - 90 | else 91 | @echo "poetry installation found" 92 | endif 93 | ~/.local/bin/poetry install 94 | 95 | 96 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pan-os-python" 3 | version = "1.12.1" 4 | description = "Framework for interacting with Palo Alto Networks devices via API" 5 | authors = ["Palo Alto Networks "] 6 | license = "ISC" 7 | keywords = ["panos", "pan-os-python"] 8 | readme = "README.md" 9 | homepage = "https://github.com/PaloAltoNetworks/pan-os-python" 10 | repository = "https://github.com/PaloAltoNetworks/pan-os-python" 11 | documentation = "https://pan-os-python.readthedocs.io" 12 | packages = [ 13 | { include = "panos" } 14 | ] 15 | classifiers = [ 16 | "Development Status :: 5 - Production/Stable", 17 | "Intended Audience :: Developers", 18 | "License :: OSI Approved :: ISC License (ISCL)", 19 | "Natural Language :: English", 20 | "Programming Language :: Python :: 3", 21 | "Programming Language :: Python :: 3.5", 22 | "Programming Language :: Python :: 3.6", 23 | "Programming Language :: Python :: 3.7", 24 | "Programming Language :: Python :: 3.8", 25 | ] 26 | 27 | [tool.poetry.dependencies] 28 | python = "^2.7 || ^3.5" 29 | pan-python = "^0.17.0" 30 | 31 | [tool.poetry.group.dev.dependencies] 32 | pytest = {version = "^5.3.2", python = "^3.5"} 33 | black = {version = "^23.12.1", python = "^3.8"} 34 | flake8-eradicate = {version = "^1.5.0", python = "^3.8.1"} 35 | flake8-bugbear = {version = "^24.1.17", python = "^3.8.1"} 36 | flake8 = {version = "^7.0.0", python = "^3.8.1"} 37 | flake8-logging-format = {version = "^0.9.0", python = "^3.8.1"} 38 | flake8-pep3101 = {version = "^2.1.0", python = "^3.8.1"} 39 | flake8-builtins = {version = "^2.2.0", python = "^3.8.1"} 40 | flake8-comprehensions = {version = "^3.14.0", python = "^3.8.1"} 41 | flake8-string-format = {version = "^0.3.0", python = "^3.8.1"} 42 | flake8-mutable = {version = "^1.2.0", python = "^3.8.1"} 43 | flake8-pytest = {version = "^1.4", python = "^3.8.1"} 44 | flake8-mock = {version = "^0.4", python = "^3.8.1"} 45 | flake8-docstrings = {version = "^1.7.0", python = "^3.8.1"} 46 | flake8-variables-names = {version = "^0.0.6", python = "^3.8.1"} 47 | pep8-naming = {version = "^0.13.3", python = "^3.7"} 48 | fissix = {version = "^19.2b1", python = "^3.6"} 49 | sphinx = {version = "^2.3.1", python = "^3.5"} 50 | pytest-cov = {version = "^2.8.1", python = "^3.5"} 51 | sphinx_rtd_theme = {version = "^0.4.3", python = "^3.5"} 52 | sphinx-autobuild = {version = "^0.7.1", python = "^3.5"} 53 | bandit = {version = "^1.7.6", python = "^3.8"} 54 | isort = {version = "^5.13.2", python = "^3.8"} 55 | m2r = "^0.3.1" 56 | mypy = {version = "^1.8.0", python = "^3.8"} 57 | packaging = {version = "^23.2", python = "^3.7"} 58 | docutils = {version = "^0.20.1", python = "^3.7"} 59 | 60 | [build-system] 61 | requires = ["poetry>=0.12"] 62 | build-backend = "poetry.masonry.api" 63 | -------------------------------------------------------------------------------- /tests/test_panorama.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | 3 | try: 4 | from unittest import mock 5 | except ImportError: 6 | import mock 7 | 8 | from panos.panorama import DeviceGroup 9 | from panos.panorama import Panorama 10 | 11 | 12 | def _device_group_hierarchy(): 13 | pano = Panorama("127.0.0.1", "admin", "admin", "secret") 14 | pano._version_info = (9999, 0, 0) 15 | dg = DeviceGroup("drums") 16 | pano.add(dg) 17 | pano.op = mock.Mock( 18 | return_value=ET.fromstring( 19 | """ 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | """, 42 | ) 43 | ) 44 | 45 | return dg 46 | 47 | 48 | def test_panorama_dg_hierarchy_top_has_none_parent(): 49 | dg = _device_group_hierarchy() 50 | 51 | ans = dg.parent.opstate.dg_hierarchy.fetch() 52 | 53 | for key in ("people", "solo group", "another solo group", "instruments", "parent"): 54 | assert key in ans 55 | assert ans[key] is None 56 | 57 | 58 | def test_panorama_dg_hierarchy_first_level_child(): 59 | dg = _device_group_hierarchy() 60 | 61 | ans = dg.parent.opstate.dg_hierarchy.fetch() 62 | 63 | fields = [ 64 | ("people", "friends"), 65 | ("instruments", "bass"), 66 | ("instruments", "drums"), 67 | ("instruments", "guitar"), 68 | ("parent", "child"), 69 | ] 70 | 71 | for parent, child in fields: 72 | assert child in ans 73 | assert ans[child] == parent 74 | 75 | 76 | def test_panorama_dg_hierarchy_second_level_children(): 77 | dg = _device_group_hierarchy() 78 | 79 | ans = dg.parent.opstate.dg_hierarchy.fetch() 80 | 81 | for field in ("jack", "jill"): 82 | assert field in ans 83 | assert ans[field] == "friends" 84 | assert ans["friends"] == "people" 85 | assert ans["people"] is None 86 | 87 | 88 | def test_device_group_hierarchy_refresh(): 89 | dg = _device_group_hierarchy() 90 | 91 | assert dg.opstate.dg_hierarchy.parent is None 92 | 93 | dg.opstate.dg_hierarchy.refresh() 94 | 95 | assert dg.opstate.dg_hierarchy.parent == "instruments" 96 | -------------------------------------------------------------------------------- /examples/log_forwarding_profile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2020, Palo Alto Networks 4 | # 5 | # Permission to use, copy, modify, and/or distribute this software for any 6 | # purpose with or without fee is hereby granted, provided that the above 7 | # copyright notice and this permission notice appear in all copies. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | 17 | """ 18 | log_forwarding_profile.py 19 | ========================== 20 | 21 | Ensure that all security rules have the same log forwarding profile assigned. 22 | 23 | This script checks if any rules are missing the specified log forwarding profile 24 | and applies the profile if it is missing. This is done with as few API calls as 25 | possible. 26 | 27 | Environment variables required: 28 | PAN_HOSTNAME: The hostname or IP of the Firewall 29 | PAN_USERNAME: The username of a firewall admin 30 | PAN_PASSWORD: The password of a firewall admin 31 | PAN_LOG_PROFILE: The name of the log forwarding profile to apply 32 | 33 | """ 34 | 35 | import os 36 | 37 | from panos.firewall import Firewall 38 | from panos.policies import Rulebase, SecurityRule 39 | 40 | HOSTNAME = os.environ["PAN_HOSTNAME"] 41 | USERNAME = os.environ["PAN_USERNAME"] 42 | PASSWORD = os.environ["PAN_PASSWORD"] 43 | LOG_PROFILE = os.environ["PAN_LOG_PROFILE"] 44 | 45 | 46 | def main(): 47 | # Create a connection to a firewall and a rulebase to work inside 48 | fw = Firewall(HOSTNAME, USERNAME, PASSWORD) 49 | rulebase = fw.add(Rulebase()) 50 | 51 | # Fetch all the security rules from the firewall into a list 52 | rules = SecurityRule.refreshall(rulebase, add=False) 53 | 54 | print(f"Checking {len(rules)} rules...") 55 | 56 | # Iterate over the list and collect names of rules that are 57 | # missing the log forwarding profile 58 | for rule in rules: 59 | if rule.log_setting != LOG_PROFILE: 60 | print(f"{rule.name}") 61 | rule.log_setting = LOG_PROFILE 62 | rule.log_start = 0 63 | rule.log_end = 1 64 | rule.apply() 65 | 66 | # At this point, we've added SecurityRule objects to the Firewall 67 | # for each rule that doesn't have the right log forwarding profile. 68 | 69 | # Now, trigger a commit 70 | # In this case, we'll wait for the commit to finish and trigger an exception 71 | # if the commit finished with any errors. 72 | print("Starting commit") 73 | fw.commit(sync=True, exception=True) 74 | print("Commit finished successfully") 75 | 76 | 77 | if __name__ == "__main__": 78 | main() 79 | -------------------------------------------------------------------------------- /examples/prisma_access_show_remote_net_per_tenant.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2022, Palo Alto Networks 4 | # 5 | # Permission to use, copy, modify, and/or distribute this software for any 6 | # purpose with or without fee is hereby granted, provided that the above 7 | # copyright notice and this permission notice appear in all copies. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | # ACTION OF CONTRACT, NEGLIGENCE OR OTpHER TORTIOUS ACTION, ARISING OUT OF 15 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | 17 | # Author: Bastien Migette 18 | 19 | """ 20 | prisma_access_show_remote_net_per_tenant.py 21 | ========== 22 | 23 | This script is an example on how to retrieve list of prisma access 24 | tenants and their remote networks 25 | 26 | """ 27 | __author__ = "bmigette" 28 | 29 | 30 | import logging 31 | import os 32 | import sys 33 | 34 | # This is needed to import module from parent folder 35 | curpath = os.path.dirname(os.path.abspath(__file__)) 36 | sys.path[:0] = [os.path.join(curpath, os.pardir)] 37 | 38 | 39 | from panos.base import PanDevice 40 | from panos.panorama import Panorama 41 | from panos.plugins import CloudServicesPlugin, RemoteNetwork, RemoteNetworks, Tenants 42 | 43 | curpath = os.path.dirname(os.path.abspath(__file__)) 44 | sys.path[:0] = [os.path.join(curpath, os.pardir)] 45 | 46 | 47 | HOSTNAME = os.environ["PAN_HOSTNAME"] 48 | USERNAME = os.environ["PAN_USERNAME"] 49 | PASSWORD = os.environ["PAN_PASSWORD"] 50 | 51 | 52 | def main(): 53 | # Setting logging to debug the PanOS SDK 54 | logging_format = "%(levelname)s:%(name)s:%(message)s" 55 | # logging.basicConfig(format=logging_format, level=logging.DEBUG - 2) #Use this to be even more verbose 56 | logging.basicConfig(format=logging_format, level=logging.DEBUG) 57 | # First, let's create the panorama object that we want to modify. 58 | pan = Panorama(HOSTNAME, USERNAME, PASSWORD) 59 | csp = pan.add(CloudServicesPlugin()) 60 | 61 | # This is to load candidate config instead of running config 62 | csp.refresh(running_config=False) 63 | 64 | if not csp.multi_tenant_enable: 65 | logging.error("Multi Tenant not enabled") 66 | sys.exit(-1) 67 | tenants = csp.findall(Tenants) 68 | 69 | ### Print Tenants ### 70 | for tenant in tenants: 71 | logging.info("====== Tenant: %s ======", tenant.name) 72 | remote_networks = tenant.findall(RemoteNetworks)[0].findall(RemoteNetwork) 73 | for remote_network in remote_networks: 74 | logging.info( 75 | "name: %s, region: %s, IPSEC Node: %s, spn name: %s", 76 | remote_network.name, 77 | remote_network.region, 78 | remote_network.ipsec_tunnel, 79 | remote_network.spn_name, 80 | ) 81 | 82 | 83 | if __name__ == "__main__": 84 | main() 85 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every 6 | little bit helps, and credit will always be given. 7 | 8 | You can contribute in many ways: 9 | 10 | Types of Contributions 11 | ---------------------- 12 | 13 | Report Bugs 14 | ~~~~~~~~~~~ 15 | 16 | Report bugs at https://github.com/PaloAltoNetworks/pan-os-python/issues. 17 | 18 | Fix Bugs 19 | ~~~~~~~~ 20 | 21 | Look through the GitHub issues for bugs. Anything tagged with "bug" 22 | is open to whoever wants to fix it. 23 | 24 | Implement Features 25 | ~~~~~~~~~~~~~~~~~~ 26 | 27 | Look through the GitHub issues for features. Anything tagged with "enhancement" 28 | is open to whoever wants to implement it. 29 | 30 | Write Documentation 31 | ~~~~~~~~~~~~~~~~~~~ 32 | 33 | The PAN-OS SDK for Python could always use more documentation, whether as part of the 34 | official pan-os-python docs, in docstrings, or even on the web in blog posts, 35 | articles, and such. 36 | 37 | The main documentation is in the `docs` directory and the API reference is 38 | generated from docstrings in the code. 39 | 40 | After you set up your development environment, type ``poetry run make docs`` to 41 | generate the documentation locally. 42 | 43 | Submit Feedback 44 | ~~~~~~~~~~~~~~~ 45 | 46 | The best way to send feedback is to file an issue at https://github.com/PaloAltoNetworks/pan-os-python/issues. 47 | 48 | If you are proposing a feature: 49 | 50 | * Explain in detail how it would work. 51 | * Keep the scope as narrow as possible, to make it easier to implement. 52 | * Remember that this is a volunteer-driven project, and that contributions 53 | are welcome :) 54 | 55 | Get Started! 56 | ------------ 57 | 58 | Ready to contribute some code? Here's how to set up `pan-os-python` for local development. 59 | 60 | 1. Install python 3.6 or higher 61 | 62 | Development must be done using python 3.6 or higher. Development on python 2.7 is 63 | no longer supported. 64 | 65 | 2. Fork the `pan-os-python` repo on GitHub. 66 | 67 | 3. Clone your fork locally:: 68 | 69 | $ git clone https://github.com/your-username/pan-os-python.git 70 | 71 | 4. Install Poetry 72 | 73 | Poetry is a dependency manager and build tool for python 74 | If you don't have poetry installed, use the instructions here to install it: 75 | 76 | https://python-poetry.org/docs/#installation 77 | 78 | 5. Create a virtual environment with dependencies:: 79 | 80 | $ poetry install 81 | 82 | 6. Create a branch for local development:: 83 | 84 | $ git checkout -b name-of-your-bugfix-or-feature 85 | 86 | 7. Now you can make your changes locally 87 | 88 | 8. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: 89 | 90 | $ poetry run make lint 91 | $ poetry run make bandit 92 | $ poetry run make test 93 | $ poetry run make test-all 94 | $ poetry run make sync-deps 95 | 96 | 9. Commit your changes and push your branch to GitHub:: 97 | 98 | $ git add -A 99 | $ git commit -m "Your detailed description of your changes." 100 | $ git push origin name-of-your-bugfix-or-feature 101 | 102 | 10. Submit a pull request through the GitHub website. 103 | -------------------------------------------------------------------------------- /tests/test_vsys_xpaths.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from panos import device 4 | from panos import firewall 5 | from panos import network 6 | from panos import objects 7 | from panos import panorama 8 | 9 | 10 | def _check(obj, vsys, with_pano, chk_import=False): 11 | if chk_import: 12 | func = "xpath_import_base" 13 | else: 14 | func = "xpath" 15 | fw = firewall.Firewall("127.0.0.1", "admin", "admin", serial="01234567890") 16 | fw.vsys = vsys 17 | fw.add(obj) 18 | 19 | if with_pano: 20 | pano = panorama.Panorama("127.0.0.1", "admin2", "admin2") 21 | pano.add(fw) 22 | 23 | expected = getattr(obj, func)() 24 | 25 | fw.remove(obj) 26 | fw.vsys = None 27 | vsys = device.Vsys(vsys or "vsys1") 28 | fw.add(vsys) 29 | vsys.add(obj) 30 | 31 | result = getattr(obj, func)() 32 | 33 | assert expected == result 34 | 35 | 36 | @pytest.mark.parametrize("vsys", [None, "vsys1", "vsys3"]) 37 | @pytest.mark.parametrize("with_pano", [False, True]) 38 | def test_xpath_for_vsys_root(vsys, with_pano): 39 | obj = network.Zone("myzone") 40 | _check(obj, vsys, with_pano) 41 | 42 | 43 | @pytest.mark.parametrize("vsys", [None, "vsys1", "vsys3"]) 44 | @pytest.mark.parametrize("with_pano", [False, True]) 45 | def test_xpath_for_device_root(vsys, with_pano): 46 | obj = device.SystemSettings(hostname="example") 47 | _check(obj, vsys, with_pano) 48 | 49 | 50 | @pytest.mark.parametrize("vsys", [None, "vsys1", "vsys3"]) 51 | @pytest.mark.parametrize("with_pano", [False, True]) 52 | def test_xpath_for_mgtconfig_root(vsys, with_pano): 53 | obj = device.Administrator("newadmin") 54 | _check(obj, vsys, with_pano) 55 | 56 | 57 | @pytest.mark.parametrize("vsys", [None, "vsys1", "vsys3"]) 58 | @pytest.mark.parametrize("with_pano", [False, True]) 59 | @pytest.mark.parametrize( 60 | "obj", 61 | [ 62 | network.EthernetInterface("ethernet1/3", "layer3"), 63 | network.Layer3Subinterface("ethernet1/4.42", 42), 64 | network.Layer2Subinterface("ethernet1/4.420", 420), 65 | network.VirtualRouter("someroute"), 66 | network.VirtualWire("tripwire"), 67 | network.Vlan("myvlan"), 68 | ], 69 | ) 70 | def test_xpath_import(vsys, with_pano, obj): 71 | _check(obj, vsys, with_pano, True) 72 | 73 | 74 | def test_vsys_xpath_unchanged(): 75 | expected = ( 76 | "/config/devices/entry[@name='localhost.localdomain']/vsys/entry[@name='vsys3']" 77 | ) 78 | c = firewall.Firewall("127.0.0.1", "admin", "admin") 79 | c.vsys = "vsys3" 80 | 81 | assert expected == c.xpath_vsys() 82 | 83 | c.vsys = None 84 | vsys = device.Vsys("vsys3") 85 | c.add(vsys) 86 | 87 | assert expected == vsys.xpath_vsys() 88 | 89 | zone = network.Zone("myzone") 90 | vsys.add(zone) 91 | 92 | assert expected == zone.xpath_vsys() 93 | 94 | 95 | def test_device_group_xpath_unchanged(): 96 | expected = "/config/devices/entry[@name='localhost.localdomain']/device-group/entry[@name='somegroup']/address/entry[@name='intnet']" 97 | pano = panorama.Panorama("127.0.0.1") 98 | dg = panorama.DeviceGroup("somegroup") 99 | ao = objects.AddressObject("intnet", "192.168.0.0/16") 100 | pano.add(dg) 101 | dg.add(ao) 102 | 103 | assert expected == ao.xpath() 104 | -------------------------------------------------------------------------------- /examples/prisma_access_show_jobs_status.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2022, Palo Alto Networks 4 | # 5 | # Permission to use, copy, modify, and/or distribute this software for any 6 | # purpose with or without fee is hereby granted, provided that the above 7 | # copyright notice and this permission notice appear in all copies. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | # ACTION OF CONTRACT, NEGLIGENCE OR OTpHER TORTIOUS ACTION, ARISING OUT OF 15 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | 17 | # Author: Bastien Migette 18 | 19 | """ 20 | prisma_access_show_jobs_status.py 21 | ========== 22 | 23 | This script is an example on how to retrieve list of prisma access 24 | jobs (commit and push), and how to get details of a specific job 25 | 26 | """ 27 | __author__ = "bmigette" 28 | 29 | 30 | import logging 31 | import os 32 | import sys 33 | 34 | # This is needed to import module from parent folder 35 | curpath = os.path.dirname(os.path.abspath(__file__)) 36 | sys.path[:0] = [os.path.join(curpath, os.pardir)] 37 | 38 | 39 | from panos.base import PanDevice 40 | from panos.panorama import Panorama 41 | from panos.plugins import CloudServicesPlugin 42 | 43 | curpath = os.path.dirname(os.path.abspath(__file__)) 44 | sys.path[:0] = [os.path.join(curpath, os.pardir)] 45 | 46 | 47 | HOSTNAME = os.environ["PAN_HOSTNAME"] 48 | USERNAME = os.environ["PAN_USERNAME"] 49 | PASSWORD = os.environ["PAN_PASSWORD"] 50 | 51 | 52 | def main(): 53 | # Setting logging to debug the PanOS SDK 54 | logging_format = "%(levelname)s:%(name)s:%(message)s" 55 | # logging.basicConfig( 56 | # format=logging_format, level=logging.DEBUG - 2 57 | # ) # Use this to be even more verbose 58 | logging.basicConfig(format=logging_format, level=logging.DEBUG) 59 | # First, let's create the panorama object that we want to modify. 60 | pan = Panorama(HOSTNAME, USERNAME, PASSWORD) 61 | csp = pan.add(CloudServicesPlugin()) 62 | 63 | csp.opstate.jobs.refresh() 64 | 65 | # get only failed for mobile-users 66 | # csp.opstate.jobs.refresh(servicetype='mobile-users', success=False, pending=False) 67 | # get only failed for mobile-users and remote networks 68 | # csp.opstate.jobs.refresh(servicetype=['mobile-users', 'remote-networks'], success=False, pending=False) 69 | 70 | ### Print jobs ### 71 | 72 | print(csp.opstate.jobs.status) 73 | svcs = [ 74 | "mobile-users", 75 | "remote-networks", 76 | "clean-pipe", 77 | "service-connection", 78 | ] 79 | for svc in svcs: 80 | print(f" -- {svc} Jobs --") 81 | print(csp.opstate.jobs.status[svc]) 82 | 83 | ### Showing a job details ### 84 | failed_job_id = csp.opstate.jobs.status["mobile-users"]["failed"][-1] 85 | failed_details = csp.opstate.jobs_details.refresh(failed_job_id, "mobile-users") 86 | 87 | print(f"Details for job {failed_job_id}: {failed_details}") 88 | print(csp.opstate.jobs_details.details) 89 | 90 | 91 | if __name__ == "__main__": 92 | main() 93 | -------------------------------------------------------------------------------- /examples/prisma_access_list_RN_regions_bw.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2022, Palo Alto Networks 4 | # 5 | # Permission to use, copy, modify, and/or distribute this software for any 6 | # purpose with or without fee is hereby granted, provided that the above 7 | # copyright notice and this permission notice appear in all copies. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | # ACTION OF CONTRACT, NEGLIGENCE OR OTpHER TORTIOUS ACTION, ARISING OUT OF 15 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | 17 | # Author: Bastien Migette 18 | 19 | """ 20 | prisma_access_list_RN_regions_bw.py 21 | ========== 22 | 23 | This script is an example on how to retrieve list of prisma access 24 | remote networks locations and bandwidth allocation and print it. 25 | 26 | """ 27 | __author__ = "bmigette" 28 | 29 | 30 | import logging 31 | import os 32 | import sys 33 | 34 | # This is needed to import module from parent folder 35 | curpath = os.path.dirname(os.path.abspath(__file__)) 36 | sys.path[:0] = [os.path.join(curpath, os.pardir)] 37 | 38 | 39 | from panos.base import PanDevice 40 | from panos.panorama import Panorama 41 | from panos.plugins import ( 42 | AggBandwidth, 43 | CloudServicesPlugin, 44 | Region, 45 | RemoteNetwork, 46 | RemoteNetworks, 47 | ) 48 | 49 | curpath = os.path.dirname(os.path.abspath(__file__)) 50 | sys.path[:0] = [os.path.join(curpath, os.pardir)] 51 | 52 | 53 | HOSTNAME = os.environ["PAN_HOSTNAME"] 54 | USERNAME = os.environ["PAN_USERNAME"] 55 | PASSWORD = os.environ["PAN_PASSWORD"] 56 | 57 | 58 | def main(): 59 | # Setting logging to debug the PanOS SDK 60 | logging_format = "%(levelname)s:%(name)s:%(message)s" 61 | # logging.basicConfig(format=logging_format, level=logging.DEBUG - 2) #Use this to be even more verbose 62 | logging.basicConfig(format=logging_format, level=logging.DEBUG) 63 | # First, let's create the panorama object that we want to modify. 64 | pan = Panorama(HOSTNAME, USERNAME, PASSWORD) 65 | csp = pan.add(CloudServicesPlugin()) 66 | 67 | csp.refresh() 68 | 69 | rn = csp.findall(RemoteNetworks) 70 | rnes = rn[0].findall(RemoteNetwork) 71 | agg_bw = rn[0].findall(AggBandwidth) 72 | 73 | regions = agg_bw[0].findall(Region) 74 | ### Print XML Dump of Prisma Config ### 75 | print(csp.element_str()) 76 | print(csp.about()) 77 | 78 | ### Print Remote networks name ### 79 | print(" -- Remote Networks --") 80 | for rne in rnes: 81 | print( 82 | f"{rne.name} - spn: {rne.spn_name}, region: {rne.region}, tunnel {rne.ipsec_tunnel}, subnets: {rne.subnets}" 83 | ) 84 | print( 85 | f"{rne.name} - secondary_wan: {rne.secondary_wan_enabled}, secondary ipsec tunnel: {rne.secondary_ipsec_tunnel}" 86 | ) 87 | 88 | ### Print Regions BW ### 89 | print(f"Agg BW Enabled: {agg_bw[0].enabled}") 90 | print(" -- Regions --") 91 | print(regions) 92 | for region in regions: 93 | print( 94 | f"Region: {region}, allocated_bw: {region.allocated_bw}, spns: {region.spn_name_list}" 95 | ) 96 | 97 | 98 | if __name__ == "__main__": 99 | main() 100 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | .. _examples: 2 | 3 | Examples 4 | ======== 5 | 6 | Example scripts 7 | --------------- 8 | 9 | There are several example scripts written as CLI programs in the [examples 10 | directory](https://github.com/PaloAltoNetworks/pan-os-python/tree/develop/examples). 11 | 12 | Cookbook examples 13 | ----------------- 14 | 15 | Get the version of a firewall 16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 17 | 18 | .. code-block:: python 19 | 20 | from panos.firewall import Firewall 21 | 22 | fw = Firewall("10.0.0.1", "admin", "mypassword") 23 | version = fw.refresh_system_info().version 24 | print version 25 | 26 | Example output:: 27 | 28 | 10.0.3 29 | 30 | 31 | We use ``refresh_system_info()`` here instead of an op commands because this 32 | method saves the version information to the Firewall object which tells all 33 | future API calls what format to use to be compatible with this version. 34 | 35 | Print a firewall rule 36 | ~~~~~~~~~~~~~~~~~~~~~ 37 | 38 | .. code-block:: python 39 | 40 | from panos.firewall import Firewall 41 | from panos.policies import Rulebase, SecurityRule 42 | 43 | # Create a config tree for the rule 44 | fw = Firewall("10.0.0.1", "admin", "mypassword", vsys="vsys1") 45 | rulebase = fw.add(Rulebase()) 46 | rule = rulebase.add(SecurityRule("my-rule")) 47 | 48 | # Refresh the rule from the live device and print it 49 | rule.refresh() 50 | print(rule.about()) 51 | 52 | List of firewall rules by name 53 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 54 | 55 | .. code-block:: python 56 | 57 | from panos.firewall import Firewall 58 | from panos.policies import Rulebase, SecurityRule 59 | 60 | # Create config tree and refresh rules from live device 61 | fw = Firewall("10.0.0.1", "admin", "mypassword", vsys="vsys1") 62 | rulebase = fw.add(Rulebase()) 63 | rules = SecurityRule.refreshall(rulebase) 64 | 65 | for rule in rules: 66 | print(rule.name) 67 | 68 | List of pre-rules on Panorama 69 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 70 | 71 | .. code-block:: python 72 | 73 | from panos.panorama import Panorama 74 | from panos.policies import PreRulebase, SecurityRule 75 | 76 | # Create config tree and refresh rules from live device 77 | pano = Panorama("10.0.0.1", "admin", "mypassword") 78 | pre_rulebase = pano.add(PreRulebase()) 79 | rules = SecurityRule.refreshall(pre_rulebase) 80 | 81 | for rule in rules: 82 | print(rule.name) 83 | 84 | List firewall devices in Panorama 85 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 86 | 87 | Print the serial, hostname, and management IP of all firewalls 88 | that Panorama knows about. 89 | 90 | .. code-block:: python 91 | 92 | from panos.panorama import Panorama 93 | from panos.device import SystemSettings 94 | 95 | # Create config tree root 96 | pano = Panorama("10.0.0.1", "admin", "mypassword") 97 | 98 | # Refresh firewalls from live Panorama 99 | devices = pano.refresh_devices(expand_vsys=False, include_device_groups=False) 100 | 101 | # Print each firewall's serial and management IP 102 | for device in devices: 103 | system_settings = device.find("", SystemSettings) 104 | print(f"{device.serial} {system_settings.hostname} {system_settings.ip_address}") 105 | 106 | Example output:: 107 | 108 | 310353000003333 PA-VM-1 10.1.1.1 109 | 310353000003334 PA-VM-2 10.1.1.2 110 | 111 | Upgrade a firewall 112 | ~~~~~~~~~~~~~~~~~~ 113 | 114 | .. code-block:: python 115 | 116 | from panos.firewall import Firewall 117 | 118 | fw = Firewall("10.0.0.1", "admin", "mypassword") 119 | fw.software.upgrade_to_version("10.1.5") 120 | 121 | This simple example will upgrade from any previous version to the target version 122 | and handle all intermediate upgrades and reboots. 123 | -------------------------------------------------------------------------------- /docs/moduleref.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2016, Palo Alto Networks 4 | # 5 | # Permission to use, copy, modify, and/or distribute this software for any 6 | # purpose with or without fee is hereby granted, provided that the above 7 | # copyright notice and this permission notice appear in all copies. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | 17 | # Author: Brian Torres-Gil 18 | 19 | """Generate API reference page for each module""" 20 | 21 | import errno 22 | import os 23 | import pkgutil 24 | import sys 25 | 26 | tree_exists = [ 27 | "device", 28 | "firewall", 29 | "ha", 30 | "network", 31 | "panorama", 32 | "plugins", 33 | "policies", 34 | ] 35 | 36 | tree_not_exists = [ 37 | "base", 38 | "errors", 39 | "objects", 40 | "updater", 41 | "userid", 42 | ] 43 | 44 | 45 | template_main = """Module: {0} 46 | ========{1} 47 | 48 | Inheritance diagram 49 | ------------------- 50 | 51 | .. inheritance-diagram:: panos.{0} 52 | :parts: 1{2} 53 | 54 | Class Reference 55 | --------------- 56 | 57 | .. automodule:: panos.{0} 58 | """ 59 | 60 | 61 | template_tree = """ 62 | 63 | Configuration tree diagram 64 | -------------------------- 65 | 66 | .. graphviz:: _diagrams/panos.{0}.dot """ 67 | 68 | 69 | def mkdir_p(path): 70 | """Make a full directory path""" 71 | try: 72 | os.makedirs(path) 73 | except OSError as exc: # Python >2.5 74 | if exc.errno == errno.EEXIST and os.path.isdir(path): 75 | pass 76 | else: 77 | raise 78 | 79 | 80 | def create_module_references(directory=None): 81 | # Set paths to package and modules 82 | curdir = os.path.dirname(os.path.abspath(__file__)) 83 | rootpath = [os.path.join(curdir, os.pardir)] 84 | libpath = [os.path.join(curdir, os.pardir, "panos")] 85 | sys.path[:0] = rootpath 86 | sys.path[:0] = libpath 87 | # print "Looking for panos in path: %s" % libpath 88 | 89 | # Import all modules in package 90 | modules = [] 91 | for importer, modname, ispkg in pkgutil.iter_modules(path=libpath, prefix="panos."): 92 | modules.append(__import__(modname, fromlist="dummy")) 93 | 94 | output = {} 95 | 96 | # Create output for each module 97 | for module in modules: 98 | module_name = module.__name__.split(".")[-1] 99 | header_pad = "=" * len(module_name) 100 | if module_name in tree_exists: 101 | config_tree = template_tree.format(module_name) 102 | else: 103 | config_tree = "" 104 | module_string = template_main.format(module_name, header_pad, config_tree) 105 | output[module_name] = module_string 106 | 107 | # Write output to file or stdout 108 | path = "" 109 | if directory is not None: 110 | mkdir_p(directory) 111 | path = directory + "/" 112 | for module, lines in output.iteritems(): 113 | if module == "interface": 114 | continue 115 | if not lines: 116 | continue 117 | with open("{0}module-{1}.rst".format(path, module), "w") as file: 118 | file.write(lines) 119 | 120 | 121 | if __name__ == "__main__": 122 | create_module_references() 123 | -------------------------------------------------------------------------------- /panos/errors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2014, Palo Alto Networks 4 | # 5 | # Permission to use, copy, modify, and/or distribute this software for any 6 | # purpose with or without fee is hereby granted, provided that the above 7 | # copyright notice and this permission notice appear in all copies. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | 17 | 18 | """Exception classes used by pan-os-python package""" 19 | 20 | from pan.xapi import PanXapiError 21 | 22 | 23 | # Exceptions used by PanDevice Class 24 | class PanDeviceError(PanXapiError): 25 | """Exception for errors in the PanDevice class 26 | 27 | The PanDevice class may raise errors when problems occur such as 28 | response parsing problems. This exception class is raised on those 29 | errors. This class is not for errors connecting to the API, as 30 | pan.xapi.PanXapiError is responsible for those. 31 | 32 | Attributes: 33 | message: The error message for the exception 34 | pan_device: A reference to the PanDevice that generated the exception 35 | 36 | """ 37 | 38 | def __init__(self, *args, **kwargs): 39 | self.pan_device = kwargs.pop("pan_device", None) 40 | super(PanDeviceError, self).__init__(*args, **kwargs) 41 | self.message = "{0}".format(self) 42 | 43 | 44 | class PanDeviceXapiError(PanDeviceError): 45 | """General error returned by an API call""" 46 | 47 | pass 48 | 49 | 50 | class PanInvalidCredentials(PanDeviceXapiError): 51 | pass 52 | 53 | 54 | class PanURLError(PanDeviceXapiError): 55 | pass 56 | 57 | 58 | class PanConnectionTimeout(PanDeviceXapiError): 59 | pass 60 | 61 | 62 | class PanJobTimeout(PanDeviceError): 63 | pass 64 | 65 | 66 | class PanLockError(PanDeviceError): 67 | pass 68 | 69 | 70 | class PanPendingChanges(PanDeviceError): 71 | pass 72 | 73 | 74 | class PanCommitInProgress(PanDeviceXapiError): 75 | pass 76 | 77 | 78 | class PanInstallInProgress(PanDeviceXapiError): 79 | pass 80 | 81 | 82 | class PanCommitFailed(PanDeviceXapiError): 83 | def __init__(self, *args, **kwargs): 84 | self.result = kwargs.pop("result", None) 85 | super(PanCommitFailed, self).__init__("Commit failed", *args, **kwargs) 86 | 87 | 88 | class PanCommitNotNeeded(PanDeviceXapiError): 89 | pass 90 | 91 | 92 | class PanSessionTimedOut(PanDeviceXapiError): 93 | pass 94 | 95 | 96 | class PanDeviceNotSet(PanDeviceError): 97 | pass 98 | 99 | 100 | class PanNotConnectedOnPanorama(PanDeviceError): 101 | pass 102 | 103 | 104 | class PanNotAttachedOnPanorama(PanDeviceError): 105 | pass 106 | 107 | 108 | class PanNoSuchNode(PanDeviceXapiError): 109 | pass 110 | 111 | 112 | class PanObjectMissing(PanDeviceXapiError): 113 | pass 114 | 115 | 116 | class PanHAConfigSyncFailed(PanDeviceXapiError): 117 | pass 118 | 119 | 120 | class PanHASyncInProgress(PanDeviceXapiError): 121 | pass 122 | 123 | 124 | class PanObjectError(PanDeviceError): 125 | pass 126 | 127 | 128 | class PanApiKeyNotSet(PanDeviceError): 129 | pass 130 | 131 | 132 | class PanActivateFeatureAuthCodeError(PanDeviceError): 133 | pass 134 | 135 | 136 | class PanOutdatedSslError(PanDeviceError): 137 | pass 138 | -------------------------------------------------------------------------------- /tests/test_userid.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018, Palo Alto Networks 2 | # 3 | # Permission to use, copy, modify, and/or distribute this software for any 4 | # purpose with or without fee is hereby granted, provided that the above 5 | # copyright notice and this permission notice appear in all copies. 6 | # 7 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | try: 15 | from unittest import mock 16 | except ImportError: 17 | import mock 18 | import sys 19 | import unittest 20 | 21 | import panos.firewall 22 | import panos.panorama 23 | 24 | 25 | class TestUserId(unittest.TestCase): 26 | """ 27 | [Test section: userid] 28 | 29 | Verify some userid methods that can be tested without 30 | a live device 31 | """ 32 | 33 | def test_login(self): 34 | # Must set up different expectations for python 3.8 and higher 35 | # Per documentation: "Changed in version 3.8: The tostring() 36 | # function now preserves the attribute order specified..." 37 | # https://docs.python.org/3/library/xml.etree.elementtree.html#xml.etree.ElementTree.tostring 38 | if sys.version_info <= (3, 8): 39 | expected = ( 40 | b"1.0" 41 | b"update" 42 | b'' 43 | b"" 44 | ) 45 | else: 46 | expected = ( 47 | b"1.0" 48 | b"update" 49 | b'' 50 | b"" 51 | ) 52 | vsys = "vsys3" 53 | 54 | fw = panos.firewall.Firewall( 55 | "fw1", "user", "passwd", "authkey", serial="Serial", vsys=vsys 56 | ) 57 | fw.xapi 58 | fw._xapi_private.user_id = mock.Mock() 59 | 60 | fw.userid.login(r"example.com\username", "10.1.1.1", timeout=10) 61 | 62 | fw._xapi_private.user_id.assert_called_once_with(cmd=expected, vsys=vsys) 63 | 64 | def test_batch_tag_user(self): 65 | fw = panos.firewall.Firewall( 66 | "fw1", "user", "passwd", "authkey", serial="Serial", vsys="vsys1" 67 | ) 68 | fw.xapi 69 | fw.userid.batch_start() 70 | fw.userid.tag_user( 71 | "user1", 72 | [ 73 | "tag1", 74 | ], 75 | ) 76 | fw.userid.tag_user( 77 | "user2", 78 | [ 79 | "tag1", 80 | ], 81 | ) 82 | 83 | def test_batch_untag_user(self): 84 | fw = panos.firewall.Firewall( 85 | "fw1", "user", "passwd", "authkey", serial="Serial", vsys="vsys2" 86 | ) 87 | fw.xapi 88 | fw.userid.batch_start() 89 | fw.userid.untag_user( 90 | "user1", 91 | [ 92 | "tag1", 93 | ], 94 | ) 95 | fw.userid.untag_user( 96 | "user2", 97 | [ 98 | "tag1", 99 | ], 100 | ) 101 | 102 | 103 | if __name__ == "__main__": 104 | unittest.main() 105 | -------------------------------------------------------------------------------- /examples/upgrade.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2014, Palo Alto Networks 4 | # 5 | # Permission to use, copy, modify, and/or distribute this software for any 6 | # purpose with or without fee is hereby granted, provided that the above 7 | # copyright notice and this permission notice appear in all copies. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | 17 | # Author: Brian Torres-Gil 18 | 19 | """ 20 | upgrade.py 21 | ========== 22 | 23 | This script upgrades a Palo Alto Networks firewall or Panorama to the 24 | specified version. It takes care of all intermediate upgrades and reboots. 25 | 26 | **Usage**:: 27 | 28 | upgrade.py [-h] [-v] [-q] [-n] hostname username password version 29 | 30 | **Examples**: 31 | 32 | Upgrade a firewall at 10.0.0.1 to PAN-OS 7.0.0:: 33 | 34 | $ python upgrade.py 10.0.0.1 admin password 7.0.0 35 | 36 | Upgrade a Panorama at 172.16.4.4 to the latest Panorama version:: 37 | 38 | $ python upgrade.py 172.16.4.4 admin password latest 39 | 40 | """ 41 | 42 | __author__ = "btorres-gil" 43 | 44 | import sys 45 | import os 46 | import argparse 47 | import logging 48 | 49 | curpath = os.path.dirname(os.path.abspath(__file__)) 50 | sys.path[:0] = [os.path.join(curpath, os.pardir)] 51 | 52 | from panos.base import PanDevice 53 | 54 | 55 | def main(): 56 | # Get command line arguments 57 | parser = argparse.ArgumentParser( 58 | description="Upgrade a Palo Alto Networks Firewall or Panorama to the specified version" 59 | ) 60 | parser.add_argument( 61 | "-v", "--verbose", action="count", help="Verbose (-vv for extra verbose)" 62 | ) 63 | parser.add_argument("-q", "--quiet", action="store_true", help="No output") 64 | parser.add_argument( 65 | "-n", 66 | "--dryrun", 67 | action="store_true", 68 | help="Print what would happen, but don't perform upgrades", 69 | ) 70 | # Palo Alto Networks related arguments 71 | fw_group = parser.add_argument_group("Palo Alto Networks Device") 72 | fw_group.add_argument("hostname", help="Hostname of Firewall or Panorama") 73 | fw_group.add_argument("username", help="Username for Firewall or Panorama") 74 | fw_group.add_argument("password", help="Password for Firewall or Panorama") 75 | fw_group.add_argument( 76 | "version", help="The target PAN-OS/Panorama version (eg. 7.0.0 or latest)" 77 | ) 78 | args = parser.parse_args() 79 | 80 | ### Set up logger 81 | # Logging Levels 82 | # WARNING is 30 83 | # INFO is 20 84 | # DEBUG is 10 85 | if args.verbose is None: 86 | args.verbose = 0 87 | if not args.quiet: 88 | logging_level = 20 - (args.verbose * 10) 89 | if logging_level <= logging.DEBUG: 90 | logging_format = "%(levelname)s:%(name)s:%(message)s" 91 | else: 92 | logging_format = "%(message)s" 93 | logging.basicConfig(format=logging_format, level=logging_level) 94 | 95 | # Connect to the device and determine its type (Firewall or Panorama). 96 | # This is important to know what version to upgrade to next. 97 | device = PanDevice.create_from_device( 98 | args.hostname, 99 | args.username, 100 | args.password, 101 | ) 102 | 103 | # Perform the upgrades in sequence with reboots between each upgrade 104 | device.software.upgrade_to_version(args.version, args.dryrun) 105 | 106 | 107 | # Call the main() function to begin the program if not 108 | # loaded as a module. 109 | if __name__ == "__main__": 110 | main() 111 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - develop 8 | - beta 9 | - alpha 10 | - '[0-9]+.x' 11 | - '[0-9]+.[0-9]+.x' 12 | pull_request: 13 | 14 | jobs: 15 | test: 16 | name: Test 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | matrix: 20 | os: [ubuntu-latest, macos-latest] 21 | python-version: ["3.8", "3.9"] 22 | steps: 23 | - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2 24 | 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | 30 | - name: Install Poetry 31 | uses: Gr1N/setup-poetry@15821dc8a61bc630db542ae4baf6a7c19a994844 # v8 32 | with: 33 | poetry-version: 1.7.1 34 | 35 | - name: Get poetry cache directory 36 | id: poetry-cache 37 | run: echo "dir=$(poetry config cache-dir)" >> $GITHUB_OUTPUT 38 | 39 | - name: Cache poetry dependencies 40 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 41 | with: 42 | path: ${{ steps.poetry-cache.outputs.dir }} 43 | key: 44 | ${{ runner.os }}-poetry-${{ matrix.python-version }}-${{ 45 | hashFiles('**/poetry.lock') }} 46 | restore-keys: | 47 | ${{ runner.os }}-poetry-${{ matrix.python-version }}- 48 | 49 | - name: Install dependencies 50 | run: poetry install 51 | 52 | - name: Test with pytest 53 | run: poetry run make test 54 | 55 | format: 56 | name: Check Code Format 57 | runs-on: ubuntu-latest 58 | steps: 59 | - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2 60 | 61 | - name: Set up Python 62 | uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 63 | with: 64 | python-version: 3.8 65 | 66 | - name: Install Poetry 67 | uses: Gr1N/setup-poetry@15821dc8a61bc630db542ae4baf6a7c19a994844 # v8 68 | with: 69 | poetry-version: 1.7.1 70 | 71 | - name: Get poetry cache directory 72 | id: poetry-cache 73 | run: echo "dir=$(poetry config cache-dir)" >> $GITHUB_OUTPUT 74 | 75 | - name: Cache poetry dependencies 76 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 77 | with: 78 | path: ${{ steps.poetry-cache.outputs.dir }} 79 | key: ${{ runner.os }}-poetry-3.8-${{ hashFiles('**/poetry.lock') }} 80 | restore-keys: | 81 | ${{ runner.os }}-poetry-3.8- 82 | 83 | - name: Install dependencies 84 | run: poetry install 85 | 86 | - name: Check formatting with black and isort 87 | run: poetry run make check-format 88 | 89 | release: 90 | name: Release 91 | if: github.event_name == 'push' && github.ref != 'refs/heads/develop' 92 | needs: [test, format] 93 | runs-on: ubuntu-latest 94 | steps: 95 | - name: Checkout 96 | uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 # v2 97 | 98 | - name: Set up Python 99 | uses: actions/setup-python@0f07f7f756721ebd886c2462646a35f78a8bc4de # v1 100 | with: 101 | python-version: 3.8 102 | 103 | - name: Install Poetry 104 | uses: Gr1N/setup-poetry@15821dc8a61bc630db542ae4baf6a7c19a994844 # v8 105 | with: 106 | poetry-version: 1.7.1 107 | 108 | - name: Get poetry cache directory 109 | id: poetry-cache 110 | run: echo "dir=$(poetry config cache-dir)" >> $GITHUB_OUTPUT 111 | 112 | - name: Cache poetry dependencies 113 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 114 | with: 115 | path: ${{ steps.poetry-cache.outputs.dir }} 116 | key: ${{ runner.os }}-poetry-3.8-${{ hashFiles('**/poetry.lock') }} 117 | restore-keys: | 118 | ${{ runner.os }}-poetry-3.8- 119 | 120 | - name: Install dependencies 121 | run: poetry install 122 | 123 | - name: Create release and publish 124 | id: release 125 | uses: cycjimmy/semantic-release-action@5982a02995853159735cb838992248c4f0f16166 # v2 126 | with: 127 | semantic_version: 17.1.1 128 | extra_plugins: | 129 | conventional-changelog-conventionalcommits@^4.4.0 130 | @semantic-release/git@^9.0.0 131 | @semantic-release/exec@^5.0.0 132 | env: 133 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 134 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} 135 | -------------------------------------------------------------------------------- /examples/userid.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2014, Palo Alto Networks 4 | # 5 | # Permission to use, copy, modify, and/or distribute this software for any 6 | # purpose with or without fee is hereby granted, provided that the above 7 | # copyright notice and this permission notice appear in all copies. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | 17 | # Author: Brian Torres-Gil 18 | 19 | """ 20 | userid.py 21 | ========= 22 | 23 | Update User-ID by adding or removing a user-to-ip mapping on the firewall 24 | 25 | **Usage**:: 26 | 27 | userid.py [-h] [-v] [-q] hostname username password action user ip 28 | 29 | **Examples**: 30 | 31 | Send a User-ID login event to a firewall at 10.0.0.1:: 32 | 33 | $ python userid.py 10.0.0.1 admin password login exampledomain/user1 4.4.4.4 34 | 35 | Send a User-ID logout event to a firewall at 172.16.4.4:: 36 | 37 | $ python userid.py 172.16.4.4 admin password logout user2 5.1.2.2 38 | 39 | """ 40 | 41 | __author__ = "btorres-gil" 42 | 43 | import sys 44 | import os 45 | import argparse 46 | import logging 47 | 48 | curpath = os.path.dirname(os.path.abspath(__file__)) 49 | sys.path[:0] = [os.path.join(curpath, os.pardir)] 50 | 51 | from panos.base import PanDevice 52 | from panos.panorama import Panorama 53 | 54 | 55 | def main(): 56 | # Get command line arguments 57 | parser = argparse.ArgumentParser( 58 | description="Update User-ID by adding or removing a user-to-ip mapping" 59 | ) 60 | parser.add_argument( 61 | "-v", "--verbose", action="count", help="Verbose (-vv for extra verbose)" 62 | ) 63 | parser.add_argument("-q", "--quiet", action="store_true", help="No output") 64 | # Palo Alto Networks related arguments 65 | fw_group = parser.add_argument_group("Palo Alto Networks Device") 66 | fw_group.add_argument("hostname", help="Hostname of Firewall") 67 | fw_group.add_argument("username", help="Username for Firewall") 68 | fw_group.add_argument("password", help="Password for Firewall") 69 | fw_group.add_argument( 70 | "action", help="The action of the user. Must be 'login' or 'logout'." 71 | ) 72 | fw_group.add_argument("user", help="The username of the user") 73 | fw_group.add_argument("ip", help="The IP address of the user") 74 | args = parser.parse_args() 75 | 76 | ### Set up logger 77 | # Logging Levels 78 | # WARNING is 30 79 | # INFO is 20 80 | # DEBUG is 10 81 | if args.verbose is None: 82 | args.verbose = 0 83 | if not args.quiet: 84 | logging_level = 20 - (args.verbose * 10) 85 | if logging_level <= logging.DEBUG: 86 | logging_format = "%(levelname)s:%(name)s:%(message)s" 87 | else: 88 | logging_format = "%(message)s" 89 | logging.basicConfig(format=logging_format, level=logging_level) 90 | 91 | # Connect to the device and determine its type (Firewall or Panorama). 92 | device = PanDevice.create_from_device( 93 | args.hostname, 94 | args.username, 95 | args.password, 96 | ) 97 | 98 | logging.debug("Detecting type of device") 99 | 100 | # Panorama does not have a userid API, so exit. 101 | # You can use the userid API on a firewall with the Panorama 'target' 102 | # parameter by creating a Panorama object first, then create a 103 | # Firewall object with the 'panorama' and 'serial' variables populated. 104 | if issubclass(type(device), Panorama): 105 | logging.error( 106 | "Connected to a Panorama, but user-id API is not possible on Panorama. Exiting." 107 | ) 108 | sys.exit(1) 109 | 110 | if args.action == "login": 111 | logging.debug("Login user %s at IP %s" % (args.user, args.ip)) 112 | device.userid.login(args.user, args.ip) 113 | elif args.action == "logout": 114 | logging.debug("Logout user %s at IP %s" % (args.user, args.ip)) 115 | device.userid.logout(args.user, args.ip) 116 | else: 117 | raise ValueError( 118 | "Unknown action: %s. Must be 'login' or 'logout'." % args.action 119 | ) 120 | 121 | logging.debug("Done") 122 | 123 | 124 | # Call the main() function to begin the program if not 125 | # loaded as a module. 126 | if __name__ == "__main__": 127 | main() 128 | -------------------------------------------------------------------------------- /tests/live/test_objects.py: -------------------------------------------------------------------------------- 1 | from panos import objects 2 | from tests.live import testlib 3 | 4 | 5 | class TestAddressObject(testlib.DevFlow): 6 | def setup_state_obj(self, dev, state): 7 | state.obj = objects.AddressObject( 8 | testlib.random_name(), 9 | value=testlib.random_ip(), 10 | type="ip-netmask", 11 | description="This is a test", 12 | ) 13 | dev.add(state.obj) 14 | 15 | def update_state_obj(self, dev, state): 16 | state.obj.type = "ip-range" 17 | state.obj.value = "10.1.1.1-10.1.1.240" 18 | 19 | 20 | class TestStaticAddressGroup(testlib.DevFlow): 21 | def create_dependencies(self, dev, state): 22 | state.aos = [ 23 | objects.AddressObject(testlib.random_name(), testlib.random_ip()) 24 | for x in range(4) 25 | ] 26 | for x in state.aos: 27 | dev.add(x) 28 | x.create() 29 | 30 | def setup_state_obj(self, dev, state): 31 | state.obj = objects.AddressGroup( 32 | testlib.random_name(), 33 | [x.name for x in state.aos[:2]], 34 | ) 35 | dev.add(state.obj) 36 | 37 | def update_state_obj(self, dev, state): 38 | state.obj.static_value = [x.name for x in state.aos[2:]] 39 | 40 | def cleanup_dependencies(self, dev, state): 41 | for x in state.aos: 42 | try: 43 | x.delete() 44 | except Exception: 45 | pass 46 | 47 | 48 | class TestDynamicAddressGroup(testlib.DevFlow): 49 | def create_dependencies(self, dev, state): 50 | state.tags = [ 51 | objects.Tag( 52 | testlib.random_name(), 53 | color="color{0}".format(x), 54 | comments=testlib.random_name(), 55 | ) 56 | for x in range(1, 5) 57 | ] 58 | for x in state.tags: 59 | dev.add(x) 60 | x.create() 61 | 62 | def setup_state_obj(self, dev, state): 63 | state.obj = objects.AddressGroup( 64 | testlib.random_name(), 65 | dynamic_value="'{0}' or '{1}'".format( 66 | state.tags[0].name, state.tags[1].name 67 | ), 68 | description="This is my description", 69 | tag=state.tags[2].name, 70 | ) 71 | dev.add(state.obj) 72 | 73 | def update_state_obj(self, dev, state): 74 | state.obj.dynamic_value = "'{0}' and '{1}'".format( 75 | state.tags[2].name, 76 | state.tags[3].name, 77 | ) 78 | state.obj.tag = state.tags[1].name 79 | 80 | def cleanup_dependencies(self, dev, state): 81 | for x in state.tags: 82 | try: 83 | x.delete() 84 | except Exception: 85 | pass 86 | 87 | 88 | class TestTag(testlib.DevFlow): 89 | def setup_state_obj(self, dev, state): 90 | state.obj = objects.Tag( 91 | testlib.random_name(), 92 | color="color1", 93 | comments="My new tag", 94 | ) 95 | dev.add(state.obj) 96 | 97 | def update_state_obj(self, dev, state): 98 | state.obj.color = "color5" 99 | state.obj.comments = testlib.random_name() 100 | 101 | 102 | class TestServiceObject(testlib.DevFlow): 103 | def setup_state_obj(self, dev, state): 104 | state.obj = objects.ServiceObject( 105 | testlib.random_name(), 106 | protocol="tcp", 107 | source_port="1025-65535", 108 | destination_port="80,443,8080", 109 | description="My service object", 110 | ) 111 | dev.add(state.obj) 112 | 113 | def update_state_obj(self, dev, state): 114 | state.obj.protocol = "udp" 115 | state.obj.source_port = "12345" 116 | 117 | 118 | class TestServiceGroup(testlib.DevFlow): 119 | def create_dependencies(self, dev, state): 120 | state.tag = None 121 | state.services = [ 122 | objects.ServiceObject( 123 | testlib.random_name(), 124 | "tcp" if x % 2 == 0 else "udp", 125 | destination_port=2000 + x, 126 | description="Service {0}".format(x), 127 | ) 128 | for x in range(4) 129 | ] 130 | for x in state.services: 131 | dev.add(x) 132 | x.create() 133 | state.tag = objects.Tag(testlib.random_name(), "color5") 134 | dev.add(state.tag) 135 | state.tag.create() 136 | 137 | def setup_state_obj(self, dev, state): 138 | state.obj = objects.ServiceGroup( 139 | testlib.random_name(), 140 | [x.name for x in state.services[:2]], 141 | tag=state.tag.name, 142 | ) 143 | dev.add(state.obj) 144 | 145 | def update_state_obj(self, dev, state): 146 | state.obj.value = [x.name for x in state.services[2:]] 147 | 148 | def cleanup_dependencies(self, dev, state): 149 | for x in state.services: 150 | try: 151 | x.delete() 152 | except Exception: 153 | pass 154 | 155 | if state.tag is not None: 156 | try: 157 | state.tag.delete() 158 | except Exception: 159 | pass 160 | 161 | 162 | # ApplicationObject 163 | # ApplicationGroup 164 | # ApplicationFilter 165 | # ApplicationContainer 166 | -------------------------------------------------------------------------------- /examples/prisma_access_create_remote_network.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2022, Palo Alto Networks 4 | # 5 | # Permission to use, copy, modify, and/or distribute this software for any 6 | # purpose with or without fee is hereby granted, provided that the above 7 | # copyright notice and this permission notice appear in all copies. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | 17 | # Author: Bastien Migette 18 | 19 | """ 20 | prisma_access_create_remote_network.py 21 | ========== 22 | 23 | This script is an example on how to create a prisma access Remote Network, 24 | along with needed IPSEC Tunnel and IKEv2 Gateway. 25 | To use the script, you need to replace the variables below with desired values. 26 | 27 | """ 28 | import logging 29 | import os 30 | import sys 31 | 32 | # This is needed to import module from parent folder 33 | curpath = os.path.dirname(os.path.abspath(__file__)) 34 | sys.path[:0] = [os.path.join(curpath, os.pardir)] 35 | 36 | 37 | from panos.network import IkeGateway, IpsecTunnel 38 | from panos.panorama import Panorama, Template 39 | from panos.plugins import ( 40 | AggBandwidth, 41 | Bgp, 42 | CloudServicesPlugin, 43 | RemoteNetwork, 44 | RemoteNetworks, 45 | ) 46 | 47 | __author__ = "bmigette" 48 | 49 | 50 | HOSTNAME = os.environ["PAN_HOSTNAME"] 51 | USERNAME = os.environ["PAN_USERNAME"] 52 | PASSWORD = os.environ["PAN_PASSWORD"] 53 | 54 | IPSEC_PEER = "1.2.3.4" 55 | BGP_PEER = "1.2.3.4" 56 | BGP_PEER_AS = 65123 57 | IPSEC_TUNNEL_NAME = "panos-sdk-tunnel" 58 | IKE_GW = "panos-sdk-ikev2-gw" 59 | IKE_PSK = "Secret123" 60 | IKE_CRYPTO = "Generic-IKE-Crypto-Default" 61 | IPSEC_CRYPTO = "Generic-IPSEC-Crypto-Default" 62 | TEMPLATE = "Remote_Network_Template" 63 | 64 | REMOTE_NETWORK_NAME = "panos-sdk-rn" 65 | # This is the Region that you put in the RN. A compute region can have multiple Regions 66 | REMOTE_NETWORK_REGION = "eu-central-1" 67 | # This is the Compute Region, used to get SPN list. You can use Panorama CLI to get available options 68 | REMOTE_NETWORK_COMPUTEREGION = "europe-central" 69 | 70 | 71 | def get_region_spn(remote_networks, region): 72 | """This function will return first SPN from a given region name. 73 | You should implement some logic here to get the correct SPN. 74 | The script will break if the region has no SPN / BW allocated 75 | 76 | Args: 77 | remote_networks (RemoteNetworks): RemoteNetworks Object 78 | region (str): The region to get SPN from 79 | 80 | Returns: 81 | str: spn name 82 | """ 83 | agg_bw = remote_networks.findall(AggBandwidth) 84 | region_obj = agg_bw[0].find(region) 85 | print(f"SPN for region {region}: {region_obj.spn_name_list[0]}") 86 | return region_obj.spn_name_list[0] 87 | 88 | 89 | def main(): 90 | # Setting logging to debug the PanOS SDK 91 | logging_format = "%(levelname)s:%(name)s:%(message)s" 92 | # logging.basicConfig(format=logging_format, level=logging.DEBUG - 2) # Use this to be even more verbose 93 | logging.basicConfig(format=logging_format, level=logging.DEBUG) 94 | # 1 - let's create the panorama object that we want to modify. 95 | pan = Panorama(HOSTNAME, USERNAME, PASSWORD) 96 | 97 | # 2 - Refreshing Prisma Access config 98 | csp = pan.add(CloudServicesPlugin()) 99 | csp.refresh() 100 | 101 | rn_template = pan.add(Template(name=TEMPLATE)) 102 | rn_template.refresh() 103 | # 3 - Getting the remote_networks object 104 | remote_networks = csp.findall(RemoteNetworks)[0] 105 | 106 | # 4 - Creating IKEv2 GW and IPSEC Tunnels 107 | # 4.1 - IKEv2 GW 108 | gw = IkeGateway( 109 | name=IKE_GW, 110 | version="ikev2", 111 | peer_ip_type="ip", 112 | peer_ip_value=IPSEC_PEER, 113 | peer_id_type="ipaddr", 114 | peer_id_value=IPSEC_PEER, 115 | auth_type="pre-shared-key", 116 | pre_shared_key=IKE_PSK, 117 | ikev2_crypto_profile=IKE_CRYPTO, 118 | enable_liveness_check=True, 119 | ) 120 | rn_template.add(gw).create() 121 | 122 | # 4.2 - IPSEC Tunnel 123 | ipsec_tun = IpsecTunnel( 124 | name=IPSEC_TUNNEL_NAME, 125 | ak_ike_gateway=IKE_GW, 126 | ak_ipsec_crypto_profile=IPSEC_CRYPTO, 127 | mk_remote_address=IPSEC_PEER, 128 | ) 129 | 130 | rn_template.add(ipsec_tun).create() 131 | 132 | # 5 - Creating Remote Network 133 | rn = RemoteNetwork( 134 | name=REMOTE_NETWORK_NAME, 135 | subnets=["10.11.12.0/24"], 136 | region=REMOTE_NETWORK_REGION, 137 | spn_name=get_region_spn(remote_networks, REMOTE_NETWORK_COMPUTEREGION), 138 | ipsec_tunnel=IPSEC_TUNNEL_NAME, 139 | ) 140 | bgp = Bgp(enable=True, peer_as=BGP_PEER_AS, peer_ip_address=BGP_PEER) 141 | 142 | rn.add(bgp) 143 | remote_networks.add(rn).create() 144 | # 6 - Commit + Push. r will be jobid 145 | # r = pan.commit_all(devicegroup="Remote_Network_Device_Group") # commit + push. 146 | r = pan.commit() # commit only 147 | print(f"commit job id: {r}") 148 | 149 | 150 | if __name__ == "__main__": 151 | main() 152 | -------------------------------------------------------------------------------- /examples/dyn_address_group.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2014, Palo Alto Networks 4 | # 5 | # Permission to use, copy, modify, and/or distribute this software for any 6 | # purpose with or without fee is hereby granted, provided that the above 7 | # copyright notice and this permission notice appear in all copies. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | 17 | # Author: Brian Torres-Gil 18 | 19 | """ 20 | dyn_address_group.py 21 | ==================== 22 | 23 | Tag/untag ip addresses for Dynamic Address Groups on a firewall 24 | 25 | **Usage**:: 26 | 27 | dyn_address_group.py [-h] [-v] [-q] [-u] [-c] hostname username password ip tags 28 | 29 | **Examples**: 30 | 31 | Tag the IP 3.3.3.3 with the tag 'linux' and 'apache':: 32 | 33 | $ python dyn_address_group.py -r linux,apache 10.0.0.1 admin password 3.3.3.3 34 | 35 | Remove the tag apache from the IP 3.3.3.3:: 36 | 37 | $ python dyn_address_group.py -u linux 10.0.0.1 admin password 3.3.3.3 38 | 39 | Clear all tags from all IP's in vsys2:: 40 | 41 | $ python dyn_address_group_vsys.py -s vsys2 -c 10.0.0.1 admin password notused notused 42 | 43 | """ 44 | 45 | __author__ = "btorres-gil" 46 | 47 | import sys 48 | import os 49 | import argparse 50 | import logging 51 | 52 | curpath = os.path.dirname(os.path.abspath(__file__)) 53 | sys.path[:0] = [os.path.join(curpath, os.pardir)] 54 | 55 | from panos.base import PanDevice 56 | from panos.panorama import Panorama 57 | 58 | 59 | def main(): 60 | # Get command line arguments 61 | parser = argparse.ArgumentParser( 62 | description="Tag an IP address on a Palo Alto Networks Next generation Firewall" 63 | ) 64 | parser.add_argument( 65 | "-v", "--verbose", action="count", help="Verbose (-vv for extra verbose)" 66 | ) 67 | parser.add_argument("-q", "--quiet", action="store_true", help="No output") 68 | parser.add_argument( 69 | "-r", 70 | "--register", 71 | help="Tags to register to an IP, for multiple tags use commas eg. linux,apache,server", 72 | ) 73 | parser.add_argument( 74 | "-u", 75 | "--unregister", 76 | help="Tags to remove from an an IP, for multiple tags use commas eg. linux,apache,server", 77 | ) 78 | parser.add_argument( 79 | "-s", 80 | "--vsys", 81 | help="Specify the vsys target in the form vsysN where N is the vsys number: vsys2, vsys4, etc.", 82 | ) 83 | parser.add_argument( 84 | "-l", "--list", action="store_true", help="List all tags for an IP" 85 | ) 86 | parser.add_argument( 87 | "-c", "--clear", action="store_true", help="Clear all tags for all IP" 88 | ) 89 | # Palo Alto Networks related arguments 90 | fw_group = parser.add_argument_group("Palo Alto Networks Device") 91 | fw_group.add_argument("hostname", help="Hostname of Firewall") 92 | fw_group.add_argument("username", help="Username for Firewall") 93 | fw_group.add_argument("password", help="Password for Firewall") 94 | fw_group.add_argument("ip", help="The IP address to tag/untag/list") 95 | args = parser.parse_args() 96 | 97 | ### Set up logger 98 | # Logging Levels 99 | # WARNING is 30 100 | # INFO is 20 101 | # DEBUG is 10 102 | if args.verbose is None: 103 | args.verbose = 0 104 | if not args.quiet: 105 | logging_level = 20 - (args.verbose * 10) 106 | if logging_level <= logging.DEBUG: 107 | logging_format = "%(levelname)s:%(name)s:%(message)s" 108 | else: 109 | logging_format = "%(message)s" 110 | logging.basicConfig(format=logging_format, level=logging_level) 111 | 112 | # Connect to the device and determine its type (Firewall or Panorama). 113 | device = PanDevice.create_from_device( 114 | args.hostname, 115 | args.username, 116 | args.password, 117 | ) 118 | 119 | # Panorama does not have a userid API, so exit. 120 | # You can use the userid API on a firewall with the Panorama 'target' 121 | # parameter by creating a Panorama object first, then create a 122 | # Firewall object with the 'panorama' and 'serial' variables populated. 123 | if issubclass(type(device), Panorama): 124 | logging.error( 125 | "Connected to a Panorama, but user-id API is not possible on Panorama. Exiting." 126 | ) 127 | sys.exit(1) 128 | 129 | if args.vsys is not None: 130 | device.vsys = args.vsys 131 | 132 | if args.clear: 133 | device.userid.clear_registered_ip() 134 | 135 | if args.list: 136 | all_tags_by_ip = device.userid.get_registered_ip() 137 | try: 138 | # Print the tags for the requested IP 139 | logging.info(all_tags_by_ip[args.ip]) 140 | except KeyError: 141 | # There were no tags for that IP 142 | logging.info("No tags for IP: %s" % args.ip) 143 | 144 | if args.unregister: 145 | device.userid.unregister(args.ip, args.unregister.split(",")) 146 | 147 | if args.register: 148 | device.userid.register(args.ip, args.register.split(",")) 149 | 150 | 151 | # Call the main() function to begin the program if not 152 | # loaded as a module. 153 | if __name__ == "__main__": 154 | main() 155 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Palo Alto Networks PAN-OS SDK for Python 2 | ======================================== 3 | 4 | The PAN-OS SDK for Python (pan-os-python) is a package to help interact with 5 | Palo Alto Networks devices (including physical and virtualized Next-generation 6 | Firewalls and Panorama). The pan-os-python SDK is object oriented and mimics 7 | the traditional interaction with the device via the GUI or CLI/API. 8 | 9 | * Documentation: http://pan-os-python.readthedocs.io 10 | 11 | ----- 12 | 13 | [![Latest version released on PyPi](https://img.shields.io/pypi/v/pan-os-python.svg)](https://pypi.python.org/pypi/pan-os-python) 14 | [![Python versions](https://img.shields.io/badge/python-3.5%20%7C%203.6%20%7C%203.7%20%7C%203.8-blueviolet)](https://pypi.python.org/pypi/pan-os-python) 15 | [![License](https://img.shields.io/pypi/l/pan-os-python)](https://github.com/PaloAltoNetworks/pan-os-python/blob/develop/LICENSE) 16 | [![Documentation Status](https://img.shields.io/badge/docs-latest-brightgreen.svg)](http://pan-os-python.readthedocs.io/en/latest/?badge=latest) 17 | [![Chat on GitHub Discussions](https://img.shields.io/badge/chat%20on-GitHub%20Discussions-brightgreen)](https://github.com/PaloAltoNetworks/pan-os-python/discussions) 18 | 19 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 20 | [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org/) 21 | [![Powered by DepHell](https://img.shields.io/badge/Powered%20by-DepHell-red)](https://github.com/dephell/dephell) 22 | [![GitHub contributors](https://img.shields.io/github/contributors/PaloAltoNetworks/pan-os-python)](https://github.com/PaloAltoNetworks/pan-os-python/graphs/contributors/) 23 | 24 | ----- 25 | 26 | Features 27 | -------- 28 | 29 | - Object model of Firewall and Panorama configuration 30 | - Multiple connection methods including Panorama as a proxy 31 | - All operations natively vsys-aware 32 | - Support for high availability pairs and retry/recovery during node failure 33 | - Batch User-ID operations 34 | - Device API exception classification 35 | 36 | Status 37 | ------ 38 | 39 | Palo Alto Networks PAN-OS SDK for Python is considered stable. It is fully tested 40 | and used in many production environments. Semantic versioning is applied to indicate 41 | bug fixes, new features, and breaking changes in each version. 42 | 43 | Install 44 | ------- 45 | 46 | Install using pip: 47 | 48 | ```shell 49 | pip install pan-os-python 50 | ``` 51 | 52 | Upgrade to the latest version: 53 | 54 | ```shell 55 | pip install --upgrade pan-os-python 56 | ``` 57 | 58 | If you have poetry installed, you can also add pan-os-python to your project: 59 | 60 | ```shell 61 | poetry add pan-os-python 62 | ``` 63 | 64 | How to import 65 | ------------- 66 | 67 | To use pan-os-python in a project: 68 | 69 | ```python 70 | import panos 71 | ``` 72 | 73 | You can also be more specific about which modules you want to import: 74 | 75 | ```python 76 | from panos import firewall 77 | from panos import network 78 | ``` 79 | 80 | 81 | A few examples 82 | -------------- 83 | 84 | For configuration tasks, create a tree structure using the classes in 85 | each module. Nodes hierarchy must follow the model in the 86 | [Configuration Tree](http://pan-os-python.readthedocs.io/en/latest/configtree.html). 87 | 88 | The following examples assume the modules were imported as such: 89 | 90 | ```python 91 | from panos import firewall 92 | from panos import network 93 | ``` 94 | 95 | Create an interface and commit: 96 | 97 | ```python 98 | fw = firewall.Firewall("10.0.0.1", api_username="admin", api_password="admin") 99 | eth1 = network.EthernetInterface("ethernet1/1", mode="layer3") 100 | fw.add(eth1) 101 | eth1.create() 102 | fw.commit() 103 | ``` 104 | 105 | Operational commands leverage the 'op' method of the device: 106 | 107 | ```python 108 | fw = firewall.Firewall("10.0.0.1", api_username="admin", api_password="admin") 109 | print fw.op("show system info") 110 | ``` 111 | 112 | Some operational commands have methods to refresh the variables in an object: 113 | 114 | ```python 115 | # populates the version, serial, and model variables from the live device 116 | fw.refresh_system_info() 117 | ``` 118 | 119 | See more examples in the [Usage Guide](http://pan-os-python.readthedocs.io/en/latest/usage.html). 120 | 121 | Upgrade from pandevice 122 | ---------------------- 123 | 124 | This `pan-os-python` package is the evolution of the older `pandevice` package. To 125 | upgrade from `pandevice` to `pan-os-python`, follow these steps. 126 | 127 | Step 1. Ensure you are using python3 128 | 129 | [Python2 is end-of-life](https://www.python.org/doc/sunset-python-2/) and not 130 | supported by `pan-os-python`. 131 | 132 | Step 2. Uninstall pandevice: 133 | 134 | ```shell 135 | pip uninstall pandevice 136 | # or 137 | poetry remove pandevice 138 | ``` 139 | 140 | Step 3. Install pan-os-python: 141 | 142 | ```shell 143 | pip3 install pan-os-python 144 | # or 145 | poetry add pan-os-python 146 | ``` 147 | 148 | Step 4. Change the import statements in your code from `pandevice` to `panos`. For example: 149 | 150 | ```python 151 | import pandevice 152 | from pandevice.firewall import Firewall 153 | 154 | # would change to 155 | 156 | import panos 157 | from panos.firewall import Firewall 158 | ``` 159 | 160 | Step 5. Test your script or application 161 | 162 | There are no known breaking changes 163 | between `pandevice v0.14.0` and `pan-os-python v1.0.0`, but it is a major 164 | upgrade so please verify everything works as expected. 165 | 166 | Contributors 167 | ------------ 168 | 169 | - Brian Torres-Gil - [btorresgil](https://github.com/btorresgil) 170 | - Garfield Freeman - [shinmog](https://github.com/shinmog) 171 | - John Anderson - [lampwins](https://github.com/lampwins) 172 | - Aditya Sripal - [AdityaSripal](https://github.com/AdityaSripal) 173 | 174 | Thank you to [Kevin Steves](https://github.com/kevinsteves), creator of the [pan-python library](https://github.com/kevinsteves/pan-python) 175 | -------------------------------------------------------------------------------- /tests/live/test_userid.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import time 3 | 4 | from tests.live import testlib 5 | from panos import base 6 | 7 | 8 | class TestUserID_FW(object): 9 | """Tests UserID on live Firewall.""" 10 | 11 | def test_01_fw_login(self, fw, state_map): 12 | state = state_map.setdefault(fw) 13 | user, ip = testlib.random_name(), testlib.random_ip() 14 | fw.userid.login(user, ip) 15 | state.single_user = [user, ip] 16 | 17 | def test_02_fw_logins(self, fw, state_map): 18 | state = state_map.setdefault(fw) 19 | users = [(testlib.random_name(), testlib.random_ip()) for i in range(10)] 20 | fw.userid.logins(users) 21 | state.multi_user = users 22 | 23 | def test_03_fw_logout(self, fw, state_map): 24 | state = state_map.setdefault(fw) 25 | if not state.single_user: 26 | raise Exception("User not logged in yet") 27 | user, ip = state.single_user 28 | fw.userid.logout(user, ip) 29 | 30 | def test_04_fw_logouts(self, fw, state_map): 31 | state = state_map.setdefault(fw) 32 | if not state.multi_user: 33 | raise Exception("User not logged in yet") 34 | fw.userid.logouts(state.multi_user) 35 | 36 | def test_05_register_str(self, fw, state_map): 37 | state = state_map.setdefault(fw) 38 | ip, tag = testlib.random_ip(), testlib.random_name() 39 | fw.userid.register(ip, tag) 40 | state.single_register = [ip, tag] 41 | 42 | def test_06_unregister_str(self, fw, state_map): 43 | state = state_map.setdefault(fw) 44 | if not state.single_register: 45 | raise Exception("No single_register") 46 | ip, tag = state.single_register 47 | fw.userid.unregister(ip, tag) 48 | 49 | def test_07_register_lst(self, fw, state_map): 50 | state = state_map.setdefault(fw) 51 | ips = [testlib.random_ip() for x in range(10)] 52 | tags = [testlib.random_name() for i in range(15)] 53 | fw.userid.register(ips, tags) 54 | state.multi_register_01 = [ips, tags] 55 | 56 | def test_08_get_registered_ip(self, fw, state_map): 57 | state = state_map.setdefault(fw) 58 | if not state.multi_register_01: 59 | raise Exception("Multi register not set") 60 | ips, tags = state.multi_register_01 61 | test1 = set(fw.userid.get_registered_ip()) 62 | assert test1 == set(ips) 63 | test2 = set(fw.userid.get_registered_ip(ips[0:3], tags)) 64 | assert test2 == set(ips[0:3]) 65 | test3 = set(fw.userid.get_registered_ip(ips[0:3], tags[0:5])) 66 | assert test3 == set(ips[0:3]) 67 | test4 = set(fw.userid.get_registered_ip(ips, tags[0:5])) 68 | assert test4 == set(ips) 69 | test5 = set(fw.userid.get_registered_ip(ips[0], tags[0])) 70 | assert test5 == set( 71 | [ 72 | ips[0], 73 | ] 74 | ) 75 | tests = [test1, test2, test3, test4, test5] 76 | assert len(test5) != 0 77 | assert all([test1 >= x for x in tests]) 78 | assert all([x >= test5 for x in tests]) 79 | assert test2 >= test3 80 | assert test4 >= test3 81 | 82 | def test_09_audit_registered_ip(self, fw, state_map): 83 | state = state_map.setdefault(fw) 84 | original = set(fw.userid.get_registered_ip()) 85 | new_ips = [testlib.random_ip() for x in range(5)] 86 | new_tags = [testlib.random_name() for i in range(8)] 87 | ip_tags_pairs = dict([(ip, tuple(new_tags)) for ip in new_ips]) 88 | fw.userid.audit_registered_ip(ip_tags_pairs) 89 | state.multi_register_02 = [new_ips, new_tags] 90 | new_set = set(fw.userid.get_registered_ip()) 91 | assert len(new_set) < len(original) 92 | assert new_set == set(new_ips) 93 | 94 | def test_10_clear_registered_ip(self, fw, state_map): 95 | state = state_map.setdefault(fw) 96 | if not state.multi_register_02: 97 | raise Exception("Multi register not set") 98 | ips, tags = state.multi_register_02 99 | original = list(fw.userid.get_registered_ip()) 100 | fw.userid.clear_registered_ip(ips[0], tags[0]) 101 | mod1 = list(fw.userid.get_registered_ip()) 102 | fw.userid.clear_registered_ip(ips[0:4], tags[0:5]) 103 | mod2 = list(fw.userid.get_registered_ip()) 104 | fw.userid.clear_registered_ip(ips[0:4], tags) 105 | mod3 = list(fw.userid.get_registered_ip()) 106 | fw.userid.clear_registered_ip(ips, tags[0:7]) 107 | mod4 = list(fw.userid.get_registered_ip()) 108 | fw.userid.clear_registered_ip() 109 | mod5 = list(fw.userid.get_registered_ip()) 110 | assert len(mod3) < len(mod2) 111 | assert len(mod3) < len(mod1) 112 | assert len(mod3) < len(original) 113 | assert len(mod5) == 0 114 | 115 | def test_11_batch(self, fw, state_map): 116 | fw.userid.clear_registered_ip() # Fresh start 117 | fw.userid.batch_start() 118 | users = [(testlib.random_name(), testlib.random_ip()) for i in range(5)] 119 | fw.userid.logins(users) 120 | ips = [testlib.random_ip() for x in range(5)] 121 | tags = [testlib.random_name() for y in range(5)] 122 | fw.userid.register(ips, tags) 123 | fw.userid.unregister(ips[2], tags[4]) 124 | fw.userid.get_registered_ip(ips[0:3], tags[2:4]) 125 | new_ips = [testlib.random_ip() for x in range(3)] 126 | new_tags = [testlib.random_name() for y in range(3)] 127 | fw.userid.audit_registered_ip(dict([(ip, tuple(new_tags)) for ip in new_ips])) 128 | fw.userid.get_registered_ip() 129 | fw.userid.unregister(new_ips, new_tags) 130 | fw.userid.batch_end() 131 | 132 | def test_12_uidmessage(self, fw, state_map): 133 | state = state_map.setdefault(fw) 134 | state.uid = fw.userid._create_uidmessage() 135 | 136 | def test_13_send(self, fw, state_map): 137 | state = state_map.setdefault(fw) 138 | if not state.uid: 139 | raise Exception("No UID") 140 | fw.userid.send( 141 | state.uid[0] 142 | ) # State.uid returns length-two tuple of XML elements 143 | -------------------------------------------------------------------------------- /docs/configtree.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2016, Palo Alto Networks 4 | # 5 | # Permission to use, copy, modify, and/or distribute this software for any 6 | # purpose with or without fee is hereby granted, provided that the above 7 | # copyright notice and this permission notice appear in all copies. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | 17 | # Author: Brian Torres-Gil 18 | 19 | """Generate class diagram from module and class source code""" 20 | 21 | import errno 22 | import inspect 23 | import os 24 | import pkgutil 25 | import sys 26 | 27 | header = """digraph configtree { 28 | graph [rankdir=LR, fontsize=10, margin=0.001]; 29 | node [shape=box, fontsize=10, height=0.001, margin=0.1, ordering=out];""" 30 | 31 | 32 | footer = "}\n" 33 | 34 | 35 | nodestyle = { 36 | # 'Firewall': '', 37 | # 'Panorama': '', 38 | "device": "fillcolor=lightpink", 39 | "firewall": "fillcolor=lightblue", 40 | "ha": "fillcolor=lavender", 41 | "network": "fillcolor=lightcyan", 42 | "objects": "fillcolor=lemonchiffon", 43 | "policies": "fillcolor=lightsalmon", 44 | "panorama": "fillcolor=darkseagreen2", 45 | "plugins": "fillcolor=wheat", 46 | } 47 | 48 | 49 | def mkdir_p(path): 50 | """Make a full directory path""" 51 | try: 52 | os.makedirs(path) 53 | except OSError as exc: # Python >2.5 54 | if exc.errno == errno.EEXIST and os.path.isdir(path): 55 | pass 56 | else: 57 | raise 58 | 59 | 60 | def node_style(cls): 61 | cls = str(cls) 62 | style = "" 63 | if "." in cls: 64 | module = cls.split(".")[0] 65 | cls_name = cls.split(".")[-1] 66 | try: 67 | style = "style=filled " + nodestyle[cls_name] + " " 68 | except KeyError: 69 | try: 70 | style = "style=filled " + nodestyle[module] + " " 71 | except: 72 | pass 73 | result = ( 74 | ' {0} [{1}URL="../module-{2}.html#panos.{3}" target="_top"];\n'.format( 75 | cls_name, style, module, cls 76 | ) 77 | ) 78 | else: 79 | if style: 80 | result = " {0} [{1}]\n".format(style) 81 | else: 82 | result = "" 83 | return result 84 | 85 | 86 | def legend(modules): 87 | result = [] 88 | result.append("graph configtree {\n") 89 | result.append(" graph [fontsize=10, margin=0.001];\n") 90 | result.append( 91 | " node [shape=box, fontsize=10, height=0.001, margin=0.1, ordering=out];\n" 92 | ) 93 | for module in modules: 94 | module_name = module.__name__.split(".")[-1] 95 | try: 96 | result.append( 97 | " {0} [style=filled {1}]\n".format( 98 | module_name, nodestyle[module_name] 99 | ) 100 | ) 101 | except KeyError: 102 | pass 103 | # result.append(" PanDevice [style=filled]\n") 104 | result.append("}\n") 105 | return result 106 | 107 | 108 | def create_object_diagram(directory=None): 109 | # Set paths to package and modules 110 | curdir = os.path.dirname(os.path.abspath(__file__)) 111 | rootpath = [os.path.join(curdir, os.pardir)] 112 | libpath = [os.path.join(curdir, os.pardir, "panos")] 113 | sys.path[:0] = rootpath 114 | sys.path[:0] = libpath 115 | # print "Looking for panos in path: %s" % libpath 116 | 117 | # Import all modules in package 118 | modules = [] 119 | for importer, modname, ispkg in pkgutil.iter_modules(path=libpath, prefix="panos."): 120 | modules.append(__import__(modname, fromlist="dummy")) 121 | 122 | output = {} 123 | 124 | output["legend"] = legend(modules) 125 | 126 | # Gather a list of all classes in all modules 127 | for module in modules: 128 | module_name = module.__name__ 129 | output[module_name] = [] 130 | classes_seen = [] 131 | for class_name, cls in inspect.getmembers(module, inspect.isclass): 132 | if hasattr(cls, "CHILDTYPES") and getattr(cls, "CHILDTYPES"): 133 | full_class_name = "{0}.{1}".format( 134 | module_name.split(".")[-1], class_name 135 | ) 136 | if full_class_name not in classes_seen: 137 | classes_seen.append(full_class_name) 138 | output[module_name].append(node_style(full_class_name)) 139 | children = list(getattr(cls, "CHILDTYPES")) 140 | children.sort() 141 | for child in children: 142 | child_module = child.split(".")[0] 143 | child_name = child.split(".")[-1] 144 | # if child_name == "IPv6Address": 145 | # continue 146 | if child not in classes_seen: 147 | classes_seen.append(child) 148 | output[module_name].append(node_style(child)) 149 | output[module_name].append( 150 | " {0} -> {1};\n".format(class_name, child_name) 151 | ) 152 | 153 | # Write output to file or stdout 154 | path = "" 155 | if directory is not None: 156 | mkdir_p(directory) 157 | path = directory + "/" 158 | for module, lines in output.items(): 159 | if not lines: 160 | continue 161 | moduleout = "".join(lines) 162 | if module == "legend": 163 | fulloutput = moduleout 164 | else: 165 | fulloutput = header + moduleout + footer 166 | with open("{0}{1}.dot".format(path, module), "w") as file: 167 | file.write(fulloutput) 168 | 169 | 170 | if __name__ == "__main__": 171 | create_object_diagram() 172 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | Palo Alto Networks PAN-OS SDK for Python 3 | ======================================== 4 | 5 | The PAN-OS SDK for Python (pan-os-python) is a package to help interact with 6 | Palo Alto Networks devices (including physical and virtualized Next-generation 7 | Firewalls and Panorama). The pan-os-python SDK is object oriented and mimics 8 | the traditional interaction with the device via the GUI or CLI/API. 9 | 10 | 11 | * Documentation: http://pan-os-python.readthedocs.io 12 | 13 | ---- 14 | 15 | 16 | .. image:: https://img.shields.io/pypi/v/pan-os-python.svg 17 | :target: https://pypi.python.org/pypi/pan-os-python 18 | :alt: Latest version released on PyPi 19 | 20 | 21 | .. image:: https://img.shields.io/badge/python-3.5%20%7C%203.6%20%7C%203.7%20%7C%203.8-blueviolet 22 | :target: https://pypi.python.org/pypi/pan-os-python 23 | :alt: Python versions 24 | 25 | 26 | .. image:: https://img.shields.io/pypi/l/pan-os-python 27 | :target: https://github.com/PaloAltoNetworks/pan-os-python/blob/develop/LICENSE 28 | :alt: License 29 | 30 | 31 | .. image:: https://img.shields.io/badge/docs-latest-brightgreen.svg 32 | :target: http://pan-os-python.readthedocs.io/en/latest/?badge=latest 33 | :alt: Documentation Status 34 | 35 | 36 | .. image:: https://img.shields.io/badge/chat%20on-GitHub%20Discussions-brightgreen 37 | :target: https://github.com/PaloAltoNetworks/pan-os-python/discussions 38 | :alt: Chat on GitHub Discussions 39 | 40 | 41 | 42 | .. image:: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg 43 | :target: https://github.com/semantic-release/semantic-release 44 | :alt: semantic-release 45 | 46 | 47 | .. image:: https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg 48 | :target: https://conventionalcommits.org/ 49 | :alt: Conventional Commits 50 | 51 | 52 | .. image:: https://img.shields.io/badge/Powered%20by-DepHell-red 53 | :target: https://github.com/dephell/dephell 54 | :alt: Powered by DepHell 55 | 56 | 57 | .. image:: https://img.shields.io/github/contributors/PaloAltoNetworks/pan-os-python 58 | :target: https://github.com/PaloAltoNetworks/pan-os-python/graphs/contributors/ 59 | :alt: GitHub contributors 60 | 61 | 62 | ---- 63 | 64 | Features 65 | -------- 66 | 67 | 68 | * Object model of Firewall and Panorama configuration 69 | * Multiple connection methods including Panorama as a proxy 70 | * All operations natively vsys-aware 71 | * Support for high availability pairs and retry/recovery during node failure 72 | * Batch User-ID operations 73 | * Device API exception classification 74 | 75 | Status 76 | ------ 77 | 78 | Palo Alto Networks PAN-OS SDK for Python is considered stable. It is fully tested 79 | and used in many production environments. Semantic versioning is applied to indicate 80 | bug fixes, new features, and breaking changes in each version. 81 | 82 | Install 83 | ------- 84 | 85 | Install using pip: 86 | 87 | .. code-block:: shell 88 | 89 | pip install pan-os-python 90 | 91 | Upgrade to the latest version: 92 | 93 | .. code-block:: shell 94 | 95 | pip install --upgrade pan-os-python 96 | 97 | If you have poetry installed, you can also add pan-os-python to your project: 98 | 99 | .. code-block:: shell 100 | 101 | poetry add pan-os-python 102 | 103 | How to import 104 | ------------- 105 | 106 | To use pan-os-python in a project: 107 | 108 | .. code-block:: python 109 | 110 | import panos 111 | 112 | You can also be more specific about which modules you want to import: 113 | 114 | .. code-block:: python 115 | 116 | from panos import firewall 117 | from panos import network 118 | 119 | A few examples 120 | -------------- 121 | 122 | For configuration tasks, create a tree structure using the classes in 123 | each module. Nodes hierarchy must follow the model in the 124 | `Configuration Tree `_. 125 | 126 | The following examples assume the modules were imported as such: 127 | 128 | .. code-block:: python 129 | 130 | from panos import firewall 131 | from panos import network 132 | 133 | Create an interface and commit: 134 | 135 | .. code-block:: python 136 | 137 | fw = firewall.Firewall("10.0.0.1", api_username="admin", api_password="admin") 138 | eth1 = network.EthernetInterface("ethernet1/1", mode="layer3") 139 | fw.add(eth1) 140 | eth1.create() 141 | fw.commit() 142 | 143 | Operational commands leverage the 'op' method of the device: 144 | 145 | .. code-block:: python 146 | 147 | fw = firewall.Firewall("10.0.0.1", api_username="admin", api_password="admin") 148 | print fw.op("show system info") 149 | 150 | Some operational commands have methods to refresh the variables in an object: 151 | 152 | .. code-block:: python 153 | 154 | # populates the version, serial, and model variables from the live device 155 | fw.refresh_system_info() 156 | 157 | See more examples in the `Usage Guide `_. 158 | 159 | Upgrade from pandevice 160 | ---------------------- 161 | 162 | This ``pan-os-python`` package is the evolution of the older ``pandevice`` package. To 163 | upgrade from ``pandevice`` to ``pan-os-python``\ , follow these steps. 164 | 165 | Step 1. Ensure you are using python3 166 | 167 | `Python2 is end-of-life `_ and not 168 | supported by ``pan-os-python``. 169 | 170 | Step 2. Uninstall pandevice: 171 | 172 | .. code-block:: shell 173 | 174 | pip uninstall pandevice 175 | # or 176 | poetry remove pandevice 177 | 178 | Step 3. Install pan-os-python: 179 | 180 | .. code-block:: shell 181 | 182 | pip3 install pan-os-python 183 | # or 184 | poetry add pan-os-python 185 | 186 | Step 4. Change the import statements in your code from ``pandevice`` to ``panos``. For example: 187 | 188 | .. code-block:: python 189 | 190 | import pandevice 191 | from pandevice.firewall import Firewall 192 | 193 | # would change to 194 | 195 | import panos 196 | from panos.firewall import Firewall 197 | 198 | Step 5. Test your script or application 199 | 200 | There are no known breaking changes 201 | between ``pandevice v0.14.0`` and ``pan-os-python v1.0.0``\ , but it is a major 202 | upgrade so please verify everything works as expected. 203 | 204 | Contributors 205 | ------------ 206 | 207 | 208 | * Brian Torres-Gil - `btorresgil `_ 209 | * Garfield Freeman - `shinmog `_ 210 | * John Anderson - `lampwins `_ 211 | * Aditya Sripal - `AdityaSripal `_ 212 | 213 | Thank you to `Kevin Steves `_\ , creator of the `pan-python library `_ 214 | -------------------------------------------------------------------------------- /tests/test_params.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import xml.etree.ElementTree as ET 3 | 4 | from panos.base import ENTRY, Root 5 | from panos.base import VersionedPanObject, VersionedParamPath 6 | from panos.firewall import Firewall 7 | 8 | 9 | class FakeObject(VersionedPanObject): 10 | """Fake object for testing.""" 11 | 12 | SUFFIX = ENTRY 13 | ROOT = Root.VSYS 14 | 15 | def _setup(self): 16 | self._xpaths.add_profile(value="/fake") 17 | 18 | params = [] 19 | 20 | params.append( 21 | VersionedParamPath( 22 | "uuid", 23 | vartype="attrib", 24 | path="uuid", 25 | ), 26 | ) 27 | params.append( 28 | VersionedParamPath( 29 | "size", 30 | vartype="int", 31 | path="size", 32 | ), 33 | ) 34 | params.append( 35 | VersionedParamPath( 36 | "listing", 37 | vartype="member", 38 | path="listing", 39 | ), 40 | ) 41 | params.append( 42 | VersionedParamPath( 43 | "pb1", 44 | vartype="exist", 45 | path="pb1", 46 | ), 47 | ) 48 | params.append( 49 | VersionedParamPath( 50 | "pb2", 51 | vartype="exist", 52 | path="pb2", 53 | ), 54 | ) 55 | params.append( 56 | VersionedParamPath( 57 | "live", 58 | vartype="yesno", 59 | path="live", 60 | ), 61 | ) 62 | params.append( 63 | VersionedParamPath( 64 | "disabled", 65 | vartype="yesno", 66 | path="disabled", 67 | ), 68 | ) 69 | params.append( 70 | VersionedParamPath( 71 | "uuid2", 72 | vartype="attrib", 73 | path="level-2/uuid", 74 | ), 75 | ) 76 | params.append( 77 | VersionedParamPath( 78 | "age", 79 | vartype="int", 80 | path="level-2/age", 81 | ), 82 | ) 83 | params.append( 84 | VersionedParamPath( 85 | "interfaces", 86 | vartype="member", 87 | path="level-2/interface", 88 | ), 89 | ) 90 | 91 | self._params = tuple(params) 92 | 93 | 94 | def _verify_render(o, expected): 95 | ans = o.element_str().decode("utf-8") 96 | 97 | assert ans == expected 98 | 99 | 100 | def _refreshed_object(): 101 | fw = Firewall("127.0.0.1", "admin", "admin", "secret") 102 | fw._version_info = (9999, 0, 0) 103 | 104 | o = FakeObject() 105 | fw.add(o) 106 | 107 | o = o.refreshall_from_xml(_refresh_xml())[0] 108 | 109 | return o 110 | 111 | 112 | def _refresh_xml(): 113 | return ET.fromstring( 114 | """ 115 | 116 | 117 | 5 118 | 119 | first 120 | second 121 | 122 | 123 | yes 124 | 125 | 12 126 | 127 | third 128 | fourth 129 | 130 | 131 | 132 | """ 133 | ) 134 | 135 | 136 | # int at base level 137 | def test_render_int(): 138 | _verify_render( 139 | FakeObject("test", size=5), 140 | '5', 141 | ) 142 | 143 | 144 | def test_parse_int(): 145 | o = _refreshed_object() 146 | 147 | assert o.size == 5 148 | 149 | 150 | # member list at base level 151 | def test_render_member(): 152 | _verify_render( 153 | FakeObject("test", listing=["one", "two"]), 154 | 'onetwo', 155 | ) 156 | 157 | 158 | def test_parse_member(): 159 | o = _refreshed_object() 160 | 161 | assert o.listing == ["first", "second"] 162 | 163 | 164 | # exist at base level 165 | def test_render_exist(): 166 | _verify_render( 167 | FakeObject("test", pb1=True), 168 | '', 169 | ) 170 | 171 | 172 | def test_parse_exists(): 173 | o = _refreshed_object() 174 | 175 | assert o.pb1 176 | assert not o.pb2 177 | 178 | 179 | # yesno at base level 180 | def test_render_yesno(): 181 | _verify_render( 182 | FakeObject("test", disabled=True), 183 | 'yes', 184 | ) 185 | 186 | 187 | def test_parse_yesno(): 188 | o = _refreshed_object() 189 | 190 | assert o.disabled 191 | 192 | 193 | # attrib 194 | def test_render_attrib(): 195 | _verify_render( 196 | FakeObject("test", uuid="123-456"), 197 | '', 198 | ) 199 | 200 | 201 | def test_parse_attrib(): 202 | o = _refreshed_object() 203 | 204 | assert o.uuid == "123-456" 205 | 206 | 207 | # int at depth 1 208 | def test_render_d1_int(): 209 | _verify_render( 210 | FakeObject("test", age=12), 211 | '12', 212 | ) 213 | 214 | 215 | def test_parse_d1_int(): 216 | o = _refreshed_object() 217 | 218 | assert o.age == 12 219 | 220 | 221 | # member list at depth 1 222 | def test_render_d1_member(): 223 | _verify_render( 224 | FakeObject("test", interfaces=["third", "fourth"]), 225 | "".join( 226 | [ 227 | '', 228 | "thirdfourth", 229 | "", 230 | ] 231 | ), 232 | ) 233 | 234 | 235 | def test_parse_d1_member(): 236 | o = _refreshed_object() 237 | 238 | assert o.interfaces == ["third", "fourth"] 239 | 240 | 241 | # uuid at depth 1 242 | def test_render_d1_attrib_standalone(): 243 | _verify_render( 244 | FakeObject("test", uuid2="456-789"), 245 | '', 246 | ) 247 | 248 | 249 | def test_render_d1_attrib_mixed(): 250 | _verify_render( 251 | FakeObject("test", uuid2="456-789", age=12), 252 | '12', 253 | ) 254 | 255 | 256 | def test_parse_d1_attrib(): 257 | o = _refreshed_object() 258 | 259 | assert o.uuid2 == "456-789" 260 | 261 | 262 | # should raise an exception 263 | def test_update_attrib_raises_not_implemented_exception(): 264 | o = _refreshed_object() 265 | 266 | with pytest.raises(NotImplementedError): 267 | o.update("uuid") 268 | -------------------------------------------------------------------------------- /examples/bulk_address_objects.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2017, Palo Alto Networks 4 | # 5 | # Permission to use, copy, modify, and/or distribute this software for any 6 | # purpose with or without fee is hereby granted, provided that the above 7 | # copyright notice and this permission notice appear in all copies. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | 17 | """ 18 | bulk_address_objects.py 19 | ========================== 20 | 21 | Use bulk operations to create / delete hundreds of firewall Address Objects. 22 | 23 | NOTE: Please update the hostname and auth credentials variables 24 | before running. 25 | 26 | This script will create a large number of address objects on the firewall 27 | and then delete them. The intent is to show how to use the new bulk 28 | operations available in pan-os-python, both how to properly use them and what 29 | to be careful of. 30 | """ 31 | 32 | import datetime 33 | import sys 34 | 35 | import panos 36 | import panos.firewall 37 | import panos.objects 38 | 39 | 40 | HOSTNAME = "127.0.0.1" 41 | USERNAME = "admin" 42 | PASSWORD = "admin" 43 | PREFIX = "BulkAddressObject" 44 | 45 | 46 | def num_as_ip(num, offset=0): 47 | """Returns a number as a 192.168 IP address.""" 48 | return "192.168.{0}.{1}".format(num // 200 + 1 + offset, num % 200 + 2) 49 | 50 | 51 | def main(): 52 | # Before we begin, you'll need to use the pan-os-python documentation both 53 | # for this example and for any scripts you may write for yourself. The 54 | # docs can be found here: 55 | # 56 | # http://pan-os-python.readthedocs.io/en/latest/reference.html 57 | # 58 | # First, let's create the firewall object that we want to modify. 59 | fw = panos.firewall.Firewall(HOSTNAME, USERNAME, PASSWORD) 60 | print("Firewall system info: {0}".format(fw.refresh_system_info())) 61 | 62 | # Get the list of current address objects, as we'll need this later. We 63 | # don't want these address objects in our firewall tree yet, so let's set 64 | # the `add` flag in the refreshall method to False. 65 | original_objects = panos.objects.AddressObject.refreshall(fw, add=False) 66 | 67 | # As a sanity check, make sure no currently configured address objects 68 | # have the same name prefix as what this script uses. If so, quit. 69 | for x in original_objects: 70 | if x.uid.startswith(PREFIX): 71 | print( 72 | "Error: prefix {0} shared with address object {1}".format(PREFIX, x.uid) 73 | ) 74 | return 75 | 76 | # Just print out how many address objects were there beforehand. 77 | print("* There are {0} address object(s) currently *".format(len(original_objects))) 78 | 79 | # Create each address object and add it to the firewall. You'll notice 80 | # that we don't call `create()` on each object as you'd expect. This is 81 | # because we'll do a bulk create after we've finished creating everything. 82 | bulk_objects = [] 83 | for num in range(1, 601): 84 | ao = panos.objects.AddressObject( 85 | "{0}{1:03}".format(PREFIX, num), num_as_ip(num) 86 | ) 87 | bulk_objects.append(ao) 88 | fw.add(ao) 89 | 90 | # Now we can bulk create all the address objects. This is accomplished by 91 | # invoking `create_similar()` on any of the address objects in our tree, 92 | # turning what would have been 600 individual API calls and condensing it 93 | # into a single API call. 94 | start = datetime.datetime.now() 95 | bulk_objects[0].create_similar() 96 | print( 97 | "Creating {0} address objects took: {1}".format( 98 | len(bulk_objects), datetime.datetime.now() - start 99 | ) 100 | ) 101 | 102 | # We've done a bulk create, now let's look at bulk apply. 103 | # 104 | # Some care is needed when using apply with pan-os-python. All "apply" methods 105 | # are doing a PANOS API `type=edit` under the hood, which does a replace of 106 | # the current config with what is specified. 107 | # 108 | # So what does this mean? This means that if we wanted to do a mass 109 | # update of the address objects we just created, we need to make sure that 110 | # our object tree contains the address objects that existed before this 111 | # script started. So let's add in the pre-existing address objects to 112 | # the firewall's object tree. We'll do this first so we don't forget 113 | # later on. 114 | for x in original_objects: 115 | fw.add(x) 116 | 117 | # With that out of the way, we're ready to update or bulk address objects 118 | # by incrementing the third octet of each IP address by 10. 119 | for num, x in enumerate(bulk_objects, 1): 120 | x.value = num_as_ip(num, 10) 121 | 122 | # Now we can do our bulk apply, invoking `apply_similar()`. As before, 123 | # we invoke this on any of the related children in our pan-os-python 124 | # object tree. Most important of all, since our firewall object has all 125 | # the pre-existing address objects in its tree, we won't accidentally 126 | # truncate them from the firewall config. 127 | start = datetime.datetime.now() 128 | bulk_objects[0].apply_similar() 129 | print( 130 | "Bulk apply {0} address objects took: {1}".format( 131 | len(bulk_objects) + len(original_objects), datetime.datetime.now() - start 132 | ) 133 | ) 134 | 135 | # We've done create, we've done edit, that leaves bulk delete. We only 136 | # want to delete the bulk address objects we created in this script, so 137 | # let's remove all the pre-existing address objects from the firewall 138 | # object. 139 | for x in original_objects: 140 | fw.remove(x) 141 | 142 | # Finally, let's invoke `delete_similar()` from the firewall. As should be 143 | # expected, we invoke this from any of the objects currently in our 144 | # pan-os-python object tree. 145 | start = datetime.datetime.now() 146 | bulk_objects[0].delete_similar() 147 | print( 148 | "Deleting {0} address objects took: {1}".format( 149 | len(bulk_objects), datetime.datetime.now() - start 150 | ) 151 | ) 152 | 153 | # At this point, we've now used all the bulk operations. If performance 154 | # is a bottleneck for you, consider if any of your automation could be 155 | # refactored to use any of the bulk operations pan-os-python offers. 156 | print("Done!") 157 | 158 | 159 | if __name__ == "__main__": 160 | # This script doesn't take command line arguments. If any are passed in, 161 | # then print out the script's docstring and exit. 162 | if len(sys.argv) != 1: 163 | print(__doc__) 164 | else: 165 | # No CLI args, so run the main function. 166 | main() 167 | -------------------------------------------------------------------------------- /tests/live/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | 4 | import pytest 5 | 6 | from panos import firewall 7 | from panos import panorama 8 | 9 | 10 | live_devices = {} 11 | one_fw_per_version = [] 12 | one_device_type_per_version = [] 13 | one_panorama_per_version = [] 14 | ha_pairs = [] 15 | panorama_fw_combinations = [] 16 | 17 | 18 | def desc(pano=None, fw=None): 19 | ans = [] 20 | if pano is not None: 21 | ans.append("{0}.{1}Pano".format(*pano)) 22 | if fw is not None: 23 | ans.append("With") 24 | if fw is not None: 25 | ans.append("{0}.{1}NGFW".format(*fw)) 26 | return "".join(ans) 27 | 28 | 29 | def init(): 30 | """ 31 | Environment variables: 32 | PD_USERNAME 33 | PD_PASSWORD 34 | PD_PANORAMAS 35 | PD_FIREWALLS 36 | """ 37 | global live_devices 38 | global one_fw_per_version 39 | global one_device_per_version 40 | global one_panorama_per_version 41 | global ha_pairs 42 | global panorama_fw_combinations 43 | 44 | # Get os.environ stuff to set the live_devices global. 45 | try: 46 | username = os.environ["PD_USERNAME"] 47 | password = os.environ["PD_PASSWORD"] 48 | panos = os.environ["PD_PANORAMAS"].split() 49 | fws = os.environ["PD_FIREWALLS"].split() 50 | except KeyError as e: 51 | print('NOT RUNNING LIVE TESTS - missing "{0}"'.format(e)) 52 | return 53 | 54 | # Add each panorama to the live_devices. 55 | for hostname in panos: 56 | c = panorama.Panorama(hostname, username, password) 57 | try: 58 | c.refresh_system_info() 59 | except Exception as e: 60 | raise ValueError( 61 | "Failed to connect to panorama {0}: {1}".format(hostname, e) 62 | ) 63 | 64 | # There should only be one panorama per version. 65 | version = c._version_info 66 | if version in live_devices: 67 | raise ValueError( 68 | "Two panoramas, same version: {0} and {1}".format( 69 | live_devices[version]["pano"].hostname, hostname 70 | ) 71 | ) 72 | live_devices.setdefault(version, {"fws": [], "pano": None}) 73 | live_devices[version]["pano"] = c 74 | 75 | # Add each firewall to the live_devices. 76 | for hostname in fws: 77 | c = firewall.Firewall(hostname, username, password) 78 | try: 79 | c.refresh_system_info() 80 | except Exception as e: 81 | raise ValueError( 82 | "Failed to connect to firewall {0}: {1}".format(hostname, e) 83 | ) 84 | 85 | # Multiple firewalls are allowed per version, but only ever the first 86 | # two will be used. 87 | version = c._version_info 88 | live_devices.setdefault(version, {"fws": [], "pano": None}) 89 | live_devices[version]["fws"].append(c) 90 | 91 | # Set: 92 | # one_fw_per_version 93 | # one_device_type_per_version 94 | # one_panorama_per_version 95 | for version in live_devices: 96 | pano = live_devices[version]["pano"] 97 | fws = live_devices[version]["fws"] 98 | if fws: 99 | fw = random.choice(fws) 100 | one_device_type_per_version.append((fw, desc(fw=version))) 101 | one_fw_per_version.append((fw, desc(fw=version))) 102 | if pano is not None: 103 | one_panorama_per_version.append((pano, desc(pano=version))) 104 | one_device_type_per_version.append((pano, desc(pano=version))) 105 | 106 | # Set: ha_pairs 107 | for version in live_devices: 108 | fws = live_devices[version]["fws"] 109 | if len(fws) >= 2: 110 | ha_pairs.append((fws[:2], version)) 111 | 112 | # Set panorama_fw_combinations 113 | for pano_version in live_devices: 114 | pano = live_devices[pano_version]["pano"] 115 | if pano is None: 116 | continue 117 | 118 | for fw_version in live_devices: 119 | fws = live_devices[fw_version]["fws"] 120 | if not fws or pano_version < fw_version: 121 | continue 122 | 123 | fw = random.choice(fws) 124 | panorama_fw_combinations.append( 125 | ( 126 | (pano, fw), 127 | desc(pano_version, fw_version), 128 | ) 129 | ) 130 | 131 | 132 | # Invoke the init() to set globals for our tests. 133 | init() 134 | 135 | 136 | def pytest_report_header(config): 137 | if not one_device_type_per_version: 138 | ans = [ 139 | "Skipping live tests; no devices in the config", 140 | ] 141 | else: 142 | ans = [ 143 | "Given the following devices:", 144 | ] 145 | for v in sorted(live_devices.keys()): 146 | line = [ 147 | "* Version:{0}.{1}.{2}".format(*v), 148 | ] 149 | if live_devices[v]["pano"] is not None: 150 | line.append("Panorama:{0}".format(live_devices[v]["pano"].hostname)) 151 | for fw in live_devices[v]["fws"]: 152 | line.append("NGFW:{0}".format(fw.hostname)) 153 | ans.append(" ".join(line)) 154 | 155 | return ans 156 | 157 | 158 | # Order tests alphabetically. This is needed because by default pytest gets 159 | # the tests of the current class, executes them, then walks the inheritance 160 | # to get parent tests, which is not what we want. 161 | def pytest_collection_modifyitems(items): 162 | grouping = {} 163 | lookup = {} 164 | reordered = [] 165 | 166 | for x in items: 167 | location, tc = x.nodeid.rsplit("::", 1) 168 | lookup[(location, tc)] = x 169 | grouping.setdefault(location, []) 170 | grouping[location].append(tc) 171 | 172 | for location in sorted(grouping.keys()): 173 | tests = sorted(grouping[location]) 174 | for tc in tests: 175 | reordered.append(lookup[(location, tc)]) 176 | 177 | items[:] = reordered 178 | 179 | 180 | # Define a state fixture. 181 | class State(object): 182 | pass 183 | 184 | 185 | class StateMap(object): 186 | def __init__(self): 187 | self.config = {} 188 | 189 | def setdefault(self, *x): 190 | key = tuple(d.hostname for d in x) 191 | return self.config.setdefault(key, State()) 192 | 193 | 194 | @pytest.fixture(scope="class") 195 | def state_map(request): 196 | yield StateMap() 197 | 198 | 199 | # Define parametrized fixtures. 200 | @pytest.fixture( 201 | scope="session", 202 | params=[x[0] for x in one_fw_per_version], 203 | ids=[x[1] for x in one_fw_per_version], 204 | ) 205 | def fw(request): 206 | return request.param 207 | 208 | 209 | @pytest.fixture( 210 | scope="session", 211 | params=[x[0] for x in one_device_type_per_version], 212 | ids=[x[1] for x in one_device_type_per_version], 213 | ) 214 | def dev(request): 215 | return request.param 216 | 217 | 218 | @pytest.fixture( 219 | scope="session", 220 | params=[x[0] for x in one_panorama_per_version], 221 | ids=[x[1] for x in one_panorama_per_version], 222 | ) 223 | def pano(request): 224 | return request.param 225 | 226 | 227 | @pytest.fixture( 228 | scope="session", 229 | params=[x[0] for x in panorama_fw_combinations], 230 | ids=[x[1] for x in panorama_fw_combinations], 231 | ) 232 | def pairing(request): 233 | return request.param 234 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\complexity.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\complexity.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end -------------------------------------------------------------------------------- /tests/test_device_profile_xpaths.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | 3 | import pytest 4 | 5 | try: 6 | from unittest import mock 7 | except ImportError: 8 | import mock 9 | 10 | from panos.device import SnmpServerProfile 11 | from panos.device import SnmpV2cServer 12 | from panos.device import SnmpV3Server 13 | 14 | from panos.device import EmailServerProfile 15 | from panos.device import EmailServer 16 | 17 | from panos.device import LdapServerProfile 18 | from panos.device import LdapServer 19 | 20 | from panos.device import SyslogServerProfile 21 | from panos.device import SyslogServer 22 | 23 | from panos.device import HttpServerProfile 24 | from panos.device import HttpServer 25 | from panos.device import HttpConfigHeader 26 | from panos.device import HttpConfigParam 27 | from panos.device import HttpSystemHeader 28 | from panos.device import HttpSystemParam 29 | from panos.device import HttpThreatHeader 30 | from panos.device import HttpThreatParam 31 | from panos.device import HttpTrafficHeader 32 | from panos.device import HttpTrafficParam 33 | from panos.device import HttpHipMatchHeader 34 | from panos.device import HttpHipMatchParam 35 | from panos.device import HttpUrlHeader 36 | from panos.device import HttpUrlParam 37 | from panos.device import HttpDataHeader 38 | from panos.device import HttpDataParam 39 | from panos.device import HttpWildfireHeader 40 | from panos.device import HttpWildfireParam 41 | from panos.device import HttpTunnelHeader 42 | from panos.device import HttpTunnelParam 43 | from panos.device import HttpUserIdHeader 44 | from panos.device import HttpUserIdParam 45 | from panos.device import HttpGtpHeader 46 | from panos.device import HttpGtpParam 47 | from panos.device import HttpAuthHeader 48 | from panos.device import HttpAuthParam 49 | from panos.device import HttpSctpHeader 50 | from panos.device import HttpSctpParam 51 | from panos.device import HttpIpTagHeader 52 | from panos.device import HttpIpTagParam 53 | 54 | from panos.firewall import Firewall 55 | from panos.device import Vsys 56 | 57 | from panos.panorama import Panorama 58 | from panos.panorama import Template 59 | 60 | 61 | OBJECTS = { 62 | SnmpServerProfile: [None, SnmpV2cServer, SnmpV3Server], 63 | EmailServerProfile: [ 64 | None, 65 | EmailServer, 66 | ], 67 | LdapServerProfile: [ 68 | None, 69 | LdapServer, 70 | ], 71 | SyslogServerProfile: [ 72 | None, 73 | SyslogServer, 74 | ], 75 | HttpServerProfile: [ 76 | None, 77 | HttpServer, 78 | HttpConfigHeader, 79 | HttpConfigParam, 80 | HttpSystemHeader, 81 | HttpSystemParam, 82 | HttpThreatHeader, 83 | HttpThreatParam, 84 | HttpTrafficHeader, 85 | HttpTrafficParam, 86 | HttpHipMatchHeader, 87 | HttpHipMatchParam, 88 | HttpUrlHeader, 89 | HttpUrlParam, 90 | HttpDataHeader, 91 | HttpDataParam, 92 | HttpWildfireHeader, 93 | HttpWildfireParam, 94 | HttpTunnelHeader, 95 | HttpTunnelParam, 96 | HttpUserIdHeader, 97 | HttpUserIdParam, 98 | HttpGtpHeader, 99 | HttpGtpParam, 100 | HttpAuthHeader, 101 | HttpAuthParam, 102 | HttpSctpHeader, 103 | HttpSctpParam, 104 | HttpIpTagHeader, 105 | HttpIpTagParam, 106 | ], 107 | } 108 | 109 | """ 110 | @pytest.fixture( 111 | scope="function", 112 | params=[x for x in DEVICES], 113 | ids=[x.__class__.__name__ for x in DEVICES], 114 | ) 115 | def pdev(request): 116 | request.param.removeall() 117 | return request.param 118 | """ 119 | 120 | 121 | @pytest.fixture( 122 | scope="function", 123 | params=[(x, y) for x, v in OBJECTS.items() for y in v], 124 | ids=[ 125 | "{0}{1}".format(x.__class__.__name__, y.__class__.__name__ if y else "None") 126 | for x, v in OBJECTS.items() 127 | for y in v 128 | ], 129 | ) 130 | def combination(request): 131 | return request.param 132 | 133 | 134 | def test_firewall_shared_xpath(combination): 135 | expected = [ 136 | "/config/shared", 137 | ] 138 | fw = Firewall("127.0.0.1", "admin", "admin", "secret") 139 | fw._version_info = (9999, 0, 0) 140 | fw.vsys = "shared" 141 | parent_cls, child_cls = combination 142 | 143 | o = parent_cls("one") 144 | expected.append(o.xpath()) 145 | if child_cls is not None: 146 | o2 = child_cls("two") 147 | expected.append(o2.xpath()) 148 | o.add(o2) 149 | o = o2 150 | fw.add(o.parent) 151 | else: 152 | fw.add(o) 153 | 154 | assert "".join(expected) == o.xpath() 155 | 156 | 157 | def test_firewall_vsys_xpath(combination): 158 | expected = [ 159 | "/config/devices/entry[@name='localhost.localdomain']", 160 | "/vsys/entry[@name='vsys1']", 161 | ] 162 | fw = Firewall("127.0.0.1", "admin", "admin", "secret") 163 | fw._version_info = (9999, 0, 0) 164 | parent_cls, child_cls = combination 165 | 166 | o = parent_cls("one") 167 | expected.append(o.xpath()) 168 | if child_cls is not None: 169 | o2 = child_cls("two") 170 | expected.append(o2.xpath()) 171 | o.add(o2) 172 | o = o2 173 | fw.add(o.parent) 174 | else: 175 | fw.add(o) 176 | 177 | assert "".join(expected) == o.xpath() 178 | 179 | 180 | def test_firewall_vsys_object_xpath(combination): 181 | expected = [ 182 | "/config/devices/entry[@name='localhost.localdomain']", 183 | "/vsys/entry[@name='vsys2']", 184 | ] 185 | fw = Firewall("127.0.0.1", "admin", "admin", "secret") 186 | fw._version_info = (9999, 0, 0) 187 | vsys = Vsys("vsys2") 188 | fw.add(vsys) 189 | parent_cls, child_cls = combination 190 | 191 | o = parent_cls("one") 192 | expected.append(o.xpath()) 193 | if child_cls is not None: 194 | o2 = child_cls("two") 195 | expected.append(o2.xpath()) 196 | o.add(o2) 197 | o = o2 198 | vsys.add(o.parent) 199 | else: 200 | vsys.add(o) 201 | 202 | assert "".join(expected) == o.xpath() 203 | 204 | 205 | def test_panorama_template_object_xpath(combination): 206 | expected = [ 207 | "/config/devices/entry[@name='localhost.localdomain']", 208 | ] 209 | pano = Panorama("127.0.0.1", "admin", "admin", "secret") 210 | pano._version_info = (9999, 0, 0) 211 | tmpl = Template("myTemplate") 212 | expected.append(tmpl.xpath()) 213 | pano.add(tmpl) 214 | expected.append("/config/shared") 215 | vsys = Vsys("shared") 216 | tmpl.add(vsys) 217 | parent_cls, child_cls = combination 218 | 219 | o = parent_cls("one") 220 | expected.append(o.xpath()) 221 | if child_cls is not None: 222 | o2 = child_cls("two") 223 | expected.append(o2.xpath()) 224 | o.add(o2) 225 | o = o2 226 | vsys.add(o.parent) 227 | else: 228 | vsys.add(o) 229 | 230 | assert "".join(expected) == o.xpath() 231 | 232 | 233 | def test_panorama_local_object_xpath(combination): 234 | expected = [ 235 | "/config/panorama", 236 | ] 237 | pano = Panorama("127.0.0.1", "admin", "admin", "secret") 238 | pano._version_info = (9999, 0, 0) 239 | parent_cls, child_cls = combination 240 | 241 | o = parent_cls("one") 242 | expected.append(o.xpath()) 243 | if child_cls is not None: 244 | o2 = child_cls("two") 245 | expected.append(o2.xpath()) 246 | o.add(o2) 247 | o = o2 248 | pano.add(o.parent) 249 | else: 250 | pano.add(o) 251 | 252 | assert "".join(expected) == o.xpath() 253 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXAUTOBUILD = sphinx-autobuild 8 | PAPER = 9 | BUILDDIR = _build 10 | 11 | # User-friendly check for sphinx-build 12 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 13 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 14 | endif 15 | 16 | # Internal variables. 17 | PAPEROPT_a4 = -D latex_paper_size=a4 18 | PAPEROPT_letter = -D latex_paper_size=letter 19 | ALLSPHINXOPTS = -a -v -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 20 | # the i18n builder cannot share the environment and doctrees with the others 21 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 22 | # options for sphinx-autobuild 23 | AUTOSPHINXOPTS = --ignore *.dot --watch ../panos 24 | 25 | 26 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 27 | 28 | help: 29 | @echo "Please use \`make ' where is one of" 30 | @echo " html to make standalone HTML files" 31 | @echo " dirhtml to make HTML files named index.html in directories" 32 | @echo " singlehtml to make a single large HTML file" 33 | @echo " pickle to make pickle files" 34 | @echo " json to make JSON files" 35 | @echo " htmlhelp to make HTML files and a HTML help project" 36 | @echo " qthelp to make HTML files and a qthelp project" 37 | @echo " devhelp to make HTML files and a Devhelp project" 38 | @echo " epub to make an epub" 39 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 40 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 41 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 42 | @echo " text to make text files" 43 | @echo " man to make manual pages" 44 | @echo " texinfo to make Texinfo files" 45 | @echo " info to make Texinfo files and run them through makeinfo" 46 | @echo " gettext to make PO message catalogs" 47 | @echo " changes to make an overview of all changed/added/deprecated items" 48 | @echo " xml to make Docutils-native XML files" 49 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 50 | @echo " linkcheck to check all external links for integrity" 51 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 52 | 53 | clean: 54 | rm -rf $(BUILDDIR)/* 55 | 56 | html: 57 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 58 | @echo 59 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 60 | 61 | autohtml: 62 | $(SPHINXAUTOBUILD) $(AUTOSPHINXOPTS) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 63 | 64 | dirhtml: 65 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 66 | @echo 67 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 68 | 69 | singlehtml: 70 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 71 | @echo 72 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 73 | 74 | pickle: 75 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 76 | @echo 77 | @echo "Build finished; now you can process the pickle files." 78 | 79 | json: 80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 81 | @echo 82 | @echo "Build finished; now you can process the JSON files." 83 | 84 | htmlhelp: 85 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 86 | @echo 87 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 88 | ".hhp project file in $(BUILDDIR)/htmlhelp." 89 | 90 | qthelp: 91 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 92 | @echo 93 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 94 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 95 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/complexity.qhcp" 96 | @echo "To view the help file:" 97 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/complexity.qhc" 98 | 99 | devhelp: 100 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 101 | @echo 102 | @echo "Build finished." 103 | @echo "To view the help file:" 104 | @echo "# mkdir -p $$HOME/.local/share/devhelp/complexity" 105 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/complexity" 106 | @echo "# devhelp" 107 | 108 | epub: 109 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 110 | @echo 111 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 112 | 113 | latex: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo 116 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 117 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 118 | "(use \`make latexpdf' here to do that automatically)." 119 | 120 | latexpdf: 121 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 122 | @echo "Running LaTeX files through pdflatex..." 123 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 124 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 125 | 126 | latexpdfja: 127 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 128 | @echo "Running LaTeX files through platex and dvipdfmx..." 129 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 130 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 131 | 132 | text: 133 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 134 | @echo 135 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 136 | 137 | man: 138 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 139 | @echo 140 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 141 | 142 | texinfo: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo 145 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 146 | @echo "Run \`make' in that directory to run these through makeinfo" \ 147 | "(use \`make info' here to do that automatically)." 148 | 149 | info: 150 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 151 | @echo "Running Texinfo files through makeinfo..." 152 | make -C $(BUILDDIR)/texinfo info 153 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 154 | 155 | gettext: 156 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 157 | @echo 158 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 159 | 160 | changes: 161 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 162 | @echo 163 | @echo "The overview file is in $(BUILDDIR)/changes." 164 | 165 | linkcheck: 166 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 167 | @echo 168 | @echo "Link check complete; look for any errors in the above output " \ 169 | "or in $(BUILDDIR)/linkcheck/output.txt." 170 | 171 | doctest: 172 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 173 | @echo "Testing of doctests in the sources finished, look at the " \ 174 | "results in $(BUILDDIR)/doctest/output.txt." 175 | 176 | xml: 177 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 178 | @echo 179 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 180 | 181 | pseudoxml: 182 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 183 | @echo 184 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 185 | -------------------------------------------------------------------------------- /tests/live/testlib.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | import pytest 4 | 5 | from panos import network 6 | 7 | 8 | def random_name(max=None): 9 | return "".join( 10 | random.choice("abcdefghijklmnopqrstuvwxyz") 11 | for x in range(10 if max is None else max) 12 | ) 13 | 14 | 15 | def random_ip(netmask=None): 16 | return "{0}.{1}.{2}.{3}{4}".format( 17 | random.randint(11, 150), 18 | random.randint(1, 200), 19 | random.randint(1, 200), 20 | 1 if netmask is not None else random.randint(2, 200), 21 | netmask or "", 22 | ) 23 | 24 | 25 | def random_netmask(): 26 | return "{0}.{1}.{2}.0/24".format( 27 | random.randint(11, 150), 28 | random.randint(1, 200), 29 | random.randint(1, 200), 30 | ) 31 | 32 | 33 | def random_ipv6(ending=None): 34 | if ending is None: 35 | return ":".join("{0:04x}".format(random.randint(1, 65535)) for x in range(8)) 36 | else: 37 | return "{0:04x}:{1:04x}:{2:04x}:{3:04x}::{4}".format( 38 | random.randint(1, 65535), 39 | random.randint(1, 65535), 40 | random.randint(1, 65535), 41 | random.randint(1, 65535), 42 | ending, 43 | ) 44 | 45 | 46 | def random_mac(): 47 | return ":".join("{0:02x}".format(random.randint(0, 255)) for x in range(6)) 48 | 49 | 50 | def get_available_interfaces(con, num=1): 51 | ifaces = network.EthernetInterface.refreshall(con, add=False) 52 | ifaces = set(x.name for x in ifaces) 53 | 54 | all_interfaces = set("ethernet1/{0}".format(x) for x in range(1, 10)) 55 | available = all_interfaces.difference(ifaces) 56 | 57 | ans = [] 58 | while len(ans) != num: 59 | # Raises KeyError 60 | ans.append(available.pop()) 61 | 62 | return ans 63 | 64 | 65 | class FwFlow(object): 66 | def test_01_setup_dependencies(self, fw, state_map): 67 | state = state_map.setdefault(fw) 68 | state.err = False 69 | state.fail_func = pytest.skip 70 | 71 | try: 72 | self.create_dependencies(fw, state) 73 | except Exception as e: 74 | print("SETUP ERROR: {0}".format(e)) 75 | state.err = True 76 | pytest.skip("Setup failed") 77 | 78 | def create_dependencies(self, fw, state): 79 | pass 80 | 81 | def sanity(self, fw, state_map): 82 | state = state_map.setdefault(fw) 83 | if state.err: 84 | state.fail_func("prereq failed") 85 | 86 | return state 87 | 88 | def test_02_create(self, fw, state_map): 89 | state = self.sanity(fw, state_map) 90 | 91 | state.fail_func = pytest.xfail 92 | state.err = True 93 | self.setup_state_obj(fw, state) 94 | state.obj.create() 95 | state.err = False 96 | 97 | def setup_state_obj(self, fw, state): 98 | pass 99 | 100 | def test_03_refreshall(self, fw, state_map): 101 | state = self.sanity(fw, state_map) 102 | 103 | objs = state.obj.refreshall(state.obj.parent, add=False) 104 | assert len(objs) >= 1 105 | 106 | def test_04_update(self, fw, state_map): 107 | state = self.sanity(fw, state_map) 108 | 109 | self.update_state_obj(fw, state) 110 | state.obj.apply() 111 | 112 | def update_state_obj(self, fw, state): 113 | pass 114 | 115 | def test_97_delete(self, fw, state_map): 116 | state = self.sanity(fw, state_map) 117 | 118 | state.obj.delete() 119 | 120 | def test_98_cleanup_dependencies(self, fw, state_map): 121 | state = state_map.setdefault(fw) 122 | self.cleanup_dependencies(fw, state) 123 | 124 | def cleanup_dependencies(self, fw, state): 125 | pass 126 | 127 | def test_99_removeall(self, fw, state_map): 128 | fw.removeall() 129 | 130 | 131 | class DevFlow(object): 132 | def test_01_setup_dependencies(self, dev, state_map): 133 | state = state_map.setdefault(dev) 134 | state.err = False 135 | state.fail_func = pytest.skip 136 | 137 | try: 138 | self.create_dependencies(dev, state) 139 | except Exception as e: 140 | print("SETUP ERROR: {0}".format(e)) 141 | state.err = True 142 | pytest.skip("Setup failed") 143 | 144 | def create_dependencies(self, dev, state): 145 | pass 146 | 147 | def sanity(self, dev, state_map): 148 | state = state_map.setdefault(dev) 149 | if state.err: 150 | state.fail_func("prereq failed") 151 | 152 | return state 153 | 154 | def test_02_create(self, dev, state_map): 155 | state = self.sanity(dev, state_map) 156 | 157 | state.fail_func = pytest.xfail 158 | state.err = True 159 | self.setup_state_obj(dev, state) 160 | state.obj.create() 161 | state.err = False 162 | 163 | def setup_state_obj(self, dev, state): 164 | pass 165 | 166 | def test_03_refreshall(self, dev, state_map): 167 | state = self.sanity(dev, state_map) 168 | 169 | objs = state.obj.refreshall(state.obj.parent, add=False) 170 | assert len(objs) >= 1 171 | 172 | def test_04_update(self, dev, state_map): 173 | state = self.sanity(dev, state_map) 174 | 175 | self.update_state_obj(dev, state) 176 | state.obj.apply() 177 | 178 | def update_state_obj(self, dev, state): 179 | pass 180 | 181 | def test_97_delete(self, dev, state_map): 182 | state = self.sanity(dev, state_map) 183 | 184 | state.obj.delete() 185 | 186 | def test_98_cleanup_dependencies(self, dev, state_map): 187 | state = state_map.setdefault(dev) 188 | 189 | self.cleanup_dependencies(dev, state) 190 | 191 | def cleanup_dependencies(self, dev, state): 192 | pass 193 | 194 | def test_99_removeall(self, dev, state_map): 195 | dev.removeall() 196 | 197 | 198 | class PanoFlow(object): 199 | def test_01_setup_dependencies(self, pano, state_map): 200 | state = state_map.setdefault(pano) 201 | state.err = False 202 | state.fail_func = pytest.skip 203 | 204 | try: 205 | self.create_dependencies(pano, state) 206 | except Exception as e: 207 | print("SETUP ERROR: {0}".format(e)) 208 | state.err = True 209 | pytest.skip("Setup failed") 210 | 211 | def create_dependencies(self, pano, state): 212 | pass 213 | 214 | def sanity(self, pano, state_map): 215 | state = state_map.setdefault(pano) 216 | if state.err: 217 | state.fail_func("prereq failed") 218 | 219 | return state 220 | 221 | def test_02_create(self, pano, state_map): 222 | state = self.sanity(pano, state_map) 223 | 224 | state.fail_func = pytest.xfail 225 | state.err = True 226 | self.setup_state_obj(pano, state) 227 | state.obj.create() 228 | state.err = False 229 | 230 | def setup_state_obj(self, pano, state): 231 | pass 232 | 233 | def test_03_refreshall(self, pano, state_map): 234 | state = self.sanity(pano, state_map) 235 | 236 | objs = state.obj.refreshall(state.obj.parent, add=False) 237 | assert len(objs) >= 1 238 | 239 | def test_04_update(self, pano, state_map): 240 | state = self.sanity(pano, state_map) 241 | 242 | self.update_state_obj(pano, state) 243 | state.obj.apply() 244 | 245 | def update_state_obj(self, pano, state): 246 | pass 247 | 248 | def test_97_delete(self, pano, state_map): 249 | state = self.sanity(pano, state_map) 250 | 251 | state.obj.delete() 252 | 253 | def test_98_cleanup_dependencies(self, pano, state_map): 254 | state = state_map.setdefault(pano) 255 | 256 | self.cleanup_dependencies(pano, state) 257 | 258 | def cleanup_dependencies(self, pano, state): 259 | pass 260 | 261 | def test_99_removeall(self, pano, state_map): 262 | pano.removeall() 263 | -------------------------------------------------------------------------------- /tests/live/test_device_config.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from panos import device 4 | from tests.live import testlib 5 | 6 | 7 | class TestDeviceConfig(object): 8 | def toggle_object_variable(self, obj, var, new_value): 9 | original_value = getattr(obj, var) 10 | for value in (new_value, original_value): 11 | setattr(obj, var, value) 12 | obj.update(var) 13 | 14 | def test_01_get_device_config(self, dev, state_map): 15 | state = state_map.setdefault(dev) 16 | state.got_device_config = False 17 | 18 | dco = device.SystemSettings.refreshall(dev) 19 | state.got_device_config = True 20 | state.dco = dco[0] 21 | 22 | def test_02_update_hostname(self, dev, state_map): 23 | state = state_map.setdefault(dev) 24 | if not state.got_device_config: 25 | pytest.xfail("failed to get device config") 26 | 27 | # Change the hostname 28 | self.toggle_object_variable(state.dco, "hostname", testlib.random_name()) 29 | 30 | def test_03_update_secondary_dns(self, dev, state_map): 31 | state = state_map.setdefault(dev) 32 | if not state.got_device_config: 33 | pytest.xfail("failed to get device config") 34 | 35 | # Toggle the secondary ip address 36 | self.toggle_object_variable(state.dco, "dns_secondary", testlib.random_ip()) 37 | 38 | def test_04_create_ntp(self, dev, state_map): 39 | state = state_map.setdefault(dev) 40 | if not state.got_device_config: 41 | pytest.xfail("failed to get device config") 42 | 43 | primary = None 44 | secondary = None 45 | 46 | for x in state.dco.children: 47 | if x.__class__ == device.NTPServerPrimary: 48 | primary = x 49 | elif x.__class__ == device.NTPServerSecondary: 50 | secondary = x 51 | 52 | state.restore_ntp = False 53 | if primary is None: 54 | state.ntp_obj = device.NTPServerPrimary(address=testlib.random_ip()) 55 | elif secondary is None: 56 | state.ntp_obj = device.NTPServerSecondary(address=testlib.random_ip()) 57 | else: 58 | state.created_ntp = True 59 | state.restore_ntp = True 60 | state.ntp_obj = secondary 61 | pytest.skip("Both primary and secondary exist, nothing to create") 62 | 63 | state.dco.add(state.ntp_obj) 64 | state.ntp_obj.create() 65 | state.created_ntp = True 66 | 67 | def test_05_update_ntp(self, dev, state_map): 68 | state = state_map.setdefault(dev) 69 | if not state.got_device_config: 70 | pytest.xfail("failed to get device config") 71 | if not state.created_ntp: 72 | pytest.xfail("failed to create ntp in previous step") 73 | 74 | self.toggle_object_variable(state.ntp_obj, "address", testlib.random_ip()) 75 | 76 | def test_06_delete_ntp(self, dev, state_map): 77 | state = state_map.setdefault(dev) 78 | if not state.got_device_config: 79 | pytest.xfail("failed to get device config") 80 | if not state.created_ntp: 81 | pytest.xfail("failed to create ntp in previous step") 82 | 83 | state.ntp_obj.delete() 84 | 85 | def test_07_restore_ntp(self, dev, state_map): 86 | state = state_map.setdefault(dev) 87 | if not state.got_device_config: 88 | pytest.xfail("failed to get device config") 89 | if not state.restore_ntp: 90 | pytest.skip("restore not needed") 91 | 92 | state.dco.add(state.ntp_obj) 93 | state.ntp_obj.create() 94 | 95 | def test_99_removeall(self, dev, state_map): 96 | dev.removeall() 97 | 98 | 99 | class TestPasswordProfile(testlib.DevFlow): 100 | def setup_state_obj(self, dev, state): 101 | state.obj = device.PasswordProfile(testlib.random_name(), 0, 0, 0, 0) 102 | dev.add(state.obj) 103 | 104 | def update_state_obj(self, dev, state): 105 | state.obj.expiration = 120 106 | state.obj.warning = 15 107 | state.obj.login_count = 1 108 | state.obj.grace_period = 15 109 | 110 | 111 | class TestFirewallAdministrator(testlib.FwFlow): 112 | def create_dependencies(self, fw, state): 113 | state.profiles = [] 114 | for x in range(2): 115 | state.profiles.append( 116 | device.PasswordProfile(testlib.random_name(), x, x, x, x) 117 | ) 118 | fw.add(state.profiles[x]) 119 | 120 | state.profiles[0].create_similar() 121 | 122 | def setup_state_obj(self, fw, state): 123 | state.obj = device.Administrator( 124 | testlib.random_name(), superuser=True, password_profile=state.profiles[0] 125 | ) 126 | fw.add(state.obj) 127 | 128 | def update_state_obj(self, fw, state): 129 | state.obj.password_profile = state.profiles[1] 130 | 131 | def test_05_superuser_read_only(self, fw, state_map): 132 | state = self.sanity(fw, state_map) 133 | 134 | state.obj.superuser = None 135 | state.obj.superuser_read_only = True 136 | 137 | state.obj.apply() 138 | 139 | def test_06_device_admin(self, fw, state_map): 140 | state = self.sanity(fw, state_map) 141 | 142 | state.obj.superuser_read_only = None 143 | state.obj.device_admin = True 144 | 145 | state.obj.apply() 146 | 147 | def test_07_device_admin_read_only(self, fw, state_map): 148 | state = self.sanity(fw, state_map) 149 | 150 | state.obj.device_admin = None 151 | state.obj.device_admin_read_only = True 152 | 153 | state.obj.apply() 154 | 155 | def test_08_set_password(self, fw, state_map): 156 | state = self.sanity(fw, state_map) 157 | 158 | # Set the password 159 | state.obj.change_password("secret") 160 | 161 | # Now verify the change by trying to login 162 | new_fw = fw.__class__(fw.hostname, state.obj.uid, "secret") 163 | new_fw.refresh_system_info() 164 | 165 | def cleanup_dependencies(self, fw, state): 166 | try: 167 | state.profiles[0].delete_similar() 168 | except IndexError: 169 | pass 170 | 171 | 172 | class TestPanoramaAdministrator(testlib.PanoFlow): 173 | def create_dependencies(self, pano, state): 174 | state.profiles = [] 175 | for x in range(2): 176 | state.profiles.append( 177 | device.PasswordProfile(testlib.random_name(), x, x, x, x) 178 | ) 179 | pano.add(state.profiles[x]) 180 | 181 | state.profiles[0].create_similar() 182 | 183 | def setup_state_obj(self, pano, state): 184 | state.obj = device.Administrator( 185 | testlib.random_name(), superuser=True, password_profile=state.profiles[0] 186 | ) 187 | pano.add(state.obj) 188 | 189 | def update_state_obj(self, pano, state): 190 | state.obj.password_profile = state.profiles[1] 191 | 192 | def test_05_superuser_read_only(self, pano, state_map): 193 | state = self.sanity(pano, state_map) 194 | 195 | state.obj.superuser = None 196 | state.obj.superuser_read_only = True 197 | 198 | state.obj.apply() 199 | 200 | def test_06_panorama_admin(self, pano, state_map): 201 | state = self.sanity(pano, state_map) 202 | 203 | state.obj.superuser_read_only = None 204 | state.obj.panorama_admin = True 205 | 206 | state.obj.apply() 207 | 208 | def test_07_set_password(self, pano, state_map): 209 | state = self.sanity(pano, state_map) 210 | 211 | # Set the password 212 | state.obj.change_password("secret") 213 | 214 | # Now verify the change by trying to login 215 | new_pano = pano.__class__(pano.hostname, state.obj.uid, "secret") 216 | new_pano.refresh_system_info() 217 | 218 | def cleanup_dependencies(self, pano, state): 219 | try: 220 | state.profiles[0].delete_similar() 221 | except IndexError: 222 | pass 223 | -------------------------------------------------------------------------------- /tests/test_predefined.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | 3 | import pytest 4 | 5 | try: 6 | from unittest import mock 7 | except ImportError: 8 | import mock 9 | 10 | from panos.firewall import Firewall 11 | from panos.objects import ApplicationContainer 12 | from panos.objects import ApplicationObject 13 | from panos.objects import ServiceObject 14 | from panos.objects import Tag 15 | 16 | 17 | PREDEFINED_CONFIG = { 18 | ApplicationContainer: { 19 | "single": "refresh_application", 20 | "multiple": "refreshall_applications", 21 | "refresher": "application", 22 | "var": "application_container_objects", 23 | }, 24 | ApplicationObject: { 25 | "single": "refresh_application", 26 | "multiple": "refreshall_applications", 27 | "refresher": "application", 28 | "var": "application_objects", 29 | }, 30 | ServiceObject: { 31 | "single": "refresh_service", 32 | "multiple": "refreshall_services", 33 | "refresher": "service", 34 | "var": "service_objects", 35 | }, 36 | Tag: { 37 | "single": "refresh_tag", 38 | "multiple": "refreshall_tags", 39 | "refresher": "tag", 40 | "var": "tag_objects", 41 | }, 42 | } 43 | 44 | PREDEFINED_TEST_DATA = ( 45 | ( 46 | """//*[contains(local-name(), "application")]/entry[@name='{0}']""", 47 | '//*[contains(local-name(), "application")]/entry', 48 | ApplicationContainer( 49 | name="ap container 1", 50 | applications=["func1", "func2"], 51 | ), 52 | ApplicationContainer( 53 | name="application container deux", 54 | applications=["a", "la", "mode"], 55 | ), 56 | ), 57 | ( 58 | """//*[contains(local-name(), "application")]/entry[@name='{0}']""", 59 | '//*[contains(local-name(), "application")]/entry', 60 | ApplicationObject( 61 | name="app1", 62 | category="cat1", 63 | subcategory="subcat1", 64 | technology="tech1", 65 | risk=1, 66 | timeout=42, 67 | evasive_behavior=True, 68 | file_type_ident=True, 69 | ), 70 | ApplicationObject( 71 | name="app2", 72 | category="cat2", 73 | subcategory="subcat2", 74 | technology="tech2", 75 | risk=5, 76 | timeout=11, 77 | used_by_malware=True, 78 | consume_big_bandwidth=True, 79 | tag=["tag1", "tag2"], 80 | ), 81 | ), 82 | ( 83 | None, 84 | "/service/entry", 85 | ServiceObject( 86 | name="foo", 87 | protocol="tcp", 88 | destination_port="12345", 89 | description="all your base", 90 | tag=["are belong", "to us"], 91 | ), 92 | ServiceObject( 93 | name="bar", 94 | protocol="udp", 95 | source_port="1025-2048", 96 | destination_port="1-1024", 97 | description="wu", 98 | tag=["tang", "clan"], 99 | ), 100 | ), 101 | ( 102 | None, 103 | "/tag/entry", 104 | Tag( 105 | name="foo", 106 | color="color1", 107 | comments="First color", 108 | ), 109 | Tag( 110 | name="bar", 111 | color="color42", 112 | comments="Another color for another time", 113 | ), 114 | ), 115 | ) 116 | 117 | 118 | def object_not_found(): 119 | elm = ET.Element("response", {"code": "7", "status": "success"}) 120 | ET.SubElement(elm, "result") 121 | 122 | return elm 123 | 124 | 125 | @pytest.fixture( 126 | scope="function", 127 | params=[(x[0], x[2]) for x in PREDEFINED_TEST_DATA], 128 | ids=[x[2].__class__.__name__ for x in PREDEFINED_TEST_DATA], 129 | ) 130 | def predef_single(request): 131 | request.param[1].parent = None 132 | return request.param 133 | 134 | 135 | @pytest.fixture( 136 | scope="function", 137 | params=[(x[1], x[2:]) for x in PREDEFINED_TEST_DATA], 138 | ids=[x[2].__class__.__name__ for x in PREDEFINED_TEST_DATA], 139 | ) 140 | def predef_multiple(request): 141 | for x in request.param[2:]: 142 | x.parent = None 143 | 144 | return request.param 145 | 146 | 147 | def _fw(*args): 148 | fw = Firewall("127.0.0.1", "admin", "admin", "secret") 149 | fw._version_info = (9999, 0, 0) 150 | 151 | if len(args) == 0: 152 | fw.xapi.get = mock.Mock(return_value=object_not_found()) 153 | else: 154 | prefix = "" 155 | suffix = "" 156 | inner = "".join(x.element_str().decode("utf-8") for x in args) 157 | fw.xapi.get = mock.Mock( 158 | return_value=ET.fromstring( 159 | prefix + inner + suffix, 160 | ) 161 | ) 162 | 163 | return fw 164 | 165 | 166 | def test_single_object_xpath(predef_single): 167 | xpath, obj = predef_single 168 | conf = PREDEFINED_CONFIG[obj.__class__] 169 | expected = "/config/predefined" 170 | if xpath is not None: 171 | expected += xpath.format(obj.uid) 172 | else: 173 | expected += obj.xpath() 174 | fw = _fw(obj) 175 | 176 | getattr(fw.predefined, conf["single"])(obj.uid) 177 | 178 | fw.xapi.get.assert_called_once_with(expected, retry_on_peer=False) 179 | 180 | 181 | def test_get_single_object(predef_single): 182 | xpath, obj = predef_single 183 | conf = PREDEFINED_CONFIG[obj.__class__] 184 | fw = _fw(obj) 185 | 186 | getattr(fw.predefined, conf["single"])(obj.uid) 187 | 188 | data = getattr(fw.predefined, conf["var"]) 189 | assert obj.uid in data 190 | assert data[obj.uid].equal(obj) 191 | 192 | 193 | def test_multiple_object_xpath(predef_multiple): 194 | xpath, objs = predef_multiple 195 | conf = PREDEFINED_CONFIG[objs[0].__class__] 196 | expected = "/config/predefined" 197 | if xpath is not None: 198 | expected += xpath 199 | else: 200 | expected += objs[0].xpath_short() 201 | fw = _fw(*objs) 202 | 203 | getattr(fw.predefined, conf["multiple"])() 204 | 205 | fw.xapi.get.assert_called_once_with(expected, retry_on_peer=False) 206 | 207 | 208 | def test_get_multiple_objects(predef_multiple): 209 | xpath, objs = predef_multiple 210 | conf = PREDEFINED_CONFIG[objs[0].__class__] 211 | fw = _fw(*objs) 212 | 213 | getattr(fw.predefined, conf["multiple"])() 214 | 215 | for x in objs: 216 | data = getattr(fw.predefined, conf["var"]) 217 | assert x.uid in data 218 | assert data[x.uid].equal(x) 219 | 220 | 221 | def test_refresher_refresh_not_needed(predef_single): 222 | xpath, obj = predef_single 223 | conf = PREDEFINED_CONFIG[obj.__class__] 224 | fw = _fw() 225 | getattr(fw.predefined, conf["var"])[obj.uid] = obj 226 | 227 | ans = getattr(fw.predefined, conf["refresher"])(obj.uid) 228 | 229 | assert not fw.xapi.get.called 230 | assert ans.equal(obj) 231 | 232 | 233 | def test_refresher_when_refresh_is_needed(predef_single): 234 | xpath, obj = predef_single 235 | conf = PREDEFINED_CONFIG[obj.__class__] 236 | fw = _fw(obj) 237 | 238 | ans = getattr(fw.predefined, conf["refresher"])(obj.uid) 239 | 240 | assert fw.xapi.get.called == 1 241 | assert ans.equal(obj) 242 | 243 | 244 | def test_refresher_object_not_found_returns_none(predef_single): 245 | xpath, obj = predef_single 246 | conf = PREDEFINED_CONFIG[obj.__class__] 247 | fw = _fw() 248 | 249 | ans = getattr(fw.predefined, conf["refresher"])("foobar") 250 | 251 | assert fw.xapi.get.called == 1 252 | assert ans is None 253 | 254 | 255 | def test_refreshall_invokes_all_refresh_methods(): 256 | fw = _fw() 257 | 258 | for spec in PREDEFINED_CONFIG.values(): 259 | getattr(fw.predefined, spec["var"])["a"] = "a" 260 | 261 | funcs = [x for x in dir(fw.predefined) if x.startswith("refreshall_")] 262 | for x in funcs: 263 | setattr(fw.predefined, x, mock.Mock()) 264 | 265 | ans = fw.predefined.refreshall() 266 | 267 | assert ans is None 268 | for x in funcs: 269 | assert getattr(fw.predefined, x).called == 1 270 | 271 | for spec in PREDEFINED_CONFIG.values(): 272 | assert len(getattr(fw.predefined, spec["var"])) == 0 273 | -------------------------------------------------------------------------------- /examples/ensure_security_rule.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2017, Palo Alto Networks 4 | # 5 | # Permission to use, copy, modify, and/or distribute this software for any 6 | # purpose with or without fee is hereby granted, provided that the above 7 | # copyright notice and this permission notice appear in all copies. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | 17 | """ 18 | ensure_security_rule.py 19 | ========================== 20 | 21 | Ensure that specified security rule is on the firewall. 22 | 23 | Note: Please update the hostname / auth credentials variables before running. 24 | 25 | This script prints all the security rules connected to the firewall, then 26 | checks to make sure that the desired rule is present. If it is there, then 27 | the script ends. If not, it is created, and then a commit is performed. 28 | """ 29 | 30 | import sys 31 | 32 | import panos 33 | import panos.firewall 34 | import panos.policies 35 | 36 | 37 | HOSTNAME = "127.0.0.1" 38 | USERNAME = "admin" 39 | PASSWORD = "admin" 40 | 41 | 42 | def main(): 43 | # Before we begin, you'll need to use the pan-os-python documentation both 44 | # for this example and for any scripts you may write for yourself. The 45 | # docs can be found here: 46 | # 47 | # http://pan-os-python.readthedocs.io/en/latest/reference.html 48 | # 49 | # Here's the security rule parameters we want for our new rule. You can 50 | # check policies.SecurityRule to see all of the parameters you could 51 | # possibly give, but we'll just set a few for our example. 52 | desired_rule_params = { 53 | "name": "Block ssh", 54 | "description": "Prevent ssh usage", 55 | "fromzone": "any", 56 | "tozone": "any", 57 | "application": "ssh", 58 | "action": "deny", 59 | "log_end": True, 60 | } 61 | 62 | # First, let's create the firewall object that we want to modify. 63 | fw = panos.firewall.Firewall(HOSTNAME, USERNAME, PASSWORD) 64 | 65 | # You can determine the parent/child relationships by checking the 66 | # various "CHILDTYPES" of each object in the various modules. 67 | # 68 | # In our case, we see that the firewall.Firewall has a child of 69 | # policies.Rulebase, which in turn has a child type of 70 | # policies.SecurityPolicy. This means that in order to get all the 71 | # current security policies, we need to recreate this hierarchy in our 72 | # object structure. 73 | # 74 | # Security policies are attached to policies.Rulebase, and there is only 75 | # ever one unnamed rulebase in PANOS. So we have two options at this 76 | # point: 1) create our own Rulebase, attach it to the firewall object, and 77 | # use that to refresh only the security policies, or 2) get all the 78 | # Rulebase objects (and children) from the firewall and then work with 79 | # that. Since we only care about the security policies, we'll go with the 80 | # former option (which will also make our script faster). 81 | # 82 | # Now, let's create our unnamed Rulebase object. 83 | rulebase = panos.policies.Rulebase() 84 | 85 | # Next, we attach it to our firewall object. 86 | fw.add(rulebase) 87 | 88 | # Then we can refresh just the security policies. All "refreshall" 89 | # functions take the parent as a first parameter, and return a list of 90 | # what's on the firewall. In our case, the parent is our rulebase 91 | # object, so we'll use that as the first parameter, and we'll save the 92 | # current security policies to a new variable: current_security_rules. 93 | current_security_rules = panos.policies.SecurityRule.refreshall(rulebase) 94 | 95 | # You'll notice that we never called any "login()" or similar function 96 | # before we refreshed. This is because pan-os-python does the API key 97 | # retrieval for you when you attempt to do something that would require 98 | # access to the live device. In our case, this was the above call 99 | # to "refreshall()". 100 | # 101 | # Since we're looking for the existance of a single policy, let's create 102 | # a boolean variable (or flag) to keep track of this for us. 103 | is_present = False 104 | 105 | # We're about to loop over all of the rules, but let's print a quick 106 | # one liner letting us know how many security rules we found. 107 | print("Current security rule(s) ({0} found):".format(len(current_security_rules))) 108 | 109 | # Now we're ready to check all the security policies that we got back from 110 | # the firewall. We'll loop over each one, one by one, and print out the 111 | # name of the policy. 112 | for rule in current_security_rules: 113 | print("- {0}".format(rule.name)) 114 | # Next, we need to check and see if this name matches the name of 115 | # the security policy we want to ensure the existance of. If the names 116 | # match, then we'll set our flag to True. 117 | if rule.name == desired_rule_params["name"]: 118 | is_present = True 119 | 120 | # To format the output a bit better, we'll just print an empty line here 121 | # to help distinguish the end of printing all the rules from the logic 122 | # of the rest of this script. 123 | print() 124 | 125 | # At this point, we've looped over all the rules on the firewall. So we 126 | # check our flag to see if it was set. If it was set, then print out a 127 | # message saying that we found the rule, then exit out of this function. 128 | if is_present: 129 | print('Rule "{0}" already exists'.format(desired_rule_params["name"])) 130 | return 131 | 132 | # If the function got to this point, then the rule is not present, so we 133 | # print out a little message saying as much, then continue on! 134 | print('Rule "{0}" not present, adding it'.format(desired_rule_params["name"])) 135 | 136 | # At this point, we know the rule doesn't exist, so let's create it! Doing 137 | # that is a three step process. 138 | # 139 | # First is to make all necessary new object(s). In our example, we only 140 | # have one object to create, which is the rule itself. 141 | new_rule = panos.policies.SecurityRule(**desired_rule_params) 142 | 143 | # Second is to configure the object hierarchy using the .add() method. As 144 | # we already know, security rules are children of the rulebase, which in 145 | # turn are children of the firewall. Our object hierarchy is already setup 146 | # as "firewall > rulebase", so we need to add our new rule to the rulebase. 147 | rulebase.add(new_rule) 148 | 149 | # Last is to invoke the .create() function to create it. Since it is the 150 | # new security rule we want to create and not the rulebase, we will 151 | # invoke create() on the new rule and not the rule base. When we call 152 | # create(), both the object we are calling it on and all the children 153 | # connected will be created. In our example, there are no children 154 | # attached to the security rule, so it's just the rule itself that gets 155 | # created. 156 | print("Creating rule...") 157 | new_rule.create() 158 | print("Done!") 159 | 160 | # Now we just have to commit. I will ask commit() to wait for the commit 161 | # to finish completely before executing the next line of my script by 162 | # using "sync=True". 163 | print("Performing commit...") 164 | fw.commit(sync=True) 165 | 166 | # As a further exercise, you could try modifying this script: we are 167 | # currently only checking that the names match, but what about the 168 | # contents of the rule? Modify this script to verify that the contents 169 | # of the rule are also as expected. If the rule is different from what 170 | # is desired, update the rule, and apply & commit it to the firewall. 171 | # 172 | # At this point, we've finished our script! 173 | print("Done!") 174 | 175 | 176 | if __name__ == "__main__": 177 | # This script doesn't take command line arguments. If any are passed in, 178 | # then print out the script's docstring and exit. 179 | if len(sys.argv) != 1: 180 | print(__doc__) 181 | else: 182 | # No CLI args, so run the main function. 183 | main() 184 | -------------------------------------------------------------------------------- /examples/bulk_subinterfaces.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2017, Palo Alto Networks 4 | # 5 | # Permission to use, copy, modify, and/or distribute this software for any 6 | # purpose with or without fee is hereby granted, provided that the above 7 | # copyright notice and this permission notice appear in all copies. 8 | # 9 | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | 17 | """ 18 | bulk_subinterfaces.py 19 | ===================== 20 | 21 | Use bulk operations to create / delete hundreds of firewall interfaces. 22 | 23 | NOTE: Please update the hostname and auth credentials variables 24 | before running. 25 | 26 | The purpose of this script is to use and explain both the bulk operations 27 | as it relates to subinterfaces as well as the new function that organizes 28 | objects into vsys. This script will show how the new bulk operations 29 | correctly handle when subinterface objects are in separate vsys trees. 30 | 31 | """ 32 | 33 | import datetime 34 | import random 35 | import sys 36 | 37 | from panos import device, firewall, network 38 | 39 | HOSTNAME = "127.0.0.1" 40 | USERNAME = "admin" 41 | PASSWORD = "admin" 42 | INTERFACE = "ethernet1/5" 43 | 44 | 45 | def main(): 46 | # Before we begin, you'll need to use the pan-os-python documentation both 47 | # for this example and for any scripts you may write for yourself. The 48 | # docs can be found here: 49 | # 50 | # http://pan-os-python.readthedocs.io/en/latest/reference.html 51 | # 52 | # First, let's create the firewall object that we want to modify. 53 | fw = firewall.Firewall(HOSTNAME, USERNAME, PASSWORD) 54 | print("Firewall system info: {0}".format(fw.refresh_system_info())) 55 | 56 | print("Desired interface: {0}".format(INTERFACE)) 57 | 58 | # Sanity Check #1: the intent here is that the interface we 59 | # specified above should not already be in use. If the interface is 60 | # already in use, then just quit out. 61 | print("Making sure interface is not currently in use...") 62 | interfaces = network.EthernetInterface.refreshall(fw, add=False) 63 | for eth in interfaces: 64 | if eth.name == INTERFACE: 65 | print( 66 | "Interface {0} already in use! Please choose another".format(INTERFACE) 67 | ) 68 | return 69 | 70 | # Sanity Check #2: this has to be a multi-vsys system. So let's make 71 | # sure that we have multiple vsys to work with. If there is only one 72 | # vsys, quit out. 73 | # 74 | # Pulling the entire vsys config from each vsys is going to be large amount 75 | # of XML, so we'll specify that we only need the names of the different 76 | # vsys, not their entire subtrees. 77 | vsys_list = device.Vsys.refreshall(fw, name_only=True) 78 | print("Found the following vsys: {0}".format(vsys_list)) 79 | if len(vsys_list) < 2: 80 | print("Only {0} vsys present, need 2 or more.".format(len(vsys_list))) 81 | return 82 | 83 | # Let's make our base interface that we're going to make subinterfaces 84 | # out of. 85 | print("Creating base interface {0} in layer2 mode".format(INTERFACE)) 86 | base = network.EthernetInterface(INTERFACE, "layer2") 87 | 88 | # Like normal, after creating the object, we need to add it to the 89 | # firewall, then finally invoke "create()" to create it. 90 | fw.add(base) 91 | base.create() 92 | 93 | # Now let's go ahead and make all of our subinterfaces. 94 | eth = None 95 | for tag in range(1, 601): 96 | name = "{0}.{1}".format(INTERFACE, tag) 97 | eth = network.Layer2Subinterface(name, tag) 98 | # Choose one of the vsys at random to put it into. 99 | vsys = random.choice(vsys_list) 100 | # Now add the subinterface to that randomly chosen vsys. 101 | vsys.add(eth) 102 | 103 | # You'll notice that we didn't invoke "create()" on the subinterfaces like 104 | # you would expect. This is because we're going to use the bulk create 105 | # function to create all of the subinterfaces in one shot, which has huge 106 | # performance gains from doing "create()" on each subinterface one-by-one. 107 | # 108 | # The function we'll use is "create_similar()". Create similar is saying, 109 | # "I want to create all objects similar to this one in my entire pan-os-python 110 | # object tree." In this case, since we'd be invoking it on a subinterface 111 | # of INTERFACE (our variable above), we are asking pan-os-python to create all 112 | # subinterfaces of INTERFACE, no matter which vsys it exists in. 113 | # 114 | # We just need any subinterface to do this. Since our last subinterface 115 | # was saved to the "eth" variable in the above loop, we can just use that 116 | # to invoke "create_similar()". 117 | print("Creating subinterfaces...") 118 | start = datetime.datetime.now() 119 | eth.create_similar() 120 | print("Creating subinterfaces took: {0}".format(datetime.datetime.now() - start)) 121 | 122 | # Now let's explore updating them. Let's say this is a completely 123 | # different script, and we want to update all of the subinterfaces 124 | # for INTERFACE. Since this is a completely new script, we don't have any 125 | # information other than the firewall and the interface INTERFACE. So 126 | # let's start from scratch at this point, and remake the firewall object 127 | # and connect. 128 | print("\n--------\n") 129 | fw = firewall.Firewall(HOSTNAME, USERNAME, PASSWORD) 130 | print("Firewall system info: {0}".format(fw.refresh_system_info())) 131 | 132 | print("Desired interface: {0}".format(INTERFACE)) 133 | 134 | # Make the base interface object and connect it to our pan-os-python tree. 135 | base = network.EthernetInterface(INTERFACE, "layer2") 136 | fw.add(base) 137 | 138 | # Now let's get all the subinterfaces for INTERFACE. Since our firewall's 139 | # default vsys is "None", this will get all subinterfaces of INTERFACE, 140 | # regardless of which vsys it exists in. 141 | print("Refreshing subinterfaces...") 142 | subinterfaces = network.Layer2Subinterface.refreshall(base) 143 | print("Found {0} subinterfaces".format(len(subinterfaces))) 144 | 145 | # Now let's go ahead and update all of them. 146 | for eth in subinterfaces: 147 | eth.comment = "Tagged {0} and in vsys {1}".format(eth.tag, eth.vsys) 148 | 149 | # Now that we have updated all of the subinterfaces, we need to save 150 | # the changes to the firewall. But hold on a second, the vsys for these 151 | # subinterfaces is currently "None". We first need to organize these 152 | # subinterfaces into the vsys they actually exist in before we can 153 | # apply these changes to the firewall. 154 | # 155 | # This is where you can use the function "organize_into_vsys()". This 156 | # takes all objects currently attached to your pan-os-python object tree 157 | # and organizes them into the vsys they belong to. 158 | # 159 | # We haven't gotten the current vsys yet (this is a new script, remember), 160 | # but the function can take care of that for us. So let's just invoke it 161 | # to organize our pan-os-python object tree into vsys. 162 | print("Organizing subinterfaces into vsys...") 163 | fw.organize_into_vsys() 164 | 165 | # Now we're ready to save our changes. We'll use "apply_similar()", 166 | # and it behaves similarly to "create_similar()" in that you can invoke 167 | # it from any subinterface of INTERFACE and it will apply all of the 168 | # changes to subinterfaces of INTERFACE only. 169 | # 170 | # We just need one subinterface to invoke this function. Again, we'll 171 | # simply use the subinterface currently saved in the "eth" variable 172 | # from our update loop we did just above. 173 | # 174 | # NOTE: As an "apply()" function, apply does a replace of config, not 175 | # a simple update. So you must be careful that all other objects are 176 | # currently attached to your pan-os-python object tree when using apply 177 | # functions. In our case, we have already refreshed all layer2 178 | # subinterfaces, and we are the only ones working with INTERFACE, so we 179 | # are safe to use this function. 180 | print("Updating all subinterfaces...") 181 | start = datetime.datetime.now() 182 | eth.apply_similar() 183 | print("Updating subinterfaces took: {0}".format(datetime.datetime.now() - start)) 184 | 185 | # Finally, all that's left is to delete all of the subinterfaces. This 186 | # is just like you think: we first need to refresh all of the 187 | # subinterfaces of INTERFACE, organize them into their appropriate vsys, 188 | # then invoke "delete_similar()" to delete everything. 189 | print("Deleting all subinterfaces...") 190 | start = datetime.datetime.now() 191 | eth.delete_similar() 192 | print("Deleting subinterfaces took: {0}".format(datetime.datetime.now() - start)) 193 | 194 | # Lastly, let's just delete the base interface INTERFACE. 195 | print("Deleting base interface...") 196 | base.delete() 197 | 198 | # And now we're done! If performance is a bottleneck in your automation, 199 | # or dealing with vsys is troublesome, consider using the vsys organizing 200 | # and/or bulk functions! 201 | print("Done!") 202 | 203 | 204 | if __name__ == "__main__": 205 | # This script doesn't take command line arguments. If any are passed in, 206 | # then print out the script's docstring and exit. 207 | if len(sys.argv) != 1: 208 | print(__doc__) 209 | else: 210 | # No CLI args, so run the main function. 211 | main() 212 | -------------------------------------------------------------------------------- /tests/test_PanOScomp.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from panos import PanOSVersion 4 | 5 | 6 | @pytest.mark.parametrize( 7 | "panos1, panos2", 8 | [ 9 | ("6.1.0", "6.1.0"), 10 | ("0.0.0", "0.0.0"), 11 | ("7.3.4-h1", "7.3.4-h1"), 12 | ("3.4.2-c5", "3.4.2-c5"), 13 | ("4.4.4-b8", "4.4.4-b8"), 14 | ], 15 | ) 16 | def test_gen_eq(panos1, panos2): 17 | x = PanOSVersion(panos1) 18 | y = PanOSVersion(panos2) 19 | assert x == y 20 | assert (x != y) == False 21 | assert x >= y 22 | assert x <= y 23 | assert (x > y) == False 24 | assert (x < y) == False 25 | 26 | 27 | @pytest.mark.parametrize( 28 | "panos1, panos2", 29 | [ 30 | ("6.1.1", "6.2.0"), 31 | ("0.0.0", "0.0.1"), 32 | ("0.0.9-h18", "9.0.0-c1"), 33 | ("7.3.4-h1", "8.1.0-b2"), 34 | ("3.4.2-c5", "3.4.2-c7"), 35 | ("4.4.4-b8", "4.4.4-b10"), 36 | ("3.3.3-h3", "3.3.3-h5"), 37 | ("2.3.4-h3", "2.3.5-h3"), 38 | ("1.8.7-c3", "1.8.7-b3"), 39 | ("3.2.1-c8", "3.2.1"), 40 | ("4.5.3-c13", "4.5.3-h13"), 41 | ("4.5.3-c13", "4.5.3-h15"), 42 | ("3.6.6-b4", "3.6.6-h4"), 43 | ("3.6.6-b4", "3.6.6-h6"), 44 | ("7.0.0", "7.0.0-h2"), 45 | ("7.0.0-b3", "7.0.0"), 46 | ("7.0.0-c7", "7.0.0"), 47 | ("2.3.3-b3", "2.3.4"), 48 | ("2.3.3-c8", "2.3.4"), 49 | ("2.3.3-h7", "2.3.4"), 50 | ("3.4.2-c3", "3.4.3-h1"), 51 | ("3.2.1-b8", "3.2.2-h1"), 52 | ("4.2.2-h10", "4.2.2-h11"), 53 | ("5.3.3-c4", "5.3.3-h1"), 54 | ("5.4.2-b8", "5.4.2-h1"), 55 | ], 56 | ) 57 | def test_comp(panos1, panos2): 58 | x = PanOSVersion(panos1) 59 | y = PanOSVersion(panos2) 60 | assert y > x 61 | assert y >= x 62 | assert x < y 63 | assert x <= y 64 | assert x != y 65 | assert (x == y) == False 66 | 67 | 68 | @pytest.mark.parametrize( 69 | "panos1, panos2, expected", 70 | [ 71 | ("3.4.1", "3.4.1", True), 72 | ("4.2.6-b8", "4.2.6-b8", True), 73 | ("7.1.9-c3", "7.1.9-c3", True), 74 | ("4.5.6-h8", "4.5.6-h8", True), 75 | ("4.2.1", "5.2.1", False), 76 | ("4.2.1", "4.2.2", False), 77 | ("4.2.1", "4.3.1", False), 78 | ("3.2.6", "3.2.6-b7", False), 79 | ("4.5.6", "4.5.6-h8", False), 80 | ("7.1.9", "7.1.9-c3", False), 81 | ("7.1.9-b7", "7.1.9-c3", False), 82 | ("4.5.6-b4", "4.5.6-h8", False), 83 | ("4.5.6-c6", "4.5.6-h8", False), 84 | ("3.2.1-h3", "3.2.1-h2", False), 85 | ], 86 | ) 87 | def test_eq(panos1, panos2, expected): 88 | x = PanOSVersion(panos1) 89 | y = PanOSVersion(panos2) 90 | assert (x == y) == expected 91 | 92 | 93 | @pytest.mark.parametrize( 94 | "panos1, panos2, expected", 95 | [ 96 | ("3.4.1", "3.4.1", False), 97 | ("4.2.6-b8", "4.2.6-b8", False), 98 | ("7.1.9-c3", "7.1.9-c3", False), 99 | ("4.5.6-h8", "4.5.6-h8", False), 100 | ("4.2.1", "5.2.1", True), 101 | ("4.2.1", "4.2.2", True), 102 | ("4.2.1", "4.3.1", True), 103 | ("3.2.6", "3.2.6-b7", True), 104 | ("4.5.6", "4.5.6-h8", True), 105 | ("7.1.9", "7.1.9-c3", True), 106 | ("7.1.9-b7", "7.1.9-c3", True), 107 | ("4.5.6-b4", "4.5.6-h8", True), 108 | ("4.5.6-c6", "4.5.6-h8", True), 109 | ("3.2.1-h3", "3.2.1-h2", True), 110 | ], 111 | ) 112 | def test_neq(panos1, panos2, expected): 113 | x = PanOSVersion(panos1) 114 | y = PanOSVersion(panos2) 115 | assert (x != y) == expected 116 | 117 | 118 | @pytest.mark.parametrize( 119 | "panos1, panos2, expected", 120 | [ 121 | ("0.9.9", "1.0.0", True), 122 | ("3.8.7", "3.8.8", True), 123 | ("6.1.1", "6.2.0", True), 124 | ("0.0.0", "0.0.1", True), 125 | ("0.0.9-h18", "9.0.0-c1", True), 126 | ("7.3.4-b2", "8.1.0-h1", True), 127 | ("3.4.2-c5", "3.4.2-c3", False), 128 | ("4.4.4-b8", "4.4.4-b10", True), 129 | ("3.3.3-h3", "3.3.3-h5", True), 130 | ("2.3.4-h3", "2.2.5-h3", False), 131 | ("1.8.7-c3", "1.8.7-b3", True), 132 | ("3.2.1-c8", "3.2.1", True), 133 | ("4.5.3-h13", "4.5.3-c13", False), 134 | ("4.5.3-c13", "4.5.3-h15", True), 135 | ("3.6.6-b4", "3.6.6-h4", True), 136 | ("3.6.6-b4", "3.6.6-h6", True), 137 | ("7.0.0", "7.0.0-b2", False), 138 | ("7.0.0-h3", "7.0.0", False), 139 | ("7.0.0-c7", "7.0.0", True), 140 | ("2.3.3-b3", "2.3.4", True), 141 | ("2.3.3-c8", "2.3.4", True), 142 | ("2.3.3-h7", "2.3.4", True), 143 | ("3.4.2-c3", "3.4.3-h1", True), 144 | ("3.2.1-b8", "3.2.2-h1", True), 145 | ("4.2.2-h10", "4.2.2-h11", True), 146 | ("5.3.3-c4", "5.3.3-h1", True), 147 | ("5.4.2-b8", "5.4.2-h1", True), 148 | ("3.4.1", "3.4.1", False), 149 | ("4.2.6-b8", "4.2.6-b8", False), 150 | ("7.1.9-c3", "7.1.9-c3", False), 151 | ("4.5.6-h8", "4.5.6-h8", False), 152 | ("9.9.9", "0.0.0", False), 153 | ("5.4.3-b9", "5.4.2-b10", False), 154 | ("3.7.8-b1", "3.7.8-c3", False), 155 | ], 156 | ) 157 | def test_lt(panos1, panos2, expected): 158 | x = PanOSVersion(panos1) 159 | y = PanOSVersion(panos2) 160 | assert (x < y) == expected 161 | 162 | 163 | @pytest.mark.parametrize( 164 | "panos1, panos2, expected", 165 | [ 166 | ("0.9.9", "1.0.0", False), 167 | ("3.8.7", "3.6.8", True), 168 | ("6.1.1", "6.2.0", False), 169 | ("0.1.0", "0.0.1", True), 170 | ("0.0.9-h18", "9.0.0-c1", False), 171 | ("7.3.4-b2", "8.1.0-h1", False), 172 | ("3.4.2-c5", "3.4.2-c3", True), 173 | ("4.4.4-b8", "4.4.4-b4", True), 174 | ("3.3.3-h3", "3.3.3-h1", True), 175 | ("2.3.4-h3", "2.2.5-h7", True), 176 | ("1.8.7-b3", "1.8.7-c3", True), 177 | ("3.2.1-c8", "3.2.1", False), 178 | ("4.5.3-h13", "4.5.3-c13", True), 179 | ("4.5.3-c13", "4.5.3-h15", False), 180 | ("3.6.6-b4", "3.6.6-c4", True), 181 | ("3.6.6-b4", "3.6.6-h6", False), 182 | ("7.0.0", "7.0.0-b2", True), 183 | ("7.0.0-h3", "7.0.0", True), 184 | ("7.0.0-c7", "7.0.0", False), 185 | ("2.3.3-h3", "2.3.4", False), 186 | ("2.3.5-c8", "2.3.4", True), 187 | ("2.3.3-h7", "2.3.4", False), 188 | ("3.4.4-c3", "3.4.3-h1", True), 189 | ("3.2.1-b8", "3.2.2-h1", False), 190 | ("4.2.2-h10", "4.2.2-h9", True), 191 | ("5.3.3-h4", "5.3.3-c1", True), 192 | ("5.4.2-b8", "5.4.2-h1", False), 193 | ("3.4.1", "3.4.1", False), 194 | ("4.2.6-b8", "4.2.6-b8", False), 195 | ("7.1.9-c3", "7.1.9-c3", False), 196 | ("4.5.6-h8", "4.5.6-h8", False), 197 | ("9.9.9", "0.0.0", True), 198 | ("5.4.2-b9", "5.4.2-c12", True), 199 | ("3.7.8-b1", "3.7.8-c3", True), 200 | ], 201 | ) 202 | def test_gt(panos1, panos2, expected): 203 | x = PanOSVersion(panos1) 204 | y = PanOSVersion(panos2) 205 | assert (x > y) == expected 206 | 207 | 208 | @pytest.mark.parametrize( 209 | "panos1, panos2, expected", 210 | [ 211 | ("0.9.9", "1.0.0", True), 212 | ("3.8.7", "3.8.8", True), 213 | ("6.1.1", "6.2.0", True), 214 | ("0.0.0", "0.0.1", True), 215 | ("0.0.9-h18", "9.0.0-c1", True), 216 | ("7.3.4-b2", "8.1.0-h1", True), 217 | ("3.4.2-c5", "3.4.2-c3", False), 218 | ("4.4.4-b8", "4.4.4-b10", True), 219 | ("3.3.3-h3", "3.3.3-h5", True), 220 | ("2.3.4-h3", "2.2.5-h3", False), 221 | ("1.8.7-c3", "1.8.7-b3", True), 222 | ("3.2.1-c8", "3.2.1", True), 223 | ("4.5.3-h13", "4.5.3-c13", False), 224 | ("4.5.3-c13", "4.5.3-h15", True), 225 | ("3.6.6-b4", "3.6.6-h4", True), 226 | ("3.6.6-b4", "3.6.6-h6", True), 227 | ("7.0.0", "7.0.0-b2", False), 228 | ("7.0.0-h3", "7.0.0", False), 229 | ("7.0.0", "7.0.0-c7", False), 230 | ("2.3.3-b3", "2.3.4", True), 231 | ("2.3.3-c8", "2.3.4", True), 232 | ("2.3.3-h7", "2.3.4", True), 233 | ("3.4.2-c3", "3.4.3-h1", True), 234 | ("3.2.1-b8", "3.2.2-h1", True), 235 | ("4.2.2-h10", "4.2.2-h11", True), 236 | ("5.3.3-c4", "5.3.3-h1", True), 237 | ("5.4.2-b8", "5.4.2-h1", True), 238 | ("3.4.1", "3.4.1", True), 239 | ("4.2.6-b8", "4.2.6-b8", True), 240 | ("7.1.9-c3", "7.1.9-c3", True), 241 | ("4.5.6-h8", "4.5.6-h8", True), 242 | ("9.9.9", "0.0.0", False), 243 | ("5.4.3-b9", "5.4.2-b10", False), 244 | ("3.7.8-b1", "3.7.8-c3", False), 245 | ], 246 | ) 247 | def test_le(panos1, panos2, expected): 248 | x = PanOSVersion(panos1) 249 | y = PanOSVersion(panos2) 250 | assert (x <= y) == expected 251 | 252 | 253 | @pytest.mark.parametrize( 254 | "panos1, panos2, expected", 255 | [ 256 | ("0.9.9", "1.0.0", False), 257 | ("3.8.7", "3.6.8", True), 258 | ("6.1.1", "6.2.0", False), 259 | ("0.1.0", "0.0.1", True), 260 | ("0.0.9-h18", "9.0.0-c1", False), 261 | ("7.3.4-b2", "8.1.0-h1", False), 262 | ("3.4.2-c5", "3.4.2-c3", True), 263 | ("4.4.4-b8", "4.4.4-b4", True), 264 | ("3.3.3-h3", "3.3.3-h1", True), 265 | ("2.3.4-h3", "2.2.5-h7", True), 266 | ("1.8.7-b3", "1.8.7-c3", True), 267 | ("3.2.1-c8", "3.2.1", False), 268 | ("4.5.3-h13", "4.5.3-c13", True), 269 | ("4.5.3-c13", "4.5.3-h15", False), 270 | ("3.6.6-b4", "3.6.6-c4", True), 271 | ("3.6.6-b4", "3.6.6-h6", False), 272 | ("7.0.0", "7.0.0-b2", True), 273 | ("7.0.0-h3", "7.0.0", True), 274 | ("7.0.0-c7", "7.0.0", False), 275 | ("2.3.3-h3", "2.3.4", False), 276 | ("2.3.5-c8", "2.3.4", True), 277 | ("2.3.3-h7", "2.3.4", False), 278 | ("3.4.4-c3", "3.4.3-h1", True), 279 | ("3.2.1-b8", "3.2.2-h1", False), 280 | ("4.2.2-h10", "4.2.2-h9", True), 281 | ("5.3.3-h4", "5.3.3-c1", True), 282 | ("5.4.2-b8", "5.4.2-h1", False), 283 | ("3.4.1", "3.4.1", True), 284 | ("4.2.6-b8", "4.2.6-b8", True), 285 | ("7.1.9-c3", "7.1.9-c3", True), 286 | ("4.5.6-h8", "4.5.6-h8", True), 287 | ("9.9.9", "0.0.0", True), 288 | ("5.4.2-b9", "5.4.2-c12", True), 289 | ("3.7.8-b1", "3.7.8-c3", True), 290 | ], 291 | ) 292 | def test_ge(panos1, panos2, expected): 293 | x = PanOSVersion(panos1) 294 | y = PanOSVersion(panos2) 295 | assert (x >= y) == expected 296 | --------------------------------------------------------------------------------