mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-04 14:31:13 +02:00
Merge remote-tracking branch 'origin/3.0' into gh-pages
This commit is contained in:
commit
5bdd307ea9
471 files changed
+75257
-4705
No files matched your search
+443
@@ -0,0 +1,443 @@
|
||||
Creative Commons Attribution-ShareAlike 4.0 International
|
||||
|
||||
Copyright (c) 2026 GNS3 Web UI Project
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Attribution-ShareAlike 4.0 International
|
||||
|
||||
Copyright (C) 2025 Creative Commons Corporation
|
||||
|
||||
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
||||
does not provide legal services or legal advice. Distribution of
|
||||
Creative Commons public licenses does not create a lawyer-client or
|
||||
other relationship. Creative Commons makes its licenses and related
|
||||
information available on an "as-is" basis. Creative Commons gives no
|
||||
warranties regarding its licenses, any material licensed under their
|
||||
terms and conditions, or any related information. Creative Commons
|
||||
disclaims all liability for damages resulting from their use to the
|
||||
fullest extent possible.
|
||||
|
||||
Using Creative Commons Public Licenses
|
||||
|
||||
Creative Commons public licenses provide a standard set of terms and
|
||||
conditions that creators and other rights holders may use to share
|
||||
original works of authorship and other material subject to copyright
|
||||
and certain other rights specified in the public license below. The
|
||||
following considerations are for informational purposes only, are not
|
||||
exhaustive, and do not form part of our licenses.
|
||||
|
||||
Considerations for licensors: Our public licenses are
|
||||
intended for use by those authorized to give the public
|
||||
permission to use material in ways otherwise restricted by
|
||||
copyright and certain other rights. Our licenses are
|
||||
irrevocable. Licensors should read and understand the terms
|
||||
and conditions of the license they choose before applying it.
|
||||
Licensors should also secure all rights necessary before
|
||||
applying our licenses so that the public can reuse the
|
||||
material as expected. Licensors should clearly mark any
|
||||
material not subject to the license. This includes other CC-
|
||||
licensed material, or material used under an exception or
|
||||
limitation to copyright. More considerations for licensors:
|
||||
wiki.creativecommons.org/Considerations_for_licensors
|
||||
|
||||
Considerations for the public: Our public licenses are
|
||||
intended for use by those authorized to give the public
|
||||
permission to use material in ways otherwise restricted by
|
||||
copyright and certain other rights. Our licenses are
|
||||
irrevocable. Licensors should read and understand the terms
|
||||
and conditions of the license they choose before applying it.
|
||||
Licensors should also secure all rights necessary before
|
||||
applying our licenses so that the public can reuse the
|
||||
material as expected. Licensors should clearly mark any
|
||||
material not subject to the license. This includes other CC-
|
||||
licensed material, or material used under an exception or
|
||||
limitation to copyright. More considerations for licensors:
|
||||
wiki.creativecommons.org/Considerations_for_licensors
|
||||
|
||||
Creative Commons Attribution-ShareAlike 4.0 International Public
|
||||
License
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree
|
||||
to be bound by the terms and conditions of this Creative Commons
|
||||
Attribution-ShareAlike 4.0 International Public License ("Public
|
||||
License"). To the extent this Public License may be interpreted as a
|
||||
contract, You are granted the Licensed Rights in consideration of Your
|
||||
acceptance of these terms and conditions, and the Licensor grants You
|
||||
such rights in consideration of benefits the Licensor receives from
|
||||
making the Licensed Material available under these terms and conditions.
|
||||
|
||||
Section 1 – Definitions.
|
||||
|
||||
a. Adapted Material means material subject to Copyright and Similar
|
||||
Rights that is derived from or based upon the Licensed Material
|
||||
and in which the Licensed Material is translated, altered,
|
||||
arranged, transformed, or otherwise modified in a manner requiring
|
||||
permission under the Copyright and Similar Rights held by the
|
||||
Licensor. For purposes of this Public License, where the Licensed
|
||||
Material is a musical work, performance, or sound recording,
|
||||
Adapted Material is always produced where the Licensed Material is
|
||||
synched in timed relation with a moving image.
|
||||
|
||||
b. Adapter's License means the license You apply to Your Copyright
|
||||
and Similar Rights in Your contributions to Adapted Material in
|
||||
accordance with the terms and conditions of this Public License.
|
||||
|
||||
c. Copyright and Similar Rights means copyright and/or similar rights
|
||||
closely related to copyright including, without limitation,
|
||||
publication, distribution, public performance, public display, and
|
||||
adaptation rights, and rights in database extensions and
|
||||
collections.
|
||||
|
||||
d. Licensed Material means the artistic or literary work, database,
|
||||
or other material to which the Licensor applied this Public
|
||||
License.
|
||||
|
||||
e. Licensed Rights means rights granted to You subject to the terms
|
||||
and conditions of this Public License, which are limited to all
|
||||
Copyright and Similar Rights that apply to Your use of the
|
||||
Licensed Material and that the Licensor has authority to license.
|
||||
|
||||
f. Licensor means the individual(s) or entity(ies) granting rights
|
||||
under this Public License.
|
||||
|
||||
g. Share means to provide material to the public by any means or
|
||||
process that requires permission under the Licensed Rights, such
|
||||
as reproduction, public display, public performance, distribution,
|
||||
dissemination, communication, or importation, and to make material
|
||||
available to the public including in ways that members of the
|
||||
public may access the material from a place and at a time
|
||||
individually chosen by them.
|
||||
|
||||
h. Sui Generis Database Rights means rights other than copyright
|
||||
resulting from Directive 96/9/EC of the European Parliament and
|
||||
of the Council of 11 March 1996 on the legal protection of
|
||||
databases, as amended and/or succeeded, as well as other
|
||||
essentially equivalent rights anywhere in the world.
|
||||
|
||||
i. You means the individual or entity exercising the Licensed Rights
|
||||
under this Public License. Your has a corresponding meaning.
|
||||
|
||||
Section 2 – Scope.
|
||||
|
||||
a. License grant.
|
||||
|
||||
1. Subject to the terms and conditions of this Public License,
|
||||
the Licensor hereby grants You a worldwide, royalty-free,
|
||||
non-sublicensable, non-exclusive, irrevocable license to
|
||||
exercise the Licensed Rights in the Licensed Material to:
|
||||
|
||||
a. reproduce and Share the Licensed Material, in whole or
|
||||
in part; and
|
||||
|
||||
b. produce, reproduce, and Share Adapted Material.
|
||||
|
||||
2. Exceptions and Limitations. For the avoidance of doubt,
|
||||
where Exceptions and Limitations apply to Your use, this
|
||||
Public License does not apply, and You do not need to
|
||||
comply with its terms and conditions.
|
||||
|
||||
3. Term. The term of this Public License is specified in
|
||||
Section 6(a).
|
||||
|
||||
4. Media and formats; technical modifications allowed. The
|
||||
Licensor authorizes You to exercise the Licensed Rights in
|
||||
all media and formats whether now known or hereafter created,
|
||||
and to make technical modifications necessary to do so. The
|
||||
Licensor waives and/or agrees not to assert any right or
|
||||
authority to forbid You from making technical modifications
|
||||
necessary to exercise the Licensed Rights, including
|
||||
technical modifications necessary to circumvent Effective
|
||||
Technological Measures. For purposes of this Public License,
|
||||
simply making modifications authorized by this Section 2(a)
|
||||
(4) never produces Adapted Material.
|
||||
|
||||
5. Downstream recipients.
|
||||
|
||||
a. Offer from the Licensor – Licensed Material. Every
|
||||
recipient of the Licensed Material automatically
|
||||
receives an offer from the Licensor to exercise the
|
||||
Licensed Rights under the terms and conditions of this
|
||||
Public License.
|
||||
|
||||
b. Additional offer from the Licensor – Adapted Material.
|
||||
Every recipient of Adapted Material from You
|
||||
automatically receives an offer from the Licensor to
|
||||
exercise the Licensed Rights in the Adapted Material
|
||||
under the conditions of the Adapter's License You
|
||||
apply.
|
||||
|
||||
6. No endorsement. Nothing in this Public License constitutes or
|
||||
may be construed as permission to assert or imply that You
|
||||
are, or that Your use of the Licensed Material is, connected
|
||||
with, or sponsored, endorsed, or granted official status by,
|
||||
the Licensor or others designated to receive attribution as
|
||||
provided in Section 3(a)(1)(A)(i).
|
||||
|
||||
b. Other rights. Moral rights, such as the right of integrity, are
|
||||
not licensed under this Public License, nor are publicity,
|
||||
privacy, and/or other similar personality rights; however, to the
|
||||
extent possible, the Licensor waives and/or agrees not to assert
|
||||
any such rights held by the Licensor to the limited extent
|
||||
necessary to allow You to exercise the Licensed Rights, but not
|
||||
otherwise.
|
||||
|
||||
c. Patent and trademark rights are not licensed under this Public
|
||||
License.
|
||||
|
||||
d. To the extent possible, the Licensor waives any right to collect
|
||||
royalties from You for the exercise of the Licensed Rights,
|
||||
whether directly or through a collecting society under any
|
||||
voluntary or waivable statutory or compulsory licensing scheme.
|
||||
In all other cases the Licensor expressly reserves any right to
|
||||
collect such royalties.
|
||||
|
||||
Section 3 – License Conditions.
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to
|
||||
the following conditions.
|
||||
|
||||
a. Attribution.
|
||||
|
||||
1. If You Share the Licensed Material (including in modified
|
||||
form), You must:
|
||||
|
||||
a. retain the following if it is supplied by the
|
||||
Licensor with the Licensed Material:
|
||||
|
||||
i. identification of the creator(s) of the
|
||||
Licensed Material and any others designated to
|
||||
receive attribution, in any reasonable manner
|
||||
requested by the Licensor (including by
|
||||
pseudonym if designated);
|
||||
|
||||
ii. a copyright notice;
|
||||
|
||||
iii. a notice that refers to this Public License;
|
||||
|
||||
iv. a notice that refers to the disclaimer of
|
||||
warranties;
|
||||
|
||||
v. a URI or hyperlink to the Licensed Material to
|
||||
the extent reasonably practicable;
|
||||
|
||||
b. indicate if You modified the Licensed Material and
|
||||
retain an indication of any previous modifications; and
|
||||
|
||||
c. indicate the Licensed Material is licensed under this
|
||||
Public License, and include the text of, or the URI
|
||||
or hyperlink to, this Public License.
|
||||
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||
reasonable manner based on the medium, means, and context in
|
||||
which You Share the Licensed Material. For example, it may
|
||||
be reasonable to satisfy the conditions by providing a URI
|
||||
or hyperlink to a resource that includes the required
|
||||
information.
|
||||
|
||||
3. If requested by the Licensor, You must remove any of the
|
||||
information required by Section 3(a)(1)(A) to the extent
|
||||
reasonably practicable.
|
||||
|
||||
4. If You Share Adapted Material You produce, the Adapter's
|
||||
License You apply must not prevent recipients of the
|
||||
Adapted Material from complying with this Public License.
|
||||
|
||||
b. ShareAlike.
|
||||
|
||||
In addition to the conditions in Section 3(a), if You Share
|
||||
Adapted Material You produce, the following conditions also apply.
|
||||
|
||||
1. The Adapter's License You apply must be a Creative Commons
|
||||
license with the same License Elements, this version or
|
||||
later, or a BY-SA Compatible License.
|
||||
|
||||
2. You must include the text of, or the URI or hyperlink to,
|
||||
the Adapter's License You apply. You may satisfy this
|
||||
condition in any reasonable manner based on the medium,
|
||||
means, and context in which You Share Adapted Material.
|
||||
|
||||
3. You may not offer or impose any additional or different terms
|
||||
or conditions on, or apply any Effective Technological
|
||||
Measures to, Adapted Material that restrict exercise of the
|
||||
rights granted under the Adapter's License You apply.
|
||||
|
||||
Section 4 – Sui Generis Database Rights.
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that
|
||||
apply to Your use of the Licensed Material:
|
||||
|
||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||
to extract, reuse, reproduce, and Share all or a substantial
|
||||
portion of the contents of the database;
|
||||
|
||||
b. if You include all or a substantial portion of the database
|
||||
contents in a database in which You have Sui Generis Database
|
||||
Rights, then the database in which You have Sui Generis Database
|
||||
Rights (but not its individual contents) is Adapted Material; and
|
||||
|
||||
c. You must comply with the conditions in Section 3(a) if You Share
|
||||
all or a substantial portion of the contents of the database.
|
||||
|
||||
For the avoidance of doubt, this Section 4 supplements and does not
|
||||
replace Your obligations under this Public License where the Licensed
|
||||
Rights include other Copyright and Similar Rights.
|
||||
|
||||
Section 5 – Disclaimer of Warranties and Limitation of Liability.
|
||||
|
||||
a. Unless otherwise separately undertaken by the Licensor, to the
|
||||
extent possible, the Licensor offers the Licensed Material as-is
|
||||
and as-available, and makes no representations or warranties of
|
||||
any kind concerning the Licensed Material, whether express,
|
||||
implied, statutory, or other. This includes, without limitation,
|
||||
warranties of title, merchantability, fitness for a particular
|
||||
purpose, non-infringement, absence of latent or other defects,
|
||||
accuracy, or the presence or absence of errors, whether or not
|
||||
known or discoverable. Where disclaimers of warranties are not
|
||||
allowed in full or in part, this disclaimer may not apply to You.
|
||||
|
||||
b. To the extent possible, in no event will the Licensor be liable
|
||||
to You on any legal theory (including, without limitation,
|
||||
negligence) or otherwise for any direct, special, indirect,
|
||||
incidental, consequential, punitive, exemplary, or other losses,
|
||||
costs, expenses, or damages that arise out of this Public License
|
||||
or use of the Licensed Material, even if the Licensor has been
|
||||
advised of the possibility of such losses, costs, expenses, or
|
||||
damages. Where a limitation of liability is not allowed in full
|
||||
or in part, this limitation may not apply to You.
|
||||
|
||||
c. The disclaimer of warranties and limitation of liability provided
|
||||
above shall be interpreted in a manner that, to the extent
|
||||
possible, most closely approximates an absolute disclaimer and
|
||||
waiver of all liability.
|
||||
|
||||
Section 6 – Term and Termination.
|
||||
|
||||
a. This Public License applies for the term of the Copyright and
|
||||
Similar Rights licensed here. However, if You fail to comply
|
||||
with this Public License, then Your rights under this Public
|
||||
License terminate automatically.
|
||||
|
||||
b. Where Your right to use the Licensed Material has terminated
|
||||
under Section 6(a), it reinstates:
|
||||
|
||||
1. automatically as of the date the violation is cured,
|
||||
provided it is cured within 30 days of Your discovery of
|
||||
the violation; or
|
||||
|
||||
2. upon express reinstatement by the Licensor.
|
||||
|
||||
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||
right the Licensor may have to seek remedies for Your violations
|
||||
of this Public License.
|
||||
|
||||
c. For the avoidance of doubt, the Licensor may also offer the
|
||||
Licensed Material under separate terms or conditions or stop
|
||||
distributing the Licensed Material at any time; however, doing so
|
||||
will not terminate this Public License.
|
||||
|
||||
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||
License.
|
||||
|
||||
Section 7 – Other Terms and Conditions.
|
||||
|
||||
a. The Licensor shall not be bound by any additional or different
|
||||
terms or conditions communicated by You unless expressly agreed.
|
||||
|
||||
b. Any arrangements, understandings, or agreements regarding the
|
||||
Licensed Material not stated herein are separate from and
|
||||
independent of the terms and conditions of this Public License.
|
||||
|
||||
Section 8 – Interpretation.
|
||||
|
||||
a. For the avoidance of doubt, this Public License does not, and
|
||||
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||
conditions on any use of the Licensed Material that could lawfully
|
||||
be made without permission under this Public License.
|
||||
|
||||
b. To the extent possible, if any provision of this Public License
|
||||
is deemed unenforceable, it shall be automatically reformed to
|
||||
the minimum extent necessary to make it enforceable. If the
|
||||
provision cannot be reformed, it shall be severed from this
|
||||
Public License without affecting the enforceability of the
|
||||
remaining terms and conditions.
|
||||
|
||||
c. No term or condition of this Public License will be waived and
|
||||
no failure to comply consented to unless expressly agreed to by
|
||||
the Licensor.
|
||||
|
||||
d. Nothing in this Public License constitutes or may be interpreted
|
||||
as a limitation upon, or waiver of, any privileges and immunities
|
||||
that apply to the Licensor or You, including from the legal
|
||||
processes of any jurisdiction or authority.
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons is not a party to its public licenses. Notwithstanding,
|
||||
Creative Commons may elect to apply one of its public licenses to
|
||||
material it publishes and in those instances will be considered the
|
||||
"Licensor." The text of the Creative Commons public licenses is
|
||||
dedicated to the public domain under the CC0 Public Domain Dedication.
|
||||
The text of the Creative Commons public licenses is dedicated to the
|
||||
public domain under the CC0 Public Domain Dedication. Except for the
|
||||
limited purpose of indicating that material is shared under a Creative
|
||||
Commons public license or as otherwise permitted by the Creative Commons
|
||||
policies published at creativecommons.org/policies, Creative Commons
|
||||
does not authorize the use of the trademark "Creative Commons" or any
|
||||
other trademark or logo of Creative Commons without its prior written
|
||||
consent including, without limitation, in connection with any
|
||||
unauthorized modifications to any of its public licenses or any other
|
||||
arrangements, understandings, or agreements concerning use of licensed
|
||||
material. For the avoidance of doubt, this paragraph does not form part
|
||||
of the public licenses.
|
||||
|
||||
Creative Commons may be contacted at creativecommons.org.
|
||||
|
||||
=======================================================================
|
||||
|
||||
For the GNS3 Web UI documentation:
|
||||
|
||||
- Documentation License: CC BY-SA 4.0
|
||||
- Project License: GPLv3 (see root LICENSE file)
|
||||
- License URL: https://creativecommons.org/licenses/by-sa/4.0/
|
||||
|
||||
All documentation files in this directory are licensed under the
|
||||
Creative Commons Attribution-ShareAlike 4.0 International License
|
||||
(CC BY-SA 4.0).
|
||||
|
||||
=======================================================================
|
||||
|
||||
IMPORTANT: ShareAlike Requirements for Software Derivatives
|
||||
|
||||
Under the CC BY-SA 4.0 ShareAlike condition, if you create derivative
|
||||
works based on this documentation, including software that is considered
|
||||
a derivative of the documentation, you must:
|
||||
|
||||
1. **License your derivative work under CC BY-SA 4.0 or a compatible
|
||||
license** (such as GPLv3, which is compatible with CC BY-SA 4.0)
|
||||
|
||||
2. **Attribute the original documentation** to the GNS3 Web UI Project
|
||||
|
||||
3. **Indicate if you modified the documentation** and retain
|
||||
indications of previous modifications
|
||||
|
||||
4. **Include the license notice** and link to the CC BY-SA 4.0 license
|
||||
|
||||
What constitutes a derivative work:
|
||||
- Translations of the documentation
|
||||
- Adaptations, modifications, or enhancements to the documentation
|
||||
- Software that incorporates substantial portions of the documentation
|
||||
- Works based on the documentation's structure, organization, or content
|
||||
|
||||
Compatible licenses include:
|
||||
- CC BY-SA 4.0 or later
|
||||
- GPLv3 (GNU General Public License version 3)
|
||||
- AGPLv3 (GNU Affero General Public License version 3)
|
||||
- Other licenses approved as compatible by Creative Commons
|
||||
|
||||
For more information on CC BY-SA compatibility, see:
|
||||
https://creativecommons.org/compatiblelicenses
|
||||
|
||||
If you are unsure whether your use constitutes a derivative work or
|
||||
which license to apply, please consult the full CC BY-SA 4.0 license
|
||||
text above or seek legal advice.
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
<!--
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
See LICENSE file for licensing information.
|
||||
-->
|
||||
|
||||
> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.
|
||||
|
||||
# GNS3 Server Documentation
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
This documentation is licensed under the **Creative Commons Attribution-ShareAlike 4.0 International License (CC BY-SA 4.0)**.
|
||||
|
||||
⚠️ **Important - ShareAlike Requirement**: If you create derivative works based on this documentation (including software that incorporates substantial portions of the documentation), your work must also be licensed under **CC BY-SA 4.0 or a compatible license** (such as GPLv3).
|
||||
|
||||
- 📄 **Full License Text**: See [docs/LICENSE](./LICENSE)
|
||||
- 🔗 **License URL**: https://creativecommons.org/licenses/by-sa/4.0/
|
||||
- 📖 **Compatibility**: https://creativecommons.org/compatiblelicenses
|
||||
|
||||
**Dual License Structure**:
|
||||
- 📚 **Documentation**: CC BY-SA 4.0 (this directory)
|
||||
- 💻 **Software Code**: GPLv3 (see root [LICENSE](../LICENSE))
|
||||
|
||||
---
|
||||
|
||||
Technical documentation for the GNS3 server project, covering features, AI Copilot, development setup, and known issues.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
docs/
|
||||
├── README.md # This file
|
||||
├── development-setup.md # Ubuntu 24.04 development environment setup
|
||||
├── openapi.json # OpenAPI specification
|
||||
├── features/ # Feature documentation
|
||||
│ ├── compute-controller-setup.md # Controller + Compute architecture & configuration
|
||||
│ ├── statistics-api.md # Aggregated statistics API for monitoring
|
||||
│ ├── vnc-websocket-console.md # Browser-based VNC console via WebSocket
|
||||
│ └── web-wireshark-business-process.md # Web Wireshark (Docker + xpra packet capture)
|
||||
├── gns3-copilot/ # AI Copilot feature documentation
|
||||
│ ├── netmiko_devices.md # Netmiko supported devices (366 types)
|
||||
│ ├── template-based-configuration-roadmap.md # Future: template-based config with HITL
|
||||
│ └── implemented/ # Implemented features
|
||||
│ ├── chat-api.md # Chat API (SSE, session management)
|
||||
│ ├── llm-model-configs.md # LLM model configuration system
|
||||
│ ├── command-security.md # Command security and filtering
|
||||
│ ├── context-window-management.md # Context window optimization
|
||||
│ ├── node-control-tools.md # Node start/stop/suspend tools
|
||||
│ └── multi-vendor-device-support.md # Multi-vendor device support
|
||||
└── bugs/ # Known issues & bug reports
|
||||
└── telnet-server-connection-race-condition.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### Controller + Compute Setup (`features/compute-controller-setup.md`)
|
||||
Architecture and minimum configuration for setting up GNS3 Controller with remote Compute nodes. Covers compute node config, controller registration, and multi-compute deployment.
|
||||
|
||||
### Statistics API (`features/statistics-api.md`)
|
||||
Aggregated server statistics API (`GET /v3/statistics`) for monitoring dashboards. Collects compute resources, project/node/link counts, and Web Wireshark container status in a single request.
|
||||
|
||||
### VNC WebSocket Console (`features/vnc-websocket-console.md`)
|
||||
Browser-based VNC console access via WebSocket. The Controller acts as WebSocket-to-WebSocket relay, and Compute bridges WebSocket to TCP for QEMU/Docker VMs. Supports noVNC clients.
|
||||
|
||||
### API Error Responses (`features/api-error-responses.md`)
|
||||
Unified error response format across all GNS3 API endpoints. Documents HTTP status codes, error types, and client-side error handling patterns.
|
||||
|
||||
### Web Wireshark (`features/web-wireshark-business-process.md`)
|
||||
Web-based packet capture analysis using Docker + xpra HTML5 client. Zero-install Wireshark experience directly in the browser, integrated with GNS3 topologies.
|
||||
|
||||
---
|
||||
|
||||
## GNS3 AI Copilot (`gns3-copilot/`)
|
||||
|
||||
### Implemented Features
|
||||
|
||||
| Feature | Description | Status |
|
||||
|---------|-------------|--------|
|
||||
| [Chat API](gns3-copilot/implemented/chat-api.md) | SSE streaming, session management, token statistics | Implemented |
|
||||
| [LLM Model Configs](gns3-copilot/implemented/llm-model-configs.md) | Multi-level model config (system/group/user) | Implemented |
|
||||
| [Command Security](gns3-copilot/implemented/command-security.md) | Command filtering, dangerous operation detection, HITL | Implemented |
|
||||
| [Context Window Management](gns3-copilot/implemented/context-window-management.md) | Token optimization, content filtering, compression | Implemented |
|
||||
| [Node Control Tools](gns3-copilot/implemented/node-control-tools.md) | Start/stop/suspend with batch ops and progress tracking | Implemented |
|
||||
| [Multi-Vendor Support](gns3-copilot/implemented/multi-vendor-device-support.md) | Cisco, Huawei, Ruijie, VPCS with custom Netmiko drivers | Implemented |
|
||||
|
||||
### Reference
|
||||
|
||||
- [Netmiko Supported Devices](gns3-copilot/netmiko_devices.md) — 366 device types (154 SSH, 55 Telnet, 3 custom GNS3 drivers)
|
||||
|
||||
### Roadmap
|
||||
|
||||
- [Template-Based Configuration with HITL](gns3-copilot/template-based-configuration-roadmap.md) — Jinja2 templates with human-in-the-loop confirmations for device configuration and node creation
|
||||
|
||||
---
|
||||
|
||||
## Known Issues (`bugs/`)
|
||||
|
||||
- [Telnet Server Connection Race Condition](bugs/telnet-server-connection-race-condition.md) — `getpeername()` error when client disconnects during connection setup (High severity, Open)
|
||||
|
||||
---
|
||||
|
||||
## Development Setup (`development-setup.md`)
|
||||
|
||||
Quick-start guide for Ubuntu 24.04: install via PPA, set up dependencies, and run gns3-server from source.
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [GNS3 Server API Documentation](https://api.gns3.com/) — Interactive API docs (also available locally via `redoc.html`)
|
||||
- [GNS3 Web UI Documentation](https://docs.gns3.com/)
|
||||
- [LangGraph Documentation](https://langchain-ai.github.io/langgraph/)
|
||||
|
||||
---
|
||||
|
||||
_Last updated: 2026-04-20_
|
||||
@@ -0,0 +1,316 @@
|
||||
<!--
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
See LICENSE file for licensing information.
|
||||
-->
|
||||
|
||||
> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.
|
||||
|
||||
|
||||
# Telnet Server Connection Race Condition Bug
|
||||
|
||||
## Bug Report
|
||||
|
||||
**Date**: 2026-03-14
|
||||
**Severity**: High
|
||||
**Status**: Open
|
||||
**Component**: Telnet Server (`gns3server/utils/asyncio/telnet_server.py`)
|
||||
|
||||
## Error Logs
|
||||
|
||||
```
|
||||
2026-03-14 15:21:31 ERROR asyncio:1879 Unhandled exception in client_connected_cb
|
||||
transport: <_SelectorSocketTransport fd=67 read=polling write=<idle, bufsize=0>>
|
||||
Traceback (most recent call last):
|
||||
File "/home/yueguobin/myCode/GNS3/gns3-server/gns3server/utils/asyncio/telnet_server.py", line 215, in run
|
||||
await self._process(network_reader, network_writer, connection)
|
||||
File "/home/yueguobin/myCode/GNS3/gns3-server/gns3server/utils/asyncio/telnet_server.py", line 305, in _process
|
||||
client_info = connection_key.get_extra_info("socket").getpeername()
|
||||
File "/usr/lib64/python3.13/asyncio/trsock.py", line 77, in getpeername
|
||||
return self._sock.getpeername()
|
||||
~~~~~~~~~~~~~~~~~~~~~~^^
|
||||
OSError: [Errno 107] Transport endpoint is not connected
|
||||
```
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
### Architecture
|
||||
|
||||
The GNS3 Telnet server architecture supports multiple concurrent client connections to a single node console:
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ Node (VPCS/Docker/Router/Switch, etc.) │
|
||||
│ - Independent process │
|
||||
│ - Normal operation, processing business logic │
|
||||
└────────────────────┬─────────────────────────────────────┘
|
||||
│ stdout (device output)
|
||||
│
|
||||
┌────────────▼─────────────────────────────────────┐
|
||||
│ AsyncioTelnetServer (Telnet Proxy Server) │
|
||||
│ - Reads output from Node │
|
||||
│ - Broadcasts to all connected clients │
|
||||
│ - ← ← Bug occurs at this layer ← ← │
|
||||
└────────────┬─────────────────────────────────────┘
|
||||
│
|
||||
┌────────────┼────────────┬──────────────────┐
|
||||
│ │ │ │
|
||||
Web Console Auto Script Client 1 Client 2
|
||||
(long-lived) (quick disco) (normal) (normal)
|
||||
```
|
||||
|
||||
### The Race Condition
|
||||
|
||||
The bug occurs in the broadcast logic when a client disconnects while the server is iterating through connections:
|
||||
|
||||
**Timeline**:
|
||||
```
|
||||
t1: Clients A, B, C connect to the same Telnet port
|
||||
t2: Device has output (e.g., log message)
|
||||
t3: Client A receives data and script immediately disconnects (FIN sent)
|
||||
t4: Server hasn't read EOF yet (event loop hasn't checked this connection)
|
||||
t5: Server iterates through connections to broadcast data
|
||||
t6: When iterating to Client A, getpeername() is called → OSError!
|
||||
```
|
||||
|
||||
### Code Location
|
||||
|
||||
**File**: `gns3server/utils/asyncio/telnet_server.py`
|
||||
**Line**: 305
|
||||
|
||||
**Problematic Code**:
|
||||
```python
|
||||
# Line 304-305
|
||||
for connection_key in list(self._connections.keys()):
|
||||
client_info = connection_key.get_extra_info("socket").getpeername() # ← OSError here
|
||||
connection = self._connections[connection_key]
|
||||
|
||||
try:
|
||||
connection.writer.write(data)
|
||||
await asyncio.wait_for(connection.writer.drain(), timeout=10)
|
||||
except:
|
||||
log.debug(f"Timeout while sending data to client: {client_info}, closing and removing from connection table.")
|
||||
connection.close()
|
||||
del self._connections[connection_key]
|
||||
```
|
||||
|
||||
### The Core Issue
|
||||
|
||||
The `getpeername()` call is **outside** the try-except block, so any OSError from it is not caught.
|
||||
|
||||
Additionally, the top-level exception handler at line 216 only catches `ConnectionError`:
|
||||
|
||||
```python
|
||||
# Line 212-227
|
||||
try:
|
||||
await self._write_intro(network_writer, echo=self._echo, binary=self._binary, naws=self._naws)
|
||||
await connection.connected()
|
||||
await self._process(network_reader, network_writer, connection)
|
||||
except ConnectionError: # ← Only catches ConnectionError
|
||||
async with self._lock:
|
||||
network_writer.close()
|
||||
if self._reader_process == network_reader:
|
||||
self._reader_process = None
|
||||
if self._current_read is not None:
|
||||
self._current_read.cancel()
|
||||
|
||||
await connection.disconnected()
|
||||
del self._connections[network_writer]
|
||||
```
|
||||
|
||||
**Python Exception Hierarchy**:
|
||||
```
|
||||
BaseException
|
||||
└─ Exception
|
||||
├─ ConnectionError ← Only this is caught
|
||||
│ ├─ ConnectionResetError
|
||||
│ ├─ BrokenPipeError
|
||||
│ └─ ...
|
||||
└─ OSError ← Actually thrown! (not a subclass of ConnectionError)
|
||||
└─ [Errno 107] Transport endpoint is not connected
|
||||
```
|
||||
|
||||
Since `OSError` is **not** a subclass of `ConnectionError`, it propagates uncaught to the asyncio event loop.
|
||||
|
||||
## Impact Assessment
|
||||
|
||||
### Immediate Effects
|
||||
|
||||
| Impact | Severity | Description |
|
||||
|--------|----------|-------------|
|
||||
| Connection interrupted | 🔴 High | The connection triggering the exception is terminated |
|
||||
| Resource leak | 🟠 Medium | socket/connection not properly cleaned up |
|
||||
| Other clients affected | 🟡 Low | Other clients on same port may miss broadcast data |
|
||||
| Service stability | 🟡 Low | Long-running may accumulate zombie connections |
|
||||
|
||||
### User-Reported Symptoms
|
||||
|
||||
After this error occurs, users report:
|
||||
|
||||
1. **Cannot open the affected node** - Clicking on the node fails
|
||||
2. **Cannot close the node** - Close button doesn't work
|
||||
3. **"Node not found" errors** - Operations on the node return 404
|
||||
4. **Refresh fixes it temporarily** - Reloading the page restores functionality
|
||||
|
||||
### Why This Happens
|
||||
|
||||
When the uncaught `OSError` occurs, the cleanup code at lines 217-227 **never executes**:
|
||||
|
||||
```python
|
||||
except ConnectionError:
|
||||
async with self._lock:
|
||||
network_writer.close() # ✗ Not executed
|
||||
if self._reader_process == network_reader:
|
||||
self._reader_process = None # ✗ Not executed
|
||||
if self._current_read is not None:
|
||||
self._current_read.cancel() # ✗ Not executed
|
||||
await connection.disconnected() # ✗ Not executed
|
||||
del self._connections[network_writer] # ✗ Not executed
|
||||
```
|
||||
|
||||
This leads to:
|
||||
- **Resource leaks**: socket and writer not closed, file descriptors leaked
|
||||
- **State inconsistency**: `_connections` dictionary retains disconnected connections
|
||||
- **Potential deadlocks**: Locks may not be released if exception occurs while holding them
|
||||
- **Subsequent operation failures**: Future operations may access zombie connections
|
||||
|
||||
### Effect on Node Process
|
||||
|
||||
**The Node process itself is NOT affected**:
|
||||
- Node continues running normally
|
||||
- Node's stdout has already been read by the proxy
|
||||
- The bug occurs during the broadcast phase, after data has been read
|
||||
|
||||
The issue is in the **Telnet Proxy layer**, not the node itself.
|
||||
|
||||
## Trigger Conditions
|
||||
|
||||
This error is more likely to occur with:
|
||||
|
||||
| Scenario | Probability | Reason |
|
||||
|----------|-------------|--------|
|
||||
| **Automated scripts** | 🔴 High | Fast connect → execute → disconnect, small time window |
|
||||
| **Manual operation** | 🟡 Medium | Can occur (e.g., closing terminal, network fluctuation) |
|
||||
| **Normal usage** | 🟢 Low | Human operations slower, server usually detects EOF first |
|
||||
|
||||
### Typical Scenario
|
||||
|
||||
1. User opens Web Console (long-lived connection)
|
||||
2. Automated script connects → executes command → quickly disconnects
|
||||
3. While script disconnects, device has output that needs broadcasting
|
||||
4. During connection iteration, script connection already closed
|
||||
5. `getpeername()` call fails with OSError
|
||||
|
||||
## Related Issues
|
||||
|
||||
A secondary issue was found in the error handler:
|
||||
|
||||
**File**: `gns3server/api/server.py`
|
||||
**Line**: 162
|
||||
|
||||
```python
|
||||
@app.exception_handler(ControllerNotFoundError)
|
||||
async def controller_not_found_error_handler(request: Request, exc: ControllerNotFoundError):
|
||||
log.error(f"Controller not found error in {request.url.path} ({request.method}): {exc}")
|
||||
# ^^^^^^^^^^^^^^^
|
||||
return JSONResponse(...)
|
||||
```
|
||||
|
||||
**Problem**: `request.method` only exists in HTTP requests, not WebSocket connections.
|
||||
|
||||
When a WebSocket request triggers this exception handler:
|
||||
```
|
||||
ControllerNotFoundError: Node ID xxx doesn't exist
|
||||
↓
|
||||
Attempt to log error
|
||||
↓
|
||||
AttributeError: 'WebSocket' object has no attribute 'method'
|
||||
```
|
||||
|
||||
This masks the original error with an attribute error.
|
||||
|
||||
## Proposed Fix
|
||||
|
||||
### Primary Fix (Telnet Server)
|
||||
|
||||
Move `getpeername()` inside the try block and catch OSError:
|
||||
|
||||
```python
|
||||
# Lines 303-314
|
||||
for connection_key in list(self._connections.keys()):
|
||||
connection = self._connections[connection_key]
|
||||
client_info = None
|
||||
|
||||
try:
|
||||
client_info = connection_key.get_extra_info("socket").getpeername()
|
||||
connection.writer.write(data)
|
||||
await asyncio.wait_for(connection.writer.drain(), timeout=10)
|
||||
except (OSError, ConnectionError, asyncio.TimeoutError) as e:
|
||||
log.debug(f"Error sending data to client {client_info}: {e}, closing and removing from connection table.")
|
||||
connection.close()
|
||||
del self._connections[connection_key]
|
||||
```
|
||||
|
||||
### Secondary Fix (Top-level Exception Handler)
|
||||
|
||||
Catch OSError in the main handler to ensure cleanup:
|
||||
|
||||
```python
|
||||
# Lines 212-227
|
||||
try:
|
||||
await self._write_intro(network_writer, echo=self._echo, binary=self._binary, naws=self._naws)
|
||||
await connection.connected()
|
||||
await self._process(network_reader, network_writer, connection)
|
||||
except (ConnectionError, OSError): # ← Add OSError
|
||||
async with self._lock:
|
||||
network_writer.close()
|
||||
if self._reader_process == network_reader:
|
||||
self._reader_process = None
|
||||
if self._current_read is not None:
|
||||
self._current_read.cancel()
|
||||
|
||||
await connection.disconnected()
|
||||
del self._connections[network_writer]
|
||||
```
|
||||
|
||||
### Tertiary Fix (API Error Handler)
|
||||
|
||||
Fix the WebSocket error handler:
|
||||
|
||||
```python
|
||||
@app.exception_handler(ControllerNotFoundError)
|
||||
async def controller_not_found_error_handler(request: Request, exc: ControllerNotFoundError):
|
||||
method = getattr(request, 'method', 'WebSocket')
|
||||
log.error(f"Controller not found error in {request.url.path} ({method}): {exc}")
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
content={"message": str(exc)},
|
||||
)
|
||||
```
|
||||
|
||||
## Reproduction Steps
|
||||
|
||||
**To be documented** after testing.
|
||||
|
||||
Potential reproduction scenario:
|
||||
1. Start a GNS3 node with console enabled (e.g., VPCS)
|
||||
2. Open web console to keep a long-lived connection
|
||||
3. Run automated script that:
|
||||
- Connects to the same console port
|
||||
- Executes a command
|
||||
- Immediately disconnects
|
||||
4. While script is disconnecting, trigger device output
|
||||
5. Observe the error in logs
|
||||
|
||||
## References
|
||||
|
||||
- **Files**:
|
||||
- `gns3server/utils/asyncio/telnet_server.py:305` (primary issue)
|
||||
- `gns3server/utils/asyncio/telnet_server.py:216` (exception handler)
|
||||
- `gns3server/api/server.py:162` (secondary issue)
|
||||
|
||||
- **Related Commits**:
|
||||
- Recent telnet-related work on feature branch
|
||||
|
||||
- **Error Patterns**:
|
||||
- Race condition in connection management
|
||||
- Incomplete exception handling in asyncio code
|
||||
@@ -0,0 +1,146 @@
|
||||
# Ubuntu 24.04 GNS3 Development Environment Setup
|
||||
|
||||
## Simplified Development Environment Setup
|
||||
|
||||
### 1. Add PPA
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install software-properties-common
|
||||
sudo add-apt-repository ppa:gns3/ppa
|
||||
sudo apt update
|
||||
```
|
||||
|
||||
### 2. Install Dependencies
|
||||
|
||||
Binary tools (Dynamips, VPCS, uBridge, etc.) are automatically installed as gns3-server dependencies
|
||||
|
||||
```bash
|
||||
sudo apt install gns3-server tshark
|
||||
```
|
||||
|
||||
### 3. Remove gns3-server While Keeping Dependencies
|
||||
|
||||
If you only want to run the server from source:
|
||||
|
||||
```bash
|
||||
# Remove main package only, keep dependencies
|
||||
sudo apt remove gns3-server
|
||||
|
||||
# Mark dependencies as manual to prevent autoremove deletion
|
||||
sudo apt-mark manual ubridge dynamips vpcs libvirt
|
||||
```
|
||||
|
||||
### 4. Install and Start Docker
|
||||
|
||||
```bash
|
||||
sudo apt install docker.io
|
||||
sudo systemctl enable --now docker
|
||||
```
|
||||
|
||||
#### Configure Docker Mirror Accelerator (China Mainland)
|
||||
|
||||
If Docker Hub is inaccessible, configure mirror accelerators:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /etc/docker
|
||||
sudo tee /etc/docker/daemon.json <<'EOF'
|
||||
{
|
||||
"registry-mirrors": [
|
||||
"https://docker.1ms.run",
|
||||
"https://docker.xuanyuan.me"
|
||||
]
|
||||
}
|
||||
EOF
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart docker
|
||||
```
|
||||
|
||||
Verify:
|
||||
|
||||
```bash
|
||||
docker info | grep -i mirror
|
||||
```
|
||||
|
||||
### 5. Add User to Groups
|
||||
|
||||
```bash
|
||||
# If running as gns3 user (created by PPA install):
|
||||
sudo usermod -aG ubridge,docker,kvm gns3
|
||||
|
||||
# If running as your own user, add yourself to these groups:
|
||||
sudo usermod -aG ubridge,docker,kvm $USER
|
||||
|
||||
# Then logout and login again for group membership to take effect
|
||||
```
|
||||
|
||||
### 6. Install Python venv
|
||||
|
||||
```bash
|
||||
sudo apt install python3.12-venv
|
||||
```
|
||||
|
||||
### 7. Run Development Version from Source
|
||||
|
||||
Clone the repository (official or your fork):
|
||||
|
||||
```bash
|
||||
# Official repository
|
||||
git clone https://github.com/GNS3/gns3-server.git
|
||||
|
||||
# Or your forked repository
|
||||
git clone https://github.com/yourname/gns3-server.git
|
||||
cd gns3-server
|
||||
git checkout your-branch
|
||||
```
|
||||
|
||||
Setup virtual environment and install dependencies:
|
||||
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# For China mainland users, use mirror:
|
||||
# pip install -e . -i https://mirrors.aliyun.com/pypi/simple/
|
||||
|
||||
pip install -e . && gns3server-web-wireshark-setup
|
||||
pip install -e .[ai-copilot]
|
||||
pip install -e .[dev]
|
||||
```
|
||||
|
||||
Run the server:
|
||||
|
||||
```bash
|
||||
python3 -m gns3server
|
||||
```
|
||||
|
||||
## Optional: Install AI Copilot Development Dependencies
|
||||
|
||||
```bash
|
||||
python3 -m pip install .[ai-copilot,dev]
|
||||
```
|
||||
|
||||
## Optional: Expand LVM Root Partition
|
||||
|
||||
If the root partition is low on space but the volume group has unallocated space:
|
||||
|
||||
Check LVM status:
|
||||
|
||||
```bash
|
||||
sudo lvm pvdisplay
|
||||
sudo lvm vgdisplay
|
||||
sudo lvm lvdisplay
|
||||
```
|
||||
|
||||
Extend the root LV using all free space:
|
||||
|
||||
```bash
|
||||
sudo lvm lvextend -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv
|
||||
sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv
|
||||
```
|
||||
|
||||
Verify:
|
||||
|
||||
```bash
|
||||
df -h
|
||||
```
|
||||
@@ -0,0 +1,80 @@
|
||||
# API Error Response Format
|
||||
|
||||
## Overview
|
||||
|
||||
GNS3 Server uses a unified error response format across all API endpoints. All error responses return a JSON object with a `message` field containing the error details.
|
||||
|
||||
## Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Error description here"
|
||||
}
|
||||
```
|
||||
|
||||
## Error Types
|
||||
|
||||
| Error Type | HTTP Status | Description |
|
||||
|------------|-------------|-------------|
|
||||
| `ControllerBadRequestError` | 400 | Invalid request parameters (e.g., validation failure) |
|
||||
| `ControllerUnauthorizedError` | 401 | Missing or invalid authentication |
|
||||
| `ControllerForbiddenError` | 403 | User lacks required privileges |
|
||||
| `ControllerNotFoundError` | 404 | Resource not found |
|
||||
| `ControllerError` | 409 | General conflict (e.g., duplicate name) |
|
||||
| `ControllerTimeoutError` | 408 | Operation timed out |
|
||||
| `ComputeConflictError` | 409 | Compute node returned a conflict |
|
||||
| `RequestValidationError` | 422 | Pydantic/FastAPI validation error (field missing or type mismatch) |
|
||||
| `SQLAlchemyError` | 500 | Database error |
|
||||
|
||||
## Error Response Examples
|
||||
|
||||
### Template Already Exists (409)
|
||||
```json
|
||||
{
|
||||
"message": "A template with name 'my-template' already exists"
|
||||
}
|
||||
```
|
||||
|
||||
### Validation Error (422)
|
||||
```json
|
||||
{
|
||||
"message": "image field is required"
|
||||
}
|
||||
```
|
||||
|
||||
### Permission Denied (403)
|
||||
```json
|
||||
{
|
||||
"message": "Permission denied (privilege Template.Allocate is required)"
|
||||
}
|
||||
```
|
||||
|
||||
### Invalid Template Type (400)
|
||||
```json
|
||||
{
|
||||
"message": "JSON schema error received while creating new template: ..."
|
||||
}
|
||||
```
|
||||
|
||||
## Client-Side Error Handling
|
||||
|
||||
When handling errors in the GNS3 Web UI or API clients:
|
||||
|
||||
```javascript
|
||||
// Extract error message from response
|
||||
function extractErrorMessage(error) {
|
||||
if (error.error && error.error.message) {
|
||||
return error.error.message;
|
||||
}
|
||||
if (error.message) {
|
||||
return error.message;
|
||||
}
|
||||
return 'Unknown error';
|
||||
}
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- Exception handlers: `gns3server/api/server.py`
|
||||
- Template validation: `gns3server/schemas/controller/templates/`
|
||||
- Error classes: `gns3server/controller/controller_error.py`
|
||||
@@ -0,0 +1,146 @@
|
||||
<!--
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
See LICENSE file for licensing information.
|
||||
-->
|
||||
|
||||
> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.
|
||||
|
||||
|
||||
# Controller + Compute Setup
|
||||
|
||||
This document describes the minimum configuration required to set up a GNS3 Controller with remote Compute nodes.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
- **Compute**: Runs individual nodes (QEMU, Docker, etc.) and provides resource monitoring
|
||||
- **Controller**: Manages multiple computes, projects, and provides the REST API
|
||||
- **Database**: Controller uses SQLite to store projects, nodes, and compute registration
|
||||
|
||||
## Minimum Configuration
|
||||
|
||||
### 1. Compute Node Configuration
|
||||
|
||||
Create the configuration file at `~/.config/GNS3/3.1/gns3_server.conf`:
|
||||
|
||||
```ini
|
||||
[Server]
|
||||
host = 0.0.0.0
|
||||
port = 3080
|
||||
compute_username = gns3
|
||||
compute_password = gns3
|
||||
```
|
||||
|
||||
Start the Compute:
|
||||
```bash
|
||||
gns3server
|
||||
```
|
||||
|
||||
### 2. Controller Node Configuration
|
||||
|
||||
Create the configuration file at `~/.config/GNS3/3.1/gns3_server.conf`:
|
||||
|
||||
```ini
|
||||
[Server]
|
||||
host = 192.168.1.140
|
||||
port = 3080
|
||||
compute_username = gns3
|
||||
compute_password = gns3
|
||||
```
|
||||
|
||||
Start the Controller:
|
||||
```bash
|
||||
gns3server
|
||||
```
|
||||
|
||||
### 3. Register Compute with Controller
|
||||
|
||||
Use the API to register a remote compute:
|
||||
|
||||
```bash
|
||||
POST /v3/computes
|
||||
{
|
||||
"protocol": "http",
|
||||
"host": "192.168.1.x",
|
||||
"port": 3080,
|
||||
"user": "gns3",
|
||||
"password": "gns3"
|
||||
}
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
### Host Configuration
|
||||
|
||||
- **Controller `host`**: If set to `0.0.0.0`, the controller will register itself as `127.0.0.1`, which breaks remote compute connections
|
||||
- **Symptom**: Compute nodes report "No common subnet for compute X (controller) and Y" even when on the same network
|
||||
- **Solution**: Always use the actual LAN IP address for the Controller's `host` field (e.g., `host = 192.168.1.104`)
|
||||
- **Dynamic IP**: If the controller machine uses DHCP, set a static lease on the router or use mDNS (`.local` domain)
|
||||
|
||||
### Password Configuration
|
||||
|
||||
- If `compute_password` is not set, a random 16-character password is auto-generated on startup
|
||||
- The Controller must use the same credentials as the Compute's configuration
|
||||
|
||||
### Network Requirements
|
||||
|
||||
- Controller and Compute must be on the same LAN for cross-compute links to work
|
||||
- UDP tunnel is used for cross-compute links, requiring network connectivity on UDP ports
|
||||
|
||||
### Configuration File Location
|
||||
|
||||
- Default location: `~/.config/GNS3/3.1/gns3_server.conf`
|
||||
- Version `3.0` uses `~/.config/GNS3/3.0/`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### 401 Unauthorized on Compute Connect
|
||||
|
||||
1. Check that Compute's `compute_username` and `compute_password` match what was passed to the API
|
||||
2. Verify the Compute's configuration file is correctly loaded
|
||||
3. Ensure the `[Server]` section is used (not `[Controller]`)
|
||||
|
||||
### No Common Subnet Error
|
||||
|
||||
If compute nodes report:
|
||||
```
|
||||
Cannot get an IP address on same subnet: No common subnet for compute X (controller) and Y
|
||||
```
|
||||
|
||||
1. **Primary cause**: Controller's `host` is set to `0.0.0.0` - it registers as `127.0.0.1` which is unreachable from compute nodes
|
||||
2. Check the controller's `/v3/version` endpoint - if `controller_host` shows `127.0.0.1`, this is the issue
|
||||
3. Set the Controller's `host` to its actual LAN IP address (e.g., `192.168.1.104`)
|
||||
4. Verify both machines are on the same network and can ping each other
|
||||
5. Check firewall rules allow TCP/UDP communication on required ports
|
||||
|
||||
### WebSocket Console Authentication Failed
|
||||
|
||||
When connecting to a remote compute node's console via WebSocket, the connection may fail with errors in the logs:
|
||||
|
||||
**Controller log:**
|
||||
```
|
||||
New client 192.168.1.104:33268 has connected to controller console WebSocket
|
||||
Forwarding console WebSocket to 'ws://192.168.1.3:3080/v3/compute/projects/.../console/ws'
|
||||
Client 192.168.1.104:33268 has disconnected from controller console WebSocket
|
||||
```
|
||||
|
||||
**Compute log:**
|
||||
```
|
||||
WebSocket /v3/compute/projects/.../console/ws" [accepted]
|
||||
ERROR gns3server.api.routes.compute.dependencies.authentication:103 Could not authenticate while connecting to compute WebSocket: Could not validate credentials
|
||||
```
|
||||
|
||||
**Cause**: The Controller forwards its own `compute_username` and `compute_password` credentials when connecting to the Compute's WebSocket endpoint. If the Compute node does not have matching credentials configured, authentication fails.
|
||||
|
||||
**Solution**: Ensure the Compute node's configuration file has matching credentials:
|
||||
|
||||
```ini
|
||||
[Server]
|
||||
enable_http_auth = True
|
||||
compute_username = gns3
|
||||
compute_password = your_password
|
||||
```
|
||||
|
||||
The Controller automatically uses its configured `compute_username` and `compute_password` when establishing WebSocket connections to remote computes (see `gns3server/api/routes/controller/nodes.py` lines 627-628).
|
||||
|
||||
**Note**: If `compute_password` is not explicitly set in the Compute's config, a random 16-character password is auto-generated at startup, making authentication impossible for any external Controller.
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
<!--
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
See LICENSE file for licensing information.
|
||||
-->
|
||||
|
||||
> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.
|
||||
|
||||
|
||||
# Statistics API
|
||||
|
||||
## Overview
|
||||
|
||||
Aggregated server statistics API for monitoring dashboards. Collects compute resources, project/node/link counts, and Web Wireshark container status in a single request.
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
Client["Client (Dashboard)"]
|
||||
|
||||
subgraph Controller["GET /v3/statistics"]
|
||||
direction TB
|
||||
F1["1. Compute Resources<br/>GET compute/statistics"]
|
||||
F2["2. Project/Node/Link<br/>Iterate in memory"]
|
||||
F3["3. Web Wireshark<br/>Docker API + docker stats"]
|
||||
end
|
||||
|
||||
Compute["Compute Nodes<br/>(psutil)"]
|
||||
Memory["Controller<br/>(in-memory data)"]
|
||||
Docker["Docker Daemon"]
|
||||
|
||||
Client -->|GET /v3/statistics| Controller
|
||||
F1 -->|HTTP| Compute
|
||||
F2 -->|read| Memory
|
||||
F3 -->|Docker API| Docker
|
||||
```
|
||||
|
||||
## Data Collection Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Controller as Controller API
|
||||
participant Compute as Compute Nodes
|
||||
participant Docker as Docker Daemon
|
||||
|
||||
Client->>Controller: GET /v3/statistics
|
||||
|
||||
rect rgb(240, 248, 255)
|
||||
Note over Controller: 1. Compute Resources
|
||||
loop For each compute
|
||||
Controller->>Compute: GET /statistics
|
||||
Compute-->>Controller: psutil data
|
||||
Note over Controller: skip on error
|
||||
end
|
||||
end
|
||||
|
||||
rect rgb(240, 255, 240)
|
||||
Note over Controller: 2. Projects / Nodes / Links
|
||||
Note over Controller: Count projects by status (opened/closed)
|
||||
Note over Controller: Count nodes: open→by_type+by_status, closed→by_type only
|
||||
Note over Controller: Count links: total + capturing
|
||||
end
|
||||
|
||||
rect rgb(255, 248, 240)
|
||||
Note over Controller: 3. Web Wireshark
|
||||
loop For opened projects only
|
||||
Controller->>Docker: Query container status
|
||||
Controller->>Docker: docker stats (2s timeout)
|
||||
Docker-->>Controller: Container resource usage
|
||||
end
|
||||
end
|
||||
|
||||
Controller-->>Client: JSON response
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Method | Path | Description | Auth |
|
||||
|--------|------|-------------|------|
|
||||
| GET | `/v3/statistics` | Aggregated server statistics | Session |
|
||||
| GET | `/v3/compute/statistics` | Single compute resource stats | Basic Auth |
|
||||
|
||||
## Response
|
||||
|
||||
```json
|
||||
{
|
||||
"computes": [
|
||||
{
|
||||
"compute_id": "local",
|
||||
"compute_name": "Local",
|
||||
"statistics": {
|
||||
"memory_total": 16777216000,
|
||||
"memory_free": 8000000000,
|
||||
"memory_used": 8777216000,
|
||||
"swap_total": 2147479552,
|
||||
"swap_free": 1500000000,
|
||||
"swap_used": 647279552,
|
||||
"cpu_usage_percent": 45,
|
||||
"memory_usage_percent": 52,
|
||||
"swap_usage_percent": 30,
|
||||
"disk_usage_percent": 67,
|
||||
"load_average_percent": [12, 8, 5]
|
||||
}
|
||||
}
|
||||
],
|
||||
"projects": {
|
||||
"total": 5,
|
||||
"opened": 3,
|
||||
"closed": 2
|
||||
},
|
||||
"nodes": {
|
||||
"total": 42,
|
||||
"open_project_nodes": 30,
|
||||
"closed_project_nodes": 12,
|
||||
"by_type": { "qemu": 20, "docker": 12, "dynamips": 6, "vpcs": 4 },
|
||||
"by_status": { "started": 25, "stopped": 12, "suspended": 5 }
|
||||
},
|
||||
"links": {
|
||||
"total": 38,
|
||||
"capturing": 2
|
||||
},
|
||||
"webwireshark": {
|
||||
"total_containers": 1,
|
||||
"running_containers": 1,
|
||||
"active_sessions": 2,
|
||||
"containers": [
|
||||
{
|
||||
"project_id": "e16e2b51-9ba9-403b-9df4-b2915d7508a3",
|
||||
"project_name": "test-project",
|
||||
"container_id": "6edc9029bac0",
|
||||
"status": "running",
|
||||
"running": true,
|
||||
"active_sessions": 2,
|
||||
"memory_limit": "2.0 GB",
|
||||
"cpu_limit": "1.0",
|
||||
"pids_limit": 1000,
|
||||
"memory": "272.5MiB / 4GiB",
|
||||
"cpu": "0.23%",
|
||||
"pids": 69
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Field Reference
|
||||
|
||||
### `computes[]`
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `compute_id` | string | Compute identifier |
|
||||
| `compute_name` | string | Display name |
|
||||
| `statistics` | object | Resource usage (see below) |
|
||||
|
||||
**`statistics` fields:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `memory_total` | int | Total RAM (bytes) |
|
||||
| `memory_free` | int | Available RAM (bytes) |
|
||||
| `memory_used` | int | Used RAM (bytes) |
|
||||
| `swap_total` | int | Total swap (bytes) |
|
||||
| `swap_free` | int | Free swap (bytes) |
|
||||
| `swap_used` | int | Used swap (bytes) |
|
||||
| `cpu_usage_percent` | int | CPU usage 0-100 |
|
||||
| `memory_usage_percent` | int | RAM usage 0-100 |
|
||||
| `swap_usage_percent` | int | Swap usage 0-100 |
|
||||
| `disk_usage_percent` | int | Project dir disk usage 0-100 |
|
||||
| `load_average_percent` | int[] | Load avg per core (1/5/15 min) |
|
||||
|
||||
### `projects`
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `total` | int | All projects |
|
||||
| `opened` | int | Currently opened |
|
||||
| `closed` | int | Currently closed |
|
||||
|
||||
### `nodes`
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `total` | int | All nodes across projects |
|
||||
| `open_project_nodes` | int | Nodes in opened projects (has runtime status) |
|
||||
| `closed_project_nodes` | int | Nodes in closed projects (from topology JSON, no status) |
|
||||
| `by_type` | object | Count by node type (qemu, docker, etc.) |
|
||||
| `by_status` | object | Count by status — **open project nodes only** |
|
||||
|
||||
### `links`
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `total` | int | All links across projects |
|
||||
| `capturing` | int | Links currently capturing |
|
||||
|
||||
### `webwireshark`
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `total_containers` | int | All Wireshark containers |
|
||||
| `running_containers` | int | Currently running |
|
||||
| `active_sessions` | int | Active capture sessions |
|
||||
| `containers` | array | Per-container details (opened projects only) |
|
||||
|
||||
**`containers[]` fields:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `project_id` | string | Project UUID |
|
||||
| `project_name` | string | Project name |
|
||||
| `container_id` | string | Docker container ID (12 chars) |
|
||||
| `status` | string | Container status (running, exited, etc.) |
|
||||
| `running` | bool | Is running |
|
||||
| `active_sessions` | int | Active captures in this project |
|
||||
| `memory_limit` | string | Configured memory limit (e.g. `"2.0 GB"`, `"unlimited"`) |
|
||||
| `cpu_limit` | string | Configured CPU limit (e.g. `"1.0"`, `"unlimited"`) |
|
||||
| `pids_limit` | int/string | Process limit (e.g. `1000`, `"unlimited"`) |
|
||||
| `memory` | string | Live memory usage (conditional, see notes) |
|
||||
| `cpu` | string | Live CPU usage (conditional, see notes) |
|
||||
| `pids` | int | Live process count (conditional, see notes) |
|
||||
|
||||
## Error Responses
|
||||
|
||||
| Status | Description |
|
||||
|--------|-------------|
|
||||
| 401 | Unauthorized — invalid or missing session |
|
||||
| 500 | Internal server error |
|
||||
|
||||
## Notes
|
||||
|
||||
- **Compute stats are best-effort**: if a compute is unreachable, it is skipped (logged as error) and other data is still returned
|
||||
- **`by_status` only reflects open project nodes**: closed projects store topology in JSON without runtime status
|
||||
- **Live container stats** (`memory`, `cpu`, `pids`) are only present when `docker stats` succeeds — may be absent if the Docker daemon is slow (2s timeout)
|
||||
- **Web Wireshark containers** are only queried for opened projects
|
||||
@@ -0,0 +1,402 @@
|
||||
<!--
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
See LICENSE file for licensing information.
|
||||
-->
|
||||
|
||||
> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.
|
||||
|
||||
|
||||
# VNC WebSocket Console
|
||||
|
||||
## Overview
|
||||
|
||||
GNS3 server supports WebSocket-based VNC console access, enabling browser-based graphical console connections to QEMU and Docker VMs without requiring standalone VNC client applications.
|
||||
|
||||
This implementation uses GNS3's API layer as a transparent WebSocket-to-TCP proxy, forwarding binary VNC protocol data between the browser and the VM's VNC server.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Connection Flow
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Browser noVNC] -->|WebSocket binary| B[GNS3 Controller]
|
||||
B -->|WebSocket + BasicAuth + SSL| C[GNS3 Compute]
|
||||
C -->|TCP| D[QEMU/Docker VNC :5900]
|
||||
```
|
||||
|
||||
**Controller** acts as a WebSocket-to-WebSocket relay (JWT auth, IPv6 handling).
|
||||
**Compute** acts as a WebSocket-to-TCP bridge (validates node state, opens VNC TCP connection).
|
||||
|
||||
### Components
|
||||
|
||||
1. **Browser (noVNC)**
|
||||
- HTML5 VNC client running in the browser
|
||||
- Connects via WebSocket using `binary` subprotocol
|
||||
- Handles RFB protocol (Remote Frame Buffer) for VNC
|
||||
|
||||
2. **GNS3 Controller API**
|
||||
- WebSocket endpoint: `/v3/projects/{project_id}/nodes/{node_id}/console/vnc`
|
||||
- Authentication: JWT token via query parameter
|
||||
- Authorization: RBAC privilege check via `has_privilege_on_websocket("Node.Console")`
|
||||
- Proxies WebSocket to compute node (WebSocket-to-WebSocket relay)
|
||||
- Handles IPv6 addresses by wrapping in brackets
|
||||
|
||||
3. **GNS3 Compute API**
|
||||
- WebSocket endpoint: `/v3/compute/projects/{project_id}/{node_type}/nodes/{node_id}/console/vnc`
|
||||
- Authentication: HTTP Basic Auth via `ws_compute_authentication()`
|
||||
- Establishes TCP connection to VNC server, bridges WebSocket ↔ TCP
|
||||
|
||||
4. **Node (QEMU/Docker)**
|
||||
- VNC server listening on configured port (default: 5900+)
|
||||
- RFB protocol for remote display
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Controller WebSocket Endpoint
|
||||
|
||||
**URL**: `ws://{controller_host}:{port}/v3/projects/{project_id}/nodes/{node_id}/console/vnc?token={jwt_token}`
|
||||
|
||||
**Authentication**:
|
||||
- JWT token via query parameter
|
||||
- User must have `Node.Console` privilege
|
||||
|
||||
**WebSocket Subprotocols**:
|
||||
- Accepts: `binary`
|
||||
- Used by noVNC for binary data transfer
|
||||
|
||||
**Request Example**:
|
||||
```javascript
|
||||
const token = "eyJ0eXAiOiJKV1QiLCJhbGc...";
|
||||
const wsUrl = `ws://localhost:3080/v3/projects/${projectId}/nodes/${nodeId}/console/vnc?token=${token}`;
|
||||
|
||||
const ws = new WebSocket(wsUrl, 'binary');
|
||||
ws.binaryType = 'arraybuffer';
|
||||
```
|
||||
|
||||
### Compute WebSocket Endpoint
|
||||
|
||||
**URL**: `ws://{compute_host}:{port}/v3/compute/projects/{project_id}/{node_type}/nodes/{node_id}/console/vnc`
|
||||
|
||||
**Authentication**:
|
||||
- HTTP Basic Auth via `ws_compute_authentication()` dependency
|
||||
- Configured via `settings.Server.compute_username` (default: `gns3`) and `settings.Server.compute_password` (default: empty)
|
||||
|
||||
**Response**:
|
||||
- Bidirectional binary WebSocket connection
|
||||
- Transparent VNC protocol forwarding
|
||||
|
||||
## Supported Node Types
|
||||
|
||||
### QEMU VMs
|
||||
|
||||
**Console Type Configuration**:
|
||||
```json
|
||||
{
|
||||
"console_type": "vnc",
|
||||
"console": 5900,
|
||||
"console_resolution": "1024x768"
|
||||
}
|
||||
```
|
||||
|
||||
**QEMU Parameters**:
|
||||
```bash
|
||||
-vnc :0 # VNC on display 0 (port = 5900 + display)
|
||||
```
|
||||
|
||||
**Implementation**:
|
||||
- File: `gns3server/api/routes/compute/qemu_nodes.py`
|
||||
- Endpoint: `/{node_id}/console/vnc`
|
||||
- Method: `start_vnc_websocket_console(websocket)`
|
||||
|
||||
### Docker Containers
|
||||
|
||||
**Console Type Configuration**:
|
||||
```json
|
||||
{
|
||||
"console_type": "vnc",
|
||||
"console": 5900,
|
||||
"console_resolution": "1024x768",
|
||||
"console_http_port": 8080,
|
||||
"console_http_path": "/"
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation**:
|
||||
- File: `gns3server/api/routes/compute/docker_nodes.py`
|
||||
- Endpoint: `/{node_id}/console/vnc`
|
||||
- Method: `start_vnc_websocket_console(websocket)`
|
||||
|
||||
## WebSocket Data Forwarding
|
||||
|
||||
### Compute Layer (WebSocket ↔ TCP)
|
||||
|
||||
**Location**: `gns3server/compute/base_node.py` — `start_vnc_websocket_console()`
|
||||
|
||||
1. Validates node is started and `console_type == "vnc"`; closes WebSocket with code 1000 otherwise
|
||||
2. Opens TCP connection to VNC server at `console_host:console_port`
|
||||
3. Runs two concurrent tasks via `asyncio.wait(FIRST_COMPLETED)`:
|
||||
- `ws_forward()`: WebSocket → TCP (catches `WebSocketDisconnect`)
|
||||
- `vnc_forward()`: TCP → WebSocket (reads 65536-byte buffer)
|
||||
4. Cancels pending tasks, closes TCP writer on completion
|
||||
|
||||
### Controller Layer (WebSocket ↔ WebSocket)
|
||||
|
||||
**Location**: `gns3server/api/routes/controller/nodes.py` — `vnc_console()`
|
||||
|
||||
1. Authenticates user via `has_privilege_on_websocket("Node.Console")` dependency
|
||||
2. Constructs compute URL with IPv6 bracket handling
|
||||
3. Connects to compute WebSocket using `aiohttp.ws_connect()` with HTTP Basic Auth and SSL context
|
||||
4. Uses `asyncio.ensure_future()` for client→compute forwarding, `async for msg` iteration for compute→client
|
||||
|
||||
### Data Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant B as Browser (noVNC)
|
||||
participant C as Controller
|
||||
participant W as Compute
|
||||
participant V as VNC Server
|
||||
|
||||
B->>C: WebSocket connect (binary, JWT token)
|
||||
C->>W: WebSocket connect (binary, BasicAuth, SSL)
|
||||
W->>V: TCP connect (asyncio.open_connection)
|
||||
|
||||
Note over B,V: Bidirectional binary forwarding active
|
||||
|
||||
B->>C: WebSocket binary frame
|
||||
C->>W: aiohttp send_bytes()
|
||||
W->>V: TCP write(data)
|
||||
|
||||
V->>W: TCP data (read 65536)
|
||||
W->>C: WebSocket binary frame
|
||||
C->>B: send_bytes()
|
||||
```
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
### Controller Layer
|
||||
|
||||
1. **Authentication**:
|
||||
- JWT token validation via `has_privilege_on_websocket("Node.Console")` dependency
|
||||
- Token passed as query parameter: `?token={jwt}`
|
||||
|
||||
2. **Authorization**:
|
||||
- RBAC privilege check: `Node.Console`
|
||||
- Per-node access control
|
||||
|
||||
3. **WebSocket Subprotocol**:
|
||||
- Client requests: `binary`
|
||||
- Server accepts: `binary` (if requested)
|
||||
|
||||
### Compute Layer
|
||||
|
||||
1. **Authentication**:
|
||||
- HTTP Basic Auth
|
||||
- Credentials from controller config
|
||||
- Username: `settings.Server.compute_username`
|
||||
- Password: `settings.Server.compute_password`
|
||||
|
||||
2. **Node Validation**:
|
||||
- Check node exists
|
||||
- Check node is started
|
||||
- Check console type is `vnc`
|
||||
|
||||
## Configuration
|
||||
|
||||
### Server Settings
|
||||
|
||||
**Controller Configuration** (`gns3-server.conf`):
|
||||
```ini
|
||||
[Server]
|
||||
host = 0.0.0.0
|
||||
port = 3080
|
||||
```
|
||||
|
||||
**Compute Configuration** (same file):
|
||||
```ini
|
||||
[Server]
|
||||
compute_username = gns3
|
||||
compute_password = # empty by default, must be set for compute auth
|
||||
```
|
||||
|
||||
### VNC Port Range
|
||||
|
||||
VNC console ports are allocated from a configurable range (`gns3server/schemas/config.py`):
|
||||
|
||||
| Setting | Default | Range |
|
||||
|---------|---------|-------|
|
||||
| `vnc_console_start_port_range` | 5900 | 5900–65535 |
|
||||
| `vnc_console_end_port_range` | 10000 | 5900–65535 |
|
||||
|
||||
Validation: `vnc_console_end_port_range` must be greater than `vnc_console_start_port_range`.
|
||||
|
||||
### Node Settings
|
||||
|
||||
**QEMU VM Example**:
|
||||
```json
|
||||
{
|
||||
"name": "vm-1",
|
||||
"node_type": "qemu",
|
||||
"console_type": "vnc",
|
||||
"console": 5900,
|
||||
"console_resolution": "1280x720",
|
||||
"properties": {
|
||||
"qemu_path": "/usr/bin/qemu-system-x86_64"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Docker Container Example**:
|
||||
```json
|
||||
{
|
||||
"name": "container-1",
|
||||
"node_type": "docker",
|
||||
"console_type": "vnc",
|
||||
"console": 5900,
|
||||
"console_resolution": "1024x768",
|
||||
"console_http_port": 8080
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**1. "Node is not started"**
|
||||
- **Symptom**: WebSocket closes immediately
|
||||
- **Solution**: Start the VM before opening console
|
||||
- **API Check**: `GET /v3/projects/{project_id}/nodes/{node_id}` → verify `status == "started"`
|
||||
|
||||
**2. "Console type is not vnc"**
|
||||
- **Symptom**: WebSocket closes with error
|
||||
- **Solution**: Set node console_type to "vnc"
|
||||
- **API Update**: `PUT /v3/projects/{project_id}/nodes/{node_id}` with `{"console_type": "vnc"}`
|
||||
|
||||
**3. "Cannot connect to VNC server"**
|
||||
- **Symptom**: Connection timeout
|
||||
- **Possible Causes**:
|
||||
- VNC server not listening
|
||||
- Port conflict
|
||||
- Firewall blocking connection
|
||||
- **Verification**:
|
||||
```bash
|
||||
# Check if VNC is listening
|
||||
netstat -tlnp | grep 5900
|
||||
|
||||
# Check QEMU process
|
||||
ps aux | grep qemu
|
||||
```
|
||||
|
||||
**4. WebSocket Subprotocol Negotiation Failed**
|
||||
- **Symptom**: Connection rejected during handshake
|
||||
- **Solution**: Ensure noVNC sends `binary` subprotocol
|
||||
- **Browser Console Check**:
|
||||
```javascript
|
||||
console.log(ws.protocol); // Should be "binary"
|
||||
```
|
||||
|
||||
**5. Authentication Errors**
|
||||
- **Symptom**: HTTP 401/403 responses
|
||||
- **Controller**: Check JWT token is valid and not expired
|
||||
- **Compute**: Verify compute username/password in config
|
||||
|
||||
### Debug Logging
|
||||
|
||||
**Enable Detailed Logging**:
|
||||
```ini
|
||||
[gns3server]
|
||||
debug = true
|
||||
```
|
||||
|
||||
**Check Controller Logs**:
|
||||
```bash
|
||||
# Look for WebSocket connection messages
|
||||
grep "VNC console WebSocket" /var/log/gns3/gns3.log
|
||||
```
|
||||
|
||||
**Check Compute Logs**:
|
||||
```bash
|
||||
# Look for VNC forwarding messages
|
||||
grep "Connected to VNC server" /var/log/gns3/gns3.log
|
||||
```
|
||||
|
||||
**Browser Console**:
|
||||
```javascript
|
||||
// Enable noVNC debugging
|
||||
RFB.messages.log = function(msg) { console.log(msg); };
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
### Authentication
|
||||
|
||||
1. **Controller → Client**
|
||||
- JWT token with expiration
|
||||
- RBAC authorization
|
||||
- Privilege: `Node.Console`
|
||||
|
||||
2. **Controller → Compute**
|
||||
- HTTP Basic Auth via `aiohttp.BasicAuth`
|
||||
- SSL context from `Controller.instance().ssl_context()`
|
||||
- Raises `ControllerForbiddenError` if `compute_username` is not set
|
||||
|
||||
3. **VNC Server**
|
||||
- Optional VNC password (QEMU only)
|
||||
- Configured via node properties
|
||||
|
||||
### Network Security
|
||||
|
||||
**Recommendations**:
|
||||
1. Use HTTPS/WSS for production deployments
|
||||
2. Firewall compute API ports
|
||||
3. Use short-lived JWT tokens
|
||||
4. Enable VNC password for sensitive VMs
|
||||
|
||||
**Example**:
|
||||
```bash
|
||||
# Controller with TLS
|
||||
gns3server --ssl --cert /path/to/cert.pem --key /path/to/key.pem
|
||||
|
||||
# VNC with password
|
||||
qemu-system-x86_64 -vnc :0,password
|
||||
```
|
||||
|
||||
### WebSocket Security
|
||||
|
||||
**Subprotocol Validation**:
|
||||
- Only `binary` subprotocol is accepted
|
||||
- Prevents protocol downgrade attacks
|
||||
|
||||
**Origin Validation**:
|
||||
- Browser enforces same-origin policy
|
||||
- Configure CORS if using separate domains
|
||||
|
||||
## Limitations
|
||||
|
||||
1. **Single Client per VNC Port**
|
||||
- VNC protocol supports one client at a time
|
||||
- New connections disconnect existing clients
|
||||
|
||||
2. **No Audio Redirection**
|
||||
- VNC protocol does not support audio
|
||||
|
||||
3. **No USB Redirection**
|
||||
- VNC protocol does not support device redirection
|
||||
|
||||
4. **Performance Overhead**
|
||||
- WebSocket-to-TCP bridging adds processing overhead compared to native VNC clients
|
||||
|
||||
## References
|
||||
|
||||
- [RFB Protocol 3.8](https://tools.ietf.org/html/rfc6143) - VNC Protocol Specification
|
||||
- [noVNC Documentation](https://github.com/novnc/noVNC) - HTML5 VNC Client
|
||||
- [FastAPI WebSocket](https://fastapi.tiangolo.com/advanced/websockets/) - WebSocket Implementation
|
||||
- [GNS3 Documentation](https://docs.gns3.com/) - General GNS3 Documentation
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| 1.1 | 2026-04-19 | Review against code: fix buffer size, auth details, add port range config, remove unverified content |
|
||||
| 1.0 | 2026-03-17 | Initial VNC WebSocket documentation |
|
||||
@@ -0,0 +1,620 @@
|
||||
<!--
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
See LICENSE file for licensing information.
|
||||
-->
|
||||
|
||||
> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.
|
||||
|
||||
|
||||
# Web Wireshark Feature - Business Process Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The **Web Wireshark** feature enables users to run Wireshark packet capture analysis directly in a web browser without requiring a desktop environment or VNC connection. This is achieved through an **xpra** (persistent remote applications) HTML5 client running inside a Docker container.
|
||||
|
||||
## Feature Summary
|
||||
|
||||
- **Core Capability**: Web-based packet capture visualization using Wireshark
|
||||
- **Technology Stack**: Docker container + xpra + HTML5 WebSocket proxy
|
||||
- **Target Users**: Network engineers and students who need to analyze network traffic in GNS3 topologies
|
||||
- **Key Benefit**: Zero-install, browser-based packet capture analysis
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
Before using Web Wireshark, install the GNS3 server and set up the Docker image:
|
||||
|
||||
```bash
|
||||
# Development install
|
||||
pip install -e . && gns3server-web-wireshark-setup
|
||||
|
||||
# Production install
|
||||
pip install gns3-server && gns3server-web-wireshark-setup
|
||||
```
|
||||
|
||||
This command will:
|
||||
1. Install the gns3-server package
|
||||
2. Pull the `gns3/web-wireshark:latest` image from Docker Hub
|
||||
3. If pull fails, build the image locally using the included Dockerfile
|
||||
|
||||
The `wireshark` command shows the raw output from `docker pull` or `docker build`, allowing you to see the full progress.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph GNS3Server["GNS3 Server"]
|
||||
WebUI["Web UI<br/>(Browser)"]
|
||||
Controller["GNS3 Controller"]
|
||||
LinkCtrl["Link Controller<br/>(start_capture / stop_capture)"]
|
||||
Manager["WebWiresharkManager<br/>(called directly, not via CLI)"]
|
||||
DockerClient["DockerHTTPClient<br/>(HTTP via Unix Socket)"]
|
||||
end
|
||||
|
||||
subgraph DockerDaemon["Docker Daemon"]
|
||||
Container["gns3-wireshark-{project_id}"]
|
||||
Xvfb["Xvfb (virtual framebuffer)"]
|
||||
Xpra["xpra server"]
|
||||
Wireshark["Wireshark"]
|
||||
end
|
||||
|
||||
ClientBrowser["Client Browser<br/>(HTML5 Client)"]
|
||||
|
||||
ClientBrowser -->|REST API| WebUI
|
||||
WebUI -->|capture/start| Controller
|
||||
Controller --> LinkCtrl
|
||||
LinkCtrl -->|start/stop/restart session| Manager
|
||||
Manager -->|Docker API<br/>/var/run/docker.sock| DockerClient
|
||||
DockerClient -->|container lifecycle| Container
|
||||
Container --- Xvfb
|
||||
Container --- Xpra
|
||||
Container --- Wireshark
|
||||
ClientBrowser -.->|WebSocket proxy| Xpra
|
||||
Xpra -.->|xpra HTML5| ClientBrowser
|
||||
|
||||
style GNS3Server fill:#e8f4fd,stroke:#2196f3
|
||||
style DockerDaemon fill:#fff3e0,stroke:#ff9800
|
||||
style ClientBrowser fill:#e8f5e9,stroke:#4caf50
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Component Description
|
||||
|
||||
### 1. Web UI (Client Browser)
|
||||
- User interface for starting/stopping packet capture
|
||||
- Receives WebSocket URL for connecting to xpra HTML5 client
|
||||
- No plugins required - pure HTML5/JavaScript
|
||||
|
||||
### 2. GNS3 Controller
|
||||
- Orchestrates the capture workflow
|
||||
- Validates user permissions (RBAC)
|
||||
- Manages link capture state
|
||||
|
||||
### 3. manage_wireshark.py (Management CLI)
|
||||
- Command-line interface for container and session management
|
||||
- **For manual debugging and testing only** — the server calls `WebWiresharkManager` directly at runtime
|
||||
- Handles Docker container lifecycle
|
||||
- Manages xpra sessions per link
|
||||
|
||||
### 4. WebWiresharkManager
|
||||
- Core business logic for Web Wireshark
|
||||
- Handles container creation, session startup/shutdown
|
||||
- Deterministic port allocation based on link_id
|
||||
|
||||
### 5. DockerHTTPClient
|
||||
- Async HTTP client for Docker API
|
||||
- Communicates via Unix socket (/var/run/docker.sock)
|
||||
- Manages container lifecycle
|
||||
|
||||
### 6. Docker Container (gns3-wireshark-{project_id})
|
||||
- Runs xpra server with HTML5 support
|
||||
- Contains Xvfb (virtual framebuffer) for headless Wireshark
|
||||
- Streams display to browser via WebSocket
|
||||
|
||||
---
|
||||
|
||||
## Business Processes
|
||||
|
||||
### Process 1: Start Packet Capture with Web Wireshark
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor User
|
||||
participant WebUI as Web UI
|
||||
participant Controller as Controller
|
||||
participant LinkCtrl as Link Controller
|
||||
participant Manager as WebWiresharkManager
|
||||
|
||||
User->>WebUI: 1. Start Capture
|
||||
WebUI->>Controller: 2. POST /capture/start
|
||||
Controller->>LinkCtrl: 3. _start_web_wireshark(jwt_token)
|
||||
LinkCtrl->>Manager: 4. start_wireshark_session()
|
||||
Note right of Manager: 5. Ensure network exists
|
||||
Note right of Manager: 6. Get/create container
|
||||
Note right of Manager: 7. Start xpra session<br/>(display + port)
|
||||
Note right of Manager: 8. Start Wireshark<br/>(curl | wireshark -i - -k)
|
||||
Manager-->>LinkCtrl: ws_url
|
||||
LinkCtrl-->>Controller: ws_url
|
||||
Controller-->>WebUI: 10. {capturing: true, ws_url: "ws://..."}
|
||||
WebUI-->>User: 9. ws_url
|
||||
Note over User,Manager: 11-12. Browser connects via WebSocket<br/>Server proxies to container xpra
|
||||
```
|
||||
|
||||
**API Endpoint**: `POST /v3/projects/{project_id}/links/{link_id}/capture/start`
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"wireshark": true,
|
||||
"data_link_type": "DLT_EN10MB",
|
||||
"capture_file_name": "capture.pcap"
|
||||
}
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"id": "link-uuid",
|
||||
"capturing": true,
|
||||
"ws_url": "ws://192.168.1.100:14500"
|
||||
}
|
||||
```
|
||||
|
||||
### Process 2: Container Lifecycle (Per Project)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["Project Open"] --> B["First Link Capture"]
|
||||
B --> C["Container Created<br/>(one per project)"]
|
||||
C --> D["Container Running"]
|
||||
|
||||
subgraph SessionArchitecture["Container Session Architecture"]
|
||||
direction TB
|
||||
D --> E["gns3-wireshark-{project_id}"]
|
||||
|
||||
subgraph Link1Session["Link 1 Session"]
|
||||
L1Xpra["xpra :14503"]
|
||||
L1Xvfb["Xvfb :14503<br/>(1920x1080x24)"]
|
||||
L1WS["Wireshark<br/>(curl | wireshark -i -)"]
|
||||
L1Bind["bind-ws=0.0.0.0:14503"]
|
||||
L1Xpra --- L1Xvfb
|
||||
L1Xpra --- L1WS
|
||||
L1Xpra --- L1Bind
|
||||
end
|
||||
|
||||
subgraph Link2Session["Link 2 Session"]
|
||||
L2Xpra["xpra :11024"]
|
||||
L2Xvfb["Xvfb :11024<br/>(1920x1080x24)"]
|
||||
L2WS["Wireshark<br/>(curl | wireshark -i -)"]
|
||||
L2Bind["bind-ws=0.0.0.0:11024"]
|
||||
L2Xpra --- L2Xvfb
|
||||
L2Xpra --- L2WS
|
||||
L2Xpra --- L2Bind
|
||||
end
|
||||
|
||||
E --> Link1Session
|
||||
E --> Link2Session
|
||||
|
||||
Note1["display = port = 10000 + hash(link_id) % 10000<br/>Each xpra creates its own Xvfb (NOT shared)"]
|
||||
end
|
||||
|
||||
D --> F["Project Close"]
|
||||
F --> G["Stop Container<br/>(all sessions terminate)"]
|
||||
G --> H["Container Stopped<br/>(preserved for reuse)"]
|
||||
H -->|Project Reopened| D
|
||||
|
||||
I["Project Delete"] --> J["Delete Container"]
|
||||
J --> K["Container Removed"]
|
||||
|
||||
style SessionArchitecture fill:#fff8e1,stroke:#ffa000
|
||||
style Link1Session fill:#e3f2fd,stroke:#1976d2
|
||||
style Link2Session fill:#e8f5e9,stroke:#388e3c
|
||||
```
|
||||
|
||||
### Process 3: WebSocket Connection Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Browser as Client Browser
|
||||
participant Server as GNS3 Server (API Route)
|
||||
participant Container as Docker Container (xpra)
|
||||
|
||||
Browser->>Server: 1. WSS Connect (ws://.../web-wireshark?token=jwt)
|
||||
Note right of Server: 2. Validate JWT<br/>(RBAC: Link.Capture)
|
||||
Note right of Server: 3. Get container IP<br/>from Docker API
|
||||
Server-->>Browser: 4. Accept connection
|
||||
Note right of Server: 5. Start WebSocket proxy
|
||||
Server->>Container: 6. Connect to xpra<br/>ws://container:port
|
||||
Container-->>Server: 7. xpra validates subprotocol
|
||||
Note over Browser,Container: 8-9. Bidirectional WebSocket proxy<br/>Browser ⟺ Server ⟺ Container
|
||||
Note left of Browser: 10. HTML5 client renders<br/>Wireshark window
|
||||
```
|
||||
|
||||
**WebSocket Endpoint**: `ws://host/v3/projects/{project_id}/links/{link_id}/capture/web-wireshark?token=<jwt_token>`
|
||||
|
||||
### Process 4: Capture Data Flow
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph GNS3Node["GNS3 Node"]
|
||||
Router["Router / Switch"]
|
||||
end
|
||||
|
||||
subgraph ComputeNode["Compute Node"]
|
||||
Capture["Link Capture Buffer"]
|
||||
end
|
||||
|
||||
Router -->|"TAP (raw packets)"| Capture
|
||||
|
||||
subgraph GNS3Server["GNS3 Server"]
|
||||
APIRoute["GET /capture/stream<br/>Proxies pcap stream"]
|
||||
end
|
||||
|
||||
Capture -->|"pcap stream"| APIRoute
|
||||
|
||||
subgraph DockerContainer["Docker Container"]
|
||||
direction TB
|
||||
Curl["curl -N -H 'Authorization: Bearer {jwt}'<br/>'{server}/capture/stream'"]
|
||||
Wireshark["wireshark -i - -k"]
|
||||
Xvfb["Xvfb :display<br/>(1920x1080x24)"]
|
||||
Xpra["xpra :display<br/>ws://0.0.0.0:{port}"]
|
||||
|
||||
Curl -->|"stdin pipe"| Wireshark
|
||||
Wireshark -->|"renders to"| Xvfb
|
||||
Xvfb -->|"X11 display"| Xpra
|
||||
end
|
||||
|
||||
APIRoute -.->|"pcap stream<br/>(curl fetches from container)"| Curl
|
||||
|
||||
subgraph Browser["Client Browser"]
|
||||
HTML5Client["HTML5/xpra Client"]
|
||||
WSUI["Wireshark Web Interface"]
|
||||
HTML5Client --> WSUI
|
||||
end
|
||||
|
||||
Xpra <-->|"WebSocket"| HTML5Client
|
||||
|
||||
style GNS3Node fill:#f3e5f5,stroke:#7b1fa2
|
||||
style ComputeNode fill:#e8eaf6,stroke:#283593
|
||||
style GNS3Server fill:#e8f4fd,stroke:#2196f3
|
||||
style DockerContainer fill:#fff3e0,stroke:#ff9800
|
||||
style Browser fill:#e8f5e9,stroke:#4caf50
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Port Allocation Strategy
|
||||
|
||||
### Deterministic Port Mapping
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
LinkID["link_id (UUID)"] --> Hash["MD5 Hash"] --> Modulo["hash % 10000"]
|
||||
Modulo --> Offset["+ 10000"]
|
||||
Offset --> Result["display = port<br/>Range: 10000 - 19999"]
|
||||
|
||||
style LinkID fill:#e3f2fd,stroke:#1976d2
|
||||
style Result fill:#e8f5e9,stroke:#388e3c
|
||||
```
|
||||
|
||||
| link_id | port/display |
|
||||
|---------|-------------|
|
||||
| `f233f27f-7432-49c3-9aa2-50e326a10eec` | 14503 |
|
||||
| `a1b2c3d4-1234-5678-90ab-cdef12345678` | 11024 |
|
||||
| `12345678-90ab-cdef-1234-567890abcdef` | 17892 |
|
||||
|
||||
**Benefits**:
|
||||
- Same link always gets same port (deterministic)
|
||||
- Display number = port number (same hash)
|
||||
- No port conflicts between sessions
|
||||
- Easy to predict and debug
|
||||
|
||||
---
|
||||
|
||||
## Network Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph HostMachine["Host Machine"]
|
||||
subgraph GNS3ServerHost["GNS3 Server<br/>192.168.1.100:3080"]
|
||||
WSProxy["WebSocket Proxy"]
|
||||
end
|
||||
|
||||
subgraph DockerNetwork["Docker Bridge: gns3-wireshark<br/>Subnet: 172.31.0.0/22"]
|
||||
Gateway["Bridge Gateway<br/>172.31.0.1"]
|
||||
Container["gns3-wireshark-{project_id}<br/>172.31.0.x"]
|
||||
Gateway --- Container
|
||||
end
|
||||
end
|
||||
|
||||
Client["Client Browser"] -->|"ws://192.168.1.100:3080<br/>/capture/web-wireshark"| WSProxy
|
||||
WSProxy -->|"via gateway 172.31.0.1<br/>ws://172.31.0.x:{port}"| Container
|
||||
|
||||
style HostMachine fill:#fafafa,stroke:#9e9e9e
|
||||
style DockerNetwork fill:#e3f2fd,stroke:#1976d2
|
||||
style GNS3ServerHost fill:#e8f5e9,stroke:#388e3c
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Session Management Commands
|
||||
|
||||
> **Note**: These commands are for manual debugging and testing only. The GNS3 server calls `WebWiresharkManager` directly at runtime.
|
||||
|
||||
### start
|
||||
|
||||
Starts Web Wireshark session for a specific link.
|
||||
|
||||
| Argument | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `--project-id` | Yes | - | Project UUID |
|
||||
| `--link-id` | Yes | - | Link UUID |
|
||||
| `--jwt-token` | Yes | - | JWT authentication token |
|
||||
| `--capture-url` | No | auto-detected | PCAP stream URL |
|
||||
| `--image` | No | `gns3/web-wireshark:latest` | Docker image |
|
||||
| `--memory` | No | `2g` | Memory limit |
|
||||
| `--cpus` | No | `1.0` | CPU cores |
|
||||
| `--pids-limit` | No | `1000` | Process limit |
|
||||
|
||||
```bash
|
||||
python manage_wireshark.py start \
|
||||
--project-id "5af0fe00-..." \
|
||||
--link-id "f233f27f-..." \
|
||||
--jwt-token "eyJhbG..."
|
||||
```
|
||||
|
||||
### Other Commands
|
||||
|
||||
| Command | Description | Key Arguments |
|
||||
|---------|-------------|---------------|
|
||||
| `stop` | Stop session for a specific link | `--project-id`, `--link-id` |
|
||||
| `restart` | Restart session (reopens Wireshark window) | `--project-id`, `--link-id`, `--jwt-token` |
|
||||
| `stop-all` | Stop all sessions for a project | `--project-id` |
|
||||
| `delete` | Delete container (alias for delete-container) | `--project-id` |
|
||||
| `stop-container` | Stop container without deleting | `--project-id` |
|
||||
| `delete-container` | Delete the container | `--project-id` |
|
||||
|
||||
---
|
||||
|
||||
## Project Close/Delete Workflow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["Project Close"] --> B["_stop_web_wireshark_container()"]
|
||||
B --> C["WebWiresharkManager.stop_container(project_id)"]
|
||||
C --> D["docker stop<br/>(all xpra/Xvfb/Wireshark terminate)"]
|
||||
D --> E["Container Stopped<br/>(preserved for reuse)"]
|
||||
|
||||
E -->|Project Reopened| F["First Capture Start"]
|
||||
F --> G["docker start<br/>(container already exists, fast)"]
|
||||
G --> H["New xpra sessions created"]
|
||||
|
||||
I["Project Delete"] --> J["_cleanup_web_wireshark_container()"]
|
||||
J --> K["WebWiresharkManager.delete_container(project_id)"]
|
||||
K --> L["docker rm<br/>(container removed)"]
|
||||
|
||||
style A fill:#e3f2fd,stroke:#1976d2
|
||||
style I fill:#ffebee,stroke:#c62828
|
||||
style E fill:#fff8e1,stroke:#ffa000
|
||||
style L fill:#ffebee,stroke:#c62828
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints Summary
|
||||
|
||||
| Method | Endpoint | Description | Privilege |
|
||||
|--------|----------|-------------|-----------|
|
||||
| POST | `/v3/projects/{id}/links/{id}/capture/start` | Start capture with Web Wireshark | Link.Capture |
|
||||
| POST | `/v3/projects/{id}/links/{id}/capture/stop` | Stop capture | Link.Capture |
|
||||
| POST | `/v3/projects/{id}/links/{id}/capture/wireshark/restart` | Restart Wireshark window | Link.Capture |
|
||||
| GET | `/v3/projects/{id}/links/{id}/capture/stream` | Stream PCAP data | Link.Capture |
|
||||
| GET | `/v3/projects/{id}/links/{id}/capture/file` | Download PCAP file | Link.Capture |
|
||||
| WS | `/v3/projects/{id}/links/{id}/capture/web-wireshark` | WebSocket proxy for xpra | Link.Capture |
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **RBAC Authentication**: All endpoints require `Link.Capture` privilege
|
||||
2. **JWT Token Validation**: WebSocket connections validate JWT token
|
||||
3. **WebSocket Subprotocol Negotiation**: Proper xpra subprotocol handling
|
||||
4. **Container Isolation**: Each project gets its own container with isolated resources
|
||||
5. **Network Segmentation**: Container runs on isolated Docker network (not host network)
|
||||
|
||||
---
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### Startup & Shutdown Performance
|
||||
|
||||
| Step | Before Optimization | After Optimization | Improvement |
|
||||
|------|---------------------|-------------------|-------------|
|
||||
| **Health Check** | ~1.0s | ~0s | Docker native status |
|
||||
| **Gateway Detection** | 0.85s | ~0.001s | Docker API vs exec |
|
||||
| **Process Cleanup** | ~850ms | ~40ms | Host perspective recursive tree walk |
|
||||
| **Xpra Startup** | 6.4s | ~3s | HTML5 client disabled |
|
||||
| **Wireshark Launch** | ~1s | ~1s | No change |
|
||||
| **Total Startup** | **~15s** | **~5-6s** | **67% faster** |
|
||||
|
||||
| Step | Before Optimization | After Optimization | Improvement |
|
||||
|------|---------------------|-------------------|-------------|
|
||||
| **Process Termination** | ~8s | ~20-40ms | Recursive tree walk, no orphans |
|
||||
| **File Cleanup** | ~850ms | ~850ms | Docker exec (safety) |
|
||||
| **Total Shutdown** | **~9s** | **~2s** | **78% faster** |
|
||||
|
||||
#### Startup Breakdown: First vs Subsequent
|
||||
|
||||
| Phase | First Startup (Container stopped) | Subsequent Startup (Container running) |
|
||||
|-------|-----------------------------------|----------------------------------------|
|
||||
| Container startup | ~1-2s | ~0s (already running) |
|
||||
| Container health check | ~1s (unhealthy→healthy) | ~0s (already healthy) |
|
||||
| Gateway detection | ~0.001s | ~0.001s |
|
||||
| Process cleanup | ~40ms | ~40ms |
|
||||
| Xpra startup | ~3s | ~3s |
|
||||
| Wireshark launch | ~1s | ~1s |
|
||||
| **Total** | **~6s** | **~5s** |
|
||||
|
||||
#### Measured Performance Data
|
||||
|
||||
**Startup (from production logs):**
|
||||
```
|
||||
13:46:53 → 13:46:59 = 6s (first startup with container start)
|
||||
13:47:38 → 13:47:43 = 5s (subsequent startup, container running)
|
||||
```
|
||||
|
||||
**Shutdown (from production logs):**
|
||||
```
|
||||
13:48:15 → 13:48:17 = 2s (complete cleanup, no orphan processes)
|
||||
13:48:50 → 13:48:52 = 2s (complete cleanup, no orphan processes)
|
||||
```
|
||||
|
||||
#### Key Optimizations
|
||||
|
||||
- **Complete Process Cleanup**: Recursive process tree traversal eliminates orphaned processes (Xvfb, pulseaudio, ibus-daemon)
|
||||
- **Fast Gateway Detection**: Docker API query instead of container exec
|
||||
- **Smart Health Check**: Trust Docker built-in status, no manual ping
|
||||
- **Xpra Optimization**: Disabled unnecessary HTML5 client (`--html=off`)
|
||||
- **Reduced Docker Exec Calls**: Combined X lock and xpra socket cleanup into single exec call
|
||||
|
||||
### Docker Exec Performance Limitations
|
||||
|
||||
**Important**: Docker daemon has internal queuing for concurrent exec requests to the same container.
|
||||
|
||||
#### Test Results (Same Container)
|
||||
|
||||
| Test Scenario | Execution Time | Avg Per Exec |
|
||||
|--------------|----------------|--------------|
|
||||
| Single docker exec | 0.854s | 0.854s |
|
||||
| Serial 7 docker exec | 7.225s | 1.032s |
|
||||
| Parallel 7 docker exec | 10.253s | 1.465s |
|
||||
|
||||
**Key Finding**: Parallel execution is **42% slower** than serial execution.
|
||||
|
||||
```
|
||||
Serial: 7.225s (7 requests processed sequentially)
|
||||
Parallel: 10.253s (Docker daemon still processes sequentially + context switch overhead)
|
||||
```
|
||||
|
||||
#### Impact on Web Wireshark
|
||||
|
||||
When stopping multiple capture sessions quickly:
|
||||
- Each stop requires 1 docker exec (cleanup files)
|
||||
- Docker daemon processes exec requests sequentially
|
||||
- 7 sessions × ~1s each = ~7-10 seconds total
|
||||
- User requests appear to "queue" even though they're concurrent
|
||||
|
||||
#### Optimization Strategy
|
||||
|
||||
Since Docker exec cannot be parallelized effectively:
|
||||
1. **Minimize exec calls** - Already implemented: 2 calls → 1 call
|
||||
2. **Accept serial processing** - No benefit to parallel execution
|
||||
3. **Focus on fast exec content** - Use simple `rm -f` commands
|
||||
|
||||
#### Future Optimization Options
|
||||
|
||||
- Use Docker API instead of exec (requires container filesystem access)
|
||||
- Delay cleanup to next startup (increases startup complexity)
|
||||
- Batch multiple stops into single operation (requires API changes)
|
||||
|
||||
### Resource Usage (Per Wireshark Instance)
|
||||
| Resource | Typical Usage |
|
||||
|----------|---------------|
|
||||
| Memory | 150-250 MB |
|
||||
| CPU | 0.5-2% (idle to active) |
|
||||
| Threads | ~30 threads |
|
||||
| Disk I/O | Minimal |
|
||||
|
||||
### Container Configuration
|
||||
|
||||
Configured via `WebWiresharkSettings` in `gns3server/schemas/config.py`:
|
||||
|
||||
| Parameter | Default | Recommended | Description |
|
||||
|-----------|---------|-------------|-------------|
|
||||
| enabled | true | - | Enable/disable Web Wireshark feature |
|
||||
| image | gns3/web-wireshark:latest | - | Docker image name |
|
||||
| network_subnet | 172.31.0.0/22 | - | Docker bridge network subnet |
|
||||
| Memory | 2GB | 2-4GB | Container memory limit |
|
||||
| CPUs | 1.0 | 1.0-2.0 | Container CPU limit |
|
||||
| PIDs Limit | 1000 | 1000 | Container process limit |
|
||||
|
||||
### Scaling Guidelines
|
||||
| Instances | Memory | Use Case |
|
||||
|-----------|--------|----------|
|
||||
| 1-3 | 450-750 MB | Light projects |
|
||||
| 4-6 | 600-1.5 GB | Medium projects |
|
||||
| 7-10 | 1-2.5 GB | Large projects |
|
||||
| 10+ | >2.5 GB | Increase memory |
|
||||
|
||||
---
|
||||
|
||||
## Docker API Compatibility
|
||||
|
||||
### Version Negotiation Mechanism
|
||||
|
||||
The Web Wireshark feature implements automatic Docker API version negotiation to ensure compatibility across different Docker versions:
|
||||
|
||||
1. **Primary**: Try API version 1.44 first
|
||||
- Supports Docker 29.3+ (API 1.54+), which requires minimum API 1.44
|
||||
|
||||
2. **Fallback**: If server rejects 1.44 with 400 error, downgrade to API 1.40
|
||||
- Supports Docker 20.10 (API 1.41)
|
||||
|
||||
### Tested Configurations
|
||||
|
||||
| Docker Version | API Version | API 1.40 | API 1.44 | Solution |
|
||||
|----------------|-------------|----------|----------|----------|
|
||||
| 20.10 | 1.41 | ✓ | ✗ | Fallback to 1.40 |
|
||||
| 29.3+ | 1.54+ | ✗ | ✓ | Use 1.44 |
|
||||
|
||||
### Container IP Retrieval
|
||||
|
||||
The `get_container_ip()` method implements a dual-strategy approach for retrieving container IP addresses:
|
||||
|
||||
1. **Primary Method**: Query Docker Container API
|
||||
- Uses `docker inspect` via HTTP API
|
||||
- Safe access to `NetworkSettings.Networks` field using `.get()`
|
||||
- Handles missing fields gracefully (fixes KeyError bug)
|
||||
|
||||
2. **Fallback Method**: Execute `hostname -I` command inside container
|
||||
- Works when Docker API response lacks network information
|
||||
- Compatible with containers without `ip` command
|
||||
- Returns first IP address from `hostname -I` output
|
||||
|
||||
This dual approach ensures compatibility across different Docker API versions that may have varying response formats.
|
||||
|
||||
---
|
||||
|
||||
## Known Limitations
|
||||
|
||||
1. **JWT Token Visibility**: Token passed via command-line arguments (visible in `/proc/<pid>/cmdline`)
|
||||
2. **Single Container**: All Wireshark instances run in a single container per project
|
||||
3. **Docker Dependency**: Requires Docker daemon running on the server
|
||||
4. **Browser Support**: Requires modern browser with WebSocket support
|
||||
5. **Port Range**: Limited to 10,000 unique ports (10000-19999)
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
gns3server/
|
||||
├── controller/
|
||||
│ ├── link.py # Link capture lifecycle
|
||||
│ └── project.py # Project cleanup hooks
|
||||
├── api/routes/controller/
|
||||
│ └── links.py # REST/WebSocket API endpoints
|
||||
└── agent/web_wireshark/
|
||||
├── setup_wireshark_image.py # Docker image setup tool
|
||||
├── manage_wireshark.py # CLI management tool (manual/debug use only)
|
||||
├── manager.py # Session management logic (called by server)
|
||||
├── docker_client.py # Docker API client
|
||||
├── stats.py # Container statistics collection
|
||||
├── docker/
|
||||
│ └── Dockerfile # Container image definition
|
||||
└── WEB_WIRESHARK.md # Technical documentation
|
||||
```
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,224 @@
|
||||
<!--
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
See LICENSE file for licensing information.
|
||||
-->
|
||||
|
||||
> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.
|
||||
|
||||
|
||||
# Command Security Configuration
|
||||
|
||||
## Overview
|
||||
|
||||
GNS3-Copilot includes multiple security layers to prevent execution of commands that may cause issues in the lab environment:
|
||||
|
||||
- **Command Filtering**: Prevents commands that may timeout or lock up the console
|
||||
- **Configuration Safety**: Restricts dangerous configuration changes (AAA, passwords, etc.)
|
||||
- **Multi-line Command Handling**: Properly processes commands with embedded newlines (banner, etc.)
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Tool Request] --> B{Command Filter}
|
||||
B -->|allowed| C[Nornir/Netmiko Execution]
|
||||
B -->|blocked| D[blocked_commands_info]
|
||||
C --> E[Process Results]
|
||||
D --> E
|
||||
E --> F[Return Result]
|
||||
|
||||
subgraph Command Filter
|
||||
B1[Load forbidden_commands.txt]
|
||||
B2[Prefix match per command]
|
||||
B1 --> B2
|
||||
end
|
||||
|
||||
subgraph Security Layers
|
||||
S1[Layer 1: Command Filter<br/>prefix-based blocking]
|
||||
S2[Layer 2: AI Prompt Rules<br/>AAA/password guidance only]
|
||||
S3[Layer 3: Multi-line Expansion<br/>banner/ACL handling]
|
||||
S1 --> S2 --> S3
|
||||
end
|
||||
```
|
||||
|
||||
## Business Process
|
||||
|
||||
### Command Filtering Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start([Tool receives commands]) --> Load[Load forbidden patterns<br/>from config file]
|
||||
Load --> Loop{For each command}
|
||||
Loop -->|next command| Prefix{Command starts with<br/>forbidden pattern?}
|
||||
Prefix -->|yes| Block[Add to blocked_commands_info]
|
||||
Prefix -->|no| Allow[Add to allowed list]
|
||||
Block --> Loop
|
||||
Allow --> Loop
|
||||
Loop -->|all checked| Empty{Allowed list empty?}
|
||||
Empty -->|yes| Return1[Return partial_success<br/>with blocked info only]
|
||||
Empty -->|no| Exec[Execute allowed commands<br/>via Nornir/Netmiko]
|
||||
Exec --> HasBlocked{Has blocked commands?}
|
||||
HasBlocked -->|yes| Return2[Return partial_success<br/>with output + blocked info]
|
||||
HasBlocked -->|no| Return3[Return success<br/>with output]
|
||||
```
|
||||
|
||||
### Multi-line Command Expansion Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[config_tools receives commands] --> B{Command contains \n?}
|
||||
B -->|yes| C[Split by newline]
|
||||
C --> D[Filter empty lines]
|
||||
D --> E[Add expanded lines to command list]
|
||||
B -->|no| F[Keep command as-is]
|
||||
E --> G[Execute via netmiko_send_config]
|
||||
F --> G
|
||||
```
|
||||
|
||||
## Tools Using Command Security
|
||||
|
||||
| Tool | File | Filter | Multi-line Expansion | Result Fields |
|
||||
|------|------|--------|---------------------|---------------|
|
||||
| `ExecuteMultipleDeviceCommands` | `display_tools_nornir.py` | Yes | No | `diagnostic_commands` |
|
||||
| `ExecuteMultipleDeviceConfigCommands` | `config_tools_nornir.py` | Yes | Yes | `config_commands` |
|
||||
|
||||
## Command Filtering System
|
||||
|
||||
### Forbidden Commands Configuration
|
||||
|
||||
The forbidden commands list is loaded from the external [GNS3-Skills](https://github.com/yueguobin/GNS3-Skills) repository at `config/forbidden_commands.txt`.
|
||||
|
||||
**Format:**
|
||||
- One command pattern per line
|
||||
- **Prefix matching** (case-insensitive) — matches the beginning of each command
|
||||
- Empty lines and lines starting with `#` are ignored
|
||||
|
||||
**Default patterns (used when skills repository is unavailable):**
|
||||
|
||||
| Pattern | Reason |
|
||||
|---------|--------|
|
||||
| `traceroute` | Can run 30+ seconds, exceeds tool timeout |
|
||||
| `tracepath` | Similar to traceroute, long execution time |
|
||||
| `tracert` | Windows traceroute, same timeout issues |
|
||||
| `ping -f` | Flood ping can overwhelm lab devices |
|
||||
| `debug` | Can produce overwhelming output and destabilize devices |
|
||||
| `test` | May affect device stability |
|
||||
|
||||
### Result Format
|
||||
|
||||
**Status values:**
|
||||
|
||||
| Status | Condition |
|
||||
|--------|-----------|
|
||||
| `"success"` | All commands executed, none blocked |
|
||||
| `"partial_success"` | Some commands blocked (including all blocked + execution succeeded) |
|
||||
| `"failed"` | Execution failed (device not found, connection error, etc.) |
|
||||
|
||||
**Example — partial success (display tool):**
|
||||
|
||||
```json
|
||||
{
|
||||
"device_name": "R-1",
|
||||
"status": "partial_success",
|
||||
"output": "...",
|
||||
"diagnostic_commands": ["show version", "show ip int brief"],
|
||||
"blocked_commands": ["traceroute 8.8.8.8"],
|
||||
"blocked_info": {
|
||||
"traceroute 8.8.8.8": "Command 'traceroute 8.8.8.8' is not allowed because it matches the forbidden pattern 'traceroute'. ..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **Note:** `diagnostic_commands` is specific to display tools. Config tools use `config_commands` instead.
|
||||
|
||||
## Module Structure
|
||||
|
||||
**Command Filter:** `gns3server/agent/gns3_copilot/utils/command_filter.py`
|
||||
|
||||
| Function | Purpose |
|
||||
|----------|---------|
|
||||
| `filter_forbidden_commands(commands)` | Returns `(allowed_commands, blocked_commands_info)` |
|
||||
| `is_command_forbidden(command)` | Check if a single command matches a forbidden pattern |
|
||||
| `get_forbidden_commands()` | Get current forbidden patterns list |
|
||||
| `reload_forbidden_commands()` | Directly load and cache commands from skills repository |
|
||||
|
||||
**Integration points:**
|
||||
- `display_tools_nornir.py` — `_filter_forbidden_commands_from_device_configs()`
|
||||
- `config_tools_nornir.py` — `_filter_forbidden_commands_from_device_configs()` + `_expand_multiline_commands()`
|
||||
|
||||
## Configuration Safety (AI Prompt Level)
|
||||
|
||||
In addition to the command filter, the AI agent is instructed via system prompt (`lab_automation_assistant_prompt.py`) to handle sensitive configuration commands with caution:
|
||||
|
||||
**Prompt-enforced rules:**
|
||||
- **FORBIDDEN (guidance only):** `enable secret`, `username`, `aaa new-model`, `service password-encryption`, `line vty` — AI provides configuration guidance instead of executing
|
||||
- **Caution required:** `reload`, `erase`, `format` — AI warns user before executing destructive operations
|
||||
|
||||
> These are prompt-level rules, not code-level enforcement. Users can always execute commands directly via device console or SSH/Telnet.
|
||||
|
||||
## Multi-line Command Handling
|
||||
|
||||
Commands containing `\n` are automatically split before execution by `config_tools_nornir.py:_expand_multiline_commands()`.
|
||||
|
||||
**Example:** `["banner motd #\nWelcome\n#"]` becomes `["banner motd #", "Welcome", "#"]`
|
||||
|
||||
Applies to any command with embedded newlines: `banner`, multi-line ACLs, route-maps, etc.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Customizing Forbidden Commands
|
||||
|
||||
Edit `config/forbidden_commands.txt` in the [GNS3-Skills repository](https://github.com/yueguobin/GNS3-Skills) and push the changes, then call `POST /copilot/reload/skills` to apply them without restarting the server.
|
||||
|
||||
## Implementation Verification
|
||||
|
||||
### Test Results (Live GNS3 Environment)
|
||||
|
||||
**Scenario:** IOU-L2-1 (Cisco IOS L3 switch), mixed allowed/forbidden commands.
|
||||
|
||||
```json
|
||||
{
|
||||
"device_name": "IOU-L2-1",
|
||||
"status": "partial_success",
|
||||
"diagnostic_commands": [
|
||||
"show ip route", "show ip interface brief",
|
||||
"ping 10.0.0.1", "ping 10.0.0.2", "ping 10.0.0.4"
|
||||
],
|
||||
"blocked_commands": ["traceroute 10.0.0.2"],
|
||||
"blocked_info": {
|
||||
"traceroute 10.0.0.2": "Command 'traceroute 10.0.0.2' is not allowed because it matches the forbidden pattern 'traceroute'. ..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Feature | Status | Notes |
|
||||
|---------|--------|-------|
|
||||
| Prefix match filtering | Verified | `traceroute` correctly matched and blocked |
|
||||
| Partial execution | Verified | Other commands executed successfully |
|
||||
| `partial_success` status | Verified | Set correctly when blocked + succeeded |
|
||||
| Multi-device support | Verified | Each device filtered independently |
|
||||
| Non-blocking behavior | Verified | No tool timeouts or console issues |
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- **Regex support** for more sophisticated pattern matching
|
||||
- **Per-project override files** for forbidden commands
|
||||
- **Web UI configuration** for managing forbidden commands
|
||||
- **Audit logging** for blocked command analysis
|
||||
- **Per-command timeouts** instead of blocking
|
||||
- **Interrupt mechanism** (Ctrl+C) for long-running commands
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Solution |
|
||||
|---------|----------|
|
||||
| Command blocked unexpectedly | Check `blocked_commands` in result, identify matching pattern, edit `forbidden_commands.txt` |
|
||||
| "File not found, using defaults" | Verify `config/forbidden_commands.txt` exists and is readable |
|
||||
| Changes not taking effect | Call `POST /copilot/reload/skills` or restart server |
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Skills Repository](skills-repository.md)
|
||||
- [Fault Injection](fault-injection.md)
|
||||
- [Chat API](chat-api.md)
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
<!--
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
See LICENSE file for licensing information.
|
||||
-->
|
||||
|
||||
> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.
|
||||
|
||||
|
||||
# LLM Context Window Management
|
||||
|
||||
## Overview
|
||||
|
||||
GNS3 Copilot's context window management prevents LLM context overflow by trimming conversation history to fit within configured token limits. It uses tiktoken for accurate token counting, supports three context strategies (conservative/balanced/aggressive), and automatically injects GNS3 topology information into the system prompt before each LLM call.
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "context_manager.py"
|
||||
A[create_pre_model_hook] --> B[_inject_topology_into_system]
|
||||
A --> C[estimate_tool_tokens]
|
||||
A --> D[trim_messages via LangChain]
|
||||
E[count_tokens via tiktoken]
|
||||
E --> C
|
||||
E --> F[_count_tokens_for_message]
|
||||
F --> D
|
||||
end
|
||||
|
||||
subgraph "gns3_copilot.py"
|
||||
G[llm_call node] --> H[Get topology via GNS3TopologyTool]
|
||||
H --> A
|
||||
A --> I[model_with_tools.invoke]
|
||||
end
|
||||
|
||||
style A fill:#4a9eff,color:#fff
|
||||
style D fill:#ff6b6b,color:#fff
|
||||
style E fill:#51cf66,color:#fff
|
||||
```
|
||||
|
||||
## Key Components
|
||||
|
||||
| Function | File | Purpose |
|
||||
|----------|------|---------|
|
||||
| `create_pre_model_hook()` | `context_manager.py` | Factory that creates the preprocessing hook |
|
||||
| `_inject_topology_into_system()` | `context_manager.py` | Injects topology into system prompt via `{{topology_info}}` placeholder |
|
||||
| `count_tokens()` | `context_manager.py` | Accurate token counting using tiktoken (`cl100k_base`) |
|
||||
| `estimate_tool_tokens()` | `context_manager.py` | Estimates token cost of tool definitions (JSON serialization) |
|
||||
| `_count_tokens_for_message()` | `context_manager.py` | Token counter adapter for LangChain's `trim_messages` |
|
||||
| `llm_call()` | `gns3_copilot.py` | StateGraph node that orchestrates the full LLM call pipeline |
|
||||
|
||||
**Token Counting**: Uses tiktoken `cl100k_base` encoding (GPT-4 compatible, 95%+ accuracy for most modern LLMs).
|
||||
|
||||
**tiktoken Cache**: Encoding files are cached locally at `<agent_package>/cache/tiktoken/` to avoid repeated downloads. This is configured before tiktoken import via `TIKTOKEN_CACHE_DIR` environment variable.
|
||||
|
||||
**Required Dependency**: `tiktoken>=0.8.0` — if not installed, `ModuleNotFoundError` is raised at startup.
|
||||
|
||||
## Trimming Process
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[llm_call node invoked] --> B[Get topology via GNS3TopologyTool]
|
||||
B --> C[Create pre_model_hook]
|
||||
C --> D[Inject topology into system prompt]
|
||||
D --> E{topology data available?}
|
||||
E -->|Yes| F[Replace placeholder with topology content]
|
||||
E -->|No| G[Replace placeholder with 'No topology information available']
|
||||
F --> H[Estimate tool definition tokens]
|
||||
G --> H
|
||||
H --> I[Calculate trim_messages budget]
|
||||
I --> J[trim_messages with strategy='last']
|
||||
J --> K[Return prepared messages]
|
||||
K --> L[model.invoke with prepared messages]
|
||||
```
|
||||
|
||||
### Token Budget Calculation
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["context_limit<br/>(e.g. 128K)"] -->|"× strategy_ratio"| B["Input Budget<br/>(e.g. 96K @ balanced)"]
|
||||
B -->|"− tool_tokens"| C["trim_messages Budget<br/>(includes system message)"]
|
||||
C --> D["trim_messages<br/>max_tokens parameter"]
|
||||
```
|
||||
|
||||
**Key**: `max_tokens` passed to `trim_messages` **includes** the system message. System tokens are NOT subtracted separately — `trim_messages` handles system message preservation via `include_system=True`.
|
||||
|
||||
### Budget Calculation Example
|
||||
|
||||
| Step | Calculation | Result |
|
||||
|------|------------|--------|
|
||||
| Context limit | 128 (configured) | 128K tokens |
|
||||
| Strategy ratio | balanced = 0.75 | 128 × 1000 × 0.75 = 96,000 |
|
||||
| Tool definitions | estimated from JSON schema | ~1,725 tokens |
|
||||
| trim_messages budget | 96,000 − 1,725 | **94,275 tokens** (includes system) |
|
||||
|
||||
### Trimming Priority
|
||||
|
||||
| Priority | Content | Behavior |
|
||||
|----------|---------|----------|
|
||||
| 1 | System Message (system prompt + topology) | Always kept (`include_system=True`) |
|
||||
| 2 | Latest conversation messages | Kept from newest to oldest |
|
||||
| 3 | Older conversation history | Discarded first when exceeding budget |
|
||||
|
||||
**Note**: System prompt and topology are merged into a single `SystemMessage` via the `{{topology_info}}` template variable and cannot be trimmed independently.
|
||||
|
||||
### Edge Case Handling
|
||||
|
||||
| Scenario | Behavior |
|
||||
|----------|----------|
|
||||
| System + tools > input budget | ERROR log with recommendations (reduce prompt/tools, use larger model, switch to conservative). `trim_messages` is still called — `include_system=True` ensures system message is kept, but no history messages will fit. |
|
||||
| Trim budget < system × 1.5 | WARNING log: minimal room for conversation history |
|
||||
| Invalid context_strategy | Falls back to `balanced` with WARNING log |
|
||||
| `trim_messages` throws exception | Returns untrimmed messages (graceful degradation) |
|
||||
| Missing `{{topology_info}}` placeholder | Returns original messages without injection, WARNING log |
|
||||
| Topology retrieval fails | Injects `"(No topology information available)"` placeholder |
|
||||
|
||||
## Integration with GNS3 Copilot
|
||||
|
||||
**File**: `gns3server/agent/gns3_copilot/agent/gns3_copilot.py`
|
||||
|
||||
### Why Direct Call, Not Config-based?
|
||||
|
||||
The agent uses a **custom StateGraph** (`StateGraph(MessagesState)`), not LangGraph's pre-built agent. LangGraph's `pre_model_hook` config parameter only works with pre-built agents like `create_react_agent`.
|
||||
|
||||
| Agent Type | pre_model_hook Support |
|
||||
|------------|------------------------|
|
||||
| `create_react_agent` | Via `config={"configurable": {"pre_model_hook": ...}}` |
|
||||
| `chat_agent_executor` | Via `config={"configurable": {"pre_model_hook": ...}}` |
|
||||
| **Custom StateGraph** | **Not supported** — must call directly |
|
||||
|
||||
### Execution Flow
|
||||
|
||||
In the `llm_call` node of `gns3_copilot.py`:
|
||||
|
||||
1. Get `llm_config` from request-scoped context variable
|
||||
2. Get `project_id` from `config["configurable"]`
|
||||
3. Retrieve topology via `GNS3TopologyTool._run(project_id)`
|
||||
4. Select tools based on `copilot_mode` (`teaching_assistant` vs `lab_automation_assistant`)
|
||||
5. Load system prompt and create `pre_model_hook`
|
||||
6. **Directly call** `pre_hook({"messages": messages, "topology_info": topology_info})` to prepare messages
|
||||
7. Invoke `model_with_tools.invoke(prepared_messages)`
|
||||
|
||||
> **Note**: The actual `llm_call` function also includes defensive checks for missing config, empty messages, and topology retrieval errors — see source code for full details.
|
||||
|
||||
## Strategy Implementation
|
||||
|
||||
### Context Strategy Ratios
|
||||
|
||||
Defined as `CONTEXT_STRATEGY_RATIOS` in `context_manager.py`, with default `DEFAULT_CONTEXT_STRATEGY = "balanced"`.
|
||||
|
||||
| Strategy | Input Ratio | Output Reserved | Calculation |
|
||||
|----------|-------------|-----------------|-------------|
|
||||
| Conservative | 60% | 40% | `context_limit × 1000 × 0.60` |
|
||||
| Balanced | 75% | 25% | `context_limit × 1000 × 0.75` |
|
||||
| Aggressive | 85% | 15% | `context_limit × 1000 × 0.85` |
|
||||
|
||||
## Log Output
|
||||
|
||||
### Normal Case (topology injected)
|
||||
|
||||
```
|
||||
INFO: Calling pre_hook to prepare 1 messages
|
||||
INFO: ✓ Topology injected: 7722 chars, nodes: ['netshoot-1', 'R1', 'R2', 'IOU-L3-1', 'IOU-L3-2']
|
||||
INFO: Context ready: 2 msgs, ~3815 tokens + 1725 tools = 5540 / 128K (4.3%), strategy=conservative
|
||||
INFO: Messages prepared: 1 → 2
|
||||
INFO: LLM call completed: tool_calls=0
|
||||
```
|
||||
|
||||
### When Trimming Occurs
|
||||
|
||||
```
|
||||
INFO: Calling pre_hook to prepare 50 messages
|
||||
INFO: ✓ Topology injected: 8500 chars, nodes: ['R1', 'R2', ...]
|
||||
INFO: Messages trimmed: 50 → 25 msgs. Total: ~82000 tokens + 1725 tools = 83725 / 128K (65.4%), strategy=balanced
|
||||
INFO: Messages prepared: 50 → 25
|
||||
```
|
||||
|
||||
### When Topology is None
|
||||
|
||||
```
|
||||
INFO: Calling pre_hook to prepare 1 messages
|
||||
WARNING: ✗ Topology data is None, injecting placeholder
|
||||
INFO: Context ready: 2 msgs, ~800 tokens + 1725 tools = 2525 / 128K (2.0%), strategy=balanced
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Behavior | Resolution |
|
||||
|-------|----------|------------|
|
||||
| tiktoken not installed | `ModuleNotFoundError` at startup | `pip install tiktoken>=0.8.0` |
|
||||
| `context_limit` missing | `ValueError` raised | Add `context_limit` to LLM config |
|
||||
| Invalid `context_limit` | `ValueError` raised | Must be positive integer |
|
||||
| Invalid `context_strategy` | WARNING, fallback to `balanced` | Use `conservative`/`balanced`/`aggressive` |
|
||||
| `trim_messages` failure | ERROR log, returns untrimmed messages | Check message format compatibility |
|
||||
|
||||
## Deprecated API
|
||||
|
||||
`prepare_context_messages()` is deprecated. It remains in `context_manager.py` for backward compatibility but emits `DeprecationWarning`. Use `create_pre_model_hook()` instead.
|
||||
|
||||
## Related Source Files
|
||||
|
||||
- `gns3server/agent/gns3_copilot/agent/context_manager.py` — Context management core logic
|
||||
- `gns3server/agent/gns3_copilot/agent/gns3_copilot.py` — LLM call node (StateGraph)
|
||||
- `gns3server/agent/gns3_copilot/agent/model_factory.py` — Model creation and tool binding
|
||||
@@ -0,0 +1,159 @@
|
||||
<!--
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
See LICENSE file for licensing information.
|
||||
-->
|
||||
|
||||
> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.
|
||||
|
||||
|
||||
# Fault Injection Feature
|
||||
|
||||
## Overview
|
||||
|
||||
The fault injection feature enables GNS3 Copilot to automatically inject realistic network faults into GNS3 labs for troubleshooting training. The agent analyzes the topology, selects an appropriate fault based on the configured protocols, injects it, and documents the results.
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "Agent Workflow"
|
||||
TG[Topology Gathering]
|
||||
CFG[Config Collection]
|
||||
FA[Fault Analysis]
|
||||
FI[Fault Injection]
|
||||
FD[Fault Documentation]
|
||||
end
|
||||
|
||||
subgraph "Skills Repository"
|
||||
IS[INJECTION_SKILLS_REGISTRY<br/>39 categories, 368 issues]
|
||||
SK[injection_skills Tool]
|
||||
end
|
||||
|
||||
subgraph "Tools"
|
||||
DC[execute_multiple_device_commands]
|
||||
CC[execute_multiple_device_config_commands]
|
||||
GK[GNS3TopologyTool]
|
||||
end
|
||||
|
||||
TG -->|Get topology| GK
|
||||
CFG -->|Get configs| DC
|
||||
FA -->|Query filtered skills| IS
|
||||
FA -->|Select fault| SK
|
||||
FI -->|Inject config changes| CC
|
||||
FD -->|Output report| FD
|
||||
```
|
||||
|
||||
## Injection Skills Tool
|
||||
|
||||
The `InjectionSkillsTool` (LangChain `BaseTool`) is the primary interface for querying available faults.
|
||||
|
||||
### Listing Faults (with context filter)
|
||||
|
||||
The LLM MUST pass topology context when listing faults:
|
||||
|
||||
```json
|
||||
{"action": "list", "context": ["ospf", "bgp", "vlan"]}
|
||||
```
|
||||
|
||||
This returns only faults matching the protocols found in the topology. The tool rejects calls without `context`:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "context parameter is required when action='list'",
|
||||
"hint": "Analyze the topology and device configurations first...",
|
||||
"available_categories": ["bgp", "interface", "mpls", "ospf", ...]
|
||||
}
|
||||
```
|
||||
|
||||
### Getting Fault Details
|
||||
|
||||
Token-efficient usage pattern:
|
||||
|
||||
```json
|
||||
// Step 1: List issue names only (~300 tokens)
|
||||
{"device_type": "injection_ospf", "detail": "index"}
|
||||
|
||||
// Step 2: Get single issue detail (~500 tokens)
|
||||
{"device_type": "injection_ospf", "issue": "ospf_hello_dead_mismatch"}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Required | Description |
|
||||
|-----------|----------|-------------|
|
||||
| `action` | No (`"get"`) | `"list"` to browse, `"get"` for details |
|
||||
| `context` | **Yes** for `action="list"` | Protocols found in topology: `["ospf", "bgp"]` |
|
||||
| `device_type` | Yes for `action="get"` | e.g. `"injection_ospf"` |
|
||||
| `detail` | No (`"full"`) | `"index"` (names), `"summary"` (+desc), `"full"` (all) |
|
||||
| `issue` | No | Single issue key for targeted detail |
|
||||
|
||||
## Fault Injection Workflow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant LLM
|
||||
participant InjectionSkillsTool
|
||||
participant TopologyTool
|
||||
participant DeviceCommands
|
||||
|
||||
LLM->>TopologyTool: Get topology info
|
||||
TopologyTool-->>LLM: Topology + device types
|
||||
|
||||
LLM->>DeviceCommands: Get device configs
|
||||
DeviceCommands-->>LLM: Running configurations
|
||||
|
||||
Note over LLM: Analyze which protocols are in use
|
||||
|
||||
LLM->>InjectionSkillsTool: {"action": "list", "context": ["ospf", "vlan"]}
|
||||
InjectionSkillsTool-->>LLM: Matching fault categories
|
||||
|
||||
LLM->>InjectionSkillsTool: {"device_type": "injection_ospf", "detail": "index"}
|
||||
InjectionSkillsTool-->>LLM: Issue names (token-efficient)
|
||||
|
||||
LLM->>InjectionSkillsTool: {"device_type": "injection_ospf", "issue": "ospf_hello_dead_mismatch"}
|
||||
InjectionSkillsTool-->>LLM: Full fault detail + config commands
|
||||
|
||||
LLM->>DeviceCommands: Inject fault configuration
|
||||
DeviceCommands-->>LLM: Execution result
|
||||
|
||||
Note over LLM: Document fault in response
|
||||
```
|
||||
|
||||
## Injection Skills Repository
|
||||
|
||||
Skills are organized by protocol/category in the external [GNS3-Skills](https://github.com/yueguobin/GNS3-Skills) repository:
|
||||
|
||||
| Category | File | Example Issues |
|
||||
|----------|------|----------------|
|
||||
| OSPF | `injection/ospf_issues.yaml` | Hello/Dead mismatch, MTU mismatch, area mismatch |
|
||||
| BGP | `injection/bgp_issues.yaml` | AS-path prepend, next-hop unreachable, route filtering |
|
||||
| VLAN | `injection/vlan_issues.yaml` | Trunk allowed mismatch, native VLAN mismatch |
|
||||
| STP | `injection/stp_issues.yaml` | Root guard, loop guard, port priority |
|
||||
| MPLS | `injection/mpls_issues.yaml` | LDP session down, label binding failure |
|
||||
| ... | 34 more files | 368 issues total |
|
||||
|
||||
## Recovery
|
||||
|
||||
Each injected fault includes restore commands in the documentation. The LLM always provides commands to fully revert all changes.
|
||||
|
||||
## API Endpoint
|
||||
|
||||
### POST /copilot/projects/{project_id}/chat/inject
|
||||
|
||||
Dedicated endpoint for fault injection. Internally sets `copilot_mode` to `troubleshooting_injection` and runs the agent.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"message": "Inject a network fault for troubleshooting practice",
|
||||
"session_id": "optional-session-uuid"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** Server-Sent Events (SSE) stream identical to the chat stream endpoint.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Skills Repository](skills-repository.md)
|
||||
- [Command Security](command-security.md)
|
||||
- [Chat API](chat-api.md)
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,772 @@
|
||||
<!--
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
See LICENSE file for licensing information.
|
||||
-->
|
||||
|
||||
> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.
|
||||
|
||||
|
||||
# Multi-Vendor Network Device Support
|
||||
|
||||
## Overview
|
||||
|
||||
GNS3-Copilot supports network devices from multiple vendors through Netmiko and Nornir integration. The system includes a custom Netmiko driver for Huawei devices in GNS3 emulation environments and supports dynamic device type detection.
|
||||
|
||||
## Supported Vendors
|
||||
|
||||
| Vendor | Device Type | Protocol | Status |
|
||||
|--------|-------------|----------|--------|
|
||||
| **Cisco** | `cisco_ios_telnet` | Telnet | ✅ Tested |
|
||||
| **Huawei** | `gns3_huawei_telnet_ce` | Telnet | ✅ Tested (Custom Driver) |
|
||||
| **Ruijie (锐捷)** | `gns3_ruijie_telnet` | Telnet | ✅ Tested (Custom Driver) |
|
||||
| **VPCS** | `gns3_vpcs_telnet` | Telnet | ✅ Tested (Custom Driver, Simulator) |
|
||||
|
||||
## Custom VPCS Driver (`VPCSTelnet`)
|
||||
|
||||
### Problem Statement
|
||||
|
||||
VPCS (Virtual PC Simulator) is a lightweight virtual PC simulator used in GNS3 lab environments. Unlike network devices (routers/switches), VPCS devices:
|
||||
|
||||
1. **No authentication** - Direct console access without username/password
|
||||
2. **Simple command interface** - No configuration modes
|
||||
3. **Simple prompt pattern** - `PC1>`, `PC2>`, etc.
|
||||
|
||||
### Solution: Lightweight Custom Driver
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[BaseConnection<br/>Netmiko base class] --> B[VPCSTelnet<br/>Custom GNS3 driver]
|
||||
B --> C[No authentication]
|
||||
B --> D[Simple prompt: PC\d+]
|
||||
B --> E[No config mode]
|
||||
B --> F[ANSI stripping]
|
||||
```
|
||||
|
||||
**Why Not Use Standard Telnet Driver?**
|
||||
- Standard drivers attempt authentication (times out)
|
||||
- No support for VPCS-specific prompt patterns (`PC\d+>`)
|
||||
- No need for configuration mode handling
|
||||
|
||||
### VPCSTelnet Implementation
|
||||
|
||||
#### Location
|
||||
```
|
||||
gns3server/agent/gns3_copilot/utils/custom_netmiko/vpcs_telnet.py
|
||||
```
|
||||
|
||||
#### Key Features
|
||||
|
||||
**1. No Authentication**
|
||||
```python
|
||||
def telnet_login(self, pri_prompt_terminator=r"PC\d+>", ...):
|
||||
# Send returns until VPCS prompt detected
|
||||
for i in range(max_loops):
|
||||
self.write_channel(self.RETURN)
|
||||
output = self.read_channel()
|
||||
|
||||
if re.search(pri_prompt_terminator, output):
|
||||
return output # Success - VPCS prompt detected
|
||||
```
|
||||
|
||||
**2. Simple Prompt Recognition**
|
||||
```
|
||||
PC1> ip 10.10.0.12/24 10.10.0.254
|
||||
PC1> ping 10.10.0.254
|
||||
```
|
||||
|
||||
**3. No Configuration Mode**
|
||||
```python
|
||||
def check_config_mode(self) -> bool:
|
||||
return False # VPCS has no config mode
|
||||
|
||||
def config_mode(self) -> str:
|
||||
return "" # No config mode to enter
|
||||
|
||||
def exit_config_mode(self) -> str:
|
||||
return "" # No config mode to exit
|
||||
```
|
||||
|
||||
**4. No Paging**
|
||||
```python
|
||||
def disable_paging(self) -> str:
|
||||
return "" # VPCS doesn't use paging
|
||||
```
|
||||
|
||||
**5. Custom `send_command`**
|
||||
VPCS overrides `send_command` with non-standard defaults:
|
||||
- `strip_prompt=False`, `strip_command=False`, `normalize=False` — VPCS output doesn't need standard Netmiko processing
|
||||
- Uses `_strip_ansi_codes()` to clean terminal escape sequences from VPCS output
|
||||
|
||||
**6. ANSI Escape Code Stripping**
|
||||
VPCS output often contains ANSI terminal codes. The `_strip_ansi_codes()` method strips bold, underline, reset, and color codes before returning output to callers.
|
||||
|
||||
### VPCS Tool Usage
|
||||
|
||||
The VPCS driver is used by the `execute_vpcs_commands` tool:
|
||||
|
||||
> File: `gns3server/agent/gns3_copilot/tools_v2/vpcs_tools_netmiko.py`
|
||||
|
||||
**VPCS Connection Parameters:**
|
||||
|
||||
VPCS devices use additional Netmiko parameters for reliable connections:
|
||||
| Parameter | Value | Reason |
|
||||
|-----------|-------|--------|
|
||||
| `fast_cli` | `False` | VPCS is slow, disable fast CLI mode |
|
||||
| `global_delay_factor` | `2.0` | Double the delay between commands |
|
||||
|
||||
```python
|
||||
"connection_options": {
|
||||
"netmiko": {
|
||||
"extras": {
|
||||
"device_type": "gns3_vpcs_telnet",
|
||||
"fast_cli": False,
|
||||
"global_delay_factor": 2.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### VPCS Built-in Template Configuration
|
||||
|
||||
**✨ Automatic Tags - No Manual Configuration Required**
|
||||
|
||||
VPCS nodes created from the built-in template automatically include the necessary tags:
|
||||
|
||||
| Tag | Value | Purpose |
|
||||
|-----|-------|---------|
|
||||
| `device_type` | `gns3_vpcs_telnet` | Netmiko driver selection |
|
||||
|
||||
**Built-in Template Definition:**
|
||||
> File: `gns3server/services/templates.py`
|
||||
|
||||
The VPCS built-in template includes `"tags": ["device_type:gns3_vpcs_telnet"]`, which is automatically applied to all VPCS nodes created from the template.
|
||||
|
||||
**User Benefits:**
|
||||
- ✅ **No manual tagging required** — Tags are applied automatically when creating VPCS nodes
|
||||
- ✅ **Automatic driver selection** — Copilot tools automatically use the correct Netmiko driver
|
||||
- ✅ **Consistent behavior** — All VPCS nodes from the built-in template work identically
|
||||
- ✅ **Zero configuration** — Users don't need to understand device_type tags
|
||||
|
||||
**How It Works:**
|
||||
1. User creates a VPCS node from the built-in "VPCS" template
|
||||
2. Node automatically inherits the tag: `device_type:gns3_vpcs_telnet`
|
||||
3. Copilot tools read the tag and select the appropriate VPCS Netmiko driver
|
||||
4. Commands execute using the VPCS-optimized driver (no authentication, simple prompts)
|
||||
|
||||
### Supported VPCS Commands
|
||||
|
||||
| Command | Description | Example |
|
||||
|---------|-------------|---------|
|
||||
| `ip` | Configure/show IP address | `ip 10.10.0.12/24 10.10.0.254` |
|
||||
| `ping` | Test connectivity | `ping 10.10.0.254` |
|
||||
| `arp` | Display ARP table | `arp` |
|
||||
| `show ip` | Show IP configuration | `show ip` |
|
||||
| `version` | Show VPCS version | `version` |
|
||||
| `save` | Save configuration | `save` |
|
||||
| `load` | Load configuration | `load` |
|
||||
|
||||
### Architecture Benefits
|
||||
|
||||
**Unified Tool Architecture:**
|
||||
- ✅ Uses Nornir for connection management (same as network device tools)
|
||||
- ✅ Uses Netmiko for command execution (consistent with other tools)
|
||||
- ✅ Follows same patterns as `config_tools_nornir.py` and `display_tools_nornir.py`
|
||||
- ✅ Simplified codebase - no need for separate telnetlib3 implementation
|
||||
|
||||
**Migration from telnetlib3:**
|
||||
| Aspect | Old (telnetlib3) | New (Netmiko + Nornir) |
|
||||
|--------|-----------------|-------------------------|
|
||||
| Library | telnetlib3 | Netmiko |
|
||||
| Framework | Manual threading | Nornir |
|
||||
| Code Lines | ~490 lines | ~580 lines (with better structure) |
|
||||
| Consistency | Unique implementation | Same as other tools |
|
||||
| Maintenance | Separate code path | Unified architecture |
|
||||
|
||||
---
|
||||
|
||||
## Custom Huawei Driver (`GNS3HuaweiTelnetCE`)
|
||||
|
||||
### Problem Statement
|
||||
|
||||
GNS3-emulated Huawei devices connect via console **without requiring authentication**. Standard Netmiko drivers attempt username/password authentication, causing connection timeouts.
|
||||
|
||||
**Standard Driver Behavior:**
|
||||
```
|
||||
Telnet Connection → Wait for username prompt → Send username → Wait for password → Send password → Access
|
||||
^ Times out after 20 seconds
|
||||
```
|
||||
|
||||
**GNS3 Huawei Device:**
|
||||
```
|
||||
Telnet Connection → Direct access to command line (no login prompts)
|
||||
<HUAWEI>
|
||||
```
|
||||
|
||||
### Solution: Custom Driver Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[BaseConnection] --> B[CiscoBaseConnection]
|
||||
B --> C[HuaweiBase<br/>VRP support, system-view,<br/>prompt patterns, paging]
|
||||
C --> D[GNS3HuaweiTelnetCE<br/>Overrides telnet_login only]
|
||||
D --> E[Skip authentication]
|
||||
D --> F[Auto-commit before exit]
|
||||
D --> G[y/n auto-confirm]
|
||||
```
|
||||
|
||||
**Why Inherit from HuaweiBase?**
|
||||
- ✅ Built-in VRP (Versatile Routing Platform) command handling
|
||||
- ✅ Huawei-specific configuration mode (`system-view`)
|
||||
- ✅ Huawei prompt patterns (`<...>`, `[...]`)
|
||||
- ✅ Huawei paging disable (`screen-length 0 temporary`)
|
||||
- ✅ Minimal code changes - only override authentication
|
||||
|
||||
### GNS3HuaweiTelnetCE Implementation
|
||||
|
||||
#### Location
|
||||
```
|
||||
gns3server/agent/gns3_copilot/utils/custom_netmiko/huawei_ce.py
|
||||
```
|
||||
|
||||
**Package Structure:**
|
||||
```
|
||||
custom_netmiko/
|
||||
├── __init__.py # Package initialization, auto-registers all drivers
|
||||
├── huawei_ce.py # Huawei CloudEngine custom driver
|
||||
├── README.md # Driver development guide
|
||||
└── tests/ # Unit tests
|
||||
├── __init__.py
|
||||
└── test_huawei_ce.py # Huawei CE driver tests
|
||||
```
|
||||
|
||||
#### Key Features
|
||||
|
||||
1. **Skip Authentication**
|
||||
- Directly detect Huawei prompt patterns
|
||||
- No username/password prompts
|
||||
- Connection ready in < 1 second
|
||||
|
||||
2. **VRP Prompt Recognition**
|
||||
```
|
||||
User view: <HUAWEI>
|
||||
System view: [HUAWEI]
|
||||
Interface: [HUAWEI-GigabitEthernet0/0/1]
|
||||
```
|
||||
|
||||
3. **Automatic Confirmation Handling**
|
||||
- Detects and responds to `[y/n]` prompts
|
||||
- Example: `return` command asks "Return to user view? [y/n]:"
|
||||
- Automatically sends `y` to confirm
|
||||
|
||||
4. **Proper Output Collection**
|
||||
- Uses Netmiko's `read_channel_timing()` for reliable output
|
||||
- Waits for command completion (2s no new data = done)
|
||||
- 30-second absolute timeout prevents hanging
|
||||
|
||||
5. **Auto-Commit Before Exit**
|
||||
- Automatically sends `commit` command before exiting config mode
|
||||
- Prevents "Uncommitted configurations [Y/N/C]" prompt
|
||||
- Ensures configuration changes are saved
|
||||
|
||||
#### Limitations
|
||||
|
||||
**Why Not Use the Standard `huawei_telnet` Driver?**
|
||||
- The standard Netmiko Huawei driver (`huawei_telnet`) has known issues in GNS3 emulation environments
|
||||
- This is the primary reason the custom `gns3_huawei_telnet_ce` driver was developed
|
||||
- The custom driver is designed for GNS3 devices **without authentication**
|
||||
- If your GNS3 Huawei device has been configured with authentication, remove it for GNS3 testing to use the custom driver
|
||||
|
||||
**When to Use:**
|
||||
|
||||
| Scenario | Recommendation |
|
||||
|----------|---------------|
|
||||
| GNS3 Huawei device | Always use `gns3_huawei_telnet_ce` |
|
||||
| GNS3 device has auth configured | Remove auth, then use `gns3_huawei_telnet_ce` |
|
||||
|
||||
#### Method Overrides
|
||||
|
||||
**1. `telnet_login` - Skip Authentication**
|
||||
```python
|
||||
def telnet_login(self, pri_prompt_terminator=r"<\S+>|>\s*$",
|
||||
alt_prompt_terminator=r"\[\S+\]", ...) -> str:
|
||||
# Clear buffer
|
||||
self.read_channel()
|
||||
|
||||
# Send returns until prompt detected
|
||||
for i in range(max_loops):
|
||||
self.write_channel(self.RETURN)
|
||||
output = self.read_channel()
|
||||
|
||||
# Check for Huawei prompts
|
||||
if re.search(pri_prompt_terminator, output):
|
||||
return output # Success!
|
||||
|
||||
return output # Best effort
|
||||
```
|
||||
|
||||
**2. `send_config_set` - Configuration Commands**
|
||||
```python
|
||||
def send_config_set(self, config_commands, **kwargs) -> str:
|
||||
# Enter config mode
|
||||
output += self.config_mode(config_command="system-view")
|
||||
|
||||
# Send all commands
|
||||
for cmd in config_commands:
|
||||
self.write_channel(f"{cmd}{self.RETURN}")
|
||||
time.sleep(delay_factor * 0.05)
|
||||
|
||||
# Collect output using Netmiko standard method
|
||||
output += self.read_channel_timing(read_timeout=30, last_read=2.0)
|
||||
|
||||
# Auto-commit before exit (prevents [Y/N/C] prompt)
|
||||
self.write_channel(f"commit{self.RETURN}")
|
||||
time.sleep(0.5 * self.global_delay_factor)
|
||||
output += self.read_channel()
|
||||
|
||||
# Exit config mode
|
||||
output += self.exit_config_mode()
|
||||
|
||||
return output
|
||||
```
|
||||
|
||||
**3. `exit_config_mode` - Handle Confirmation**
|
||||
```python
|
||||
def exit_config_mode(self, exit_config="return", pattern=r"<\S+>|>\s*$") -> str:
|
||||
self.write_channel(f"return{self.RETURN}")
|
||||
|
||||
# Look for confirmation prompt
|
||||
for _ in range(20):
|
||||
new_output = self.read_channel()
|
||||
|
||||
if re.search(r"\[y/n\]", new_output):
|
||||
self.write_channel(f"y{self.RETURN}") # Auto-confirm
|
||||
|
||||
if re.search(pattern, new_output):
|
||||
return output # Back to user view
|
||||
|
||||
return output
|
||||
```
|
||||
|
||||
### Device Type Registration
|
||||
|
||||
The custom driver must be registered with Netmiko's global mappings:
|
||||
|
||||
```python
|
||||
def register_custom_device_type() -> None:
|
||||
import importlib
|
||||
sd = importlib.import_module("netmiko.ssh_dispatcher")
|
||||
|
||||
# Register in CLASS_MAPPER (for ConnectHandler)
|
||||
sd.CLASS_MAPPER["gns3_huawei_telnet_ce"] = GNS3HuaweiTelnetCE
|
||||
sd.CLASS_MAPPER["huawei_ce"] = GNS3HuaweiTelnetCE
|
||||
|
||||
# Register in CLASS_MAPPER_BASE (for base class definitions)
|
||||
sd.CLASS_MAPPER_BASE["gns3_huawei_telnet_ce"] = GNS3HuaweiTelnetCE
|
||||
sd.CLASS_MAPPER_BASE["huawei_ce"] = GNS3HuaweiTelnetCE
|
||||
|
||||
# CRITICAL: Rebuild static lists
|
||||
sd.platforms = list(sd.CLASS_MAPPER.keys())
|
||||
sd.platforms.sort()
|
||||
sd.telnet_platforms = [x for x in sd.platforms if "telnet" in x]
|
||||
```
|
||||
|
||||
**Important: Static List Problem**
|
||||
- `ssh_dispatcher.platforms` is computed at module import time
|
||||
- Modifying `CLASS_MAPPER` doesn't automatically update `platforms`
|
||||
- Must manually rebuild the list after registration
|
||||
|
||||
**Auto-Registration**
|
||||
```python
|
||||
# Automatically runs on module import
|
||||
try:
|
||||
register_custom_device_type()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to register custom device type: {e}")
|
||||
```
|
||||
|
||||
## Custom Ruijie Driver (`RuijieTelnetEnhanced`)
|
||||
|
||||
### Problem Statement
|
||||
|
||||
Ruijie (锐捷) network devices exhibit Cisco-like command syntax but have interactive prompts during configuration that can cause standard Netmiko drivers to fail.
|
||||
|
||||
**Common Issue - OSPF Router-ID:**
|
||||
```
|
||||
Router(config-router)#router-id 10.0.0.1
|
||||
Change router-id and update OSPF process! [yes/no]:
|
||||
```
|
||||
|
||||
Standard Netmiko's `send_config_set()` waits for a prompt pattern, but the `[yes/no]:` prompt doesn't match the expected config mode pattern, causing:
|
||||
1. **ReadTimeout**: Netmiko times out waiting for the prompt
|
||||
2. **Command Failure**: Subsequent commands are not executed
|
||||
3. **Device Lockup**: Console remains in the waiting state
|
||||
|
||||
### Solution: Hybrid Strategy with Interactive Prompt Handling
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[BaseConnection] --> B[CiscoBaseConnection]
|
||||
B --> C[RuijieOSBase<br/>Netmiko built-in]
|
||||
C --> D[RuijieTelnetEnhanced<br/>Interactive prompt handling]
|
||||
D --> E[Preprocessing<br/>Insert yes after known commands]
|
||||
D --> F[Batch send<br/>Fast path 2-3s]
|
||||
D --> G[Fallback one-by-one<br/>Slow but reliable ~10s]
|
||||
```
|
||||
|
||||
**Why Hybrid Strategy?**
|
||||
1. **Preprocessing**: Automatically insert `yes` after known interactive commands
|
||||
2. **Fast Path**: Batch send for most commands (2-3 seconds for 13 commands)
|
||||
3. **Fallback**: One-by-one send with real-time detection (reliable but slower)
|
||||
|
||||
### RuijieTelnetEnhanced Implementation
|
||||
|
||||
#### Location
|
||||
```
|
||||
gns3server/agent/gns3_copilot/utils/custom_netmiko/ruijie_telnet.py
|
||||
```
|
||||
|
||||
#### Key Features
|
||||
|
||||
**1. Preprocessing - Known Interactive Commands**
|
||||
```python
|
||||
INTERACTIVE_PATTERNS = [
|
||||
re.compile(r'^router-id\s+', re.IGNORECASE), # OSPF/EIGRP/BGP router-id
|
||||
re.compile(r'^erase\s+', re.IGNORECASE), # erase startup-config
|
||||
re.compile(r'^delete\s+', re.IGNORECASE), # delete files
|
||||
re.compile(r'^format\s+', re.IGNORECASE), # format filesystem
|
||||
re.compile(r'^reload\b', re.IGNORECASE), # reload/reboot
|
||||
re.compile(r'^boot\s+system\s+', re.IGNORECASE), # change boot image
|
||||
]
|
||||
```
|
||||
|
||||
**2. Hybrid Send Strategy**
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Input Configuration Commands] --> B[Preprocessing]
|
||||
B --> B1[Detect interactive commands]
|
||||
B1 --> B2[Insert 'yes' after them]
|
||||
B2 --> C[Try Batch Send - Fast]
|
||||
C --> C1[Write all commands rapidly]
|
||||
C1 --> C2[Read output once, last_read=2.0s]
|
||||
C2 --> D{Success?}
|
||||
D -->|Yes| E[Return output]
|
||||
D -->|No| F[Fallback: One-by-One]
|
||||
F --> F1[Send each command individually]
|
||||
F1 --> F2[Detect prompts after each]
|
||||
F2 --> F3[Send 'yes' when needed, last_read=0.5s]
|
||||
F3 --> E
|
||||
```
|
||||
|
||||
**3. Batch Send Performance**
|
||||
- **13 commands** with 1 interactive command
|
||||
- **Fast path**: ~2-3 seconds (includes device processing time)
|
||||
- **Fallback**: ~10 seconds (if batch fails)
|
||||
|
||||
**4. Real-time Detection (Fallback)**
|
||||
```python
|
||||
# After each command
|
||||
new_output = self.read_channel_timing(read_timeout=10, last_read=0.5)
|
||||
|
||||
# Check for [yes/no] prompt
|
||||
if re.search(r"\[yes/no\]", new_output, re.IGNORECASE):
|
||||
self.write_channel(f"yes{self.RETURN}")
|
||||
output += self.read_channel_timing(read_timeout=30, last_read=0.5)
|
||||
```
|
||||
|
||||
#### GNS3 Node Tag Configuration
|
||||
|
||||
**For Ruijie devices in GNS3:**
|
||||
```
|
||||
device_type:gns3_ruijie_telnet
|
||||
```
|
||||
|
||||
**Example Usage:**
|
||||
```python
|
||||
from netmiko import ConnectHandler
|
||||
from gns3server.agent.gns3_copilot.utils import custom_netmiko
|
||||
|
||||
device = {
|
||||
"device_type": "gns3_ruijie_telnet",
|
||||
"host": "127.0.0.1",
|
||||
"port": 5000,
|
||||
}
|
||||
|
||||
with ConnectHandler(**device) as conn:
|
||||
# These commands include router-id (interactive)
|
||||
config = [
|
||||
"router ospf 1",
|
||||
"router-id 10.0.0.1", # Triggers [yes/no] prompt
|
||||
"network 192.168.1.0 0.0.0.255 area 0",
|
||||
]
|
||||
# Automatically handles the [yes/no] prompt
|
||||
output = conn.send_config_set(config)
|
||||
```
|
||||
|
||||
#### Limitations
|
||||
|
||||
**Interactive Command Coverage:**
|
||||
- **Covered**: `router-id`, `erase`, `delete`, `format`, `reload`, `boot system`
|
||||
- **Not Covered**: Unknown or vendor-specific interactive prompts
|
||||
- **Fallback**: If batch fails, falls back to one-by-one with real-time detection
|
||||
|
||||
**When to Use:**
|
||||
|
||||
| Scenario | Use Driver |
|
||||
|----------|------------|
|
||||
| GNS3 Ruijie (known interactive commands) | `gns3_ruijie_telnet` (batch + auto-fallback) |
|
||||
| GNS3 Ruijie (unknown interactive prompts) | `gns3_ruijie_telnet` (auto-fallback to one-by-one) |
|
||||
|
||||
## Dynamic Device Type Detection
|
||||
|
||||
### GNS3 Node Tags
|
||||
|
||||
Device type is extracted from GNS3 node tags:
|
||||
|
||||
```
|
||||
device_type:gns3_huawei_telnet_ce → Netmiko driver selection
|
||||
```
|
||||
|
||||
**Tag Examples:**
|
||||
|
||||
| Vendor | Device Type Tag | Template Source |
|
||||
|--------|----------------|-----------------|
|
||||
| Cisco IOS | `device_type:cisco_ios_telnet` | User appliance |
|
||||
| Huawei CE | `device_type:gns3_huawei_telnet_ce` | User appliance |
|
||||
| Ruijie | `device_type:gns3_ruijie_telnet` | User appliance |
|
||||
| **VPCS** | `device_type:gns3_vpcs_telnet` | **Built-in ✅** |
|
||||
|
||||
**VPCS Built-in Template:**
|
||||
- VPCS has a **built-in template** with pre-configured `device_type` tag
|
||||
- The tag is **automatically applied** when creating VPCS nodes
|
||||
- No manual configuration required — works out of the box
|
||||
- Other devices require users to import appliances and configure tags manually
|
||||
|
||||
### Nornir Best Practice: Host-Level Connection Configuration
|
||||
|
||||
The system uses **Nornir's configuration priority** (host > group > defaults) to handle multi-vendor environments efficiently:
|
||||
|
||||
```python
|
||||
# From get_gns3_device_port.py
|
||||
hosts_data[device_name] = {
|
||||
"port": console_port,
|
||||
"groups": ["network_devices"], # All devices share one group
|
||||
"connection_options": {
|
||||
"netmiko": {
|
||||
"extras": {"device_type": device_type} # Device-specific driver
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Why Host-Level `connection_options`?**
|
||||
- ✅ Each device has its own `device_type` (host-level config)
|
||||
- ✅ All devices share common settings via group inheritance (`hostname`, `timeout`)
|
||||
- ✅ No need to dynamically create multiple groups for each device type
|
||||
- ✅ Cleaner code structure — single generic group for all devices
|
||||
- ✅ Follows Nornir best practice: "configuration proximity"
|
||||
|
||||
**Configuration Priority:**
|
||||
```mermaid
|
||||
graph TD
|
||||
A["Host Level<br/>connection_options.device_type<br/>(gns3_huawei_telnet_ce, cisco_ios_telnet, etc.)"]
|
||||
B["Group Level<br/>hostname=127.0.0.1, timeout=120<br/>username, password"]
|
||||
C["Defaults Level<br/>data.location=gns3"]
|
||||
A -->|overrides| B
|
||||
B -->|overrides| C
|
||||
```
|
||||
|
||||
| Aspect | Old: Dynamic Groups | New: Host-Level Config |
|
||||
|--------|--------------------|-----------------------|
|
||||
| Group count | One per vendor type | Single `network_devices` group |
|
||||
| Device type location | Group `connection_options` | Host `connection_options` |
|
||||
| Complexity | Dynamic group creation logic | No dynamic group logic |
|
||||
|
||||
## Architecture Evolution
|
||||
|
||||
### Problem: Multi-Vendor Device Support
|
||||
|
||||
**Initial Challenge:**
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Topology: Cisco R1 + Huawei SW1 + Juniper SRX] --> B[Need: Different Netmiko drivers per device]
|
||||
B --> C[Question: How to configure Nornir for multiple device types?]
|
||||
```
|
||||
|
||||
### Solution Evolution
|
||||
|
||||
| Approach | Strategy | Issue |
|
||||
|----------|----------|-------|
|
||||
| ❌ Approach 1 | Single group with first device's `device_type` | All devices use wrong driver |
|
||||
| ❌ Approach 2 | Dynamic groups per vendor (`huawei_telnet`, `cisco_telnet`, ...) | Complex logic, code duplication, not Nornir best practice |
|
||||
| ✅ Approach 3 (Current) | **Host-level `connection_options`** + single generic group | Clean, follows Nornir host > group > defaults priority |
|
||||
|
||||
> See `get_gns3_device_port.py:get_device_ports_from_topology()` for the current implementation.
|
||||
|
||||
### Configuration Priority Demonstration
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["Host Level (highest)<br/>device_type: gns3_huawei_telnet_ce"]
|
||||
B["Group Level (middle)<br/>hostname: 127.0.0.1, timeout: 120"]
|
||||
C["Defaults Level (lowest)<br/>data.location: gns3"]
|
||||
A -->|overrides| B -->|overrides| C
|
||||
D[Result] --> D1["Each device uses its own device_type"]
|
||||
D --> D2["All share hostname, timeout from group"]
|
||||
D --> D3["All share data.location from defaults"]
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Direct Netmiko Usage
|
||||
|
||||
**Custom driver auto-registers on import. No username/password needed for GNS3 devices.**
|
||||
|
||||
| Device Type | `device_type` | Auth | Notes |
|
||||
|-------------|---------------|------|-------|
|
||||
| Huawei CE | `gns3_huawei_telnet_ce` | None | Custom driver, skip auth |
|
||||
| Ruijie | `gns3_ruijie_telnet` | None | Custom driver, interactive prompts handled |
|
||||
| VPCS | `gns3_vpcs_telnet` | None | Custom driver, no config mode |
|
||||
| Cisco IOS | `cisco_ios_telnet` | Required | Standard Netmiko driver |
|
||||
|
||||
### GNS3 Copilot Tool Usage
|
||||
|
||||
> Tools: `tools_v2/display_tools_nornir.py`, `tools_v2/config_tools_nornir.py`, `tools_v2/vpcs_tools_netmiko.py`
|
||||
|
||||
Each tool reads node tags to determine `device_type`, builds host-level Nornir inventory, and dispatches commands to the appropriate Netmiko driver. Multi-vendor topologies are handled transparently — each device uses its own driver.
|
||||
|
||||
## Module Structure
|
||||
|
||||
```
|
||||
gns3server/agent/gns3_copilot/
|
||||
├── utils/
|
||||
│ ├── custom_netmiko/ # Custom Netmiko drivers package
|
||||
│ │ ├── __init__.py # Auto-registers all drivers on import
|
||||
│ │ ├── huawei_ce.py # Huawei CloudEngine driver
|
||||
│ │ ├── ruijie_telnet.py # Ruijie enhanced driver
|
||||
│ │ ├── vpcs_telnet.py # VPCS simulator driver
|
||||
│ │ ├── README.md # Driver development guide
|
||||
│ │ └── tests/ # Unit tests (52 total)
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── test_huawei_ce.py # Huawei CE driver tests (9)
|
||||
│ │ ├── test_ruijie_telnet.py # Ruijie driver tests (15)
|
||||
│ │ └── test_vpcs_telnet.py # VPCS driver tests (28)
|
||||
│ └── get_gns3_device_port.py # Device port extraction
|
||||
│ └── get_device_ports_from_topology() # Extract ports + device_type from tags
|
||||
├── tools_v2/
|
||||
│ ├── display_tools_nornir.py # Multi-vendor display commands
|
||||
│ ├── config_tools_nornir.py # Multi-vendor config commands
|
||||
│ │ └── _expand_multiline_commands() # Expand banner commands
|
||||
│ └── vpcs_tools_netmiko.py # VPCS commands using Nornir + Netmiko
|
||||
│ └── VPCSCommands # VPCS tool class
|
||||
```
|
||||
|
||||
**Key Architectural Changes (2026-03-14):**
|
||||
- ❌ Removed: `vpcs_tools_telnetlib3.py` - Replaced with Netmiko implementation
|
||||
- ✅ Added: `vpcs_telnet.py` - Custom VPCS driver for Netmiko
|
||||
- ✅ Added: `vpcs_tools_netmiko.py` - VPCS tool using Nornir + Netmiko
|
||||
- ✅ Simplified: Unified tool architecture - all tools use Nornir + Netmiko
|
||||
- ✅ Updated: Module structure - all tools follow same pattern
|
||||
|
||||
**Key Architectural Changes (2026-03-13):**
|
||||
- ❌ Removed: `_get_nornir_groups_config()` - No longer needed
|
||||
- ❌ Removed: `_get_nornir_group()` - No longer needed
|
||||
- ✅ Simplified: `_initialize_nornir()` - Uses single generic group
|
||||
- ✅ Updated: `get_gns3_device_port.py()` - Returns host-level `connection_options`
|
||||
- ✅ Added: `ruijie_telnet.py` - Custom Ruijie driver with interactive prompt handling
|
||||
- ✅ Added: `_expand_multiline_commands()` - Auto-expands banner and multi-line commands
|
||||
- ✅ Added: device_type tag validation with error feedback (inline in `_run`)
|
||||
|
||||
## Unit Testing
|
||||
|
||||
### Test Coverage
|
||||
|
||||
> Test files located at: `gns3server/agent/gns3_copilot/utils/custom_netmiko/tests/`
|
||||
|
||||
**Total: 52 tests across 3 test files**
|
||||
|
||||
| Test File | Tests | Coverage Focus |
|
||||
|-----------|-------|----------------|
|
||||
| `test_huawei_ce.py` | 9 | Registration, inheritance, VRP methods, mock connection |
|
||||
| `test_ruijie_telnet.py` | 15 | Registration, interactive patterns, preprocessing, batch/fallback |
|
||||
| `test_vpcs_telnet.py` | 28 | Registration, login, command sending, ANSI stripping, config mode |
|
||||
|
||||
#### Huawei CE Tests (`TestHuaweiTelnetCEDriver` + `TestHuaweiTelnetCEIntegration`)
|
||||
- Device type registration in CLASS_MAPPER
|
||||
- Inheritance from HuaweiBase
|
||||
- VRP-specific methods (`config_mode`, `check_config_mode`, `exit_config_mode`)
|
||||
- Prompt pattern constants
|
||||
- Mock telnet connection
|
||||
|
||||
#### Ruijie Tests (`TestRuijieTelnetEnhancedDriver` + `TestRuijieTelnetEnhancedIntegration`)
|
||||
- Device type registration
|
||||
- Inheritance from RuijieOSBase
|
||||
- Interactive pattern matching (`router-id`, `erase`, `delete`, etc.)
|
||||
- Preprocessing of interactive commands
|
||||
- Batch and fallback send strategies
|
||||
|
||||
#### VPCS Tests (`TestVPCSTelnetDriver` + 5 more test classes)
|
||||
- Device type registration
|
||||
- Inheritance from BaseConnection
|
||||
- No-auth login with `PC\d+>` prompt
|
||||
- `send_command` / `send_command_timing` overrides
|
||||
- ANSI escape code stripping (8 dedicated tests)
|
||||
- Config mode always returns empty/false
|
||||
|
||||
**Running Tests:**
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
python -m pytest gns3server/agent/gns3_copilot/utils/custom_netmiko/tests/
|
||||
```
|
||||
|
||||
**Current Test Status:** ✅ All 52 tests passing
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Custom Netmiko README](../../../../../gns3server/agent/gns3_copilot/utils/custom_netmiko/README.md) - Driver development guide
|
||||
- [Node Control Tools](./node-control-tools.md) - Device lifecycle management
|
||||
- [Command Security](./command-security.md) - Command filtering and validation
|
||||
- [Chat API](./chat-api.md) - Session management and SSE
|
||||
|
||||
## References
|
||||
|
||||
- [Netmiko Documentation](https://ktbyers.github.io/netmiko/)
|
||||
- [Netmiko PLATFORMS.md](https://github.com/ktbyers/netmiko/blob/master/PLATFORMS.md)
|
||||
- [Nornir Documentation](https://nornir.readthedocs.io/)
|
||||
|
||||
---
|
||||
|
||||
_Implementation Date: 2026-03-12_
|
||||
|
||||
_Last Updated: 2026-04-20 (Documentation review: fixed VPCS template tags, updated test coverage, added Mermaid diagrams, removed code bloat)_
|
||||
|
||||
_Status: ✅ Implemented - Custom drivers for Huawei, Ruijie, and VPCS; multi-vendor support with Cisco IOS, Huawei, Ruijie, and VPCS tested_
|
||||
|
||||
_Architecture: Nornir best practice - host-level connection_options with single generic group_
|
||||
|
||||
_Unit Tests: ✅ 52/52 passing (9 Huawei + 15 Ruijie + 28 VPCS)_
|
||||
|
||||
_Changelog:_
|
||||
- **2026-03-14**: Added VPCS support and unified tool architecture
|
||||
- Implemented `VPCSTelnet` custom Netmiko driver for VPCS simulator
|
||||
- Replaced `vpcs_tools_telnetlib3.py` with `vpcs_tools_netmiko.py`
|
||||
- Unified all tools to use Nornir + Netmiko architecture
|
||||
- Removed dependency on telnetlib3 for VPCS devices
|
||||
- Improved code consistency and maintainability
|
||||
- **2026-03-13 (Evening)**: Added Ruijie enhanced driver and interactive command handling
|
||||
- Implemented `RuijieTelnetEnhanced` with hybrid batch/fallback strategy
|
||||
- Added automatic `yes` insertion for known interactive commands (`router-id`, `erase`, etc.)
|
||||
- Achieves ~2-3 seconds for 13 commands (vs 10+ seconds for one-by-one)
|
||||
- Auto-fallback to real-time detection for unknown interactive prompts
|
||||
- Registered `gns3_ruijie_telnet` device type
|
||||
- **2026-03-13 (Afternoon)**: Enhanced configuration safety and command handling
|
||||
- Added AAA/password configuration prohibition in system prompts
|
||||
- Implemented multi-line command expansion (`_expand_multiline_commands()`)
|
||||
- Added `device_type` tag validation with error feedback
|
||||
- Prevents execution of commands that could lock users out of devices
|
||||
- Properly handles banner and other multi-line configuration commands
|
||||
- **2026-03-13 (Morning)**: Refactored to use host-level `connection_options` instead of dynamic groups
|
||||
- Removed `_get_nornir_groups_config()` and `_get_nornir_group()` helper functions
|
||||
- Simplified `_initialize_nornir()` to use single generic group
|
||||
- Updated `get_gns3_device_port.py()` to return host-level configuration
|
||||
- **2026-03-12**: Initial implementation with custom Huawei driver
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,171 @@
|
||||
<!--
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
See LICENSE file for licensing information.
|
||||
-->
|
||||
|
||||
> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.
|
||||
|
||||
|
||||
# External Skills Repository
|
||||
|
||||
## Overview
|
||||
|
||||
GNS3 Copilot loads all skills, prompts, and security configurations from an external Git repository at [github.com/yueguobin/GNS3-Skills](https://github.com/yueguobin/GNS3-Skills). This enables dynamic updates without server redeployment.
|
||||
|
||||
The repository provides:
|
||||
- **Injection skills** (39 categories): Network fault scenarios for troubleshooting practice
|
||||
- **Device skills**: Device-specific command knowledge (VPCS, etc.)
|
||||
- **Feature skills**: Topology planning, network design
|
||||
- **System prompts**: Agent personality and behavior definitions
|
||||
- **Forbidden commands**: Security rules for command filtering
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "GNS3-Skills Repository"
|
||||
YAML[injection/*.yaml<br/>device/*.yaml<br/>feature/*.yaml]
|
||||
MD[prompts/*.md]
|
||||
CFG[config/forbidden_commands.txt]
|
||||
end
|
||||
|
||||
subgraph "GNS3 Server"
|
||||
SM[SkillsManager]
|
||||
SL[SkillsLoader]
|
||||
REG[Registry<br/>SKILLS_REGISTRY<br/>INJECTION_SKILLS_REGISTRY]
|
||||
PROMPT[PROMPTS_CACHE]
|
||||
FC[command_filter]
|
||||
end
|
||||
|
||||
YAML --> SL
|
||||
MD --> SL
|
||||
CFG --> FC
|
||||
SL --> REG
|
||||
SL --> PROMPT
|
||||
SM --> SL
|
||||
SM -->|git pull| YAML
|
||||
```
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
GNS3-Skills/
|
||||
├── injection/ # 39 YAML files, one per protocol/category
|
||||
│ ├── ospf_issues.yaml
|
||||
│ ├── bgp_issues.yaml
|
||||
│ ├── vlan_issues.yaml
|
||||
│ └── ...
|
||||
├── device/ # Device-specific skills
|
||||
│ └── vpcs.yaml
|
||||
├── feature/ # Feature skills
|
||||
│ └── topology_planner.yaml
|
||||
├── prompts/ # System prompts (Markdown)
|
||||
│ ├── teaching_assistant.md
|
||||
│ ├── lab_automation_assistant.md
|
||||
│ ├── troubleshooting_injection.md
|
||||
│ └── title.md
|
||||
└── config/ # Security configuration
|
||||
└── forbidden_commands.txt
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Skills repository settings are configured in `gns3_server.conf` under the `[Server]` section:
|
||||
|
||||
```ini
|
||||
[Server]
|
||||
skills_repo_url = https://github.com/yueguobin/GNS3-Skills.git
|
||||
skills_repo_branch = main
|
||||
skills_auto_update = true
|
||||
```
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `skills_repo_url` | `https://github.com/yueguobin/GNS3-Skills.git` | Git repository URL |
|
||||
| `skills_repo_branch` | `main` | Git branch to track |
|
||||
| `skills_auto_update` | `true` | Automatically pull on reload |
|
||||
|
||||
## Initialization Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Server
|
||||
participant agent/__init__.py
|
||||
participant SkillsManager
|
||||
participant Git
|
||||
|
||||
Server->>agent/__init__.py: Import module
|
||||
agent/__init__.py->>SkillsManager: Start background init thread
|
||||
Note over SkillsManager: Thread.join(5s timeout)
|
||||
|
||||
SkillsManager->>Git: Check local repo exists?
|
||||
alt No local repo
|
||||
Git->>SkillsManager: git clone (with timeout env)
|
||||
else Local repo exists
|
||||
SkillsManager->>Git: Check uncommitted changes?
|
||||
alt Has uncommitted changes
|
||||
Git-->>SkillsManager: Warn, skip pull
|
||||
else No changes
|
||||
SkillsManager->>Git: git fetch (timeout 10s)
|
||||
SkillsManager->>Git: Behind remote?
|
||||
alt Behind
|
||||
Git->>SkillsManager: git pull
|
||||
else Up to date
|
||||
Git-->>SkillsManager: Nothing to do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
SkillsManager->>SkillsManager: reload_skills() - load YAML files
|
||||
SkillsManager->>SkillsManager: reload_prompts() - load Markdown files
|
||||
```
|
||||
|
||||
### Git Timeout Configuration
|
||||
|
||||
Git operations use per-command environment variables to prevent hanging:
|
||||
|
||||
```python
|
||||
_GIT_TIMEOUT_ENV = {
|
||||
'GIT_HTTP_TIMEOUT': '10', # Connection timeout (default: 120s)
|
||||
'GIT_HTTP_LOW_SPEED_TIME': '5', # Slow speed threshold window
|
||||
'GIT_HTTP_LOW_SPEED_LIMIT': '1000', # < 1 KB/s = slow → abort
|
||||
}
|
||||
```
|
||||
|
||||
These apply only to the specific `clone`/`fetch`/`pull` subprocess, not to the global environment.
|
||||
|
||||
## API Endpoint
|
||||
|
||||
### POST /copilot/reload/skills
|
||||
|
||||
Triggers a full reload of the skills repository. Performs one git update check, then reloads all skills, prompts, and forbidden commands from local files.
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"skills": true,
|
||||
"skill_count": 39,
|
||||
"prompts": true,
|
||||
"prompt_count": 4,
|
||||
"forbidden_commands": 6,
|
||||
"version": "abc123def456..."
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `success` | Overall success (true if skills or prompts loaded) |
|
||||
| `skills` | Skills reload result |
|
||||
| `skill_count` | Number of injection skills loaded |
|
||||
| `prompts` | Prompts reload result |
|
||||
| `prompt_count` | Number of prompts loaded |
|
||||
| `forbidden_commands` | Number of forbidden command patterns |
|
||||
| `version` | Git commit hash of the repository |
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Fault Injection](fault-injection.md)
|
||||
- [Command Security](command-security.md)
|
||||
- [Chat API](chat-api.md)
|
||||
@@ -0,0 +1,64 @@
|
||||
<!--
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
See LICENSE file for licensing information.
|
||||
-->
|
||||
|
||||
> This document is a roadmap/planning document. The described features have not been implemented yet.
|
||||
|
||||
|
||||
# Injection Fault Tracking — Roadmap
|
||||
|
||||
## Problem
|
||||
|
||||
When using the AI Copilot in troubleshooting injection mode across multiple chat sessions within the same project, there is no mechanism to prevent the same fault from being injected repeatedly. Each new session starts with a clean slate, unaware of what faults have already been used. This leads to:
|
||||
|
||||
- Duplicate injection scenarios across different sessions
|
||||
- No visibility for instructors into which faults have been used
|
||||
- No record of which devices were affected or whether faults were resolved
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
Track injected faults per chat session and feed the history into the LLM's system prompt as context, so the model actively avoids repeating scenarios.
|
||||
|
||||
### 1. Storage
|
||||
|
||||
The existing `chat_sessions` table has `metadata` and `stats` JSON columns. After each fault injection, the agent records:
|
||||
|
||||
```json
|
||||
// stored in chat_session.metadata
|
||||
{
|
||||
"injected_fault": {
|
||||
"fault_type": "injection_ospf",
|
||||
"issue_key": "ospf_hello_dead_mismatch",
|
||||
"issue_name": "OSPF Hello/Dead Interval Mismatch",
|
||||
"device": "R1",
|
||||
"severity": "major",
|
||||
"injected_at": "2026-05-11T23:00:00Z",
|
||||
"resolved": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Context Injection
|
||||
|
||||
When starting a new troubleshooting session, query all previous sessions for the project, filter by `copilot_mode = "troubleshooting_injection"`, extract `injected_fault` data, and inject a summary into the system prompt via a placeholder (similar to the existing `{{topology_info}}` mechanism):
|
||||
|
||||
```
|
||||
Previously injected faults in this project:
|
||||
1. OSPF Hello/Dead Interval Mismatch on R1 (2026-05-11) — resolved
|
||||
2. BGP Route Reflector misconfiguration on R3 (2026-05-10) — resolved
|
||||
|
||||
Select a fault that has NOT been used before.
|
||||
```
|
||||
|
||||
### 3. Prompt Update
|
||||
|
||||
The `troubleshooting_injection.md` prompt in the GNS3-Skills repository gains an `{{injection_history}}` placeholder, and the agent is instructed to choose a fault not in the history list.
|
||||
|
||||
## Status
|
||||
|
||||
- [ ] GNS3-Skills: add `{{injection_history}}` placeholder to `troubleshooting_injection.md`
|
||||
- [ ] gns3-server: implement injection fault recording in agent chat session flow
|
||||
- [ ] gns3-server: implement injection history query across sessions per project
|
||||
- [ ] gns3-server: inject history into system prompt via `context_manager.py`
|
||||
- [ ] gns3-server: add `exclude` parameter to `InjectionSkillsTool` for filtering used faults
|
||||
@@ -0,0 +1,255 @@
|
||||
<!--
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
See LICENSE file for licensing information.
|
||||
-->
|
||||
> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.
|
||||
|
||||
|
||||
|
||||
# Netmiko Supported Devices
|
||||
|
||||
**Netmiko Version:** 4.6.0
|
||||
**Generated:** 2026-03-14 13:10:00
|
||||
**Last Updated:** 2026-03-14 (Added VPCS support)
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
- **Total Device Types:** 366
|
||||
- **SSH Devices:** 154
|
||||
- **Telnet Devices:** 55
|
||||
- **Custom Devices:** 3
|
||||
|
||||
## Custom GNS3 Drivers
|
||||
|
||||
GNS3-Copilot includes custom Netmiko drivers optimized for GNS3 emulation:
|
||||
|
||||
| Platform | Device Type | Description |
|
||||
|----------|-------------|-------------|
|
||||
| Huawei CE | `gns3_huawei_telnet_ce` | Huawei CloudEngine driver with no authentication |
|
||||
| Ruijie | `gns3_ruijie_telnet` | Ruijie OS enhanced driver with interactive command handling |
|
||||
| VPCS | `gns3_vpcs_telnet` | VPCS simulator driver with ANSI code stripping |
|
||||
|
||||
## SSH Supported Devices
|
||||
|
||||
| Platform | Device Type | Source |
|
||||
|----------|-------------|--------|
|
||||
| A10 | `a10_ssh` | Netmiko |
|
||||
| Accedian | `accedian_ssh` | Netmiko |
|
||||
| Adtran | `adtran_os_ssh` | Netmiko |
|
||||
| Adva | `adva_fsp150f2_ssh` | Netmiko |
|
||||
| Adva | `adva_fsp150f3_ssh` | Netmiko |
|
||||
| Alaxala | `alaxala_ax26s_ssh` | Netmiko |
|
||||
| Alaxala | `alaxala_ax36s_ssh` | Netmiko |
|
||||
| Alcatel | `alcatel_aos_ssh` | Netmiko |
|
||||
| Alcatel | `alcatel_sros_ssh` | Netmiko |
|
||||
| Allied | `allied_telesis_awplus_ssh` | Netmiko |
|
||||
| Apresia | `apresia_aeos_ssh` | Netmiko |
|
||||
| Arista | `arista_eos_ssh` | Netmiko |
|
||||
| Arris | `arris_cer_ssh` | Netmiko |
|
||||
| Aruba | `aruba_aoscx_ssh` | Netmiko |
|
||||
| Aruba | `aruba_os_ssh` | Netmiko |
|
||||
| Aruba | `aruba_osswitch_ssh` | Netmiko |
|
||||
| Aruba | `aruba_procurve_ssh` | Netmiko |
|
||||
| Asterfusion | `asterfusion_asternos_ssh` | Netmiko |
|
||||
| Audiocode | `audiocode_66_ssh` | Netmiko |
|
||||
| Audiocode | `audiocode_72_ssh` | Netmiko |
|
||||
| Audiocode | `audiocode_shell_ssh` | Netmiko |
|
||||
| Avaya | `avaya_ers_ssh` | Netmiko |
|
||||
| Avaya | `avaya_vsp_ssh` | Netmiko |
|
||||
| Bintec | `bintec_boss_ssh` | Netmiko |
|
||||
| Broadcom | `broadcom_icos_ssh` | Netmiko |
|
||||
| Brocade | `brocade_fastiron_ssh` | Netmiko |
|
||||
| Brocade | `brocade_fos_ssh` | Netmiko |
|
||||
| Brocade | `brocade_netiron_ssh` | Netmiko |
|
||||
| Brocade | `brocade_nos_ssh` | Netmiko |
|
||||
| Brocade | `brocade_vdx_ssh` | Netmiko |
|
||||
| Brocade | `brocade_vyos_ssh` | Netmiko |
|
||||
| Calix | `calix_b6_ssh` | Netmiko |
|
||||
| Casa | `casa_cmts_ssh` | Netmiko |
|
||||
| Cdot | `cdot_cros_ssh` | Netmiko |
|
||||
| Centec | `centec_os_ssh` | Netmiko |
|
||||
| Check Point | `checkpoint_gaia_ssh` | Netmiko |
|
||||
| Ciena | `ciena_saos10_ssh` | Netmiko |
|
||||
| Ciena | `ciena_saos_ssh` | Netmiko |
|
||||
| Ciena | `ciena_waveserver_ssh` | Netmiko |
|
||||
| Cisco | `cisco_apic_ssh` | Netmiko |
|
||||
| Cisco | `cisco_asa_ssh` | Netmiko |
|
||||
| Cisco | `cisco_ftd_ssh` | Netmiko |
|
||||
| Cisco | `cisco_ios_ssh` | Netmiko |
|
||||
| Cisco | `cisco_nxos_ssh` | Netmiko |
|
||||
| Cisco | `cisco_s200_ssh` | Netmiko |
|
||||
| Cisco | `cisco_s300_ssh` | Netmiko |
|
||||
| Cisco | `cisco_tp_ssh` | Netmiko |
|
||||
| Cisco | `cisco_viptela_ssh` | Netmiko |
|
||||
| Cisco | `cisco_wlc_ssh` | Netmiko |
|
||||
| Cisco | `cisco_xe_ssh` | Netmiko |
|
||||
| Cisco | `cisco_xr_ssh` | Netmiko |
|
||||
| Cloudgenix | `cloudgenix_ion_ssh` | Netmiko |
|
||||
| Corelight | `corelight_linux_ssh` | Netmiko |
|
||||
| Coriant | `coriant_ssh` | Netmiko |
|
||||
| Cumulus | `cumulus_linux_ssh` | Netmiko |
|
||||
| Dell | `dell_dnos9_ssh` | Netmiko |
|
||||
| Dell | `dell_force10_ssh` | Netmiko |
|
||||
| Dell | `dell_isilon_ssh` | Netmiko |
|
||||
| Dell | `dell_os10_ssh` | Netmiko |
|
||||
| Dell | `dell_os6_ssh` | Netmiko |
|
||||
| Dell | `dell_os9_ssh` | Netmiko |
|
||||
| Dell | `dell_powerconnect_ssh` | Netmiko |
|
||||
| Dell | `dell_sonic_ssh` | Netmiko |
|
||||
| Digi | `digi_transport_ssh` | Netmiko |
|
||||
| Dlink | `dlink_ds_ssh` | Netmiko |
|
||||
| Edgecore | `edgecore_sonic_ssh` | Netmiko |
|
||||
| Ekinops | `ekinops_ek360_ssh` | Netmiko |
|
||||
| Eltex | `eltex_esr_ssh` | Netmiko |
|
||||
| Eltex | `eltex_ssh` | Netmiko |
|
||||
| Endace | `endace_ssh` | Netmiko |
|
||||
| Enterasys | `enterasys_ssh` | Netmiko |
|
||||
| Ericsson | `ericsson_ipos_ssh` | Netmiko |
|
||||
| Ericsson | `ericsson_mltn63_ssh` | Netmiko |
|
||||
| Ericsson | `ericsson_mltn66_ssh` | Netmiko |
|
||||
| Extreme | `extreme_ers_ssh` | Netmiko |
|
||||
| Extreme | `extreme_exos_ssh` | Netmiko |
|
||||
| Extreme | `extreme_netiron_ssh` | Netmiko |
|
||||
| Extreme | `extreme_nos_ssh` | Netmiko |
|
||||
| Extreme | `extreme_slx_ssh` | Netmiko |
|
||||
| Extreme | `extreme_ssh` | Netmiko |
|
||||
| Extreme | `extreme_tierra_ssh` | Netmiko |
|
||||
| Extreme | `extreme_vdx_ssh` | Netmiko |
|
||||
| Extreme | `extreme_vsp_ssh` | Netmiko |
|
||||
| Extreme | `extreme_wing_ssh` | Netmiko |
|
||||
| F5 | `f5_linux_ssh` | Netmiko |
|
||||
| F5 | `f5_ltm_ssh` | Netmiko |
|
||||
| F5 | `f5_tmsh_ssh` | Netmiko |
|
||||
| Fiberstore | `fiberstore_fsos_ssh` | Netmiko |
|
||||
| Fiberstore | `fiberstore_fsosv2_ssh` | Netmiko |
|
||||
| Fiberstore | `fiberstore_networkos_ssh` | Netmiko |
|
||||
| Flexvnf | `flexvnf_ssh` | Netmiko |
|
||||
| Fortinet | `fortinet_ssh` | Netmiko |
|
||||
| Garderos | `garderos_grs_ssh` | Netmiko |
|
||||
| Generic | `generic_ssh` | Netmiko |
|
||||
| Generic | `generic_termserver_ssh` | Netmiko |
|
||||
| H3C (华三) | `h3c_comware_ssh` | Netmiko |
|
||||
| HP | `hp_comware_ssh` | Netmiko |
|
||||
| HP | `hp_procurve_ssh` | Netmiko |
|
||||
| Hillstone | `hillstone_stoneos_ssh` | Netmiko |
|
||||
| Huawei | `huawei_olt_ssh` | Netmiko |
|
||||
| Huawei | `huawei_smartax_ssh` | Netmiko |
|
||||
| Huawei | `huawei_smartaxmmi_ssh` | Netmiko |
|
||||
| Huawei | `huawei_ssh` | Netmiko |
|
||||
| Huawei | `huawei_vrp_ssh` | Netmiko |
|
||||
| Huawei | `huawei_vrpv8_ssh` | Netmiko |
|
||||
| Infinera | `infinera_packet_ssh` | Netmiko |
|
||||
| Ipinfusion | `ipinfusion_ocnos_ssh` | Netmiko |
|
||||
| Juniper | `juniper_junos_ssh` | Netmiko |
|
||||
| Juniper | `juniper_screenos_ssh` | Netmiko |
|
||||
| Juniper | `juniper_ssh` | Netmiko |
|
||||
| Keymile | `keymile_nos_ssh` | Netmiko |
|
||||
| Keymile | `keymile_ssh` | Netmiko |
|
||||
| Lancom | `lancom_lcossx4_ssh` | Netmiko |
|
||||
| Linux | `linux_ssh` | Netmiko |
|
||||
| Maipu (迈普) | `maipu_ssh` | Netmiko |
|
||||
| Mellanox | `mellanox_mlnxos_ssh` | Netmiko |
|
||||
| Mellanox | `mellanox_ssh` | Netmiko |
|
||||
| Mikrotik | `mikrotik_routeros_ssh` | Netmiko |
|
||||
| Mikrotik | `mikrotik_switchos_ssh` | Netmiko |
|
||||
| Mrv | `mrv_lx_ssh` | Netmiko |
|
||||
| Mrv | `mrv_optiswitch_ssh` | Netmiko |
|
||||
| Nec | `nec_ix_ssh` | Netmiko |
|
||||
| Netapp | `netapp_cdot_ssh` | Netmiko |
|
||||
| Netgear | `netgear_prosafe_ssh` | Netmiko |
|
||||
| Netscaler | `netscaler_ssh` | Netmiko |
|
||||
| Nokia | `nokia_srl_ssh` | Netmiko |
|
||||
| Nokia | `nokia_sros_ssh` | Netmiko |
|
||||
| Oneaccess | `oneaccess_oneos_ssh` | Netmiko |
|
||||
| Ovs | `ovs_linux_ssh` | Netmiko |
|
||||
| Palo Alto | `paloalto_panos_ssh` | Netmiko |
|
||||
| Pluribus | `pluribus_ssh` | Netmiko |
|
||||
| Quanta | `quanta_mesh_ssh` | Netmiko |
|
||||
| Rad | `rad_etx_ssh` | Netmiko |
|
||||
| Raisecom | `raisecom_roap_ssh` | Netmiko |
|
||||
| Ruckus | `ruckus_fastiron_ssh` | Netmiko |
|
||||
| Ruijie (锐捷) | `ruijie_os_ssh` | Netmiko |
|
||||
| Silverpeak | `silverpeak_vxoa_ssh` | Netmiko |
|
||||
| Sixwind | `sixwind_os_ssh` | Netmiko |
|
||||
| Sophos | `sophos_sfos_ssh` | Netmiko |
|
||||
| Supermicro | `supermicro_smis_ssh` | Netmiko |
|
||||
| Telcosystems | `telcosystems_binos_ssh` | Netmiko |
|
||||
| Teldat | `teldat_cit_ssh` | Netmiko |
|
||||
| Tplink | `tplink_jetstream_ssh` | Netmiko |
|
||||
| Ubiquiti | `ubiquiti_edge_ssh` | Netmiko |
|
||||
| Ubiquiti | `ubiquiti_edgerouter_ssh` | Netmiko |
|
||||
| Ubiquiti | `ubiquiti_edgeswitch_ssh` | Netmiko |
|
||||
| Ubiquiti | `ubiquiti_unifiswitch_ssh` | Netmiko |
|
||||
| Vertiv | `vertiv_mph_ssh` | Netmiko |
|
||||
| Vyatta | `vyatta_vyos_ssh` | Netmiko |
|
||||
| Vyos | `vyos_ssh` | Netmiko |
|
||||
| Watchguard | `watchguard_fireware_ssh` | Netmiko |
|
||||
| Yamaha | `yamaha_ssh` | Netmiko |
|
||||
| ZTE (中兴) | `zte_zxros_ssh` | Netmiko |
|
||||
| Zyxel | `zyxel_os_ssh` | Netmiko |
|
||||
|
||||
## Telnet Supported Devices
|
||||
|
||||
| Platform | Device Type | Source |
|
||||
|----------|-------------|--------|
|
||||
| Adtran | `adtran_os_telnet` | Netmiko |
|
||||
| Apresia | `apresia_aeos_telnet` | Netmiko |
|
||||
| Arista | `arista_eos_telnet` | Netmiko |
|
||||
| Aruba | `aruba_procurve_telnet` | Netmiko |
|
||||
| Audiocode | `audiocode_66_telnet` | Netmiko |
|
||||
| Audiocode | `audiocode_72_telnet` | Netmiko |
|
||||
| Audiocode | `audiocode_shell_telnet` | Netmiko |
|
||||
| Bintec | `bintec_boss_telnet` | Netmiko |
|
||||
| Brocade | `brocade_fastiron_telnet` | Netmiko |
|
||||
| Brocade | `brocade_netiron_telnet` | Netmiko |
|
||||
| Calix | `calix_b6_telnet` | Netmiko |
|
||||
| Centec | `centec_os_telnet` | Netmiko |
|
||||
| Ciena | `ciena_saos_telnet` | Netmiko |
|
||||
| Cisco | `cisco_ios_telnet` | Netmiko |
|
||||
| Cisco | `cisco_nxos_telnet` | Netmiko |
|
||||
| Cisco | `cisco_s200_telnet` | Netmiko |
|
||||
| Cisco | `cisco_s300_telnet` | Netmiko |
|
||||
| Cisco | `cisco_xr_telnet` | Netmiko |
|
||||
| Dell | `dell_dnos6_telnet` | Netmiko |
|
||||
| Dell | `dell_powerconnect_telnet` | Netmiko |
|
||||
| Dlink | `dlink_ds_telnet` | Netmiko |
|
||||
| Extreme | `extreme_exos_telnet` | Netmiko |
|
||||
| Extreme | `extreme_netiron_telnet` | Netmiko |
|
||||
| Extreme | `extreme_telnet` | Netmiko |
|
||||
| Fiberstore | `fiberstore_fsosv2_telnet` | Netmiko |
|
||||
| Generic | `generic_telnet` | Netmiko |
|
||||
| Generic | `generic_termserver_telnet` | Netmiko |
|
||||
| Genexis | `genexis_solt33_telnet` | Netmiko |
|
||||
| HP | `hp_comware_telnet` | Netmiko |
|
||||
| HP | `hp_procurve_telnet` | Netmiko |
|
||||
| Huawei | `huawei_olt_telnet` | Netmiko |
|
||||
| Huawei | `huawei_telnet` | Netmiko |
|
||||
| Huawei | `gns3_huawei_telnet_ce` | Custom ✨ (GNS3) |
|
||||
| Infinera | `infinera_packet_telnet` | Netmiko |
|
||||
| Ipinfusion | `ipinfusion_ocnos_telnet` | Netmiko |
|
||||
| Juniper | `juniper_junos_telnet` | Netmiko |
|
||||
| Maipu (迈普) | `maipu_telnet` | Netmiko |
|
||||
| Nec | `nec_ix_telnet` | Netmiko |
|
||||
| Nokia | `nokia_sros_telnet` | Netmiko |
|
||||
| Oneaccess | `oneaccess_oneos_telnet` | Netmiko |
|
||||
| Optilink | `optilink_eolt11444_telnet` | Netmiko |
|
||||
| Optilink | `optilink_eolt9702_telnet` | Netmiko |
|
||||
| Palo Alto | `paloalto_panos_telnet` | Netmiko |
|
||||
| Rad | `rad_etx_telnet` | Netmiko |
|
||||
| Raisecom | `raisecom_telnet` | Netmiko |
|
||||
| Ruckus | `ruckus_fastiron_telnet` | Netmiko |
|
||||
| Ruijie (锐捷) | `ruijie_os_telnet` | Netmiko |
|
||||
| Ruijie (锐捷) | `gns3_ruijie_telnet` | Custom ✨ (GNS3 Enhanced) |
|
||||
| Supermicro | `supermicro_smis_telnet` | Netmiko |
|
||||
| VPCS | `gns3_vpcs_telnet` | Custom ✨ (GNS3 VPCS Simulator) |
|
||||
| Telcosystems | `telcosystems_binos_telnet` | Netmiko |
|
||||
| Teldat | `teldat_cit_telnet` | Netmiko |
|
||||
| Tplink | `tplink_jetstream_telnet` | Netmiko |
|
||||
| Yamaha | `yamaha_telnet` | Netmiko |
|
||||
| ZTE (中兴) | `zte_zxros_telnet` | Netmiko |
|
||||
|
||||
---
|
||||
|
||||
*This document was generated automatically by the Netmiko device list script.*
|
||||
@@ -0,0 +1,92 @@
|
||||
<!--
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
See LICENSE file for licensing information.
|
||||
-->
|
||||
|
||||
> This document is a roadmap/planning document. The described features have not been implemented yet.
|
||||
|
||||
|
||||
# Protocol-Oriented Packet Analysis — Roadmap
|
||||
|
||||
## Problem
|
||||
|
||||
The current `PacketCaptureTool` (`analyze_packets`) only accepts a single `packet_number` parameter and runs `tshark -V` on that one frame. This approach:
|
||||
|
||||
- Forces the LLM to guess packet numbers without any visibility into the capture
|
||||
- Returns raw verbose output instead of structured data
|
||||
- Has no protocol awareness — every protocol looks the same to the tool
|
||||
- Provides no built-in anomaly detection; the LLM must infer issues from raw output each time
|
||||
- Downloads the same pcap from the server on every call (no caching)
|
||||
|
||||
## Proposed Architecture
|
||||
|
||||
Move from number-based to **protocol-oriented** packet analysis. Protocol definitions (fields, display filters, anomaly checks) are stored as YAML files in the GNS3-Skills repository. The tool only needs `link_id` + `protocol` — no complex parameters for the LLM to get wrong.
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
GNS3-Skills/packet_analysis/<protocol>.yaml
|
||||
│
|
||||
▼ loaded at startup / reload
|
||||
PACKET_ANALYSIS_REGISTRY (in-memory dict)
|
||||
│
|
||||
▼
|
||||
PacketAnalysisTool(link_id, protocol)
|
||||
│
|
||||
├── download pcap (with link_id caching)
|
||||
├── tshark -T fields -e <predefined fields>
|
||||
├── run anomaly checks from YAML
|
||||
└── return structured JSON + check results
|
||||
│
|
||||
▼
|
||||
LLM produces natural language explanation
|
||||
```
|
||||
|
||||
### YAML Format
|
||||
|
||||
```yaml
|
||||
name: "OSPF Packet Analysis"
|
||||
protocol: "ospf"
|
||||
display_filter: "ospf"
|
||||
fields:
|
||||
- label: "Source IP"
|
||||
field: "ip.src"
|
||||
description: "Source IPv4 address"
|
||||
- label: "OSPF Message Type"
|
||||
field: "ospf.msg"
|
||||
description: "1=Hello, 2=DBD, 3=LSR, 4=LSU, 5=LSAck"
|
||||
checks:
|
||||
- name: hello_dead_mismatch
|
||||
severity: critical
|
||||
message: "Hello/Dead Interval mismatch between {src} and {dst}"
|
||||
condition: "Same broadcast domain has inconsistent hello/dead intervals"
|
||||
```
|
||||
|
||||
### Planned Protocols
|
||||
|
||||
| File | Protocols | Key Checks |
|
||||
|------|-----------|------------|
|
||||
| `arp.yaml` | ARP, NDP (ICMPv6 NS/NA) | Duplicate IP, no ARP reply, ARP flooding |
|
||||
| `icmp.yaml` | ICMPv4, ICMPv6 | Unreachable classification, ping loss, PMTUD issues |
|
||||
| `ospf.yaml` | OSPFv2, OSPFv3 | Hello/Dead mismatch, Area ID mismatch, Router ID conflict |
|
||||
| `bgp.yaml` | BGPv4, BGP+ | Hold timer mismatch, Notification analysis, AS_PATH loop |
|
||||
|
||||
### Tool Interface
|
||||
|
||||
```json
|
||||
{
|
||||
"link_id": "uuid (required)",
|
||||
"protocol": "arp | icmp | ospf | bgp (required)",
|
||||
"summary_only": "bool (optional, default: false)"
|
||||
}
|
||||
```
|
||||
|
||||
## Status
|
||||
|
||||
- [ ] GNS3-Skills: create `packet_analysis/` directory and YAML definitions
|
||||
- [ ] gns3-server: add `PACKET_ANALYSIS_REGISTRY` loading from skills repo
|
||||
- [ ] gns3-server: implement `PacketAnalysisTool` with tshark field extraction
|
||||
- [ ] gns3-server: implement protocol-specific anomaly checks
|
||||
- [ ] gns3-server: add pcap caching by `link_id`
|
||||
- [ ] gns3-server: register tool in teaching assistant and lab automation modes
|
||||
- [ ] gns3-server: deprecate and remove old `PacketCaptureTool`
|
||||
@@ -0,0 +1,67 @@
|
||||
<!--
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
See LICENSE file for licensing information.
|
||||
-->
|
||||
|
||||
> This document is a roadmap/planning document. The described features have not been implemented yet.
|
||||
|
||||
|
||||
# Server Settings REST API — Roadmap
|
||||
|
||||
## Problem
|
||||
|
||||
Currently, `gns3_server.conf` can only be modified by directly editing the file on disk. There is no REST API endpoint to read or write server configuration, which prevents the Web UI from offering a settings page for server parameters.
|
||||
|
||||
## Proposed API
|
||||
|
||||
```
|
||||
GET /v3/settings → Return all current server settings
|
||||
PUT /v3/settings → Update and persist server settings
|
||||
```
|
||||
|
||||
### Implementation Plan
|
||||
|
||||
**1. Add `save_config()` to `Config` class** (`gns3server/config.py`)
|
||||
|
||||
The `Config` class currently only reads configuration (via `read_config()` / `reload()`). A `save_config()` method is needed to serialize the in-memory `ServerConfig` pydantic model back to INI format and write it to disk.
|
||||
|
||||
Serialization details:
|
||||
- `bool` → `"True"` / `"False"` (configparser convention)
|
||||
- `SecretStr` → `get_secret_value()`
|
||||
- `Enum` → `.value`
|
||||
- `List[str]` → semi-colon for `additional_images_paths`, comma for `allowed_interfaces`
|
||||
- `None` → skip
|
||||
|
||||
**2. Add a settings getter/setter** to `Config` to allow programmatic updates to the in-memory settings.
|
||||
|
||||
**3. New route file** (`gns3server/api/routes/controller/settings.py`):
|
||||
|
||||
- `GET /v3/settings` — returns the full `ServerConfig` as JSON (pydantic automatically masks `SecretStr` fields as `"********"`)
|
||||
- `PUT /v3/settings` — accepts `ServerConfig`, merges existing secrets when placeholder values (`"********"`) are submitted, calls `save_config()`, and triggers runtime config update callbacks
|
||||
|
||||
Both endpoints require `get_current_active_user` for authentication.
|
||||
|
||||
**4. Register the new router** in `gns3server/api/routes/controller/__init__.py` under the `/settings` prefix.
|
||||
|
||||
### Security
|
||||
|
||||
- All settings endpoints require admin authentication (`get_current_active_user`)
|
||||
- `SecretStr` fields (`compute_password`, `default_admin_password`, `jwt_secret_key`) are masked in responses
|
||||
- On write, unchanged secrets are preserved via placeholder detection
|
||||
|
||||
### Related Files
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `gns3server/config.py` | Config singleton with `read_config()` / `reload()` |
|
||||
| `gns3server/schemas/config.py` | `ServerConfig` pydantic model with all 9 sub-models |
|
||||
| `gns3server/api/routes/controller/__init__.py` | Controller router mounting |
|
||||
| `gns3server/controller/__init__.py` | `Controller._update_config()` for runtime credential sync |
|
||||
|
||||
## Status
|
||||
|
||||
- [ ] gns3server/config.py: add `save_config()` method
|
||||
- [ ] gns3server/config.py: add settings getter/setter
|
||||
- [ ] gns3server/api/routes/controller/settings.py: new route file with GET and PUT endpoints
|
||||
- [ ] gns3server/api/routes/controller/__init__.py: register settings router under `/settings`
|
||||
- [ ] gns3server/api/routes/controller/controller.py: add notification emission on config change
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,60 @@
|
||||
<!--
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
See LICENSE file for licensing information.
|
||||
-->
|
||||
|
||||
> This document is a roadmap/planning document. The described features have not been implemented yet.
|
||||
|
||||
|
||||
# User Preferences API — Roadmap
|
||||
|
||||
## Problem
|
||||
|
||||
Currently, user-specific preferences (UI theme, language, AI Copilot settings, workspace layout, etc.) can only be stored in browser `localStorage`. This has several drawbacks:
|
||||
|
||||
1. **localStorage clearing** — clearing browser data or switching devices loses all settings
|
||||
2. **No cross-device sync** — users must reconfigure preferences on each device
|
||||
3. **No API access** — CLI tools and third-party clients cannot read/write preferences
|
||||
4. **Fragile persistence** — localStorage can be cleared by browser maintenance or incognito mode
|
||||
|
||||
## Proposed API
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `GET` | `/v3/access/users/me/preferences` | Get all preferences for current user |
|
||||
| `PUT` | `/v3/access/users/me/preferences` | Merge preferences (top-level merge, not replace) |
|
||||
| `DELETE` | `/v3/access/users/me/preferences` | Clear all preferences |
|
||||
|
||||
### Storage
|
||||
|
||||
A `preferences` JSON column on the existing `users` table:
|
||||
|
||||
- Type: `JSON`, non-nullable, default `'{}'`
|
||||
- No schema enforcement — clients can store arbitrary key-value pairs
|
||||
- Examples: `{"theme": "dark", "language": "zh-CN", "copilot_default_mode": "teaching_assistant"}`
|
||||
|
||||
### Implementation Plan
|
||||
|
||||
1. Add `preferences` JSON column to `User` ORM model
|
||||
2. Create Alembic migration
|
||||
3. Add `UserPreferencesUpdate` Pydantic schema with `extra="allow"`
|
||||
4. Add `get_user_preferences()` and `update_user_preferences()` to `UsersRepository`
|
||||
5. Add three endpoints to `users.py` router: `GET/PUT/DELETE /me/preferences`
|
||||
|
||||
### Related Files
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `gns3server/db/models/user.py` | User ORM model |
|
||||
| `gns3server/db/repositories/users.py` | UsersRepository with database operations |
|
||||
| `gns3server/api/routes/controller/users.py` | User API router |
|
||||
| `gns3server/schemas/` | Pydantic schemas |
|
||||
| `gns3server/db/versions/` | Alembic migrations |
|
||||
|
||||
## Status
|
||||
|
||||
- [ ] Add `preferences` JSON column to User ORM model
|
||||
- [ ] Create Alembic migration
|
||||
- [ ] Add `UserPreferencesUpdate` schema
|
||||
- [ ] Add repository methods
|
||||
- [ ] Add API endpoints to users.py router
|
||||
@@ -1,3 +1,7 @@
|
||||
<!--
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
See LICENSE file for licensing information.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
+24
-12
@@ -1,14 +1,12 @@
|
||||
<!--
|
||||
SPDX-License-Identifier: CC-BY-SA-4.0
|
||||
See LICENSE file for licensing information.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>GNS3 controller API - ReDoc</title>
|
||||
<!-- needed for adaptive design -->
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
||||
|
||||
<link type="text/css" rel="stylesheet" href="/static/swagger-ui.css">
|
||||
<link rel="shortcut icon" href="https://fastapi.tiangolo.com/img/favicon.png">
|
||||
<!--
|
||||
ReDoc doesn't change outer page styles
|
||||
@@ -21,11 +19,25 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
ReDoc requires Javascript to function. Please enable it to browse the documentation.
|
||||
</noscript>
|
||||
<redoc spec-url="openapi.json"></redoc>
|
||||
<script src="https://cdn.jsdelivr.net/npm/redoc@2/bundles/redoc.standalone.js"> </script>
|
||||
<div id="swagger-ui">
|
||||
</div>
|
||||
<script src="/static/swagger-ui-bundle.js"></script>
|
||||
<!-- `SwaggerUIBundle` is now available on the page -->
|
||||
<script>
|
||||
const ui = SwaggerUIBundle({
|
||||
url: 'openapi.json',
|
||||
oauth2RedirectUrl: window.location.origin + '/docs/oauth2-redirect',
|
||||
dom_id: '#swagger-ui',
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
||||
],
|
||||
layout: "BaseLayout",
|
||||
deepLinking: true,
|
||||
showExtensions: true,
|
||||
showCommonExtensions: true
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in new issue
Block a user