diff --git a/.circleci/config.yml b/.circleci/config.yml index ac3cca5..3d5f0a7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -20,15 +20,12 @@ jobs: - checkout - restore_cache: keys: - - deps-{{ checksum "poetry.lock" }} + # - deps-{{ checksum "poetry.lock" }} + - deps-clear - run: name: Install Dependencies command: | poetry install - - save_cache: - key: deps-{{ checksum "poetry.lock" }} - paths: - - /home/circleci/.cache/pypoetry/virtualenvs - run: name: Run flake8 command: | @@ -37,6 +34,16 @@ jobs: name: Run black formatting check command: | poetry run black --diff --check . + - run: + name: Run tox + command: | + poetry run tox + - save_cache: + # key: deps-{{ checksum "poetry.lock" }} + key: deps-clear + paths: + - /home/circleci/.cache/pypoetry/virtualenvs + - /home/circleci/.cache/.tox - run: name: Running tests command: | diff --git a/Makefile b/Makefile index 046f436..96aa1f7 100644 --- a/Makefile +++ b/Makefile @@ -6,3 +6,9 @@ docs-generate: docs-show: cd docs; mkdocs serve + +test: + poetry run flake8 . + poetry run black --diff --check . + poetry run tox + poetry run pytest --cov-report=xml --cov=gns3fy tests/ diff --git a/README.md b/README.md index 6362583..91e98d9 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ -[![Circle CI](https://circleci.com/gh/davidban77/gns3fy/tree/develop.svg?style=shield&circle-token=:circle-token)](https://circleci.com/gh/davidban77/gns3fy/tree/develop)[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)[![codecov](https://img.shields.io/codecov/c/github/davidban77/gns3fy)](https://codecov.io/gh/davidban77/gns3fy) +[![Circle CI](https://circleci.com/gh/davidban77/gns3fy/tree/develop.svg?style=shield&circle-token=:circle-token)](https://circleci.com/gh/davidban77/gns3fy/tree/develop)[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)[![codecov](https://img.shields.io/codecov/c/github/davidban77/gns3fy)](https://codecov.io/gh/davidban77/gns3fy)[![Total alerts](https://img.shields.io/lgtm/alerts/g/davidban77/gns3fy.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/davidban77/gns3fy/alerts/)[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/davidban77/gns3fy.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/davidban77/gns3fy/context:python) + # gns3fy Python wrapper around [GNS3 Server API](http://api.gns3.net/en/2.2/index.html). -Its main objective is to interact with the GNS3 server in a programatic way, so it can be integrated with the likes of Ansible, docker and scripts. +Its main objective is to interact with the GNS3 server in a programatic way, so it can be integrated with the likes of Ansible, docker and scripts. Ideal for network CI/CD pipeline tooling. ## Documentation -Check out the [Documentation](https://davidban77.github.io/gns3fy/) to explore use cases, and the API Reference +Check out the [Documentation](https://davidban77.github.io/gns3fy/) to explore use cases and the API Reference ## Install @@ -26,38 +27,54 @@ You can start the library and use the `Gns3Connector` object and the `Project` o For example: ```python -import gns3fy +>>> import gns3fy +>>> from tabulate import tabulate # Define the server object to establish the connection -gns3_server = gns3fy.Gns3Connector("http://:3080") +>>> gns3_server = gns3fy.Gns3Connector("http://:3080") + +# Show the available projects on the server +>>> print( + tabulate( + gns3_server.projects_summary(is_print=False), + headers=["Project Name", "Project ID", "Total Nodes", "Total Links", "Status"], + ) + ) +""" +Project Name Project ID Total Nodes Total Links Status +-------------- ------------------------------------ ------------- ------------- -------- +test2 c9dc56bf-37b9-453b-8f95-2845ce8908e3 10 9 opened +API_TEST 4b21dfb3-675a-4efa-8613-2f7fb32e76fe 6 4 opened +mpls-bgpv2 f5de5917-0ac5-4850-82b1-1d7e3c777fa1 30 40 closed +""" # Define the lab you want to load and assign the server connector -lab = gns3fy.Project(name="API_TEST", connector=gns3_server) +>>> lab = gns3fy.Project(name="API_TEST", connector=gns3_server) # Retrieve its information and display -lab.get() - -print(lab) -# Project(project_id='4b21dfb3-675a-4efa-8613-2f7fb32e76fe', name='API_TEST', status='opened', ... +>>> lab.get() +>>> print(lab) +"Project(project_id='4b21dfb3-675a-4efa-8613-2f7fb32e76fe', name='API_TEST', status='opened', ...)" # Access the project attributes -print(f"Name: {lab.name} -- Status: {lab.status} -- Is auto_closed?: {lab.auto_close}") -# Name: API_TEST -- Status: closed -- Is auto_closed?: False +>>> print(f"Name: {lab.name} -- Status: {lab.status} -- Is auto_closed?: {lab.auto_close}") +"Name: API_TEST -- Status: closed -- Is auto_closed?: False" # Open the project -lab.open() -print(lab.status) -# opened +>>> lab.open() +>>> lab.status +opened # Verify the stats -print(lab.stats) -# {'drawings': 0, 'links': 4, 'nodes': 6, 'snapshots': 0} +>>> lab.stats +{'drawings': 0, 'links': 4, 'nodes': 6, 'snapshots': 0} # List the names and status of all the nodes in the project -for node in lab.nodes: - print(f"Node: {node.name} -- Node Type: {node.node_type} -- Status: {node.status}") -# Node: Ethernetswitch-1 -- Node Type: ethernet_switch -- Status: started -# ... +>>> for node in lab.nodes: +... print(f"Node: {node.name} -- Node Type: {node.node_type} -- Status: {node.status}") + +"Node: Ethernetswitch-1 -- Node Type: ethernet_switch -- Status: started" +... ``` Take a look at the API documentation for complete information about the attributes retrieved. @@ -67,70 +84,73 @@ Take a look at the API documentation for complete information about the attribut You have access to the `Node` and `Link` objects as well, this gives you the ability to start, stop, suspend the individual element in a GNS3 project. ```python -from gns3fy import Node, Link, Gns3Connector +>>> from gns3fy import Node, Link, Gns3Connector -PROJECT_ID = "" -server = Gns3Connector("http://:3080") +>>> PROJECT_ID = "" +>>> server = Gns3Connector("http://:3080") -alpine1 = Node(project_id=PROJECT_ID, name="alpine-1", connector=server) +>>> alpine1 = Node(project_id=PROJECT_ID, name="alpine-1", connector=server) -alpine1.get() -print(alpine1) -# Node(name='alpine-1', node_type='docker', node_directory= ... +>>> alpine1.get() +>>> print(alpine1) +"Node(name='alpine-1', node_type='docker', node_directory= ...)" # And you can access the attributes the same way as the project -print(f"Name: {alpine1.name} -- Status: {alpine1.status} -- Console: {alpine1.console}") -# Name: alpine-1 -- Status: started -- Console: 5005 +>>> print(f"Name: {alpine1.name} -- Status: {alpine1.status} -- Console: {alpine1.console}") +"Name: alpine-1 -- Status: started -- Console: 5005" # Stop the node and start (you can just restart it as well) -alpine1.stop() -print(alpine1.status) -# stopped +>>> alpine1.stop() +>>> alpine1.status +stopped -alpine1.start() -print(alpine1.status) -# started +>>> alpine1.start() +>>> alpine1.status +started # You can also see the Link objects assigned to this node -print(alpine1.links) -# [Link(link_id='4d9f1235-7fd1-466b-ad26-0b4b08beb778', link_type='ethernet', .... +>>> alpine1.links +[Link(link_id='4d9f1235-7fd1-466b-ad26-0b4b08beb778', link_type='ethernet', ...)] # And in the same way you can interact with a Link object -link1 = alpine1.links[0] -print(f"Link Type: {link1.link_type} -- Capturing?: {link1.capturing} -- Endpoints: {link1.nodes}") -# Link Type: ethernet -- Capturing?: False -- Endpoints: [{'adapter_number': 2, ... +>>> link1 = alpine1.links[0] +>>> print(f"Link Type: {link1.link_type} -- Capturing?: {link1.capturing} -- Endpoints: {link1.nodes}") +"Link Type: ethernet -- Capturing?: False -- Endpoints: [{'adapter_number': 2, ...}]" ``` -### Bonus +### Useful functions You also have some commodity methods like the `nodes_summary` and `links_summary`, that if used with a library like `tabulate` you can see the following: ```python -... -from tabulate import tabulate - -nodes_summary = lab.nodes_summary(is_print=False) - -print( - tabulate(nodes_summary, headers=["Node", "Status", "Console Port", "ID"]) -) -# Node Status Console Port ID -# ---------------- -------- -------------- ------------------------------------ -# Ethernetswitch-1 started 5000 da28e1c0-9465-4f7c-b42c-49b2f4e1c64d -# IOU1 started 5001 de23a89a-aa1f-446a-a950-31d4bf98653c -# IOU2 started 5002 0d10d697-ef8d-40af-a4f3-fafe71f5458b -# vEOS-4.21.5F-1 started 5003 8283b923-df0e-4bc1-8199-be6fea40f500 -# alpine-1 started 5005 ef503c45-e998-499d-88fc-2765614b313e -# Cloud-1 started cde85a31-c97f-4551-9596-a3ed12c08498 - -links_summary = lab.links_summary(is_print=False) -print( - tabulate(links_summary, headers=["Node A", "Port A", "Node B", "Port B"]) -) -# Node A Port A Node B Port B -# -------------- ----------- ---------------- ----------- -# IOU1 Ethernet1/0 IOU2 Ethernet1/0 -# vEOS-4.21.5F-1 Management1 Ethernetswitch-1 Ethernet0 -# vEOS-4.21.5F-1 Ethernet1 alpine-1 eth0 -# Cloud-1 eth1 Ethernetswitch-1 Ethernet7 + +>>> from tabulate import tabulate + +>>> nodes_summary = lab.nodes_summary(is_print=False) + +>>> print( +... tabulate(nodes_summary, headers=["Node", "Status", "Console Port", "ID"]) +... ) +""" +Node Status Console Port ID +---------------- -------- -------------- ------------------------------------ +Ethernetswitch-1 started 5000 da28e1c0-9465-4f7c-b42c-49b2f4e1c64d +IOU1 started 5001 de23a89a-aa1f-446a-a950-31d4bf98653c +IOU2 started 5002 0d10d697-ef8d-40af-a4f3-fafe71f5458b +vEOS-4.21.5F-1 started 5003 8283b923-df0e-4bc1-8199-be6fea40f500 +alpine-1 started 5005 ef503c45-e998-499d-88fc-2765614b313e +Cloud-1 started cde85a31-c97f-4551-9596-a3ed12c08498 +""" +>>> links_summary = lab.links_summary(is_print=False) +>>> print( +... tabulate(links_summary, headers=["Node A", "Port A", "Node B", "Port B"]) +... ) +""" +Node A Port A Node B Port B +-------------- ----------- ---------------- ----------- +IOU1 Ethernet1/0 IOU2 Ethernet1/0 +vEOS-4.21.5F-1 Management1 Ethernetswitch-1 Ethernet0 +vEOS-4.21.5F-1 Ethernet1 alpine-1 eth0 +Cloud-1 eth1 Ethernetswitch-1 Ethernet7 +""" ``` diff --git a/docs/content/about/changelog.md b/docs/content/about/changelog.md index d1b38aa..417b970 100644 --- a/docs/content/about/changelog.md +++ b/docs/content/about/changelog.md @@ -1,5 +1,20 @@ +# Upgrading + +``` +pip install -U gns3fy +``` + # Releases +## 0.3.0 + +**Enhacement:** + +- `tox` for pipeline testing. https://github.com/davidban77/gns3fy/pull/15 +- `projects_summary` and `templates_summary` methods for `Gns3Connector`. https://github.com/davidban77/gns3fy/pull/17 +- Improved `nodes_inventory` method. https://github.com/davidban77/gns3fy/pull/23 +- Refactor of `Node` creation, basically changed the API endpoint from Node to Template. https://github.com/davidban77/gns3fy/pull/27 + ## 0.2.0 **New features:** @@ -16,7 +31,7 @@ ## 0.1.1 -**Enhancement** +**Enhancement:** - Adding `Gns3Connector` method `get_version` ## 0.1.0 diff --git a/docs/content/about/license.md b/docs/content/about/license.md index 4eefde6..2732caa 100644 --- a/docs/content/about/license.md +++ b/docs/content/about/license.md @@ -1,201 +1,21 @@ -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2019 David Flores - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +MIT License + +Copyright (c) 2019 David Flores + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docs/content/api_reference.md b/docs/content/api_reference.md index f9f9e11..52331c7 100644 --- a/docs/content/api_reference.md +++ b/docs/content/api_reference.md @@ -66,6 +66,17 @@ def get_version(self) Returns the version information of GNS3 server +### `Gns3Connector.projects_summary()` + +```python +def projects_summary(self, is_print=True) +``` + +Returns a summary of the projects in the server. If `is_print` is `False`, it +will return a list of tuples like: + +`[(name, project_id, total_nodes, total_links, status) ...]` + ### `Gns3Connector.get_projects()` ```python @@ -86,6 +97,17 @@ Retrieves a project from either a name or ID - `name` or `project_id` +### `Gns3Connector.templates_summary()` + +```python +def templates_summary(self, is_print=True) +``` + +Returns a summary of the templates in the server. If `is_print` is `False`, it +will return a list of tuples like: + +`[(name, template_id, template_type, builtin, console_type, category) ...]` + ### `Gns3Connector.get_templates()` ```python @@ -401,10 +423,32 @@ Suspends the node. - `connector` - `node_id` +### `Node.update()` + +```python +def update(self, kwargs) +``` + +Updates the node instance by passing the keyword arguments of the attributes +you want updated + +Example: + +```python +router01.update(name="router01-CSX") +``` + +This will update the project `auto_close` attribute to `True` + +**Required Attributes:** + +- `project_id` +- `connector` + ### `Node.create()` ```python -def create(self, extra_properties={}) +def create(self) ``` Creates a node. @@ -413,14 +457,12 @@ By default it will fetch the nodes properties for creation based on the `template` or `template_id` attribute supplied. This can be overriden/updated by sending a dictionary of the properties under `extra_properties`. -**Required Attributes:** +**Required Node instance attributes:** - `project_id` - `connector` - `compute_id`: Defaults to "local" -- `name` -- `node_type` -- `template` or `template_id` +- `template` or `template_id` - if not passed as arguments ### `Node.delete()` @@ -518,9 +560,13 @@ def update(self, kwargs) ``` Updates the project instance by passing the keyword arguments of the attributes -you want to be updated +you want updated -Example: `lab.update(auto_close=True)` +Example: + +```python +lab.update(auto_close=True) +``` This will update the project `auto_close` attribute to `True` @@ -700,7 +746,7 @@ Example: `{ "router01": { - "hostname": "127.0.0.1", + "server": "127.0.0.1", "name": "router01", "console_port": 5077, "type": "vEOS" @@ -765,21 +811,23 @@ necessary ### `Project.create_node()` ```python -def create_node(self, name=None, kwargs) +def create_node(self, kwargs) ``` -Creates a node. +Creates a node. To know available parameters see `Node` object, specifically +the `create` method. The most basic example would be: -**Required Attributes:** +```python +project.create_node(name='test-switch01', template='Ethernet switch') +``` + +**Required Project instance attributes:** - `project_id` - `connector` -**Required Keyword attributes:** +**Required keyword aguments:** -- `name` -- `node_type` -- `compute_id`: Defaults to "local" - `template` or `template_id` ### `Project.create_link()` diff --git a/docs/content/index.md b/docs/content/index.md index 58b4dc5..82f71b2 100644 --- a/docs/content/index.md +++ b/docs/content/index.md @@ -17,39 +17,39 @@ Use [poetry](https://github.com/sdispater/poetry) to install the package when cl ## Quick Start -``` -import gns3fy +```python +>>> import gns3fy # Define the server object to establish the connection -gns3_server = gns3fy.Gns3Connector("http://:3080") +>>> gns3_server = gns3fy.Gns3Connector("http://:3080") # Define the lab you want to load and assign the server connector -lab = gns3fy.Project(name="API_TEST", connector=gns3_server) +>>> lab = gns3fy.Project(name="API_TEST", connector=gns3_server) # Retrieve its information and display -lab.get() - -print(lab) -# Project(project_id='4b21dfb3-675a-4efa-8613-2f7fb32e76fe', name='API_TEST', status='opened', ... +>>> lab.get() +>>> print(lab) +"Project(project_id='4b21dfb3-675a-4efa-8613-2f7fb32e76fe', name='API_TEST', status='opened', ...)" # Access the project attributes -print(f"Name: {lab.name} -- Status: {lab.status} -- Is auto_closed?: {lab.auto_close}") -# Name: API_TEST -- Status: closed -- Is auto_closed?: False +>>> print(f"Name: {lab.name} -- Status: {lab.status} -- Is auto_closed?: {lab.auto_close}") +"Name: API_TEST -- Status: closed -- Is auto_closed?: False" # Open the project -lab.open() -print(lab.status) -# opened +>>> lab.open() +>>> lab.status +opened # Verify the stats -print(lab.stats) -# {'drawings': 0, 'links': 4, 'nodes': 6, 'snapshots': 0} +>>> lab.stats +{'drawings': 0, 'links': 4, 'nodes': 6, 'snapshots': 0} # List the names and status of all the nodes in the project -for node in lab.nodes: - print(f"Node: {node.name} -- Node Type: {node.node_type} -- Status: {node.status}") -# Node: Ethernetswitch-1 -- Node Type: ethernet_switch -- Status: started -# ... +>>> for node in lab.nodes: +... print(f"Node: {node.name} -- Node Type: {node.node_type} -- Status: {node.status}") + +"Node: Ethernetswitch-1 -- Node Type: ethernet_switch -- Status: started" +... ``` ## Release Notes diff --git a/docs/content/user-guide.md b/docs/content/user-guide.md index a353051..2b71ac6 100644 --- a/docs/content/user-guide.md +++ b/docs/content/user-guide.md @@ -18,50 +18,63 @@ Next you can see different ways to interact with the library. Here is an example of defining a connector object and a project that is already configured on a local GNS3 server: ```python -from gns3fy import Gns3Connector, Project - -server = Gns3Connector("http://localhost:3080") - -lab = Project(name="API_TEST", connector=server) +>>> from gns3fy import Gns3Connector, Project +>>> from tabulate import tabulate + +>>> server = Gns3Connector("http://localhost:3080") + +# To show the available projects on the server +>>> print( + tabulate( + server.projects_summary(is_print=False), + headers=["Project Name", "Project ID", "Total Nodes", "Total Links", "Status"], + ) + ) +""" +Project Name Project ID Total Nodes Total Links Status +-------------- ------------------------------------ ------------- ------------- -------- +test2 c9dc56bf-37b9-453b-8f95-2845ce8908e3 10 9 opened +API_TEST 4b21dfb3-675a-4efa-8613-2f7fb32e76fe 6 4 opened +mpls-bgpv2 f5de5917-0ac5-4850-82b1-1d7e3c777fa1 30 40 closed +""" + +>>> lab = Project(name="API_TEST", connector=server) # Retrieve its information and display -lab.get() +>>> lab.get() -print(lab) -# Project(project_id='4b21dfb3-675a-4efa-8613-2f7fb32e76fe', name='API_TEST', status='opened', ... +>>> print(lab) +Project(project_id='4b21dfb3-675a-4efa-8613-2f7fb32e76fe', name='API_TEST', status='opened', ...) # Access the project attributes -print(f"Name: {lab.name} -- Status: {lab.status} -- Is auto_closed?: {lab.auto_close}") -# Name: API_TEST -- Status: closed -- Is auto_closed?: False +>>> print(f"Name: {lab.name} -- Status: {lab.status} -- Is auto_closed?: {lab.auto_close}") +"Name: API_TEST -- Status: closed -- Is auto_closed?: False" # Open the project -lab.open() -print(lab.status) -# opened +>>> lab.open() +>>> lab.status +opened # Verify the stats -print(lab.stats) -# {'drawings': 0, 'links': 4, 'nodes': 6, 'snapshots': 0} +>>> lab.stats +{'drawings': 0, 'links': 4, 'nodes': 6, 'snapshots': 0} # List the names and status of all the nodes in the project for node in lab.nodes: print(f"Node: {node.name} -- Node Type: {node.node_type} -- Status: {node.status}") -# Node: Ethernetswitch-1 -- Node Type: ethernet_switch -- Status: started -# ... +""" +Node: Ethernetswitch-1 -- Node Type: ethernet_switch -- Status: started +... +""" ``` As noted before you can also use the `Gns3Connector` as an interface object to the GNS3 server REST API. ```python -In [1]: from gns3fy import Gns3Connector, Project, Node, Link +>>> server.get_version() +{'local': False, 'version': '2.2.0b4'} -In [2]: server = Gns3Connector(url="http://localhost:3080") - -In [3]: server.get_version() -Out[3]: {'local': False, 'version': '2.2.0b4'} - -In [4]: server.get_templates() -Out[4]: +>>> server.get_templates() [{'adapter_type': 'e1000', 'adapters': 13, 'bios_image': '', @@ -77,38 +90,35 @@ Out[4]: You have access to the `Node` and `Link` objects as well, and this gives you the ability to start, stop, suspend the individual element in a GNS3 project. ```python -from gns3fy import Node, Link, Gns3Connector - -PROJECT_ID = "" -server = Gns3Connector("http://:3080") - -alpine1 = Node(project_id=PROJECT_ID, name="alpine-1", connector=server) +... +>>> PROJECT_ID = "4b21dfb3-675a-4efa-8613-2f7fb32e76fe" +>>> alpine1 = Node(project_id=PROJECT_ID, name="alpine-1", connector=server) -alpine1.get() -print(alpine1) -# Node(name='alpine-1', node_type='docker', node_directory= ... +>>> alpine1.get() +>>> print(alpine1) +Node(name='alpine-1', node_type='docker', node_directory= ...) # And you can access the attributes the same way as the project -print(f"Name: {alpine1.name} -- Status: {alpine1.status} -- Console: {alpine1.console}") -# Name: alpine-1 -- Status: started -- Console: 5005 +>>> print(f"Name: {alpine1.name} -- Status: {alpine1.status} -- Console: {alpine1.console}") +"Name: alpine-1 -- Status: started -- Console: 5005" # Stop the node and start (you can just restart it as well) -alpine1.stop() -print(alpine1.status) -# stopped +>>> alpine1.stop() +>>> alpine1.status +stopped -alpine1.start() -print(alpine1.status) -# started +>>> alpine1.start() +>>> alpine1.status +started # You can also see the Link objects assigned to this node -print(alpine1.links) -# [Link(link_id='4d9f1235-7fd1-466b-ad26-0b4b08beb778', link_type='ethernet', .... +>>> alpine1.links +[Link(link_id='4d9f1235-7fd1-466b-ad26-0b4b08beb778', link_type='ethernet', ...)] # And in the same way you can interact with a Link object -link1 = alpine1.links[0] -print(f"Link Type: {link1.link_type} -- Capturing?: {link1.capturing} -- Endpoints: {link1.nodes}") -# Link Type: ethernet -- Capturing?: False -- Endpoints: [{'adapter_number': 2, ... +>>> link1 = alpine1.links[0] +>>> print(f"Link Type: {link1.link_type} -- Capturing?: {link1.capturing} -- Endpoints: {link1.nodes}") +Link Type: ethernet -- Capturing?: False -- Endpoints: [{'adapter_number': 2, ...}] ``` ## Creating a new Project @@ -122,18 +132,13 @@ To navigate to some of them and see their value, lets create a simple lab on the Lets start by creating a lab called `test_lab` ```python -In [1]: from gns3fy import Gns3Connector, Project, Node, Link - -In [2]: server = Gns3Connector(url="http://localhost:3080") - -In [3]: lab = Project(name="test_lab", connector=server) - -In [4]: lab.create() +... +>>> lab = Project(name="test_lab", connector=server) -In [5]: +>>> lab.create() -In [5]: lab -Out[5]: Project(project_id='e83f1275-3a6f-48f7-88ee-36386ee27a55', name='test_lab', status='opened',... +>>> lab +Project(project_id='e83f1275-3a6f-48f7-88ee-36386ee27a55', name='test_lab', status='opened',...) ``` You can see you get the `project_id`. In GNS3 the project ID is key for all interactions under that project. @@ -146,17 +151,18 @@ You can see you get the `project_id`. In GNS3 the project ID is key for all inte Next, lets try and create a Ethernet switch node. For this we need to know the template and node type of it. ```python hl_lines="6" -In [7]: for template in server.get_templates(): - ...: if "switch" in template["name"]: - ...: print(f"Template: {template['name']} -- ID: {template['template_id']}") - ...: - ...: +... +>>> for template in server.get_templates(): +...: if "switch" in template["name"]: +...: print(f"Template: {template['name']} -- ID: {template['template_id']}") +...: +""" Template: Ethernet switch -- ID: 1966b864-93e7-32d5-965f-001384eec461 Template: Frame Relay switch -- ID: dd0f6f3a-ba58-3249-81cb-a1dd88407a47 Template: ATM switch -- ID: aaa764e2-b383-300f-8a0e-3493bbfdb7d2 +""" -In [8]: server.get_template_by_name("Ethernet switch") -Out[8]: +>>> server.get_template_by_name("Ethernet switch") {'builtin': True, 'category': 'switch', 'console_type': 'none', @@ -169,19 +175,18 @@ Out[8]: By knowing the template information of the device we can create the Node instace of it ```python +... +>>> switch = Node( +...: project_id=lab.project_id, +...: connector=server, +...: name="Ethernet-switch", +...: template="Ethernet switch" +...:) -In [9]: switch = Node( - project_id=lab.project_id, - connector=server, - name="Ethernet-switch", - node_type="ethernet_switch", - template="Ethernet switch" -) - -In [10]: switch.create() +>>> switch.create() -In [11]: switch -Out[11]: Node(name='Ethernet-switch', project_id='6e75bca5-3fa0-4219-a7cf-f82c0540fb73', node_id='c3607609-49... +>>> switch +Node(name='Ethernet-switch', project_id='6e75bca5-3fa0-4219-a7cf-f82c0540fb73', node_id='c3607609-49'...) ``` !!! Note @@ -190,21 +195,20 @@ Out[11]: Node(name='Ethernet-switch', project_id='6e75bca5-3fa0-4219-a7cf-f82c05 Now lets add an docker Alpine host to the project (**NOTE:** The docker image and template needs to be already configured in GNS3) ```python -In [12]: alpine = Node( - project_id=lab.project_id, - connector=server, - name="alpine-host", - node_type="docker", - template="alpine" -) +... +>>> alpine = Node( +...: project_id=lab.project_id, +...: connector=server, +...: name="alpine-host", +...: template="alpine" +...:) -In [13]: alpine.create() +>>> alpine.create() -In [14]: alpine -Out[14]: Node(name='alpine-host', project_id='6e75bca5-3fa0-4219-a7cf-f82c0540fb73', node_id='8c11eb8b... +>>> alpine +Node(name='alpine-host', project_id='6e75bca5-3fa0-4219-a7cf-f82c0540fb73', node_id='8c11eb8b'...) -In [15]: alpine.properties -Out[15]: +>>> alpine.properties {'adapters': 2, 'aux': 5026, 'category': 'guest', @@ -223,8 +227,7 @@ Out[15]: 'symbol': ':/symbols/affinity/circle/gray/docker.svg', 'usage': ''} -In [16]: alpine.ports -Out[16]: +>>> alpine.ports [{'adapter_number': 0, 'data_link_types': {'Ethernet': 'DLT_EN10MB'}, 'link_type': 'ethernet', @@ -244,14 +247,16 @@ You can access all of the host attributes and see their specifications based on To update the `lab` object with their latest nodes added ```python -In [17]: lab.get() +... +>>> lab.get() # I have shorten the output shown -In [18]: lab.nodes -Out[18]: +>>> lab.nodes +""" [Node(name='Ethernet-switch', project_id='6e75bca5-3fa0-4219-a7cf-f82c0540fb73', node_id='c3607609... Node(name='alpine-host1', project_id='6e75bca5-3fa0-4219-a7cf-f82c0540fb73', node_id='8c11eb8b... ] +""" ``` ### Link creation @@ -261,12 +266,12 @@ Next lets create a link between the switch and the alpine host. `Switch Etherner0 <--> Alpine Eth1` ```python +... +>>> lab.create_link('Ethernet-switch', 'Ethernet0', 'alpine-host1', 'eth1') +"Created Link-ID: b0d0df11-8ed8-4d1d-98e4-3776c9b7bdce -- Type: ethernet" -In [15]: lab.create_link('Ethernet-switch', 'Ethernet0', 'alpine-host1', 'eth1') -Created Link-ID: b0d0df11-8ed8-4d1d-98e4-3776c9b7bdce -- Type: ethernet - -In [16]: lab.links -Out[16]: [Link(link_id='b0d0df11-8ed8-4d1d-98e4-3776c9b7bdce', link_type='ethernet', project_id='6e7 +>>> lab.links +[Link(link_id='b0d0df11-8ed8-4d1d-98e4-3776c9b7bdce', link_type='ethernet', project_id='6e7'...)] ``` !!! Note For a complete list of the attibutes you can see the [API Reference](api_reference.md#link-objects) @@ -280,9 +285,8 @@ We need the link mapping to be set under the `nodes` attribute of the `Link` ins - `port_number` ```python hl_lines="9 13 19 23" - -In [17]: switch.ports -Out[17]: +... +>>> switch.ports [{'adapter_number': 0, 'data_link_types': {'Ethernet': 'DLT_EN10MB'}, 'link_type': 'ethernet', @@ -297,8 +301,7 @@ Out[17]: 'short_name': 'e1'}, ... -In [18]: alpine.ports -Out[18]: +>>> alpine.ports [{'adapter_number': 0, 'data_link_types': {'Ethernet': 'DLT_EN10MB'}, 'link_type': 'ethernet', @@ -316,29 +319,28 @@ Out[18]: Gettings this information from both nodes we can create the Link. ```python hl_lines="2 3" - -In [19]: nodes = [ +... +>>> nodes = [ dict(node_id=switch.node_id, adapter_number=0, port_number=1), dict(node_id=alpine.node_id, adapter_number=0, port_number=0) ] -In [20]: extra_link = Link(project_id=lab.project_id, connector=server, nodes=nodes) +>>> extra_link = Link(project_id=lab.project_id, connector=server, nodes=nodes) -In [21]: extra_link.create() +>>> extra_link.create() -In [22]: extra_link -Out[22]: Link(link_id='edf38e1a-67e7-4060-8493-0e222ec22072', link_type='ethernet', project_id='6e75bca5... +>>> extra_link +Link(link_id='edf38e1a-67e7-4060-8493-0e222ec22072', link_type='ethernet', project_id='6e75bca5'...) ``` You can get the latest link information on the project ```python - -In [41]: lab.get_links() +... +>>> lab.get_links() # You can see the 2 links created earlier -In [42]: lab.links -Out[42]: +>>> lab.links [Link(link_id='b0d0df11-8ed8-4d1d-98e4-3776c9b7bdce', link_type='ethernet'... Link(link_id='=', link_type='ethernet'...] ``` @@ -354,48 +356,15 @@ You can see the final result if you open the lab on your GNS3 client: Here are some examples of what you can do with the library -### Get Nodes and Links summary - -For a given project you can use `nodes_summary` and `links_summary`, that if used with a library like `tabulate` you can obtain the following: - -```python -... -from tabulate import tabulate - -nodes_summary = lab.nodes_summary(is_print=False) - -print( - tabulate(nodes_summary, headers=["Node", "Status", "Console Port", "ID"]) -) -# Node Status Console Port ID -# ---------------- -------- -------------- ------------------------------------ -# Ethernetswitch-1 started 5000 da28e1c0-9465-4f7c-b42c-49b2f4e1c64d -# IOU1 started 5001 de23a89a-aa1f-446a-a950-31d4bf98653c -# IOU2 started 5002 0d10d697-ef8d-40af-a4f3-fafe71f5458b -# vEOS-4.21.5F-1 started 5003 8283b923-df0e-4bc1-8199-be6fea40f500 -# alpine-1 started 5005 ef503c45-e998-499d-88fc-2765614b313e -# Cloud-1 started cde85a31-c97f-4551-9596-a3ed12c08498 - -links_summary = lab.links_summary(is_print=False) -print( - tabulate(links_summary, headers=["Node A", "Port A", "Node B", "Port B"]) -) -# Node A Port A Node B Port B -# -------------- ----------- ---------------- ----------- -# IOU1 Ethernet1/0 IOU2 Ethernet1/0 -# vEOS-4.21.5F-1 Management1 Ethernetswitch-1 Ethernet0 -# vEOS-4.21.5F-1 Ethernet1 alpine-1 eth0 -# Cloud-1 eth1 Ethernetswitch-1 Ethernet7 -``` - -### Manipulate a Node from a Project +### Manipulate a Node from a Project instance The `Project` object gives you all the nodes configured on it. This is typically saved under `Project.nodes` as a list of `Node` instances. When collecting the information of a project on a given time, you also retrieve by default all of its nodes. Each of the nodes are assigned the `Gns3Connector` by default if you follow the procedure below. ```python ->>> server = Gns3Connector(url="http://
:3080")] +... +>>> server = Gns3Connector(url="http://localhost:3080")] >>> print(server) '' >>> lab = Project(name="lab", connector=server) @@ -420,3 +389,139 @@ When collecting the information of a project on a given time, you also retrieve `node_1` has the same connector object as reference for interaction with the server. The same can be done with a `Link` by interacting with the `Project.links` attribute. + + +### Check existing templates and projects available + +The templates and projects configured on the GNS3 server can be viewed by using the `templates_summary` and `projects_summary`. + +```python +... +>>> from tabulate import tabulate +>>> templates_summary = server.templates_summary(is_print=False) +>>> print( + tabulate( + server.templates_summary(is_print=False), + headers=[ + "Template Name", + "Template ID", + "Type", + "Builtin", + "Console", + "Category", + ], + ) + ) + +""" +Template Name Template ID Type Builtin Console Category +------------------ ------------------------------------ ------------------ --------- --------- ---------- +IOU-L3 8504c605-7914-4a8f-9cd4-a2638382db0e iou False telnet router +IOU-L2 92cccfb2-6401-48f2-8964-3c75323be3cb iou False telnet switch +vEOS c6203d4b-d0ce-4951-bf18-c44369d46804 qemu False telnet router +alpine 847e5333-6ac9-411f-a400-89838584371b docker False telnet guest +Cloud 39e257dc-8412-3174-b6b3-0ee3ed6a43e9 cloud True N/A guest +NAT df8f4ea9-33b7-3e96-86a2-c39bc9bb649c nat True N/A guest +VPCS 19021f99-e36f-394d-b4a1-8aaa902ab9cc vpcs True N/A guest +Ethernet switch 1966b864-93e7-32d5-965f-001384eec461 ethernet_switch True none switch +Ethernet hub b4503ea9-d6b6-3695-9fe4-1db3b39290b0 ethernet_hub True N/A switch +Frame Relay switch dd0f6f3a-ba58-3249-81cb-a1dd88407a47 frame_relay_switch True N/A switch +ATM switch aaa764e2-b383-300f-8a0e-3493bbfdb7d2 atm_switch True N/A switch +""" + +>>> projects_summary = server.projects_summary(is_print=False) +>>> print( + tabulate( + server.projects_summary(is_print=False), + headers=[ + "Project Name", + "Project ID", + "Total Nodes", + "Total Links", + "Status", + ], + ) + ) + +""" +Project name Project ID Total Nodes Total Links Status +-------------- ------------------------------------ ------------- ------------- -------- +mgmt_network c9dc56bf-37b9-453b-8f95-2845ce8908e3 10 9 opened +ospf_lab 4b21dfb3-675a-4efa-8613-2f7fb32e76fe 6 4 opened +[API] New test f5de5917-0ac5-4850-82b1-1d7e3c777fa1 0 0 closed +test_ansible 5599f8f5-9074-4372-b20e-e96eb3bd27c6 4 4 opened +""" +``` + + +### Get Nodes and Links summary + +For a given project you can use `nodes_summary` and `links_summary`, that if used with a library like `tabulate` you can obtain the following: + +```python +... +>>> from tabulate import tabulate + +>>> nodes_summary = lab.nodes_summary(is_print=False) + +>>> print( +... tabulate(nodes_summary, headers=["Node", "Status", "Console Port", "ID"]) +... ) +""" +Node Status Console Port ID +---------------- -------- -------------- ------------------------------------ +Ethernetswitch-1 started 5000 da28e1c0-9465-4f7c-b42c-49b2f4e1c64d +IOU1 started 5001 de23a89a-aa1f-446a-a950-31d4bf98653c +IOU2 started 5002 0d10d697-ef8d-40af-a4f3-fafe71f5458b +vEOS-4.21.5F-1 started 5003 8283b923-df0e-4bc1-8199-be6fea40f500 +alpine-1 started 5005 ef503c45-e998-499d-88fc-2765614b313e +Cloud-1 started cde85a31-c97f-4551-9596-a3ed12c08498 +""" +>>> links_summary = lab.links_summary(is_print=False) +>>> print( +... tabulate(links_summary, headers=["Node A", "Port A", "Node B", "Port B"]) +... ) +""" +Node A Port A Node B Port B +-------------- ----------- ---------------- ----------- +IOU1 Ethernet1/0 IOU2 Ethernet1/0 +vEOS-4.21.5F-1 Management1 Ethernetswitch-1 Ethernet0 +vEOS-4.21.5F-1 Ethernet1 alpine-1 eth0 +Cloud-1 eth1 Ethernetswitch-1 Ethernet7 +""" +``` + +I have shown the `projects_summary` earlier, here is another one that is helpful + +```python +... +>>> print( + tabulate( + server.templates_summary(is_print=False), + headers=[ + "Template Name", + "Template ID", + "Type", + "Builtin", + "Console", + "Category", + ], + ) + ) + +""" +Template Name Template ID Type Builtin Console Category +------------------ ------------------------------------ ------------------ --------- --------- ---------- +IOU-L3 8504c605-7914-4a8f-9cd4-a2638382db0e iou False telnet router +IOU-L2 92cccfb2-6401-48f2-8964-3c75323be3cb iou False telnet switch +vEOS c6203d4b-d0ce-4951-bf18-c44369d46804 qemu False telnet router +alpine 847e5333-6ac9-411f-a400-89838584371b docker False telnet guest +Cloud 39e257dc-8412-3174-b6b3-0ee3ed6a43e9 cloud True N/A guest +NAT df8f4ea9-33b7-3e96-86a2-c39bc9bb649c nat True N/A guest +VPCS 19021f99-e36f-394d-b4a1-8aaa902ab9cc vpcs True N/A guest +Ethernet switch 1966b864-93e7-32d5-965f-001384eec461 ethernet_switch True none switch +Ethernet hub b4503ea9-d6b6-3695-9fe4-1db3b39290b0 ethernet_hub True N/A switch +Frame Relay switch dd0f6f3a-ba58-3249-81cb-a1dd88407a47 frame_relay_switch True N/A switch +ATM switch aaa764e2-b383-300f-8a0e-3493bbfdb7d2 atm_switch True N/A switch +""" +``` diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 9d40327..85e06e0 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -1,11 +1,19 @@ site_name: gns3fy +site_url: https://davidban77.github.io/gns3fy/ +site_description: GNS3 REST API python library. +site_author: David Flores repo_url: https://github.com/davidban77/gns3fy/ +edit_uri: edit/develop/docs/content/ docs_dir: "content" -theme: cinder +# theme: cinder +theme: + name: cinder + markdown_extensions: - toc: toc_depth: 2-4 - admonition + nav: - Overview: index.md - User Guide: user-guide.md @@ -13,3 +21,5 @@ nav: - About: - Release Notes: about/changelog.md - License: about/license.md + +copyright: 'Copyright © 2019 David Flores' diff --git a/gns3fy/gns3fy.py b/gns3fy/gns3fy.py index 971fecb..c6f9797 100644 --- a/gns3fy/gns3fy.py +++ b/gns3fy/gns3fy.py @@ -153,6 +153,36 @@ def get_version(self): """ return self.http_call("get", url=f"{self.base_url}/version").json() + def projects_summary(self, is_print=True): + """ + Returns a summary of the projects in the server. If `is_print` is `False`, it + will return a list of tuples like: + + `[(name, project_id, total_nodes, total_links, status) ...]` + """ + _projects_summary = [] + for _p in self.get_projects(): + # Retrieve the project stats + _stats = self.http_call( + "get", f"{self.base_url}/projects/{_p['project_id']}/stats" + ).json() + if is_print: + print( + f"{_p['name']}: {_p['project_id']} -- Nodes: {_stats['nodes']} -- " + f"Links: {_stats['links']} -- Status: {_p['status']}" + ) + _projects_summary.append( + ( + _p["name"], + _p["project_id"], + _stats["nodes"], + _stats["links"], + _p["status"], + ) + ) + + return _projects_summary if not is_print else None + def get_projects(self): """ Returns the list of the projects on the server @@ -180,6 +210,36 @@ def get_project(self, name=None, project_id=None): else: raise ValueError("Must provide either a name or project_id") + def templates_summary(self, is_print=True): + """ + Returns a summary of the templates in the server. If `is_print` is `False`, it + will return a list of tuples like: + + `[(name, template_id, template_type, builtin, console_type, category) ...]` + """ + _templates_summary = [] + for _t in self.get_templates(): + if "console_type" not in _t: + _t["console_type"] = "N/A" + if is_print: + print( + f"{_t['name']}: {_t['template_id']} -- Type: {_t['template_type']}" + f" -- Builtin: {_t['builtin']} -- Console: {_t['console_type']} -- " + f"Category: {_t['category']}" + ) + _templates_summary.append( + ( + _t["name"], + _t["template_id"], + _t["template_type"], + _t["builtin"], + _t["console_type"], + _t["category"], + ) + ) + + return _templates_summary if not is_print else None + def get_templates(self): """ Returns the templates defined on the server. @@ -701,7 +761,37 @@ def suspend(self): else: self.get() - def create(self, extra_properties={}): + def update(self, **kwargs): + """ + Updates the node instance by passing the keyword arguments of the attributes + you want updated + + Example: + + ```python + router01.update(name="router01-CSX") + ``` + + This will update the project `auto_close` attribute to `True` + + **Required Attributes:** + + - `project_id` + - `connector` + """ + self._verify_before_action() + + _url = ( + f"{self.connector.base_url}/projects/{self.project_id}/nodes/{self.node_id}" + ) + + # TODO: Verify that the passed kwargs are supported ones + _response = self.connector.http_call("put", _url, json_data=kwargs) + + # Update object + self._update(_response.json()) + + def create(self): """ Creates a node. @@ -709,67 +799,56 @@ def create(self, extra_properties={}): `template` or `template_id` attribute supplied. This can be overriden/updated by sending a dictionary of the properties under `extra_properties`. - **Required Attributes:** + **Required Node instance attributes:** - `project_id` - `connector` - `compute_id`: Defaults to "local" - - `name` - - `node_type` - - `template` or `template_id` + - `template` or `template_id` - if not passed as arguments """ + if self.node_id: + raise ValueError("Node already created") if not self.connector: raise ValueError("Gns3Connector not assigned under 'connector'") if not self.project_id: - raise ValueError("Need to submit project_id") - if not self.name: - raise ValueError("Need to submit name") - if not self.node_type: - raise ValueError("Need to submit node_type") - if self.node_id: - raise ValueError("Node already created") - - _url = f"{self.connector.base_url}/projects/{self.project_id}/nodes" + raise ValueError("Node object needs to have project_id attribute") + if not self.template_id: + if self.template: + self.template_id = self.connector.get_template(name=self.template).get( + "template_id" + ) + else: + raise ValueError("Need either 'template' of 'template_id'") - data = { + cached_data = { k: v for k, v in self.__dict__.items() if k - not in ("project_id", "template", "links", "connector", "__initialised__") + not in ( + "project_id", + "template", + "template_id", + "links", + "connector", + "__initialised__", + ) if v is not None } - # Fetch template for properties - if self.template_id: - _properties = self.connector.get_template(template_id=self.template_id) - elif self.template: - _properties = self.connector.get_template(name=self.template) - else: - raise ValueError("You must provide template or template_id") - - # Delete not needed fields - for _field in ( - "compute_id", - "default_name_format", - "template_type", - "template_id", - "builtin", - "name", # Needs to be deleted because it overrides the name of host - ): - try: - _properties.pop(_field) - except KeyError: - continue - - # Override/Merge extra properties - if extra_properties: - _properties.update(**extra_properties) - data.update(properties=_properties) + _url = ( + f"{self.connector.base_url}/projects/{self.project_id}/" + f"templates/{self.template_id}" + ) - _response = self.connector.http_call("post", _url, json_data=data) + _response = self.connector.http_call( + "post", _url, json_data=dict(x=0, y=0, compute_id=self.compute_id) + ) self._update(_response.json()) + # Update the node attributes based on cached data + self.update(**cached_data) + def delete(self): """ Deletes the node from the project. It sets to `None` the attributes `node_id` @@ -956,9 +1035,13 @@ def create(self): def update(self, **kwargs): """ Updates the project instance by passing the keyword arguments of the attributes - you want to be updated + you want updated - Example: `lab.update(auto_close=True)` + Example: + + ```python + lab.update(auto_close=True) + ``` This will update the project `auto_close` attribute to `True` @@ -1218,7 +1301,7 @@ def nodes_inventory(self): `{ "router01": { - "hostname": "127.0.0.1", + "server": "127.0.0.1", "name": "router01", "console_port": 5077, "type": "vEOS" @@ -1235,17 +1318,19 @@ def nodes_inventory(self): self.get_nodes() _nodes_inventory = {} - _hostname = urlparse(self.connector.base_url).hostname + _server = urlparse(self.connector.base_url).hostname for _n in self.nodes: _nodes_inventory.update( { _n.name: { - "hostname": _hostname, + "server": _server, "name": _n.name, "console_port": _n.console, + "console_type": _n.console_type, "type": _n.node_type, + "template": _n.template, } } ) @@ -1354,37 +1439,29 @@ def get_link(self, link_id): """ return self._search_link(key="link_id", value=link_id) - def create_node(self, name=None, **kwargs): + def create_node(self, **kwargs): """ - Creates a node. + Creates a node. To know available parameters see `Node` object, specifically + the `create` method. The most basic example would be: - **Required Attributes:** + ```python + project.create_node(name='test-switch01', template='Ethernet switch') + ``` + + **Required Project instance attributes:** - `project_id` - `connector` - **Required Keyword attributes:** + **Required keyword aguments:** - - `name` - - `node_type` - - `compute_id`: Defaults to "local" - `template` or `template_id` """ if not self.nodes: self.get_nodes() - # Even though GNS3 allow same name to be pushed because it automatically - # generates a new name if matches an exising node, here is verified beforehand - # and forces developer to create new name. - _matches = [(_n.name, _n.node_id) for _n in self.nodes if name == _n.name] - if _matches: - raise ValueError( - f"Node with equal name found: {_matches[0][0]} - ID: {_matches[0][-1]}" - ) + _node = Node(project_id=self.project_id, connector=self.connector, **kwargs) - _node = Node( - project_id=self.project_id, connector=self.connector, name=name, **kwargs - ) _node.create() self.nodes.append(_node) print( diff --git a/poetry.lock b/poetry.lock index b931f85..0a802fc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -110,6 +110,14 @@ optional = false python-versions = ">=2.7" version = "0.3" +[[package]] +category = "dev" +description = "A platform independent file lock." +name = "filelock" +optional = false +python-versions = "*" +version = "3.0.12" + [[package]] category = "dev" description = "the modular source code checker: pep8, pyflakes and co" @@ -372,7 +380,7 @@ description = "Data validation and settings management using python 3.6 type hin name = "pydantic" optional = false python-versions = ">=3.6" -version = "0.31.1" +version = "0.32.2" [[package]] category = "main" @@ -391,7 +399,6 @@ six = ">=0.11.0" reference = "468cace9378a64a267848c07c21a5aedbfab2cf3" type = "git" url = "https://github.com/NiklasRosenstein/pydoc-markdown.git" - [[package]] category = "dev" description = "passive checker of Python programs" @@ -508,6 +515,24 @@ optional = false python-versions = ">= 3.5" version = "6.0.3" +[[package]] +category = "dev" +description = "tox is a generic virtualenv management and test command line tool" +name = "tox" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "3.13.2" + +[package.dependencies] +filelock = ">=3.0.0,<4" +importlib-metadata = ">=0.12,<1" +packaging = ">=14" +pluggy = ">=0.12.0,<1" +py = ">=1.4.17,<2" +six = ">=1.0.0,<2" +toml = ">=0.9.4" +virtualenv = ">=14.0.0" + [[package]] category = "dev" description = "Traitlets Python config system" @@ -537,6 +562,14 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" version = "1.25.3" +[[package]] +category = "dev" +description = "Virtual Python Environment builder" +name = "virtualenv" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "16.7.3" + [[package]] category = "dev" description = "Measures number of Terminal column cells of wide-character codes" @@ -554,7 +587,7 @@ python-versions = ">=2.7" version = "0.5.2" [metadata] -content-hash = "ce2489518292598aba61b374aa124eb114857064aab67ef919c604513b5d109b" +content-hash = "adb092ecae44255e15947e155f4f43383bda8680832051aed4763c70716a49ae" python-versions = "^3.7" [metadata.hashes] @@ -571,6 +604,7 @@ colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", coverage = ["08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", "0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", "141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", "19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", "23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", "245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", "331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", "386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", "3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", "60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", "63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", "6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", "6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", "7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", "826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", "93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", "9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", "af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", "bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", "bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", "c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", "dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", "df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", "e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", "e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", "e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", "eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", "eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", "ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", "efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", "fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", "ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025"] decorator = ["86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de", "f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6"] entrypoints = ["589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", "c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"] +filelock = ["18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", "929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"] flake8 = ["19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", "8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696"] idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"] importlib-metadata = ["23d3d873e008a513952355379d93cbcab874c58f4f034ff657c7a87422fa64e8", "80d2de76188eabfbfcf27e6a37342c2827801e59c4cc14b0371c56fed43820e3"] @@ -595,7 +629,7 @@ prompt-toolkit = ["11adf3389a996a6d45cc277580d0d53e8a5afd281d0c9ec71b28e6f121463 ptyprocess = ["923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", "d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"] py = ["64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", "dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"] pycodestyle = ["95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", "e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"] -pydantic = ["08dd1503f0524b78d03df76daf8b66ae213b9eaa0f072ad89d05a981e7aeaad2", "2b5db9a3e976fad1ddcb5e2daf4de87017f6b429e8e4871d3f52c5f004f7eaf7", "68085bebf700c2dbdf0ccdd5f09a4b19c13ced26b9bdd3447920f710fcd98c57", "772300e1efbd1271aade7bb03127c51bf8a2f705c861a8f5e82e0bc97f1f125e", "8bf39547da39abf5e02c79ed13ed377e65a095e2a71069ac7ce625de97840e63", "9324efe3eaa10cca3a488dc8ede1e0907f64394949fc6b51ced68cd2c8c30048"] +pydantic = ["18598557f0d9ab46173045910ed50458c4fb4d16153c23346b504d7a5b679f77", "6a9335c968e13295430a208487e74d69fef40168b72dea8d975765d14e2da660", "6f5eb88fe4c21380aa064b7d249763fc6306f0b001d7e7d52d80866d1afc9ed3", "bc6c6a78647d7a65a493e1107572d993f26a652c49183201e3c7d23924bf7311", "e1a63b4e6bf8820833cb6fa239ffbe8eec57ccdd7d66359eff20e68a83c1deeb", "ede2d65ae33788d4e26e12b330b4a32c53cb14131c65bca3a59f037c73f6ee7a"] pydoc-markdown = [] pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", "d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"] pygments = ["71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", "881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"] @@ -608,8 +642,10 @@ requests-mock = ["12e17c7ad1397fd1df5ead7727eb3f1bdc9fe1c18293b0492e0e01b57997e3 six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"] toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"] tornado = ["349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c", "398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60", "4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281", "559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5", "abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7", "c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9", "c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5"] +tox = ["dab0b0160dd187b654fc33d690ee1d7bf328bd5b8dc6ef3bb3cc468969c659ba", "ee35ffce74933a6c6ac10c9a0182e41763140a5a5070e21b114feca56eaccdcd"] traitlets = ["9c4bd2d267b7153df9152698efb1050a5d84982d3384a37b2c1f7723ba3e7835", "c6cb5e6f57c5a9bdaa40fa71ce7b4af30298fbab9ece9815b5d995ab6217c7d9"] typing = ["38566c558a0a94d6531012c8e917b1b8518a41e418f7f15f00e129cc80162ad3", "53765ec4f83a2b720214727e319607879fec4acde22c4fbb54fa2604e79e44ce", "84698954b4e6719e912ef9a42a2431407fe3755590831699debda6fba92aac55"] urllib3 = ["b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1", "dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"] +virtualenv = ["5e4d92f9a36359a745ddb113cabb662e6100e71072a1e566eb6ddfcc95fdb7ed", "b6711690882013bc79e0eac55889d901596f0967165d80adfa338c5729db1c71"] wcwidth = ["3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", "f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"] zipp = ["4970c3758f4e89a7857a973b1e2a5d75bcdc47794442f2e2dd4fe8e0466e809a", "8a5712cfd3bb4248015eb3b0b3c54a5f6ee3f2425963ef2a0125b8bc40aafaec"] diff --git a/pyproject.toml b/pyproject.toml index 9adf164..95362e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "gns3fy" -version = "0.2.0" +version = "0.3.0" description = "Python wrapper around GNS3 Server API" authors = ["David Flores "] license = "MIT" @@ -10,12 +10,13 @@ homepage = "https://github.com/davidban77/gns3fy" keywords = ["network", "gns3"] classifiers = [ "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.6", ] [tool.poetry.dependencies] python = "^3.7" requests = "^2.22" -pydantic = "^0.31.0" +pydantic = "^0.*" [tool.poetry.dev-dependencies] ipython = "^7.7" @@ -27,6 +28,7 @@ pytest-cov = "^2.7" mkdocs = "^1.0" pydoc-markdown = { git = "https://github.com/NiklasRosenstein/pydoc-markdown.git", branch = "develop", category="dev" } mkdocs-cinder = "^0.17.0" +tox = "^3.13" [build-system] requires = ["poetry>=0.12"] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index e14b761..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[flake8] -max-line-length=88 diff --git a/tests/data/projects.json b/tests/data/projects.json index 8f64766..1c4c108 100644 --- a/tests/data/projects.json +++ b/tests/data/projects.json @@ -18,13 +18,7 @@ "status": "closed", "supplier": null, "variables": null, - "zoom": 100, - "stats": { - "drawings": 0, - "links": 9, - "nodes": 10, - "snapshots": 0 - } + "zoom": 100 }, { "auto_close": false, diff --git a/tests/data/projects.py b/tests/data/projects.py index acd81ff..1f9919d 100644 --- a/tests/data/projects.py +++ b/tests/data/projects.py @@ -5,8 +5,8 @@ "2845ce8908e3', filename='test2.gns3', auto_start=False, auto_close=False, " "auto_open=False, drawing_grid_size=25, grid_size=75, scene_height=1000, " "scene_width=2000, show_grid=True, show_interface_labels=False, show_layers=" - "False, snap_to_grid=False, supplier=None, variables=None, zoom=100, stats={'" - "drawings': 0, 'links': 9, 'nodes': 10, 'snapshots': 0})" + "False, snap_to_grid=False, supplier=None, variables=None, zoom=100, stats=" + "None)" ), ( "Project(name='API_TEST', project_id='4b21dfb3-675a-4efa-8613-2f7fb32e76fe', " diff --git a/tests/test_api.py b/tests/test_api.py index 4b4cf43..482782c 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -96,6 +96,13 @@ def post_put_matcher(request): resp.status_code = 204 resp.json = lambda: _returned return resp + elif request.path_url.endswith( + f"/{CPROJECT['id']}/templates/{CTEMPLATE['id']}" + ): + _returned = json_api_test_node() + resp.status_code = 201 + resp.json = lambda: _returned + return resp elif request.path_url.endswith(f"/{CPROJECT['id']}/nodes"): _data = request.json() if not any(x in _data for x in ("compute_id", "name", "node_type")): @@ -182,6 +189,25 @@ def post_put_matcher(request): resp.status_code = 200 resp.json = lambda: {**_returned, **_data} return resp + elif request.path_url.endswith(f"/{CPROJECT['id']}/nodes/{CNODE['id']}"): + _data = request.json() + _returned = json_api_test_node() + # Update the node data based on the _data sent on the request + for sitem in _data: + if sitem in _returned: + if isinstance(_returned[sitem], list): + continue + elif isinstance(_returned[sitem], dict): + for k, v in _data[sitem].items(): + if k in _returned[sitem]: + _returned[sitem][k] = v + else: + _returned[sitem] = _data[sitem] + if _data.get("name") != "alpine-1": + _returned.update(node_id="NEW_NODE_ID") + resp.status_code = 200 + resp.json = lambda: _returned + return resp return None @@ -236,6 +262,12 @@ def _apply_responses(self): f"{self.base_url}/projects/{CPROJECT['id']}/stats", json={"drawings": 0, "links": 4, "nodes": 6, "snapshots": 0}, ) + # Extra project + self.adapter.register_uri( + "GET", + f"{self.base_url}/projects/c9dc56bf-37b9-453b-8f95-2845ce8908e3/stats", + json={"drawings": 0, "links": 9, "nodes": 10, "snapshots": 0}, + ) self.adapter.register_uri( "POST", f"{self.base_url}/projects/{CPROJECT['id']}/nodes/start", @@ -512,6 +544,70 @@ def test_delete_project(self, gns3_server): response = gns3_server.delete_project(project_id=CPROJECT["id"]) assert response is None + def test_projects_summary(self, gns3_server): + projects_summary = gns3_server.projects_summary(is_print=False) + assert ( + str(projects_summary) + == "[('test2', 'c9dc56bf-37b9-453b-8f95-2845ce8908e3', 10, 9, 'closed'), " + "('API_TEST', '4b21dfb3-675a-4efa-8613-2f7fb32e76fe', 6, 4, 'opened')]" + ) + + def test_projects_summary_print(self, capsys, gns3_server): + gns3_server.projects_summary(is_print=True) + captured = capsys.readouterr() + assert captured.out == ( + "test2: c9dc56bf-37b9-453b-8f95-2845ce8908e3 -- Nodes: 10 -- Links: 9 -- " + "Status: closed\nAPI_TEST: 4b21dfb3-675a-4efa-8613-2f7fb32e76fe -- Nodes: " + "6 -- Links: 4 -- Status: opened\n" + ) + + def test_templates_summary(self, gns3_server): + templates_summary = gns3_server.templates_summary(is_print=False) + assert ( + str(templates_summary) + == "[('IOU-L3', '8504c605-7914-4a8f-9cd4-a2638382db0e', 'iou', False, " + "'telnet', 'router'), ('IOU-L2', '92cccfb2-6401-48f2-8964-3c75323be3cb', " + "'iou', False, 'telnet', 'switch'), ('vEOS', 'c6203d4b-d0ce-4951-bf18-" + "c44369d46804', 'qemu', False, 'telnet', 'router'), ('alpine', " + "'847e5333-6ac9-411f-a400-89838584371b', 'docker', False, 'telnet', 'guest'" + "), ('Cloud', '39e257dc-8412-3174-b6b3-0ee3ed6a43e9', 'cloud', True, 'N/A'" + ", 'guest'), ('NAT', 'df8f4ea9-33b7-3e96-86a2-c39bc9bb649c', 'nat', True, '" + "N/A', 'guest'), ('VPCS', '19021f99-e36f-394d-b4a1-8aaa902ab9cc', 'vpcs', " + "True, 'N/A', 'guest'), ('Ethernet switch', '1966b864-93e7-32d5-965f-" + "001384eec461', 'ethernet_switch', True, 'none', 'switch'), ('Ethernet hub" + "', 'b4503ea9-d6b6-3695-9fe4-1db3b39290b0', 'ethernet_hub', True, 'N/A', '" + "switch'), ('Frame Relay switch', 'dd0f6f3a-ba58-3249-81cb-a1dd88407a47', " + "'frame_relay_switch', True, 'N/A', 'switch'), ('ATM switch', " + "'aaa764e2-b383-300f-8a0e-3493bbfdb7d2', 'atm_switch', True, 'N/A', 'switch" + "')]" + ) + + def test_templates_summary_print(self, capsys, gns3_server): + gns3_server.templates_summary(is_print=True) + captured = capsys.readouterr() + assert captured.out == ( + "IOU-L3: 8504c605-7914-4a8f-9cd4-a2638382db0e -- Type: iou -- Builtin: " + "False -- Console: telnet -- Category: router\nIOU-L2: " + "92cccfb2-6401-48f2-8964-3c75323be3cb -- Type: iou -- Builtin: False -- " + "Console: telnet -- Category: switch\nvEOS: c6203d4b-d0ce-4951-bf18-" + "c44369d46804 -- Type: qemu -- Builtin: False -- Console: telnet -- " + "Category: router\nalpine: 847e5333-6ac9-411f-a400-89838584371b -- Type: " + "docker -- Builtin: False -- Console: telnet -- Category: guest\nCloud: " + "39e257dc-8412-3174-b6b3-0ee3ed6a43e9 -- Type: cloud -- Builtin: True -- " + "Console: N/A -- Category: guest\nNAT: df8f4ea9-33b7-3e96-86a2-" + "c39bc9bb649c -- Type: nat -- Builtin: True -- Console: N/A -- Category: " + "guest\nVPCS: 19021f99-e36f-394d-b4a1-8aaa902ab9cc -- Type: vpcs -- Builtin" + ": True -- Console: N/A -- Category: guest\nEthernet switch: " + "1966b864-93e7-32d5-965f-001384eec461 -- Type: ethernet_switch -- Builtin: " + "True -- Console: none -- Category: switch\nEthernet hub: " + "b4503ea9-d6b6-3695-9fe4-1db3b39290b0 -- Type: ethernet_hub -- Builtin: " + "True -- Console: N/A -- Category: switch\nFrame Relay switch: " + "dd0f6f3a-ba58-3249-81cb-a1dd88407a47 -- Type: frame_relay_switch -- " + "Builtin: True -- Console: N/A -- Category: switch\nATM switch: " + "aaa764e2-b383-300f-8a0e-3493bbfdb7d2 -- Type: atm_switch -- Builtin: True " + "-- Console: N/A -- Category: switch\n" + ) + def test_wrong_server_url(self, gns3_server): gns3_server.base_url = "WRONG URL" with pytest.raises(requests.exceptions.InvalidURL): @@ -719,13 +815,11 @@ def test_create_override_properties(self, gns3_server): connector=gns3_server, project_id=CPROJECT["id"], template=CTEMPLATE["name"], + properties=dict(console_http_port=8080), ) - node.create(extra_properties={"console_http_port": 8080}) + node.create() assert "alpine-1" == node.name - # NOTE: The image name of alpine in teh template is different than the one - # defined on the node properties, which has the version alpine:latest. - # Need to keep an eye - assert "alpine" == node.properties["image"] + assert "alpine:latest" == node.properties["image"] assert node.properties["console_http_port"] == 8080 def test_error_create_with_invalid_parameter_type(self, gns3_server): @@ -743,31 +837,16 @@ def test_error_create_with_invalid_parameter_type(self, gns3_server): "params,expected", [ ({"project_id": "SOME_ID"}, "Gns3Connector not assigned under 'connector'"), - ({"connector": "SOME_CONN"}, "Need to submit project_id"), ( - { - "connector": "SOME_CONN", - "project_id": "SOME_ID", - "compute_id": "SOME_ID", - }, - "Need to submit name", + {"connector": "SOME_CONN"}, + "Node object needs to have project_id attribute", ), ( { "connector": "SOME_CONN", "project_id": "SOME_ID", "compute_id": "SOME_ID", - "name": "SOME_NAME", - }, - "Need to submit node_type", - ), - ( - { - "connector": "SOME_CONN", - "project_id": "SOME_ID", - "compute_id": "SOME_ID", - "name": "SOME_NAME", - "node_type": "docker", + "template": "SOME_TEMPLATE", "node_id": "SOME_ID", }, "Node already created", @@ -780,7 +859,7 @@ def test_error_create_with_invalid_parameter_type(self, gns3_server): "name": "SOME_NAME", "node_type": "docker", }, - "You must provide template or template_id", + "Need either 'template' of 'template_id'", ), ], ) @@ -968,13 +1047,28 @@ def test_nodes_summary(self, api_test_project): "'cde85a31-c97f-4551-9596-a3ed12c08498')]" ) + def test_nodes_summary_print(self, capsys, api_test_project): + api_test_project.nodes_summary(is_print=True) + captured = capsys.readouterr() + assert captured.out == ( + "Ethernetswitch-1: started -- Console: 5000 -- ID: " + "da28e1c0-9465-4f7c-b42c-49b2f4e1c64d\nIOU1: started -- Console: 5001 -- ID" + ": de23a89a-aa1f-446a-a950-31d4bf98653c\nIOU2: started -- Console: 5002 -- " + "ID: 0d10d697-ef8d-40af-a4f3-fafe71f5458b\nvEOS: started -- Console: 5003 " + "-- ID: 8283b923-df0e-4bc1-8199-be6fea40f500\nalpine-1: started -- Console" + ": 5005 -- ID: ef503c45-e998-499d-88fc-2765614b313e\nCloud-1: started -- " + "Console: None -- ID: cde85a31-c97f-4551-9596-a3ed12c08498\n" + ) + def test_nodes_inventory(self, api_test_project): nodes_inventory = api_test_project.nodes_inventory() assert { - "hostname": "gns3server", + "server": "gns3server", "name": "alpine-1", "console_port": 5005, + "console_type": "telnet", "type": "docker", + "template": None, } == nodes_inventory["alpine-1"] def test_links_summary(self, api_test_project): @@ -987,6 +1081,16 @@ def test_links_summary(self, api_test_project): "'eth0'), ('Cloud-1', 'eth1', 'Ethernetswitch-1', 'Ethernet7')]" ) + def test_links_summary_print(self, capsys, api_test_project): + api_test_project.links_summary(is_print=True) + captured = capsys.readouterr() + assert captured.out == ( + "IOU1: Ethernet0/0 ---- Ethernetswitch-1: Ethernet1\nIOU1: Ethernet1/0 " + "---- IOU2: Ethernet1/0\nvEOS: Management1 ---- Ethernetswitch-1: Ethernet0" + "\nvEOS: Ethernet1 ---- alpine-1: eth0\nCloud-1: eth1 ---- Ethernetswitch-" + "1: Ethernet7\n" + ) + def test_get_node_by_name(self, api_test_project): switch = api_test_project.get_node(name="IOU1") assert switch.name == "IOU1" @@ -1005,24 +1109,15 @@ def test_get_link_by_id(self, api_test_project): def test_create_node(self, api_test_project): api_test_project.create_node( - name="alpine-2", node_type="docker", template=CTEMPLATE["name"] + name="alpine-2", console=5077, template=CTEMPLATE["name"] ) alpine2 = api_test_project.get_node(name="alpine-2") + print(api_test_project.nodes_summary()) assert alpine2.console == 5077 assert alpine2.name == "alpine-2" assert alpine2.node_type == "docker" assert alpine2.node_id == "NEW_NODE_ID" - def test_error_create_node_with_equal_name(self, api_test_project): - with pytest.raises(ValueError, match="Node with equal name found"): - api_test_project.create_node( - name="alpine-1", - node_type="docker", - template=CTEMPLATE["name"], - connector=gns3_server, - project_id=CPROJECT["id"], - ) - def test_create_link(self, api_test_project): api_test_project.create_link("IOU1", "Ethernet1/1", "vEOS", "Ethernet2") link = api_test_project.get_link(link_id="NEW_LINK_ID") diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..09c3d1f --- /dev/null +++ b/tox.ini @@ -0,0 +1,12 @@ +[flake8] +max-line-length=88 + +[tox] +isolated_build = true +envlist = python3.6, python3.7 + +[testenv] +whitelist_externals = poetry +commands = + poetry install -v + poetry run pytest tests/