├── .coveragerc ├── .flake8 ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── github-actions.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── docs └── sample_responses.txt ├── examples └── plugins │ ├── README.md │ ├── csv_inv.py │ ├── labs_tsv.py │ └── retitle_cmd.py ├── pyproject.toml ├── requirements.txt ├── requirements_dev.txt ├── setup.cfg ├── setup.py ├── test-requirements.txt ├── tests ├── __init__.py └── v2 │ ├── __init__.py │ ├── bad_plugins.py │ ├── cluster.py │ ├── console.py │ ├── definitions.py │ ├── down.py │ ├── extract.py │ ├── generate_ansible.py │ ├── generate_nso.py │ ├── generate_pyats.py │ ├── good_plugins.py │ ├── groups.py │ ├── id.py │ ├── ls.py │ ├── mocks │ ├── __init__.py │ ├── api.py │ ├── github.py │ └── nso.py │ ├── nodes.py │ ├── plugins_bad_cmd │ └── cmd_plug_bad_run.py │ ├── plugins_bad_gen │ └── generator_plugin_bad.py │ ├── plugins_bad_viewer │ └── viewer_plugin_bad.py │ ├── plugins_good │ ├── cmd_plug.py │ ├── generator_plugin.py │ └── viewer_plugin.py │ ├── pull.py │ ├── rm.py │ ├── save.py │ ├── search.py │ ├── ssh.py │ ├── start.py │ ├── static │ ├── fake_image_definitions.yaml │ ├── fake_repo_bad_topology.yaml │ ├── fake_repo_topology.yaml │ ├── node_defs_list_output.txt │ ├── node_defs_list_output_cml22.txt │ ├── response_get_node_defs.json │ └── response_get_node_defs_cml22.json │ ├── stop.py │ ├── telnet.py │ ├── test_cli.py │ ├── tmux.py │ ├── up.py │ ├── use.py │ ├── users.py │ └── wipe.py └── virl ├── __init__.py ├── about.py ├── api ├── __init__.py ├── api.py ├── cml.py ├── credentials.py ├── github.py ├── nso.py └── plugin.py ├── cli ├── __init__.py ├── clear │ ├── __init__.py │ └── commands.py ├── cluster │ ├── __init__.py │ └── info │ │ ├── __init__.py │ │ └── commands.py ├── cockpit │ ├── __init__.py │ └── commands.py ├── command │ ├── __init__.py │ └── commands.py ├── console │ ├── __init__.py │ └── commands.py ├── definitions │ ├── __init__.py │ ├── images │ │ ├── __init__.py │ │ ├── export │ │ │ ├── __init__.py │ │ │ └── commands.py │ │ ├── iimport │ │ │ ├── __init__.py │ │ │ ├── definition │ │ │ │ ├── __init__.py │ │ │ │ └── commands.py │ │ │ └── image_file │ │ │ │ ├── __init__.py │ │ │ │ └── commands.py │ │ └── ls │ │ │ ├── __init__.py │ │ │ └── commands.py │ └── nodes │ │ ├── __init__.py │ │ ├── export │ │ ├── __init__.py │ │ └── commands.py │ │ ├── ls │ │ ├── __init__.py │ │ └── commands.py │ │ └── nimport │ │ ├── __init__.py │ │ └── commands.py ├── down │ ├── __init__.py │ └── commands.py ├── extract │ ├── __init__.py │ └── commands.py ├── generate │ ├── __init__.py │ ├── ansible │ │ ├── __init__.py │ │ └── commands.py │ ├── nso │ │ ├── __init__.py │ │ └── commands.py │ └── pyats │ │ ├── __init__.py │ │ └── commands.py ├── groups │ ├── __init__.py │ ├── create │ │ ├── __init__.py │ │ └── commands.py │ ├── delete │ │ ├── __init__.py │ │ └── commands.py │ ├── ls │ │ ├── __init__.py │ │ └── commands.py │ └── update │ │ ├── __init__.py │ │ └── commands.py ├── id │ ├── __init__.py │ └── commands.py ├── license │ ├── __init__.py │ ├── deregister │ │ ├── __init__.py │ │ └── commands.py │ ├── features │ │ ├── __init__.py │ │ ├── show │ │ │ ├── __init__.py │ │ │ └── commands.py │ │ └── update │ │ │ ├── __init__.py │ │ │ └── commands.py │ ├── register │ │ ├── __init__.py │ │ └── commands.py │ ├── renew │ │ ├── __init__.py │ │ ├── authorization │ │ │ ├── __init__.py │ │ │ └── commands.py │ │ └── registration │ │ │ ├── __init__.py │ │ │ └── commands.py │ └── show │ │ ├── __init__.py │ │ └── commands.py ├── ls │ ├── __init__.py │ └── commands.py ├── main.py ├── nodes │ ├── __init__.py │ └── commands.py ├── pull │ ├── __init__.py │ └── commands.py ├── rm │ ├── __init__.py │ └── commands.py ├── save │ ├── __init__.py │ └── commands.py ├── search │ ├── __init__.py │ └── commands.py ├── ssh │ ├── __init__.py │ └── commands.py ├── start │ ├── __init__.py │ └── commands.py ├── stop │ ├── __init__.py │ └── commands.py ├── telnet │ ├── __init__.py │ └── commands.py ├── tmux │ ├── __init__.py │ └── commands.py ├── ui │ ├── __init__.py │ └── commands.py ├── up │ ├── __init__.py │ └── commands.py ├── use │ ├── __init__.py │ └── commands.py ├── users │ ├── __init__.py │ ├── create │ │ ├── __init__.py │ │ └── commands.py │ ├── delete │ │ ├── __init__.py │ │ └── commands.py │ ├── ls │ │ ├── __init__.py │ │ └── commands.py │ └── update │ │ ├── __init__.py │ │ └── commands.py ├── version │ ├── __init__.py │ └── commands.py ├── views │ ├── __init__.py │ ├── cluster │ │ ├── __init__.py │ │ └── cluster_views.py │ ├── console │ │ ├── __init__.py │ │ └── console_views.py │ ├── generate │ │ ├── __init__.py │ │ └── nso │ │ │ ├── __init__.py │ │ │ └── sync_result.py │ ├── groups │ │ ├── __init__.py │ │ └── group_views.py │ ├── images │ │ ├── __init__.py │ │ └── image_views.py │ ├── labs │ │ ├── __init__.py │ │ └── lab_views.py │ ├── license │ │ ├── __init__.py │ │ └── license_views.py │ ├── node_defs │ │ ├── __init__.py │ │ └── node_def_views.py │ ├── nodes │ │ ├── __init__.py │ │ └── node_views.py │ ├── search │ │ ├── __init__.py │ │ └── views.py │ └── users │ │ ├── __init__.py │ │ └── user_views.py └── wipe │ ├── __init__.py │ ├── lab │ ├── __init__.py │ └── commands.py │ └── node │ ├── __init__.py │ └── commands.py ├── generators ├── __init__.py ├── ansible_inventory.py ├── nso_payload.py └── pyats_testbed.py ├── helpers.py └── templates ├── ansible ├── inventory_ini_template.j2 └── inventory_template.j2 ├── nso └── xml_payload.j2 └── pyats └── testbed_yaml.j2 /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | # omit anything in a .local directory anywhere 4 | */.local/* 5 | */templates/* 6 | */static/* 7 | */examples/* 8 | tests/* 9 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = cmlutils,venv*,.git,.eggs,__pycache__,docs/source/conf.py,old,build,dist 3 | ignore = E731 4 | max-complexity = 25 5 | max-line-length = 140 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/workflows/github-actions.yml: -------------------------------------------------------------------------------- 1 | name: cmlutils CI 2 | on: [push, pull_request, workflow_dispatch] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | python-version: 10 | - "3.8" 11 | - "3.9" 12 | - "3.10" 13 | - "3.11" 14 | - "3.12" 15 | env: 16 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Setup Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | - name: Upgrade pip 24 | run: | 25 | pip install -U pip 26 | - name: Install dependencies 27 | run: | 28 | pip install -r requirements.txt 29 | pip install -r test-requirements.txt 30 | - name: Execute tests 31 | run: | 32 | make lint 33 | make coverage 34 | pip freeze 35 | - name: Run coveralls 36 | run: | 37 | coveralls --service=github 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dev stuff 2 | *.code-workspace 3 | scratch* 4 | .vscode/ 5 | .idea 6 | 7 | # virl stuff 8 | .virl/ 9 | .virlrc 10 | topology.yaml 11 | topology.virl 12 | 13 | # Byte-compiled / optimized / DLL files 14 | __pycache__/ 15 | *.py[cod] 16 | *$py.class 17 | 18 | # C extensions 19 | *.so 20 | 21 | # Distribution / packaging 22 | .Python 23 | env/ 24 | build/ 25 | develop-eggs/ 26 | dist/ 27 | downloads/ 28 | eggs/ 29 | .eggs/ 30 | lib/ 31 | lib64/ 32 | parts/ 33 | sdist/ 34 | var/ 35 | wheels/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Installer logs 47 | pip-log.txt 48 | pip-delete-this-directory.txt 49 | 50 | # Unit test / coverage reports 51 | htmlcov/ 52 | .tox/ 53 | .coverage 54 | .coverage.* 55 | .cache 56 | nosetests.xml 57 | coverage.xml 58 | *.cover 59 | .hypothesis/ 60 | 5f0d96.yaml 61 | default_inventory.* 62 | default_testbed.yaml 63 | 64 | # Translations 65 | *.mo 66 | *.pot 67 | 68 | # Django stuff: 69 | *.log 70 | local_settings.py 71 | 72 | # Flask stuff: 73 | instance/ 74 | .webassets-cache 75 | 76 | # Scrapy stuff: 77 | .scrapy 78 | 79 | # Sphinx documentation 80 | docs/_build/ 81 | 82 | # PyBuilder 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # pyenv 89 | .python-version 90 | 91 | # celery beat schedule file 92 | celerybeat-schedule 93 | 94 | # SageMath parsed files 95 | *.sage.py 96 | 97 | # dotenv 98 | .env 99 | 100 | # virtualenv 101 | .venv 102 | venv*/ 103 | ENV/ 104 | cmlutils/ 105 | 106 | # Spyder project settings 107 | .spyderproject 108 | .spyproject 109 | 110 | # Rope project settings 111 | .ropeproject 112 | 113 | # mkdocs documentation 114 | /site 115 | 116 | # mypy 117 | .mypy_cache/ 118 | 119 | # macOS 120 | .DS_Store 121 | .AppleDouble 122 | .LSOverride 123 | Icon 124 | ._* 125 | .DocumentRevisions-V100 126 | .fseventsd 127 | .Spotlight-V100 128 | .TemporaryItems 129 | .Trashes 130 | .VolumeIcon.icns 131 | .com.apple.timemachine.donotpresent 132 | .AppleDB 133 | .AppleDesktop 134 | Network Trash Folder 135 | Temporary Items 136 | .apdisk 137 | *.icloud 138 | 139 | # virlutils test 140 | 5f0d96_inventory.ini 141 | 5f0d96_inventory.yaml 142 | 5f0d96_testbed.yaml 143 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at devnet-github-owners@cisco.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Guidance on how to contribute 2 | 3 | Contributions to this code are welcome and appreciated. 4 | Please adhere to our [Code of Conduct](./CODE_OF_CONDUCT.md) at all times. 5 | 6 | > All contributions to this code will be released under the terms of the [LICENSE](./LICENSE) of this code. By submitting a pull request or filing a bug, issue, or feature request, you are agreeing to comply with this waiver of copyright interest. Details can be found in our [LICENSE](./LICENSE). 7 | 8 | There are two primary ways to contribute: 9 | 10 | 1. Using the issue tracker 11 | 2. Changing the codebase 12 | 13 | 14 | ## Using the issue tracker 15 | 16 | Use the issue tracker to suggest feature requests, report bugs, and ask questions. This is also a great way to connect with the developers of the project as well as others who are interested in this solution. 17 | 18 | Use the issue tracker to find ways to contribute. Find a bug or a feature, mention in the issue that you will take on that effort, then follow the _Changing the codebase_ guidance below. 19 | 20 | 21 | ## Changing the codebase 22 | 23 | Generally speaking, you should fork this repository, make changes in your own fork, and then submit a pull request. 24 | 25 | ## Setting up the development environment 26 | 27 | Ensure that you have the minimum required version of Python installed. See the `python_requires` line in the `./setup.py` file. To set up your development environment, create a virtual environment, and then use the `pip` to install the requirements and the development requirements. For example 28 | 29 | $ python3 -mvenv ~/venvs/virlutils-dev/ 30 | $ source ~/venvs/virlutils-dev/bin/activate 31 | (virlutils-dev) $ pip install -r requirements.txt 32 | (virlutils-dev) $ pip install -r requirements_dev.txt 33 | 34 | ### Unit tests 35 | 36 | All new code should have associated unit tests (if applicable) that validate implemented features and the presence or lack of defects. Before you start making changes, check that all existing tests pass. We recommend using a test-driven development (TDD) to making changes. Add one or more new unit tests that demonstrate the bug or gap you're trying to fix. Initially, the test should be failing because you haven't fixed the problem, yet. Before you submit a pull request, ensure that your new test passes and that all existing tests still pass. 37 | 38 | You can run the unit tests from the command line by running `python setup.py tests` from the root directory of the project. That's currently equivalent to running `python -m unittest discover -v -s ./tests -p '*.py'` from the root directory of the project. 39 | 40 | You can also generally configure a Python-aware IDE to run the tests. For example, if you are using [VSCode](https://code.visualstudio.com/), click **Run > Command Palette > Python: Discover Tests** and chose *unittest*, *tests* folder, and `*.py` pattern. You can verify the settings by searching your VSCode workspace settings for `Unittest`. Ensure that the checkbox under **Python > Testing: Unittest Enabled** is checked and that the **Python > Testing: Unittest Args** are 41 | 42 | -v 43 | -s 44 | ./tests 45 | -p 46 | *.py 47 | 48 | With those settings, navigate to **View > Testing** in VSCode. The view should list all of the test methods discovered under the `tests` folder. You may need to **Refresh Tests** first. Because the tests use mock responses, you can run them without access to a CML server. 49 | 50 | ### Code Style 51 | 52 | Additionally, the code should follow any stylistic and architectural guidelines prescribed by the project. The project now uses [black](https://black.readthedocs.io/) and [isort](https://pycqa.github.io/isort/) to ensure consistent code formatting. When you installed the requirements_dev.txt in your virtual environment, it installed the `black` and `isort` commands. For each file that you are modifying, run the following before you commit the file or submit a pull request: 53 | 54 | ```sh 55 | black ./virl/path/to/file.py 56 | isort ./virl/path/to/file.py 57 | ``` 58 | 59 | ### Linting 60 | 61 | We use flake 8 to lint our code. Please keep the repository clean by running: 62 | 63 | ```sh 64 | flake8 65 | ``` 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2020 Cisco Systems, Inc. and its affiliates 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | recursive-include virl/templates * 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Heavily borrowed from: 2 | # https://github.com/audreyr/cookiecutter-pypackage/blob/master/%7B%7Bcookiecutter.project_slug%7D%7D/Makefile 3 | 4 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 5 | 6 | 7 | clean-build: ## remove build artifacts 8 | rm -fr build/ 9 | rm -fr dist/ 10 | rm -fr .eggs/ 11 | find . -name '*.egg-info' -and -not -name "venv*" -exec rm -fr {} + 12 | find . -name '*.egg' -and -not -name 'venv*' -exec rm -rf {} + 13 | 14 | clean-pyc: ## remove Python file artifacts 15 | find . -name '*.pyc' -exec rm -f {} + 16 | find . -name '*.pyo' -exec rm -f {} + 17 | find . -name '*~' -exec rm -f {} + 18 | find . -name '__pycache__' -exec rm -fr {} + 19 | 20 | clean-test: ## remove test and coverage artifacts 21 | rm -fr .tox/ 22 | rm -f .coverage 23 | rm -fr htmlcov/ 24 | rm -f 5f0d96.yaml 25 | rm -f 5f0d96_inventory.ini 26 | rm -f 5f0d96_inventory.yaml 27 | rm -f 5f0d96_testbed.yaml 28 | rm -f default_inventory.ini 29 | rm -f default_inventory.yaml 30 | rm -f default_testbed.yaml 31 | rm -f topology.yaml 32 | rm -f .virl/cached_cml_labs/* 33 | rm -f .virl/current_cml_lab 34 | 35 | lint: ## check style with flake8 36 | flake8 37 | 38 | coverage: 39 | coverage run --source=virl -m pytest tests/v2/* 40 | 41 | report: coverage 42 | coverage html 43 | coverage report 44 | open htmlcov/index.html 45 | 46 | test: ## run tests quickly with the default Python 47 | pytest tests/v2/* 48 | 49 | release: dist ## package and upload a release 50 | @echo "*** Uploading virlutils... ***" 51 | twine upload dist/virl* 52 | @echo "*** Uploading cmlutils... ***" 53 | twine upload dist/cml* 54 | 55 | dist: clean ## builds source and wheel package 56 | # Build virlutils 57 | python setup.py sdist 58 | python setup.py bdist_wheel 59 | # Flip the name 60 | sed -i .orig -e 's|NAME,|CMLNAME,|' setup.py 61 | # Build cmlutils 62 | python setup.py sdist 63 | python setup.py bdist_wheel 64 | cp -f setup.py.orig setup.py 65 | rm -f setup.py.orig 66 | ls -l dist 67 | 68 | install: clean ## install the package to the active Python's site-packages 69 | python setup.py install 70 | -------------------------------------------------------------------------------- /docs/sample_responses.txt: -------------------------------------------------------------------------------- 1 | from virl.api import VIRLServer 2 | 3 | server = VIRLServer() 4 | 5 | print(server.list_simulations()) 6 | # {u'virl_cli_default_1dNVCr': {u'status': u'ACTIVE', u'expires': None, u'launched': u'2017-12-08T23:39:07.721310'}, u'topology-fpyHFs': {u'status': u'ACTIVE', u'expires': None, u'launched': u'2017-12-08T18:48:34.174486'}} 7 | 8 | sample_sim_data = """ 9 | 10 | 11 | 12 | 13 | """ 14 | print(server.launch_simulation('test', sample_sim_data)) 15 | # test 16 | 17 | print(server.get_nodes('test').json()) 18 | # u'~mgmt-lxc': {u'vnc-console': False, u'subtype': u'mgmt-lxc', u'state': u'ABSENT', u'reachable': None, u'management-protocol': u'ssh', u'management-proxy': u'self', u'serial-ports': 0}, u'iosv-1': {u'vnc-console': False, u'subtype': u'IOSv', u'state': u'ABSENT', u'reachable': None, u'management-protocol': u'telnet', u'management-proxy': u'lxc', u'serial-ports': 2}} 19 | 20 | print(server.get_node_console('test').json()) 21 | # {"iosv-1": "10.94.140.41:17016","~mgmt-lxc": null} 22 | 23 | print(server.stop_simulation('test').text) 24 | # SUCCESS 25 | -------------------------------------------------------------------------------- /examples/plugins/csv_inv.py: -------------------------------------------------------------------------------- 1 | import csv 2 | 3 | import click 4 | 5 | from virl.api import GeneratorPlugin, VIRLServer 6 | from virl.helpers import (get_cml_client, get_current_lab, get_node_mgmt_ip, 7 | safe_join_existing_lab) 8 | 9 | 10 | class CSVInventory(GeneratorPlugin, generator="csv"): 11 | @staticmethod 12 | def write_inventory(nodes, delimiter, output): 13 | with open(output, "w") as fd: 14 | csvwriter = csv.writer(fd, delimiter=delimiter, quoting=csv.QUOTE_MINIMAL) 15 | csvwriter.writerow(["Name", "Type", "Tags", "IP Address"]) 16 | for node in nodes: 17 | mgmt_ip = get_node_mgmt_ip(node) 18 | if not mgmt_ip: 19 | continue 20 | 21 | row = [node.label] 22 | row.append(node.node_definition.lower()) 23 | row.append(" ".join(node.tags())) 24 | row.append(mgmt_ip) 25 | csvwriter.writerow(row) 26 | 27 | @staticmethod 28 | @click.command() 29 | @click.option( 30 | "--delimiter", "-d", default=",", show_default=False, required=False, help="Delimiter to use between fields (default: ',')" 31 | ) 32 | @click.option( 33 | "--output", 34 | "-o", 35 | default="inventory.csv", 36 | show_default=False, 37 | required=False, 38 | help="File to write inventory (default: inventory.csv)", 39 | ) 40 | def generate(delimiter, output): 41 | """ 42 | generate generic CSV inventory 43 | """ 44 | server = VIRLServer() 45 | client = get_cml_client(server) 46 | 47 | current_lab = get_current_lab() 48 | if not current_lab: 49 | click.secho("Current lab is not set", fg="red") 50 | exit(1) 51 | 52 | lab = safe_join_existing_lab(current_lab, client) 53 | if not lab: 54 | click.secho("Failed to find running lab {}".format(current_lab), fg="red") 55 | exit(1) 56 | 57 | exit(CSVInventory.write_inventory(lab.nodes(), delimiter, output)) 58 | -------------------------------------------------------------------------------- /examples/plugins/labs_tsv.py: -------------------------------------------------------------------------------- 1 | from virl.api import ViewerPlugin 2 | 3 | 4 | class LabsTSVViewer(ViewerPlugin, viewer="lab"): 5 | def visualize(self, **kwargs): 6 | """ 7 | Render the labs list as a tab-delimited set of 8 | rows. Replaces the output of `cml ls`. 9 | 10 | The input will be kwargs["labs"], kwargs["cached_labs"] and 11 | kwargs["ownerids_usernames"] (a mapping UUID to username) 12 | """ 13 | 14 | labs = kwargs["labs"] 15 | if kwargs["cached_labs"]: 16 | labs += kwargs["cached_labs"] 17 | ownerids_usernames = kwargs["ownerids_usernames"] 18 | 19 | print("ID\tTitle\tDescription\tOwner\tStatus\tNodes\tLinks\tInterfaces") 20 | for lab in labs: 21 | """ 22 | Each lab is of type virl2_client.models.lab.Lab whereas each cached lab is of 23 | type virl.api.cml.CachedLab. The two are similar enough for these properties to 24 | be common 25 | """ 26 | print( 27 | "{id}\t{title}\t{description}\t{owner}\t{status}\t{nodes}\t{links}\t{interfaces}".format( 28 | id=lab.id, 29 | title=lab.title, 30 | description=lab.description, 31 | owner=ownerids_usernames.get(lab.owner, lab.owner), 32 | status=lab.state(), 33 | nodes=lab.statistics["nodes"], 34 | links=lab.statistics["links"], 35 | interfaces=lab.statistics["interfaces"], 36 | ) 37 | ) 38 | -------------------------------------------------------------------------------- /examples/plugins/retitle_cmd.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api import CommandPlugin, VIRLServer 4 | from virl.helpers import (get_cml_client, get_current_lab, 5 | safe_join_existing_lab) 6 | 7 | 8 | class RetitleCommand(CommandPlugin, command="retitle"): 9 | @staticmethod 10 | @click.command() 11 | @click.option("--new-title", "-n", required=True, help="New title for the lab") 12 | def run(new_title): 13 | """ 14 | re-title the current lab 15 | """ 16 | 17 | server = VIRLServer() 18 | client = get_cml_client(server) 19 | 20 | clab = get_current_lab() 21 | 22 | if not clab: 23 | click.secho("Current lab is not set", fg="red") 24 | exit(1) 25 | 26 | lab = safe_join_existing_lab(clab, client) 27 | if lab: 28 | lab.title = new_title 29 | else: 30 | click.secho("Current lab {} is not present on the server".format(clab), fg="red") 31 | exit(1) 32 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 140 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyYAML 2 | click 3 | jinja2 4 | libtmux>=0.34.0 5 | requests 6 | tabulate 7 | virl2-client>=2.7.0 8 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | Sphinx 3 | black 4 | bumpversion 5 | coverage 6 | flake8 7 | isort 8 | pip 9 | requests-mock 10 | tox 11 | twine 12 | watchdog 13 | wheel 14 | white 15 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description_file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from setuptools import find_packages, setup # noqa: H301 3 | 4 | from virl import __version__ 5 | 6 | NAME = "virlutils" 7 | CMLNAME = "cmlutils" 8 | VERSION = __version__ 9 | # To install the library, run the following 10 | # 11 | # python setup.py install 12 | # 13 | # prerequisite: setuptools 14 | # http://pypi.python.org/pypi/setuptools 15 | 16 | 17 | def requirements(f): 18 | with open(f, "r") as fd: 19 | return fd.read() 20 | 21 | 22 | def readme(): 23 | with open("README.md", "r") as f: 24 | return f.read() 25 | 26 | 27 | setup( 28 | name=NAME, 29 | version=VERSION, 30 | description="A collection of utilities for interacting with Cisco Modeling Labs", 31 | author="Joe Clarke", # With a big thanks to its original author, Kevin Corbin 32 | author_email="jclarke@cisco.com", 33 | url="https://github.com/CiscoDevNet/virlutils", 34 | entry_points={"console_scripts": ["virl=virl.cli.main:virl", "cml=virl.cli.main:virl"]}, 35 | packages=find_packages(), 36 | package_data={"virl": ["templates/**/*.j2", "swagger/templates/*", "swagger/static/*", "examples/plugins/*"]}, 37 | include_package_data=True, 38 | install_requires=requirements("requirements.txt"), 39 | long_description_content_type="text/markdown", 40 | long_description=readme(), 41 | test_suite="tests", 42 | tests_require=requirements("test-requirements.txt"), 43 | zip_safe=False, 44 | python_requires=">=3.8", 45 | classifiers=[ 46 | "Programming Language :: Python :: 3", 47 | "License :: OSI Approved :: MIT License", 48 | "Operating System :: OS Independent", 49 | ], 50 | ) 51 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | coverage 2 | coveralls 3 | flake8 4 | pytest 5 | requests_mock 6 | respx 7 | setuptools 8 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/tests/__init__.py -------------------------------------------------------------------------------- /tests/v2/bad_plugins.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from click.testing import CliRunner 4 | 5 | from virl.api.plugin import _test_enable_plugins 6 | 7 | from . import BaseCMLTest 8 | 9 | try: 10 | from unittest.mock import patch 11 | except ImportError: 12 | from mock import patch # noqa 13 | 14 | 15 | class CMLBadPluginTest(BaseCMLTest): 16 | def localSetUp(self, pdir): 17 | os.environ["CML_PLUGIN_PATH"] = os.path.realpath("./tests/v2/{}".format(pdir)) 18 | _test_enable_plugins() 19 | 20 | def tearDown(self): 21 | super().tearDown() 22 | os.environ.pop("CML_PLUGIN_PATH", None) 23 | 24 | @classmethod 25 | def tearDownClass(cls): 26 | super().tearDownClass() 27 | os.environ.pop("CML_PLUGIN_PATH", None) 28 | _test_enable_plugins(enabled=False) 29 | 30 | @patch("virl.cli.main.click.secho", autospec=False) 31 | def test_cmd_plugin_bad(self, secho_mock): 32 | self.localSetUp("plugins_bad_cmd") 33 | virl = self.get_virl() 34 | with self.get_context() as m: 35 | runner = CliRunner() 36 | # Mock the request to return what we expect from the API. 37 | self.setup_mocks(m) 38 | result = runner.invoke(virl, ["--help"]) 39 | self.assertEqual(0, result.exit_code) 40 | self.assertNotIn("test-bad-cmd", result.output) 41 | secho_mock.assert_called_once_with( 42 | "ERROR: Malformed plugin for command test-bad-cmd. The `run` method must be static and a click.command", fg="red" 43 | ) 44 | 45 | @patch("virl.cli.generate.click.secho", autospec=False) 46 | def test_gen_plugin_bad(self, secho_mock): 47 | self.localSetUp("plugins_bad_gen") 48 | virl = self.get_virl() 49 | with self.get_context() as m: 50 | runner = CliRunner() 51 | # Mock the request to return what we expect from the API. 52 | self.setup_mocks(m) 53 | result = runner.invoke(virl, ["generate", "--help"], catch_exceptions=False) 54 | self.assertEqual(0, result.exit_code) 55 | self.assertNotIn("test-bad-gen", result.output) 56 | secho_mock.assert_called_once_with( 57 | "ERROR: Malformed plugin for generator test-bad-gen. The `generate` method must be static and a click.command", fg="red" 58 | ) 59 | 60 | @patch("virl.api.plugin.click.secho", autospec=False) 61 | def test_view_plugin_bad(self, secho_mock): 62 | self.localSetUp("plugins_bad_viewer") 63 | virl = self.get_virl() 64 | with self.get_context() as m: 65 | runner = CliRunner() 66 | # Mock the request to return what we expect from the API. 67 | self.setup_mocks(m) 68 | result = runner.invoke(virl, ["ls"]) 69 | self.assertEqual(0, result.exit_code) 70 | self.assertNotIn("VIEWER PLUGIN", result.output) 71 | secho_mock.assert_any_call("invalid plugin BadLabViewer", fg="red") 72 | 73 | 74 | """ 75 | 76 | def test_gen_plugin(self): 77 | virl = self.get_virl() 78 | with requests_mock.Mocker() as m: 79 | # Mock the request to return what we expect from the API. 80 | self.setup_mocks(m) 81 | runner = CliRunner() 82 | result = runner.invoke(virl, ["generate", "test-gen"]) 83 | self.assertEqual("TEST GENERATOR\n", result.output) 84 | 85 | def test_view_plugin(self): 86 | virl = self.get_virl() 87 | with requests_mock.Mocker() as m: 88 | # Mock the request to return what we expect from the API. 89 | self.setup_mocks(m) 90 | runner = CliRunner() 91 | result = runner.invoke(virl, ["ls"]) 92 | self.assertEqual("TEST VIEWER\n", result.output) 93 | """ 94 | -------------------------------------------------------------------------------- /tests/v2/cluster.py: -------------------------------------------------------------------------------- 1 | from click.testing import CliRunner 2 | 3 | from . import BaseCMLTest 4 | 5 | 6 | class TestCMLCluster(BaseCMLTest): 7 | def setup_mocks(self, m): 8 | super().setup_mocks(m) 9 | self.setup_func("get", m, "system_health", json=TestCMLCluster.get_system_health) 10 | 11 | @staticmethod 12 | def get_system_health(req, ctx=None): 13 | response = { 14 | "valid": True, 15 | "computes": { 16 | "17e91b4e-865a-4627-a6bb-50e3dfa988ab": { 17 | "kvm_vmx_enabled": True, 18 | "enough_cpus": True, 19 | "refplat_images_available": True, 20 | "lld_connected": True, 21 | "valid": True, 22 | "is_controller": True, 23 | "hostname": "cml-controller", 24 | } 25 | }, 26 | "is_licensed": True, 27 | "is_enterprise": True, 28 | } 29 | return response 30 | 31 | def test_cml_cluster_info(self): 32 | with self.get_context() as m: 33 | # Mock the request to return what we expect from the API. 34 | self.setup_mocks(m) 35 | virl = self.get_virl() 36 | runner = CliRunner() 37 | result = runner.invoke(virl, ["cluster", "info"]) 38 | self.assertEqual(0, result.exit_code) 39 | self.assertIn("17e91b4e-865a-4627-a6bb-50e3dfa988ab", result.output) 40 | -------------------------------------------------------------------------------- /tests/v2/console.py: -------------------------------------------------------------------------------- 1 | from click.testing import CliRunner 2 | 3 | from . import BaseCMLTest 4 | 5 | try: 6 | from unittest.mock import patch 7 | except ImportError: 8 | from mock import patch 9 | 10 | 11 | class CMLConsoleTests(BaseCMLTest): 12 | def test_cml_console_display(self): 13 | with self.get_context() as m: 14 | # Mock the request to return what we expect from the API. 15 | self.setup_mocks(m) 16 | virl = self.get_virl() 17 | runner = CliRunner() 18 | result = runner.invoke(virl, ["console", "rtr-1", "--display"]) 19 | self.assertEqual(0, result.exit_code) 20 | 21 | @patch("virl.cli.console.commands.call", autospec=False) 22 | def test_cml_console_connect(self, call_mock): 23 | with self.get_context() as m: 24 | # Mock the request to return what we expect from the API. 25 | self.setup_mocks(m) 26 | virl = self.get_virl() 27 | runner = CliRunner() 28 | runner.invoke(virl, ["console", "rtr-1"]) 29 | call_mock.assert_called_once_with(["ssh", "-t", "admin@localhost", "open", "/5f0d96/n1/0"]) 30 | 31 | @patch("virl.cli.console.commands.call", autospec=False) 32 | def test_cml_console_connect_24(self, call_mock): 33 | with self.get_context() as m: 34 | # Mock the request to return what we expect from the API. 35 | self.setup_mocks(m) 36 | virl = self.get_virl() 37 | runner = CliRunner() 38 | runner.invoke(virl, ["use", "--id", self.get_cml24_id()]) 39 | runner.invoke(virl, ["console", "rtr-1"]) 40 | call_mock.assert_called_once_with(["ssh", "-t", "admin@localhost", "open", "/Mock", "Test", "2.4/rtr-1/0"]) 41 | -------------------------------------------------------------------------------- /tests/v2/down.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from click.testing import CliRunner 4 | 5 | from . import BaseCMLTest 6 | 7 | 8 | class CMLTestDown(BaseCMLTest): 9 | def setup_mocks(self, m): 10 | super().setup_mocks(m) 11 | self.setup_func("put", m, "labs/{}/stop".format(self.get_test_id()), json="STOPPED") 12 | self.setup_func("get", m, "labs/{}/check_if_converged".format(self.get_test_id()), json=True) 13 | 14 | def test_cml_down(self): 15 | with self.get_context() as m: 16 | # Mock the request to return what we expect from the API. 17 | self.setup_mocks(m) 18 | virl = self.get_virl() 19 | runner = CliRunner() 20 | result = runner.invoke(virl, ["down"]) 21 | self.assertEqual(0, result.exit_code) 22 | 23 | def test_cml_down_by_name(self): 24 | 25 | try: 26 | os.remove(".virl/current_cml_lab") 27 | except OSError: 28 | pass 29 | 30 | with self.get_context() as m: 31 | # Mock the request to return what we expect from the API. 32 | self.setup_mocks(m) 33 | virl = self.get_virl() 34 | runner = CliRunner() 35 | result = runner.invoke(virl, ["down", "--lab-name", self.get_test_title()]) 36 | self.assertEqual(0, result.exit_code) 37 | 38 | def test_cml_down_by_id(self): 39 | 40 | try: 41 | os.remove(".virl/current_cml_lab") 42 | except OSError: 43 | pass 44 | 45 | with self.get_context() as m: 46 | # Mock the request to return what we expect from the API. 47 | self.setup_mocks(m) 48 | virl = self.get_virl() 49 | runner = CliRunner() 50 | result = runner.invoke(virl, ["down", "--id", self.get_test_id()]) 51 | self.assertEqual(0, result.exit_code) 52 | -------------------------------------------------------------------------------- /tests/v2/extract.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from click.testing import CliRunner 4 | 5 | from . import BaseCMLTest 6 | 7 | 8 | class CMLExtractTests(BaseCMLTest): 9 | def test_cml_extract(self): 10 | with self.get_context() as m: 11 | # Mock the request to return what we expect from the API. 12 | self.setup_mocks(m) 13 | virl = self.get_virl() 14 | runner = CliRunner() 15 | result = runner.invoke(virl, ["extract"]) 16 | self.assertEqual(0, result.exit_code) 17 | 18 | def test_cml_extract_no_lab(self): 19 | try: 20 | os.remove(".virl/current_cml_lab") 21 | except OSError: 22 | pass 23 | 24 | with self.get_context() as m: 25 | # Mock the request to return what we expect from the API. 26 | self.setup_mocks(m) 27 | virl = self.get_virl() 28 | runner = CliRunner() 29 | result = runner.invoke(virl, ["extract"]) 30 | self.assertEqual(1, result.exit_code) 31 | self.assertIn("Current lab is not set", result.output) 32 | 33 | def test_cml_extract_bogus_lab(self): 34 | try: 35 | os.remove(".virl/current_cml_lab") 36 | except OSError: 37 | pass 38 | 39 | src_dir = os.path.realpath(".virl") 40 | with open(".virl/cached_cml_labs/123456", "w") as fd: 41 | fd.write("lab: bogus\n") 42 | 43 | os.symlink("{}/cached_cml_labs/123456".format(src_dir), "{}/current_cml_lab".format(src_dir)) 44 | 45 | with self.get_context() as m: 46 | # Mock the request to return what we expect from the API. 47 | self.setup_mocks(m) 48 | virl = self.get_virl() 49 | runner = CliRunner() 50 | result = runner.invoke(virl, ["extract"]) 51 | os.remove(".virl/cached_cml_labs/123456") 52 | os.remove(".virl/current_cml_lab") 53 | self.assertEqual(1, result.exit_code) 54 | self.assertIn("Failed to find running lab 123456", result.output) 55 | -------------------------------------------------------------------------------- /tests/v2/generate_ansible.py: -------------------------------------------------------------------------------- 1 | from click.testing import CliRunner 2 | 3 | from . import BaseCMLTest 4 | 5 | 6 | class Tests(BaseCMLTest): 7 | data = { 8 | "n0": {"name": "Lab Net", "interfaces": {}}, 9 | "n1": { 10 | "name": "rtr-1", 11 | "interfaces": { 12 | "52:54:00:1f:27:95": {"id": "i2", "ip4": ["10.1.1.1"], "ip6": ["fc00::1"], "label": "MgmtEth0/RP0/CPU0/0"}, 13 | "52:54:00:06:b7:7c": {"id": "i3", "ip4": [], "ip6": [], "label": "donotuse1"}, 14 | "52:54:00:16:ef:1a": {"id": "i4", "ip4": [], "ip6": [], "label": "donotuse2"}, 15 | "52:54:00:00:73:28": {"id": "i5", "ip4": [], "ip6": [], "label": "GigabitEthernet0/0/0/0"}, 16 | }, 17 | }, 18 | } 19 | 20 | def test_virl_generate_ansible_yaml(self): 21 | with self.get_context() as m: 22 | # Mock the request to return what we expect from the API. 23 | self.setup_mocks(m) 24 | self.setup_func("get", m, "labs/{}/layer3_addresses".format(self.get_test_id()), json=self.data) 25 | self.setup_func("get", m, "labs/{}/nodes/n0/layer3_addresses".format(self.get_test_id()), json=self.data["n0"]) 26 | self.setup_func("get", m, "labs/{}/nodes/n1/layer3_addresses".format(self.get_test_id()), json=self.data["n1"]) 27 | virl = self.get_virl() 28 | runner = CliRunner() 29 | result = runner.invoke(virl, ["generate", "ansible"]) 30 | self.assertEqual(0, result.exit_code) 31 | 32 | def test_virl_generate_ansible_topology_yaml(self): 33 | with self.get_context() as m: 34 | # Mock the request to return what we expect from the API. 35 | self.setup_mocks(m) 36 | self.setup_func("get", m, "labs/{}/layer3_addresses".format(self.get_test_id()), json=self.data) 37 | self.setup_func("get", m, "labs/{}/nodes/n0/layer3_addresses".format(self.get_test_id()), json=self.data["n0"]) 38 | self.setup_func("get", m, "labs/{}/nodes/n1/layer3_addresses".format(self.get_test_id()), json=self.data["n1"]) 39 | virl = self.get_virl() 40 | runner = CliRunner() 41 | result = runner.invoke(virl, ["generate", "ansible", "-o", "topology.yaml"]) 42 | self.assertEqual(0, result.exit_code) 43 | 44 | def test_virl_generate_ansible_ini(self): 45 | with self.get_context() as m: 46 | # Mock the request to return what we expect from the API. 47 | self.setup_mocks(m) 48 | self.setup_func("get", m, "labs/{}/layer3_addresses".format(self.get_test_id()), json=self.data) 49 | self.setup_func("get", m, "labs/{}/nodes/n0/layer3_addresses".format(self.get_test_id()), json=self.data["n0"]) 50 | self.setup_func("get", m, "labs/{}/nodes/n1/layer3_addresses".format(self.get_test_id()), json=self.data["n1"]) 51 | virl = self.get_virl() 52 | runner = CliRunner() 53 | result = runner.invoke(virl, ["generate", "ansible", "--style", "ini"]) 54 | self.assertEqual(0, result.exit_code) 55 | -------------------------------------------------------------------------------- /tests/v2/generate_nso.py: -------------------------------------------------------------------------------- 1 | import requests_mock 2 | from click.testing import CliRunner 3 | 4 | from . import BaseCMLTest 5 | from .mocks.nso import MockNSOServer 6 | 7 | 8 | class Tests(BaseCMLTest): 9 | def test_virl_generate_nso(self): 10 | data = { 11 | "n0": {"name": "Lab Net", "interfaces": {}}, 12 | "n1": { 13 | "name": "rtr-1", 14 | "interfaces": { 15 | "52:54:00:1f:27:95": {"id": "i2", "ip4": ["10.1.1.1"], "ip6": ["fc00::1"], "label": "MgmtEth0/RP0/CPU0/0"}, 16 | "52:54:00:06:b7:7c": {"id": "i3", "ip4": [], "ip6": [], "label": "donotuse1"}, 17 | "52:54:00:16:ef:1a": {"id": "i4", "ip4": [], "ip6": [], "label": "donotuse2"}, 18 | "52:54:00:00:73:28": {"id": "i5", "ip4": [], "ip6": [], "label": "GigabitEthernet0/0/0/0"}, 19 | }, 20 | }, 21 | } 22 | with self.get_context() as m, requests_mock.mock() as nso_mock: 23 | # Mock the request to return what we expect from the API. 24 | self.setup_mocks(m) 25 | self.setup_func("get", m, "labs/{}/layer3_addresses".format(self.get_test_id()), json=data) 26 | self.setup_func("get", m, "labs/{}/nodes/n0/layer3_addresses".format(self.get_test_id()), json=data["n0"]) 27 | self.setup_func("get", m, "labs/{}/nodes/n1/layer3_addresses".format(self.get_test_id()), json=data["n1"]) 28 | 29 | # mock the responses we expect from NSO 30 | devices_url = "http://localhost:8080/api/config/devices/" 31 | nso_mock.patch(devices_url, json=MockNSOServer.update_devices()) 32 | sync_url = "http://localhost:8080/api/running/devices/" 33 | sync_url += "_operations/sync-from" 34 | nso_mock.post(sync_url, json=MockNSOServer.perform_sync_from()) 35 | ned_url = "http://localhost:8080/api/config/devices/ned-ids/ned-id" 36 | nso_mock.get(ned_url, json=MockNSOServer.get_ned_list()) 37 | module_url = "http://localhost:8080/api/config/modules-state/module" 38 | nso_mock.get(module_url, json=MockNSOServer.get_module_list()) 39 | 40 | virl = self.get_virl() 41 | runner = CliRunner() 42 | result = runner.invoke(virl, ["generate", "nso", "--syncfrom"]) 43 | self.assertEqual(0, result.exit_code) 44 | -------------------------------------------------------------------------------- /tests/v2/generate_pyats.py: -------------------------------------------------------------------------------- 1 | from click.testing import CliRunner 2 | 3 | from . import BaseCMLTest 4 | 5 | 6 | class Tests(BaseCMLTest): 7 | def test_virl_generate_pyats(self): 8 | with self.get_context() as m: 9 | # Mock the request to return what we expect from the API. 10 | self.setup_mocks(m) 11 | virl = self.get_virl() 12 | runner = CliRunner() 13 | result = runner.invoke(virl, ["generate", "pyats"]) 14 | self.assertEqual(0, result.exit_code) 15 | 16 | def test_virl_generate_pyats_topology(self): 17 | with self.get_context() as m: 18 | # Mock the request to return what we expect from the API. 19 | self.setup_mocks(m) 20 | virl = self.get_virl() 21 | runner = CliRunner() 22 | result = runner.invoke(virl, ["generate", "pyats", "-o", "topology.yaml"]) 23 | self.assertEqual(0, result.exit_code) 24 | -------------------------------------------------------------------------------- /tests/v2/good_plugins.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from click.testing import CliRunner 4 | 5 | from virl.api.plugin import _test_enable_plugins 6 | 7 | from . import BaseCMLTest 8 | 9 | 10 | class CMLGoodPluginTest(BaseCMLTest): 11 | def setUp(self): 12 | _test_enable_plugins() 13 | super().setUp() 14 | os.environ["CML_PLUGIN_PATH"] = os.path.realpath("./tests/v2/plugins_good") 15 | 16 | @classmethod 17 | def tearDownClass(cls): 18 | super().tearDownClass() 19 | os.environ.pop("CML_PLUGIN_PATH", None) 20 | _test_enable_plugins(enabled=False) 21 | 22 | def test_cmd_plugin_output(self): 23 | virl = self.get_virl() 24 | with self.get_context() as m: 25 | runner = CliRunner() 26 | # Mock the request to return what we expect from the API. 27 | self.setup_mocks(m) 28 | result = runner.invoke(virl, ["--help"]) 29 | self.assertEqual(0, result.exit_code) 30 | self.assertIn("test-cmd", result.output) 31 | 32 | def test_cmd_plugin(self): 33 | virl = self.get_virl() 34 | with self.get_context() as m: 35 | # Mock the request to return what we expect from the API. 36 | self.setup_mocks(m) 37 | runner = CliRunner() 38 | result = runner.invoke(virl, ["test-cmd"]) 39 | self.assertEqual("TEST COMMAND\n", result.output) 40 | 41 | def test_gen_plugin_output(self): 42 | virl = self.get_virl() 43 | with self.get_context() as m: 44 | runner = CliRunner() 45 | # Mock the request to return what we expect from the API. 46 | self.setup_mocks(m) 47 | result = runner.invoke(virl, ["generate", "--help"]) 48 | self.assertEqual(0, result.exit_code) 49 | self.assertIn("test-gen", result.output) 50 | 51 | def test_gen_plugin(self): 52 | virl = self.get_virl() 53 | with self.get_context() as m: 54 | # Mock the request to return what we expect from the API. 55 | self.setup_mocks(m) 56 | runner = CliRunner() 57 | result = runner.invoke(virl, ["generate", "test-gen"]) 58 | self.assertEqual("TEST GENERATOR\n", result.output) 59 | 60 | def test_view_plugin(self): 61 | virl = self.get_virl() 62 | with self.get_context() as m: 63 | # Mock the request to return what we expect from the API. 64 | self.setup_mocks(m) 65 | runner = CliRunner() 66 | result = runner.invoke(virl, ["ls"]) 67 | self.assertEqual("TEST VIEWER\n", result.output) 68 | -------------------------------------------------------------------------------- /tests/v2/id.py: -------------------------------------------------------------------------------- 1 | from click.testing import CliRunner 2 | 3 | from . import BaseCMLTest 4 | 5 | 6 | class CMLIdTest(BaseCMLTest): 7 | def test_cml_id(self): 8 | virl = self.get_virl() 9 | with self.get_context() as m: 10 | # Mock the request to return what we expect from the API. 11 | self.setup_mocks(m) 12 | runner = CliRunner() 13 | result = runner.invoke(virl, ["id"]) 14 | self.assertEqual("{} (ID: {})\n".format(self.get_test_title(), self.get_test_id()), result.output) 15 | -------------------------------------------------------------------------------- /tests/v2/ls.py: -------------------------------------------------------------------------------- 1 | from click.testing import CliRunner 2 | 3 | from . import BaseCMLTest 4 | 5 | 6 | class TestCMLLs(BaseCMLTest): 7 | def test_cml_ls_all(self): 8 | with self.get_context() as m: 9 | self.setup_mocks(m) 10 | virl = self.get_virl() 11 | runner = CliRunner() 12 | result = runner.invoke(virl, ["ls", "--all"]) 13 | self.assertEqual(0, result.exit_code) 14 | 15 | def test_cml_ls(self): 16 | with self.get_context() as m: 17 | self.setup_mocks(m) 18 | virl = self.get_virl() 19 | runner = CliRunner() 20 | result = runner.invoke(virl, ["ls"]) 21 | self.assertEqual(0, result.exit_code) 22 | -------------------------------------------------------------------------------- /tests/v2/mocks/__init__.py: -------------------------------------------------------------------------------- 1 | from .api import MockCMLServer # noqa 2 | -------------------------------------------------------------------------------- /tests/v2/mocks/github.py: -------------------------------------------------------------------------------- 1 | class MockGitHub(object): 2 | @staticmethod 3 | def get_topology(req, ctx=None): 4 | with open("tests/v2/static/fake_repo_topology.yaml", "r") as fh: 5 | response = fh.read() 6 | return response 7 | -------------------------------------------------------------------------------- /tests/v2/mocks/nso.py: -------------------------------------------------------------------------------- 1 | class MockNSOServer: 2 | @classmethod 3 | def launch_simulation(cls): 4 | response = u"TEST_ENV" 5 | return response 6 | 7 | @classmethod 8 | def get_node_console(cls): 9 | sim_response = {u"router2": u"10.94.241.194:17002", u"router1": u"10.94.241.194:17001"} 10 | return sim_response 11 | 12 | @classmethod 13 | def update_devices(cls): 14 | return {} 15 | 16 | @classmethod 17 | def perform_sync_from(cls): 18 | response = {"tailf-ncs:output": {"sync-result": [{"device": "router1", "result": True}, {"device": "router2", "result": True}]}} 19 | return response 20 | 21 | @classmethod 22 | def get_module_list(cls): 23 | response = { 24 | "ietf-yang-library:module": [ 25 | { 26 | "name": "cisco-ios-cli-6.42", 27 | "revision": "", 28 | "schema": "http://localhost:8080/restconf/tailf/modules/cisco-ios-cli-6.42", 29 | "namespace": "http://tail-f.com/ns/ned-id/cisco-ios-cli-6.42", 30 | "conformance-type": "import", 31 | }, 32 | ], 33 | } 34 | return response 35 | 36 | @classmethod 37 | def get_ned_list(cls): 38 | response = { 39 | "tailf-ncs:ned-id": [ 40 | { 41 | "id": "cisco-ios-cli-6.42:cisco-ios-cli-6.42", 42 | "module": [ 43 | {"name": "ietf-interfaces", "revision": "2014-05-08", "namespace": "urn:ietf:params:xml:ns:yang:ietf-interfaces"}, 44 | {"name": "ietf-ip", "revision": "2014-06-16", "namespace": "urn:ietf:params:xml:ns:yang:ietf-ip"}, 45 | {"name": "tailf-ned-cisco-ios", "revision": "2020-01-03", "namespace": "urn:ios"}, 46 | {"name": "tailf-ned-cisco-ios-id", "namespace": "urn:ios-id"}, 47 | {"name": "tailf-ned-cisco-ios-stats", "namespace": "urn:ios-stats"}, 48 | ], 49 | }, 50 | ], 51 | } 52 | return response 53 | -------------------------------------------------------------------------------- /tests/v2/nodes.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from click.testing import CliRunner 4 | 5 | from . import BaseCMLTest 6 | 7 | 8 | class CMLNodesTest(BaseCMLTest): 9 | def test_cml_nodes(self): 10 | data = { 11 | "n0": {"name": "Lab Net", "interfaces": {}}, 12 | "n1": { 13 | "name": "rtr-1", 14 | "interfaces": { 15 | "52:54:00:1f:27:95": {"id": "i2", "ip4": ["10.1.1.1"], "ip6": ["fc00::1"], "label": "MgmtEth0/RP0/CPU0/0"}, 16 | "52:54:00:06:b7:7c": {"id": "i3", "ip4": [], "ip6": [], "label": "donotuse1"}, 17 | "52:54:00:16:ef:1a": {"id": "i4", "ip4": [], "ip6": [], "label": "donotuse2"}, 18 | "52:54:00:00:73:28": {"id": "i5", "ip4": [], "ip6": [], "label": "GigabitEthernet0/0/0/0"}, 19 | }, 20 | }, 21 | } 22 | with self.get_context() as m: 23 | # Mock the request to return what we expect from the API. 24 | self.setup_mocks(m) 25 | self.setup_func("get", m, "labs/{}/layer3_addresses".format(self.get_test_id()), json=data) 26 | self.setup_func("get", m, "labs/{}/nodes/n0/layer3_addresses".format(self.get_test_id()), json=data["n0"]) 27 | self.setup_func("get", m, "labs/{}/nodes/n1/layer3_addresses".format(self.get_test_id()), json=data["n1"]) 28 | virl = self.get_virl() 29 | runner = CliRunner() 30 | result = runner.invoke(virl, ["nodes"]) 31 | self.assertEqual(0, result.exit_code) 32 | 33 | def test_cml_nodes_no_lab(self): 34 | try: 35 | os.remove(".virl/current_cml_lab") 36 | except OSError: 37 | pass 38 | 39 | with self.get_context() as m: 40 | # Mock the request to return what we expect from the API. 41 | self.setup_mocks(m) 42 | virl = self.get_virl() 43 | runner = CliRunner() 44 | result = runner.invoke(virl, ["nodes"]) 45 | self.assertEqual(1, result.exit_code) 46 | self.assertIn("No current lab selected", result.output) 47 | 48 | def test_cml_nodes_bogus_lab(self): 49 | try: 50 | os.remove(".virl/current_cml_lab") 51 | except OSError: 52 | pass 53 | 54 | src_dir = os.path.realpath(".virl") 55 | with open(".virl/cached_cml_labs/123456", "w") as fd: 56 | fd.write("lab: bogus\n") 57 | 58 | os.symlink("{}/cached_cml_labs/123456".format(src_dir), "{}/current_cml_lab".format(src_dir)) 59 | 60 | with self.get_context() as m: 61 | # Mock the request to return what we expect from the API. 62 | self.setup_mocks(m) 63 | virl = self.get_virl() 64 | runner = CliRunner() 65 | result = runner.invoke(virl, ["nodes"]) 66 | os.remove(".virl/cached_cml_labs/123456") 67 | self.assertEqual(1, result.exit_code) 68 | self.assertIn("Lab 123456 is not running", result.output) 69 | -------------------------------------------------------------------------------- /tests/v2/plugins_bad_cmd/cmd_plug_bad_run.py: -------------------------------------------------------------------------------- 1 | from virl.api.plugin import CommandPlugin 2 | 3 | 4 | class TestBadCmdPlugin(CommandPlugin, command="test-bad-cmd"): 5 | def run(): 6 | print("TEST COMMAND") 7 | -------------------------------------------------------------------------------- /tests/v2/plugins_bad_gen/generator_plugin_bad.py: -------------------------------------------------------------------------------- 1 | from virl.api.plugin import GeneratorPlugin 2 | 3 | 4 | class TestBadGenPlugin(GeneratorPlugin, generator="test-bad-gen"): 5 | def generate(): 6 | print("TEST GENERATOR") 7 | -------------------------------------------------------------------------------- /tests/v2/plugins_bad_viewer/viewer_plugin_bad.py: -------------------------------------------------------------------------------- 1 | from virl.api.plugin import ViewerPlugin 2 | 3 | 4 | class BadLabViewer(ViewerPlugin): 5 | def visualize(self, **kwargs): 6 | print("VIEWER PLUGIN") 7 | -------------------------------------------------------------------------------- /tests/v2/plugins_good/cmd_plug.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api.plugin import CommandPlugin 4 | 5 | 6 | class TestCmdPlugin(CommandPlugin, command="test-cmd"): 7 | @staticmethod 8 | @click.command() 9 | def run(): 10 | print("TEST COMMAND") 11 | -------------------------------------------------------------------------------- /tests/v2/plugins_good/generator_plugin.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api.plugin import GeneratorPlugin 4 | 5 | 6 | class TestGenPlugin(GeneratorPlugin, generator="test-gen"): 7 | @staticmethod 8 | @click.command() 9 | def generate(): 10 | print("TEST GENERATOR") 11 | -------------------------------------------------------------------------------- /tests/v2/plugins_good/viewer_plugin.py: -------------------------------------------------------------------------------- 1 | from virl.api import ViewerPlugin 2 | 3 | 4 | class LabViewer(ViewerPlugin, viewer="lab"): 5 | def visualize(self, **kwargs): 6 | print("TEST VIEWER") 7 | -------------------------------------------------------------------------------- /tests/v2/pull.py: -------------------------------------------------------------------------------- 1 | import requests_mock 2 | from click.testing import CliRunner 3 | 4 | from . import BaseCMLTest 5 | from .mocks.github import MockGitHub 6 | 7 | 8 | class Tests(BaseCMLTest): 9 | def test_virl_pull(self): 10 | with requests_mock.mock() as m: 11 | # Mock the request to return what we expect from the API. 12 | topo_url = "https://raw.githubusercontent.com/" 13 | topo_url += "foo/bar/main/topology.yaml" 14 | m.get(topo_url, json=MockGitHub.get_topology) 15 | virl = self.get_virl() 16 | runner = CliRunner() 17 | result = runner.invoke(virl, ["pull", "foo/bar"]) 18 | self.assertEqual(0, result.exit_code) 19 | 20 | def test_virl_pull_invalid_repo(self): 21 | with requests_mock.mock() as m: 22 | # Mock the request to return what we expect from the API. 23 | topo_url = "https://raw.githubusercontent.com/" 24 | topo_url += "doesnt/exist/main/topology.yaml" 25 | m.get(topo_url, status_code=400) 26 | virl = self.get_virl() 27 | runner = CliRunner() 28 | result = runner.invoke(virl, ["pull", "doesnt/exist"]) 29 | expected = ( 30 | "Pulling topology.yaml from doesnt/exist on branch main\nError pulling topology.yaml from doesnt/exist on branch " 31 | "main - repo, file, or branch not found\n" 32 | ) 33 | self.assertEqual(result.output, expected) 34 | -------------------------------------------------------------------------------- /tests/v2/save.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from click.testing import CliRunner 4 | 5 | from . import BaseCMLTest 6 | 7 | 8 | class CMLSaveTests(BaseCMLTest): 9 | def test_cml_save(self): 10 | with self.get_context() as m: 11 | # Mock the request to return what we expect from the API. 12 | self.setup_mocks(m) 13 | virl = self.get_virl() 14 | runner = CliRunner() 15 | result = runner.invoke(virl, ["save"]) 16 | self.assertEqual(0, result.exit_code) 17 | self.assertIn("Extracting configurations", result.output) 18 | self.assertIn("Writing topology.yaml", result.output) 19 | 20 | def test_cml_save_no_extract(self): 21 | with self.get_context() as m: 22 | # Mock the request to return what we expect from the API. 23 | self.setup_mocks(m) 24 | virl = self.get_virl() 25 | runner = CliRunner() 26 | result = runner.invoke(virl, ["save", "--no-extract"]) 27 | self.assertEqual(0, result.exit_code) 28 | self.assertNotIn("Extracting configurations", result.output) 29 | self.assertIn("Writing topology.yaml", result.output) 30 | 31 | def test_cml_save_filename(self): 32 | with self.get_context() as m: 33 | # Mock the request to return what we expect from the API. 34 | self.setup_mocks(m) 35 | virl = self.get_virl() 36 | runner = CliRunner() 37 | result = runner.invoke(virl, ["save", "-f", "{}.yaml".format(self.get_test_id())]) 38 | self.assertEqual(0, result.exit_code) 39 | self.assertIn("Extracting configurations", result.output) 40 | self.assertIn("Writing {}.yaml".format(self.get_test_id()), result.output) 41 | 42 | def test_cml_save_no_lab(self): 43 | try: 44 | os.remove(".virl/current_cml_lab") 45 | except OSError: 46 | pass 47 | 48 | with self.get_context() as m: 49 | # Mock the request to return what we expect from the API. 50 | self.setup_mocks(m) 51 | virl = self.get_virl() 52 | runner = CliRunner() 53 | result = runner.invoke(virl, ["save"]) 54 | self.assertEqual(1, result.exit_code) 55 | self.assertIn("Current lab is not set", result.output) 56 | 57 | def test_cml_save_bogus_lab(self): 58 | try: 59 | os.remove(".virl/current_cml_lab") 60 | except OSError: 61 | pass 62 | 63 | src_dir = os.path.realpath(".virl") 64 | with open(".virl/cached_cml_labs/123456", "w") as fd: 65 | fd.write("lab: bogus\n") 66 | 67 | os.symlink("{}/cached_cml_labs/123456".format(src_dir), "{}/current_cml_lab".format(src_dir)) 68 | 69 | with self.get_context() as m: 70 | # Mock the request to return what we expect from the API. 71 | self.setup_mocks(m) 72 | virl = self.get_virl() 73 | runner = CliRunner() 74 | result = runner.invoke(virl, ["save"]) 75 | os.remove(".virl/cached_cml_labs/123456") 76 | os.remove(".virl/current_cml_lab") 77 | self.assertEqual(1, result.exit_code) 78 | self.assertIn("Failed to find running lab 123456", result.output) 79 | -------------------------------------------------------------------------------- /tests/v2/search.py: -------------------------------------------------------------------------------- 1 | import requests_mock 2 | from click.testing import CliRunner 3 | 4 | from . import BaseCMLTest 5 | 6 | 7 | class SearchTests(BaseCMLTest): 8 | @requests_mock.mock() 9 | def test_virl_search(self, m): 10 | m.get("https://api.github.com/orgs/virlfiles/repos", json=self.mock_response()) 11 | virl = self.get_virl() 12 | runner = CliRunner() 13 | result = runner.invoke(virl, ["search"]) 14 | self.assertEqual(0, result.exit_code) 15 | 16 | @requests_mock.mock() 17 | def test_virl_search_with_query_name(self, m): 18 | m.get("https://api.github.com/orgs/virlfiles/repos", json=self.mock_response()) 19 | virl = self.get_virl() 20 | runner = CliRunner() 21 | result = runner.invoke(virl, ["search", "ios"]) 22 | self.assertEqual(0, result.exit_code) 23 | 24 | @requests_mock.mock() 25 | def test_virl_search_with_query_descr(self, m): 26 | # Mock the request to return what we expect from the API. 27 | m.get("https://api.github.com/orgs/virlfiles/repos", json=self.mock_response()) 28 | virl = self.get_virl() 29 | runner = CliRunner() 30 | result = runner.invoke(virl, ["search", "hello"]) 31 | self.assertEqual(0, result.exit_code) 32 | 33 | def mock_response(self): 34 | response = [ 35 | {"name": "2-ios-router", "full_name": "virlfiles/2-ios-router", "description": "hello world virlfile", "stargazers_count": 344} 36 | ] 37 | return response 38 | -------------------------------------------------------------------------------- /tests/v2/ssh.py: -------------------------------------------------------------------------------- 1 | from click.testing import CliRunner 2 | 3 | from . import BaseCMLTest 4 | 5 | try: 6 | from unittest.mock import patch 7 | except ImportError: 8 | from mock import patch 9 | 10 | 11 | class Tests(BaseCMLTest): 12 | data = { 13 | "n0": {"name": "Lab Net", "interfaces": {}}, 14 | "n1": { 15 | "name": "rtr-1", 16 | "interfaces": { 17 | "52:54:00:1f:27:95": {"id": "i2", "ip4": ["10.1.1.1"], "ip6": ["fc00::1"], "label": "MgmtEth0/RP0/CPU0/0"}, 18 | "52:54:00:06:b7:7c": {"id": "i3", "ip4": [], "ip6": [], "label": "donotuse1"}, 19 | "52:54:00:16:ef:1a": {"id": "i4", "ip4": [], "ip6": [], "label": "donotuse2"}, 20 | "52:54:00:00:73:28": {"id": "i5", "ip4": [], "ip6": [], "label": "GigabitEthernet0/0/0/0"}, 21 | }, 22 | }, 23 | } 24 | 25 | @patch("virl.cli.ssh.commands.call", autospec=False) 26 | def test_virl_ssh(self, call_mock): 27 | with self.get_context() as m: 28 | # Mock the request to return what we expect from the API. 29 | self.setup_mocks(m) 30 | self.setup_func("get", m, "labs/{}/layer3_addresses".format(self.get_test_id()), json=self.data) 31 | self.setup_func("get", m, "labs/{}/nodes/n0/layer3_addresses".format(self.get_test_id()), json=self.data["n0"]) 32 | self.setup_func("get", m, "labs/{}/nodes/n1/layer3_addresses".format(self.get_test_id()), json=self.data["n1"]) 33 | virl = self.get_virl() 34 | runner = CliRunner() 35 | runner.invoke(virl, ["ssh", "rtr-1"]) 36 | call_mock.assert_called_once_with(["ssh", "cisco@10.1.1.1"]) 37 | -------------------------------------------------------------------------------- /tests/v2/static/fake_image_definitions.yaml: -------------------------------------------------------------------------------- 1 | id: alpine-3-10-base 2 | node_definition_id: alpine 3 | description: Alpine Linux and network tools 4 | label: Alpine 3.10 5 | disk_image: alpine-3-10-base.qcow2 6 | read_only: true 7 | disk_subfolder: alpine-3-10-base 8 | schema_version: 0.0.1 9 | -------------------------------------------------------------------------------- /tests/v2/static/fake_repo_bad_topology.yaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tests/v2/static/fake_repo_topology.yaml: -------------------------------------------------------------------------------- 1 | lab: 2 | description: '' 3 | notes: '' 4 | title: Fake Lab 5 | version: 0.1.0 6 | links: 7 | - id: l0 8 | n1: n0 9 | n2: n1 10 | i1: i0 11 | i2: i0 12 | label: test-sw-mgmt0<->ext-conn-0-port 13 | nodes: 14 | - boot_disk_size: 0 15 | configuration: hostname inserthostname_here 16 | cpu_limit: 100 17 | cpus: 0 18 | data_volume: 0 19 | hide_links: false 20 | id: n0 21 | image_definition: nxosv-7-3-0 22 | label: test-sw 23 | node_definition: nxosv 24 | ram: 0 25 | tags: [] 26 | x: -350 27 | y: 0 28 | interfaces: 29 | - id: i0 30 | label: mgmt0 31 | slot: 0 32 | type: physical 33 | - id: i1 34 | label: Ethernet2/1 35 | slot: 1 36 | type: physical 37 | - id: i2 38 | label: Ethernet2/2 39 | slot: 2 40 | type: physical 41 | - id: i3 42 | label: Ethernet2/3 43 | slot: 3 44 | type: physical 45 | - boot_disk_size: 0 46 | configuration: bridge0 47 | cpu_limit: 100 48 | cpus: 0 49 | data_volume: 0 50 | hide_links: false 51 | id: n1 52 | label: ext-conn-0 53 | node_definition: external_connector 54 | ram: 0 55 | tags: [] 56 | x: 0 57 | y: 0 58 | interfaces: 59 | - id: i0 60 | label: port 61 | slot: 0 62 | type: physical 63 | -------------------------------------------------------------------------------- /tests/v2/telnet.py: -------------------------------------------------------------------------------- 1 | from click.testing import CliRunner 2 | 3 | from . import BaseCMLTest 4 | 5 | try: 6 | from unittest.mock import patch 7 | except ImportError: 8 | from mock import patch 9 | 10 | 11 | class Tests(BaseCMLTest): 12 | data = { 13 | "n0": {"name": "Lab Net", "interfaces": {}}, 14 | "n1": { 15 | "name": "rtr-1", 16 | "interfaces": { 17 | "52:54:00:1f:27:95": {"id": "i2", "ip4": ["10.1.1.1"], "ip6": ["fc00::1"], "label": "MgmtEth0/RP0/CPU0/0"}, 18 | "52:54:00:06:b7:7c": {"id": "i3", "ip4": [], "ip6": [], "label": "donotuse1"}, 19 | "52:54:00:16:ef:1a": {"id": "i4", "ip4": [], "ip6": [], "label": "donotuse2"}, 20 | "52:54:00:00:73:28": {"id": "i5", "ip4": [], "ip6": [], "label": "GigabitEthernet0/0/0/0"}, 21 | }, 22 | }, 23 | } 24 | 25 | @patch("virl.cli.telnet.commands.call", autospec=False) 26 | def test_virl_telnet(self, call_mock): 27 | with self.get_context() as m: 28 | # Mock the request to return what we expect from the API. 29 | self.setup_mocks(m) 30 | self.setup_func("get", m, "labs/{}/layer3_addresses".format(self.get_test_id()), json=self.data) 31 | self.setup_func("get", m, "labs/{}/nodes/n0/layer3_addresses".format(self.get_test_id()), json=self.data["n0"]) 32 | self.setup_func("get", m, "labs/{}/nodes/n1/layer3_addresses".format(self.get_test_id()), json=self.data["n1"]) 33 | virl = self.get_virl() 34 | runner = CliRunner() 35 | runner.invoke(virl, ["telnet", "rtr-1"]) 36 | call_mock.assert_called_once_with(["telnet", "10.1.1.1"]) 37 | -------------------------------------------------------------------------------- /tests/v2/test_cli.py: -------------------------------------------------------------------------------- 1 | from click.testing import CliRunner 2 | 3 | from . import BaseCMLTest 4 | 5 | 6 | class TestCMLHelp(BaseCMLTest): 7 | def test_cml_help(self): 8 | runner = CliRunner() 9 | virl = self.get_virl() 10 | result = runner.invoke(virl, ["--help"]) 11 | self.assertEqual(0, result.exit_code) 12 | for command in [ 13 | "clear", 14 | "cockpit", 15 | "command", 16 | "console", 17 | "definitions", 18 | "down", 19 | "extract", 20 | "generate", 21 | "groups", 22 | "id", 23 | "license", 24 | "ls", 25 | "nodes", 26 | "pull", 27 | "rm", 28 | "save", 29 | "search", 30 | "ssh", 31 | "start", 32 | "stop", 33 | "telnet", 34 | "tmux", 35 | "ui", 36 | "up", 37 | "use", 38 | "users", 39 | "version", 40 | "wipe", 41 | ]: 42 | self.assertIn(command, result.output) 43 | -------------------------------------------------------------------------------- /tests/v2/tmux.py: -------------------------------------------------------------------------------- 1 | from click.testing import CliRunner 2 | 3 | from . import BaseCMLTest 4 | 5 | try: 6 | from unittest.mock import call, patch 7 | except ImportError: 8 | from mock import call, patch 9 | 10 | 11 | class CMLTmuxTests(BaseCMLTest): 12 | @patch("virl.cli.tmux.commands.libtmux.server.Server") 13 | def test_cml_tmux_panes(self, mock_server): 14 | with self.get_context() as m: 15 | # Mocking libtmux server 16 | mock_session = mock_server.return_value.new_session.return_value 17 | mock_window = mock_session.windows[0] 18 | mock_pane = mock_window.panes[0] 19 | mock_window.split_window.return_value = mock_pane 20 | mock_window.panes = [mock_pane] 21 | # Mock the request to return what we expect from the API. 22 | self.setup_mocks(m) 23 | virl = self.get_virl() 24 | runner = CliRunner() 25 | runner.invoke(virl, ["tmux"]) 26 | mock_server.assert_called_once() 27 | mock_server.return_value.new_session.assert_called_once_with(session_name="Mock Test-5f0d", kill_session=True) 28 | expected_calls = [ 29 | call("printf '\\033]2;%s\\033\\\\' 'rtr-1'", suppress_history=True), 30 | call("ssh -t admin@localhost open /5f0d96/n1/0", suppress_history=True), 31 | ] 32 | mock_pane.send_keys.assert_has_calls(expected_calls, any_order=True) 33 | 34 | @patch("virl.cli.tmux.commands.libtmux.server.Server") 35 | def test_cml_tmux_panes24(self, mock_server): 36 | with self.get_context() as m: 37 | # Mocking libtmux server 38 | mock_session = mock_server.return_value.new_session.return_value 39 | mock_window = mock_session.windows[0] 40 | mock_pane = mock_window.panes[0] 41 | mock_window.split_window.return_value = mock_pane 42 | mock_window.panes = [mock_pane] 43 | # Mock the request to return what we expect from the API. 44 | self.setup_mocks(m) 45 | virl = self.get_virl() 46 | runner = CliRunner() 47 | lab_id = self.get_cml24_id() 48 | runner.invoke(virl, ["use", "--id", lab_id]) 49 | runner.invoke(virl, ["tmux"]) 50 | mock_server.assert_called_once() 51 | mock_server.return_value.new_session.assert_called_once_with(session_name="Mock Test 2_4-8811", kill_session=True) 52 | expected_calls = [ 53 | call("printf '\\033]2;%s\\033\\\\' 'rtr-1'", suppress_history=True), 54 | call("ssh -t admin@localhost open /Mock Test 2.4/rtr-1/0", suppress_history=True), 55 | ] 56 | mock_pane.send_keys.assert_has_calls(expected_calls, any_order=True) 57 | 58 | @patch("virl.cli.tmux.commands.libtmux.server.Server") 59 | def test_cml_tmux_windows_24(self, mock_server): 60 | with self.get_context() as m: 61 | # Mocking libtmux server 62 | mock_session = mock_server.return_value.new_session.return_value 63 | mock_window = mock_session.windows[0] 64 | mock_session.windows = [mock_window] 65 | # Mock the request to return what we expect from the API. 66 | self.setup_mocks(m) 67 | virl = self.get_virl() 68 | runner = CliRunner() 69 | lab_id = self.get_cml24_id() 70 | runner.invoke(virl, ["use", "--id", lab_id]) 71 | runner.invoke(virl, ["tmux", "--group", "windows"]) 72 | mock_server.assert_called_once() 73 | mock_server.return_value.new_session.assert_called_once_with(session_name="Mock Test 2_4-8811", kill_session=True) 74 | expected_calls = [ 75 | call("rename-window", "rtr-1"), 76 | ] 77 | mock_window.cmd.assert_has_calls(expected_calls, any_order=True) 78 | -------------------------------------------------------------------------------- /tests/v2/use.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from click.testing import CliRunner 4 | 5 | from . import BaseCMLTest 6 | 7 | try: 8 | from unittest.mock import patch 9 | except ImportError: 10 | from mock import patch # noqa 11 | 12 | 13 | class CMLUseTest(BaseCMLTest): 14 | @patch("virl.cli.use.commands.call", autospec=False, return_value=0) 15 | def test_cml_use(self, call_mock): 16 | with self.get_context() as m: 17 | self.setup_mocks(m) 18 | virl = self.get_virl() 19 | runner = CliRunner() 20 | runner.invoke(virl, ["use"]) 21 | call_mock.assert_called_once_with(["virl", "use", "--help"]) 22 | 23 | def test_cml_use_with_lab(self): 24 | with self.get_context() as m: 25 | self.setup_mocks(m) 26 | virl = self.get_virl() 27 | runner = CliRunner() 28 | result = runner.invoke(virl, ["use", self.get_test_title()]) 29 | self.assertEqual(0, result.exit_code) 30 | 31 | def test_cml_use_with_id(self): 32 | with self.get_context() as m: 33 | self.setup_mocks(m) 34 | virl = self.get_virl() 35 | runner = CliRunner() 36 | result = runner.invoke(virl, ["use", "--id", self.get_test_id()]) 37 | self.assertEqual(0, result.exit_code) 38 | 39 | def test_cml_use_with_lab_name(self): 40 | with self.get_context() as m: 41 | self.setup_mocks(m) 42 | virl = self.get_virl() 43 | runner = CliRunner() 44 | result = runner.invoke(virl, ["use", "--lab-name", self.get_test_title()]) 45 | self.assertEqual(0, result.exit_code) 46 | 47 | def test_cml_use_with_cache(self): 48 | try: 49 | os.remove(".virl/current_cml_lab") 50 | except OSError: 51 | pass 52 | 53 | try: 54 | os.remove(".virl/cached_cml_labs/{}".format(self.get_test_id())) 55 | except OSError: 56 | pass 57 | 58 | with self.get_context() as m: 59 | self.setup_mocks(m) 60 | virl = self.get_virl() 61 | runner = CliRunner() 62 | result = runner.invoke(virl, ["use", "--id", self.get_test_id()]) 63 | self.assertEqual(0, result.exit_code) 64 | 65 | def test_cml_use_with_bogus_id(self): 66 | with self.get_context() as m: 67 | self.setup_mocks(m) 68 | virl = self.get_virl() 69 | runner = CliRunner() 70 | result = runner.invoke(virl, ["use", "--id", "123456"]) 71 | self.assertEqual(1, result.exit_code) 72 | self.assertIn("Unable to find unique lab in the cache or on the server", result.output) 73 | -------------------------------------------------------------------------------- /virl/__init__.py: -------------------------------------------------------------------------------- 1 | from .about import __version__ # noqa -------------------------------------------------------------------------------- /virl/about.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.5.0" 2 | -------------------------------------------------------------------------------- /virl/api/__init__.py: -------------------------------------------------------------------------------- 1 | from .api import VIRLServer # noqa 2 | from .cml import CachedLab # noqa 3 | from .plugin import (CommandPlugin, GeneratorPlugin, NoPluginError, # noqa 4 | Plugin, ViewerPlugin, check_valid_plugin, load_plugins) 5 | -------------------------------------------------------------------------------- /virl/api/api.py: -------------------------------------------------------------------------------- 1 | from .credentials import get_credentials 2 | 3 | 4 | class VIRLServer(object): 5 | def __init__(self): 6 | self._host, self._user, self._passwd, self._config = get_credentials() 7 | 8 | @property 9 | def host(self): 10 | return self._host 11 | 12 | @property 13 | def user(self): 14 | return self._user 15 | 16 | @property 17 | def passwd(self): 18 | return self._passwd 19 | 20 | @property 21 | def config(self): 22 | return self._config 23 | -------------------------------------------------------------------------------- /virl/api/cml.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from yaml import load 4 | 5 | try: 6 | from yaml import CLoader as Loader 7 | except ImportError: 8 | from yaml import Loader 9 | 10 | 11 | class CachedLab(object): 12 | """ 13 | This is a stub class to represent a pseudo-lab that is cached locally. 14 | Only enough of the lab model is implemented for the required virlutils functions. 15 | """ 16 | 17 | __id = None 18 | __title = None 19 | __description = None 20 | __stats = { 21 | "nodes": 0, 22 | "links": 0, 23 | "interfaces": 0, 24 | } 25 | 26 | def __init__(self, lab_id, cache_file): 27 | if not os.path.exists(cache_file): 28 | raise FileNotFoundError("Cached lab {} not found".format(cache_file)) 29 | 30 | self.__id = lab_id 31 | 32 | with open(cache_file, "rb") as fd: 33 | lab = load(fd, Loader=Loader) 34 | 35 | self.__title = lab["lab"]["title"] 36 | self.__description = lab["lab"]["description"] 37 | self.__stats["nodes"] = len(lab["nodes"]) 38 | self.__stats["links"] = len(lab["links"]) 39 | for n in lab["nodes"]: 40 | self.__stats["interfaces"] += len(n["interfaces"]) 41 | 42 | @property 43 | def id(self): 44 | return self.__id 45 | 46 | @property 47 | def title(self): 48 | return self.__title 49 | 50 | @property 51 | def description(self): 52 | return self.__description 53 | 54 | def state(self): 55 | return "CACHED" 56 | 57 | @property 58 | def statistics(self): 59 | return self.__stats 60 | 61 | @property 62 | def owner(self): 63 | return "N/A" 64 | 65 | @property 66 | def username(self): 67 | return "N/A" 68 | -------------------------------------------------------------------------------- /virl/api/credentials.py: -------------------------------------------------------------------------------- 1 | """ 2 | Collection of utility classes to make getting credentials and 3 | configuration easier. 4 | """ 5 | import getpass 6 | import os 7 | 8 | from virl.helpers import find_virl 9 | 10 | 11 | def _get_from_user(prompt): # pragma: no cover 12 | """ 13 | Get the input from the user through interactive prompt. 14 | """ 15 | resp = input(prompt) 16 | return resp 17 | 18 | 19 | def _get_password(prompt): 20 | """ 21 | Get the password from the user through interactive prompt. 22 | Using this will ensure that the password is not displayed as 23 | it is typed. 24 | """ 25 | return getpass.getpass(prompt) 26 | 27 | 28 | def _get_from_file(virlrc, prop_name): 29 | if os.path.isfile(virlrc): 30 | with open(virlrc) as fh: 31 | config = fh.readlines() 32 | 33 | for line in config: 34 | if line.startswith(prop_name): 35 | prop = line.split("=")[1].strip() 36 | if prop.startswith('"') and prop.endswith('"'): 37 | prop = prop[1:-1] 38 | return prop 39 | 40 | 41 | def get_prop(prop_name): 42 | """ 43 | Gets a variable using the following order 44 | 45 | * Check for .virlrc in current directory 46 | 47 | * recurse up directory tree for .virlrc 48 | 49 | * Check environment variables 50 | 51 | * Check ~/.virlrc 52 | 53 | * Prompt user 54 | 55 | """ 56 | # check for .virlrc in current directory 57 | cwd = os.getcwd() 58 | virlrc = os.path.join(cwd, ".virlrc") 59 | prop = _get_from_file(virlrc, prop_name) 60 | 61 | if prop: 62 | return prop 63 | 64 | # search up directory tree for a .virlrc 65 | virl_dir = find_virl() 66 | if virl_dir: 67 | virlrc = os.path.join(virl_dir, ".virlrc") 68 | prop = _get_from_file(virlrc, prop_name) 69 | 70 | if prop: 71 | return prop 72 | 73 | # try environment next 74 | prop = os.getenv(prop_name, None) 75 | if prop: 76 | return prop 77 | 78 | # check for .virlrc in home directory 79 | path = os.path.expanduser("~") 80 | virlrc = os.path.join(path, ".virlrc") 81 | prop = _get_from_file(virlrc, prop_name) 82 | 83 | return prop or None 84 | 85 | 86 | def get_credentials(rcfile="~/.virlrc"): 87 | """ 88 | Used to get the VIRL credentials 89 | 90 | * The login credentials are taken in the following order 91 | 92 | * Check for .virlrc in current directory 93 | 94 | * Check environment variables 95 | 96 | * Check ~/.virlrc 97 | 98 | * Prompt user 99 | 100 | """ 101 | # initialize vars 102 | host = None 103 | username = None 104 | password = None 105 | config = dict() 106 | 107 | host = get_prop("VIRL_HOST") 108 | username = get_prop("VIRL_USERNAME") 109 | password = get_prop("VIRL_PASSWORD") 110 | 111 | # some additional configuration that can be set / overriden 112 | configurable_props = [ 113 | "VIRL_TELNET_COMMAND", 114 | "VIRL_CONSOLE_COMMAND", 115 | "VIRL_SSH_COMMAND", 116 | "VIRL_SSH_USERNAME", 117 | "CML_CONSOLE_COMMAND", 118 | "CML2_PLUS", 119 | "CML_VERIFY_CERT", 120 | "CML_DEVICE_USERNAME", 121 | "CML_DEVICE_PASSWORD", 122 | "CML_DEVICE_ENABLE_PASSWORD", 123 | "CML_PLUGIN_PATH", 124 | ] 125 | 126 | for p in configurable_props: 127 | if get_prop(p): 128 | config[p] = get_prop(p) 129 | 130 | if not host: # pragma: no cover 131 | prompt = "Please enter the IP / hostname of your virl server: " 132 | host = _get_from_user(prompt) 133 | 134 | if not username: # pragma: no cover 135 | username = _get_from_user("Please enter your VIRL username: ") 136 | 137 | if not password: # pragma: no cover 138 | password = _get_password("Please enter your password: ") 139 | 140 | if not all([host, username, password]): # pragma: no cover 141 | print("Unable to determine CML/VIRL credentials, please see docs") 142 | exit(1) 143 | else: 144 | return (host, username, password, config) 145 | -------------------------------------------------------------------------------- /virl/api/github.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | def get_repos(org='virlfiles', query=None): 5 | resp = requests.get('https://api.github.com/orgs/{}/repos'.format(org)) 6 | ret = list() 7 | 8 | if query is None: 9 | return resp.json() 10 | 11 | for repo in resp.json(): 12 | name = repo['name'] 13 | descr = repo['description'] 14 | if query in name: 15 | ret.append(repo) 16 | if descr and query in descr: 17 | ret.append(repo) 18 | 19 | return ret 20 | -------------------------------------------------------------------------------- /virl/api/plugin.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from abc import ABC, abstractmethod 4 | from importlib import import_module 5 | from pkgutil import iter_modules 6 | 7 | import click 8 | 9 | _plugins_enabled = True 10 | 11 | 12 | class NoPluginError(Exception): 13 | pass 14 | 15 | 16 | class Plugin(ABC): 17 | _command_plugins = {} 18 | _generator_plugins = {} 19 | _viewer_plugins = {} 20 | 21 | _plugin_map = { 22 | "command": _command_plugins, 23 | "generator": _generator_plugins, 24 | "viewer": _viewer_plugins, 25 | } 26 | 27 | _plugin_types = [ 28 | "CommandPlugin", 29 | "GeneratorPlugin", 30 | "ViewerPlugin", 31 | ] 32 | 33 | def __new__(cls, **kwargs): 34 | # only provide a plugin if global plugin support is enabled. 35 | if not _plugins_enabled: 36 | raise NoPluginError("plugin support is disabled") 37 | 38 | for t, d in cls._plugin_map.items(): 39 | if t in kwargs: 40 | val = kwargs.pop(t) 41 | if val in d: 42 | return object.__new__(d[val]) 43 | else: 44 | raise NoPluginError("no {} plugin for {}".format(t, val)) 45 | 46 | raise ValueError("unsupported plugin") 47 | 48 | def __init_subclass__(cls, **kwargs): 49 | ptype = None 50 | pdict = None 51 | good_plugin = False 52 | for t, d in cls._plugin_map.items(): 53 | nptype = kwargs.pop(t, None) 54 | if nptype and ptype: 55 | raise ValueError("plugin may only contain one type: {}".format(", ".join(cls._plugin_map.keys()))) 56 | 57 | if nptype: 58 | ptype = nptype 59 | pdict = d 60 | 61 | if ptype: 62 | good_plugin = True 63 | 64 | if ptype not in pdict: 65 | pdict[ptype] = cls 66 | 67 | if cls.__name__ not in cls._plugin_types and not good_plugin: 68 | raise ValueError("invalid plugin {}".format(cls.__name__)) 69 | 70 | @classmethod 71 | def get_plugins(cls, t): 72 | return list(cls._plugin_map[t].keys()) 73 | 74 | @classmethod 75 | def remove_plugin(cls, t, name): 76 | cls._plugin_map[t].pop(name, None) 77 | 78 | 79 | class CommandPlugin(Plugin, ABC): 80 | def __init__(self, **kwargs): 81 | self._command = kwargs.pop("command") 82 | 83 | @property 84 | def command(self): 85 | return self._command 86 | 87 | @staticmethod 88 | @abstractmethod 89 | @click.command() 90 | def run(): 91 | """ 92 | This must be a "click" command. 93 | """ 94 | raise NotImplementedError 95 | 96 | 97 | class GeneratorPlugin(Plugin, ABC): 98 | def __init__(self, **kwargs): 99 | self._generator = kwargs.pop("generator") 100 | 101 | @property 102 | def generator(self): 103 | return self._generator 104 | 105 | @staticmethod 106 | @abstractmethod 107 | @click.command() 108 | def generate(): 109 | """ 110 | This must be a "click" command. 111 | """ 112 | raise NotImplementedError 113 | 114 | 115 | class ViewerPlugin(Plugin, ABC): 116 | def __init__(self, **kwargs): 117 | self._viewer = kwargs.pop("viewer") 118 | 119 | @property 120 | def viewer(self): 121 | return self._viewer 122 | 123 | @abstractmethod 124 | def visualize(self, **kwargs): 125 | raise NotImplementedError 126 | 127 | 128 | def load_plugins(basedirs): 129 | if isinstance(basedirs, str): 130 | basedirs = basedirs.split(os.pathsep) 131 | 132 | modules = iter_modules(path=basedirs) 133 | for d in basedirs: 134 | if os.path.isdir: 135 | sys.path.append(d) 136 | 137 | for mod in modules: 138 | try: 139 | import_module(name=mod.name) 140 | except (AttributeError, ImportError, ValueError, TypeError) as e: 141 | # This is not a valid plugin 142 | click.secho(str(e), fg="red") 143 | 144 | 145 | def check_valid_plugin(pl, mtd, mtd_name, is_click=True): 146 | if is_click: 147 | if not hasattr(mtd, "hidden") or not isinstance(pl.__class__.__dict__[mtd_name], staticmethod): 148 | return False 149 | 150 | return True 151 | 152 | 153 | def _test_enable_plugins(enabled=True): 154 | """ 155 | This function allows the unit tests to toggle 156 | plugin support on and off. Without it, once 157 | pugins are loaded, they remain loaded for the whole 158 | test suite. This can break subsequent tests. 159 | """ 160 | global _plugins_enabled 161 | _plugins_enabled = enabled 162 | -------------------------------------------------------------------------------- /virl/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/__init__.py -------------------------------------------------------------------------------- /virl/cli/clear/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/clear/__init__.py -------------------------------------------------------------------------------- /virl/cli/clear/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.helpers import clear_current_lab 4 | 5 | 6 | @click.command() 7 | def clear(): 8 | """ 9 | clear the current lab ID 10 | """ 11 | 12 | clear_current_lab() 13 | -------------------------------------------------------------------------------- /virl/cli/cluster/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.cli.cluster.info.commands import info 4 | 5 | 6 | @click.group() 7 | def cluster(): 8 | """ 9 | display and manage CML cluster details 10 | """ 11 | pass 12 | 13 | 14 | cluster.add_command(info) 15 | -------------------------------------------------------------------------------- /virl/cli/cluster/info/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/cluster/info/__init__.py -------------------------------------------------------------------------------- /virl/cli/cluster/info/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api import NoPluginError, ViewerPlugin, VIRLServer 4 | from virl.cli.views import cluster_list_table 5 | from virl.helpers import get_cml_client 6 | 7 | 8 | @click.command() 9 | def info(): 10 | """ 11 | display cluster configuration details 12 | """ 13 | 14 | server = VIRLServer() 15 | client = get_cml_client(server) 16 | pl = None 17 | 18 | system_health = None 19 | try: 20 | system_health = client.get_system_health() 21 | except Exception as e: 22 | click.secho(f"Failed to get system health data: {e}", fg="red") 23 | exit(1) 24 | 25 | try: 26 | pl = ViewerPlugin(viewer="cluster") 27 | except NoPluginError: 28 | pass 29 | 30 | if pl: 31 | pl.visualize(computes=system_health["computes"]) 32 | else: 33 | cluster_list_table(system_health["computes"]) 34 | -------------------------------------------------------------------------------- /virl/cli/cockpit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/cockpit/__init__.py -------------------------------------------------------------------------------- /virl/cli/cockpit/commands.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | import click 4 | 5 | from virl.api import VIRLServer 6 | 7 | 8 | @click.command() 9 | def cockpit(): 10 | """ 11 | opens the Cockpit UI 12 | """ 13 | server = VIRLServer() 14 | url = "https://{}:9090".format(server.host) 15 | subprocess.Popen(["open", url]) 16 | -------------------------------------------------------------------------------- /virl/cli/command/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/command/__init__.py -------------------------------------------------------------------------------- /virl/cli/command/commands.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import click 4 | from virl2_client.models.cl_pyats import (ClPyats, PyatsDeviceNotFound, 5 | PyatsNotInstalled) 6 | 7 | from virl.api import VIRLServer 8 | from virl.helpers import (get_cml_client, get_current_lab, 9 | safe_join_existing_lab) 10 | 11 | 12 | @click.command() 13 | @click.argument("node", nargs=1) 14 | @click.argument("command", nargs=1) 15 | @click.option("--config/--no-config", default=False, show_default=False, help="Command is a configuration command (default: False)") 16 | def command(node, command, config, **kwargs): 17 | """ 18 | send a command or config to a node (requires pyATS) 19 | """ 20 | server = VIRLServer() 21 | client = get_cml_client(server) 22 | 23 | current_lab = get_current_lab() 24 | if current_lab: 25 | lab = safe_join_existing_lab(current_lab, client) 26 | if lab: 27 | pylab = None 28 | try: 29 | pylab = ClPyats(lab) 30 | except PyatsNotInstalled: 31 | click.secho("pyATS is not installed, run 'pip install pyats'", fg="red") 32 | exit(1) 33 | 34 | pyats_username = server.config.get("CML_DEVICE_USERNAME") 35 | pyats_password = server.config.get("CML_DEVICE_PASSWORD") 36 | pyats_auth_password = server.config.get("CML_DEVICE_ENABLE_PASSWORD") 37 | 38 | if pyats_username: 39 | os.environ["PYATS_USERNAME"] = pyats_username 40 | if pyats_password: 41 | os.environ["PYATS_PASSWORD"] = pyats_password 42 | if pyats_auth_password: 43 | os.environ["PYATS_AUTH_PASS"] = pyats_auth_password 44 | 45 | pylab.sync_testbed(server.user, server.passwd) 46 | 47 | try: 48 | result = "" 49 | if config: 50 | result = pylab.run_config_command(node, command) 51 | else: 52 | result = pylab.run_command(node, command) 53 | 54 | click.secho(result) 55 | except PyatsDeviceNotFound: 56 | click.secho("Node '{}' is not supported by pyATS".format(node), fg="yellow") 57 | except Exception as e: 58 | click.secho("Failed to run '{}' on '{}': {}".format(command, node, e)) 59 | exit(1) 60 | else: 61 | click.secho("Unable to find lab {}".format(current_lab), fg="red") 62 | exit(1) 63 | else: 64 | click.secho("No current lab set", fg="red") 65 | exit(1) 66 | -------------------------------------------------------------------------------- /virl/cli/console/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/console/__init__.py -------------------------------------------------------------------------------- /virl/cli/console/commands.py: -------------------------------------------------------------------------------- 1 | import platform 2 | from subprocess import call 3 | 4 | import click 5 | from virl2_client.exceptions import NodeNotFound 6 | 7 | from virl import helpers 8 | from virl.api import NoPluginError, ViewerPlugin, VIRLServer 9 | from virl.cli.views.console import console_table 10 | from virl.helpers import (get_cml_client, get_current_lab, 11 | safe_join_existing_lab) 12 | 13 | 14 | @click.command() 15 | @click.argument("node", nargs=1) 16 | @click.option("--display/--none", default="False", help="Display Console information") 17 | def console(node, display, **kwargs): 18 | """ 19 | console for node 20 | """ 21 | server = VIRLServer() 22 | client = get_cml_client(server) 23 | skip_types = ["external_connector", "unmanaged_switch"] 24 | 25 | current_lab = get_current_lab() 26 | if current_lab: 27 | lab = safe_join_existing_lab(current_lab, client) 28 | if lab: 29 | try: 30 | node_obj = lab.get_node_by_label(node) 31 | except NodeNotFound: 32 | click.secho("Node {} was not found in lab {}".format(node, current_lab), fg="red") 33 | exit(1) 34 | 35 | if node_obj.node_definition not in skip_types: 36 | if node_obj.is_active(): 37 | if len(lab.id) == 6: 38 | # Old-style (CML 2.2) lab IDs; console uses lab_id/node_id 39 | console = "/{}/{}/0".format(lab.id, node_obj.id) 40 | else: 41 | # From CML 2.3, console uses lab_title/node_label 42 | console = "/{}/{}/0".format(lab.title, node_obj.label) 43 | if display: 44 | try: 45 | pl = ViewerPlugin(viewer="console") 46 | pl.visualize(consoles=[{"node": node, "console": console}]) 47 | except NoPluginError: 48 | console_table([{"node": node, "console": console}]) 49 | else: 50 | # use user specified ssh command 51 | if "CML_CONSOLE_COMMAND" in server.config: 52 | cmd = server.config["CML_CONSOLE_COMMAND"] 53 | cmd = cmd.format(host=server.host, user=server.user, console="open " + console) 54 | print("Calling user specified command: {}".format(cmd)) 55 | exit(call(cmd.split())) 56 | 57 | # someone still uses windows 58 | elif platform.system() == "Windows": 59 | with helpers.disable_file_system_redirection(): 60 | cmd = "ssh -t {}@{} open {}".format(server.user, server.host, console) 61 | exit(call(cmd.split())) 62 | 63 | # why is shit so complicated? 64 | else: 65 | cmd = "ssh -t {}@{} open {}".format(server.user, server.host, console) 66 | exit(call(cmd.split())) 67 | else: 68 | click.secho("Node {} is not active".format(node), fg="red") 69 | exit(1) 70 | else: 71 | click.secho("Node type {} does not support console connectivity".format(node_obj.node_definition), fg="yellow") 72 | else: 73 | click.secho("Unable to find lab {}".format(current_lab), fg="red") 74 | exit(1) 75 | else: 76 | click.secho("No current lab set", fg="red") 77 | exit(1) 78 | -------------------------------------------------------------------------------- /virl/cli/definitions/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.cli.definitions.images import images 4 | from virl.cli.definitions.nodes import nodes 5 | 6 | 7 | @click.group() 8 | def definitions(): 9 | """ 10 | manage image and node definitions 11 | """ 12 | pass 13 | 14 | 15 | definitions.add_command(nodes) 16 | definitions.add_command(images) 17 | -------------------------------------------------------------------------------- /virl/cli/definitions/images/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.cli.definitions.images.export.commands import export 4 | from virl.cli.definitions.images.iimport import iimport 5 | from virl.cli.definitions.images.ls.commands import ls 6 | 7 | 8 | @click.group() 9 | def images(): 10 | """ 11 | manage image definitions 12 | """ 13 | pass 14 | 15 | 16 | images.add_command(ls) 17 | images.add_command(export) 18 | images.add_command(iimport, name="import") 19 | -------------------------------------------------------------------------------- /virl/cli/definitions/images/export/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/definitions/images/export/__init__.py -------------------------------------------------------------------------------- /virl/cli/definitions/images/export/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api import VIRLServer 4 | from virl.helpers import get_cml_client 5 | 6 | 7 | @click.command() 8 | @click.argument("image", nargs=1) 9 | @click.option("-f", "--filename", required=False, metavar="", help="filename to save to") 10 | def export(image, filename): 11 | """ 12 | export an image definition 13 | """ 14 | 15 | server = VIRLServer() 16 | client = get_cml_client(server) 17 | 18 | if not filename: 19 | filename = image + ".yaml" 20 | 21 | defs = client.definitions 22 | 23 | try: 24 | idef = defs.download_image_definition(image) 25 | except Exception as e: 26 | click.secho("Failed to download image definition for {}: {}".format(image, e), fg="red") 27 | exit(1) 28 | else: 29 | with open(filename, "w") as fd: 30 | fd.write(idef) 31 | -------------------------------------------------------------------------------- /virl/cli/definitions/images/iimport/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.cli.definitions.images.iimport.definition.commands import definition 4 | from virl.cli.definitions.images.iimport.image_file.commands import image_file 5 | 6 | 7 | @click.group() 8 | def iimport(): 9 | """ 10 | import images and image definitions 11 | """ 12 | pass 13 | 14 | 15 | iimport.add_command(image_file) 16 | iimport.add_command(definition) 17 | -------------------------------------------------------------------------------- /virl/cli/definitions/images/iimport/definition/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/definitions/images/iimport/definition/__init__.py -------------------------------------------------------------------------------- /virl/cli/definitions/images/iimport/definition/commands.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import click 4 | 5 | from virl.api import VIRLServer 6 | from virl.helpers import get_cml_client 7 | 8 | 9 | @click.command() 10 | @click.option("-f", "--filename", required=True, metavar="", help="path to the local image definition file") 11 | def definition(filename): 12 | """ 13 | import an image definition 14 | """ 15 | 16 | server = VIRLServer() 17 | client = get_cml_client(server) 18 | 19 | if not os.path.isfile(filename): 20 | click.secho("Image definition file {} does not exist or is not a file", fg="red") 21 | exit(1) 22 | else: 23 | defs = client.definitions 24 | contents = None 25 | 26 | with open(filename, "r") as fd: 27 | contents = fd.read() 28 | 29 | try: 30 | defs.upload_image_definition(contents) 31 | except Exception as e: 32 | click.secho("Failed to import image definition for {}: {}".format(filename, e), fg="red") 33 | exit(1) 34 | -------------------------------------------------------------------------------- /virl/cli/definitions/images/iimport/image_file/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/definitions/images/iimport/image_file/__init__.py -------------------------------------------------------------------------------- /virl/cli/definitions/images/iimport/image_file/commands.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import click 4 | 5 | from virl.api import VIRLServer 6 | from virl.helpers import get_cml_client 7 | 8 | 9 | @click.command() 10 | @click.option("-f", "--filename", required=True, metavar="", help="path to local image file") 11 | @click.option("--rename", required=False, metavar="", help="optional new name to give the file on the server") 12 | def image_file(filename, rename): 13 | """ 14 | import an image file 15 | """ 16 | 17 | server = VIRLServer() 18 | client = get_cml_client(server) 19 | 20 | if not os.path.isfile(filename): 21 | click.secho("Image file {} does not exist or is not a file", fg="red") 22 | exit(1) 23 | else: 24 | defs = client.definitions 25 | try: 26 | defs.upload_image_file(filename, rename) 27 | except Exception as e: 28 | click.secho("Failed to import image file {}: {}".format(filename, e), fg="red") 29 | exit(1) 30 | -------------------------------------------------------------------------------- /virl/cli/definitions/images/ls/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/definitions/images/ls/__init__.py -------------------------------------------------------------------------------- /virl/cli/definitions/images/ls/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api import NoPluginError, ViewerPlugin, VIRLServer 4 | from virl.cli.views import image_list_table 5 | from virl.helpers import get_cml_client 6 | 7 | 8 | @click.command() 9 | @click.option("--image", default=None) 10 | def ls(**kwargs): 11 | """ 12 | list all images or the details of a specific image 13 | """ 14 | 15 | image = kwargs.get("image") 16 | server = VIRLServer() 17 | client = get_cml_client(server) 18 | pl = None 19 | 20 | # Regardless of the argument, we have to get all the flavors 21 | # In the case of no arg, we print them all. 22 | # In the case of an arg, we have to go back and get details. 23 | defs = client.definitions.image_definitions() 24 | 25 | try: 26 | pl = ViewerPlugin(viewer="image_def") 27 | except NoPluginError: 28 | pass 29 | 30 | if image: 31 | for f in list(defs): 32 | if f["name"] == image: 33 | if pl: 34 | pl.visualize(image_defs=[f]) 35 | else: 36 | image_list_table([f]) 37 | break 38 | else: 39 | if pl: 40 | pl.visualize(image_defs=defs) 41 | else: 42 | image_list_table(defs) 43 | -------------------------------------------------------------------------------- /virl/cli/definitions/nodes/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.cli.definitions.nodes.export.commands import export 4 | from virl.cli.definitions.nodes.ls.commands import ls 5 | from virl.cli.definitions.nodes.nimport.commands import nimport 6 | 7 | 8 | @click.group() 9 | def nodes(): 10 | """ 11 | manage node definitions 12 | """ 13 | pass 14 | 15 | 16 | nodes.add_command(ls) 17 | nodes.add_command(export) 18 | nodes.add_command(nimport, name="import") 19 | -------------------------------------------------------------------------------- /virl/cli/definitions/nodes/export/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/definitions/nodes/export/__init__.py -------------------------------------------------------------------------------- /virl/cli/definitions/nodes/export/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api import VIRLServer 4 | from virl.helpers import get_cml_client 5 | 6 | 7 | @click.command() 8 | @click.argument("node", nargs=1) 9 | @click.option("-f", "--filename", required=False, metavar="", help="filename to save to") 10 | def export(node, filename): 11 | """ 12 | export a node definition 13 | """ 14 | 15 | server = VIRLServer() 16 | client = get_cml_client(server) 17 | 18 | if not filename: 19 | filename = node + ".yaml" 20 | 21 | defs = client.definitions 22 | 23 | try: 24 | ndef = defs.download_node_definition(node) 25 | except Exception as e: 26 | click.secho("Failed to download node definition for {}: {}".format(node, e), fg="red") 27 | exit(1) 28 | else: 29 | with open(filename, "w") as fd: 30 | fd.write(ndef) 31 | -------------------------------------------------------------------------------- /virl/cli/definitions/nodes/ls/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/definitions/nodes/ls/__init__.py -------------------------------------------------------------------------------- /virl/cli/definitions/nodes/ls/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api import NoPluginError, ViewerPlugin, VIRLServer 4 | from virl.cli.views import node_def_list_table 5 | from virl.helpers import get_cml_client 6 | 7 | 8 | @click.command() 9 | @click.option("--node", default=None) 10 | def ls(**kwargs): 11 | """ 12 | list all node definitions or the details of a specific node definition 13 | """ 14 | 15 | node = kwargs.get("node") 16 | server = VIRLServer() 17 | client = get_cml_client(server) 18 | pl = None 19 | 20 | # Regardless of the argument, we have to get all the node definitions 21 | # In the case of no arg, we print them all. 22 | # In the case of an arg, we have to go back and get details. 23 | defs_orig = client.definitions.node_definitions() 24 | 25 | # Create a new list of the *flattened* node definitions. CML 2.3 removed the 26 | # extra "data" layer of nesting in the node def JSON format. To make the rest 27 | # of the code work no matter which version of CML we're talking to, create a 28 | # list of node definitions, removing the extra layer of nesting if needed. 29 | defs = [f["data"] if "data" in f else f for f in defs_orig] 30 | 31 | try: 32 | pl = ViewerPlugin(viewer="node_def") 33 | except NoPluginError: 34 | pass 35 | 36 | if node: 37 | for f in list(defs): 38 | if f["id"] == node: 39 | if pl: 40 | pl.visualize(node_defs=[f]) 41 | else: 42 | node_def_list_table([f]) 43 | break 44 | else: 45 | if pl: 46 | pl.visualize(node_defs=defs) 47 | else: 48 | node_def_list_table(defs) 49 | -------------------------------------------------------------------------------- /virl/cli/definitions/nodes/nimport/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/definitions/nodes/nimport/__init__.py -------------------------------------------------------------------------------- /virl/cli/definitions/nodes/nimport/commands.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import click 4 | 5 | from virl.api import VIRLServer 6 | from virl.helpers import get_cml_client 7 | 8 | 9 | @click.command() 10 | @click.option("-f", "--filename", required=True, metavar="", help="path to the local node definition file") 11 | def nimport(filename): 12 | """ 13 | import a node definition 14 | """ 15 | 16 | server = VIRLServer() 17 | client = get_cml_client(server) 18 | 19 | if not os.path.isfile(filename): 20 | click.secho("Node definition file {} does not exist or is not a file", fg="red") 21 | exit(1) 22 | else: 23 | defs = client.definitions 24 | contents = None 25 | 26 | with open(filename, "r") as fd: 27 | contents = fd.read() 28 | 29 | try: 30 | defs.upload_node_definition(contents) 31 | except Exception as e: 32 | click.secho("Failed to import node definition: {}".format(e), fg="red") 33 | exit(1) 34 | -------------------------------------------------------------------------------- /virl/cli/down/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/down/__init__.py -------------------------------------------------------------------------------- /virl/cli/down/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api import VIRLServer 4 | from virl.helpers import (get_cml_client, get_current_lab, 5 | safe_join_existing_lab, 6 | safe_join_existing_lab_by_title) 7 | 8 | 9 | @click.command() 10 | @click.option("--id", required=False, help="An existing lab ID to stop (lab-name is ignored)") 11 | @click.option("--lab-name", "-n", "--sim-name", required=False, help="An existing lab name to stop") 12 | def down(id=None, lab_name=None): 13 | """ 14 | stop a lab 15 | """ 16 | server = VIRLServer() 17 | client = get_cml_client(server) 18 | 19 | lab = None 20 | 21 | if id: 22 | lab = safe_join_existing_lab(id, client) 23 | 24 | if not lab and lab_name: 25 | lab = safe_join_existing_lab_by_title(lab_name, client) 26 | 27 | if not lab: 28 | lab_id = get_current_lab() 29 | if lab_id: 30 | lab = safe_join_existing_lab(lab_id, client) 31 | 32 | if lab: 33 | if lab.is_active(): 34 | click.secho("Shutting down lab {} (ID: {}).....".format(lab.title, lab.id)) 35 | lab.stop() 36 | click.echo(click.style("SUCCESS", fg="green")) 37 | else: 38 | click.secho("Lab with ID {} and title {} is already stopped".format(lab.id, lab.title)) 39 | 40 | else: 41 | click.secho("Failed to find lab on server", fg="red") 42 | exit(1) 43 | -------------------------------------------------------------------------------- /virl/cli/extract/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/extract/__init__.py -------------------------------------------------------------------------------- /virl/cli/extract/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api import VIRLServer 4 | from virl.helpers import (cache_lab, extract_configurations, get_cml_client, 5 | get_current_lab, safe_join_existing_lab) 6 | 7 | 8 | @click.command() 9 | @click.option("--update-cache/--no-update-cache", default=True, help="update the local cache (default: True)") 10 | def extract(update_cache, **kwargs): 11 | """ 12 | extract configurations from all nodes in a lab 13 | """ 14 | server = VIRLServer() 15 | client = get_cml_client(server) 16 | 17 | current_lab = get_current_lab() 18 | if current_lab: 19 | lab = safe_join_existing_lab(current_lab, client) 20 | if lab: 21 | extract_configurations(lab) 22 | 23 | if update_cache: 24 | cache_lab(lab, force=True) 25 | else: 26 | click.secho("Failed to find running lab {}".format(current_lab), fg="red") 27 | exit(1) 28 | else: 29 | click.secho("Current lab is not set", fg="red") 30 | exit(1) 31 | -------------------------------------------------------------------------------- /virl/cli/generate/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api import NoPluginError, check_valid_plugin, plugin 4 | from virl.cli.generate.ansible.commands import ansible 5 | from virl.cli.generate.nso.commands import nso 6 | from virl.cli.generate.pyats.commands import pyats 7 | 8 | 9 | @click.group() 10 | def generate(): 11 | """ 12 | generate inv file for various tools 13 | """ 14 | pass 15 | 16 | 17 | def init_generators(): 18 | generate.add_command(ansible) 19 | generate.add_command(pyats) 20 | generate.add_command(nso) 21 | 22 | for gen in plugin.Plugin.get_plugins("generator"): 23 | try: 24 | pl = plugin.GeneratorPlugin(generator=gen) 25 | except NoPluginError: 26 | continue 27 | if not check_valid_plugin(pl, pl.generate, "generate"): 28 | click.secho( 29 | "ERROR: Malformed plugin for generator {}. The `generate` method must be static and a click.command".format(gen), fg="red" 30 | ) 31 | plugin.Plugin.remove_plugin("generator", gen) 32 | else: 33 | generate.add_command(pl.generate, name=gen) 34 | -------------------------------------------------------------------------------- /virl/cli/generate/ansible/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/generate/ansible/__init__.py -------------------------------------------------------------------------------- /virl/cli/generate/ansible/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api import VIRLServer 4 | from virl.generators import ansible_inventory_generator 5 | from virl.helpers import (get_cml_client, get_current_lab, 6 | safe_join_existing_lab) 7 | 8 | 9 | @click.command() 10 | @click.option("--output", "-o", help="output File name ") 11 | @click.option("--style", help="output format (default is yaml)", type=click.Choice(["ini", "yaml"])) 12 | def ansible(**kwargs): 13 | """ 14 | generate ansible inventory 15 | """ 16 | server = VIRLServer() 17 | client = get_cml_client(server) 18 | 19 | current_lab = get_current_lab() 20 | if current_lab: 21 | lab = safe_join_existing_lab(current_lab, client) 22 | if lab: 23 | if kwargs.get("output"): 24 | file_name = kwargs.get("output") 25 | elif kwargs.get("style") == "ini": 26 | file_name = "{}_inventory.ini".format(lab.id) 27 | else: 28 | file_name = "{}_inventory.yaml".format(lab.id) 29 | 30 | inv = None 31 | 32 | if kwargs.get("style") == "ini": 33 | inv = ansible_inventory_generator(lab, server, style="ini") 34 | else: 35 | inv = ansible_inventory_generator(lab, server) 36 | 37 | if inv: 38 | click.secho("Writing {}".format(file_name)) 39 | with open(file_name, "w") as fd: 40 | fd.write(inv) 41 | else: 42 | click.secho("Failed to get inventory data", fg="red") 43 | exit(1) 44 | else: 45 | click.secho("Failed to find running lab {}".format(current_lab), fg="red") 46 | exit(1) 47 | else: 48 | click.secho("Current lab is not set", fg="red") 49 | exit(1) 50 | -------------------------------------------------------------------------------- /virl/cli/generate/nso/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/generate/nso/__init__.py -------------------------------------------------------------------------------- /virl/cli/generate/nso/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api import VIRLServer 4 | from virl.api.nso import NSO 5 | from virl.cli.views import sync_table 6 | from virl.generators import nso_payload_generator 7 | from virl.helpers import (get_cml_client, get_current_lab, 8 | safe_join_existing_lab) 9 | 10 | 11 | @click.command() 12 | @click.option("--output", "-o", help="just dump the payload to file without sending") 13 | @click.option("--syncfrom/--no-syncfrom", default=False, help="Perform sync-from after updating devices") 14 | def nso(syncfrom, **kwargs): 15 | """ 16 | generate nso inventory 17 | """ 18 | 19 | server = VIRLServer() 20 | client = get_cml_client(server) 21 | 22 | current_lab = get_current_lab() 23 | if current_lab: 24 | lab = safe_join_existing_lab(current_lab, client) 25 | if lab: 26 | if kwargs.get("output"): 27 | file_name = kwargs.get("output") 28 | else: 29 | file_name = None 30 | 31 | inv = nso_payload_generator(lab, server) 32 | 33 | if inv: 34 | if file_name: 35 | click.secho("Writing {}".format(file_name)) 36 | with open(file_name, "w") as fd: 37 | fd.write(inv) 38 | else: 39 | click.secho("Updating NSO....") 40 | nso_obj = NSO() 41 | nso_response = nso_obj.update_devices(inv) 42 | if nso_response.ok: 43 | click.secho("Successfully added CML devices to NSO") 44 | else: 45 | click.secho("Error updating NSO: ", fg="red") 46 | click.secho(nso_response.text) 47 | if syncfrom: 48 | resp = nso_obj.perform_sync_from() 49 | sync_table(resp.json()) 50 | else: 51 | click.secho("Failed to get inventory data", fg="red") 52 | exit(1) 53 | else: 54 | click.secho("Failed to find running lab {}".format(current_lab), fg="red") 55 | exit(1) 56 | else: 57 | click.secho("Current lab is not set", fg="red") 58 | exit(1) 59 | -------------------------------------------------------------------------------- /virl/cli/generate/pyats/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/generate/pyats/__init__.py -------------------------------------------------------------------------------- /virl/cli/generate/pyats/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api import VIRLServer 4 | from virl.generators import pyats_testbed_generator 5 | from virl.helpers import (get_cml_client, get_current_lab, 6 | safe_join_existing_lab) 7 | 8 | 9 | @click.command() 10 | @click.option("--output", "-o", help="output File name") 11 | def pyats(**kwargs): 12 | """ 13 | generates a pyats testbed config for a lab 14 | """ 15 | server = VIRLServer() 16 | client = get_cml_client(server) 17 | 18 | current_lab = get_current_lab() 19 | if current_lab: 20 | lab = safe_join_existing_lab(current_lab, client) 21 | if lab: 22 | if kwargs.get("output"): 23 | # user specified output filename 24 | file_name = kwargs.get("output") 25 | else: 26 | # writes to _testbed.yaml by default 27 | file_name = "{}_testbed.yaml".format(lab.id) 28 | 29 | testbed = pyats_testbed_generator(lab) 30 | 31 | if testbed: 32 | click.secho("Writing {}".format(file_name)) 33 | with open(file_name, "w") as fd: 34 | fd.write(testbed) 35 | else: 36 | click.secho("Failed to get testbed data", fg="red") 37 | exit(1) 38 | else: 39 | click.secho("Failed to find running lab {}".format(current_lab), fg="red") 40 | exit(1) 41 | else: 42 | click.secho("Current lab is not set", fg="red") 43 | exit(1) 44 | -------------------------------------------------------------------------------- /virl/cli/groups/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.cli.groups.create.commands import create_groups 4 | from virl.cli.groups.delete.commands import delete_groups 5 | from virl.cli.groups.ls.commands import list_groups 6 | from virl.cli.groups.update.commands import update_groups 7 | 8 | 9 | @click.group() 10 | def groups(): 11 | """ 12 | manage groups 13 | """ 14 | pass 15 | 16 | 17 | groups.add_command(list_groups, name="ls") 18 | groups.add_command(create_groups, name="create") 19 | groups.add_command(update_groups, name="update") 20 | groups.add_command(delete_groups, name="delete") 21 | -------------------------------------------------------------------------------- /virl/cli/groups/create/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/groups/create/__init__.py -------------------------------------------------------------------------------- /virl/cli/groups/create/commands.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import click 4 | 5 | from virl.api import VIRLServer 6 | from virl.helpers import get_cml_client 7 | 8 | 9 | @click.command() 10 | @click.option("--member", help="Assign one or more users to the groups (e.g., --member user1 --member user2)", multiple=True) 11 | @click.option("--add-all-users", help="Assign all users to the groups", is_flag=True) 12 | @click.option( 13 | "--lab", 14 | type=(str, click.Choice(["read_only", "read_write"])), 15 | help="Labs to assign the groups (e.g, --labs lab_id1 read_only --labs lab_id2 read_write)", 16 | default=[], 17 | multiple=True, 18 | metavar="lab_id [read_only|read_write]", 19 | ) 20 | @click.option( 21 | "--add-all-labs", 22 | type=click.Choice(["read_only", "read_write"]), 23 | help="Assign all labs to the groups with either read_only or read_write permissions", 24 | ) 25 | @click.argument("groupnames", nargs=-1, required=True) 26 | def create_groups(groupnames, member, add_all_users, lab, add_all_labs): 27 | """ 28 | Create one or more groups (e.g., group1 group2) 29 | """ 30 | 31 | server = VIRLServer() 32 | client = get_cml_client(server) 33 | 34 | all_users = client.user_management.users() 35 | all_users_ids = [u["id"] for u in all_users] 36 | members_ids = all_users_ids if add_all_users else [u["id"] for u in all_users if u["username"] in member] 37 | 38 | lab_ids = [{"id": lab_id, "permission": permission} for lab_id, permission in lab] 39 | lab_ids = None if add_all_labs is None else [{"id": lid, "permission": add_all_labs} for lid in client.get_lab_list()] 40 | 41 | for name in groupnames: 42 | kwargs = { 43 | "name": name, 44 | "members": members_ids, 45 | "labs": lab_ids, 46 | } 47 | try: 48 | client.group_management.create_group(**kwargs) 49 | click.secho(f"Group {name} successfully created", fg="green") 50 | except Exception as e: 51 | click.secho(f"Failed to create group: {e}", fg="red") 52 | sys.exit(1) 53 | -------------------------------------------------------------------------------- /virl/cli/groups/delete/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/groups/delete/__init__.py -------------------------------------------------------------------------------- /virl/cli/groups/delete/commands.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import click 4 | 5 | from virl.api import VIRLServer 6 | from virl.helpers import get_cml_client 7 | 8 | 9 | @click.command() 10 | @click.argument("groupnames", nargs=-1, required=True) 11 | def delete_groups(groupnames): 12 | """ 13 | Delete one or more groups (e.g., group1 group2) 14 | """ 15 | 16 | server = VIRLServer() 17 | client = get_cml_client(server) 18 | group_mapping = {g["name"]: g["id"] for g in client.group_management.groups()} 19 | 20 | for groupname in groupnames: 21 | try: 22 | group_id = group_mapping[groupname] 23 | client.group_management.delete_group(group_id) 24 | click.secho(f"Group {groupname} successfully deleted", fg="green") 25 | except KeyError: 26 | click.secho(f"Group {groupname} not found", fg="red") 27 | sys.exit(1) 28 | except Exception as e: 29 | click.secho(f"Failed to delete group: {e}", fg="red") 30 | sys.exit(1) 31 | -------------------------------------------------------------------------------- /virl/cli/groups/ls/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/groups/ls/__init__.py -------------------------------------------------------------------------------- /virl/cli/groups/ls/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api import NoPluginError, ViewerPlugin, VIRLServer 4 | from virl.cli.views import group_list_table 5 | from virl.helpers import get_cml_client 6 | 7 | 8 | @click.command() 9 | @click.option("-v", "--verbose", is_flag=True, help="Include user IDs in the output") 10 | def list_groups(verbose): 11 | """ 12 | List all groups on the server 13 | """ 14 | server = VIRLServer() 15 | client = get_cml_client(server) 16 | user_mapping = {u["id"]: u["username"] for u in client.user_management.users()} 17 | labs_mapping = {lab.id: lab.title for lab in client.all_labs(show_all=True)} 18 | groups = client.group_management.groups() 19 | for group in groups: 20 | group["members"] = [user_mapping[uid] for uid in group["members"]] 21 | group["labs"] = [{"title": labs_mapping[lab["id"]], "permission": lab["permission"]} for lab in group["labs"]] 22 | try: 23 | pl = ViewerPlugin(viewer="group") 24 | pl.visualize(groups=groups) 25 | except NoPluginError: 26 | group_list_table(groups, verbose=verbose) 27 | -------------------------------------------------------------------------------- /virl/cli/groups/update/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/groups/update/__init__.py -------------------------------------------------------------------------------- /virl/cli/groups/update/commands.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import click 4 | 5 | from virl.api import VIRLServer 6 | from virl.helpers import get_cml_client 7 | 8 | 9 | @click.command() 10 | @click.option("--member", help="Assign one or more users to the groups (e.g., --member user1 --member user2)", multiple=True) 11 | @click.option("--add-all-users", help="Assign all users to the groups", is_flag=True) 12 | @click.option( 13 | "--lab", 14 | type=(str, click.Choice(["read_only", "read_write"])), 15 | help="Labs to assign the groups", 16 | default=[], 17 | multiple=True, 18 | metavar="lab_id [read_only|read_write]", 19 | ) 20 | @click.option( 21 | "--add-all-labs", 22 | type=click.Choice(["read_only", "read_write"]), 23 | help="Assign all labs to the groups with either read_only or read_write permissions", 24 | ) 25 | @click.argument("groupnames", nargs=-1, required=True) 26 | def update_groups(groupnames, member, add_all_users, lab, add_all_labs): 27 | """ 28 | Update one or more groups (e.g., group1 group2) 29 | """ 30 | 31 | server = VIRLServer() 32 | client = get_cml_client(server) 33 | 34 | all_users = client.user_management.users() 35 | all_users_ids = [u["id"] for u in all_users] 36 | members_ids = all_users_ids if add_all_users else [u["id"] for u in all_users if u["username"] in member] 37 | members_ids = members_ids if members_ids else None 38 | 39 | lab_ids = [{"id": lab_id, "permission": permission} for lab_id, permission in lab] 40 | lab_ids = None if add_all_labs is None else [{"id": lid, "permission": add_all_labs} for lid in client.get_lab_list()] 41 | lab_ids = lab_ids if lab_ids else None 42 | 43 | groups = client.group_management.groups() 44 | group_mapping = {group["name"]: group["id"] for group in groups} 45 | for name in groupnames: 46 | group_id = group_mapping[name] 47 | kwargs = { 48 | "group_id": group_id, 49 | "members": members_ids, 50 | "labs": lab_ids, 51 | } 52 | # only pass kwargs that are not None 53 | kwargs = {k: v for k, v in kwargs.items() if v is not None} 54 | try: 55 | client.group_management.update_group(**kwargs) 56 | click.secho(f"Group {name} successfully updated", fg="green") 57 | except Exception as e: 58 | click.secho(f"Failed to update group: {e}", fg="red") 59 | sys.exit(1) 60 | -------------------------------------------------------------------------------- /virl/cli/id/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/id/__init__.py -------------------------------------------------------------------------------- /virl/cli/id/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api import CachedLab, VIRLServer 4 | from virl.helpers import (get_cml_client, get_current_lab, 5 | get_current_lab_link, safe_join_existing_lab) 6 | 7 | 8 | @click.command() 9 | def lid(): 10 | """ 11 | get the current lab title and ID 12 | """ 13 | server = VIRLServer() 14 | client = get_cml_client(server) 15 | current_lab = get_current_lab() 16 | if current_lab: 17 | lab = safe_join_existing_lab(current_lab, client) 18 | # The lab really should be on the server. 19 | if not lab: 20 | try: 21 | lab = CachedLab(current_lab, get_current_lab_link()) 22 | except Exception: 23 | pass 24 | 25 | if lab: 26 | click.echo("{} (ID: {})".format(lab.title, current_lab)) 27 | else: 28 | click.secho("Current lab is set to {}, but is not on server or in cache!".format(current_lab), fg="red") 29 | -------------------------------------------------------------------------------- /virl/cli/license/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.cli.license.deregister.commands import deregister as deregisterc 4 | from virl.cli.license.features import features as featuresc 5 | from virl.cli.license.register.commands import register as registerc 6 | from virl.cli.license.renew import renew as renewc 7 | from virl.cli.license.show.commands import show as showc 8 | 9 | 10 | @click.group() 11 | def license(): 12 | """ 13 | work with product licensing 14 | """ 15 | pass 16 | 17 | 18 | license.add_command(showc, name="show") 19 | license.add_command(registerc, name="register") 20 | license.add_command(renewc, name="renew") 21 | license.add_command(deregisterc, name="deregister") 22 | license.add_command(featuresc, name="features") 23 | -------------------------------------------------------------------------------- /virl/cli/license/deregister/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/license/deregister/__init__.py -------------------------------------------------------------------------------- /virl/cli/license/deregister/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api import VIRLServer 4 | from virl.helpers import get_cml_client 5 | 6 | 7 | @click.command() 8 | @click.option( 9 | "--confirm/--no-confirm", 10 | show_default=False, 11 | default=True, 12 | help="Do not prompt for confirmation (default: prompt)", 13 | required=False, 14 | ) 15 | def deregister(confirm): 16 | """ 17 | deregister the Smart License 18 | """ 19 | server = VIRLServer() 20 | client = get_cml_client(server) 21 | licensing = client.licensing 22 | 23 | ret = "y" 24 | if confirm: 25 | ret = input("Are you sure you want to deregister [y/N]? ") 26 | if not ret.lower().startswith("y"): 27 | click.secho("Not deregistering") 28 | exit(0) 29 | 30 | try: 31 | licensing.deregister() 32 | except Exception as e: 33 | click.secho("Failed to deregister with Smart Licensing: {}".format(e), fg="red") 34 | exit(1) 35 | -------------------------------------------------------------------------------- /virl/cli/license/features/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.cli.license.features.show.commands import show as showc 4 | from virl.cli.license.features.update.commands import update as updatec 5 | 6 | 7 | @click.group() 8 | def features(): 9 | """ 10 | work with licensed features 11 | """ 12 | pass 13 | 14 | 15 | features.add_command(showc, name="show") 16 | features.add_command(updatec, name="update") 17 | -------------------------------------------------------------------------------- /virl/cli/license/features/show/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/license/features/show/__init__.py -------------------------------------------------------------------------------- /virl/cli/license/features/show/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api import NoPluginError, ViewerPlugin, VIRLServer 4 | from virl.cli.views import license_features_table 5 | from virl.helpers import get_cml_client 6 | 7 | 8 | @click.command() 9 | def show(): 10 | """ 11 | display license details 12 | """ 13 | server = VIRLServer() 14 | client = get_cml_client(server) 15 | licensing = client.licensing 16 | 17 | try: 18 | license = licensing.features() 19 | except Exception as e: 20 | click.secho("Failed to get license features: {}".format(e), fg="red") 21 | exit(1) 22 | else: 23 | try: 24 | pl = ViewerPlugin(viewer="license_feature") 25 | pl.visualize(features=license) 26 | except NoPluginError: 27 | license_features_table(license) 28 | -------------------------------------------------------------------------------- /virl/cli/license/features/update/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/license/features/update/__init__.py -------------------------------------------------------------------------------- /virl/cli/license/features/update/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api import VIRLServer 4 | from virl.helpers import get_cml_client 5 | 6 | 7 | @click.command() 8 | @click.option( 9 | "--id", 10 | "-i", 11 | required=True, 12 | help="ID for the Smart License feature to modify", 13 | ) 14 | @click.option("--value", "-v", required=True, type=int, help="Number of licenses of this feature to use") 15 | def update(id, value): 16 | """ 17 | update the number of feature instances 18 | """ 19 | server = VIRLServer() 20 | client = get_cml_client(server) 21 | licensing = client.licensing 22 | 23 | try: 24 | licensing.update_features({id: value}) 25 | except Exception as e: 26 | click.secho("Failed to update features: {}".format(e), fg="red") 27 | exit(1) 28 | -------------------------------------------------------------------------------- /virl/cli/license/register/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/license/register/__init__.py -------------------------------------------------------------------------------- /virl/cli/license/register/commands.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import click 4 | 5 | from virl.api import VIRLServer 6 | from virl.helpers import get_cml_client 7 | 8 | 9 | @click.command() 10 | @click.option( 11 | "--token", 12 | "-t", 13 | required=True, 14 | help="Smart License token for registration", 15 | ) 16 | @click.option( 17 | "--reregister/--no-reregister", 18 | "--force/--no-force", 19 | required=False, 20 | default=False, 21 | help="Force registration even if already registered", 22 | ) 23 | @click.option("--smart-license-server", "-s", required=False, help="URL for the Smart License server or satellite") 24 | @click.option("--proxy-host", "-p", required=False, help="Hostname or IP address of proxy to use for registration (if required)") 25 | @click.option("--proxy-port", "-o", required=False, default=80, help="Port number to use for proxy host (default: 80)") 26 | @click.option("--certificate", "-c", required=False, help="Path to a PEM-encoded certificate for the Smart License SSMS") 27 | def register(token, **kwargs): 28 | """ 29 | register with a Smart License account 30 | """ 31 | ssms = kwargs["smart_license_server"] 32 | proxy = kwargs["proxy_host"] 33 | port = None 34 | cert = kwargs["certificate"] 35 | reregister = kwargs["reregister"] 36 | server = VIRLServer() 37 | client = get_cml_client(server) 38 | licensing = client.licensing 39 | 40 | if ssms or proxy: 41 | if not ssms: 42 | ssms = licensing.status()["transport"]["default_ssms"] 43 | if proxy: 44 | port = kwargs["proxy_port"] 45 | 46 | try: 47 | licensing.set_transport(ssms, proxy, port) 48 | except Exception as e: 49 | click.secho("Failed to configure Smart License server and proxy: {}".format(e), fg="red") 50 | exit(1) 51 | else: 52 | try: 53 | licensing.delete_certificate() 54 | except Exception: 55 | pass 56 | 57 | licensing.set_default_transport() 58 | 59 | if cert: 60 | if not os.path.isfile(cert): 61 | click.secho("Certificate {} is not a valid file!".format(cert), fg="red") 62 | exit(1) 63 | 64 | with open(cert, "r") as fd: 65 | contents = fd.read() 66 | try: 67 | licensing.upload_certificate(contents) 68 | except Exception as e: 69 | click.secho("Failed to upload certificate {}: {}".format(cert, e), fg="red") 70 | exit(1) 71 | 72 | try: 73 | licensing.register(token, reregister) 74 | except Exception as e: 75 | click.secho("Failed to register with Smart Licensing: {}".format(e), fg="red") 76 | exit(1) 77 | -------------------------------------------------------------------------------- /virl/cli/license/renew/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.cli.license.renew.authorization.commands import \ 4 | authorization as authorizationc 5 | from virl.cli.license.renew.registration.commands import \ 6 | registration as registrationc 7 | 8 | 9 | @click.group() 10 | def renew(): 11 | """ 12 | renew registration or authorization 13 | """ 14 | pass 15 | 16 | 17 | renew.add_command(registrationc, name="registration") 18 | renew.add_command(authorizationc, name="authorization") 19 | -------------------------------------------------------------------------------- /virl/cli/license/renew/authorization/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/license/renew/authorization/__init__.py -------------------------------------------------------------------------------- /virl/cli/license/renew/authorization/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api import VIRLServer 4 | from virl.helpers import get_cml_client 5 | 6 | 7 | @click.command() 8 | def authorization(): 9 | """ 10 | renew Smart License authorization 11 | """ 12 | server = VIRLServer() 13 | client = get_cml_client(server) 14 | licensing = client.licensing 15 | 16 | try: 17 | licensing.renew_authorization() 18 | except Exception as e: 19 | click.secho("Failed to renew authorization: {}".format(e), fg="red") 20 | exit(1) 21 | -------------------------------------------------------------------------------- /virl/cli/license/renew/registration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/license/renew/registration/__init__.py -------------------------------------------------------------------------------- /virl/cli/license/renew/registration/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api import VIRLServer 4 | from virl.helpers import get_cml_client 5 | 6 | 7 | @click.command() 8 | def registration(): 9 | """ 10 | renew Smart License registration 11 | """ 12 | server = VIRLServer() 13 | client = get_cml_client(server) 14 | licensing = client.licensing 15 | 16 | try: 17 | licensing.register_renew() 18 | except Exception as e: 19 | click.secho("Failed to renew registration: {}".format(e), fg="red") 20 | exit(1) 21 | -------------------------------------------------------------------------------- /virl/cli/license/show/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/license/show/__init__.py -------------------------------------------------------------------------------- /virl/cli/license/show/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api import NoPluginError, ViewerPlugin, VIRLServer 4 | from virl.cli.views import license_details_table 5 | from virl.helpers import get_cml_client 6 | 7 | 8 | @click.command() 9 | def show(): 10 | """ 11 | display license details 12 | """ 13 | server = VIRLServer() 14 | client = get_cml_client(server) 15 | licensing = client.licensing 16 | 17 | try: 18 | license = licensing.status() 19 | except Exception as e: 20 | click.secho("Failed to get license details: {}".format(e), fg="red") 21 | exit(1) 22 | else: 23 | try: 24 | pl = ViewerPlugin(viewer="license") 25 | pl.visualize(license=license) 26 | except NoPluginError: 27 | license_details_table(license) 28 | -------------------------------------------------------------------------------- /virl/cli/ls/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/ls/__init__.py -------------------------------------------------------------------------------- /virl/cli/ls/commands.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import click 4 | 5 | from virl.api import CachedLab, NoPluginError, ViewerPlugin, VIRLServer 6 | from virl.cli.views import lab_list_table 7 | from virl.helpers import get_cache_root, get_cml_client 8 | 9 | 10 | @click.command() 11 | @click.option( 12 | "--all/--server", 13 | default=False, 14 | show_default=False, 15 | required=False, 16 | help="Display cached labs in addition to those on the server (default: server labs only)", 17 | ) 18 | @click.option( 19 | "--all-users/--only-me", 20 | default=False, 21 | show_default=False, 22 | required=False, 23 | help="Display labs for all users (only if current user is an admin) (default: only show labs owned by me)", 24 | ) 25 | def ls(all, all_users): 26 | """ 27 | lists running labs and optionally those in the cache 28 | """ 29 | server = VIRLServer() 30 | client = get_cml_client(server) 31 | labs = [] 32 | cached_labs = None 33 | users = client.user_management.users() 34 | ownerids_usernames = {u["id"]: u["username"] for u in users} 35 | 36 | lab_ids = client.get_lab_list(all_users) 37 | for id in lab_ids: 38 | labs.append(client.join_existing_lab(id)) 39 | 40 | if all: 41 | cached_labs = [] 42 | cache_root = get_cache_root() 43 | if os.path.isdir(cache_root): 44 | for f in os.listdir(cache_root): 45 | lab_id = f 46 | cached_labs.append(CachedLab(lab_id, cache_root + "/" + f)) 47 | 48 | try: 49 | pl = ViewerPlugin(viewer="lab") 50 | pl.visualize(labs=labs, ownerids_usernames=ownerids_usernames, cached_labs=cached_labs) 51 | except NoPluginError: 52 | lab_list_table(labs, ownerids_usernames, cached_labs=cached_labs) 53 | -------------------------------------------------------------------------------- /virl/cli/nodes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/nodes/__init__.py -------------------------------------------------------------------------------- /virl/cli/nodes/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api import NoPluginError, ViewerPlugin, VIRLServer 4 | from virl.cli.views import node_list_table 5 | from virl.helpers import (get_cml_client, get_current_lab, 6 | safe_join_existing_lab) 7 | 8 | 9 | @click.command() 10 | def nodes(): 11 | """ 12 | get node list for the current lab 13 | """ 14 | server = VIRLServer() 15 | client = get_cml_client(server) 16 | 17 | current_lab = get_current_lab() 18 | if current_lab: 19 | lab = safe_join_existing_lab(current_lab, client) 20 | if lab: 21 | # Force an operational sync. 22 | try: 23 | lab.sync_operational_if_outdated() 24 | except Exception: 25 | pass 26 | 27 | computes = {} 28 | try: 29 | computes = client.get_system_health()["computes"] 30 | except Exception: 31 | pass 32 | 33 | try: 34 | pl = ViewerPlugin(viewer="node") 35 | pl.visualize(nodes=lab.nodes(), computes=computes) 36 | except NoPluginError: 37 | node_list_table(lab.nodes(), computes) 38 | else: 39 | click.secho("Lab {} is not running".format(current_lab), fg="red") 40 | exit(1) 41 | else: 42 | click.secho("No current lab selected", fg="red") 43 | exit(1) 44 | -------------------------------------------------------------------------------- /virl/cli/pull/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/pull/__init__.py -------------------------------------------------------------------------------- /virl/cli/pull/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | import requests 3 | 4 | 5 | def do_pull(repo, fname, branch="master", recurse=False): 6 | click.secho("Pulling {} from {} on branch {}".format(fname, repo, branch)) 7 | url = "https://raw.githubusercontent.com/" 8 | url = url + "{}/{}/{}".format(repo, branch, fname) 9 | resp = requests.get(url) 10 | if resp.ok: 11 | with open(fname, "w") as fh: 12 | fh.write(resp.text) 13 | click.secho("Saved topology as {}".format(fname), fg="green") 14 | return True 15 | else: 16 | click.secho("Error pulling {} from {} on branch {} - repo, file, or branch not found".format(fname, repo, branch), fg="red") 17 | return False 18 | 19 | 20 | @click.command() 21 | @click.argument("repo") 22 | @click.option("--file", default="topology.yaml", required=False, help="Filename to pull (default: topology.yaml)") 23 | @click.option("--branch", default="main", required=False, help="Branch name from which to pull (default: main)") 24 | def pull(repo, file, branch): 25 | """ 26 | pull CML lab YAML file from repo 27 | """ 28 | ret = do_pull(repo, fname=file, branch=branch) 29 | if not ret: 30 | exit(1) 31 | -------------------------------------------------------------------------------- /virl/cli/rm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/rm/__init__.py -------------------------------------------------------------------------------- /virl/cli/rm/commands.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import click 4 | 5 | from virl.api import VIRLServer 6 | from virl.helpers import (check_lab_cache, clear_current_lab, get_cml_client, 7 | get_current_lab, safe_join_existing_lab) 8 | 9 | 10 | @click.command() 11 | @click.option( 12 | "--force/--no-force", 13 | "-f", 14 | default=False, 15 | required=False, 16 | help="Stop and/or wipe a lab (if it's started) then remove it (default: False)", 17 | ) 18 | @click.option( 19 | "--confirm/--no-confirm", 20 | show_default=False, 21 | default=True, 22 | help="Do not prompt for confirmation (default: prompt)", 23 | required=False, 24 | ) 25 | @click.option( 26 | "--from-cache/--no-from-cache", 27 | default=False, 28 | required=False, 29 | show_default=False, 30 | help="Remove the lab from the cache (default: do not remove from cache)", 31 | ) 32 | def rm(force, confirm, from_cache): 33 | """ 34 | remove a lab 35 | """ 36 | server = VIRLServer() 37 | client = get_cml_client(server) 38 | 39 | current_lab = get_current_lab() 40 | if current_lab: 41 | lab = safe_join_existing_lab(current_lab, client) 42 | if lab: 43 | if lab.is_active() and force: 44 | lab.stop(wait=True) 45 | 46 | if lab.state() != "DEFINED_ON_CORE" and force: 47 | lab.wipe(wait=True) 48 | 49 | # Check again just to be sure. 50 | if lab.state() == "DEFINED_ON_CORE": 51 | ret = "y" 52 | if confirm: 53 | ret = input("Are you sure you want to remove lab {} (ID: {}) [y/N]? ".format(lab.title, current_lab)) 54 | if ret.lower().startswith("y"): 55 | # We need to save the lab's title before we remove it. 56 | title = lab.title 57 | lab.remove() 58 | click.secho("Lab {} (ID: {}) removed".format(title, current_lab)) 59 | if from_cache: 60 | try: 61 | os.remove(check_lab_cache(current_lab)) 62 | except OSError: 63 | # File doesn't exist. 64 | pass 65 | 66 | click.secho("Removed lab {} from cache".format(current_lab)) 67 | clear_current_lab() 68 | else: 69 | click.secho("Not removing lab {} (ID: {})".format(lab.title, current_lab)) 70 | 71 | else: 72 | click.secho( 73 | "Lab {} (ID: {}) is either active or not wiped; either down and wipe it or use --force".format(lab.title, current_lab), 74 | fg="red", 75 | ) 76 | exit(1) 77 | else: 78 | click.secho("Unable to find lab {}".format(current_lab), fg="red") 79 | exit(1) 80 | else: 81 | click.secho("Current lab is not set", fg="red") 82 | exit(1) 83 | -------------------------------------------------------------------------------- /virl/cli/save/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/save/__init__.py -------------------------------------------------------------------------------- /virl/cli/save/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api import VIRLServer 4 | from virl.helpers import (extract_configurations, get_cml_client, 5 | get_current_lab, safe_join_existing_lab) 6 | 7 | 8 | @click.command() 9 | @click.option("--extract/--no-extract", default=True, help="extract the configurations from devices before export (default: True)") 10 | @click.option( 11 | "-f", "--filename", required=False, default="topology.yaml", metavar="", help="filename to save to, defaults to topology.yaml" 12 | ) 13 | def save(extract, filename, **kwargs): 14 | """ 15 | save lab to a local yaml file 16 | """ 17 | server = VIRLServer() 18 | client = get_cml_client(server) 19 | 20 | current_lab = get_current_lab() 21 | if current_lab: 22 | lab = safe_join_existing_lab(current_lab, client) 23 | if lab: 24 | if extract: 25 | click.secho("Extracting configurations...") 26 | extract_configurations(lab) 27 | 28 | lab_export = lab.download() 29 | 30 | click.secho("Writing {}".format(filename)) 31 | with open(filename, "w") as fd: 32 | fd.write(lab_export) 33 | else: 34 | click.secho("Failed to find running lab {}".format(current_lab), fg="red") 35 | exit(1) 36 | else: 37 | click.secho("Current lab is not set", fg="red") 38 | exit(1) 39 | -------------------------------------------------------------------------------- /virl/cli/search/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/search/__init__.py -------------------------------------------------------------------------------- /virl/cli/search/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api import NoPluginError, ViewerPlugin 4 | from virl.api.github import get_repos 5 | from virl.cli.views.search import repo_table 6 | 7 | 8 | @click.command() 9 | @click.argument("query", required=False) 10 | @click.option("--org", default="virlfiles", required=False, help="GitHub organization to search (default: virlfiles)") 11 | def search(query=None, **kwargs): 12 | """ 13 | list topologies available via github 14 | """ 15 | 16 | repos = get_repos(org=kwargs["org"], query=query) 17 | if query is not None: 18 | click.secho("Displaying {} Results For {}".format(len(repos), query)) 19 | else: 20 | click.secho("Displaying {} Results".format(len(repos))) 21 | try: 22 | pl = ViewerPlugin(viewer="search") 23 | pl.visualize(repos=repos) 24 | except NoPluginError: 25 | repo_table(repos) 26 | -------------------------------------------------------------------------------- /virl/cli/ssh/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/ssh/__init__.py -------------------------------------------------------------------------------- /virl/cli/ssh/commands.py: -------------------------------------------------------------------------------- 1 | from subprocess import call 2 | 3 | import click 4 | from virl2_client.exceptions import NodeNotFound 5 | 6 | from virl.api import VIRLServer 7 | from virl.helpers import (get_cml_client, get_current_lab, get_node_mgmt_ip, 8 | safe_join_existing_lab) 9 | 10 | 11 | @click.command() 12 | @click.argument("node", nargs=1) 13 | def ssh(node): 14 | """ 15 | ssh to a node 16 | """ 17 | server = VIRLServer() 18 | client = get_cml_client(server) 19 | username = server.config.get("VIRL_SSH_USERNAME", "cisco") 20 | 21 | current_lab = get_current_lab() 22 | if current_lab: 23 | lab = safe_join_existing_lab(current_lab, client) 24 | if lab: 25 | try: 26 | node_obj = lab.get_node_by_label(node) 27 | except NodeNotFound: 28 | click.secho("Node {} was not found in lab {}".format(node, current_lab), fg="red") 29 | exit(1) 30 | 31 | if node_obj.is_active(): 32 | mgmtip = get_node_mgmt_ip(node_obj) 33 | if mgmtip: 34 | if "VIRL_SSH_COMMAND" in server.config: 35 | cmd = server.config["VIRL_SSH_COMMAND"] 36 | cmd = cmd.format(host=mgmtip, username=username) 37 | print("Calling user specified command: {}".format(cmd)) 38 | exit(call(cmd.split())) 39 | else: 40 | click.secho("Attemping ssh connection to {} at {}".format(node_obj.label, mgmtip)) 41 | 42 | exit(call(["ssh", "{}@{}".format(username, mgmtip)])) 43 | else: 44 | click.secho("Node {} does not have an external management IP".format(node_obj.label)) 45 | else: 46 | click.secho("Node {} is not active".format(node_obj.label), fg="yellow") 47 | else: 48 | click.secho("Unable to find lab {}".format(current_lab), fg="red") 49 | exit(1) 50 | else: 51 | click.secho("No current lab set", fg="red") 52 | exit(1) 53 | -------------------------------------------------------------------------------- /virl/cli/start/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/start/__init__.py -------------------------------------------------------------------------------- /virl/cli/start/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | from subprocess import call 3 | from virl2_client.exceptions import NodeNotFound 4 | 5 | from virl.api import VIRLServer 6 | from virl.helpers import get_cml_client, get_current_lab, safe_join_existing_lab, get_command 7 | 8 | 9 | @click.command() 10 | @click.argument("node", required=False) 11 | @click.option("--id", required=False, help="An existing node ID to start (the node name argument is ignored)") 12 | def start(node, id): 13 | """ 14 | start a node 15 | """ 16 | if not node and not id: 17 | exit(call([get_command(), "start", "--help"])) 18 | 19 | server = VIRLServer() 20 | client = get_cml_client(server) 21 | 22 | current_lab = get_current_lab() 23 | if current_lab: 24 | lab = safe_join_existing_lab(current_lab, client) 25 | if lab: 26 | try: 27 | if id: 28 | node_obj = lab.get_node_by_id(id) 29 | else: 30 | node_obj = lab.get_node_by_label(node) 31 | 32 | if not node_obj.is_active(): 33 | node_obj.start(wait=True) 34 | click.secho("Started node {}".format(node_obj.label)) 35 | else: 36 | click.secho("Node {} is already active".format(node_obj.label), fg="yellow") 37 | except NodeNotFound: 38 | click.secho("Node {} was not found in lab {}".format(id if id else node, current_lab), fg="red") 39 | exit(1) 40 | else: 41 | click.secho("Unable to find lab {}".format(current_lab), fg="red") 42 | exit(1) 43 | else: 44 | click.secho("No current lab set", fg="red") 45 | exit(1) 46 | -------------------------------------------------------------------------------- /virl/cli/stop/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/stop/__init__.py -------------------------------------------------------------------------------- /virl/cli/stop/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | from subprocess import call 3 | 4 | from virl2_client.exceptions import NodeNotFound 5 | 6 | from virl.api import VIRLServer 7 | from virl.helpers import get_cml_client, get_current_lab, safe_join_existing_lab, get_command 8 | 9 | 10 | @click.command() 11 | @click.argument("node", required=False) 12 | @click.option("--id", required=False, help="An existing node ID to stop (the node name argument is ignored)") 13 | def stop(node, id): 14 | """ 15 | stop a node 16 | """ 17 | if not node and not id: 18 | exit(call([get_command(), "stop", "--help"])) 19 | 20 | server = VIRLServer() 21 | client = get_cml_client(server) 22 | 23 | current_lab = get_current_lab() 24 | if current_lab: 25 | lab = safe_join_existing_lab(current_lab, client) 26 | if lab: 27 | try: 28 | if id: 29 | node_obj = lab.get_node_by_id(id) 30 | else: 31 | node_obj = lab.get_node_by_label(node) 32 | 33 | if node_obj.is_active(): 34 | node_obj.stop(wait=True) 35 | click.secho("Stopped node {}".format(node_obj.label)) 36 | else: 37 | click.secho("Node {} is already stopped".format(node_obj.label), fg="yellow") 38 | except NodeNotFound: 39 | click.secho("Node {} was not found in lab {}".format(id if id else node, current_lab), fg="red") 40 | exit(1) 41 | else: 42 | click.secho("Unable to find lab {}".format(current_lab), fg="red") 43 | exit(1) 44 | else: 45 | click.secho("No current lab set", fg="red") 46 | exit(1) 47 | -------------------------------------------------------------------------------- /virl/cli/telnet/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/telnet/__init__.py -------------------------------------------------------------------------------- /virl/cli/telnet/commands.py: -------------------------------------------------------------------------------- 1 | from subprocess import call 2 | 3 | import click 4 | from virl2_client.exceptions import NodeNotFound 5 | 6 | from virl.api import VIRLServer 7 | from virl.helpers import (get_cml_client, get_current_lab, get_node_mgmt_ip, 8 | safe_join_existing_lab) 9 | 10 | 11 | @click.command() 12 | @click.argument("node", nargs=1) 13 | def telnet(node): 14 | """ 15 | telnet to a node 16 | """ 17 | server = VIRLServer() 18 | client = get_cml_client(server) 19 | 20 | current_lab = get_current_lab() 21 | if current_lab: 22 | lab = safe_join_existing_lab(current_lab, client) 23 | if lab: 24 | try: 25 | node_obj = lab.get_node_by_label(node) 26 | except NodeNotFound: 27 | click.secho("Node {} was not found in lab {}".format(node, current_lab), fg="red") 28 | exit(1) 29 | 30 | if node_obj.is_active(): 31 | mgmtip = get_node_mgmt_ip(node_obj) 32 | if mgmtip: 33 | if "VIRL_TELNET_COMMAND" in server.config: 34 | cmd = server.config["VIRL_TELNET_COMMAND"] 35 | cmd = cmd.format(host=mgmtip) 36 | print("Calling user specified command: {}".format(cmd)) 37 | exit(call(cmd.split())) 38 | else: 39 | click.secho("Attemping telnet connection to {} at {}".format(node_obj.label, mgmtip)) 40 | 41 | exit(call(["telnet", mgmtip])) 42 | else: 43 | click.secho("Node {} does not have an external management IP".format(node_obj.label)) 44 | else: 45 | click.secho("Node {} is not active".format(node_obj.label), fg="yellow") 46 | else: 47 | click.secho("Unable to find lab {}".format(current_lab), fg="red") 48 | exit(1) 49 | else: 50 | click.secho("No current lab set", fg="red") 51 | exit(1) 52 | -------------------------------------------------------------------------------- /virl/cli/tmux/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/tmux/__init__.py -------------------------------------------------------------------------------- /virl/cli/tmux/commands.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import click 4 | import libtmux 5 | 6 | from virl.api import VIRLServer 7 | from virl.helpers import (get_cml_client, get_current_lab, 8 | safe_join_existing_lab) 9 | 10 | 11 | def connect_tmux(session_title, node_console_cmd, group): 12 | tmux_server = libtmux.server.Server() 13 | session = tmux_server.new_session(session_name=session_title, kill_session=True) 14 | if group == "panes": 15 | window = session.windows[0] 16 | panes_len = len(window.panes) 17 | for node_obj, cmd in node_console_cmd: 18 | label = node_obj.label 19 | if panes_len == 1: 20 | panes_len += 1 21 | pane = window.panes[0] 22 | else: 23 | pane = window.split_window() 24 | pane.send_keys("printf '\\033]2;%s\\033\\\\' '{}'".format(label), suppress_history=True) 25 | pane.send_keys(cmd, suppress_history=True) 26 | window.select_layout("tiled") 27 | 28 | if group == "windows": 29 | windows_len = len(session.windows) 30 | for node_obj, cmd in node_console_cmd: 31 | label = node_obj.label 32 | if windows_len == 1: 33 | windows_len += 1 34 | window = session.windows[0] 35 | window.cmd("rename-window", label) 36 | else: 37 | window = session.new_window(window_name=label) 38 | pane = window.panes[0] 39 | pane.send_keys(cmd, suppress_history=True) 40 | 41 | if "TMUX" in os.environ: 42 | session.switch_client() 43 | else: 44 | session.attach_session() 45 | 46 | 47 | @click.command(help="console to all nodes using tmux") 48 | @click.option( 49 | "--group", 50 | type=click.Choice(("panes", "windows")), 51 | default="panes", 52 | show_default=True, 53 | help="'panes': group all nodes in one window, 'windows': one node per window", 54 | ) 55 | def tmux(group): 56 | """ 57 | console to all devices in the lab with tmux 58 | """ 59 | server = VIRLServer() 60 | client = get_cml_client(server) 61 | skip_types = ["external_connector", "unmanaged_switch"] 62 | 63 | node_console_cmd = [] 64 | current_lab = get_current_lab() 65 | if current_lab: 66 | lab = safe_join_existing_lab(current_lab, client) 67 | if lab: 68 | for node_obj in lab.nodes(): 69 | if node_obj.node_definition in skip_types or not node_obj.is_active(): 70 | continue 71 | if len(lab.id) == 6: 72 | # Old-style (CML 2.2) lab IDs; console uses lab_id/node_id 73 | console = "/{}/{}/0".format(lab.id, node_obj.id) 74 | else: 75 | # From CML 2.3, console uses lab_title/node_label 76 | console = "/{}/{}/0".format(lab.title, node_obj.label) 77 | # use user specified ssh command 78 | if "CML_CONSOLE_COMMAND" in server.config: 79 | cmd = server.config["CML_CONSOLE_COMMAND"] 80 | cmd = cmd.format(host=server.host, user=server.user, console="open " + console) 81 | print("Calling user specified command: {}".format(cmd)) 82 | else: 83 | cmd = "ssh -t {}@{} open {}".format(server.user, server.host, console) 84 | node_console_cmd.append((node_obj, cmd)) 85 | 86 | if node_console_cmd: 87 | session_title = "{}-{}".format(str(lab.title).replace(".", "_").replace(":", "_"), lab.id[:4]) 88 | connect_tmux(session_title, node_console_cmd, group) 89 | else: 90 | click.secho("Unable to find any valid nodes", fg="red") 91 | exit(1) 92 | else: 93 | click.secho("Unable to find lab {}".format(current_lab), fg="red") 94 | exit(1) 95 | else: 96 | click.secho("No current lab set", fg="red") 97 | exit(1) 98 | -------------------------------------------------------------------------------- /virl/cli/ui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/ui/__init__.py -------------------------------------------------------------------------------- /virl/cli/ui/commands.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | import click 4 | 5 | from virl.api import VIRLServer 6 | from virl.helpers import (get_cml_client, get_current_lab, 7 | safe_join_existing_lab) 8 | 9 | 10 | @click.command() 11 | def ui(): 12 | """ 13 | opens the Workbench for the current lab 14 | """ 15 | server = VIRLServer() 16 | client = get_cml_client(server) 17 | 18 | current_lab = get_current_lab() 19 | if current_lab: 20 | lab = safe_join_existing_lab(current_lab, client) 21 | if lab: 22 | url = "https://{}/lab/{}".format(server.host, current_lab) 23 | subprocess.Popen(["open", url]) 24 | -------------------------------------------------------------------------------- /virl/cli/up/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/up/__init__.py -------------------------------------------------------------------------------- /virl/cli/use/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/use/__init__.py -------------------------------------------------------------------------------- /virl/cli/use/commands.py: -------------------------------------------------------------------------------- 1 | from subprocess import call 2 | 3 | import click 4 | 5 | from virl.api import VIRLServer 6 | from virl.helpers import (cache_lab, check_lab_cache, get_cml_client, 7 | get_command, safe_join_existing_lab, 8 | safe_join_existing_lab_by_title, set_current_lab) 9 | 10 | 11 | # This may need to become a helper 12 | def check_lab_cache_server(lab_id, client): 13 | """ 14 | check if a lab exists in either the cache or the server. 15 | if on server and not in cache, cache the lab. 16 | """ 17 | ret = None 18 | 19 | if not check_lab_cache(lab_id): 20 | lab_obj = safe_join_existing_lab(lab_id, client) 21 | if lab_obj: 22 | cache_lab(lab_obj) 23 | ret = lab_id 24 | else: 25 | ret = lab_id 26 | 27 | return ret 28 | 29 | 30 | @click.command() 31 | @click.argument("lab", required=False) 32 | @click.option("--id", required=False, help="An existing lab ID to make the current lab (lab-name is ignored)") 33 | @click.option("--lab-name", "-n", required=False, help="An existing lab name to make the current lab") 34 | def use(lab, id, lab_name): 35 | """ 36 | use lab launched elsewhere 37 | """ 38 | server = VIRLServer() 39 | client = get_cml_client(server) 40 | lab_id = None 41 | 42 | if not lab and not id and not lab_name: 43 | exit(call([get_command(), "use", "--help"])) 44 | 45 | if id: 46 | lab_id = check_lab_cache_server(id, client) 47 | 48 | # Prefer --lab-name over positional argument 49 | if lab_name: 50 | lab = lab_name 51 | 52 | if not id and lab: 53 | lab_obj = safe_join_existing_lab_by_title(lab, client) 54 | if lab_obj: 55 | # Make sure this lab is cached. 56 | lab_id = check_lab_cache_server(lab_obj.id, client) 57 | 58 | if lab_id: 59 | set_current_lab(lab_id) 60 | else: 61 | click.secho("Unable to find unique lab in the cache or on the server", fg="red") 62 | exit(1) 63 | -------------------------------------------------------------------------------- /virl/cli/users/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.cli.users.create.commands import create_users 4 | from virl.cli.users.delete.commands import delete_users 5 | from virl.cli.users.ls.commands import list_users 6 | from virl.cli.users.update.commands import update_users 7 | 8 | 9 | @click.group() 10 | def users(): 11 | """ 12 | manage users 13 | """ 14 | pass 15 | 16 | 17 | users.add_command(list_users, name="ls") 18 | users.add_command(create_users, name="create") 19 | users.add_command(update_users, name="update") 20 | users.add_command(delete_users, name="delete") 21 | -------------------------------------------------------------------------------- /virl/cli/users/create/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/users/create/__init__.py -------------------------------------------------------------------------------- /virl/cli/users/create/commands.py: -------------------------------------------------------------------------------- 1 | import getpass 2 | import sys 3 | 4 | import click 5 | 6 | from virl.api import VIRLServer 7 | from virl.helpers import get_cml_client 8 | 9 | 10 | @click.command() 11 | @click.option("--admin/--no-admin", is_flag=True, default=False, help="Grant or revoke admin privileges for the users") 12 | @click.option("--group", default=[], multiple=True, help="Assign the users to one or more groups (e.g., --group group1 --group group2)") 13 | @click.argument("usernames", nargs=-1, required=True) 14 | def create_users(usernames, admin, group): 15 | """ 16 | Create one or more users (e.g., user1 user2) 17 | """ 18 | 19 | server = VIRLServer() 20 | client = get_cml_client(server) 21 | group_ids = [client.group_management.group_id(g) for g in group] 22 | 23 | for username in usernames: 24 | kwargs = { 25 | "username": username, 26 | "admin": admin, 27 | "groups": group_ids, 28 | } 29 | try: 30 | passwd = confirm_password(username) 31 | kwargs["pwd"] = passwd 32 | client.user_management.create_user(**kwargs) 33 | click.secho(f"User {username} successfully created", fg="green") 34 | except Exception as e: 35 | click.secho(f"Failed to create user: {e}", fg="red") 36 | sys.exit(1) 37 | 38 | 39 | def confirm_password(username): 40 | """ 41 | Prompts the user for a password and confirms it 42 | """ 43 | passwd = getpass.getpass(f"Enter {username}'s password: ") 44 | re_entered_passwd = getpass.getpass(f"Re-Enter {username}'s password: ") 45 | if passwd != re_entered_passwd: 46 | click.secho("Passwords do not match", fg="red") 47 | sys.exit(1) 48 | return passwd 49 | -------------------------------------------------------------------------------- /virl/cli/users/delete/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/users/delete/__init__.py -------------------------------------------------------------------------------- /virl/cli/users/delete/commands.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import click 4 | 5 | from virl.api import VIRLServer 6 | from virl.helpers import get_cml_client 7 | 8 | 9 | @click.command() 10 | @click.argument("usernames", nargs=-1, required=True) 11 | def delete_users(usernames): 12 | """ 13 | Delete one or more users (e.g., user1 user2) 14 | """ 15 | 16 | server = VIRLServer() 17 | client = get_cml_client(server) 18 | user_mapping = {u["username"]: u["id"] for u in client.user_management.users()} 19 | 20 | for username in usernames: 21 | try: 22 | user_id = user_mapping[username] 23 | client.user_management.delete_user(user_id) 24 | click.secho(f"User {username} successfully deleted", fg="green") 25 | except Exception as e: 26 | click.secho(f"Failed to delete user: {e}", fg="red") 27 | sys.exit(1) 28 | -------------------------------------------------------------------------------- /virl/cli/users/ls/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/users/ls/__init__.py -------------------------------------------------------------------------------- /virl/cli/users/ls/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api import NoPluginError, ViewerPlugin, VIRLServer 4 | from virl.cli.views import user_list_table 5 | from virl.helpers import get_cml_client 6 | 7 | 8 | @click.command() 9 | @click.option("-v", "--verbose", is_flag=True, help="Include user IDs in the output") 10 | def list_users(verbose): 11 | """ 12 | List all users on the server 13 | """ 14 | server = VIRLServer() 15 | client = get_cml_client(server) 16 | 17 | users = client.user_management.users() 18 | user_keys = ( 19 | "id", 20 | "created", 21 | "modified", 22 | "username", 23 | "fullname", 24 | "email", 25 | "description", 26 | "admin", 27 | "directory_dn", 28 | "opt_in", 29 | "resource_pool", 30 | "tour_version", 31 | "pubkey_info", 32 | ) 33 | labs_mapping = {lab.id: lab.title for lab in client.all_labs(show_all=True)} 34 | group_mapping = {g["id"]: g["name"] for g in client.group_management.groups()} 35 | for user in users: 36 | user["groups"] = [group_mapping[group_id] for group_id in user.get("groups", [])] 37 | user["labs"] = [labs_mapping[lab_id] for lab_id in user.get("labs", [])] 38 | for k in user_keys: 39 | user.setdefault(k, "N/A") 40 | try: 41 | pl = ViewerPlugin(viewer="user") 42 | pl.visualize(users=users) 43 | except NoPluginError: 44 | user_list_table(users, verbose=verbose) 45 | -------------------------------------------------------------------------------- /virl/cli/users/update/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/users/update/__init__.py -------------------------------------------------------------------------------- /virl/cli/users/update/commands.py: -------------------------------------------------------------------------------- 1 | import getpass 2 | import sys 3 | 4 | import click 5 | 6 | from virl.api import VIRLServer 7 | from virl.helpers import get_cml_client 8 | 9 | 10 | @click.command() 11 | @click.option("--admin/--no-admin", is_flag=True, default=None, help="Grant or revoke admin privileges for the users") 12 | @click.option( 13 | "--group", 14 | multiple=True, 15 | help="Assign the user to one or more groups (e.g., --group group1 --group group2)", 16 | ) 17 | @click.option("--remove-from-all-groups", is_flag=True, help="Remove the users from all groups") 18 | @click.option("--change-password", is_flag=True, help="Prompt to change the users' password") 19 | @click.option("--all-users", is_flag=True, help="Apply the changes to all users") 20 | @click.argument("usernames", nargs=-1, required=True) 21 | def update_users(usernames, admin, group, remove_from_all_groups, change_password, all_users): 22 | """ 23 | Update one or more users (e.g., user1 user2) 24 | """ 25 | 26 | server = VIRLServer() 27 | client = get_cml_client(server) 28 | 29 | group = group if group else None 30 | group = [] if remove_from_all_groups else group 31 | group_ids = [g["id"] for g in client.group_management.groups() if g["name"] in group] if group else group 32 | 33 | users = client.user_management.users() 34 | user_mapping = {user["username"]: user["id"] for user in users} 35 | all_usernames = users if all_users else usernames 36 | 37 | for username in all_usernames: 38 | user_id = user_mapping[username] 39 | password_dict = get_password_dict(username) if change_password else None 40 | kwargs = { 41 | "user_id": user_id, 42 | "admin": admin, 43 | "groups": group_ids, 44 | "password_dict": password_dict, 45 | } 46 | # only pass kwargs that are not None 47 | kwargs = {k: v for k, v in kwargs.items() if v is not None} 48 | try: 49 | client.user_management.update_user(**kwargs) 50 | click.secho(f"User {username} successfully updated", fg="green") 51 | except Exception as e: 52 | click.secho(f"Failed to create user: {e}", fg="red") 53 | sys.exit(1) 54 | 55 | 56 | def get_password_dict(username): 57 | """ 58 | Prompt the user for old and new password and verify it 59 | A user with administrative privileges can set a new password by providing an arbitrary or empty old password 60 | """ 61 | old_passwd = getpass.getpass(f"Enter {username}'s old password (password can be blank if you are an admin): ") 62 | new_passwd = getpass.getpass(f"Enter {username}'s new password: ") 63 | return {"old_password": old_passwd, "new_password": new_passwd} 64 | -------------------------------------------------------------------------------- /virl/cli/version/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/version/__init__.py -------------------------------------------------------------------------------- /virl/cli/version/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl import __version__ 4 | from virl.api import VIRLServer 5 | from virl.helpers import get_cml_client 6 | 7 | 8 | @click.command() 9 | def version(): 10 | """ 11 | version information 12 | """ 13 | server = VIRLServer() 14 | client = get_cml_client(server) 15 | server_version = "Unknown" 16 | try: 17 | server_version = client.system_info()["version"] 18 | except Exception: 19 | pass 20 | virlutils_version = __version__ 21 | click.secho("cmlutils Version: {}".format(virlutils_version)) 22 | click.secho("CML Controller Version: {}".format(server_version)) 23 | -------------------------------------------------------------------------------- /virl/cli/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .cluster.cluster_views import cluster_list_table # noqa 2 | from .generate.nso import sync_table # noqa 3 | from .groups.group_views import group_list_table # noqa 4 | from .images.image_views import image_list_table # noqa 5 | from .labs.lab_views import lab_list_table # noqa 6 | from .license.license_views import (license_details_table, # noqa 7 | license_features_table) 8 | from .node_defs.node_def_views import node_def_list_table # noqa 9 | from .nodes.node_views import node_list_table # noqa 10 | from .users.user_views import user_list_table # noqa 11 | -------------------------------------------------------------------------------- /virl/cli/views/cluster/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/views/cluster/__init__.py -------------------------------------------------------------------------------- /virl/cli/views/cluster/cluster_views.py: -------------------------------------------------------------------------------- 1 | import click 2 | import tabulate 3 | 4 | 5 | def cluster_list_table(computes: dict) -> None: 6 | table = list() 7 | headers = ["ID", "Hostname", "Is Controller?", "Status"] 8 | for cid, compute in computes.items(): 9 | tr = list() 10 | 11 | tr.append(cid) 12 | tr.append(compute["hostname"]) 13 | tr.append(compute["is_controller"]) 14 | 15 | healthy = "HEALTHY" 16 | bad_props = [] 17 | for stat_prop, description in { 18 | "kvm_vmx_enabled": "KVM", 19 | "enough_cpus": "CPU", 20 | "refplat_images_available": "REFPLAT", 21 | "lld_connected": "LLD", 22 | "valid": "VALID", 23 | }.items(): 24 | if not compute[stat_prop]: 25 | healthy = "UNHEALTHY" 26 | bad_props.append(description) 27 | 28 | color = "green" 29 | if len(bad_props) > 0: 30 | healthy += " ({})".format(",".join(bad_props)) 31 | color = "red" 32 | 33 | tr.append(click.style(healthy, fg=color)) 34 | table.append(tr) 35 | # wrap the output in this try/except block as some terminals 36 | # may have problem with the 'fancy_grid' 37 | try: 38 | click.echo(tabulate.tabulate(table, headers, tablefmt="fancy_grid")) 39 | except UnicodeEncodeError: 40 | click.echo(tabulate.tabulate(table, headers, tablefmt="grid")) 41 | -------------------------------------------------------------------------------- /virl/cli/views/console/__init__.py: -------------------------------------------------------------------------------- 1 | from .console_views import console_table # noqa 2 | -------------------------------------------------------------------------------- /virl/cli/views/console/console_views.py: -------------------------------------------------------------------------------- 1 | import click 2 | import tabulate 3 | 4 | 5 | def console_table(consoles): 6 | click.secho("Here is a list of all the running consoles") 7 | headers = ["Node", "Console Path"] 8 | table = list() 9 | for console in consoles: 10 | tr = list() 11 | tr.append(console["node"]) 12 | tr.append(console["console"]) 13 | table.append(tr) 14 | # wrap the output in this try/except block as some terminals 15 | # may have problem with the 'fancy_grid' 16 | try: 17 | click.echo(tabulate.tabulate(table, headers, tablefmt="fancy_grid")) 18 | except UnicodeEncodeError: 19 | click.echo(tabulate.tabulate(table, headers, tablefmt="grid")) 20 | -------------------------------------------------------------------------------- /virl/cli/views/generate/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/views/generate/__init__.py -------------------------------------------------------------------------------- /virl/cli/views/generate/nso/__init__.py: -------------------------------------------------------------------------------- 1 | from .sync_result import sync_table # noqa 2 | -------------------------------------------------------------------------------- /virl/cli/views/generate/nso/sync_result.py: -------------------------------------------------------------------------------- 1 | import click 2 | import tabulate 3 | 4 | 5 | def sync_table(sync_result): 6 | click.secho(""" 7 | NSO Sync Report 8 | """) 9 | headers = ["Device", "Result"] 10 | table = list() 11 | 12 | sync_results = sync_result['tailf-ncs:output']['sync-result'] 13 | for item in sync_results: 14 | tr = list() 15 | tr.append(item['device']) 16 | 17 | result = item['result'] 18 | if result is True: 19 | result = "SUCCESS" 20 | color = 'green' 21 | else: 22 | result = "FAILED" 23 | color = 'red' 24 | 25 | tr.append(click.style(result, fg=color)) 26 | table.append(tr) 27 | # wrap the output in this try/except block as some terminals 28 | # may have problem with the 'fancy_grid' 29 | try: 30 | click.echo(tabulate.tabulate(table, headers, tablefmt="fancy_grid")) 31 | except UnicodeEncodeError: 32 | click.echo(tabulate.tabulate(table, headers, tablefmt="grid")) 33 | -------------------------------------------------------------------------------- /virl/cli/views/groups/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/views/groups/__init__.py -------------------------------------------------------------------------------- /virl/cli/views/groups/group_views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import textwrap 3 | 4 | import click 5 | import tabulate 6 | 7 | 8 | def group_list_table(groups, verbose=False): 9 | click.secho("Groups on Server", fg="green") 10 | table = [] 11 | 12 | headers = ["ID"] if verbose else [] 13 | headers.extend(["Name", "Description", "Users", "Labs"]) 14 | for group in groups: 15 | tr = [] 16 | if verbose: 17 | tr.append(group["id"]) 18 | tr.append(group["name"]) 19 | wrapped_description = textwrap.fill(group["description"], width=20) 20 | tr.append(wrapped_description) 21 | 22 | tr.append("\n".join(group["members"])) 23 | tr.append("\n".join(f"{lab['title']} ({lab['permission']})" for lab in group["labs"])) 24 | table.append(tr) 25 | # wrap the output in this try/except block as some terminals 26 | # may have problem with the 'fancy_grid' 27 | try: 28 | click.echo(tabulate.tabulate(table, headers, tablefmt="fancy_grid")) 29 | except UnicodeEncodeError: 30 | click.echo(tabulate.tabulate(table, headers, tablefmt="grid")) 31 | -------------------------------------------------------------------------------- /virl/cli/views/images/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/views/images/__init__.py -------------------------------------------------------------------------------- /virl/cli/views/images/image_views.py: -------------------------------------------------------------------------------- 1 | import click 2 | import tabulate 3 | 4 | 5 | def image_list_table(image_list): 6 | 7 | headers = ["ID", "Node Definition ID", "Label", "Description", "RAM", "CPUs", "Boot Disk Size"] 8 | table = list() 9 | 10 | for f in list(image_list): 11 | tr = list() 12 | tr.append(str(f["id"])) 13 | tr.append(str(f["node_definition_id"])) 14 | tr.append(str(f["label"])) 15 | tr.append(str(f["description"])) 16 | tr.append(str(f["ram"])) 17 | tr.append(str(f["cpus"])) 18 | tr.append(str(f["boot_disk_size"])) 19 | 20 | table.append(tr) 21 | 22 | try: 23 | click.echo(tabulate.tabulate(table, headers, tablefmt="fancy_grid")) 24 | except UnicodeEncodeError: 25 | click.echo(tabulate.tabulate(table, headers, tablefmt="grid")) 26 | -------------------------------------------------------------------------------- /virl/cli/views/labs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/views/labs/__init__.py -------------------------------------------------------------------------------- /virl/cli/views/labs/lab_views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import textwrap 3 | 4 | import click 5 | import tabulate 6 | 7 | 8 | def lab_list_table(labs, ownerids_usernames, cached_labs=None): 9 | click.secho("Labs on Server", fg="green") 10 | print_labs(labs, ownerids_usernames) 11 | if cached_labs: 12 | click.secho("Cached Labs", fg="yellow") 13 | print_labs(cached_labs, ownerids_usernames) 14 | 15 | 16 | def print_labs(labs, ownerids_usernames): 17 | table = list() 18 | headers = ["ID", "Title", "Description", "Owner", "Status", "Nodes", "Links", "Interfaces"] 19 | for lab in labs: 20 | tr = list() 21 | tr.append(lab.id) 22 | tr.append(lab.title) 23 | wrapped_description = textwrap.fill(lab.description, width=40) 24 | tr.append(wrapped_description) 25 | owner = ownerids_usernames.get(lab.owner, lab.owner) 26 | tr.append(owner) 27 | status = lab.state() 28 | stats = lab.statistics 29 | if status in {"BOOTED", "STARTED"}: 30 | color = "green" 31 | elif status in {"QUEUED"}: 32 | color = "yellow" 33 | else: 34 | color = "red" 35 | tr.append(click.style(status, fg=color)) 36 | tr.append(stats["nodes"]) 37 | tr.append(stats["links"]) 38 | tr.append(stats["interfaces"]) 39 | table.append(tr) 40 | # wrap the output in this try/except block as some terminals 41 | # may have problem with the 'fancy_grid' 42 | try: 43 | click.echo(tabulate.tabulate(table, headers, tablefmt="fancy_grid")) 44 | except UnicodeEncodeError: 45 | click.echo(tabulate.tabulate(table, headers, tablefmt="grid")) 46 | -------------------------------------------------------------------------------- /virl/cli/views/license/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/views/license/__init__.py -------------------------------------------------------------------------------- /virl/cli/views/license/license_views.py: -------------------------------------------------------------------------------- 1 | import click 2 | import tabulate 3 | 4 | 5 | def license_details_table(license): 6 | click.secho("Registration Details") 7 | print_registration(license["registration"]) 8 | click.secho("Authorization Details") 9 | print_authorization(license["authorization"]) 10 | click.secho("Features") 11 | print_features(license["features"]) 12 | 13 | 14 | def license_features_table(license): 15 | table = list() 16 | headers = ["ID", "Name", "In Use"] 17 | for feature in license: 18 | tr = list() 19 | tr.append(feature["id"]) 20 | tr.append(feature["name"]) 21 | tr.append(feature["in_use"]) 22 | 23 | table.append(tr) 24 | 25 | # wrap the output in this try/except block as some terminals 26 | # may have problem with the 'fancy_grid' 27 | try: 28 | click.echo(tabulate.tabulate(table, headers, tablefmt="fancy_grid")) 29 | except UnicodeEncodeError: 30 | click.echo(tabulate.tabulate(table, headers, tablefmt="grid")) 31 | 32 | 33 | def print_registration(reg_obj): 34 | table = list() 35 | headers = ["Status", "Expires", "Smart Account", "Virtual Account", "Registration Time", "Registration Status", "Next Renewal Time"] 36 | tr = list() 37 | stat_color = None 38 | if reg_obj["status"] == "COMPLETED": 39 | stat_color = "green" 40 | elif reg_obj["status"] == "IN_PROGRESS": 41 | stat_color = "yellow" 42 | else: 43 | stat_color = "red" 44 | tr.append(click.style(reg_obj["status"], fg=stat_color)) 45 | tr.append(reg_obj["expires"]) 46 | tr.append(reg_obj["smart_account"]) 47 | tr.append(reg_obj["virtual_account"]) 48 | if reg_obj["register_time"]["attempted"]: 49 | reg_color = None 50 | tr.append(reg_obj["register_time"]["attempted"]) 51 | if reg_obj["register_time"]["success"] == "SUCCESS": 52 | reg_color = "green" 53 | else: 54 | reg_color = "red" 55 | tr.append(click.style(reg_obj["register_time"]["success"], fg=reg_color)) 56 | else: 57 | tr.append("N/A") 58 | tr.append("N/A") 59 | if reg_obj["renew_time"]["scheduled"]: 60 | tr.append(reg_obj["renew_time"]["scheduled"]) 61 | else: 62 | tr.append("N/A") 63 | 64 | table.append(tr) 65 | 66 | # wrap the output in this try/except block as some terminals 67 | # may have problem with the 'fancy_grid' 68 | try: 69 | click.echo(tabulate.tabulate(table, headers, tablefmt="fancy_grid")) 70 | except UnicodeEncodeError: 71 | click.echo(tabulate.tabulate(table, headers, tablefmt="grid")) 72 | 73 | 74 | def print_authorization(auth_obj): 75 | table = list() 76 | headers = ["Status", "Expires", "Renewal Time", "Renewal Status", "Next Renewal Time"] 77 | tr = list() 78 | auth_color = None 79 | if auth_obj["status"] == "IN_COMPLIANCE": 80 | auth_color = "green" 81 | else: 82 | auth_color = "red" 83 | tr.append(click.style(auth_obj["status"], fg=auth_color)) 84 | tr.append(auth_obj["expires"]) 85 | if auth_obj["renew_time"]["attempted"]: 86 | renew_color = None 87 | if auth_obj["renew_time"]["status"] == "SUCCEEDED": 88 | renew_color = "green" 89 | elif auth_obj["renew_time"]["status"] == "NOT STARTED": 90 | renew_color = "yellow" 91 | else: 92 | renew_color = "red" 93 | tr.append(auth_obj["renew_time"]["attempted"]) 94 | tr.append(click.style(auth_obj["renew_time"]["status"], fg=renew_color)) 95 | else: 96 | tr.append("N/A") 97 | tr.append("N/A") 98 | if auth_obj["renew_time"]["scheduled"]: 99 | tr.append(auth_obj["renew_time"]["scheduled"]) 100 | else: 101 | tr.append("N/A") 102 | 103 | table.append(tr) 104 | 105 | # wrap the output in this try/except block as some terminals 106 | # may have problem with the 'fancy_grid' 107 | try: 108 | click.echo(tabulate.tabulate(table, headers, tablefmt="fancy_grid")) 109 | except UnicodeEncodeError: 110 | click.echo(tabulate.tabulate(table, headers, tablefmt="grid")) 111 | 112 | 113 | def print_features(feature_obj): 114 | table = list() 115 | headers = ["Name", "Description", "In Use", "Status", "Version"] 116 | for feature in feature_obj: 117 | tr = list() 118 | tr.append(feature["name"]) 119 | tr.append(feature["description"]) 120 | tr.append(feature["in_use"]) 121 | color = None 122 | if feature["status"] == "IN_COMPLIANCE": 123 | color = "green" 124 | elif feature["status"] != "INIT": 125 | color = "red" 126 | tr.append(click.style(feature["status"], fg=color)) 127 | tr.append(feature["version"]) 128 | 129 | table.append(tr) 130 | 131 | # wrap the output in this try/except block as some terminals 132 | # may have problem with the 'fancy_grid' 133 | try: 134 | click.echo(tabulate.tabulate(table, headers, tablefmt="fancy_grid")) 135 | except UnicodeEncodeError: 136 | click.echo(tabulate.tabulate(table, headers, tablefmt="grid")) 137 | -------------------------------------------------------------------------------- /virl/cli/views/node_defs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/views/node_defs/__init__.py -------------------------------------------------------------------------------- /virl/cli/views/node_defs/node_def_views.py: -------------------------------------------------------------------------------- 1 | import click 2 | import tabulate 3 | 4 | 5 | def node_def_list_table(image_list): 6 | headers = ["ID", "Label", "Description", "Max No. Interfaces", "RAM", "CPUs", "Boot Disk Size"] 7 | table = list() 8 | 9 | for f in list(image_list): 10 | tr = list() 11 | tr.append(str(f["id"])) 12 | tr.append(str(f["ui"].get("label", "N/A"))) 13 | tr.append(str(f["general"]["description"])) 14 | tr.append(str(len(f["device"]["interfaces"]["physical"]))) 15 | linux_native = f["sim"].get("linux_native", None) 16 | if linux_native: 17 | ram = int(linux_native.get("ram", 0)) 18 | unit = "GB" 19 | if ram > 1024: 20 | ram /= 1024 21 | else: 22 | unit = "MB" 23 | tr.append(str(ram) + " " + unit) 24 | tr.append(str(linux_native.get("cpus", "N/A"))) 25 | if "boot_disk_size" in linux_native: 26 | tr.append(str(linux_native["boot_disk_size"]) + " GB") 27 | else: 28 | tr.append("N/A") 29 | else: 30 | tr.append("N/A") # RAM 31 | tr.append("N/A") # CPU 32 | tr.append("N/A") # Boot Disk Size 33 | 34 | table.append(tr) 35 | 36 | try: 37 | click.echo(tabulate.tabulate(table, headers, tablefmt="fancy_grid")) 38 | except UnicodeEncodeError: 39 | click.echo(tabulate.tabulate(table, headers, tablefmt="grid")) 40 | -------------------------------------------------------------------------------- /virl/cli/views/nodes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/views/nodes/__init__.py -------------------------------------------------------------------------------- /virl/cli/views/nodes/node_views.py: -------------------------------------------------------------------------------- 1 | import click 2 | import tabulate 3 | 4 | 5 | def node_list_table(nodes, computes): 6 | click.secho("Here is a list of nodes in this lab") 7 | table = list() 8 | headers = ["ID", "Label", "Type"] 9 | if computes: 10 | headers.append("Compute Node") 11 | 12 | headers += ["State", "Wiped?", "L3 Address(es)"] 13 | skip_types = [] 14 | for node in nodes: 15 | # Skip a full operational sync per node. 16 | node.lab.auto_sync = True 17 | for sync in ( 18 | # "sync_statistics_if_outdated", 19 | "sync_states_if_outdated", 20 | "sync_l3_addresses_if_outdated", 21 | "sync_topology_if_outdated", 22 | ): 23 | try: 24 | meth = getattr(node.lab, sync) 25 | except AttributeError: 26 | pass 27 | else: 28 | meth() 29 | 30 | node.lab.auto_sync = False 31 | 32 | tr = list() 33 | if node.node_definition in skip_types: 34 | continue 35 | 36 | tr.append(node.id) 37 | tr.append(node.label) 38 | tr.append(node.node_definition) 39 | try: 40 | node_compute_id = node.compute_id 41 | except AttributeError: 42 | node_compute_id = None 43 | 44 | if node_compute_id and node_compute_id in computes: 45 | tr.append(computes[node_compute_id]["hostname"]) 46 | elif computes: 47 | tr.append("Unknown") 48 | 49 | color = "red" 50 | booted = node.is_booted() 51 | if booted: 52 | color = "green" 53 | elif node.is_active(): 54 | color = "yellow" 55 | 56 | node_state = node.state 57 | tr.append(click.style(node_state, fg=color)) 58 | tr.append(node_state == "DEFINED_ON_CORE") 59 | intfs = [] 60 | if booted: 61 | for i in node.interfaces(): 62 | disc_ipv4 = i.discovered_ipv4 63 | if disc_ipv4: 64 | intfs += disc_ipv4 65 | 66 | disc_ipv6 = i.discovered_ipv6 67 | if disc_ipv6: 68 | intfs += disc_ipv6 69 | 70 | tr.append("\n".join(intfs)) 71 | table.append(tr) 72 | # wrap the output in this try/except block as some terminals 73 | # may have problem with the 'fancy_grid' 74 | try: 75 | click.echo(tabulate.tabulate(table, headers, tablefmt="fancy_grid")) 76 | except UnicodeEncodeError: 77 | click.echo(tabulate.tabulate(table, headers, tablefmt="grid")) 78 | -------------------------------------------------------------------------------- /virl/cli/views/search/__init__.py: -------------------------------------------------------------------------------- 1 | from .views import repo_table # noqa 2 | -------------------------------------------------------------------------------- /virl/cli/views/search/views.py: -------------------------------------------------------------------------------- 1 | import click 2 | import tabulate 3 | 4 | 5 | def repo_table(repo_entries): 6 | # sort by date 7 | headers = ["Name", "Stars", "Description"] 8 | table = list() 9 | 10 | for repo in repo_entries: 11 | tr = list() 12 | tr.append(repo["full_name"]) 13 | tr.append(repo["stargazers_count"]) 14 | tr.append(repo["description"]) 15 | table.append(tr) 16 | # wrap the output in this try/except block as some terminals 17 | # may have problem with the 'fancy_grid' 18 | try: 19 | click.echo(tabulate.tabulate(table, headers, tablefmt="fancy_grid")) 20 | except UnicodeEncodeError: 21 | click.echo(tabulate.tabulate(table, headers, tablefmt="grid")) 22 | -------------------------------------------------------------------------------- /virl/cli/views/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/views/users/__init__.py -------------------------------------------------------------------------------- /virl/cli/views/users/user_views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import textwrap 3 | 4 | import click 5 | import tabulate 6 | 7 | 8 | def user_list_table(users, verbose=False): 9 | click.secho("Users on Server", fg="green") 10 | table = [] 11 | headers = ["ID"] if verbose else [] 12 | headers.extend(["Username", "Administrator", "Full Name", "Email", "Groups", "Labs"]) 13 | for user in users: 14 | tr = [] 15 | if verbose: 16 | tr.append(user["id"]) 17 | tr.append(user["username"]) 18 | tr.append(user["admin"]) 19 | wrapped_fullname = textwrap.fill(user["fullname"], width=20) 20 | tr.append(wrapped_fullname) 21 | tr.append(user["email"]) 22 | tr.append("\n".join(user["groups"])) 23 | tr.append("\n".join(user["labs"])) 24 | table.append(tr) 25 | # wrap the output in this try/except block as some terminals 26 | # may have problem with the 'fancy_grid' 27 | try: 28 | click.echo(tabulate.tabulate(table, headers, tablefmt="fancy_grid")) 29 | except UnicodeEncodeError: 30 | click.echo(tabulate.tabulate(table, headers, tablefmt="grid")) 31 | -------------------------------------------------------------------------------- /virl/cli/wipe/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.cli.wipe.lab.commands import lab as labc 4 | from virl.cli.wipe.node.commands import node as nodec 5 | 6 | 7 | @click.group() 8 | def wipe(): 9 | """ 10 | wipe a lab or nodes within a lab 11 | """ 12 | pass 13 | 14 | 15 | wipe.add_command(labc, name="lab") 16 | wipe.add_command(nodec, name="node") 17 | -------------------------------------------------------------------------------- /virl/cli/wipe/lab/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/wipe/lab/__init__.py -------------------------------------------------------------------------------- /virl/cli/wipe/lab/commands.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from virl.api import VIRLServer 4 | from virl.helpers import (get_cml_client, get_current_lab, 5 | safe_join_existing_lab) 6 | 7 | 8 | @click.command() 9 | @click.option("--force/--no-force", "-f", default=False, required=False, help="Stop a lab (if it's started) then wipe it (default: False)") 10 | @click.option( 11 | "--confirm/--no-confirm", 12 | show_default=False, 13 | default=True, 14 | help="Do not prompt for confirmation (default: prompt)", 15 | required=False, 16 | ) 17 | def lab(force, confirm): 18 | """ 19 | wipe a lab 20 | """ 21 | server = VIRLServer() 22 | client = get_cml_client(server) 23 | 24 | current_lab = get_current_lab() 25 | if current_lab: 26 | lab = safe_join_existing_lab(current_lab, client) 27 | if lab: 28 | active = lab.is_active() 29 | if active and force: 30 | lab.stop(wait=True) 31 | 32 | # Check again just to be sure. 33 | if not lab.is_active(): 34 | ret = "y" 35 | if confirm: 36 | ret = input("Are you sure you want to wipe lab {} (ID: {}) [y/N]? ".format(lab.title, current_lab)) 37 | if ret.lower().startswith("y"): 38 | lab.wipe(wait=True) 39 | click.secho("Lab {} (ID: {}) wiped".format(lab.title, current_lab)) 40 | else: 41 | click.secho("Not wiping lab {} (ID: {})".format(lab.title, current_lab)) 42 | 43 | else: 44 | click.secho("Lab {} (ID: {}) is active; either down it or use --force".format(lab.title, current_lab), fg="red") 45 | exit(1) 46 | else: 47 | click.secho("Unable to find lab {}".format(current_lab), fg="red") 48 | exit(1) 49 | else: 50 | click.secho("Current lab is not set", fg="red") 51 | exit(1) 52 | -------------------------------------------------------------------------------- /virl/cli/wipe/node/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CiscoDevNet/virlutils/6cec1fcd73f51e99a0aef8e34ae0a0c0bcb1bf91/virl/cli/wipe/node/__init__.py -------------------------------------------------------------------------------- /virl/cli/wipe/node/commands.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import click 4 | from virl2_client import NodeNotFound 5 | 6 | from virl.api import VIRLServer 7 | from virl.helpers import (get_cml_client, get_current_lab, 8 | safe_join_existing_lab) 9 | 10 | 11 | @click.command() 12 | @click.option("--force/--no-force", "-f", default=False, required=False, help="Stop a node (if it's started) then wipe it (default: False)") 13 | @click.option( 14 | "--confirm/--no-confirm", 15 | show_default=False, 16 | default=True, 17 | help="Do not prompt for confirmation (default: prompt)", 18 | required=False, 19 | ) 20 | @click.argument("node", nargs=1) 21 | def node(node, force, confirm): 22 | """ 23 | wipe a node 24 | """ 25 | server = VIRLServer() 26 | client = get_cml_client(server) 27 | 28 | current_lab = get_current_lab() 29 | if current_lab: 30 | lab = safe_join_existing_lab(current_lab, client) 31 | if lab: 32 | try: 33 | node_obj = lab.get_node_by_label(node) 34 | except NodeNotFound: 35 | click.secho("Node {} was not found in lab {}".format(node, current_lab), fg="red") 36 | exit(1) 37 | 38 | if node_obj.is_active() and force: 39 | node_obj.stop() 40 | while node_obj.is_active(): 41 | time.sleep(1) 42 | 43 | if not node_obj.is_active(): 44 | ret = "y" 45 | if confirm: 46 | ret = input("Are you sure you want to wipe node {} [y/N]? ".format(node_obj.label)) 47 | if ret.lower().startswith("y"): 48 | node_obj.wipe(wait=True) 49 | click.secho("Node {} wiped".format(node_obj.label)) 50 | else: 51 | click.secho("Not wiping node {}".format(node_obj.label)) 52 | else: 53 | click.secho("Node {} is active; either stop it or use --force".format(node_obj.label), fg="red") 54 | exit(1) 55 | else: 56 | click.secho("Unable to find lab {}".format(current_lab), fg="red") 57 | exit(1) 58 | else: 59 | click.secho("No current lab set", fg="red") 60 | exit(1) 61 | -------------------------------------------------------------------------------- /virl/generators/__init__.py: -------------------------------------------------------------------------------- 1 | from .ansible_inventory import ansible_inventory_generator # noqa 2 | from .nso_payload import nso_payload_generator # noqa 3 | from .pyats_testbed import pyats_testbed_generator # noqa 4 | -------------------------------------------------------------------------------- /virl/generators/ansible_inventory.py: -------------------------------------------------------------------------------- 1 | from jinja2 import Environment, PackageLoader 2 | 3 | from virl.helpers import get_node_mgmt_ip 4 | 5 | 6 | def generate_inventory_dict(lab, server): 7 | """ 8 | common inventory info accross yaml/ini 9 | """ 10 | # create inventory skeleton 11 | inventory = dict() 12 | inventory["all"] = dict() 13 | inventory["all"]["children"] = dict() 14 | inventory["all"]["hosts"] = dict() 15 | 16 | for node in lab.nodes(): 17 | mgmtip = get_node_mgmt_ip(node) 18 | 19 | if not mgmtip: 20 | continue 21 | 22 | name = node.label 23 | entry = dict() 24 | entry["ansible_host"] = mgmtip 25 | # map console ports if they are available 26 | entry["console_server"] = server.host 27 | entry["console_user"] = server.user 28 | entry["console_path"] = "/{}/{}/0".format(lab.id, node.id) 29 | 30 | # determine device/os type 31 | try: 32 | type = node.node_definition.lower() 33 | if "nx" in type: 34 | entry["device_type"] = "nxos" 35 | elif "xr" in type: 36 | entry["device_type"] = "iosxr" 37 | elif "csr" in type: 38 | entry["device_type"] = "ios" 39 | elif "ios" in type: 40 | entry["device_type"] = "ios" 41 | elif "asa" in type: 42 | entry["device_type"] = "asa" 43 | else: 44 | entry["device_type"] = "unknown" 45 | 46 | except KeyError: 47 | entry["device_type"] = "unknown" 48 | 49 | ansible_group = None 50 | for tag in node.tags(): 51 | if tag.startswith("ansible_group="): 52 | ansible_group = tag.split("=")[1].strip() 53 | break 54 | 55 | # try to map to ansible group 56 | if ansible_group: 57 | print("Placing {} into ansible group {}".format(name, ansible_group)) 58 | if ansible_group not in inventory["all"]["children"]: 59 | inventory["all"]["children"][ansible_group] = dict() 60 | if name not in inventory["all"]["children"]: 61 | inventory["all"]["children"][ansible_group][name] = entry 62 | else: 63 | inventory["all"]["hosts"][name] = entry 64 | 65 | return inventory 66 | 67 | 68 | def render_inventory(lab, server, style): 69 | """ 70 | render the YAML version of the inventory 71 | """ 72 | 73 | j2_env = Environment(loader=PackageLoader("virl"), trim_blocks=False) 74 | 75 | inventory = generate_inventory_dict(lab, server) 76 | template = None 77 | if style == "ini": 78 | template = j2_env.get_template("ansible/inventory_ini_template.j2") 79 | elif style == "yaml": 80 | template = j2_env.get_template("ansible/inventory_template.j2") 81 | 82 | if template: 83 | return template.render(inventory=inventory, lab_id=lab.id, lab_title=lab.title) 84 | 85 | return None 86 | 87 | 88 | def ansible_inventory_generator(lab, server, style="yaml"): 89 | """ 90 | given a lab produces an inventory file suitable for use with ansible 91 | """ 92 | inv = render_inventory(lab, server, style) 93 | 94 | return inv 95 | -------------------------------------------------------------------------------- /virl/generators/nso_payload.py: -------------------------------------------------------------------------------- 1 | from jinja2 import Environment, PackageLoader 2 | 3 | from virl.helpers import get_node_mgmt_ip 4 | 5 | 6 | def lab_info(lab, server, protocol): 7 | """ 8 | common inventory info across xml/json 9 | """ 10 | inventory = list() 11 | 12 | for node in lab.nodes(): 13 | mgmtip = get_node_mgmt_ip(node) 14 | 15 | if not mgmtip: 16 | continue 17 | 18 | name = node.label 19 | entry = dict() 20 | entry["address"] = mgmtip 21 | entry["protocol"] = protocol 22 | name = node.label 23 | entry["name"] = name 24 | entry["ned"] = "unknown" 25 | entry["ns"] = "unkown" 26 | 27 | # determine device/os type 28 | try: 29 | node_type = node.node_definition.lower() 30 | if "nx" in node_type: 31 | entry["prefix"] = "{{ NX_PREFIX }}" 32 | entry["ned"] = "{{ NX_NED_ID }}" 33 | entry["ns"] = "{{ NX_NAMESPACE }}" 34 | elif "xr" in node_type: 35 | entry["prefix"] = "{{ XR_PREFIX }}" 36 | entry["ned"] = "{{ XR_NED_ID }}" 37 | entry["ns"] = "{{ XR_NAMESPACE }}" 38 | elif "csr" in node_type or "ios" in node_type or "cat" in node_type: 39 | entry["prefix"] = "{{ IOS_PREFIX }}" 40 | entry["ned"] = "{{ IOS_NED_ID }}" 41 | entry["ns"] = "{{ IOS_NAMESPACE }}" 42 | elif "asa" in node_type: 43 | entry["prefix"] = "{{ ASA_PREFIX }}" 44 | entry["ned"] = "{{ ASA_NED_ID }}" 45 | entry["ns"] = "{{ ASA_NAMESPACE }}" 46 | except KeyError: 47 | pass 48 | 49 | if entry.get("ned", None) not in ["unknown", None]: 50 | inventory.append(entry) 51 | 52 | return inventory 53 | 54 | 55 | def render_payload(lab, server, protocol, style): 56 | env = Environment(loader=PackageLoader("virl"), trim_blocks=False) 57 | 58 | if style == "json": # pragma: no cover 59 | raise NotImplementedError 60 | 61 | inventory = lab_info(lab, server, protocol) 62 | payload = env.get_template("nso/xml_payload.j2").render(inventory=inventory) 63 | return payload 64 | 65 | 66 | def nso_payload_generator(lab, server, style="xml", protocol="ssh"): 67 | """ 68 | given a lab produces a payload file suitable for 69 | use with network services orchestrator 70 | """ 71 | payload = render_payload(lab, server, protocol, style) 72 | 73 | return payload 74 | -------------------------------------------------------------------------------- /virl/generators/pyats_testbed.py: -------------------------------------------------------------------------------- 1 | def pyats_testbed_generator(lab): 2 | return lab.get_pyats_testbed() 3 | -------------------------------------------------------------------------------- /virl/templates/ansible/inventory_ini_template.j2: -------------------------------------------------------------------------------- 1 | # cmlutils generated ansible file for lab id: {{ lab_id }}, title: {{ lab_title }} 2 | # 3 | # the overall structure of the inventory follows best practices 4 | # at http://docs.ansible.com/ansible/latest/intro_inventory.html 5 | 6 | # we've rendered what we think is best if you disagree, override 7 | # virl.generators.ansible_inventory_generator 8 | 9 | # you can modify grouping behavior by adding a tag to your nodes: 10 | # ansible_group=GROUP_NAME 11 | 12 | {%- for group, devices in inventory.all.children.items() %} 13 | [{{group}}] 14 | {%- for name, device in devices.items() %} 15 | {{ name }} ansible_host={{ device.ansible_host }}{%- if device.console_server %}{%- if device.console_user %}{%- if device.console_path %} console_server={{ device.console_server }} console_user={{ device.console_user }} console_path={{ device.console_path }}{%- endif %}{%- endif %}{%- endif %}{%- if device.device_type %} ansible_network_os={{ device.device_type }}{%- endif %}{%- endfor %}{% endfor %} 16 | -------------------------------------------------------------------------------- /virl/templates/ansible/inventory_template.j2: -------------------------------------------------------------------------------- 1 | # cmlutils generated ansible file for lab id: {{ lab_id }}, title: {{ lab_title }} 2 | # 3 | # the overall structure of the inventory follows best practices 4 | # at http://docs.ansible.com/ansible/latest/intro_inventory.html 5 | 6 | # we've rendered what we think is best if you disagree, override 7 | # virl.generators.ansible_inventory_generator 8 | 9 | # you can modify grouping behavior by adding a tag to your nodes: 10 | # ansible_group=GROUP_NAME 11 | 12 | all: 13 | children: 14 | 15 | {%- for group, devices in inventory.all.children.items() %} 16 | {{group}}: 17 | hosts: 18 | {%- for name, device in devices.items() %} 19 | 20 | {{ name }}: 21 | ansible_host: {{ device.ansible_host }} 22 | {%- if device.console_server %} 23 | {%- if device.console_user %} 24 | {%- if device.console_path %} 25 | console_server: {{ device.console_server }} 26 | console_user: {{ device.console_user }} 27 | console_path: {{ device.console_path }} 28 | {%- endif %} 29 | {%- endif %} 30 | {%- endif %} 31 | {%- if device.device_type %} 32 | ansible_network_os: {{ device.device_type }} 33 | {%- endif %} 34 | 35 | {%- endfor %} 36 | {% endfor %} 37 | -------------------------------------------------------------------------------- /virl/templates/nso/xml_payload.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | virl 5 | 6 | cisco 7 | cisco 8 | 9 | 10 | admin 11 | cisco 12 | cisco 13 | 14 | 15 | 16 | {% for node in inventory %} 17 | 18 | {{ node.name }} 19 |
{{ node.address }}
20 | 21 | none 22 | 23 | virl 24 | 25 | 26 | {{ node.prefix}}:{{ node.ned }} 27 | {{ node.protocol }} 28 | 29 | 30 | 31 | unlocked 32 | 33 |
34 | {% endfor %} 35 |
36 | -------------------------------------------------------------------------------- /virl/templates/pyats/testbed_yaml.j2: -------------------------------------------------------------------------------- 1 | testbed: 2 | 3 | name: {{ name }} 4 | 5 | tacacs: 6 | username: "%ENV{PYATS_USERNAME}" 7 | passwords: 8 | tacacs: "%ENV{PYATS_PASSWORD}" 9 | enable: "%ENV{PYATS_AUTH_PASS}" 10 | line: "%ENV{PYATS_PASSWORD}" 11 | 12 | {%- if servers.items() %} 13 | servers: 14 | {%- for srv_name, props in servers.items() %} 15 | {{ srv_name }}: 16 | address: {{ props.address|default("''", true) }} 17 | server: {{ props.server|default("''", true) }} 18 | {%- endfor %} 19 | {%-endif %} 20 | 21 | devices: 22 | {%- for dev_name, props in devices.items() %} 23 | 24 | {{ dev_name }}: 25 | alias: {{ props.alias|default("''", true) }} 26 | {%- if props.os %} 27 | os: {{ props.os }} 28 | {%- endif %} 29 | type: {{ props.type|default("''", true) }} 30 | platform: {{ props.type|default("unknown", true) }} 31 | 32 | connections: 33 | 34 | defaults: 35 | class: {{ conn_class }} 36 | 37 | {%- for key, props in props.connections.items() %} 38 | {{ key }}: 39 | {%- for prop, prop_value in props.items() %} 40 | {%- if prop and prop_value %} 41 | {{ prop }}: {{ prop_value }} 42 | {%- endif %} 43 | 44 | {%- endfor %} 45 | {%- endfor %} 46 | custom: 47 | abstraction: 48 | order: [os, type] 49 | {%- endfor %} 50 | 51 | topology: 52 | {%- for dev, dev_ints in topology.items() %} 53 | {%- if dev in devices %} 54 | {{dev}}: 55 | interfaces: 56 | {%- for id, interface in dev_ints.items() %} 57 | {{ interface.name }}: 58 | {%- if interface['ip-address'] %} 59 | ipv4: {{ interface['ip-address'] }} 60 | {%- endif %} 61 | {%- if interface['network'] %} 62 | link: {{ interface['network'] }} 63 | {%- endif %} 64 | {%- if interface['type'] and 'oop' in interface['type'] %} 65 | type: loopback 66 | {%- else %} 67 | type: ethernet 68 | {%- endif %} 69 | 70 | {%- endfor %} 71 | {%- endif %} 72 | {%- endfor %} 73 | --------------------------------------------------------------------------------