Compare commits

..

504 Commits

Author SHA1 Message Date
646f0fd837 Release v3.0.0b1 2023-11-27 11:13:39 +10:00
3d383b25f0 Bundle web-ui v3.0.0b1 2023-11-27 11:10:37 +10:00
8d28178e56 Upgrade sentry-sdk to v1.37.1 2023-11-27 11:05:20 +10:00
0d63bf5128 Upgrade aiohttp to v3.9.1 2023-11-27 10:49:45 +10:00
37f720ac73 Fix bug when listing endpoints for opened project 2023-11-23 15:38:42 +10:00
9509d3a713 Make images executable after importing a project 2023-11-23 11:30:46 +10:00
061ee1491d Disable IOS hostname check for Dynamips ghost instances 2023-11-22 18:47:05 +10:00
c8245e81be Upgrade sentry-sdk 2023-11-22 12:44:56 +10:00
ff7e10fa7f Merge branch '2.2' into 3.0
# Conflicts:
#	CHANGELOG
#	gns3server/crash_report.py
#	gns3server/main.py
#	gns3server/version.py
#	gns3server/web/web_server.py
#	requirements.txt
2023-11-22 12:42:04 +10:00
6c5f75bf43 Upgrade sentry-sdk and aiohttp 2023-11-22 10:32:50 +10:00
24d958d9ec Release v3.0.0a6 2023-11-15 12:07:48 +10:00
48dab4af61 Bundle web-ui v3.0.0a6 2023-11-15 12:03:04 +10:00
67be4259ff Fix tests 2023-11-15 11:57:14 +10:00
4e9bc190a4 Upgrade to aiohttp v3.9.0rc0 2023-11-15 11:47:48 +10:00
261dd1d6d5 Upgrade dependencies 2023-11-15 10:30:15 +10:00
e80e80a080 Merge pull request #2326 from GNS3/fix/2325
Install Docker resources in writable location
2023-11-13 11:28:56 +10:00
09ff807055 Install Docker resources in writable location 2023-11-13 11:23:26 +10:00
303cbf3642 Fix tests 2023-11-12 18:11:41 +10:00
e367b3a148 Default compute username is "gns3" 2023-11-12 18:02:19 +10:00
7afdcbb3d3 Development in 2.2.45.dev3 2023-11-07 19:11:57 +10:00
c88f76b740 Release v2.2.44.1 2023-11-07 14:59:28 +10:00
b81cc14cef Do not compute checksums on macOS 2023-11-07 14:30:39 +10:00
aa133c3b8a Add multiprocessing.set_start_method() 2023-11-07 13:33:43 +10:00
7449064ea2 Bump version to v2.2.45.dev2 2023-11-07 12:21:39 +10:00
db315e3c3c Have freeze support for macOS as well 2023-11-07 11:17:48 +10:00
c437482e85 Catch exceptions when computing image checksums. Ref https://github.com/GNS3/gns3-server/issues/2228 2023-11-07 11:08:47 +10:00
aac6fbfc31 Add freeze_support() for multiprocessing 2023-11-07 10:42:53 +10:00
8a208dbf04 Merge branch '2.2' into 3.0
# Conflicts:
#	CHANGELOG
#	gns3server/compute/qemu/__init__.py
#	gns3server/crash_report.py
#	gns3server/static/web-ui/index.html
#	gns3server/static/web-ui/main.383fdade2fd9dbccffbc.js
#	gns3server/version.py
#	gns3server/web/web_server.py
#	requirements.txt
2023-11-06 17:09:34 +10:00
3c4b5db1ca Development on 2.2.45.dev1 2023-11-06 17:00:17 +10:00
5c8abdc6fd Release v2.2.44 2023-11-06 16:02:23 +10:00
f80f41b5b3 Sync appliances 2023-11-06 15:58:21 +10:00
503373a6e9 Bundle web-ui v2.2.44 2023-11-06 15:53:51 +10:00
8236c85b5f Upgrade sentry-sdk 2023-11-06 15:44:30 +10:00
ae825b0080 Non-blocking checksums computation when server starts. Fixes #2228 2023-11-06 12:32:23 +10:00
191feed669 Fix timeout issue when creating Qemu disk image. Fixes https://github.com/GNS3/gns3-server/issues/2313 2023-11-05 15:41:46 +10:00
cd785e0d17 Fix broken link to Web UI in 3.0 branch. Fixes #2312 2023-11-03 20:56:31 +10:00
e1bd4638c8 Fix sample config: VMware section declared twice. Fixes #2311 2023-11-03 20:46:36 +10:00
61dede72f9 Fix ws console and packet capture over SSL 2023-11-03 15:35:49 +10:00
13630179b2 Merge branch '2.2' into 3.0
# Conflicts:
#	appveyor.yml
#	gns3server/compute/project_manager.py
#	gns3server/handlers/api/controller/node_handler.py
#	gns3server/static/web-ui/index.html
#	gns3server/static/web-ui/main.febf2f1259a67875a3e3.js
2023-11-03 14:56:55 +10:00
3b22bcfe96 Support for web socket console over HTTPS 2023-11-03 14:40:40 +10:00
0f07b9e019 Add back script create_cert.sh 2023-11-03 12:31:34 +10:00
8676a3dccf Bundle web-ui 2023-11-02 17:40:47 +10:00
b0dbf59ac4 Use Python 3.8 in appveyor.yml 2023-10-31 14:48:08 +10:00
14fd165f2a Merge pull request #2307 from GNS3/fix/3521
Allow disabling hardware virtualization check
2023-10-30 15:03:50 +10:00
7c49a9160c Allow disabling hardware virtualization check 2023-10-30 14:57:11 +10:00
23b1c7a989 Development on 3.0.0.dev10 2023-10-27 14:12:23 +10:00
399e925f2e Release v3.0.0a5 2023-10-27 13:38:14 +10:00
ac11c984cb Bundle web-ui v3.0.0a5 2023-10-27 13:32:31 +10:00
0e8e4fd2f7 Merge branch '2.2' into 3.0 2023-10-27 13:28:32 +10:00
ed99a98fe1 Sync appliances 2023-10-27 13:20:15 +10:00
19edc193f9 Merge pull request #2303 from GNS3/fix/1484
Fix L2IOU "failed code signing checks"
2023-10-26 15:13:12 +10:00
3f50319990 Fix L2IOU "failed code signing checks" when IOU base file name is >= 63 characters 2023-10-26 15:05:10 +10:00
fb6d29aeff Python 3.12 support 2023-10-24 18:37:13 +10:00
5475eedce7 Add igb Qemu adapter 2023-10-24 18:33:26 +10:00
d680bbbc77 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3server/schemas/qemu_template.py
#	requirements.txt
#	tests/compute/qemu/test_qemu_vm.py
#	tests/handlers/api/compute/test_qemu.py
2023-10-24 18:31:51 +10:00
248737c34b Merge pull request #2296 from ventaquil/feature/add-qemu-igb-nic
Add Qemu IGB network device
2023-10-24 17:23:00 +10:00
240708112c Merge branch '2.2' into feature/add-qemu-igb-nic 2023-10-24 17:14:27 +10:00
c759c4e3f6 Upgrade to actions/checkout@v3 and actions/setup-python@v3 2023-10-23 16:17:31 +10:00
34ee75e1f1 Fix tests running on Python 3.12 2023-10-23 16:12:12 +10:00
4af5edbc03 Install aiohttp 3.9.0b0 when running on Python 3.12 2023-10-23 15:21:06 +10:00
0d15f40930 Change "ip cef" to "no ip cef" in IOU default configs. Fixes #2298 2023-10-23 15:16:49 +10:00
f3ad97c398 Merge pull request #2300 from GNS3/fix/1468
Fix compute authentication for websocket endpoints
2023-10-22 16:03:21 +10:00
a55e1cbc99 Update dev-requirements.txt 2023-10-22 15:47:21 +10:00
9c369a2597 Merge branch '3.0' into fix/1468 2023-10-22 15:44:25 +10:00
4dcb0dce57 Merge pull request #2301 from GNS3/drop-python-3.7
Drop Pyhton 3.7 support (end-of-life)
2023-10-22 15:43:26 +10:00
0f0412158d Drop support for Python 3.7 and upgrade dependencies 2023-10-22 15:37:00 +10:00
2f3689588e Fix httpx-ws dependency on Python 3.7 2023-10-22 12:53:47 +10:00
44c066ac68 Fix compute authentication for websocket endpoints 2023-10-22 12:44:39 +10:00
ffc49ea954 Development on 3.0.0.dev9 2023-10-18 20:15:11 +10:00
d610c933fb Release v3.0.0a4 2023-10-18 19:01:13 +10:00
8f35b871d5 Downgrade watchfiles to v0.20.0 2023-10-18 18:35:02 +10:00
29c9d2c2e9 Revert "Temporary support for Python 3.12"
This reverts commit 9911b5199b.
2023-10-18 18:31:44 +10:00
5ad7761337 Upgrade FastAPI, watchfiles and platformdirs dependencies 2023-10-18 18:01:59 +10:00
9911b5199b Temporary support for Python 3.12 2023-10-18 17:58:24 +10:00
edcc69f617 Remove old web-ui files 2023-10-18 17:58:11 +10:00
fe8b61373c Bundle web-ui v3.0.0a4 2023-10-18 17:51:41 +10:00
87960f1ff0 Merge branch '2.2' into 3.0
# Conflicts:
#	README.md
2023-10-18 17:48:09 +10:00
3b0d89be5a Merge branch 'master' into 2.2 2023-10-18 17:46:51 +10:00
5ff3043fb4 Sync appliances 2023-10-18 17:45:17 +10:00
47d3f3f349 Do not enforce Compute.Audit and Template.Audit privileges due to current web-ui limitations 2023-10-18 15:51:43 +10:00
109feeb647 Revert to aiohttp v3.8.6 2023-10-18 15:00:42 +10:00
56839413fa Remove testing with Python 3.6 (not supported) 2023-10-18 14:58:19 +10:00
18012dc18a Upgrade aiohttp and sqlalchemy dependencies 2023-10-18 14:56:19 +10:00
46c02ad2f3 Merge branch '2.2' into 3.0
# Conflicts:
#	requirements.txt
2023-10-18 14:54:39 +10:00
08ac80e451 Upgrade sentry and psutil dependencies 2023-10-18 14:53:25 +10:00
6ca15cbcff Merge branch '2.2' into 3.0
# Conflicts:
#	.github/workflows/testing.yml
#	requirements.txt
#	setup.py
2023-10-18 14:49:49 +10:00
722512d851 Remove model validator. Fixes https://github.com/GNS3/gns3-gui/issues/3522
Default port name and port segment size are already set in the code when a new node is created.
2023-10-15 11:16:05 +10:00
737664a3d4 Add Qemu IGB network device 2023-10-12 11:35:53 +02:00
29f4b238b0 Add Python 3.12 support. Fixes https://github.com/GNS3/gns3-server/issues/2273 2023-10-09 16:54:47 +10:00
b6127af067 Merge pull request #2294 from GNS3/list-items-closed-project
[API] Allow listing items from a closed project
2023-10-09 13:28:58 +10:00
5092bd2fdf List elements when a project is closed 2023-10-09 13:16:12 +10:00
a27db6b4eb Fix deleting resource from resource pool. Ref #2293 2023-09-28 18:38:38 +10:00
66b66cc3e1 Bump version to 3.0.0.dev8 2023-09-25 21:38:20 +10:00
7215b150dd Merge pull request #2292 from GNS3/fix/3422
Support to create empty disk images on the controller
2023-09-25 21:29:24 +10:00
674381f1be Fix tests 2023-09-25 21:08:23 +10:00
1ae6d13022 Support to create empty disk images on the controller 2023-09-25 17:51:14 +10:00
6886f1f566 Merge pull request #2290 from GNS3/fix/2147
Fix issue when using importlib.resources.files()
2023-09-23 20:54:57 +10:00
999a47f747 Fix issue with importlib.resources.files() and Python 3.9 2023-09-23 20:44:00 +10:00
c1507b4155 Upgrade sqlalchemy 2023-09-23 15:56:42 +10:00
d9214a3c67 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3server/compute/virtualbox/__init__.py
#	gns3server/controller/gns3vm/virtualbox_gns3_vm.py
#	gns3server/crash_report.py
#	gns3server/version.py
#	requirements.txt
2023-09-23 15:54:54 +10:00
f675f24ba7 Revert "Install importlib-resources only with Python < '3.9'. Ref #2147"
This reverts commit dc1eed42da.
2023-09-23 14:48:04 +10:00
dc1eed42da Install importlib-resources only with Python < '3.9'. Ref #2147 2023-09-23 14:35:50 +10:00
2c5a9ef9b7 Merge pull request #2288 from Quanterm/master
Update README.md
2023-09-21 11:44:36 +10:00
e75d151611 Update README.md
The release cycle of NixOs is around every half of the year. 
On the unstable channel you have the latest gns3-server version. 
Do not worry to use unstable because it mostly just the latest stable version. You can also do overlays to keep your other systems pkgs on the regular release version.
2023-09-20 11:41:05 +00:00
d87cff7fd5 Merge pull request #2287 from munahaf/Inappropriate_Logic-5node.py11635999804432162276.diff
Update a test expression to remove a logical short circuit
2023-09-20 14:31:04 +07:00
ffb58a4ed2 Merge branch 'master' into Inappropriate_Logic-5node.py11635999804432162276.diff 2023-09-20 14:17:58 +07:00
4d7b3fb9d6 Comment: Updated a test expression to remove a logical short circuit. 2023-09-20 06:51:41 +00:00
94abdfc9f0 Development on 2.2.44.dev1 2023-09-19 21:08:24 +07:00
6f345bb1ec Merge pull request #2286 from GNS3/release-v2.2.43
Release v2.2.43
2023-09-19 21:04:52 +07:00
89ec458f5c Release v2.2.43 2023-09-19 20:16:52 +07:00
80bc1a726b Sync appliances 2023-09-19 20:12:32 +07:00
6265d3d55f Upgrade sentry-sdk and truststore 2023-09-19 20:07:57 +07:00
a96a82821c Merge branch 'master' into 2.2 2023-09-19 20:04:15 +07:00
ad47ffbe29 Force English output for VBoxManage. Fixes #2266 2023-09-19 18:14:05 +07:00
eca1243fc0 Merge pull request #2285 from Orange-OpenSource/3.0
rbac fix: cannot add multiple time same privilege to a role
2023-09-18 18:41:01 +07:00
0d834ee2a0 rbac fix: cannot add multiple time same privilege to a role 2023-09-18 13:34:46 +02:00
e1c5c05492 Merge pull request #2280 from GNS3/resource-pools
Resource pools support
2023-09-14 22:47:02 +07:00
7534718a1c Remove privileges endpoint from roles 2023-09-14 22:41:08 +07:00
63c1defd9a Merge branch '3.0' into resource-pools 2023-09-14 22:38:52 +07:00
1f90bb14b6 Require users to be logged in for privilege API endpoints 2023-09-14 22:36:21 +07:00
ae00dd422f Merge pull request #2283 from Orange-OpenSource/3.0
API: add endpoint to expose availables privileges to web UI
2023-09-13 22:51:05 +07:00
10eeefc1f5 API: add endpoint to expose availables privileges to web UI 2023-09-12 15:24:54 +02:00
702fea89fb Automatically add vboxnet and DHCP server if not present for VirtualBox GNS3 VM. Ref #2266 2023-09-12 16:14:44 +07:00
a95dda0d1d Complete resource pool support for projects 2023-09-11 18:15:03 +07:00
d53ef175f8 DB and API for resource pools 2023-09-07 17:31:11 +07:00
f7d287242f Upgrade platformdirs 2023-09-07 16:32:04 +07:00
c11b3c3911 Fix tests after merge 2023-09-06 23:42:50 +07:00
1ec056c1a6 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3server/compute/docker/docker_vm.py
#	gns3server/controller/__init__.py
#	gns3server/controller/appliance_manager.py
#	gns3server/web/route.py
#	requirements.txt
#	tests/compute/docker/test_docker_vm.py
2023-09-06 23:30:00 +07:00
285b19d97f Merge pull request #2272 from GNS3/fix/2271
Correctly install built-in appliances
2023-09-06 23:16:34 +07:00
709aa46074 Fix issue with controller config saved before checking current version with previous one 2023-09-06 16:48:24 +07:00
0c12849d0d Use controller vars file to store version and appliance etag 2023-09-06 16:28:46 +07:00
8aa9d1aae6 Upgrade dependencies and fix user group tests 2023-09-03 17:58:51 +07:00
66047b3431 Fix OAuth2PasswordBearer token URL 2023-09-03 17:31:05 +07:00
71d06abaf4 Merge pull request #2278 from GNS3/update-publish-api-doc
Update publish api doc with GitHub Actions
2023-09-03 17:00:13 +07:00
05ef9f2a7c No need for dev dependencies 2023-09-03 16:59:48 +07:00
476658b829 Update publish-api-documentation.yml 2023-09-03 16:52:30 +07:00
6ec97a704f Put user, groups, roles and ACL under /access 2023-09-02 19:10:02 +07:00
a358369606 Move notifications under controller router 2023-09-02 18:57:53 +07:00
5155aea923 Upgrade dependencies 2023-09-02 18:49:16 +07:00
b0c4fc17ad Merge pull request #2275 from GNS3/rbac-new-implementation
New RBAC implementation
2023-09-02 18:47:35 +07:00
e72b07bf5c Prepare resource pools table for future development 2023-09-02 18:42:40 +07:00
170f476733 Add more built-in roles 2023-09-02 18:15:00 +07:00
0077fd98aa Add required privileges to all endpoints 2023-09-02 17:54:24 +07:00
f3a4ad49f4 Check for group ACEs to find user privileges 2023-08-28 18:14:34 +10:00
3e0592520b Handle ACE propagate and allowed 2023-08-28 12:06:01 +10:00
d3d0cc039d Merge pull request #2277 from GNS3/fix/2276
Prevent X11 socket file to be modified by Docker container
2023-08-27 19:53:50 +10:00
c2783d355f Fix test_create_vnc test 2023-08-27 18:41:25 +10:00
d6e1ee5dbb Prevent X11 socket file to be modified by Docker container 2023-08-27 18:30:37 +10:00
57197c3d1c Comment unused code 2023-08-27 18:23:10 +10:00
60ce1172e0 Use an ACL table to check for privileges 2023-08-27 18:20:42 +10:00
6bd855b3c5 New database schema for better RBAC 2023-08-21 21:32:23 +10:00
74cb3be910 Merge remote-tracking branch 'origin/3.0' into 3.0 2023-08-19 12:29:53 +10:00
425ad845fc Speed up tests 2023-08-19 12:29:25 +10:00
1ce0c13fc9 Better mocking in Docker tests 2023-08-18 12:20:54 +10:00
e9e2dc2ca7 Fix validation issues and improve exceptions logs 2023-08-17 17:36:50 +10:00
e61ada69bb Merge pull request #2274 from lethedata/openwrt-18.06.5-18.06.2-hashfix
Hash Update openwrt.gns3a
2023-08-17 10:00:14 +10:00
a6b0f32b57 Hash Update openwrt.gns3a
Update hashes for openwrt-18.06.5 and openwrt-18.06.2 based hashes from download links.
2023-08-16 18:37:55 -05:00
a69feb3682 Use an older version of platformdirs 2023-08-12 19:15:29 +10:00
090d1c8c84 Only use platformdirs with Python >= '3.7' 2023-08-12 19:04:14 +10:00
df2f96828e Use the user data dir to store built-in appliances 2023-08-12 18:48:43 +10:00
6a614fbd78 Downgrade jsonschema 2023-08-12 17:51:24 +10:00
2d7438446c Upgrade dependencies 2023-08-12 17:47:48 +10:00
77d4eabadc Catch ConnectionResetError exception when client disconnects 2023-08-12 17:31:58 +10:00
ca48efa5be Upgrade SQLAlchemy to v2.0.18 and fix async_timeout warning 2023-08-11 22:52:29 +10:00
b0657b39ef Upgrade dependencies 2023-08-11 18:54:05 +10:00
cefa459721 Fix command line issue with certfile and certkey 2023-08-11 18:34:16 +10:00
66dd8bdadb Merge branch '2.2' into 3.0 2023-08-11 18:14:16 +10:00
bbb0a407e0 Upgrade to PyQt 5.15.9 and pywin32 2023-08-11 18:13:41 +10:00
96ce5eac8d Merge pull request #2270 from GNS3/packaging-migration
Packaging migration
2023-08-11 18:09:31 +10:00
2f2aabeb5a Fix tests when running Python 3.7 2023-08-11 17:58:00 +10:00
d9eb61efc4 Fix tests with asyncio_patch 2023-08-11 17:37:11 +10:00
1fd8444d22 Add tests for install_busybox() 2023-08-11 17:32:05 +10:00
f3b6825e40 Test if busybox is not dynamically linked 2023-08-11 14:10:25 +10:00
719458764f Fix tests 2023-08-10 23:23:11 +10:00
483db91851 Use dev for optional development dependencies 2023-08-10 23:16:57 +10:00
f3d43aeb39 Fix testing.yml 2023-08-10 22:52:35 +10:00
1cb433c5bc New packaging relying only pyproject.toml 2023-08-10 22:44:37 +10:00
17f71f970e Add truststore inject_into_ssl 2023-08-10 15:01:49 +10:00
fe90d2b146 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3server/controller/__init__.py
#	gns3server/controller/appliance_manager.py
#	gns3server/crash_report.py
#	gns3server/run.py
#	gns3server/static/web-ui/index.html
#	gns3server/utils/images.py
#	gns3server/version.py
#	requirements.txt
#	scripts/update-bundled-web-ui.sh
2023-08-10 15:00:22 +10:00
35e1519073 Development on v2.2.43.dev1 2023-08-09 22:15:15 +10:00
9f5aece8b6 Merge pull request #2267 from GNS3/release-v2.2.42
Release v2.2.42
2023-08-09 22:09:29 +10:00
08139ebdd1 Release v2.2.42 2023-08-09 21:11:57 +10:00
cd87ac4474 Bundle web-ui v2.2.42 2023-08-09 21:03:40 +10:00
77744cc31d Handle API version key in VirtualBox 7. Fixes #2266 2023-08-09 20:51:53 +10:00
df9d642a56 Sync appliances 2023-08-08 19:07:04 +10:00
fad28c766a Enable system certificate store later in the code and bump version to 2.2.42.dev4 2023-08-08 17:22:27 +10:00
21d9d968c4 Revert "Use truststore in appliance_manager.py instead"
This reverts commit a2628042e8.
2023-08-07 20:44:37 +10:00
945a2c98cd Revert "Use finer-grained control for truststore"
This reverts commit 5e1792ff4d.
2023-08-07 20:44:33 +10:00
5e1792ff4d Use finer-grained control for truststore 2023-08-07 19:15:14 +10:00
a2628042e8 Use truststore in appliance_manager.py instead 2023-08-07 19:04:55 +10:00
e616b09028 Use truststore 2023-08-07 18:40:03 +10:00
958865e919 Upgrade dependencies 2023-08-06 20:39:27 +10:00
7f595b089b Use certifi to get SSL root certificates 2023-08-06 20:37:10 +10:00
a76d7576c6 Bump version to 2.2.42.dev3 2023-08-06 18:14:21 +10:00
533c4544ea Use certifi on Windows and macOS 2023-08-06 18:09:56 +10:00
04a302a389 Merge pull request #2265 from GNS3/use-bundled-cacert
Use bundled cacert file for frozen app
2023-08-05 22:37:06 +10:00
5da742394e Use bundled cacert file on Windows and macOS 2023-08-05 22:21:08 +10:00
962c5eed8e Add log message for comparing controller version with config version. Ref https://github.com/GNS3/gns3-gui/issues/3486 2023-08-05 20:24:11 +10:00
f4dab1482f Bump version to 2.2.42.dev2 2023-08-05 20:16:30 +10:00
05d594f3cf Upgrade FastAPI version to 0.101.0 2023-08-05 20:13:30 +10:00
d44f6eb2f1 Merge pull request #2262 from GNS3/fix/2257
Support for Pydantic v2
2023-08-04 18:27:18 +10:00
9c456532d0 Pydantic v2 migration 2023-08-04 18:20:06 +10:00
7ef9b7600c Use DEFAULT_BUFFER_SIZE for md5sum 2023-08-03 12:09:24 +10:00
11f51b3809 Allow connection to ws console over IPv6 2023-08-02 18:29:01 +10:00
1d59afa9fb Merge branch '2.2' into 3.0
# Conflicts:
#	gns3server/controller/__init__.py
#	gns3server/crash_report.py
#	gns3server/handlers/api/controller/node_handler.py
#	gns3server/utils/images.py
2023-08-02 18:26:50 +10:00
c51d71a4fa Fix version check when installing appliances. Ref https://github.com/GNS3/gns3-gui/issues/3486 2023-08-02 17:58:53 +10:00
af83adfdf6 Add debug message when calculating MD5 checksum 2023-08-02 17:41:51 +10:00
658bfb7624 Allow connection to ws console over IPv6. Fixes https://github.com/GNS3/gns3-web-ui/issues/1400 2023-08-02 15:28:32 +10:00
88a1cef21f Revert "Support for Python 3.12"
This reverts commit 1ced862c08.
2023-07-30 17:50:25 +10:00
1ced862c08 Support for Python 3.12 2023-07-30 17:48:30 +10:00
6097166055 Remove import urllib3 and let sentry_sdk import and patch it. Fixes https://github.com/GNS3/gns3-gui/issues/3498 2023-07-30 17:42:52 +10:00
46b8ee3279 Downgrade sqlalchemy 2023-07-23 12:56:30 +10:00
d82a07600b Downgrade uvicorn 2023-07-23 12:50:29 +10:00
2991899d50 Upgrade dependencies 2023-07-23 12:47:34 +10:00
9238c52f97 Merge branch '2.2' into 3.0 2023-07-23 12:36:35 +10:00
38ee79c15f Merge remote-tracking branch 'origin/master' into 2.2 2023-07-23 12:35:46 +10:00
3f691b60c9 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3server/compute/qemu/qemu_vm.py
#	gns3server/crash_report.py
#	gns3server/schemas/qemu.py
#	gns3server/schemas/qemu_template.py
#	gns3server/static/web-ui/index.html
#	gns3server/static/web-ui/main.1379a5647e8bc6d3e401.js
#	gns3server/version.py
#	tests/compute/qemu/test_qemu_vm.py
2023-07-13 10:09:26 +10:00
a6ca7c4e36 Development on 2.2.42.dev1 2023-07-12 18:26:26 +10:00
b76d2c2150 Merge pull request #2258 from GNS3/release-v2.2.41
Release v2.2.41
2023-07-12 18:24:40 +10:00
d2a0632ec4 Release v2.2.41 2023-07-12 17:07:39 +10:00
fde5f5cbe2 Merge remote-tracking branch 'origin/master' into 2.2 2023-07-12 17:04:12 +10:00
7dc5628845 Bundle web-ui v2.2.41 2023-07-12 16:31:54 +10:00
723b519e8f Sync appliance files 2023-07-12 16:20:29 +10:00
9c653f7dbf Bump version to 2.2.41.dev3 2023-07-12 13:26:22 +10:00
c226b2a9ef Catch urllib3 exceptions when sending crash report. Ref https://github.com/GNS3/gns3-gui/issues/3483 2023-07-06 17:16:05 +10:00
c5c4e1ad56 Merge pull request #2255 from GNS3/fix/2243
Only add speed/duplex params when using Qemu >= v2.12
2023-07-06 16:43:54 +10:00
8e2989d747 Fix issue with tests and Qemu version on Windows 2023-07-06 16:36:36 +10:00
c181df6935 Only fetch Qemu version once when starting Qemu + only add speed/duplex for virtio-net-pci with Qemu version >= 2.12 2023-07-06 16:29:55 +10:00
24c03406e7 Merge pull request #2253 from GNS3/uefi-boot-mode
Bundle recent OVMF firmware and improve UEFI boot mode
2023-07-04 13:38:47 +10:00
6c8c5c1787 Ignore uefi test on Windows 2023-07-04 13:31:21 +10:00
f541c03b94 Fix uefi test on Windows 2023-07-04 13:22:18 +10:00
0a14a08d54 Use recent OVMF firmware (stable-202305) and use flash drives to configure Qemu command line 2023-07-04 13:06:34 +10:00
9357ac6035 Add builtin field in Appliance model 2023-07-03 21:02:17 +10:00
785eff869d Fix issue with appliance API returning unset data 2023-07-03 18:55:19 +10:00
24db2a039d Upgrade FastAPI to v0.99.1 2023-07-03 17:12:46 +10:00
bfb317a302 Upgrade dependencies 2023-06-30 17:47:55 +10:00
29b4e89d21 Merge pull request #2252 from Raizo62/rights_empty8G.qcow2
- remove executable permissions to empty8G.qcow2
2023-06-28 19:55:56 +09:30
bbb68cb148 - remove the useless executable permissions to the file gns3server/disks/empty8G.qcow2
https://github.com/GNS3/gns3-gui/issues/3491
2023-06-28 08:30:13 +02:00
427bbc40b9 Fix websocket authentication after upgrade to FastAPI 0.97.0 + tests 2023-06-24 14:55:43 +09:30
4b791d4924 Fix websocket compute notifications after upgrade to FastAPI 0.97.0 2023-06-23 23:26:08 +09:30
6f132a2e09 Merge pull request #2248 from GNS3/backport-uefi-boot-mode
Backport UEFI boot mode support for Qemu VMs
2023-06-23 11:31:52 +09:30
d5cfb85de5 Backport UEFI boot mode support for Qemu VMs 2023-06-23 11:18:25 +09:30
d366d77ff7 Merge pull request #2247 from GNS3/dynamic-compute-allocation
Allow computes to be dynamically or manually allocated
2023-06-21 22:41:56 +09:30
21049d73a5 Allow computes to be dynamically or manually allocated 2023-06-21 22:28:09 +09:30
c290d75eec Merge pull request #2246 from GNS3/add-uefi-boot-mode
Add UEFI boot mode option for Qemu VMs
2023-06-21 18:12:31 +09:30
40c265152b Add UEFI boot mode option for Qemu VMs 2023-06-21 17:56:34 +09:30
e5eeab662e Check if server config file is readable 2023-06-20 21:26:21 +09:30
a7a946c067 Upgrade dev dependencies 2023-06-20 16:17:13 +09:30
b762d1e60d Upgrade dependencies 2023-06-20 16:13:35 +09:30
1ff23348d3 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3server/compute/base_node.py
#	gns3server/compute/docker/__init__.py
#	gns3server/compute/qemu/qemu_vm.py
#	gns3server/controller/compute.py
#	gns3server/controller/gns3vm/virtualbox_gns3_vm.py
#	gns3server/controller/node.py
#	gns3server/controller/project.py
#	gns3server/crash_report.py
#	gns3server/handlers/api/controller/template_handler.py
#	gns3server/static/web-ui/index.html
#	gns3server/static/web-ui/main.8448c96e4facbe79a613.js
#	gns3server/version.py
#	tests/compute/iou/test_iou_vm.py
#	tests/compute/qemu/test_qemu_vm.py
#	tests/handlers/api/controller/test_template.py
2023-06-20 16:06:53 +09:30
3dbc714f37 Developement on v2.2.41.dev2 2023-06-10 21:40:39 +09:30
682af449f8 Merge pull request #2240 from GNS3/release-v2.2.40.1
Release v2.2.40.1
2023-06-10 21:36:51 +09:30
60f1d76eab Release v2.2.40.1 2023-06-10 20:06:40 +09:30
16b619066a Merge branch 'master' into 2.2 2023-06-10 17:22:33 +09:30
d44038305f Add missing web-ui files. Fixes #2239 2023-06-10 15:50:28 +09:30
3641742930 Development on v2.2.41.dev1 2023-06-06 12:43:20 +09:30
4bf7838543 Merge pull request #2238 from GNS3/release-v2.2.40
Release v2.2.40
2023-06-06 12:41:40 +09:30
998fe6d8a4 Merge branch 'master' into release-v2.2.40 2023-06-06 12:28:30 +09:30
aa79f4cb25 Release v2.2.40 2023-06-06 10:23:42 +09:30
2602602193 Merge branch 'master' into 2.2 2023-06-06 10:20:07 +09:30
8d969349c8 Bundle web-ui v2.2.40 2023-06-06 10:18:18 +09:30
bf67fec242 Sync appliances 2023-06-06 10:07:50 +09:30
a070a72f66 Merge pull request #2235 from Raizo62/virtio-net-pci
qemu : with "virtio-net-pci", fix the speed and duplex
2023-06-03 21:03:04 +09:30
71d1aefb65 Test Qemu command with virtio-net-pci adapter 2023-06-03 20:57:23 +09:30
0b6436d2cb qemu : with network adapter_type equal to "virtio-net-pci", fix the speed to 10000 and duplex to full.
The values are actually fake.
	(https://github.com/GNS3/gns3-gui/issues/3476)
2023-06-03 11:48:55 +02:00
77f5044aec Merge pull request #2234 from GNS3/fix-tests
Fix tests
2023-06-02 20:32:25 +09:30
088ae699de Merge pull request #2223 from GNS3/fix/2214
Use proc.communicate() when checking for subprocess output
2023-06-02 20:29:40 +09:30
21dc2a8dbc Fix tests 2023-06-02 20:25:33 +09:30
aae7bf9865 Merge pull request #2231 from GNS3/fix/3452
Fix Docker + VNC issues
2023-06-02 20:24:55 +09:30
4338afab7f Fix tests 2023-06-02 20:16:06 +09:30
ef62eba4e8 Merge pull request #2233
Fix unexpected indent
2023-06-02 20:08:55 +09:30
cc4ead40e2 Fix unexpected indent 2023-06-02 20:08:21 +09:30
67943c4727 Merge pull request #2232 from Mlastawi/master
Parse name for request to node creation from template
2023-06-02 18:16:29 +09:30
045c0c4c14 Add missing comma 2023-06-02 10:38:42 +02:00
a0943b0b5a Parse name for request to node creation from template 2023-06-02 09:39:08 +02:00
84c886b058 Remove left over Xvfb related code 2023-05-31 21:33:39 +09:30
e59b5fb95b Remove Xvfb + x11vnc support 2023-05-31 21:28:06 +09:30
cc4d6759e3 Merge pull request #2229 from GNS3/fix/3472
Fix support for the GNS3 VM on macOS with VirtualBox 7
2023-05-31 20:46:39 +09:30
e45467af57 Use the correct VirtualBox host-only type 2023-05-31 20:39:25 +09:30
c6d6fcfe66 Search for correct VirtualBox network type 2023-05-31 20:19:13 +09:30
73bcd019cc Require a Host-Only Network to start the VirtualBox GNS3 VM on macOS with VirtualBox 7 2023-05-30 21:22:57 +09:30
2361e138ec Properly catch aiohttp client exception. Ref #2228 2023-05-30 16:17:12 +09:30
0f02cff5f2 Catch ConnectionResetError when waiting for the wrap console 2023-05-25 18:29:19 +08:00
72af145a23 Fix open IPv6 address for HTTP consoles on controller. Fixes https://github.com/GNS3/gns3-gui/issues/3448 2023-05-22 19:42:01 +08:00
8cc0244c5a Upgrade dependencies 2023-05-22 15:21:57 +08:00
b3dfb87622 Merge branch '2.2' into 3.0
# Conflicts:
#	gns3server/controller/__init__.py
#	gns3server/crash_report.py
#	gns3server/static/web-ui/index.html
#	gns3server/static/web-ui/main.96be36058f5df0ca7e7f.js
#	gns3server/utils/images.py
#	gns3server/version.py
#	requirements.txt
#	tests/compute/docker/test_docker_vm.py
#	tests/controller/test_controller.py
2023-05-22 15:11:17 +08:00
d03e7ac9c5 Fix tests 2023-05-14 14:20:45 +08:00
af2fc8c111 Use proc.communicate() when checking for subprocess output
As recommended in https://docs.python.org/3/library/asyncio-subprocess.html#asyncio.subprocess.Process.stderr
2023-05-14 13:58:50 +08:00
25c03b7823 Merge pull request #2220 from GNS3/release-v2.2.39
Release v2.2.39
2023-05-08 20:27:28 +08:00
b4bfb24a80 Development on v2.2.40.dev1 2023-05-08 20:26:40 +08:00
d6ea546ff7 Release v2.2.39 2023-05-08 19:17:02 +08:00
68f9c55f3d Install web-ui v2.2.39 2023-05-08 19:10:22 +08:00
9868c28bc6 Merge pull request #2219 from GNS3/install-qemu-empty-disks
Install empty Qemu disks
2023-05-08 16:58:26 +08:00
f3f7921525 Add generic function to install resource files 2023-05-07 21:57:44 +08:00
65cc12c850 Sync appliance files 2023-05-05 22:46:42 +08:00
161f62d083 Install empty Qemu disks on first start 2023-05-05 22:40:58 +08:00
af2b5c3448 Upgrade dependencies 2023-04-30 23:17:01 -10:00
1591ca114d Upgrade dependencies 2023-04-23 05:33:12 -10:00
d6141d4652 Fix checking for ":" in project dir on Windows for Docker containers 2023-03-23 22:17:29 -10:00
8bf5eb2754 Upgrade sentry-sdk 2023-03-23 21:41:50 -10:00
ce220e5ae4 Merge pull request #2211 from eantowne/master
remote-install.sh: added variable declaration
2023-03-24 03:03:12 +08:00
791ce6a56e Added declaration for in remote-install.sh to resolve 'unary operator operator expected' error 2023-03-23 07:59:20 -04:00
66157a335d Merge pull request #2207 from GNS3/pyproject-migration
Support for pyproject.toml
2023-03-21 18:00:13 +08:00
513bc0fb16 Do not use setup.py directly 2023-03-21 18:56:00 +10:00
ffe35b6559 Migrate to pyproject.toml 2023-03-21 17:41:01 +10:00
f08ce9d3f1 Check for colon in project name. Fixes #2203 2023-03-19 18:26:26 +10:00
2bb71838ac Merge pull request #2204 from GNS3/fix/2203
Allow ':' in project name when using Docker containers
2023-03-19 16:00:04 +08:00
f347e21100 Fix Docker tests 2023-03-19 17:56:07 +10:00
3fb138b9a1 Allow ':' in project name when Docker containers are used 2023-03-19 17:29:29 +10:00
9de847fc45 Upgrade Fastapi to v0.95.0 2023-03-19 11:18:58 +10:00
ea550508f6 Fix uvicorn dependency version 2023-03-17 17:48:14 +10:00
267c4cbbbb Merge branch '2.2' into 3.0
# Conflicts:
#	README.md
#	gns3server/compute/base_node.py
#	gns3server/compute/dynamips/__init__.py
#	gns3server/compute/dynamips/hypervisor.py
#	gns3server/compute/qemu/__init__.py
#	gns3server/compute/qemu/qemu_vm.py
#	gns3server/controller/__init__.py
#	gns3server/crash_report.py
#	gns3server/handlers/api/controller/node_handler.py
#	gns3server/schemas/qemu_template.py
#	gns3server/static/web-ui/index.html
#	gns3server/static/web-ui/main.11410ae4eaf4d4c08cd0.js
#	gns3server/version.py
#	requirements.txt
2023-03-17 17:44:32 +10:00
c33a13fd45 Upgrade dependencies 2023-03-17 16:23:48 +10:00
ceb8208002 Upgrade distro and aiohttp dependencies 2023-03-01 18:03:28 +10:00
42767eff56 Development on 2.2.39.dev1 2023-02-28 17:09:39 +10:00
0406083991 Merge pull request #2192 from GNS3/2.2
Release v2.2.38
2023-02-28 14:41:03 +08:00
91dba66800 Release v2.2.38 2023-02-28 15:35:17 +10:00
69b741a01e Sync appliance files 2023-02-28 15:31:02 +10:00
8903ab8a47 Bundle web-ui v2.2.38 2023-02-28 15:29:17 +10:00
4d62c6943b Merge branch 'master' into 2.2 2023-02-28 15:07:48 +10:00
2a66f67b5f Merge remote-tracking branch 'origin/2.2' into 2.2 2023-02-27 22:59:08 +10:00
8ef59c7832 Fix c7200_i0_log.txt is created in the current directory. Fixes #2191 2023-02-27 22:58:52 +10:00
cbdab1f0d7 Merge pull request #2190 from GNS3/check_tpm
Check swtpm version and start it before qemu
2023-02-26 18:59:08 +08:00
7968ee7ff2 Check swtpm version and start swtpm before qemu 2023-02-26 20:51:24 +10:00
be557abba9 Merge pull request #2185 from Xatrekak/master
Update remote-install.sh and add welcome.py
2023-02-19 13:02:54 +08:00
3040e87a7d Merge pull request #2189 from GNS3/fix/2188
Fix broken websocket console with Python 3.11
2023-02-18 15:44:06 +08:00
1f85abb036 Fix broken websocket console with Python 3.11 2023-02-18 15:32:57 +08:00
d867e1f1c1 Fix tests 2023-02-17 21:25:19 +08:00
1490e458b6 Upgrade fastapi to v0.92.0. Fixes #2186 2023-02-17 18:52:18 +08:00
30c85703c8 Attempt to fix "cannot reopen console". Ref #2182 2023-02-13 19:32:44 +08:00
4bdd405bc5 Merge branch 'final_stacked_merge'
This updates the script to assume that welcome.py has been merged into the GNS3 master repo
2023-02-12 18:46:38 -05:00
229168367b Merge branch 'remote_install_changes'
Changes to remote-install to make it compatible with the latest versions of ubuntu server. Also adds an option to install the welcome dialog from gns3vm
2023-02-12 18:44:12 -05:00
e257fb425e This commit is stacked and assumes welcome.py has already been merged into the GNS3 master repo 2023-02-12 18:30:01 -05:00
fac224ac48 Updated to latest welcome.py commit. Raw cache still bugged 2023-02-12 17:26:53 -05:00
62c2ca9be1 Added Web UI to Dialog 2023-02-12 17:20:25 -05:00
f98a60fc74 added an OS update to end when using welcome option 2023-02-12 14:42:53 -05:00
42a5f1956e Changed mechanism used to set gns3 user password that works on all shells. 2023-02-12 14:21:01 -05:00
13df828ca7 switched to a direct link to the latest commit instead of the branch alias to bypass github raw caching issues. 2023-02-12 13:12:20 -05:00
19a8d1caed Update welcome.py 2023-02-12 12:59:06 -05:00
290df5290a fixed transcription 2023-02-12 02:48:17 -05:00
fbe10360c2 Finished gns3 user setup 2023-02-12 02:44:15 -05:00
ac042b02c4 changes self.d to self.display so it would be more intuitive. 2023-02-12 02:30:28 -05:00
e44999f588 Refactored welcome.py to use a class so its functions would be callable by remote-install.sh. This ensure the setup uses the same IP address that will be displayed by Dialog. 2023-02-12 02:07:18 -05:00
952b4e7249 fixed some shell bugs 2023-02-11 23:24:23 -05:00
3b0336bd7e fixed typo 2023-02-11 22:23:45 -05:00
7689b7841f switched to modifed welcome by and full path to it 2023-02-11 21:00:37 -05:00
bf5970b904 Update install and welcome to support running it 2023-02-11 19:57:54 -05:00
2eca92e34d The legacy get_ip function no longer worked on new versions of ubuntu LTS. 2023-02-11 18:05:03 -05:00
70d3f991ed Add welcome.py script from GNS3vm 2023-02-11 17:57:12 -05:00
77b6ef1a1b Merge pull request #2179 from GNS3/vmware-virtualbox-deprecated
Mark VMware and VirtualBox support as deprecated
2023-02-06 06:26:28 +05:45
aaa6ca9445 Mark VMware and VirtualBox support as deprecated 2023-02-05 10:09:55 +08:00
2b6bec1b93 Merge pull request #2178 from GNS3/fix/2177
Fix Qemu binary not set when adding appliance from template
2023-02-04 10:12:30 +05:45
0f49911432 Fix Qemu binary not set when adding appliance from template 2023-02-04 12:18:52 +08:00
9c158848bd Make port name for custom adapters optional. Fixes https://github.com/GNS3/gns3-web-ui/issues/1430 2023-02-01 16:19:39 +08:00
3a6ff9b44f Add long description content type in setup.py 2023-02-01 09:56:02 +08:00
83cb8693e6 Automatically add new issues to GNS3 project 2023-01-31 09:31:52 +08:00
e5ecdd270d Development 2.2.38.dev1 2023-01-25 18:36:39 +08:00
260ac44e87 Release v2.2.37 2023-01-25 15:06:12 +08:00
1fa4632b55 Merge branch 'master' into 2.2 2023-01-25 14:44:54 +08:00
a0cf711634 Bump version to 2.2.37.dev3 2023-01-25 14:13:07 +08:00
b83645c531 Add web-ui v2.2.37 2023-01-24 15:06:21 +08:00
a7daae1c6a Sync appliances 2023-01-24 14:57:58 +08:00
a7d19fd89a Fix StreamWriter doesn't have the wait_closed() method in Python3.6. Fixes #2170 2023-01-20 13:14:57 +08:00
3c2ae5363c Install built-in appliances when no previous version has been detected. Fixes #2168 2023-01-17 13:39:01 +08:00
24d1aeee32 Bump version to 2.2.37.dev2 2023-01-17 13:30:09 +08:00
8d56b7b18c Fix sync_appliances.sh 2023-01-17 13:11:56 +08:00
90d7478679 Sync appliances 2023-01-17 13:11:18 +08:00
fb4c04ac41 Update documentation to install gns3-server. Fixes #2124 2023-01-17 09:45:14 +08:00
dc1b98a1d0 Merge pull request #2166 from GNS3/fix/2165
Find Dynamips version before hypervisor is started
2023-01-17 07:15:33 +05:45
c57b0cbb53 Find Dynamips version before hypervisor launch and do not require Dynamips v0.2.23 2023-01-16 18:04:46 +08:00
ab1f9a4385 Handle Alembic CommandError exceptions 2023-01-13 12:52:31 +08:00
ce3bf9930c Merge pull request #2160 from GNS3/db-migrations
Support for database schema migrations
2023-01-11 08:21:44 +05:45
a033080418 Support for database schema migrations using alembic 2023-01-11 10:15:04 +08:00
af9b883c08 Fix and adjustments after merge 2023-01-10 12:09:36 +08:00
f09594ae4c Merge branch '2.2' into 3.0
# Conflicts:
#	README.md
#	README.rst
#	tests/handlers/api/compute/test_nat.py
2023-01-10 11:52:58 +08:00
5a3b502024 Fix NIO update UDP test 2023-01-10 11:39:18 +08:00
997e0ad6ee Fix more tests 2023-01-10 11:22:12 +08:00
eb3fc1bb3a Fix more tests 2023-01-10 11:09:27 +08:00
e9c95ff299 Fix tests 2023-01-10 10:07:26 +08:00
78c301653f Convert README to Markdown 2023-01-10 08:23:06 +08:00
2a392ef991 Merge branch '2.2' into 3.0 2023-01-06 20:09:41 +08:00
535f89e29e Give udhcpc executable right. Fixes #2159 2023-01-06 20:09:17 +08:00
72e3d8f0c1 Fix tests after merge 2023-01-05 12:57:00 +08:00
27d9063e56 Merge 2.2 2023-01-05 12:38:00 +08:00
f8f5d7ec07 Development on 2.2.37.dev1 2023-01-05 09:13:30 +08:00
d0141c351b Merge branch '2.2' 2023-01-05 09:12:11 +08:00
8e2992fbc7 Release v2.2.36 2023-01-04 19:46:30 +08:00
8986f10506 Install web-ui v2.2.36 2023-01-04 19:05:24 +08:00
10f3adcb60 Sync appliance files 2023-01-04 18:28:14 +08:00
5459543eb5 Fix issue when detecting Dynamips version (version is not set until after Dynamips has started) 2023-01-04 18:21:17 +08:00
e15c36f05c Merge remote-tracking branch 'origin/2.2' into 2.2 2023-01-04 15:12:26 +08:00
da7c7d16e4 Fix starting Dynamips on Windows 2023-01-04 15:12:09 +08:00
8d69f7f792 Merge pull request #2157 from GNS3/qemu-tpm-support
Trusted Platform Module (TPM) support for Qemu VMs
2023-01-04 10:55:52 +05:45
297ada529c Prevent TPM to run on Windows 2023-01-04 12:57:48 +08:00
ae200d9add Add Trusted Platform Module (TPM) support for Qemu VMs 2023-01-04 12:13:19 +08:00
eb1b70456f Merge pull request #2155 from GNS3/fix/2143
Binding address for console
2023-01-04 07:00:33 +05:45
9132002b80 Fix typos 2023-01-04 09:08:40 +08:00
771a9a5ddb Require Dynamips 0.2.23 and bind Dynamips hypervisor on 127.0.0.1 2023-01-02 15:26:59 +08:00
04ba3b6549 Merge pull request #2154 from GNS3/fix/2069
Use a stock BusyBox for the Docker integration
2023-01-01 16:18:37 +05:45
8f9800f444 Update README.rst about static busybox 2023-01-01 18:24:42 +08:00
90c971ed74 Merge pull request #2152 from GNS3/fix/2151
Install built-in appliance files at the first start of a newer version of the server
2023-01-01 15:40:48 +05:45
d2ad9dc5e2 Delete the built-in appliance directory before installing updated files 2023-01-01 17:49:00 +08:00
e5c8ae4bde Use a stock BusyBox for the Docker integration 2023-01-01 17:04:48 +08:00
5bccf4841d Overwrite built-in appliance files when starting a more recent version of the server 2023-01-01 15:57:41 +08:00
b3a6b9173b Fix reset console. Fixes #1619 2022-12-31 09:43:17 +08:00
2d6b260188 Revert "Install importlib-resources==1.3 with Python < 3.9"
This reverts commit 343022c63b.
2022-12-30 21:13:59 +08:00
c814245426 Revert "Change importlib-resources dependency to v1.4.0"
This reverts commit d787f38c21.
2022-12-30 21:13:49 +08:00
d787f38c21 Change importlib-resources dependency to v1.4.0 2022-12-30 21:09:54 +08:00
343022c63b Install importlib-resources==1.3 with Python < 3.9 2022-12-30 21:05:18 +08:00
1148dbc48e Fix issue when calling reset_console with running VPCS and Qemu nodes. Ref #1619 2022-12-30 20:54:37 +08:00
85679aaa94 Try importlib-resources for Python 3.9 2022-12-30 11:44:29 +08:00
5bcc247881 Make gns3server.appliances a package 2022-12-30 11:37:34 +08:00
b8d595928b Try to fix tests 2022-12-30 10:35:29 +08:00
3804249d89 Fix tests 2022-12-30 10:01:43 +08:00
c56a8ef8f7 Only use importlib_resources for Python < 3.9. Fixes #2147 2022-12-30 09:15:40 +08:00
076e85ddb3 Update sentry-sdk dependency 2022-12-28 15:13:26 +08:00
2550fb3495 Support when the user field defined in Docker container is an ID. Fixes #2134 2022-12-28 11:05:22 +08:00
f837912ebc Development on 3.0.0.dev7 2022-12-28 08:29:26 +08:00
830cc108d0 Release v3.0.0a3 2022-12-27 15:22:16 +08:00
36ffe2bb59 Sync appliances 2022-12-27 13:40:07 +08:00
f8739eb5e1 Merge branch '2.2' into 3.0
# Conflicts:
#	.github/workflows/testing.yml
#	gns3server/crash_report.py
#	gns3server/version.py
#	requirements.txt
2022-12-27 13:28:11 +08:00
5bc030688e Sync web-ui v3.0.0a3 2022-12-27 13:17:12 +08:00
9f75a49e0a Fix syntax error in .whitesource 2022-12-27 12:27:26 +08:00
7f440f43d8 Add more base branches for Mend to scan 2022-12-27 12:25:49 +08:00
3e29ae4276 Add config option to change the server name. Ref #2149 2022-12-27 10:05:13 +08:00
91b50eb5f2 Merge pull request #2146 from GNS3/fix/2126
Improve image discovery process
2022-12-26 10:23:57 +05:45
c6e31d98db Option to disable image discovery and do not scan parent directory 2022-12-26 12:32:40 +08:00
9a7b3bed25 Allow raw images by default. Fixes https://github.com/GNS3/gns3-server/issues/2097 2022-12-26 11:28:51 +08:00
8ad7b3f613 Fix bug when creating Dynamips router with chassis setting 2022-12-24 18:03:00 +08:00
00691390eb Merge pull request #2145 from GNS3/fix/2144
Stricter checks to create/update an Ethernet switch and add tests
2022-12-24 06:16:18 +05:45
17e0b2e259 Stricter checks to create/update an Ethernet switch and add tests 2022-12-24 08:20:51 +08:00
5666b43ee0 Fix schema for removing WICs from Cisco routers. Fixes #3392 2022-12-22 20:02:28 +08:00
84f256dd88 Downgrade to flake8 v5.0.4 2022-12-22 09:46:11 +08:00
1bbecf1ceb Update dependencies 2022-12-22 09:42:14 +08:00
72a4d0df4e Update GH actions checkout and setup-python 2022-12-22 09:35:45 +08:00
93449c942f Fix tests 2022-12-22 09:24:17 +08:00
649173b293 Merge remote-tracking branch 'origin/master' 2022-12-21 17:14:00 +08:00
1c183e660f Downgrade Ubuntu to 20.04 to fix missing Python 3.6 2022-12-21 17:12:09 +08:00
634e6a3549 Update SECURITY.md 2022-12-20 21:28:46 +08:00
a91ed34f43 Downgrade Ubuntu to 20.04 to fix missing Python 3.6 2022-12-18 14:24:50 +08:00
5209009df9 Upgrade dependencies 2022-12-18 14:14:56 +08:00
5ba526e175 Development on v2.2.36.dev2 2022-11-11 00:36:47 +08:00
d17c243b13 Release v2.2.35.1 2022-11-10 22:21:13 +08:00
9e4ae6bc24 Sync appliance files 2022-11-10 22:10:21 +08:00
4b410cfa42 Merge branch 'master' into 2.2 2022-11-10 22:06:09 +08:00
b9e415b6fe Push missing file for Web-Ui v2.2.35 2022-11-10 13:09:10 +08:00
d2d327cc76 Re-release Web-Ui v2.2.35 2022-11-10 12:38:33 +08:00
7643185fa2 Add missing importlib_resources 2022-11-09 23:14:38 +08:00
263febecbc Merge branch '2.2' into 3.0
# Conflicts:
#	.github/workflows/testing.yml
#	CHANGELOG
#	appveyor.yml
#	dev-requirements.txt
#	gns3server/compute/base_node.py
#	gns3server/controller/__init__.py
#	gns3server/controller/appliance_manager.py
#	gns3server/crash_report.py
#	gns3server/static/web-ui/index.html
#	gns3server/utils/get_resource.py
#	gns3server/version.py
#	gns3server/web/route.py
#	requirements.txt
#	tests/handlers/api/compute/test_qemu.py
#	win-requirements.txt
2022-11-09 20:30:28 +08:00
0a046e43d7 Development on 2.2.36.dev1 2022-11-09 20:02:20 +08:00
a7036d14d5 Merge branch '2.2' 2022-11-09 20:00:48 +08:00
1aca7dbe04 Release v2.2.35 2022-11-08 23:40:25 +08:00
338128ca3e Upgrade pywin32 to v305 2022-11-08 23:39:10 +08:00
e165cc48e3 Use Visual Studio 2022 in appveyor.yml 2022-11-08 23:09:23 +08:00
c80a55b18c Install appliances and configs without importlib_resources for Windows only 2022-11-08 23:07:45 +08:00
284a4b62fb Fix copying appliances and configs when app is frozen 2022-11-08 22:50:59 +08:00
d33fefa183 Merge branch 'master' into 2.2 2022-11-08 19:29:39 +08:00
dfb48c884b Release web-ui v2.2.35 2022-11-08 19:29:21 +08:00
18ba60d1a3 Downgrade psutil to v5.9.2 2022-11-08 19:18:07 +08:00
521132726e Upgrade psutil to v5.9.4 2022-11-08 18:49:31 +08:00
f80aca7633 Sync appliance files 2022-11-08 18:40:27 +08:00
84914ecfa4 Add debug messages to fix VMnet interface list refresh. Ref https://github.com/GNS3/gns3-gui/issues/3381 2022-11-08 12:54:47 +08:00
5696d1ccb7 Fix issues with VMnet interface on macOS >= 11.0. Ref #3381 2022-11-07 23:59:33 +08:00
18b0863ba8 Merge remote-tracking branch 'origin/2.2' into 2.2
# Conflicts:
#	requirements.txt
2022-11-07 21:50:59 +08:00
ece47dc279 Upgrade dependencies 2022-11-07 21:48:45 +08:00
6e1d49d8ca Merge pull request #2133 from GNS3/use-importlib-resources
Migrate to importlib_resources
2022-11-07 21:24:35 +08:00
2e550d839e Fix tests 2022-11-07 20:33:02 +08:00
a4b24eaceb Use importlib_resources instead of pkg_resources and install built-in appliances in config dir. 2022-11-07 20:12:03 +08:00
f04702d607 Fix console vnc don't use configured ports in some case. Fixes #2111 2022-11-06 19:36:31 +08:00
6f11df6189 Add missing VMware settings in gns3_server.conf 2022-11-06 19:30:24 +08:00
9cf2e4f5a6 Make version PEP 440 compliant 2022-11-06 17:51:31 +08:00
9316876f97 Merge pull request #2123 from blueneekone/patch-1
Update gns3.service.systemd
2022-11-04 23:01:57 +08:00
f3b8f43689 Fix creating asyncio task in Python 3.6 2022-10-30 22:21:28 +08:00
2d74d1ad94 Fix tests for Python 3.11 2022-10-30 22:07:44 +08:00
ec50cc7c0d Support for Python 3.11 2022-10-30 19:04:54 +08:00
f8ee3b3d50 Upgrade pywin32 to v304 2022-10-19 18:31:17 +08:00
bbf9c04d20 Upgrade pywin32 to v304 2022-10-19 18:30:28 +08:00
67e42f9353 Merge pull request #2128 from GNS3/fix/3393
Update requirements.txt
2022-10-18 22:31:56 +08:00
31a490211b Merge branch '2.2' into fix/3393 2022-10-18 22:31:04 +08:00
c7df8331ed Upgrade to Visual Studio 2022 in appveyor.yml 2022-10-18 21:47:10 +08:00
de1654a50f Upgrade pip and setuptools in appveyor.yml 2022-10-18 21:40:08 +08:00
a6959de1f8 Upgrade pytest. Fixes #2130 2022-10-18 21:14:53 +08:00
8cfedce468 Use jsonschema v3.2.0 for Python 3.6 2022-10-12 22:13:34 +08:00
77dd772314 Allow for more dependency versions at patch level 2022-10-12 22:07:37 +08:00
9ff302592c Upgrade to aiohttp 3.8.3 2022-10-12 21:40:23 +08:00
6b3d4feab8 Replace deprecated distro.linux_distribution() call 2022-10-11 23:28:11 +08:00
78b2b93540 Update dev-requirements.txt 2022-10-11 23:23:15 +08:00
0303e21059 Update requirements.txt 2022-10-11 23:01:24 +08:00
e20d1adb0e Downgrade aiohttp to v3.8.1 2022-10-10 16:30:28 +08:00
e5bee8ff52 Downgrade Jinja2 to support Python 3.6 2022-10-10 15:52:36 +08:00
58471ea7f4 Downgrade aiofiles to support Python 3.6 2022-10-10 15:50:35 +08:00
9fb0ba5a71 Upgrade dependencies 2022-10-10 14:29:04 +08:00
3d738e83f5 Update gns3.service.systemd
In reference to bug #1918, the gns3.service.systemd file references ExecPath /usr/bin/gns3server which needs to be changed to /usr/local/bin/gns3server for the daemon/service to run without failure.

Please see the change I made to the service above.
2022-10-02 23:25:58 -06:00
19de2732f2 Upgrade to FastAPI 0.85.0 and check embedded web-ui index.html can be found. 2022-09-29 15:11:15 +02:00
40c22e389e Merge branch 'master' into 2.2 2022-09-29 14:56:35 +02:00
0419c081fe Merge pull request #2083 from a60814billy/fix/binding-on-wrong-interface-in-windows
fix: binding to wrong interface in windows
2022-09-28 17:36:31 +02:00
dced70a565 Merge branch 'master' into 2.2 2022-09-27 11:55:53 +02:00
9a7222b83e Upgrade FastAPI to v0.84.0 2022-09-14 21:09:29 +02:00
b0e646b97d Fix some issues with HTTP notification streams 2022-09-13 22:10:01 +02:00
e8973b9c44 Development on 3.0.0dev5 2022-09-07 01:16:14 +02:00
eeae27ae0d Merge branch '2.2' 2022-08-29 11:11:29 +02:00
3634cc8307 Merge pull request #2094 from mm1ke/master
gns3.service.openrc: make openrc script posix compliant
2022-08-08 19:29:30 +02:00
3b108563a0 gns3.service.openrc: make openrc script posix compliant 2022-08-08 19:23:29 +02:00
790bf4521d fix: use exact match to find interface in windows to avoid get wrong interface 2022-07-06 14:22:11 +08:00
388 changed files with 11591 additions and 3254 deletions

View File

@ -0,0 +1,16 @@
name: Add new issues to GNS3 project
on:
issues:
types:
- opened
jobs:
add-to-project:
name: Add issue to project
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@v0.4.0
with:
project-url: https://github.com/orgs/GNS3/projects/3
github-token: ${{ secrets.ADD_NEW_ISSUES_TO_PROJECT }}

View File

@ -12,11 +12,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: 0
ref: "gh-pages"
- uses: actions/setup-python@v2
- uses: actions/setup-python@v3
with:
python-version: 3.7
- name: Merge changes from 3.0 branch
@ -24,13 +24,10 @@ jobs:
git config user.name github-actions
git config user.email github-actions@github.com
git merge origin/3.0 -X theirs
- name: Install dependencies
- name: Install GNS3 server and dependencies
run: |
python -m pip install --upgrade pip
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Install GNS3 server
run: |
python setup.py install
python -m pip install .
- name: Generate the API documentation
run: |
cd scripts

View File

@ -17,12 +17,12 @@ jobs:
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Display Python version
@ -30,10 +30,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -r dev-requirements.txt
- name: Install Windows dependencies
if: matrix.os == 'windows-latest'
run: python -m pip install -r win-requirements.txt
python -m pip install .[dev]
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names

View File

@ -2,7 +2,8 @@
"scanSettings": {
"configMode": "AUTO",
"configExternalURL": "",
"projectToken" : ""
"projectToken" : "",
"baseBranches": ["master", "2.2", "3.0"]
},
"checkRunSettings": {
"vulnerableCheckRunConclusionLevel": "failure"

View File

@ -1,2 +0,0 @@
Jeremy Grossmann
Julien Duponchelle

237
CHANGELOG
View File

@ -1,5 +1,242 @@
# Change Log
## 3.0.0b1 27/11/2023
* Bundle web-ui v3.0.0b1
* Upgrade sentry-sdk to v1.37.1
* Upgrade aiohttp to v3.9.1
* Fix bug when listing endpoints for opened project
* Make images executable after importing a project
* Disable IOS hostname check for Dynamips ghost instances
## 3.0.0a6 15/11/2023
* Bundle web-ui v3.0.0a6
* Upgrade to aiohttp v3.9.0rc0
* Install Docker resources in writable location
* Default compute username is "gns3"
* Non-blocking checksums computation when server starts. Fixes #2228
* Fix timeout issue when creating Qemu disk image. Fixes https://github.com/GNS3/gns3-server/issues/2313
* Fix broken link to Web UI in 3.0 branch. Fixes #2312
* Fix sample config: VMware section declared twice. Fixes #2311
* Fix ws console and packet capture over SSL
* Support for web socket console over HTTPS
* Allow disabling hardware virtualization check
## 2.2.44.1 07/11/2023
* Catch exceptions when computing image checksums. Ref https://github.com/GNS3/gns3-server/issues/2228
* Add freeze_support() for multiprocessing
## 2.2.44 06/11/2023
* Bundle web-ui v2.2.44
* Non-blocking checksums computation when server starts. Fixes #2228
* Fix timeout issue when creating Qemu disk image. Fixes https://github.com/GNS3/gns3-server/issues/2313
* Support for web socket console over HTTPS
* Add back script create_cert.sh
* Allow disabling hardware virtualization check
* Fix L2IOU "failed code signing checks" when IOU base file name is >= 63 characters
* Change "ip cef" to "no ip cef" in IOU default configs. Fixes #2298
* Add Qemu IGB network device
* Add Python 3.12 support.
* Fix issue with importlib.resources.files() and Python 3.9
## 3.0.0a5 27/10/2023
* Bundle web-ui v3.0.0a5
* Fix L2IOU "failed code signing checks" when IOU base file name is >= 63 characters
* Python 3.12 support
* Add igb Qemu adapter
* Change "ip cef" to "no ip cef" in IOU default configs. Fixes #2298
* Drop support for Python 3.7 and upgrade dependencies
* Fix compute authentication for websocket endpoints
* Add Qemu IGB network device
## 3.0.0a4 18/10/2023
* Bundle web-ui v3.0.0a4
* Do not enforce Compute.Audit and Template.Audit privileges due to current web-ui limitations
* Support to create empty disk images on the controller
* Fix issue with importlib.resources.files() and Python 3.9
* New RBAC system with resource pools support.
* Use controller vars file to store version and appliance etag
* Pydantic v2 migration
* Allow connection to ws console over IPv6
* Allow computes to be dynamically or manually allocated
* Add UEFI boot mode option for Qemu VMs
* Mark VMware and VirtualBox support as deprecated
* Make port name for custom adapters optional. Fixes https://github.com/GNS3/gns3-web-ui/issues/1430
* Support for database schema migrations using alembic
* Add config option to change the server name. Ref #2149
* Option to disable image discovery and do not scan parent directory
* Allow raw images by default. Fixes https://github.com/GNS3/gns3-server/issues/2097
* Fix bug when creating Dynamips router with chassis setting
* Stricter checks to create/update an Ethernet switch and add tests
* Fix schema for removing WICs from Cisco routers. Fixes #3392
* Fix some issues with HTTP notification streams
* API endpoint to get the locked status of a project
* Global project lock and unlock
* Require name for custom adapters. Fixes #2098
* Allow empty adapter slots for Dynamips templates. Ref https://github.com/GNS3/gns3-gui/issues/3373
* Custom adapters should not be in node (compute) properties returned to clients. Fixes https://github.com/GNS3/gns3-gui/issues/3366
* Optionally allow Qemu raw images
* Ignore image detection for IOU user libraries in image directory
* Checks for valid hostname on server side for Dynamips, IOU, Qemu and Docker nodes
* Only check files (not directories) when looking for new images on file system.
* Support user defined loader/libraries to run IOU
* Remove explicit Response for VPCS endpoints returning HTTP 204 status code
* Remove explicit Response for endpoints returning HTTP 204 status code
* Make 'vendor_url' and 'maintainer_email' optional for template validation.
* Allow auth token to be passed as a URL param
* Add controller endpoints to get VirtualBox VMs, VMware VMs and Docker images
* Detect new images added to the default image directory. * Images can be present before the server starts or while it is running * Images are recorded in the database
* Support delete Qemu disk image from API Return the real disk image name in the 'hdx_disk_image_backed' property for Qemu VMs
* Fix ComputeConflictError import
* Handle creating Qemu disk images and resizing
* Finish to clean up local setting usage. Ref #1460
* "Local" command line parameter is only for stopping a server that has been started by the desktop GUI
* Fix AsyncSession handling after breaking changes in FastAPI 0.74.0 See https://github.com/tiangolo/fastapi/releases/tag/0.74.0 for details.
* Detect image type instead of requesting it from user
* Add connect endpoint for computes Param to connect to compute after creation Report compute unauthorized HTTP errors to client
* Replace CORS origins by origin regex
* Allow empty compute_id. Ref #1657
* Secure controller to compute communication using HTTP basic authentication
* Secure websocket endpoints
* Allocate compute when compute_id is unset
* Return the current controller hostname/IP from any compute
* Remove Qemu legacy networking support
* Appliance management refactoring: * Install an appliance based on selected version * Each template have unique name and version * Allow to download an appliance file
* Add isolate and unisolate endpoints. Ref https://github.com/GNS3/gns3-gui/issues/3190
* Allow images to be stored in subdirs and used by templates.
* Use uuid5 to create new compute_id. Fixes #1641 #1887
* Migrate PCAP streaming code to work with FastAPI.
* Refactor WebSocket console code to work with FastAPI. Fix endpoint routes.
## 2.2.43 19/09/2023
* Force English output for VBoxManage. Fixes #2266
* Automatically add vboxnet and DHCP server if not present for VirtualBox GNS3 VM. Ref #2266
* Fix issue with controller config saved before checking current version with previous one
* Prevent X11 socket file to be modified by Docker container
* Use the user data dir to store built-in appliances
* Catch ConnectionResetError exception when client disconnects
* Upgrade to PyQt 5.15.9 and pywin32
## 2.2.42 09/08/2023
* Bundle web-ui v2.2.42
* Handle API version key in VirtualBox 7. Fixes #2266
* Enable system certificate store for SSL connections
* Use DEFAULT_BUFFER_SIZE for md5sum
* Fix version check when installing appliances. Ref https://github.com/GNS3/gns3-gui/issues/3486
* Allow connection to ws console over IPv6. Fixes https://github.com/GNS3/gns3-web-ui/issues/1400
* Support for Python 3.12
* Remove import urllib3 and let sentry_sdk import and patch it. Fixes https://github.com/GNS3/gns3-gui/issues/3498
## 2.2.41 12/07/2023
* Bundle web-ui v2.2.41
* Catch urllib3 exceptions when sending crash report. Ref https://github.com/GNS3/gns3-gui/issues/3483
* Only fetch Qemu version once when starting Qemu + only add speed/duplex for virtio-net-pci with Qemu version >= 2.12
* Use recent OVMF firmware (stable-202305) and use flash drives to configure Qemu command line
* Remove the useless executable permissions to the file gns3server/disks/empty8G.qcow2
* Backport UEFI boot mode support for Qemu VMs
## 2.2.40.1 10/06/2023
* Re-bundle Web-Ui v2.2.40. Fixes #2239
## 2.2.40 06/06/2023
* qemu : with network adapter_type equal to "virtio-net-pci", fix the speed to 10000 and duplex to full. The values are actually fake. (https://github.com/GNS3/gns3-gui/issues/3476)
* Parse name for request to node creation from template
* Remove Xvfb + x11vnc support
* Require a Host-Only Network to start the VirtualBox GNS3 VM on macOS with VirtualBox 7
* Properly catch aiohttp client exception. Ref #2228
* Catch ConnectionResetError when waiting for the wrap console
* Fix open IPv6 address for HTTP consoles on controller. Fixes https://github.com/GNS3/gns3-gui/issues/3448
* Use proc.communicate() when checking for subprocess output As recommended in https://docs.python.org/3/library/asyncio-subprocess.html#asyncio.subprocess.Process.stderr
## 2.2.39 08/05/2023
* Install web-ui v2.2.39
* Add generic function to install resource files
* Install empty Qemu disks on first start
* Check for colon in project name. Fixes #2203
* Upgrade distro and aiohttp dependencies
## 2.2.38 28/02/2023
* Bundle web-ui v2.2.38
* Fix c7200_i0_log.txt is created in the current directory. Fixes #2191
* Check swtpm version and start swtpm before qemu
* Fix broken websocket console with Python 3.11
* Fix "cannot reopen console". Ref #2182
* Fix Qemu binary not set when adding appliance from template
## 2.2.37 25/01/2023
* Fix link communication issues on Windows with uBridge
* Fix StreamWriter doesn't have the wait_closed() method in Python3.6. Fixes #2170
* Install built-in appliances when no previous version has been detected. Fixes #2168
* Update documentation to install gns3-server. Fixes #2124
* Give udhcpc executable right. Fixes #2159
## 2.2.36 04/01/2023
* Install web-ui v2.2.36
* Add Trusted Platform Module (TPM) support for Qemu VMs
* Require Dynamips 0.2.23 and bind Dynamips hypervisor on 127.0.0.1
* Delete the built-in appliance directory before installing updated files
* Use a stock BusyBox for the Docker integration
* Overwrite built-in appliance files when starting a more recent version of the server
* Fix reset console. Fixes #1619
* Only use importlib_resources for Python <= 3.9. Fixes #2147
* Support when the user field defined in Docker container is an ID. Fixes #2134
## 3.0.0a3 27/12/2022
* Add web-ui v3.0.0a3
* Add config option to change the server name. Ref #2149
* Option to disable image discovery and do not scan parent directory
* Allow raw images by default. Fixes https://github.com/GNS3/gns3-server/issues/2097
* Fix bug when creating Dynamips router with chassis setting
* Stricter checks to create/update an Ethernet switch and add tests
* Fix schema for removing WICs from Cisco routers. Fixes #3392
* Fix issues with VMnet interface on macOS >= 11.0. Ref #3381
* Use importlib_resources instead of pkg_resources and install built-in appliances in config dir.
* Fix console vnc don't use configured ports in some case. Fixes #2111
* Add missing VMware settings in gns3_server.conf
* Make version PEP 440 compliant
* Support for Python 3.11
* Allow for more dependency versions at patch level
* Replace deprecated distro.linux_distribution() call
* Update gns3.service.systemd
* Fix some issues with HTTP notification streams
* gns3.service.openrc: make openrc script posix compliant
* fix: use exact match to find interface in windows to avoid get wrong interface
## 2.2.35.1 10/11/2022
* Re-release Web-Ui v2.2.35
## 2.2.35 08/11/2022
* Release web-ui v2.2.35
* Fix issues with VMnet interface on macOS >= 11.0. Ref #3381
* Use importlib_resources instead of pkg_resources and install built-in appliances in config dir.
* Fix console vnc don't use configured ports in some case. Fixes #2111
* Add missing VMware settings in gns3_server.conf
* Make version PEP 440 compliant
* Support for Python 3.11
* Allow for more dependency versions at patch level
* Replace deprecated distro.linux_distribution() call
* Update gns3.service.systemd
* gns3.service.openrc: make openrc script posix compliant
* fix: use exact match to find interface in windows to avoid get wrong interface
## 3.0.0a2 06/09/2022
* Add web-ui v3.0.0a2

View File

@ -34,4 +34,4 @@ COPY . /gns3server
RUN mkdir -p ~/.config/GNS3/3.0/
RUN cp scripts/gns3_server.conf ~/.config/GNS3/3.0/
RUN python3 setup.py install
RUN python3 -m pip install .

View File

@ -1,11 +1,7 @@
include README.rst
include AUTHORS
include README.md
include LICENSE
include MANIFEST.in
include requirements.txt
include conf/*.conf
recursive-include tests *
recursive-exclude docs *
include CHANGELOG
recursive-include gns3server *
recursive-exclude docs *
recursive-exclude * __pycache__
recursive-exclude * *.py[co]

View File

@ -6,7 +6,7 @@
[![Snyk scanning](https://snyk.io/test/github/GNS3/gns3-server/badge.svg)](https://snyk.io/test/github/GNS3/gns3-server)
The GNS3 server manages emulators and other virtualization software such as Dynamips, Qemu/KVM, Docker, VPCS, VirtualBox and VMware Workstation.
Clients like the [GNS3 GUI](https://github.com/GNS3/gns3-gui/) and the [GNS3 Web UI](https://github.com/GNS3/gns3-web-ui>) control the server using a HTTP REST API.
Clients like the [GNS3 GUI](https://github.com/GNS3/gns3-gui/) and the [GNS3 Web UI](https://github.com/GNS3/gns3-web-ui/) control the server using a HTTP REST API.
## Installation
@ -85,8 +85,8 @@ cd gns3-server
git checkout 3.0
python3 -m venv venv-gns3server
source venv-gns3server/bin/activate
python3 setup.py install
python3 -m gns3server --local
python3 -m pip install .
python3 -m gns3server
```
You will have to manually install other software dependencies (see above), for Dynamips, VPCS and uBridge the easiest is to install from our PPA.

View File

@ -14,4 +14,4 @@ currently being supported with security updates.
## Reporting a Vulnerability
Please contact us at security@gns3.net
Please use GitHub's report a vulnerability feature. More information can be found in https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability

View File

@ -1,8 +1,7 @@
-r requirements.txt
pytest==7.1.2
flake8==5.0.4
pytest-timeout==2.1.0
pytest-asyncio==0.19.0
requests==2.28.1
httpx==0.23.0
pytest==7.4.2
flake8==6.1.0
pytest-timeout==2.2.0
pytest-asyncio==0.21.1
requests==2.31.0
httpx==0.24.1 # version 0.24.1 is required by httpx_ws
httpx_ws==0.4.2

103
gns3server/alembic.ini Normal file
View File

@ -0,0 +1,103 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = db_migrations
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python-dateutil library that can be
# installed by adding `alembic[tz]` to the pip requirements
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to db_migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:db_migrations/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url =
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@ -58,7 +58,6 @@ log = logging.getLogger(__name__)
compute_api = FastAPI(
title="GNS3 compute API",
dependencies=[Depends(compute_authentication)],
description="This page describes the private compute API for GNS3. PLEASE DO NOT USE DIRECTLY!",
version="v3",
)
@ -158,11 +157,13 @@ async def http_exception_handler(request: Request, exc: StarletteHTTPException):
compute_api.include_router(
capabilities.router,
dependencies=[Depends(compute_authentication)],
tags=["Capabilities"]
)
compute_api.include_router(
compute.router,
dependencies=[Depends(compute_authentication)],
tags=["Compute"]
)
@ -173,21 +174,25 @@ compute_api.include_router(
compute_api.include_router(
projects.router,
dependencies=[Depends(compute_authentication)],
tags=["Projects"]
)
compute_api.include_router(
images.router,
dependencies=[Depends(compute_authentication)],
tags=["Images"]
)
compute_api.include_router(
atm_switch_nodes.router,
dependencies=[Depends(compute_authentication)],
prefix="/projects/{project_id}/atm_switch/nodes",
tags=["ATM switch"]
)
compute_api.include_router(
cloud_nodes.router,
dependencies=[Depends(compute_authentication)],
prefix="/projects/{project_id}/cloud/nodes",
tags=["Cloud nodes"]
)
@ -206,18 +211,21 @@ compute_api.include_router(
compute_api.include_router(
ethernet_hub_nodes.router,
dependencies=[Depends(compute_authentication)],
prefix="/projects/{project_id}/ethernet_hub/nodes",
tags=["Ethernet hub nodes"]
)
compute_api.include_router(
ethernet_switch_nodes.router,
dependencies=[Depends(compute_authentication)],
prefix="/projects/{project_id}/ethernet_switch/nodes",
tags=["Ethernet switch nodes"]
)
compute_api.include_router(
frame_relay_switch_nodes.router,
dependencies=[Depends(compute_authentication)],
prefix="/projects/{project_id}/frame_relay_switch/nodes",
tags=["Frame Relay switch nodes"]
)
@ -229,6 +237,7 @@ compute_api.include_router(
compute_api.include_router(
nat_nodes.router,
dependencies=[Depends(compute_authentication)],
prefix="/projects/{project_id}/nat/nodes",
tags=["NAT nodes"]
)

View File

@ -45,7 +45,7 @@ router = APIRouter()
@router.post("/projects/{project_id}/ports/udp", status_code=status.HTTP_201_CREATED)
def allocate_udp_port(project_id: UUID) -> dict:
"""
Allocate an UDP port on the compute.
Allocate a UDP port on the compute.
"""
pm = ProjectManager.instance()
@ -56,7 +56,7 @@ def allocate_udp_port(project_id: UUID) -> dict:
@router.get("/network/interfaces")
def network_interfaces() -> dict:
def network_interfaces() -> List[dict]:
"""
List all the network interfaces available on the compute"
"""

View File

@ -15,12 +15,17 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import secrets
import base64
import binascii
import logging
from fastapi import Depends, HTTPException, status
from fastapi import Depends, HTTPException, WebSocket, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.security.utils import get_authorization_scheme_param
from gns3server.config import Config
from typing import Optional
from typing import Optional, Union
log = logging.getLogger(__name__)
security = HTTPBasic()
@ -35,3 +40,44 @@ def compute_authentication(credentials: Optional[HTTPBasicCredentials] = Depends
detail="Invalid compute username or password",
headers={"WWW-Authenticate": "Basic"},
)
async def ws_compute_authentication(websocket: WebSocket) -> Union[None, WebSocket]:
"""
"""
await websocket.accept()
# handle basic HTTP authentication
invalid_user_credentials_exc = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Basic"},
)
try:
authorization = websocket.headers.get("Authorization")
scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "basic":
raise invalid_user_credentials_exc
try:
data = base64.b64decode(param).decode("ascii")
except (ValueError, UnicodeDecodeError, binascii.Error):
raise invalid_user_credentials_exc
username, separator, password = data.partition(":")
if not separator:
raise invalid_user_credentials_exc
server_settings = Config.instance().settings.Server
username = secrets.compare_digest(username, server_settings.compute_username)
password = secrets.compare_digest(password, server_settings.compute_password.get_secret_value())
if not (username and password):
raise invalid_user_credentials_exc
except HTTPException as e:
err_msg = f"Could not authenticate while connecting to compute WebSocket: {e.detail}"
websocket_error = {"action": "log.error", "event": {"message": err_msg}}
await websocket.send_json(websocket_error)
log.error(err_msg)
return await websocket.close(code=1008)
return websocket

View File

@ -20,15 +20,18 @@ API routes for Docker nodes.
import os
from fastapi import APIRouter, WebSocket, Depends, Body, Response, status
from fastapi import APIRouter, WebSocket, Depends, Body, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from uuid import UUID
from typing import Union
from gns3server import schemas
from gns3server.compute.docker import Docker
from gns3server.compute.docker.docker_vm import DockerVM
from .dependencies.authentication import compute_authentication, ws_compute_authentication
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Docker node"}}
router = APIRouter(responses=responses)
@ -49,6 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> DockerVM:
response_model=schemas.Docker,
status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Docker node"}},
dependencies=[Depends(compute_authentication)]
)
async def create_docker_node(project_id: UUID, node_data: schemas.DockerCreate) -> schemas.Docker:
"""
@ -85,7 +89,11 @@ async def create_docker_node(project_id: UUID, node_data: schemas.DockerCreate)
return container.asdict()
@router.get("/{node_id}", response_model=schemas.Docker)
@router.get(
"/{node_id}",
response_model=schemas.Docker,
dependencies=[Depends(compute_authentication)]
)
def get_docker_node(node: DockerVM = Depends(dep_node)) -> schemas.Docker:
"""
Return a Docker node.
@ -94,7 +102,11 @@ def get_docker_node(node: DockerVM = Depends(dep_node)) -> schemas.Docker:
return node.asdict()
@router.put("/{node_id}", response_model=schemas.Docker)
@router.put(
"/{node_id}",
response_model=schemas.Docker,
dependencies=[Depends(compute_authentication)]
)
async def update_docker_node(node_data: schemas.DockerUpdate, node: DockerVM = Depends(dep_node)) -> schemas.Docker:
"""
Update a Docker node.
@ -131,7 +143,11 @@ async def update_docker_node(node_data: schemas.DockerUpdate, node: DockerVM = D
return node.asdict()
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def start_docker_node(node: DockerVM = Depends(dep_node)) -> None:
"""
Start a Docker node.
@ -140,7 +156,11 @@ async def start_docker_node(node: DockerVM = Depends(dep_node)) -> None:
await node.start()
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_docker_node(node: DockerVM = Depends(dep_node)) -> None:
"""
Stop a Docker node.
@ -149,7 +169,11 @@ async def stop_docker_node(node: DockerVM = Depends(dep_node)) -> None:
await node.stop()
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def suspend_docker_node(node: DockerVM = Depends(dep_node)) -> None:
"""
Suspend a Docker node.
@ -158,7 +182,11 @@ async def suspend_docker_node(node: DockerVM = Depends(dep_node)) -> None:
await node.pause()
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/reload",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reload_docker_node(node: DockerVM = Depends(dep_node)) -> None:
"""
Reload a Docker node.
@ -167,7 +195,11 @@ async def reload_docker_node(node: DockerVM = Depends(dep_node)) -> None:
await node.restart()
@router.post("/{node_id}/pause", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/pause",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def pause_docker_node(node: DockerVM = Depends(dep_node)) -> None:
"""
Pause a Docker node.
@ -176,7 +208,11 @@ async def pause_docker_node(node: DockerVM = Depends(dep_node)) -> None:
await node.pause()
@router.post("/{node_id}/unpause", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/unpause",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def unpause_docker_node(node: DockerVM = Depends(dep_node)) -> None:
"""
Unpause a Docker node.
@ -185,7 +221,11 @@ async def unpause_docker_node(node: DockerVM = Depends(dep_node)) -> None:
await node.unpause()
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_docker_node(node: DockerVM = Depends(dep_node)) -> None:
"""
Delete a Docker node.
@ -194,7 +234,12 @@ async def delete_docker_node(node: DockerVM = Depends(dep_node)) -> None:
await node.delete()
@router.post("/{node_id}/duplicate", response_model=schemas.Docker, status_code=status.HTTP_201_CREATED)
@router.post(
"/{node_id}/duplicate",
response_model=schemas.Docker,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(compute_authentication)]
)
async def duplicate_docker_node(
destination_node_id: UUID = Body(..., embed=True),
node: DockerVM = Depends(dep_node)
@ -211,6 +256,7 @@ async def duplicate_docker_node(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
dependencies=[Depends(compute_authentication)]
)
async def create_docker_node_nio(
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: DockerVM = Depends(dep_node)
@ -229,6 +275,7 @@ async def create_docker_node_nio(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
dependencies=[Depends(compute_authentication)]
)
async def update_docker_node_nio(
adapter_number: int, port_number: int, nio_data: schemas.UDPNIO, node: DockerVM = Depends(dep_node)
@ -245,7 +292,11 @@ async def update_docker_node_nio(
return nio.asdict()
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_docker_node_nio(
adapter_number: int,
port_number: int,
@ -259,7 +310,10 @@ async def delete_docker_node_nio(
await node.adapter_remove_nio_binding(adapter_number)
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
dependencies=[Depends(compute_authentication)]
)
async def start_docker_node_capture(
adapter_number: int,
port_number: int,
@ -278,7 +332,8 @@ async def start_docker_node_capture(
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_docker_node_capture(
adapter_number: int,
@ -293,7 +348,10 @@ async def stop_docker_node_capture(
await node.stop_capture(adapter_number)
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
@router.get(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
dependencies=[Depends(compute_authentication)]
)
async def stream_pcap_file(
adapter_number: int,
port_number: int,
@ -310,15 +368,23 @@ async def stream_pcap_file(
@router.websocket("/{node_id}/console/ws")
async def console_ws(websocket: WebSocket, node: DockerVM = Depends(dep_node)) -> None:
async def console_ws(
websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
node: DockerVM = Depends(dep_node)
) -> None:
"""
Console WebSocket.
"""
await node.start_websocket_console(websocket)
if websocket:
await node.start_websocket_console(websocket)
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reset_console(node: DockerVM = Depends(dep_node)) -> None:
await node.reset_console()

View File

@ -20,16 +20,18 @@ API routes for Dynamips nodes.
import os
from fastapi import APIRouter, WebSocket, Depends, Response, status
from fastapi import APIRouter, WebSocket, Depends, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from typing import List
from typing import List, Union
from uuid import UUID
from gns3server.compute.dynamips import Dynamips
from gns3server.compute.dynamips.nodes.router import Router
from gns3server import schemas
from .dependencies.authentication import compute_authentication, ws_compute_authentication
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Dynamips node"}}
router = APIRouter(responses=responses)
@ -53,6 +55,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> Router:
response_model=schemas.Dynamips,
status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Dynamips node"}},
dependencies=[Depends(compute_authentication)]
)
async def create_router(project_id: UUID, node_data: schemas.DynamipsCreate) -> schemas.Dynamips:
"""
@ -61,9 +64,11 @@ async def create_router(project_id: UUID, node_data: schemas.DynamipsCreate) ->
dynamips_manager = Dynamips.instance()
platform = node_data.platform
chassis = None
print(node_data.chassis, platform in DEFAULT_CHASSIS)
if not node_data.chassis and platform in DEFAULT_CHASSIS:
chassis = DEFAULT_CHASSIS[platform]
else:
chassis = node_data.chassis
node_data = jsonable_encoder(node_data, exclude_unset=True)
vm = await dynamips_manager.create_node(
node_data.pop("name"),
@ -82,7 +87,11 @@ async def create_router(project_id: UUID, node_data: schemas.DynamipsCreate) ->
return vm.asdict()
@router.get("/{node_id}", response_model=schemas.Dynamips)
@router.get(
"/{node_id}",
response_model=schemas.Dynamips,
dependencies=[Depends(compute_authentication)]
)
def get_router(node: Router = Depends(dep_node)) -> schemas.Dynamips:
"""
Return Dynamips router.
@ -91,7 +100,11 @@ def get_router(node: Router = Depends(dep_node)) -> schemas.Dynamips:
return node.asdict()
@router.put("/{node_id}", response_model=schemas.Dynamips)
@router.put(
"/{node_id}",
response_model=schemas.Dynamips,
dependencies=[Depends(compute_authentication)]
)
async def update_router(node_data: schemas.DynamipsUpdate, node: Router = Depends(dep_node)) -> schemas.Dynamips:
"""
Update a Dynamips router.
@ -102,7 +115,11 @@ async def update_router(node_data: schemas.DynamipsUpdate, node: Router = Depend
return node.asdict()
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_router(node: Router = Depends(dep_node)) -> None:
"""
Delete a Dynamips router.
@ -111,7 +128,11 @@ async def delete_router(node: Router = Depends(dep_node)) -> None:
await Dynamips.instance().delete_node(node.id)
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def start_router(node: Router = Depends(dep_node)) -> None:
"""
Start a Dynamips router.
@ -124,7 +145,11 @@ async def start_router(node: Router = Depends(dep_node)) -> None:
await node.start()
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_router(node: Router = Depends(dep_node)) -> None:
"""
Stop a Dynamips router.
@ -133,13 +158,21 @@ async def stop_router(node: Router = Depends(dep_node)) -> None:
await node.stop()
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def suspend_router(node: Router = Depends(dep_node)) -> None:
await node.suspend()
@router.post("/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/resume",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def resume_router(node: Router = Depends(dep_node)) -> None:
"""
Resume a suspended Dynamips router.
@ -148,7 +181,11 @@ async def resume_router(node: Router = Depends(dep_node)) -> None:
await node.resume()
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/reload",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reload_router(node: Router = Depends(dep_node)) -> None:
"""
Reload a suspended Dynamips router.
@ -161,6 +198,7 @@ async def reload_router(node: Router = Depends(dep_node)) -> None:
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
dependencies=[Depends(compute_authentication)]
)
async def create_nio(
adapter_number: int,
@ -181,6 +219,7 @@ async def create_nio(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
dependencies=[Depends(compute_authentication)]
)
async def update_nio(
adapter_number: int,
@ -199,7 +238,11 @@ async def update_nio(
return nio.asdict()
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_nio(adapter_number: int, port_number: int, node: Router = Depends(dep_node)) -> None:
"""
Delete a NIO (Network Input/Output) from the node.
@ -209,7 +252,10 @@ async def delete_nio(adapter_number: int, port_number: int, node: Router = Depen
await nio.delete()
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
dependencies=[Depends(compute_authentication)]
)
async def start_capture(
adapter_number: int,
port_number: int,
@ -226,7 +272,9 @@ async def start_capture(
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_capture(adapter_number: int, port_number: int, node: Router = Depends(dep_node)) -> None:
"""
@ -236,7 +284,10 @@ async def stop_capture(adapter_number: int, port_number: int, node: Router = Dep
await node.stop_capture(adapter_number, port_number)
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
@router.get(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
dependencies=[Depends(compute_authentication)]
)
async def stream_pcap_file(
adapter_number: int,
port_number: int,
@ -251,7 +302,10 @@ async def stream_pcap_file(
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
@router.get("/{node_id}/idlepc_proposals")
@router.get(
"/{node_id}/idlepc_proposals",
dependencies=[Depends(compute_authentication)]
)
async def get_idlepcs(node: Router = Depends(dep_node)) -> List[str]:
"""
Retrieve Dynamips idle-pc proposals
@ -261,7 +315,10 @@ async def get_idlepcs(node: Router = Depends(dep_node)) -> List[str]:
return await node.get_idle_pc_prop()
@router.get("/{node_id}/auto_idlepc")
@router.get(
"/{node_id}/auto_idlepc",
dependencies=[Depends(compute_authentication)]
)
async def get_auto_idlepc(node: Router = Depends(dep_node)) -> dict:
"""
Get an automatically guessed best idle-pc value.
@ -271,7 +328,12 @@ async def get_auto_idlepc(node: Router = Depends(dep_node)) -> dict:
return {"idlepc": idlepc}
@router.post("/{node_id}/duplicate", response_model=schemas.Dynamips, status_code=status.HTTP_201_CREATED)
@router.post(
"/{node_id}/duplicate",
response_model=schemas.Dynamips,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(compute_authentication)]
)
async def duplicate_router(destination_node_id: UUID, node: Router = Depends(dep_node)) -> schemas.Dynamips:
"""
Duplicate a router.
@ -282,15 +344,24 @@ async def duplicate_router(destination_node_id: UUID, node: Router = Depends(dep
@router.websocket("/{node_id}/console/ws")
async def console_ws(websocket: WebSocket, node: Router = Depends(dep_node)) -> None:
async def console_ws(
websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
node: Router = Depends(dep_node)
) -> None:
"""
Console WebSocket.
"""
await node.start_websocket_console(websocket)
if websocket:
await node.start_websocket_console(websocket)
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reset_console(node: Router = Depends(dep_node)) -> None:
await node.reset_console()

View File

@ -150,12 +150,22 @@ def suspend_ethernet_switch(node: EthernetSwitch = Depends(dep_node)) -> None:
pass
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
def reload_ethernet_switch(node: EthernetSwitch = Depends(dep_node)) -> None:
"""
Reload an Ethernet switch.
This endpoint results in no action since Ethernet switch nodes are always on.
"""
pass
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
)
async def create_nio(
async def create_ethernet_switch_nio(
*,
adapter_number: int = Path(..., ge=0, le=0),
port_number: int,
@ -169,7 +179,7 @@ async def create_nio(
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
async def delete_nio(
async def delete_ethernet_switch_nio(
*,
adapter_number: int = Path(..., ge=0, le=0),
port_number: int,
@ -185,7 +195,7 @@ async def delete_nio(
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
async def start_capture(
async def start_ethernet_switch_capture(
*,
adapter_number: int = Path(..., ge=0, le=0),
port_number: int,
@ -205,7 +215,7 @@ async def start_capture(
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
)
async def stop_capture(
async def stop_ethernet_switch_capture(
*,
adapter_number: int = Path(..., ge=0, le=0),
port_number: int,

View File

@ -34,7 +34,7 @@ router = APIRouter()
@router.get("/docker/images")
async def get_docker_images() -> List[str]:
async def get_docker_images() -> List[dict]:
"""
Get all Docker images.
"""
@ -44,7 +44,7 @@ async def get_docker_images() -> List[str]:
@router.get("/dynamips/images")
async def get_dynamips_images() -> List[str]:
async def get_dynamips_images() -> List[dict]:
"""
Get all Dynamips images.
"""
@ -85,7 +85,7 @@ async def download_dynamips_image(filename: str) -> FileResponse:
@router.get("/iou/images")
async def get_iou_images() -> List[str]:
async def get_iou_images() -> List[dict]:
"""
Get all IOU images.
"""
@ -125,7 +125,7 @@ async def download_iou_image(filename: str) -> FileResponse:
@router.get("/qemu/images")
async def get_qemu_images() -> List[str]:
async def get_qemu_images() -> List[dict]:
qemu_manager = Qemu.instance()
return await qemu_manager.list_images()

View File

@ -30,6 +30,8 @@ from gns3server import schemas
from gns3server.compute.iou import IOU
from gns3server.compute.iou.iou_vm import IOUVM
from .dependencies.authentication import compute_authentication, ws_compute_authentication
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or IOU node"}}
router = APIRouter(responses=responses)
@ -50,6 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> IOUVM:
response_model=schemas.IOU,
status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create IOU node"}},
dependencies=[Depends(compute_authentication)]
)
async def create_iou_node(project_id: UUID, node_data: schemas.IOUCreate) -> schemas.IOU:
"""
@ -82,7 +85,11 @@ async def create_iou_node(project_id: UUID, node_data: schemas.IOUCreate) -> sch
return vm.asdict()
@router.get("/{node_id}", response_model=schemas.IOU)
@router.get(
"/{node_id}",
response_model=schemas.IOU,
dependencies=[Depends(compute_authentication)]
)
def get_iou_node(node: IOUVM = Depends(dep_node)) -> schemas.IOU:
"""
Return an IOU node.
@ -91,7 +98,11 @@ def get_iou_node(node: IOUVM = Depends(dep_node)) -> schemas.IOU:
return node.asdict()
@router.put("/{node_id}", response_model=schemas.IOU)
@router.put(
"/{node_id}",
response_model=schemas.IOU,
dependencies=[Depends(compute_authentication)]
)
async def update_iou_node(node_data: schemas.IOUUpdate, node: IOUVM = Depends(dep_node)) -> schemas.IOU:
"""
Update an IOU node.
@ -112,7 +123,11 @@ async def update_iou_node(node_data: schemas.IOUUpdate, node: IOUVM = Depends(de
return node.asdict()
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_iou_node(node: IOUVM = Depends(dep_node)) -> None:
"""
Delete an IOU node.
@ -121,7 +136,12 @@ async def delete_iou_node(node: IOUVM = Depends(dep_node)) -> None:
await IOU.instance().delete_node(node.id)
@router.post("/{node_id}/duplicate", response_model=schemas.IOU, status_code=status.HTTP_201_CREATED)
@router.post(
"/{node_id}/duplicate",
response_model=schemas.IOU,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(compute_authentication)]
)
async def duplicate_iou_node(
destination_node_id: UUID = Body(..., embed=True),
node: IOUVM = Depends(dep_node)
@ -134,7 +154,11 @@ async def duplicate_iou_node(
return new_node.asdict()
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def start_iou_node(start_data: schemas.IOUStart, node: IOUVM = Depends(dep_node)) -> None:
"""
Start an IOU node.
@ -148,7 +172,11 @@ async def start_iou_node(start_data: schemas.IOUStart, node: IOUVM = Depends(dep
await node.start()
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_iou_node(node: IOUVM = Depends(dep_node)) -> None:
"""
Stop an IOU node.
@ -157,7 +185,11 @@ async def stop_iou_node(node: IOUVM = Depends(dep_node)) -> None:
await node.stop()
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
def suspend_iou_node(node: IOUVM = Depends(dep_node)) -> None:
"""
Suspend an IOU node.
@ -167,7 +199,11 @@ def suspend_iou_node(node: IOUVM = Depends(dep_node)) -> None:
pass
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/reload",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reload_iou_node(node: IOUVM = Depends(dep_node)) -> None:
"""
Reload an IOU node.
@ -180,6 +216,7 @@ async def reload_iou_node(node: IOUVM = Depends(dep_node)) -> None:
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
dependencies=[Depends(compute_authentication)]
)
async def create_iou_node_nio(
adapter_number: int,
@ -200,6 +237,7 @@ async def create_iou_node_nio(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=Union[schemas.EthernetNIO, schemas.TAPNIO, schemas.UDPNIO],
dependencies=[Depends(compute_authentication)]
)
async def update_iou_node_nio(
adapter_number: int,
@ -218,7 +256,11 @@ async def update_iou_node_nio(
return nio.asdict()
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_iou_node_nio(adapter_number: int, port_number: int, node: IOUVM = Depends(dep_node)) -> None:
"""
Delete a NIO (Network Input/Output) from the node.
@ -227,7 +269,10 @@ async def delete_iou_node_nio(adapter_number: int, port_number: int, node: IOUVM
await node.adapter_remove_nio_binding(adapter_number, port_number)
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
dependencies=[Depends(compute_authentication)]
)
async def start_iou_node_capture(
adapter_number: int,
port_number: int,
@ -244,7 +289,9 @@ async def start_iou_node_capture(
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_iou_node_capture(adapter_number: int, port_number: int, node: IOUVM = Depends(dep_node)) -> None:
"""
@ -254,7 +301,10 @@ async def stop_iou_node_capture(adapter_number: int, port_number: int, node: IOU
await node.stop_capture(adapter_number, port_number)
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
@router.get(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
dependencies=[Depends(compute_authentication)]
)
async def stream_pcap_file(
adapter_number: int,
port_number: int,
@ -269,16 +319,26 @@ async def stream_pcap_file(
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
@router.websocket("/{node_id}/console/ws")
async def console_ws(websocket: WebSocket, node: IOUVM = Depends(dep_node)) -> None:
@router.websocket(
"/{node_id}/console/ws",
)
async def console_ws(
websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
node: IOUVM = Depends(dep_node)
) -> None:
"""
Console WebSocket.
"""
await node.start_websocket_console(websocket)
if websocket:
await node.start_websocket_console(websocket)
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reset_console(node: IOUVM = Depends(dep_node)) -> None:
await node.reset_console()

View File

@ -18,14 +18,13 @@
API routes for compute notifications.
"""
import base64
import binascii
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, status, HTTPException
from fastapi.security.utils import get_authorization_scheme_param
from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect
from typing import Union
from websockets.exceptions import ConnectionClosed, WebSocketException
from gns3server.compute.notification_manager import NotificationManager
from .dependencies.authentication import ws_compute_authentication
import logging
@ -35,53 +34,27 @@ router = APIRouter()
@router.websocket("/notifications/ws")
async def notification_ws(websocket: WebSocket) -> None:
async def project_ws_notifications(websocket: Union[None, WebSocket] = Depends(ws_compute_authentication)) -> None:
"""
Receive project notifications about the project from WebSocket.
"""
await websocket.accept()
# handle basic HTTP authentication
invalid_user_credentials_exc = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Basic"},
)
try:
authorization = websocket.headers.get("Authorization")
scheme, param = get_authorization_scheme_param(authorization)
if not authorization or scheme.lower() != "basic":
raise invalid_user_credentials_exc
if websocket:
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to compute WebSocket")
try:
data = base64.b64decode(param).decode("ascii")
except (ValueError, UnicodeDecodeError, binascii.Error):
raise invalid_user_credentials_exc
username, separator, password = data.partition(":")
if not separator:
raise invalid_user_credentials_exc
except invalid_user_credentials_exc as e:
websocket_error = {"action": "log.error", "event": {"message": f"Could not authenticate while connecting to "
f"compute WebSocket: {e.detail}"}}
await websocket.send_json(websocket_error)
return await websocket.close(code=1008)
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to compute WebSocket")
try:
with NotificationManager.instance().queue() as queue:
while True:
notification = await queue.get_json(5)
await websocket.send_text(notification)
except (ConnectionClosed, WebSocketDisconnect):
log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from compute WebSocket")
except WebSocketException as e:
log.warning(f"Error while sending to controller event to WebSocket client: {e}")
finally:
try:
await websocket.close()
except OSError:
pass # ignore OSError: [Errno 107] Transport endpoint is not connected
with NotificationManager.instance().queue() as queue:
while True:
notification = await queue.get_json(5)
await websocket.send_text(notification)
except (ConnectionClosed, WebSocketDisconnect):
log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from compute WebSocket")
except WebSocketException as e:
log.warning(f"Error while sending to controller event to WebSocket client: {e}")
finally:
try:
await websocket.close()
except OSError:
pass # ignore OSError: [Errno 107] Transport endpoint is not connected
if __name__ == "__main__":

View File

@ -20,15 +20,17 @@ API routes for Qemu nodes.
import os
from fastapi import APIRouter, WebSocket, Depends, Body, Path, Response, status
from fastapi import APIRouter, WebSocket, Depends, Body, Path, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from typing import Union
from uuid import UUID
from gns3server import schemas
from gns3server.compute.qemu import Qemu
from gns3server.compute.qemu.qemu_vm import QemuVM
from .dependencies.authentication import compute_authentication, ws_compute_authentication
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or Qemu node"}}
@ -50,6 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> QemuVM:
response_model=schemas.Qemu,
status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create Qemu node"}},
dependencies=[Depends(compute_authentication)]
)
async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate) -> schemas.Qemu:
"""
@ -78,7 +81,11 @@ async def create_qemu_node(project_id: UUID, node_data: schemas.QemuCreate) -> s
return vm.asdict()
@router.get("/{node_id}", response_model=schemas.Qemu)
@router.get(
"/{node_id}",
response_model=schemas.Qemu,
dependencies=[Depends(compute_authentication)]
)
def get_qemu_node(node: QemuVM = Depends(dep_node)) -> schemas.Qemu:
"""
Return a Qemu node.
@ -87,7 +94,11 @@ def get_qemu_node(node: QemuVM = Depends(dep_node)) -> schemas.Qemu:
return node.asdict()
@router.put("/{node_id}", response_model=schemas.Qemu)
@router.put(
"/{node_id}",
response_model=schemas.Qemu,
dependencies=[Depends(compute_authentication)]
)
async def update_qemu_node(node_data: schemas.QemuUpdate, node: QemuVM = Depends(dep_node)) -> schemas.Qemu:
"""
Update a Qemu node.
@ -103,7 +114,11 @@ async def update_qemu_node(node_data: schemas.QemuUpdate, node: QemuVM = Depends
return node.asdict()
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
"""
Delete a Qemu node.
@ -112,7 +127,12 @@ async def delete_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
await Qemu.instance().delete_node(node.id)
@router.post("/{node_id}/duplicate", response_model=schemas.Qemu, status_code=status.HTTP_201_CREATED)
@router.post(
"/{node_id}/duplicate",
response_model=schemas.Qemu,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(compute_authentication)]
)
async def duplicate_qemu_node(
destination_node_id: UUID = Body(..., embed=True),
node: QemuVM = Depends(dep_node)
@ -127,7 +147,8 @@ async def duplicate_qemu_node(
@router.post(
"/{node_id}/disk_image/{disk_name}",
status_code=status.HTTP_204_NO_CONTENT
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def create_qemu_disk_image(
disk_name: str,
@ -144,7 +165,8 @@ async def create_qemu_disk_image(
@router.put(
"/{node_id}/disk_image/{disk_name}",
status_code=status.HTTP_204_NO_CONTENT
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def update_qemu_disk_image(
disk_name: str,
@ -161,7 +183,8 @@ async def update_qemu_disk_image(
@router.delete(
"/{node_id}/disk_image/{disk_name}",
status_code=status.HTTP_204_NO_CONTENT
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_qemu_disk_image(
disk_name: str,
@ -174,7 +197,11 @@ async def delete_qemu_disk_image(
node.delete_disk_image(disk_name)
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def start_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
"""
Start a Qemu node.
@ -183,7 +210,11 @@ async def start_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
await node.start()
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
"""
Stop a Qemu node.
@ -192,7 +223,11 @@ async def stop_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
await node.stop()
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/reload",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reload_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
"""
Reload a Qemu node.
@ -201,7 +236,11 @@ async def reload_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
await node.reload()
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def suspend_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
"""
Suspend a Qemu node.
@ -210,7 +249,11 @@ async def suspend_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
await node.suspend()
@router.post("/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/resume",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def resume_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
"""
Resume a Qemu node.
@ -223,6 +266,7 @@ async def resume_qemu_node(node: QemuVM = Depends(dep_node)) -> None:
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
dependencies=[Depends(compute_authentication)]
)
async def create_qemu_node_nio(
*,
@ -245,6 +289,7 @@ async def create_qemu_node_nio(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
dependencies=[Depends(compute_authentication)]
)
async def update_qemu_node_nio(
*,
@ -267,7 +312,11 @@ async def update_qemu_node_nio(
return nio.asdict()
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_qemu_node_nio(
adapter_number: int,
port_number: int = Path(..., ge=0, le=0),
@ -281,7 +330,10 @@ async def delete_qemu_node_nio(
await node.adapter_remove_nio_binding(adapter_number)
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
dependencies=[Depends(compute_authentication)]
)
async def start_qemu_node_capture(
*,
adapter_number: int,
@ -300,7 +352,9 @@ async def start_qemu_node_capture(
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_qemu_node_capture(
adapter_number: int,
@ -315,7 +369,10 @@ async def stop_qemu_node_capture(
await node.stop_capture(adapter_number)
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
@router.get(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
dependencies=[Depends(compute_authentication)]
)
async def stream_pcap_file(
adapter_number: int,
port_number: int = Path(..., ge=0, le=0),
@ -330,16 +387,26 @@ async def stream_pcap_file(
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
@router.websocket("/{node_id}/console/ws")
async def console_ws(websocket: WebSocket, node: QemuVM = Depends(dep_node)) -> None:
@router.websocket(
"/{node_id}/console/ws"
)
async def console_ws(
websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
node: QemuVM = Depends(dep_node)
) -> None:
"""
Console WebSocket.
"""
await node.start_websocket_console(websocket)
if websocket:
await node.start_websocket_console(websocket)
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reset_console(node: QemuVM = Depends(dep_node)) -> None:
await node.reset_console()

View File

@ -20,19 +20,22 @@ API routes for VirtualBox nodes.
import os
from fastapi import APIRouter, WebSocket, Depends, Path, Response, status
from fastapi import APIRouter, WebSocket, Depends, Path, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from uuid import UUID
from typing import Union
from gns3server import schemas
from gns3server.compute.virtualbox import VirtualBox
from gns3server.compute.virtualbox.virtualbox_error import VirtualBoxError
from gns3server.compute.virtualbox.virtualbox_vm import VirtualBoxVM
from .dependencies.authentication import compute_authentication, ws_compute_authentication
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VirtualBox node"}}
router = APIRouter(responses=responses)
router = APIRouter(responses=responses, deprecated=True)
def dep_node(project_id: UUID, node_id: UUID) -> VirtualBoxVM:
@ -50,6 +53,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> VirtualBoxVM:
response_model=schemas.VirtualBox,
status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VirtualBox node"}},
dependencies=[Depends(compute_authentication)]
)
async def create_virtualbox_node(project_id: UUID, node_data: schemas.VirtualBoxCreate) -> schemas.VirtualBox:
"""
@ -82,7 +86,11 @@ async def create_virtualbox_node(project_id: UUID, node_data: schemas.VirtualBox
return vm.asdict()
@router.get("/{node_id}", response_model=schemas.VirtualBox)
@router.get(
"/{node_id}",
response_model=schemas.VirtualBox,
dependencies=[Depends(compute_authentication)]
)
def get_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> schemas.VirtualBox:
"""
Return a VirtualBox node.
@ -91,7 +99,11 @@ def get_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> schemas.Virtu
return node.asdict()
@router.put("/{node_id}", response_model=schemas.VirtualBox)
@router.put(
"/{node_id}",
response_model=schemas.VirtualBox,
dependencies=[Depends(compute_authentication)]
)
async def update_virtualbox_node(
node_data: schemas.VirtualBoxUpdate,
node: VirtualBoxVM = Depends(dep_node)
@ -136,7 +148,11 @@ async def update_virtualbox_node(
return node.asdict()
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
"""
Delete a VirtualBox node.
@ -145,7 +161,11 @@ async def delete_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None
await VirtualBox.instance().delete_node(node.id)
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def start_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
"""
Start a VirtualBox node.
@ -154,7 +174,11 @@ async def start_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
await node.start()
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
"""
Stop a VirtualBox node.
@ -163,7 +187,11 @@ async def stop_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
await node.stop()
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def suspend_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
"""
Suspend a VirtualBox node.
@ -172,7 +200,11 @@ async def suspend_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> Non
await node.suspend()
@router.post("/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/resume",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def resume_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
"""
Resume a VirtualBox node.
@ -181,7 +213,11 @@ async def resume_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None
await node.resume()
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/reload",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reload_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None:
"""
Reload a VirtualBox node.
@ -194,6 +230,7 @@ async def reload_virtualbox_node(node: VirtualBoxVM = Depends(dep_node)) -> None
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
dependencies=[Depends(compute_authentication)]
)
async def create_virtualbox_node_nio(
*,
@ -216,6 +253,7 @@ async def create_virtualbox_node_nio(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
dependencies=[Depends(compute_authentication)]
)
async def update_virtualbox_node_nio(
*,
@ -238,7 +276,11 @@ async def update_virtualbox_node_nio(
return nio.asdict()
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_virtualbox_node_nio(
adapter_number: int,
port_number: int = Path(..., ge=0, le=0),
@ -252,7 +294,10 @@ async def delete_virtualbox_node_nio(
await node.adapter_remove_nio_binding(adapter_number)
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
dependencies=[Depends(compute_authentication)]
)
async def start_virtualbox_node_capture(
*,
adapter_number: int,
@ -271,7 +316,9 @@ async def start_virtualbox_node_capture(
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_virtualbox_node_capture(
adapter_number: int,
@ -286,7 +333,10 @@ async def stop_virtualbox_node_capture(
await node.stop_capture(adapter_number)
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
@router.get(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
dependencies=[Depends(compute_authentication)]
)
async def stream_pcap_file(
adapter_number: int,
port_number: int = Path(..., ge=0, le=0),
@ -302,8 +352,13 @@ async def stream_pcap_file(
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
@router.websocket("/{node_id}/console/ws")
async def console_ws(websocket: WebSocket, node: VirtualBoxVM = Depends(dep_node)) -> None:
@router.websocket(
"/{node_id}/console/ws"
)
async def console_ws(
websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
node: VirtualBoxVM = Depends(dep_node)
) -> None:
"""
Console WebSocket.
"""
@ -311,7 +366,11 @@ async def console_ws(websocket: WebSocket, node: VirtualBoxVM = Depends(dep_node
await node.start_websocket_console(websocket)
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reset_console(node: VirtualBoxVM = Depends(dep_node)) -> None:
await node.reset_console()

View File

@ -20,19 +20,21 @@ API routes for VMware nodes.
import os
from fastapi import APIRouter, WebSocket, Depends, Path, Response, status
from fastapi import APIRouter, WebSocket, Depends, Path, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from uuid import UUID
from typing import Union
from gns3server import schemas
from gns3server.compute.vmware import VMware
from gns3server.compute.project_manager import ProjectManager
from gns3server.compute.vmware.vmware_vm import VMwareVM
from .dependencies.authentication import compute_authentication, ws_compute_authentication
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}}
router = APIRouter(responses=responses)
router = APIRouter(responses=responses, deprecated=True)
def dep_node(project_id: UUID, node_id: UUID) -> VMwareVM:
@ -50,6 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> VMwareVM:
response_model=schemas.VMware,
status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VMware node"}},
dependencies=[Depends(compute_authentication)]
)
async def create_vmware_node(project_id: UUID, node_data: schemas.VMwareCreate) -> schemas.VMware:
"""
@ -76,7 +79,11 @@ async def create_vmware_node(project_id: UUID, node_data: schemas.VMwareCreate)
return vm.asdict()
@router.get("/{node_id}", response_model=schemas.VMware)
@router.get(
"/{node_id}",
response_model=schemas.VMware,
dependencies=[Depends(compute_authentication)]
)
def get_vmware_node(node: VMwareVM = Depends(dep_node)) -> schemas.VMware:
"""
Return a VMware node.
@ -85,7 +92,11 @@ def get_vmware_node(node: VMwareVM = Depends(dep_node)) -> schemas.VMware:
return node.asdict()
@router.put("/{node_id}", response_model=schemas.VMware)
@router.put(
"/{node_id}",
response_model=schemas.VMware,
dependencies=[Depends(compute_authentication)]
)
def update_vmware_node(node_data: schemas.VMwareUpdate, node: VMwareVM = Depends(dep_node)) -> schemas.VMware:
"""
Update a VMware node.
@ -102,7 +113,11 @@ def update_vmware_node(node_data: schemas.VMwareUpdate, node: VMwareVM = Depends
return node.asdict()
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
"""
Delete a VMware node.
@ -111,7 +126,11 @@ async def delete_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
await VMware.instance().delete_node(node.id)
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def start_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
"""
Start a VMware node.
@ -120,7 +139,11 @@ async def start_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
await node.start()
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
"""
Stop a VMware node.
@ -129,7 +152,11 @@ async def stop_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
await node.stop()
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def suspend_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
"""
Suspend a VMware node.
@ -138,7 +165,11 @@ async def suspend_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
await node.suspend()
@router.post("/{node_id}/resume", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/resume",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def resume_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
"""
Resume a VMware node.
@ -147,7 +178,11 @@ async def resume_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
await node.resume()
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/reload",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reload_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
"""
Reload a VMware node.
@ -160,6 +195,7 @@ async def reload_vmware_node(node: VMwareVM = Depends(dep_node)) -> None:
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
dependencies=[Depends(compute_authentication)]
)
async def create_vmware_node_nio(
*,
@ -182,6 +218,7 @@ async def create_vmware_node_nio(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
dependencies=[Depends(compute_authentication)]
)
async def update_vmware_node_nio(
*,
@ -202,7 +239,11 @@ async def update_vmware_node_nio(
return nio.asdict()
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_vmware_node_nio(
adapter_number: int,
port_number: int = Path(..., ge=0, le=0),
@ -216,7 +257,10 @@ async def delete_vmware_node_nio(
await node.adapter_remove_nio_binding(adapter_number)
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
dependencies=[Depends(compute_authentication)]
)
async def start_vmware_node_capture(
*,
adapter_number: int,
@ -235,7 +279,9 @@ async def start_vmware_node_capture(
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_vmware_node_capture(
adapter_number: int,
@ -250,7 +296,10 @@ async def stop_vmware_node_capture(
await node.stop_capture(adapter_number)
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
@router.get(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
dependencies=[Depends(compute_authentication)]
)
async def stream_pcap_file(
adapter_number: int,
port_number: int = Path(..., ge=0, le=0),
@ -266,7 +315,11 @@ async def stream_pcap_file(
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
@router.post("/{node_id}/interfaces/vmnet", status_code=status.HTTP_201_CREATED)
@router.post(
"/{node_id}/interfaces/vmnet",
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(compute_authentication)]
)
def allocate_vmnet(node: VMwareVM = Depends(dep_node)) -> dict:
"""
Allocate a VMware VMnet interface on the server.
@ -280,16 +333,23 @@ def allocate_vmnet(node: VMwareVM = Depends(dep_node)) -> dict:
@router.websocket("/{node_id}/console/ws")
async def console_ws(websocket: WebSocket, node: VMwareVM = Depends(dep_node)) -> None:
async def console_ws(
websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
node: VMwareVM = Depends(dep_node)
) -> None:
"""
Console WebSocket.
"""
await node.start_websocket_console(websocket)
if websocket:
await node.start_websocket_console(websocket)
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reset_console(node: VMwareVM = Depends(dep_node)) -> None:
await node.reset_console()

View File

@ -20,15 +20,18 @@ API routes for VPCS nodes.
import os
from fastapi import APIRouter, WebSocket, Depends, Body, Path, Response, status
from fastapi import APIRouter, WebSocket, Depends, Body, Path, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import StreamingResponse
from typing import Union
from uuid import UUID
from gns3server import schemas
from gns3server.compute.vpcs import VPCS
from gns3server.compute.vpcs.vpcs_vm import VPCSVM
from .dependencies.authentication import compute_authentication, ws_compute_authentication
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or VMware node"}}
router = APIRouter(responses=responses)
@ -49,6 +52,7 @@ def dep_node(project_id: UUID, node_id: UUID) -> VPCSVM:
response_model=schemas.VPCS,
status_code=status.HTTP_201_CREATED,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create VMware node"}},
dependencies=[Depends(compute_authentication)]
)
async def create_vpcs_node(project_id: UUID, node_data: schemas.VPCSCreate) -> schemas.VPCS:
"""
@ -69,7 +73,11 @@ async def create_vpcs_node(project_id: UUID, node_data: schemas.VPCSCreate) -> s
return vm.asdict()
@router.get("/{node_id}", response_model=schemas.VPCS)
@router.get(
"/{node_id}",
response_model=schemas.VPCS,
dependencies=[Depends(compute_authentication)]
)
def get_vpcs_node(node: VPCSVM = Depends(dep_node)) -> schemas.VPCS:
"""
Return a VPCS node.
@ -78,7 +86,11 @@ def get_vpcs_node(node: VPCSVM = Depends(dep_node)) -> schemas.VPCS:
return node.asdict()
@router.put("/{node_id}", response_model=schemas.VPCS)
@router.put(
"/{node_id}",
response_model=schemas.VPCS,
dependencies=[Depends(compute_authentication)]
)
def update_vpcs_node(node_data: schemas.VPCSUpdate, node: VPCSVM = Depends(dep_node)) -> schemas.VPCS:
"""
Update a VPCS node.
@ -92,7 +104,11 @@ def update_vpcs_node(node_data: schemas.VPCSUpdate, node: VPCSVM = Depends(dep_n
return node.asdict()
@router.delete("/{node_id}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
"""
Delete a VPCS node.
@ -101,7 +117,12 @@ async def delete_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
await VPCS.instance().delete_node(node.id)
@router.post("/{node_id}/duplicate", response_model=schemas.VPCS, status_code=status.HTTP_201_CREATED)
@router.post(
"/{node_id}/duplicate",
response_model=schemas.VPCS,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(compute_authentication)]
)
async def duplicate_vpcs_node(
destination_node_id: UUID = Body(..., embed=True),
node: VPCSVM = Depends(dep_node)) -> None:
@ -113,7 +134,11 @@ async def duplicate_vpcs_node(
return new_node.asdict()
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def start_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
"""
Start a VPCS node.
@ -122,7 +147,11 @@ async def start_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
await node.start()
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
"""
Stop a VPCS node.
@ -131,7 +160,11 @@ async def stop_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
await node.stop()
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def suspend_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
"""
Suspend a VPCS node.
@ -141,7 +174,11 @@ async def suspend_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
pass
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/reload",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reload_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
"""
Reload a VPCS node.
@ -154,6 +191,7 @@ async def reload_vpcs_node(node: VPCSVM = Depends(dep_node)) -> None:
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
dependencies=[Depends(compute_authentication)]
)
async def create_vpcs_node_nio(
*,
@ -176,6 +214,7 @@ async def create_vpcs_node_nio(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_201_CREATED,
response_model=schemas.UDPNIO,
dependencies=[Depends(compute_authentication)]
)
async def update_vpcs_node_nio(
*,
@ -196,7 +235,11 @@ async def update_vpcs_node_nio(
return nio.asdict()
@router.delete("/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/nio",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def delete_vpcs_node_nio(
*,
adapter_number: int = Path(..., ge=0, le=0),
@ -211,7 +254,10 @@ async def delete_vpcs_node_nio(
await node.port_remove_nio_binding(port_number)
@router.post("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start")
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/start",
dependencies=[Depends(compute_authentication)]
)
async def start_vpcs_node_capture(
*,
adapter_number: int = Path(..., ge=0, le=0),
@ -230,7 +276,9 @@ async def start_vpcs_node_capture(
@router.post(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop", status_code=status.HTTP_204_NO_CONTENT
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def stop_vpcs_node_capture(
*,
@ -246,13 +294,10 @@ async def stop_vpcs_node_capture(
await node.stop_capture(port_number)
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
async def reset_console(node: VPCSVM = Depends(dep_node)) -> None:
await node.reset_console()
@router.get("/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream")
@router.get(
"/{node_id}/adapters/{adapter_number}/ports/{port_number}/capture/stream",
dependencies=[Depends(compute_authentication)]
)
async def stream_pcap_file(
*,
adapter_number: int = Path(..., ge=0, le=0),
@ -269,10 +314,24 @@ async def stream_pcap_file(
return StreamingResponse(stream, media_type="application/vnd.tcpdump.pcap")
@router.websocket("/{node_id}/console/ws")
async def console_ws(websocket: WebSocket, node: VPCSVM = Depends(dep_node)) -> None:
@router.websocket(
"/{node_id}/console/ws"
)
async def console_ws(
websocket: Union[None, WebSocket] = Depends(ws_compute_authentication),
node: VPCSVM = Depends(dep_node)) -> None:
"""
Console WebSocket.
"""
await node.start_websocket_console(websocket)
@router.post(
"/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(compute_authentication)]
)
async def reset_console(node: VPCSVM = Depends(dep_node)) -> None:
await node.reset_console()

View File

@ -23,7 +23,6 @@ from . import drawings
from . import gns3vm
from . import links
from . import nodes
from . import notifications
from . import projects
from . import snapshots
from . import symbols
@ -32,73 +31,81 @@ from . import images
from . import users
from . import groups
from . import roles
from . import permissions
from . import acl
from . import pools
from . import privileges
from .dependencies.authentication import get_current_active_user
router = APIRouter()
router.include_router(controller.router, tags=["Controller"])
router.include_router(users.router, prefix="/users", tags=["Users"])
router.include_router(
controller.router,
tags=["Controller"]
)
router.include_router(
users.router,
prefix="/access/users",
tags=["Users"]
)
router.include_router(
groups.router,
dependencies=[Depends(get_current_active_user)],
prefix="/groups",
prefix="/access/groups",
tags=["Users groups"]
)
router.include_router(
roles.router,
dependencies=[Depends(get_current_active_user)],
prefix="/roles",
prefix="/access/roles",
tags=["Roles"]
)
router.include_router(
permissions.router,
privileges.router,
dependencies=[Depends(get_current_active_user)],
prefix="/permissions",
tags=["Permissions"]
prefix="/access/privileges",
tags=["Privileges"]
)
router.include_router(
acl.router,
prefix="/access/acl",
tags=["ACL"]
)
router.include_router(
images.router,
dependencies=[Depends(get_current_active_user)],
prefix="/images",
tags=["Images"]
)
router.include_router(
templates.router,
dependencies=[Depends(get_current_active_user)],
prefix="/templates",
tags=["Templates"]
)
router.include_router(
projects.router,
dependencies=[Depends(get_current_active_user)],
prefix="/projects",
tags=["Projects"])
router.include_router(
nodes.router,
dependencies=[Depends(get_current_active_user)],
prefix="/projects/{project_id}/nodes",
tags=["Nodes"]
)
router.include_router(
links.router,
dependencies=[Depends(get_current_active_user)],
prefix="/projects/{project_id}/links",
tags=["Links"]
)
router.include_router(
drawings.router,
dependencies=[Depends(get_current_active_user)],
prefix="/projects/{project_id}/drawings",
tags=["Drawings"])
@ -109,7 +116,6 @@ router.include_router(
router.include_router(
snapshots.router,
dependencies=[Depends(get_current_active_user)],
prefix="/projects/{project_id}/snapshots",
tags=["Snapshots"])
@ -120,23 +126,22 @@ router.include_router(
tags=["Computes"]
)
router.include_router(
notifications.router,
dependencies=[Depends(get_current_active_user)],
prefix="/notifications",
tags=["Notifications"])
router.include_router(
appliances.router,
dependencies=[Depends(get_current_active_user)],
prefix="/appliances",
tags=["Appliances"]
)
router.include_router(
pools.router,
prefix="/pools",
tags=["Resource pools"]
)
router.include_router(
gns3vm.router,
deprecated=True,
dependencies=[Depends(get_current_active_user)],
deprecated=True,
prefix="/gns3vm",
tags=["GNS3 VM"]
)

View File

@ -0,0 +1,268 @@
#!/usr/bin/env python
#
# Copyright (C) 2023 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
API routes for ACL.
"""
import re
from fastapi import APIRouter, Depends, Request, status
from fastapi.routing import APIRoute
from uuid import UUID
from typing import List
from gns3server import schemas
from gns3server.controller.controller_error import (
ControllerBadRequestError,
ControllerNotFoundError
)
from gns3server.controller import Controller
from gns3server.db.repositories.users import UsersRepository
from gns3server.db.repositories.rbac import RbacRepository
from gns3server.db.repositories.images import ImagesRepository
from gns3server.db.repositories.templates import TemplatesRepository
from gns3server.db.repositories.pools import ResourcePoolsRepository
from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege
import logging
log = logging.getLogger(__name__)
router = APIRouter()
@router.get(
"/endpoints",
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(has_privilege("ACE.Audit"))]
)
async def endpoints(
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
) -> List[dict]:
"""
List all endpoints to be used in ACL entries.
"""
controller = Controller.instance()
endpoints = [{"endpoint": "/", "name": "All endpoints", "endpoint_type": "root"}]
def add_to_endpoints(endpoint: str, name: str, endpoint_type: str) -> None:
if endpoint not in endpoints:
endpoints.append({"endpoint": endpoint, "name": name, "endpoint_type": endpoint_type})
# projects
add_to_endpoints("/projects", "All projects", "project")
projects = [p for p in controller.projects.values()]
for project in projects:
add_to_endpoints(f"/projects/{project.id}", f'Project "{project.name}"', "project")
if project.status == "closed":
nodes = project.nodes.values()
links = project.links.values()
else:
nodes = [v.asdict() for v in project.nodes.values()]
links = [v.asdict() for v in project.links.values()]
# nodes
add_to_endpoints(f"/projects/{project.id}/nodes", f'All nodes in project "{project.name}"', "node")
for node in nodes:
add_to_endpoints(
f"/projects/{project.id}/nodes/{node['node_id']}",
f'Node "{node["name"]}" in project "{project.name}"',
endpoint_type="node"
)
# links
add_to_endpoints(f"/projects/{project.id}/links", f'All links in project "{project.name}"', "link")
for link in links:
node_id_1 = link["nodes"][0]["node_id"]
node_id_2 = link["nodes"][1]["node_id"]
node_name_1 = node_name_2 = "N/A"
for node in nodes:
if node["node_id"] == node_id_1:
node_name_1 = node["name"]
if node["node_id"] == node_id_2:
node_name_2 = node["name"]
add_to_endpoints(
f"/projects/{project.id}/links/{link['link_id']}",
f'Link from "{node_name_1}" to "{node_name_2}" in project "{project.name}"',
endpoint_type="link"
)
# users
add_to_endpoints("/access/users", "All users", "user")
users = await users_repo.get_users()
for user in users:
add_to_endpoints(f"/users/{user.user_id}", f'User "{user.username}"', "user")
# groups
add_to_endpoints("/access/groups", "All groups", "group")
groups = await users_repo.get_user_groups()
for group in groups:
add_to_endpoints(f"/groups/{group.user_group_id}", f'Group "{group.name}"', "group")
# roles
add_to_endpoints("/access/roles", "All roles", "role")
roles = await rbac_repo.get_roles()
for role in roles:
add_to_endpoints(f"/roles/{role.role_id}", f'Role "{role.name}"', "role")
# images
add_to_endpoints("/images", "All images", "image")
images = await images_repo.get_images()
for image in images:
add_to_endpoints(f"/images/{image.filename}", f'Image "{image.filename}"', "image")
# templates
add_to_endpoints("/templates", "All templates", "template")
templates = await templates_repo.get_templates()
for template in templates:
add_to_endpoints(f"/templates/{template.template_id}", f'Template "{template.name}"', "template")
# resource pools
add_to_endpoints("/pools", "All resource pools", "pool")
pools = await pools_repo.get_resource_pools()
for pool in pools:
add_to_endpoints(f"/pools/{pool.resource_pool_id}", f'Resource pool "{pool.name}"', "pool")
return endpoints
@router.get(
"",
response_model=List[schemas.ACE],
dependencies=[Depends(has_privilege("ACE.Audit"))]
)
async def get_aces(
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> List[schemas.ACE]:
"""
Get all ACL entries.
Required privilege: ACE.Audit
"""
return await rbac_repo.get_aces()
@router.post(
"",
response_model=schemas.ACE,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(has_privilege("ACE.Allocate"))]
)
async def create_ace(
request: Request,
ace_create: schemas.ACECreate,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> schemas.ACE:
"""
Create a new ACL entry.
Required privilege: ACE.Allocate
"""
for route in request.app.routes:
if isinstance(route, APIRoute):
# remove the prefix (e.g. "/v3") from the route path
route_path = re.sub(r"^/v[0-9]", "", route.path)
# replace route path ID parameters by a UUID regex
route_path = re.sub(r"{\w+_id}", "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}", route_path)
# replace remaining route path parameters by a word matching regex
route_path = re.sub(r"/{[\w:]+}", r"/\\w+", route_path)
if re.fullmatch(route_path, ace_create.path):
log.info(f"Creating ACE for route path {route_path}")
return await rbac_repo.create_ace(ace_create)
raise ControllerBadRequestError(f"Path '{ace_create.path}' doesn't match any existing endpoint")
@router.get(
"/{ace_id}",
response_model=schemas.ACE,
dependencies=[Depends(has_privilege("ACE.Audit"))]
)
async def get_ace(
ace_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> schemas.ACE:
"""
Get an ACL entry.
Required privilege: ACE.Audit
"""
ace = await rbac_repo.get_ace(ace_id)
if not ace:
raise ControllerNotFoundError(f"ACL entry '{ace_id}' not found")
return ace
@router.put(
"/{ace_id}",
response_model=schemas.ACE,
dependencies=[Depends(has_privilege("ACE.Modify"))]
)
async def update_ace(
ace_id: UUID,
ace_update: schemas.ACEUpdate,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> schemas.ACE:
"""
Update an ACL entry.
Required privilege: ACE.Modify
"""
ace = await rbac_repo.get_ace(ace_id)
if not ace:
raise ControllerNotFoundError(f"ACL entry '{ace_id}' not found")
return await rbac_repo.update_ace(ace_id, ace_update)
@router.delete(
"/{ace_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("ACE.Allocate"))]
)
async def delete_ace(
ace_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> None:
"""
Delete an ACL entry.
Required privilege: ACE.Allocate
"""
ace = await rbac_repo.get_ace(ace_id)
if not ace:
raise ControllerNotFoundError(f"ACL entry '{ace_id}' not found")
success = await rbac_repo.delete_ace(ace_id)
if not success:
raise ControllerNotFoundError(f"ACL entry '{ace_id}' could not be deleted")

View File

@ -20,7 +20,7 @@ API routes for appliances.
import logging
from fastapi import APIRouter, Depends, Response, status
from fastapi import APIRouter, Depends, status
from typing import Optional, List
from uuid import UUID
@ -38,19 +38,28 @@ from gns3server.db.repositories.rbac import RbacRepository
from .dependencies.authentication import get_current_active_user
from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege
log = logging.getLogger(__name__)
router = APIRouter()
@router.get("")
@router.get(
"",
response_model=List[schemas.Appliance],
response_model_exclude_unset=True,
dependencies=[Depends(has_privilege("Appliance.Audit"))]
)
async def get_appliances(
update: Optional[bool] = False,
symbol_theme: Optional[str] = None
) -> List[schemas.Appliance]:
"""
Return all appliances known by the controller.
Required privilege: Appliance.Audit
"""
controller = Controller.instance()
@ -60,10 +69,17 @@ async def get_appliances(
return [c.asdict() for c in controller.appliance_manager.appliances.values()]
@router.get("/{appliance_id}")
@router.get(
"/{appliance_id}",
response_model=schemas.Appliance,
response_model_exclude_unset=True,
dependencies=[Depends(has_privilege("Appliance.Audit"))]
)
def get_appliance(appliance_id: UUID) -> schemas.Appliance:
"""
Get an appliance file.
Required privilege: Appliance.Audit
"""
controller = Controller.instance()
@ -73,10 +89,16 @@ def get_appliance(appliance_id: UUID) -> schemas.Appliance:
return appliance.asdict()
@router.post("/{appliance_id}/version", status_code=status.HTTP_201_CREATED)
def add_appliance_version(appliance_id: UUID, appliance_version: schemas.ApplianceVersion) -> schemas.Appliance:
@router.post(
"/{appliance_id}/version",
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(has_privilege("Appliance.Allocate"))]
)
def add_appliance_version(appliance_id: UUID, appliance_version: schemas.ApplianceVersion) -> dict:
"""
Add a version to an appliance
Add a version to an appliance.
Required privilege: Appliance.Allocate
"""
controller = Controller.instance()
@ -94,11 +116,15 @@ def add_appliance_version(appliance_id: UUID, appliance_version: schemas.Applian
if version.get("name") == appliance_version.name:
raise ControllerError(message=f"Appliance '{appliance_id}' already has version '{appliance_version.name}'")
appliance.versions.append(appliance_version.dict(exclude_unset=True))
appliance.versions.append(appliance_version.model_dump(exclude_unset=True))
return appliance.asdict()
@router.post("/{appliance_id}/install", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{appliance_id}/install",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Appliance.Allocate"))]
)
async def install_appliance(
appliance_id: UUID,
version: Optional[str] = None,
@ -109,6 +135,8 @@ async def install_appliance(
) -> None:
"""
Install an appliance.
Required privilege: Appliance.Allocate
"""
controller = Controller.instance()

View File

@ -18,16 +18,18 @@
API routes for computes.
"""
from fastapi import APIRouter, Depends, Response, status
from typing import List, Union, Optional
from fastapi import APIRouter, Depends, status
from typing import Any, List, Union, Optional
from uuid import UUID
from gns3server.controller import Controller
from gns3server.db.repositories.computes import ComputesRepository
from gns3server.db.repositories.rbac import RbacRepository
from gns3server.services.computes import ComputesService
from gns3server import schemas
from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege
responses = {404: {"model": schemas.ErrorMessage, "description": "Compute not found"}}
@ -43,6 +45,7 @@ router = APIRouter(responses=responses)
409: {"model": schemas.ErrorMessage, "description": "Could not create compute"},
401: {"model": schemas.ErrorMessage, "description": "Invalid authentication for compute"},
},
dependencies=[Depends(has_privilege("Compute.Allocate"))]
)
async def create_compute(
compute_create: schemas.ComputeCreate,
@ -51,15 +54,23 @@ async def create_compute(
) -> schemas.Compute:
"""
Create a new compute on the controller.
Required privilege: Compute.Allocate
"""
return await ComputesService(computes_repo).create_compute(compute_create, connect)
@router.post("/{compute_id}/connect", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{compute_id}/connect",
status_code=status.HTTP_204_NO_CONTENT,
#dependencies=[Depends(has_privilege("Compute.Audit"))] # FIXME: this is a temporary workaround due to a bug in the web-ui
)
async def connect_compute(compute_id: Union[str, UUID]) -> None:
"""
Connect to compute on the controller.
Required privilege: Compute.Audit
"""
compute = Controller.instance().get_compute(str(compute_id))
@ -67,29 +78,48 @@ async def connect_compute(compute_id: Union[str, UUID]) -> None:
await compute.connect(report_failed_connection=True)
@router.get("/{compute_id}", response_model=schemas.Compute, response_model_exclude_unset=True)
@router.get(
"/{compute_id}",
response_model=schemas.Compute,
response_model_exclude_unset=True,
#dependencies=[Depends(has_privilege("Compute.Audit"))] # FIXME: this is a temporary workaround due to a bug in the web-ui
)
async def get_compute(
compute_id: Union[str, UUID], computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository))
) -> schemas.Compute:
"""
Return a compute from the controller.
Required privilege: Compute.Audit
"""
return await ComputesService(computes_repo).get_compute(compute_id)
@router.get("", response_model=List[schemas.Compute], response_model_exclude_unset=True)
@router.get(
"",
response_model=List[schemas.Compute],
response_model_exclude_unset=True,
#dependencies=[Depends(has_privilege("Compute.Audit"))] # FIXME: this is a temporary workaround due to a bug in the web-ui
)
async def get_computes(
computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)),
) -> List[schemas.Compute]:
"""
Return all computes known by the controller.
Required privilege: Compute.Audit
"""
return await ComputesService(computes_repo).get_computes()
@router.put("/{compute_id}", response_model=schemas.Compute, response_model_exclude_unset=True)
@router.put(
"/{compute_id}",
response_model=schemas.Compute,
response_model_exclude_unset=True,
dependencies=[Depends(has_privilege("Compute.Modify"))]
)
async def update_compute(
compute_id: Union[str, UUID],
compute_update: schemas.ComputeUpdate,
@ -97,20 +127,31 @@ async def update_compute(
) -> schemas.Compute:
"""
Update a compute on the controller.
Required privilege: Compute.Modify
"""
return await ComputesService(computes_repo).update_compute(compute_id, compute_update)
@router.delete("/{compute_id}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{compute_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Compute.Allocate"))]
)
async def delete_compute(
compute_id: Union[str, UUID], computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository))
compute_id: Union[str, UUID],
computes_repo: ComputesRepository = Depends(get_repository(ComputesRepository)),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> None:
"""
Delete a compute from the controller.
Required privilege: Compute.Allocate
"""
await ComputesService(computes_repo).delete_compute(compute_id)
await rbac_repo.delete_all_ace_starting_with_path(f"/computes/{compute_id}")
@router.get("/{compute_id}/docker/images", response_model=List[schemas.ComputeDockerImage])
@ -157,7 +198,7 @@ async def dynamips_autoidlepc(compute_id: Union[str, UUID], auto_idle_pc: schema
@router.get("/{compute_id}/{emulator}/{endpoint_path:path}", deprecated=True)
async def forward_get(compute_id: Union[str, UUID], emulator: str, endpoint_path: str) -> dict:
async def forward_get(compute_id: Union[str, UUID], emulator: str, endpoint_path: str) -> Any:
"""
Forward a GET request to a compute.
Read the full compute API documentation for available routes.
@ -169,7 +210,7 @@ async def forward_get(compute_id: Union[str, UUID], emulator: str, endpoint_path
@router.post("/{compute_id}/{emulator}/{endpoint_path:path}", deprecated=True)
async def forward_post(compute_id: Union[str, UUID], emulator: str, endpoint_path: str, compute_data: dict) -> dict:
async def forward_post(compute_id: Union[str, UUID], emulator: str, endpoint_path: str, compute_data: dict) -> Any:
"""
Forward a POST request to a compute.
Read the full compute API documentation for available routes.
@ -180,7 +221,7 @@ async def forward_post(compute_id: Union[str, UUID], emulator: str, endpoint_pat
@router.put("/{compute_id}/{emulator}/{endpoint_path:path}", deprecated=True)
async def forward_put(compute_id: Union[str, UUID], emulator: str, endpoint_path: str, compute_data: dict) -> dict:
async def forward_put(compute_id: Union[str, UUID], emulator: str, endpoint_path: str, compute_data: dict) -> Any:
"""
Forward a PUT request to a compute.
Read the full compute API documentation for available routes.

View File

@ -18,9 +18,12 @@ import asyncio
import signal
import os
from fastapi import APIRouter, Depends, Request, Response, status
from fastapi import APIRouter, Request, Depends, WebSocket, WebSocketDisconnect, status
from fastapi.responses import StreamingResponse
from fastapi.encoders import jsonable_encoder
from fastapi.routing import Mount
from websockets.exceptions import ConnectionClosed, WebSocketException
from typing import List
from gns3server.config import Config
@ -29,7 +32,7 @@ from gns3server.version import __version__
from gns3server.controller.controller_error import ControllerError, ControllerForbiddenError
from gns3server import schemas
from .dependencies.authentication import get_current_active_user
from .dependencies.authentication import get_current_active_user, get_current_active_user_from_websocket
import logging
@ -174,6 +177,57 @@ async def statistics() -> List[dict]:
return compute_statistics
@router.get("/notifications", dependencies=[Depends(get_current_active_user)])
async def controller_http_notifications(request: Request) -> StreamingResponse:
"""
Receive controller notifications about the controller from HTTP stream.
"""
from gns3server.api.server import app
log.info(f"New client {request.client.host}:{request.client.port} has connected to controller HTTP "
f"notification stream")
async def event_stream():
try:
with Controller.instance().notification.controller_queue() as queue:
while not app.state.exiting:
msg = await queue.get_json(5)
yield f"{msg}\n".encode("utf-8")
finally:
log.info(f"Client {request.client.host}:{request.client.port} has disconnected from controller HTTP "
f"notification stream")
return StreamingResponse(event_stream(), media_type="application/json")
@router.websocket("/notifications/ws")
async def controller_ws_notifications(
websocket: WebSocket,
current_user: schemas.User = Depends(get_current_active_user_from_websocket)
) -> None:
"""
Receive project notifications about the controller from WebSocket.
"""
if current_user is None:
return
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to controller WebSocket")
try:
with Controller.instance().notification.controller_queue() as queue:
while True:
notification = await queue.get_json(5)
await websocket.send_text(notification)
except (ConnectionClosed, WebSocketDisconnect):
log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller WebSocket")
except WebSocketException as e:
log.warning(f"Error while sending to controller event to WebSocket client: {e}")
finally:
try:
await websocket.close()
except OSError:
pass # ignore OSError: [Errno 107] Transport endpoint is not connected
# @Route.post(
# r"/debug",
# description="Dump debug information to disk (debug directory in config directory). Work only for local server",

View File

@ -14,7 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
import logging
from fastapi import Request, Query, Depends, HTTPException, WebSocket, status
from fastapi.security import OAuth2PasswordBearer
@ -26,7 +26,8 @@ from gns3server.db.repositories.rbac import RbacRepository
from gns3server.services import auth_service
from .database import get_repository
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/v3/users/login", auto_error=False)
log = logging.getLogger(__name__)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/v3/access/users/login", auto_error=False)
async def get_user_from_token(
@ -74,21 +75,6 @@ async def get_current_active_user(
headers={"WWW-Authenticate": "Bearer"},
)
# remove the prefix (e.g. "/v3") from URL path
path = re.sub(r"^/v[0-9]", "", request.url.path)
# special case: always authorize access to the "/users/me" endpoint
if path == "/users/me":
return current_user
authorized = await rbac_repo.check_user_is_authorized(current_user.user_id, request.method, path)
if not authorized:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"User is not authorized '{current_user.user_id}' on {request.method} '{path}'",
headers={"WWW-Authenticate": "Bearer"},
)
return current_user
@ -96,7 +82,6 @@ async def get_current_active_user_from_websocket(
websocket: WebSocket,
token: str = Query(...),
user_repo: UsersRepository = Depends(get_repository(UsersRepository)),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> Optional[schemas.User]:
await websocket.accept()
@ -121,22 +106,12 @@ async def get_current_active_user_from_websocket(
detail=f"'{username}' is not an active user"
)
# remove the prefix (e.g. "/v3") from URL path
path = re.sub(r"^/v[0-9]", "", websocket.url.path)
# there are no HTTP methods for web sockets, assuming "GET"...
authorized = await rbac_repo.check_user_is_authorized(user.user_id, "GET", path)
if not authorized:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"User is not authorized '{user.user_id}' on '{path}'",
headers={"WWW-Authenticate": "Bearer"},
)
return user
except HTTPException as e:
websocket_error = {"action": "log.error", "event": {"message": f"Could not authenticate while connecting to "
f"WebSocket: {e.detail}"}}
err_msg = f"Could not authenticate while connecting to controller WebSocket: {e.detail}"
websocket_error = {"action": "log.error", "event": {"message": err_msg}}
await websocket.send_json(websocket_error)
await websocket.close(code=1008)
log.error(err_msg)
return await websocket.close(code=1008)

View File

@ -0,0 +1,78 @@
#
# Copyright (C) 2023 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
from fastapi import Request, WebSocket, Depends, HTTPException
from gns3server import schemas
from gns3server.db.repositories.rbac import RbacRepository
from .authentication import get_current_active_user, get_current_active_user_from_websocket
from .database import get_repository
import logging
log = logging.getLogger()
def has_privilege(
privilege_name: str
):
async def get_user_and_check_privilege(
request: Request,
current_user: schemas.User = Depends(get_current_active_user),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
):
if not current_user.is_superadmin:
path = re.sub(r"^/v[0-9]", "", request.url.path) # remove the prefix (e.g. "/v3") from URL path
log.debug(f"Checking user {current_user.username} has privilege {privilege_name} on '{path}'")
if not await rbac_repo.check_user_has_privilege(current_user.user_id, path, privilege_name):
raise HTTPException(status_code=403, detail=f"Permission denied (privilege {privilege_name} is required)")
return current_user
return get_user_and_check_privilege
def has_privilege_on_websocket(
privilege_name: str
):
async def get_user_and_check_privilege(
websocket: WebSocket,
current_user: schemas.User = Depends(get_current_active_user_from_websocket),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
):
if not current_user.is_superadmin:
path = re.sub(r"^/v[0-9]", "", websocket.url.path) # remove the prefix (e.g. "/v3") from URL path
log.debug(f"Checking user {current_user.username} has privilege {privilege_name} on '{path}'")
if not await rbac_repo.check_user_has_privilege(current_user.user_id, path, privilege_name):
raise HTTPException(status_code=403, detail=f"Permission denied (privilege {privilege_name} is required)")
return current_user
return get_user_and_check_privilege
# class PrivilegeChecker:
#
# def __init__(self, required_privilege: str) -> None:
# self._required_privilege = required_privilege
#
# async def __call__(
# self,
# current_user: schemas.User = Depends(get_current_active_user),
# rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
# ) -> bool:
#
# if not await rbac_repo.check_user_has_privilege(current_user.user_id, "/projects", self._required_privilege):
# raise HTTPException(status_code=403, detail=f"Permission denied (privilege {self._required_privilege} is required)")
# return True
# Depends(PrivilegeChecker("Project.Audit"))

View File

@ -18,33 +18,54 @@
API routes for drawings.
"""
from fastapi import APIRouter, Response, status
from fastapi import APIRouter, Depends, status
from fastapi.encoders import jsonable_encoder
from typing import List
from uuid import UUID
from gns3server.controller import Controller
from gns3server.db.repositories.rbac import RbacRepository
from gns3server import schemas
from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege
responses = {404: {"model": schemas.ErrorMessage, "description": "Project or drawing not found"}}
router = APIRouter(responses=responses)
@router.get("", response_model=List[schemas.Drawing], response_model_exclude_unset=True)
@router.get(
"",
response_model=List[schemas.Drawing],
response_model_exclude_unset=True,
dependencies=[Depends(has_privilege("Drawing.Audit"))]
)
async def get_drawings(project_id: UUID) -> List[schemas.Drawing]:
"""
Return the list of all drawings for a given project.
Required privilege: Drawing.Audit
"""
project = await Controller.instance().get_loaded_project(str(project_id))
if project.status == "closed":
# allow to retrieve drawings from a closed project
return project.drawings.values()
return [v.asdict() for v in project.drawings.values()]
@router.post("", status_code=status.HTTP_201_CREATED, response_model=schemas.Drawing)
@router.post(
"",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Drawing,
dependencies=[Depends(has_privilege("Drawing.Allocate"))]
)
async def create_drawing(project_id: UUID, drawing_data: schemas.Drawing) -> schemas.Drawing:
"""
Create a new drawing.
Required privilege: Drawing.Allocate
"""
project = await Controller.instance().get_loaded_project(str(project_id))
@ -52,10 +73,17 @@ async def create_drawing(project_id: UUID, drawing_data: schemas.Drawing) -> sch
return drawing.asdict()
@router.get("/{drawing_id}", response_model=schemas.Drawing, response_model_exclude_unset=True)
@router.get(
"/{drawing_id}",
response_model=schemas.Drawing,
response_model_exclude_unset=True,
dependencies=[Depends(has_privilege("Drawing.Audit"))]
)
async def get_drawing(project_id: UUID, drawing_id: UUID) -> schemas.Drawing:
"""
Return a drawing.
Required privilege: Drawing.Audit
"""
project = await Controller.instance().get_loaded_project(str(project_id))
@ -63,10 +91,17 @@ async def get_drawing(project_id: UUID, drawing_id: UUID) -> schemas.Drawing:
return drawing.asdict()
@router.put("/{drawing_id}", response_model=schemas.Drawing, response_model_exclude_unset=True)
@router.put(
"/{drawing_id}",
response_model=schemas.Drawing,
response_model_exclude_unset=True,
dependencies=[Depends(has_privilege("Drawing.Modify"))]
)
async def update_drawing(project_id: UUID, drawing_id: UUID, drawing_data: schemas.Drawing) -> schemas.Drawing:
"""
Update a drawing.
Required privilege: Drawing.Modify
"""
project = await Controller.instance().get_loaded_project(str(project_id))
@ -75,11 +110,22 @@ async def update_drawing(project_id: UUID, drawing_id: UUID, drawing_data: schem
return drawing.asdict()
@router.delete("/{drawing_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_drawing(project_id: UUID, drawing_id: UUID) -> None:
@router.delete(
"/{drawing_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Drawing.Allocate"))]
)
async def delete_drawing(
project_id: UUID,
drawing_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> None:
"""
Delete a drawing.
Required privilege: Drawing.Allocate
"""
project = await Controller.instance().get_loaded_project(str(project_id))
await project.delete_drawing(str(drawing_id))
await rbac_repo.delete_all_ace_starting_with_path(f"/drawings/{drawing_id}")

View File

@ -19,7 +19,7 @@
API routes for user groups.
"""
from fastapi import APIRouter, Depends, Response, status
from fastapi import APIRouter, Depends, status
from uuid import UUID
from typing import List
@ -33,6 +33,8 @@ from gns3server.controller.controller_error import (
from gns3server.db.repositories.users import UsersRepository
from gns3server.db.repositories.rbac import RbacRepository
from .dependencies.rbac import has_privilege
from .dependencies.database import get_repository
import logging
@ -42,12 +44,18 @@ log = logging.getLogger(__name__)
router = APIRouter()
@router.get("", response_model=List[schemas.UserGroup])
@router.get(
"",
response_model=List[schemas.UserGroup],
dependencies=[Depends(has_privilege("Group.Audit"))]
)
async def get_user_groups(
users_repo: UsersRepository = Depends(get_repository(UsersRepository))
) -> List[schemas.UserGroup]:
"""
Get all user groups.
Required privilege: Group.Audit
"""
return await users_repo.get_user_groups()
@ -56,7 +64,8 @@ async def get_user_groups(
@router.post(
"",
response_model=schemas.UserGroup,
status_code=status.HTTP_201_CREATED
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(has_privilege("Group.Allocate"))]
)
async def create_user_group(
user_group_create: schemas.UserGroupCreate,
@ -64,6 +73,8 @@ async def create_user_group(
) -> schemas.UserGroup:
"""
Create a new user group.
Required privilege: Group.Allocate
"""
if await users_repo.get_user_group_by_name(user_group_create.name):
@ -72,13 +83,19 @@ async def create_user_group(
return await users_repo.create_user_group(user_group_create)
@router.get("/{user_group_id}", response_model=schemas.UserGroup)
@router.get(
"/{user_group_id}",
response_model=schemas.UserGroup,
dependencies=[Depends(has_privilege("Group.Audit"))]
)
async def get_user_group(
user_group_id: UUID,
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
) -> schemas.UserGroup:
"""
Get an user group.
Get a user group.
Required privilege: Group.Audit
"""
user_group = await users_repo.get_user_group(user_group_id)
@ -87,14 +104,20 @@ async def get_user_group(
return user_group
@router.put("/{user_group_id}", response_model=schemas.UserGroup)
@router.put(
"/{user_group_id}",
response_model=schemas.UserGroup,
dependencies=[Depends(has_privilege("Group.Modify"))]
)
async def update_user_group(
user_group_id: UUID,
user_group_update: schemas.UserGroupUpdate,
users_repo: UsersRepository = Depends(get_repository(UsersRepository))
) -> schemas.UserGroup:
"""
Update an user group.
Update a user group.
Required privilege: Group.Modify
"""
user_group = await users_repo.get_user_group(user_group_id)
if not user_group:
@ -108,14 +131,18 @@ async def update_user_group(
@router.delete(
"/{user_group_id}",
status_code=status.HTTP_204_NO_CONTENT
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Group.Allocate"))]
)
async def delete_user_group(
user_group_id: UUID,
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
user_group_id: UUID,
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> None:
"""
Delete an user group
Delete a user group.
Required privilege: Group.Allocate
"""
user_group = await users_repo.get_user_group(user_group_id)
@ -128,15 +155,22 @@ async def delete_user_group(
success = await users_repo.delete_user_group(user_group_id)
if not success:
raise ControllerError(f"User group '{user_group_id}' could not be deleted")
await rbac_repo.delete_all_ace_starting_with_path(f"/groups/{user_group_id}")
@router.get("/{user_group_id}/members", response_model=List[schemas.User])
@router.get(
"/{user_group_id}/members",
response_model=List[schemas.User],
dependencies=[Depends(has_privilege("Group.Audit"))]
)
async def get_user_group_members(
user_group_id: UUID,
users_repo: UsersRepository = Depends(get_repository(UsersRepository))
) -> List[schemas.User]:
"""
Get all user group members.
Required privilege: Group.Audit
"""
return await users_repo.get_user_group_members(user_group_id)
@ -144,7 +178,8 @@ async def get_user_group_members(
@router.put(
"/{user_group_id}/members/{user_id}",
status_code=status.HTTP_204_NO_CONTENT
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Group.Modify"))]
)
async def add_member_to_group(
user_group_id: UUID,
@ -152,13 +187,20 @@ async def add_member_to_group(
users_repo: UsersRepository = Depends(get_repository(UsersRepository))
) -> None:
"""
Add member to an user group.
Add member to a user group.
Required privilege: Group.Modify
"""
user = await users_repo.get_user(user_id)
if not user:
raise ControllerNotFoundError(f"User '{user_id}' not found")
user_groups = await users_repo.get_user_memberships(user_id)
for group in user_groups:
if group.user_group_id == user_group_id:
raise ControllerBadRequestError(f"Username '{user.username}' is already member of group '{group.name}'")
user_group = await users_repo.add_member_to_user_group(user_group_id, user)
if not user_group:
raise ControllerNotFoundError(f"User group '{user_group_id}' not found")
@ -166,7 +208,8 @@ async def add_member_to_group(
@router.delete(
"/{user_group_id}/members/{user_id}",
status_code=status.HTTP_204_NO_CONTENT
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Group.Modify"))]
)
async def remove_member_from_group(
user_group_id: UUID,
@ -174,7 +217,9 @@ async def remove_member_from_group(
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
) -> None:
"""
Remove member from an user group.
Remove member from a user group.
Required privilege: Group.Modify
"""
user = await users_repo.get_user(user_id)
@ -184,61 +229,3 @@ async def remove_member_from_group(
user_group = await users_repo.remove_member_from_user_group(user_group_id, user)
if not user_group:
raise ControllerNotFoundError(f"User group '{user_group_id}' not found")
@router.get("/{user_group_id}/roles", response_model=List[schemas.Role])
async def get_user_group_roles(
user_group_id: UUID,
users_repo: UsersRepository = Depends(get_repository(UsersRepository))
) -> List[schemas.Role]:
"""
Get all user group roles.
"""
return await users_repo.get_user_group_roles(user_group_id)
@router.put(
"/{user_group_id}/roles/{role_id}",
status_code=status.HTTP_204_NO_CONTENT
)
async def add_role_to_group(
user_group_id: UUID,
role_id: UUID,
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> Response:
"""
Add role to an user group.
"""
role = await rbac_repo.get_role(role_id)
if not role:
raise ControllerNotFoundError(f"Role '{role_id}' not found")
user_group = await users_repo.add_role_to_user_group(user_group_id, role)
if not user_group:
raise ControllerNotFoundError(f"User group '{user_group_id}' not found")
@router.delete(
"/{user_group_id}/roles/{role_id}",
status_code=status.HTTP_204_NO_CONTENT
)
async def remove_role_from_group(
user_group_id: UUID,
role_id: UUID,
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> None:
"""
Remove role from an user group.
"""
role = await rbac_repo.get_role(role_id)
if not role:
raise ControllerNotFoundError(f"Role '{role_id}' not found")
user_group = await users_repo.remove_role_from_user_group(user_group_id, role)
if not user_group:
raise ControllerNotFoundError(f"User group '{user_group_id}' not found")

View File

@ -22,14 +22,16 @@ import os
import logging
import urllib.parse
from fastapi import APIRouter, Request, Response, Depends, status
from fastapi import APIRouter, Request, Depends, status
from fastapi.encoders import jsonable_encoder
from starlette.requests import ClientDisconnect
from sqlalchemy.orm.exc import MultipleResultsFound
from typing import List, Optional
from gns3server import schemas
from gns3server.config import Config
from gns3server.utils.images import InvalidImageError, write_image
from gns3server.compute.qemu import Qemu
from gns3server.utils.images import InvalidImageError, write_image, read_image_info, default_images_directory
from gns3server.db.repositories.images import ImagesRepository
from gns3server.db.repositories.templates import TemplatesRepository
from gns3server.db.repositories.rbac import RbacRepository
@ -43,25 +45,84 @@ from gns3server.controller.controller_error import (
from .dependencies.authentication import get_current_active_user
from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege
log = logging.getLogger(__name__)
router = APIRouter()
@router.get("", response_model=List[schemas.Image])
@router.post(
"/qemu/{image_path:path}",
response_model=schemas.Image,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(has_privilege("Image.Allocate"))]
)
async def create_qemu_image(
image_path: str,
image_data: schemas.QemuDiskImageCreate,
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
) -> schemas.Image:
"""
Create a new blank Qemu image.
Required privilege: Image.Allocate
"""
allow_raw_image = Config.instance().settings.Server.allow_raw_images
if image_data.format == schemas.QemuDiskImageFormat.raw and not allow_raw_image:
raise ControllerBadRequestError("Raw images are not allowed")
disk_image_path = urllib.parse.unquote(image_path)
image_dir, image_name = os.path.split(disk_image_path)
# check if the path is within the default images directory
base_images_directory = os.path.expanduser(Config.instance().settings.Server.images_path)
full_path = os.path.abspath(os.path.join(base_images_directory, image_dir, image_name))
if os.path.commonprefix([base_images_directory, full_path]) != base_images_directory:
raise ControllerForbiddenError(f"Cannot write disk image, '{disk_image_path}' is forbidden")
if not image_dir:
# put the image in the default images directory for Qemu
directory = default_images_directory(image_type="qemu")
os.makedirs(directory, exist_ok=True)
disk_image_path = os.path.abspath(os.path.join(directory, disk_image_path))
if await images_repo.get_image(disk_image_path):
raise ControllerBadRequestError(f"Disk image '{disk_image_path}' already exists")
options = jsonable_encoder(image_data, exclude_unset=True)
# FIXME: should we have the create_disk_image in the compute code since
# this code is used to create images on the controller?
await Qemu.instance().create_disk_image(disk_image_path, options)
image_info = await read_image_info(disk_image_path, "qemu")
return await images_repo.add_image(**image_info)
@router.get(
"",
response_model=List[schemas.Image],
dependencies=[Depends(has_privilege("Image.Audit"))]
)
async def get_images(
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
image_type: Optional[schemas.ImageType] = None
) -> List[schemas.Image]:
"""
Return all images.
Required privilege: Image.Audit
"""
return await images_repo.get_images(image_type)
@router.post("/upload/{image_path:path}", response_model=schemas.Image, status_code=status.HTTP_201_CREATED)
@router.post(
"/upload/{image_path:path}",
response_model=schemas.Image,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(has_privilege("Image.Allocate"))]
)
async def upload_image(
image_path: str,
request: Request,
@ -70,13 +131,14 @@ async def upload_image(
current_user: schemas.User = Depends(get_current_active_user),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
install_appliances: Optional[bool] = False,
allow_raw_image: Optional[bool] = False
) -> schemas.Image:
"""
Upload an image.
Example: curl -X POST http://host:port/v3/images/upload/my_image_name.qcow2 \
-H 'Authorization: Bearer <token>' --data-binary @"/path/to/image.qcow2"
Required privilege: Image.Allocate
"""
image_path = urllib.parse.unquote(image_path)
@ -91,6 +153,7 @@ async def upload_image(
raise ControllerBadRequestError(f"Image '{image_path}' already exists")
try:
allow_raw_image = Config.instance().settings.Server.allow_raw_images
image = await write_image(image_path, full_path, request.stream(), images_repo, allow_raw_image=allow_raw_image)
except (OSError, InvalidImageError, ClientDisconnect) as e:
raise ControllerError(f"Could not save image '{image_path}': {e}")
@ -110,13 +173,19 @@ async def upload_image(
return image
@router.get("/{image_path:path}", response_model=schemas.Image)
@router.get(
"/{image_path:path}",
response_model=schemas.Image,
dependencies=[Depends(has_privilege("Image.Audit"))]
)
async def get_image(
image_path: str,
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
) -> schemas.Image:
"""
Return an image.
Required privilege: Image.Audit
"""
image_path = urllib.parse.unquote(image_path)
@ -126,13 +195,19 @@ async def get_image(
return image
@router.delete("/{image_path:path}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{image_path:path}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Image.Allocate"))]
)
async def delete_image(
image_path: str,
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
) -> None:
"""
Delete an image.
Required privilege: Image.Allocate
"""
image_path = urllib.parse.unquote(image_path)
@ -161,12 +236,18 @@ async def delete_image(
raise ControllerError(f"Image '{image_path}' could not be deleted")
@router.post("/prune", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/prune",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Image.Allocate"))]
)
async def prune_images(
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
) -> None:
"""
Prune images not attached to any template.
Required privilege: Image.Allocate
"""
await images_repo.prune_images()

View File

@ -1,5 +1,5 @@
#
# Copyright (C) 2016 GNS3 Technologies Inc.
# Copyright (C) 2023 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -21,7 +21,7 @@ API routes for links.
import multidict
import aiohttp
from fastapi import APIRouter, Depends, Request, Response, status
from fastapi import APIRouter, Depends, Request, status
from fastapi.responses import StreamingResponse
from fastapi.encoders import jsonable_encoder
from typing import List
@ -29,10 +29,14 @@ from uuid import UUID
from gns3server.controller import Controller
from gns3server.controller.controller_error import ControllerError
from gns3server.db.repositories.rbac import RbacRepository
from gns3server.controller.link import Link
from gns3server.utils.http_client import HTTPClient
from gns3server import schemas
from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege
import logging
log = logging.getLogger(__name__)
@ -52,13 +56,23 @@ async def dep_link(project_id: UUID, link_id: UUID) -> Link:
return link
@router.get("", response_model=List[schemas.Link], response_model_exclude_unset=True)
@router.get(
"",
response_model=List[schemas.Link],
response_model_exclude_unset=True,
dependencies=[Depends(has_privilege("Link.Audit"))]
)
async def get_links(project_id: UUID) -> List[schemas.Link]:
"""
Return all links for a given project.
Required privilege: Link.Audit
"""
project = await Controller.instance().get_loaded_project(str(project_id))
if project.status == "closed":
# allow to retrieve links from a closed project
return project.links.values()
return [v.asdict() for v in project.links.values()]
@ -70,10 +84,13 @@ async def get_links(project_id: UUID) -> List[schemas.Link]:
404: {"model": schemas.ErrorMessage, "description": "Could not find project"},
409: {"model": schemas.ErrorMessage, "description": "Could not create link"},
},
dependencies=[Depends(has_privilege("Link.Allocate"))]
)
async def create_link(project_id: UUID, link_data: schemas.LinkCreate) -> schemas.Link:
"""
Create a new link.
Required privilege: Link.Allocate
"""
project = await Controller.instance().get_loaded_project(str(project_id))
@ -99,28 +116,47 @@ async def create_link(project_id: UUID, link_data: schemas.LinkCreate) -> schema
return link.asdict()
@router.get("/{link_id}/available_filters")
@router.get(
"/{link_id}/available_filters",
dependencies=[Depends(has_privilege("Link.Audit"))]
)
async def get_filters(link: Link = Depends(dep_link)) -> List[dict]:
"""
Return all filters available for a given link.
Required privilege: Link.Audit
"""
return link.available_filters()
@router.get("/{link_id}", response_model=schemas.Link, response_model_exclude_unset=True)
@router.get(
"/{link_id}",
response_model=schemas.Link,
response_model_exclude_unset=True,
dependencies=[Depends(has_privilege("Link.Audit"))]
)
async def get_link(link: Link = Depends(dep_link)) -> schemas.Link:
"""
Return a link.
Required privilege: Link.Audit
"""
return link.asdict()
@router.put("/{link_id}", response_model=schemas.Link, response_model_exclude_unset=True)
@router.put(
"/{link_id}",
response_model=schemas.Link,
response_model_exclude_unset=True,
dependencies=[Depends(has_privilege("Link.Modify"))]
)
async def update_link(link_data: schemas.LinkUpdate, link: Link = Depends(dep_link)) -> schemas.Link:
"""
Update a link.
Required privilege: Link.Modify
"""
link_data = jsonable_encoder(link_data, exclude_unset=True)
@ -135,30 +171,54 @@ async def update_link(link_data: schemas.LinkUpdate, link: Link = Depends(dep_li
return link.asdict()
@router.delete("/{link_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_link(project_id: UUID, link: Link = Depends(dep_link)) -> None:
@router.delete(
"/{link_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Link.Allocate"))]
)
async def delete_link(
project_id: UUID,
link: Link = Depends(dep_link),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> None:
"""
Delete a link.
Required privilege: Link.Allocate
"""
project = await Controller.instance().get_loaded_project(str(project_id))
await project.delete_link(link.id)
await rbac_repo.delete_all_ace_starting_with_path(f"/links/{link.id}")
@router.post("/{link_id}/reset", response_model=schemas.Link)
@router.post(
"/{link_id}/reset",
response_model=schemas.Link,
dependencies=[Depends(has_privilege("Link.Modify"))]
)
async def reset_link(link: Link = Depends(dep_link)) -> schemas.Link:
"""
Reset a link.
Required privilege: Link.Modify
"""
await link.reset()
return link.asdict()
@router.post("/{link_id}/capture/start", status_code=status.HTTP_201_CREATED, response_model=schemas.Link)
@router.post(
"/{link_id}/capture/start",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Link,
dependencies=[Depends(has_privilege("Link.Capture"))]
)
async def start_capture(capture_data: dict, link: Link = Depends(dep_link)) -> schemas.Link:
"""
Start packet capture on the link.
Required privilege: Link.Capture
"""
await link.start_capture(
@ -168,19 +228,30 @@ async def start_capture(capture_data: dict, link: Link = Depends(dep_link)) -> s
return link.asdict()
@router.post("/{link_id}/capture/stop", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{link_id}/capture/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Link.Capture"))]
)
async def stop_capture(link: Link = Depends(dep_link)) -> None:
"""
Stop packet capture on the link.
Required privilege: Link.Capture
"""
await link.stop_capture()
@router.get("/{link_id}/capture/stream")
@router.get(
"/{link_id}/capture/stream",
dependencies=[Depends(has_privilege("Link.Capture"))]
)
async def stream_pcap(request: Request, link: Link = Depends(dep_link)) -> StreamingResponse:
"""
Stream the PCAP capture file from compute.
Required privilege: Link.Capture
"""
if not link.capturing:
@ -196,11 +267,13 @@ async def stream_pcap(request: Request, link: Link = Depends(dep_link)) -> Strea
async def compute_pcap_stream():
try:
ssl_context = Controller.instance().ssl_context()
async with HTTPClient.request(
request.method,
pcap_streaming_url,
user=compute.user,
password=compute.password,
ssl_context=ssl_context,
timeout=None,
data=body
) as response:

View File

@ -20,6 +20,7 @@ API routes for nodes.
import aiohttp
import asyncio
import ipaddress
from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect, Request, Response, status
from fastapi.encoders import jsonable_encoder
@ -28,13 +29,18 @@ from typing import List, Callable
from uuid import UUID
from gns3server.controller import Controller
from gns3server.config import Config
from gns3server.controller.node import Node
from gns3server.controller.project import Project
from gns3server.utils import force_unix_path
from gns3server.utils.http_client import HTTPClient
from gns3server.controller.controller_error import ControllerForbiddenError, ControllerBadRequestError
from gns3server.db.repositories.rbac import RbacRepository
from gns3server import schemas
from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege, has_privilege_on_websocket
import logging
log = logging.getLogger(__name__)
@ -107,10 +113,13 @@ async def dep_node(node_id: UUID, project: Project = Depends(dep_project)) -> No
404: {"model": schemas.ErrorMessage, "description": "Could not find project"},
409: {"model": schemas.ErrorMessage, "description": "Could not create node"},
},
dependencies=[Depends(has_privilege("Node.Allocate"))]
)
async def create_node(node_data: schemas.NodeCreate, project: Project = Depends(dep_project)) -> schemas.Node:
"""
Create a new node.
Required privilege: Node.Allocate
"""
controller = Controller.instance()
@ -120,65 +129,92 @@ async def create_node(node_data: schemas.NodeCreate, project: Project = Depends(
return node.asdict()
@router.get("", response_model=List[schemas.Node], response_model_exclude_unset=True)
async def get_nodes(project: Project = Depends(dep_project)) -> List[schemas.Node]:
@router.get(
"",
response_model=List[schemas.Node],
response_model_exclude_unset=True,
dependencies=[Depends(has_privilege("Node.Audit"))]
)
def get_nodes(project: Project = Depends(dep_project)) -> List[schemas.Node]:
"""
Return all nodes belonging to a given project.
Required privilege: Node.Audit
"""
if project.status == "closed":
# allow to retrieve nodes from a closed project
return project.nodes.values()
return [v.asdict() for v in project.nodes.values()]
@router.post("/start", status_code=status.HTTP_204_NO_CONTENT)
@router.post("/start", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node.PowerMgmt"))])
async def start_all_nodes(project: Project = Depends(dep_project)) -> None:
"""
Start all nodes belonging to a given project.
Required privilege: Node.PowerMgmt
"""
await project.start_all()
@router.post("/stop", status_code=status.HTTP_204_NO_CONTENT)
@router.post("/stop", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node.PowerMgmt"))])
async def stop_all_nodes(project: Project = Depends(dep_project)) -> None:
"""
Stop all nodes belonging to a given project.
Required privilege: Node.PowerMgmt
"""
await project.stop_all()
@router.post("/suspend", status_code=status.HTTP_204_NO_CONTENT)
@router.post("/suspend", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node.PowerMgmt"))])
async def suspend_all_nodes(project: Project = Depends(dep_project)) -> None:
"""
Suspend all nodes belonging to a given project.
Required privilege: Node.PowerMgmt
"""
await project.suspend_all()
@router.post("/reload", status_code=status.HTTP_204_NO_CONTENT)
@router.post("/reload", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(has_privilege("Node.PowerMgmt"))])
async def reload_all_nodes(project: Project = Depends(dep_project)) -> None:
"""
Reload all nodes belonging to a given project.
Required privilege: Node.PowerMgmt
"""
await project.stop_all()
await project.start_all()
@router.get("/{node_id}", response_model=schemas.Node)
@router.get("/{node_id}", response_model=schemas.Node, dependencies=[Depends(has_privilege("Node.Audit"))])
def get_node(node: Node = Depends(dep_node)) -> schemas.Node:
"""
Return a node from a given project.
Required privilege: Node.Audit
"""
return node.asdict()
@router.put("/{node_id}", response_model=schemas.Node, response_model_exclude_unset=True)
@router.put(
"/{node_id}",
response_model=schemas.Node,
response_model_exclude_unset=True,
dependencies=[Depends(has_privilege("Node.Modify"))]
)
async def update_node(node_data: schemas.NodeUpdate, node: Node = Depends(dep_node)) -> schemas.Node:
"""
Update a node.
Required privilege: Node.Modify
"""
node_data = jsonable_encoder(node_data, exclude_unset=True)
@ -196,85 +232,142 @@ async def update_node(node_data: schemas.NodeUpdate, node: Node = Depends(dep_no
"/{node_id}",
status_code=status.HTTP_204_NO_CONTENT,
responses={**responses, 409: {"model": schemas.ErrorMessage, "description": "Cannot delete node"}},
dependencies=[Depends(has_privilege("Node.Allocate"))]
)
async def delete_node(node_id: UUID, project: Project = Depends(dep_project)) -> None:
async def delete_node(
node_id: UUID, project: Project = Depends(dep_project),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> None:
"""
Delete a node from a project.
Required privilege: Node.Allocate
"""
await project.delete_node(str(node_id))
await rbac_repo.delete_all_ace_starting_with_path(f"/projects/{project.id}/nodes/{node_id}")
@router.post("/{node_id}/duplicate", response_model=schemas.Node, status_code=status.HTTP_201_CREATED)
@router.post(
"/{node_id}/duplicate",
response_model=schemas.Node,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(has_privilege("Node.Allocate"))]
)
async def duplicate_node(duplicate_data: schemas.NodeDuplicate, node: Node = Depends(dep_node)) -> schemas.Node:
"""
Duplicate a node.
Required privilege: Node.Allocate
"""
new_node = await node.project.duplicate_node(node, duplicate_data.x, duplicate_data.y, duplicate_data.z)
return new_node.asdict()
@router.post("/{node_id}/start", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/start",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Node.PowerMgmt"))]
)
async def start_node(start_data: dict, node: Node = Depends(dep_node)) -> None:
"""
Start a node.
Required privilege: Node.PowerMgmt
"""
await node.start(data=start_data)
@router.post("/{node_id}/stop", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/stop",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Node.PowerMgmt"))]
)
async def stop_node(node: Node = Depends(dep_node)) -> None:
"""
Stop a node.
Required privilege: Node.PowerMgmt
"""
await node.stop()
@router.post("/{node_id}/suspend", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/suspend",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Node.PowerMgmt"))]
)
async def suspend_node(node: Node = Depends(dep_node)) -> None:
"""
Suspend a node.
Required privilege: Node.PowerMgmt
"""
await node.suspend()
@router.post("/{node_id}/reload", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/reload",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Node.PowerMgmt"))]
)
async def reload_node(node: Node = Depends(dep_node)) -> None:
"""
Reload a node.
Required privilege: Node.PowerMgmt
"""
await node.reload()
@router.post("/{node_id}/isolate", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/isolate",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Link.Modify"))]
)
async def isolate_node(node: Node = Depends(dep_node)) -> None:
"""
Isolate a node (suspend all attached links).
Required privilege: Link.Modify
"""
for link in node.links:
await link.update_suspend(True)
@router.post("/{node_id}/unisolate", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/unisolate",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Link.Modify"))]
)
async def unisolate_node(node: Node = Depends(dep_node)) -> None:
"""
Un-isolate a node (resume all attached suspended links).
Required privilege: Link.Modify
"""
for link in node.links:
await link.update_suspend(False)
@router.get("/{node_id}/links", response_model=List[schemas.Link], response_model_exclude_unset=True)
@router.get(
"/{node_id}/links",
response_model=List[schemas.Link],
response_model_exclude_unset=True,
dependencies = [Depends(has_privilege("Link.Audit"))]
)
async def get_node_links(node: Node = Depends(dep_node)) -> List[schemas.Link]:
"""
Return all the links connected to a node.
Required privilege: Link.Audit
"""
links = []
@ -283,10 +376,12 @@ async def get_node_links(node: Node = Depends(dep_node)) -> List[schemas.Link]:
return links
@router.get("/{node_id}/dynamips/auto_idlepc")
async def auto_idlepc(node: Node = Depends(dep_node)) -> str:
@router.get("/{node_id}/dynamips/auto_idlepc", dependencies=[Depends(has_privilege("Node.Audit"))])
async def auto_idlepc(node: Node = Depends(dep_node)) -> dict:
"""
Compute an Idle-PC value for a Dynamips node
Required privilege: Node.Audit
"""
if node.node_type != "dynamips":
@ -294,10 +389,12 @@ async def auto_idlepc(node: Node = Depends(dep_node)) -> str:
return await node.dynamips_auto_idlepc()
@router.get("/{node_id}/dynamips/idlepc_proposals")
@router.get("/{node_id}/dynamips/idlepc_proposals", dependencies=[Depends(has_privilege("Node.Audit"))])
async def idlepc_proposals(node: Node = Depends(dep_node)) -> List[str]:
"""
Compute a list of potential idle-pc values for a Dynamips node
Required privilege: Node.Audit
"""
if node.node_type != "dynamips":
@ -305,7 +402,11 @@ async def idlepc_proposals(node: Node = Depends(dep_node)) -> List[str]:
return await node.dynamips_idlepc_proposals()
@router.post("/{node_id}/qemu/disk_image/{disk_name}", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/qemu/disk_image/{disk_name}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Node.Allocate"))]
)
async def create_disk_image(
disk_name: str,
disk_data: schemas.QemuDiskImageCreate,
@ -313,14 +414,20 @@ async def create_disk_image(
) -> None:
"""
Create a Qemu disk image.
Required privilege: Node.Allocate
"""
if node.node_type != "qemu":
raise ControllerBadRequestError("Creating a disk image is only supported on a Qemu node")
await node.post(f"/disk_image/{disk_name}", data=disk_data.dict(exclude_unset=True))
await node.post(f"/disk_image/{disk_name}", data=disk_data.model_dump(exclude_unset=True))
@router.put("/{node_id}/qemu/disk_image/{disk_name}", status_code=status.HTTP_204_NO_CONTENT)
@router.put(
"/{node_id}/qemu/disk_image/{disk_name}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Node.Allocate"))]
)
async def update_disk_image(
disk_name: str,
disk_data: schemas.QemuDiskImageUpdate,
@ -328,20 +435,28 @@ async def update_disk_image(
) -> None:
"""
Update a Qemu disk image.
Required privilege: Node.Allocate
"""
if node.node_type != "qemu":
raise ControllerBadRequestError("Updating a disk image is only supported on a Qemu node")
await node.put(f"/disk_image/{disk_name}", data=disk_data.dict(exclude_unset=True))
await node.put(f"/disk_image/{disk_name}", data=disk_data.model_dump(exclude_unset=True))
@router.delete("/{node_id}/qemu/disk_image/{disk_name}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{node_id}/qemu/disk_image/{disk_name}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Node.Allocate"))]
)
async def delete_disk_image(
disk_name: str,
node: Node = Depends(dep_node)
) -> None:
"""
Delete a Qemu disk image.
Required privilege: Node.Allocate
"""
if node.node_type != "qemu":
@ -349,10 +464,12 @@ async def delete_disk_image(
await node.delete(f"/disk_image/{disk_name}")
@router.get("/{node_id}/files/{file_path:path}")
@router.get("/{node_id}/files/{file_path:path}", dependencies=[Depends(has_privilege("Node.Audit"))])
async def get_file(file_path: str, node: Node = Depends(dep_node)) -> Response:
"""
Return a file in the node directory
Return a file from the node directory.
Required privilege: Node.Audit
"""
path = force_unix_path(file_path)
@ -368,10 +485,16 @@ async def get_file(file_path: str, node: Node = Depends(dep_node)) -> Response:
return Response(res.body, media_type="application/octet-stream", status_code=res.status)
@router.post("/{node_id}/files/{file_path:path}", status_code=status.HTTP_201_CREATED)
@router.post(
"/{node_id}/files/{file_path:path}",
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(has_privilege("Node.Modify"))]
)
async def post_file(file_path: str, request: Request, node: Node = Depends(dep_node)):
"""
Write a file in the node directory.
Required privilege: Node.Modify
"""
path = force_unix_path(file_path)
@ -389,18 +512,36 @@ async def post_file(file_path: str, request: Request, node: Node = Depends(dep_n
@router.websocket("/{node_id}/console/ws")
async def ws_console(websocket: WebSocket, node: Node = Depends(dep_node)) -> None:
async def ws_console(
websocket: WebSocket,
current_user: schemas.User = Depends(has_privilege_on_websocket("Node.Console")),
node: Node = Depends(dep_node)
) -> None:
"""
WebSocket console.
Required privilege: Node.Console
"""
if current_user is None:
return
compute = node.compute
await websocket.accept()
log.info(
f"New client {websocket.client.host}:{websocket.client.port} has connected to controller console WebSocket"
)
compute_host = compute.host
try:
# handle IPv6 address
ip = ipaddress.ip_address(compute_host)
if isinstance(ip, ipaddress.IPv6Address):
compute_host = '[' + compute_host + ']'
except ValueError:
pass
ws_console_compute_url = (
f"ws://{compute.host}:{compute.port}/v3/compute/projects/"
f"{websocket.url.scheme}://{compute_host}:{compute.port}/v3/compute/projects/"
f"{node.project.id}/{node.node_type}/nodes/{node.id}/console/ws"
)
@ -423,9 +564,21 @@ async def ws_console(websocket: WebSocket, node: Node = Depends(dep_node)) -> No
try:
# receive WebSocket data from compute console WebSocket and forward to client.
async with HTTPClient.get_client().ws_connect(ws_console_compute_url) as ws_console_compute:
asyncio.ensure_future(ws_receive(ws_console_compute))
async for msg in ws_console_compute:
log.info(f"Forwarding console WebSocket to '{ws_console_compute_url}'")
server_config = Config.instance().settings.Server
user = server_config.compute_username
password = server_config.compute_password
if not user:
raise ControllerForbiddenError("Compute username is not set")
user = user.strip()
if user and password:
auth = aiohttp.BasicAuth(user, password.get_secret_value(), "utf-8")
else:
auth = aiohttp.BasicAuth(user, "")
ssl_context = Controller.instance().ssl_context()
async with HTTPClient.get_client().ws_connect(ws_console_compute_url, auth=auth, ssl_context=ssl_context) as ws:
asyncio.ensure_future(ws_receive(ws))
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
await websocket.send_text(msg.data)
elif msg.type == aiohttp.WSMsgType.BINARY:
@ -436,16 +589,31 @@ async def ws_console(websocket: WebSocket, node: Node = Depends(dep_node)) -> No
log.error(f"Client error received when forwarding to compute console WebSocket: {e}")
@router.post("/console/reset", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Node.Console"))]
)
async def reset_console_all_nodes(project: Project = Depends(dep_project)) -> None:
"""
Reset console for all nodes belonging to the project.
Required privilege: Node.Console
"""
await project.reset_console_all()
@router.post("/{node_id}/console/reset", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{node_id}/console/reset",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Node.Console"))]
)
async def console_reset(node: Node = Depends(dep_node)) -> None:
"""
Reset a console for a given node.
Required privilege: Node.Console
"""
await node.post("/console/reset")

View File

@ -1,78 +0,0 @@
#
# Copyright (C) 2020 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
API routes for controller notifications.
"""
from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect
from fastapi.responses import StreamingResponse
from websockets.exceptions import ConnectionClosed, WebSocketException
from gns3server.controller import Controller
from gns3server import schemas
from .dependencies.authentication import get_current_active_user, get_current_active_user_from_websocket
import logging
log = logging.getLogger(__name__)
router = APIRouter()
@router.get("", dependencies=[Depends(get_current_active_user)])
async def controller_http_notifications() -> StreamingResponse:
"""
Receive controller notifications about the controller from HTTP stream.
"""
async def event_stream():
with Controller.instance().notification.controller_queue() as queue:
while True:
msg = await queue.get_json(5)
yield f"{msg}\n".encode("utf-8")
return StreamingResponse(event_stream(), media_type="application/json")
@router.websocket("/ws")
async def controller_ws_notifications(
websocket: WebSocket,
current_user: schemas.User = Depends(get_current_active_user_from_websocket)
) -> None:
"""
Receive project notifications about the controller from WebSocket.
"""
if current_user is None:
return
log.info(f"New client {websocket.client.host}:{websocket.client.port} has connected to controller WebSocket")
try:
with Controller.instance().notification.controller_queue() as queue:
while True:
notification = await queue.get_json(5)
await websocket.send_text(notification)
except (ConnectionClosed, WebSocketDisconnect):
log.info(f"Client {websocket.client.host}:{websocket.client.port} has disconnected from controller WebSocket")
except WebSocketException as e:
log.warning(f"Error while sending to controller event to WebSocket client: {e}")
finally:
try:
await websocket.close()
except OSError:
pass # ignore OSError: [Errno 107] Transport endpoint is not connected

View File

@ -1,161 +0,0 @@
#!/usr/bin/env python
#
# Copyright (C) 2021 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
API routes for permissions.
"""
import re
from fastapi import APIRouter, Depends, Response, Request, status
from fastapi.routing import APIRoute
from uuid import UUID
from typing import List
from gns3server import schemas
from gns3server.controller.controller_error import (
ControllerBadRequestError,
ControllerNotFoundError,
ControllerForbiddenError,
)
from gns3server.db.repositories.rbac import RbacRepository
from .dependencies.database import get_repository
from .dependencies.authentication import get_current_active_user
import logging
log = logging.getLogger(__name__)
router = APIRouter()
@router.get("", response_model=List[schemas.Permission])
async def get_permissions(
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> List[schemas.Permission]:
"""
Get all permissions.
"""
return await rbac_repo.get_permissions()
@router.post("", response_model=schemas.Permission, status_code=status.HTTP_201_CREATED)
async def create_permission(
request: Request,
permission_create: schemas.PermissionCreate,
current_user: schemas.User = Depends(get_current_active_user),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> schemas.Permission:
"""
Create a new permission.
"""
# TODO: should we prevent having multiple permissions with same methods/path?
#if await rbac_repo.check_permission_exists(permission_create):
# raise ControllerBadRequestError(f"Permission '{permission_create.methods} {permission_create.path} "
# f"{permission_create.action}' already exists")
for route in request.app.routes:
if isinstance(route, APIRoute):
# remove the prefix (e.g. "/v3") from the route path
route_path = re.sub(r"^/v[0-9]", "", route.path)
# replace route path ID parameters by an UUID regex
route_path = re.sub(r"{\w+_id}", "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}", route_path)
# replace remaining route path parameters by an word matching regex
route_path = re.sub(r"/{[\w:]+}", r"/\\w+", route_path)
# the permission can match multiple routes
if permission_create.path.endswith("/*"):
route_path += r"/.*"
if re.fullmatch(route_path, permission_create.path):
for method in permission_create.methods:
if method in list(route.methods):
# check user has the right to add the permission (i.e has already to right on the path)
if not await rbac_repo.check_user_is_authorized(current_user.user_id, method, permission_create.path):
raise ControllerForbiddenError(f"User '{current_user.username}' doesn't have the rights to "
f"add a permission on {method} {permission_create.path} or "
f"the endpoint doesn't exist")
return await rbac_repo.create_permission(permission_create)
raise ControllerBadRequestError(f"Permission '{permission_create.methods} {permission_create.path}' "
f"doesn't match any existing endpoint")
@router.get("/{permission_id}", response_model=schemas.Permission)
async def get_permission(
permission_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> schemas.Permission:
"""
Get a permission.
"""
permission = await rbac_repo.get_permission(permission_id)
if not permission:
raise ControllerNotFoundError(f"Permission '{permission_id}' not found")
return permission
@router.put("/{permission_id}", response_model=schemas.Permission)
async def update_permission(
permission_id: UUID,
permission_update: schemas.PermissionUpdate,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> schemas.Permission:
"""
Update a permission.
"""
permission = await rbac_repo.get_permission(permission_id)
if not permission:
raise ControllerNotFoundError(f"Permission '{permission_id}' not found")
return await rbac_repo.update_permission(permission_id, permission_update)
@router.delete("/{permission_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_permission(
permission_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> None:
"""
Delete a permission.
"""
permission = await rbac_repo.get_permission(permission_id)
if not permission:
raise ControllerNotFoundError(f"Permission '{permission_id}' not found")
success = await rbac_repo.delete_permission(permission_id)
if not success:
raise ControllerNotFoundError(f"Permission '{permission_id}' could not be deleted")
@router.post("/prune", status_code=status.HTTP_204_NO_CONTENT)
async def prune_permissions(
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> None:
"""
Prune orphaned permissions.
"""
await rbac_repo.prune_permissions()

View File

@ -0,0 +1,239 @@
#!/usr/bin/env python
#
# Copyright (C) 2023 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
API routes for resource pools.
"""
from fastapi import APIRouter, Depends, status
from uuid import UUID
from typing import List
from gns3server import schemas
from gns3server.controller.controller_error import (
ControllerError,
ControllerBadRequestError,
ControllerNotFoundError
)
from gns3server.controller import Controller
from gns3server.db.repositories.rbac import RbacRepository
from gns3server.db.repositories.pools import ResourcePoolsRepository
from .dependencies.rbac import has_privilege
from .dependencies.database import get_repository
import logging
log = logging.getLogger(__name__)
router = APIRouter()
@router.get(
"",
response_model=List[schemas.ResourcePool],
dependencies=[Depends(has_privilege("Pool.Audit"))]
)
async def get_resource_pools(
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
) -> List[schemas.ResourcePool]:
"""
Get all resource pools.
Required privilege: Pool.Audit
"""
return await pools_repo.get_resource_pools()
@router.post(
"",
response_model=schemas.ResourcePool,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(has_privilege("Pool.Allocate"))]
)
async def create_resource_pool(
resource_pool_create: schemas.ResourcePoolCreate,
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
) -> schemas.ResourcePool:
"""
Create a new resource pool
Required privilege: Pool.Allocate
"""
if await pools_repo.get_resource_pool_by_name(resource_pool_create.name):
raise ControllerBadRequestError(f"Resource pool '{resource_pool_create.name}' already exists")
return await pools_repo.create_resource_pool(resource_pool_create)
@router.get(
"/{resource_pool_id}",
response_model=schemas.ResourcePool,
dependencies=[Depends(has_privilege("Pool.Audit"))]
)
async def get_resource_pool(
resource_pool_id: UUID,
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
) -> schemas.ResourcePool:
"""
Get a resource pool.
Required privilege: Pool.Audit
"""
resource_pool = await pools_repo.get_resource_pool(resource_pool_id)
if not resource_pool:
raise ControllerNotFoundError(f"Resource pool '{resource_pool_id}' not found")
return resource_pool
@router.put(
"/{resource_pool_id}",
response_model=schemas.ResourcePool,
dependencies=[Depends(has_privilege("Pool.Modify"))]
)
async def update_resource_pool(
resource_pool_id: UUID,
resource_pool_update: schemas.ResourcePoolUpdate,
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
) -> schemas.ResourcePool:
"""
Update a resource pool.
Required privilege: Pool.Modify
"""
resource_pool = await pools_repo.get_resource_pool(resource_pool_id)
if not resource_pool:
raise ControllerNotFoundError(f"Resource pool '{resource_pool_id}' not found")
return await pools_repo.update_resource_pool(resource_pool_id, resource_pool_update)
@router.delete(
"/{resource_pool_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Pool.Allocate"))]
)
async def delete_resource_pool(
resource_pool_id: UUID,
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> None:
"""
Delete a resource pool.
Required privilege: Pool.Allocate
"""
resource_pool = await pools_repo.get_resource_pool(resource_pool_id)
if not resource_pool:
raise ControllerNotFoundError(f"Resource pool '{resource_pool_id}' not found")
success = await pools_repo.delete_resource_pool(resource_pool_id)
if not success:
raise ControllerError(f"Resource pool '{resource_pool_id}' could not be deleted")
await rbac_repo.delete_all_ace_starting_with_path(f"/pools/{resource_pool_id}")
@router.get(
"/{resource_pool_id}/resources",
response_model=List[schemas.Resource],
dependencies=[Depends(has_privilege("Pool.Audit"))]
)
async def get_pool_resources(
resource_pool_id: UUID,
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)),
) -> List[schemas.Resource]:
"""
Get all resource in a pool.
Required privilege: Pool.Audit
"""
return await pools_repo.get_pool_resources(resource_pool_id)
@router.put(
"/{resource_pool_id}/resources/{resource_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Pool.Modify"))]
)
async def add_resource_to_pool(
resource_pool_id: UUID,
resource_id: UUID,
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)),
) -> None:
"""
Add resource to a resource pool.
Required privilege: Pool.Modify
"""
resource_pool = await pools_repo.get_resource_pool(resource_pool_id)
if not resource_pool:
raise ControllerNotFoundError(f"Resource pool '{resource_pool_id}' not found")
# TODO: consider if a resource can belong to multiple pools
resources = await pools_repo.get_pool_resources(resource_pool_id)
for resource in resources:
if resource.resource_id == resource_id:
raise ControllerBadRequestError(f"Resource '{resource_id}' is already in '{resource_pool.name}'")
# we only support projects in resource pools for now
project = Controller.instance().get_project(str(resource_id))
resource = await pools_repo.get_resource(resource_id)
if not resource:
# the resource is not in the database yet, create it
resource_create = schemas.ResourceCreate(resource_id=resource_id, resource_type="project", name=project.name)
resource = await pools_repo.create_resource(resource_create)
await pools_repo.add_resource_to_pool(resource_pool_id, resource)
@router.delete(
"/{resource_pool_id}/resources/{resource_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Pool.Modify"))]
)
async def remove_resource_from_pool(
resource_pool_id: UUID,
resource_id: UUID,
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository)),
) -> None:
"""
Remove resource from a resource pool.
Required privilege: Pool.Modify
"""
resource = await pools_repo.get_resource(resource_id)
if not resource:
raise ControllerNotFoundError(f"Resource '{resource_id}' not found")
resource_pool = await pools_repo.remove_resource_from_pool(resource_pool_id, resource)
if not resource_pool:
raise ControllerNotFoundError(f"Resource pool '{resource_pool_id}' not found")
# TODO: consider if a resource can belong to multiple pools
success = await pools_repo.delete_resource(resource.resource_id)
if not success:
raise ControllerError(f"Resource '{resource_id}' could not be deleted")

View File

@ -0,0 +1,43 @@
#
# Software Name : GNS3 server
# Version: 3
# SPDX-FileCopyrightText: Copyright (c) 2023 Orange Business Services
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This software is distributed under the GPL-3.0 or any later version,
# the text of which is available at https://www.gnu.org/licenses/gpl-3.0.txt
# or see the "LICENSE" file for more details.
#
# Author: Sylvain MATHIEU
#
"""
API route for privileges
"""
from typing import List
from gns3server.db.repositories.rbac import RbacRepository
from .dependencies.database import get_repository
from fastapi import APIRouter, Depends
import logging
from gns3server import schemas
log = logging.getLogger(__name__)
router = APIRouter()
@router.get(
"",
response_model=List[schemas.Privilege],
)
async def get_privileges(
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> List[schemas.Privilege]:
"""
Get all privileges.
Required privilege: None
"""
return await rbac_repo.get_privileges()

View File

@ -45,11 +45,13 @@ from gns3server.controller.import_project import import_project as import_contro
from gns3server.controller.export_project import export_project as export_controller_project
from gns3server.utils.asyncio import aiozipstream
from gns3server.utils.path import is_safe_path
from gns3server.db.repositories.rbac import RbacRepository
from gns3server.db.repositories.templates import TemplatesRepository
from gns3server.db.repositories.rbac import RbacRepository
from gns3server.db.repositories.pools import ResourcePoolsRepository
from gns3server.services.templates import TemplatesService
from .dependencies.authentication import get_current_active_user, get_current_active_user_from_websocket
from .dependencies.rbac import has_privilege, has_privilege_on_websocket
from .dependencies.authentication import get_current_active_user
from .dependencies.database import get_repository
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project"}}
@ -66,29 +68,39 @@ def dep_project(project_id: UUID) -> Project:
return project
CHUNK_SIZE = 1024 * 8 # 8KB
@router.get("", response_model=List[schemas.Project], response_model_exclude_unset=True)
@router.get(
"",
response_model=List[schemas.Project],
response_model_exclude_unset=True
)
async def get_projects(
current_user: schemas.User = Depends(get_current_active_user),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
pools_repo: ResourcePoolsRepository = Depends(get_repository(ResourcePoolsRepository))
) -> List[schemas.Project]:
"""
Return all projects.
Required privilege: Project.Audit
"""
controller = Controller.instance()
projects = []
if current_user.is_superadmin:
# super admin sees all projects
return [p.asdict() for p in controller.projects.values()]
else:
user_projects = []
for project in controller.projects.values():
authorized = await rbac_repo.check_user_is_authorized(
current_user.user_id, "GET", f"/projects/{project.id}")
if authorized:
user_projects.append(project.asdict())
return user_projects
elif await rbac_repo.check_user_has_privilege(current_user.user_id, "/projects", "Project.Audit"):
# user with Project.Audit privilege on '/projects' sees all projects except those in resource pools
project_ids_in_pools = [str(r.resource_id) for r in await pools_repo.get_resources() if r.resource_type == "project"]
projects.extend([p.asdict() for p in controller.projects.values() if p.id not in project_ids_in_pools])
# user with Project.Audit privilege on resource pools sees the projects in these pools
user_pool_resources = await rbac_repo.get_user_pool_resources(current_user.user_id, "Project.Audit")
project_ids_in_pools = [str(r.resource_id) for r in user_pool_resources if r.resource_type == "project"]
projects.extend([p.asdict() for p in controller.projects.values() if p.id in project_ids_in_pools])
return projects
@router.post(
@ -97,63 +109,80 @@ async def get_projects(
response_model=schemas.Project,
response_model_exclude_unset=True,
responses={409: {"model": schemas.ErrorMessage, "description": "Could not create project"}},
dependencies=[Depends(has_privilege("Project.Allocate"))]
)
async def create_project(
project_data: schemas.ProjectCreate,
current_user: schemas.User = Depends(get_current_active_user),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> schemas.Project:
"""
Create a new project.
Required privilege: Project.Allocate
"""
controller = Controller.instance()
project = await controller.add_project(**jsonable_encoder(project_data, exclude_unset=True))
await rbac_repo.add_permission_to_user_with_path(current_user.user_id, f"/projects/{project.id}/*")
return project.asdict()
@router.get("/{project_id}", response_model=schemas.Project)
@router.get("/{project_id}", response_model=schemas.Project, dependencies=[Depends(has_privilege("Project.Audit"))])
def get_project(project: Project = Depends(dep_project)) -> schemas.Project:
"""
Return a project.
Required privilege: Project.Audit
"""
return project.asdict()
@router.put("/{project_id}", response_model=schemas.Project, response_model_exclude_unset=True)
@router.put(
"/{project_id}",
response_model=schemas.Project,
response_model_exclude_unset=True,
dependencies=[Depends(has_privilege("Project.Modify"))]
)
async def update_project(
project_data: schemas.ProjectUpdate,
project: Project = Depends(dep_project)
) -> schemas.Project:
"""
Update a project.
Required privilege: Project.Modify
"""
await project.update(**jsonable_encoder(project_data, exclude_unset=True))
return project.asdict()
@router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{project_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Project.Allocate"))]
)
async def delete_project(
project: Project = Depends(dep_project),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> None:
"""
Delete a project.
Required privilege: Project.Allocate
"""
controller = Controller.instance()
await project.delete()
controller.remove_project(project)
await rbac_repo.delete_all_permissions_with_path(f"/projects/{project.id}")
await rbac_repo.delete_all_ace_starting_with_path(f"/projects/{project.id}")
@router.get("/{project_id}/stats")
@router.get("/{project_id}/stats", dependencies=[Depends(has_privilege("Project.Audit"))])
def get_project_stats(project: Project = Depends(dep_project)) -> dict:
"""
Return a project statistics.
Required privilege: Project.Audit
"""
return project.stats()
@ -163,10 +192,13 @@ def get_project_stats(project: Project = Depends(dep_project)) -> dict:
"/{project_id}/close",
status_code=status.HTTP_204_NO_CONTENT,
responses={**responses, 409: {"model": schemas.ErrorMessage, "description": "Could not close project"}},
dependencies=[Depends(has_privilege("Project.Allocate"))]
)
async def close_project(project: Project = Depends(dep_project)) -> None:
"""
Close a project.
Required privilege: Project.Allocate
"""
await project.close()
@ -177,10 +209,13 @@ async def close_project(project: Project = Depends(dep_project)) -> None:
status_code=status.HTTP_201_CREATED,
response_model=schemas.Project,
responses={**responses, 409: {"model": schemas.ErrorMessage, "description": "Could not open project"}},
dependencies=[Depends(has_privilege("Project.Allocate"))]
)
async def open_project(project: Project = Depends(dep_project)) -> schemas.Project:
"""
Open a project.
Required privilege: Project.Allocate
"""
await project.open()
@ -192,10 +227,13 @@ async def open_project(project: Project = Depends(dep_project)) -> schemas.Proje
status_code=status.HTTP_201_CREATED,
response_model=schemas.Project,
responses={**responses, 409: {"model": schemas.ErrorMessage, "description": "Could not load project"}},
dependencies=[Depends(has_privilege("Project.Allocate"))]
)
async def load_project(path: str = Body(..., embed=True)) -> schemas.Project:
"""
Load a project (local server only).
Required privilege: Project.Allocate
"""
controller = Controller.instance()
@ -204,24 +242,27 @@ async def load_project(path: str = Body(..., embed=True)) -> schemas.Project:
return project.asdict()
@router.get("/{project_id}/notifications")
@router.get("/{project_id}/notifications", dependencies=[Depends(has_privilege("Project.Audit"))])
async def project_http_notifications(project_id: UUID) -> StreamingResponse:
"""
Receive project notifications about the controller from HTTP stream.
Required privilege: Project.Audit
"""
from gns3server.api.server import app
controller = Controller.instance()
project = controller.get_project(str(project_id))
log.info(f"New client has connected to the notification stream for project ID '{project.id}' (HTTP steam method)")
log.info(f"New client has connected to the notification stream for project ID '{project.id}' (HTTP stream method)")
async def event_stream():
try:
with controller.notification.project_queue(project.id) as queue:
while True:
while not app.state.exiting:
msg = await queue.get_json(5)
yield (f"{msg}\n").encode("utf-8")
yield f"{msg}\n".encode("utf-8")
finally:
log.info(f"Client has disconnected from notification for project ID '{project.id}' (HTTP stream method)")
if project.auto_close:
@ -239,10 +280,12 @@ async def project_http_notifications(project_id: UUID) -> StreamingResponse:
async def project_ws_notifications(
project_id: UUID,
websocket: WebSocket,
current_user: schemas.User = Depends(get_current_active_user_from_websocket)
current_user: schemas.User = Depends(has_privilege_on_websocket("Project.Audit"))
) -> None:
"""
Receive project notifications about the controller from WebSocket.
Required privilege: Project.Audit
"""
if current_user is None:
@ -275,7 +318,7 @@ async def project_ws_notifications(
await project.close()
@router.get("/{project_id}/export")
@router.get("/{project_id}/export", dependencies=[Depends(has_privilege("Project.Audit"))])
async def export_project(
project: Project = Depends(dep_project),
include_snapshots: bool = False,
@ -286,6 +329,8 @@ async def export_project(
) -> StreamingResponse:
"""
Export a project as a portable archive.
Required privilege: Project.Audit
"""
compression_query = compression.lower()
@ -332,7 +377,7 @@ async def export_project(
log.info(f"Project '{project.name}' exported in {time.time() - begin:.4f} seconds")
# Will be raise if you have no space left or permission issue on your temporary directory
# Will be raised if you have no space left or permission issue on your temporary directory
# RuntimeError: something was wrong during the zip process
except (ValueError, OSError, RuntimeError) as e:
raise ConnectionError(f"Cannot export project: {e}")
@ -341,7 +386,12 @@ async def export_project(
return StreamingResponse(streamer(), media_type="application/gns3project", headers=headers)
@router.post("/{project_id}/import", status_code=status.HTTP_201_CREATED, response_model=schemas.Project)
@router.post(
"/{project_id}/import",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Project,
dependencies=[Depends(has_privilege("Project.Allocate"))]
)
async def import_project(
project_id: UUID,
request: Request,
@ -349,6 +399,8 @@ async def import_project(
) -> schemas.Project:
"""
Import a project from a portable archive.
Required privilege: Project.Allocate
"""
controller = Controller.instance()
@ -376,56 +428,72 @@ async def import_project(
status_code=status.HTTP_201_CREATED,
response_model=schemas.Project,
responses={**responses, 409: {"model": schemas.ErrorMessage, "description": "Could not duplicate project"}},
dependencies=[Depends(has_privilege("Project.Allocate"))]
)
async def duplicate_project(
project_data: schemas.ProjectDuplicate,
project: Project = Depends(dep_project),
current_user: schemas.User = Depends(get_current_active_user),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
project: Project = Depends(dep_project)
) -> schemas.Project:
"""
Duplicate a project.
Required privilege: Project.Allocate
"""
reset_mac_addresses = project_data.reset_mac_addresses
new_project = await project.duplicate(
name=project_data.name, reset_mac_addresses=reset_mac_addresses
)
await rbac_repo.add_permission_to_user_with_path(current_user.user_id, f"/projects/{new_project.id}/*")
return new_project.asdict()
@router.get("/{project_id}/locked")
@router.get("/{project_id}/locked", dependencies=[Depends(has_privilege("Project.Audit"))])
async def locked_project(project: Project = Depends(dep_project)) -> bool:
"""
Returns whether a project is locked or not
Returns whether a project is locked or not.
Required privilege: Project.Audit
"""
return project.locked
@router.post("/{project_id}/lock", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{project_id}/lock",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Project.Modify"))]
)
async def lock_project(project: Project = Depends(dep_project)) -> None:
"""
Lock all drawings and nodes in a given project.
Required privilege: Project.Audit
"""
project.lock()
@router.post("/{project_id}/unlock", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{project_id}/unlock",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Project.Modify"))]
)
async def unlock_project(project: Project = Depends(dep_project)) -> None:
"""
Unlock all drawings and nodes in a given project.
Required privilege: Project.Modify
"""
project.unlock()
@router.get("/{project_id}/files/{file_path:path}")
@router.get("/{project_id}/files/{file_path:path}", dependencies=[Depends(has_privilege("Project.Audit"))])
async def get_file(file_path: str, project: Project = Depends(dep_project)) -> FileResponse:
"""
Return a file from a project.
Required privilege: Project.Audit
"""
file_path = urllib.parse.unquote(file_path)
@ -442,10 +510,16 @@ async def get_file(file_path: str, project: Project = Depends(dep_project)) -> F
return FileResponse(path, media_type="application/octet-stream")
@router.post("/{project_id}/files/{file_path:path}", status_code=status.HTTP_204_NO_CONTENT)
@router.post(
"/{project_id}/files/{file_path:path}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Project.Modify"))]
)
async def write_file(file_path: str, request: Request, project: Project = Depends(dep_project)) -> None:
"""
Write a file to a project.
Required privilege: Project.Modify
"""
file_path = urllib.parse.unquote(file_path)
@ -474,6 +548,7 @@ async def write_file(file_path: str, request: Request, project: Project = Depend
response_model=schemas.Node,
status_code=status.HTTP_201_CREATED,
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find project or template"}},
dependencies=[Depends(has_privilege("Node.Allocate"))]
)
async def create_node_from_template(
project_id: UUID,
@ -483,6 +558,8 @@ async def create_node_from_template(
) -> schemas.Node:
"""
Create a new node from a template.
Required privilege: Node.Allocate
"""
template = await TemplatesService(templates_repo).get_template(template_id)

View File

@ -19,7 +19,7 @@
API routes for roles.
"""
from fastapi import APIRouter, Depends, Response, status
from fastapi import APIRouter, Depends, status
from uuid import UUID
from typing import List
@ -33,6 +33,7 @@ from gns3server.controller.controller_error import (
from gns3server.db.repositories.rbac import RbacRepository
from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege
import logging
@ -41,24 +42,37 @@ log = logging.getLogger(__name__)
router = APIRouter()
@router.get("", response_model=List[schemas.Role])
@router.get(
"",
response_model=List[schemas.Role],
dependencies=[Depends(has_privilege("Role.Audit"))]
)
async def get_roles(
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> List[schemas.Role]:
"""
Get all roles.
Required privilege: Role.Audit
"""
return await rbac_repo.get_roles()
@router.post("", response_model=schemas.Role, status_code=status.HTTP_201_CREATED)
@router.post(
"",
response_model=schemas.Role,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(has_privilege("Role.Allocate"))]
)
async def create_role(
role_create: schemas.RoleCreate,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> schemas.Role:
"""
Create a new role.
Required privilege: Role.Allocate
"""
if await rbac_repo.get_role_by_name(role_create.name):
@ -67,13 +81,19 @@ async def create_role(
return await rbac_repo.create_role(role_create)
@router.get("/{role_id}", response_model=schemas.Role)
@router.get(
"/{role_id}",
response_model=schemas.Role,
dependencies=[Depends(has_privilege("Role.Audit"))]
)
async def get_role(
role_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> schemas.Role:
"""
Get a role.
Required privilege: Role.Audit
"""
role = await rbac_repo.get_role(role_id)
@ -82,7 +102,11 @@ async def get_role(
return role
@router.put("/{role_id}", response_model=schemas.Role)
@router.put(
"/{role_id}",
response_model=schemas.Role,
dependencies=[Depends(has_privilege("Role.Modify"))]
)
async def update_role(
role_id: UUID,
role_update: schemas.RoleUpdate,
@ -90,6 +114,8 @@ async def update_role(
) -> schemas.Role:
"""
Update a role.
Required privilege: Role.Modify
"""
role = await rbac_repo.get_role(role_id)
@ -102,13 +128,19 @@ async def update_role(
return await rbac_repo.update_role(role_id, role_update)
@router.delete("/{role_id}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{role_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Role.Allocate"))]
)
async def delete_role(
role_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> None:
"""
Delete a role.
Required privilege: Role.Allocate
"""
role = await rbac_repo.get_role(role_id)
@ -121,59 +153,72 @@ async def delete_role(
success = await rbac_repo.delete_role(role_id)
if not success:
raise ControllerError(f"Role '{role_id}' could not be deleted")
await rbac_repo.delete_all_ace_starting_with_path(f"/roles/{role_id}")
@router.get("/{role_id}/permissions", response_model=List[schemas.Permission])
async def get_role_permissions(
@router.get(
"/{role_id}/privileges",
response_model=List[schemas.Privilege],
dependencies=[Depends(has_privilege("Role.Audit"))]
)
async def get_role_privileges(
role_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> List[schemas.Permission]:
) -> List[schemas.Privilege]:
"""
Get all role permissions.
Get all role privileges.
Required privilege: Role.Audit
"""
return await rbac_repo.get_role_permissions(role_id)
return await rbac_repo.get_role_privileges(role_id)
@router.put(
"/{role_id}/permissions/{permission_id}",
status_code=status.HTTP_204_NO_CONTENT
"/{role_id}/privileges/{privilege_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Role.Modify"))]
)
async def add_permission_to_role(
async def add_privilege_to_role(
role_id: UUID,
permission_id: UUID,
privilege_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> None:
"""
Add a permission to a role.
Add a privilege to a role.
Required privilege: Role.Modify
"""
permission = await rbac_repo.get_permission(permission_id)
if not permission:
raise ControllerNotFoundError(f"Permission '{permission_id}' not found")
privilege = await rbac_repo.get_privilege(privilege_id)
if not privilege:
raise ControllerNotFoundError(f"Privilege '{privilege_id}' not found")
role = await rbac_repo.add_permission_to_role(role_id, permission)
role = await rbac_repo.add_privilege_to_role(role_id, privilege)
if not role:
raise ControllerNotFoundError(f"Role '{role_id}' not found")
@router.delete(
"/{role_id}/permissions/{permission_id}",
status_code=status.HTTP_204_NO_CONTENT
"/{role_id}/privileges/{privilege_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Role.Modify"))]
)
async def remove_permission_from_role(
async def remove_privilege_from_role(
role_id: UUID,
permission_id: UUID,
privilege_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> None:
"""
Remove member from an user group.
Remove privilege from a role.
Required privilege: Role.Modify
"""
permission = await rbac_repo.get_permission(permission_id)
if not permission:
raise ControllerNotFoundError(f"Permission '{permission_id}' not found")
privilege = await rbac_repo.get_privilege(privilege_id)
if not privilege:
raise ControllerNotFoundError(f"Privilege '{privilege_id}' not found")
role = await rbac_repo.remove_permission_from_role(role_id, permission)
role = await rbac_repo.remove_privilege_from_role(role_id, privilege)
if not role:
raise ControllerNotFoundError(f"Role '{role_id}' not found")

View File

@ -23,14 +23,18 @@ import logging
log = logging.getLogger()
from fastapi import APIRouter, Depends, Response, status
from fastapi import APIRouter, Depends, status
from typing import List
from uuid import UUID
from gns3server.controller.project import Project
from gns3server.db.repositories.rbac import RbacRepository
from gns3server import schemas
from gns3server.controller import Controller
from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find project or snapshot"}}
router = APIRouter(responses=responses)
@ -45,42 +49,74 @@ def dep_project(project_id: UUID) -> Project:
return project
@router.post("", status_code=status.HTTP_201_CREATED, response_model=schemas.Snapshot)
@router.post(
"",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Snapshot,
dependencies=[Depends(has_privilege("Snapshot.Allocate"))]
)
async def create_snapshot(
snapshot_data: schemas.SnapshotCreate,
project: Project = Depends(dep_project)
) -> schemas.Snapshot:
"""
Create a new snapshot of a project.
Required privilege: Snapshot.Allocate
"""
snapshot = await project.snapshot(snapshot_data.name)
return snapshot.asdict()
@router.get("", response_model=List[schemas.Snapshot], response_model_exclude_unset=True)
@router.get(
"",
response_model=List[schemas.Snapshot],
response_model_exclude_unset=True,
dependencies=[Depends(has_privilege("Snapshot.Audit"))]
)
def get_snapshots(project: Project = Depends(dep_project)) -> List[schemas.Snapshot]:
"""
Return all snapshots belonging to a given project.
Required privilege: Snapshot.Audit
"""
snapshots = [s for s in project.snapshots.values()]
return [s.asdict() for s in sorted(snapshots, key=lambda s: (s.created_at, s.name))]
@router.delete("/{snapshot_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_snapshot(snapshot_id: UUID, project: Project = Depends(dep_project)) -> None:
@router.delete(
"/{snapshot_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Snapshot.Allocate"))]
)
async def delete_snapshot(
snapshot_id: UUID,
project: Project = Depends(dep_project),
rbac_repo=Depends(get_repository(RbacRepository))
) -> None:
"""
Delete a snapshot.
Required privilege: Snapshot.Allocate
"""
await project.delete_snapshot(str(snapshot_id))
await rbac_repo.delete_all_ace_starting_with_path(f"/projects/{project.id}/snapshots/{snapshot_id}")
@router.post("/{snapshot_id}/restore", status_code=status.HTTP_201_CREATED, response_model=schemas.Project)
@router.post(
"/{snapshot_id}/restore",
status_code=status.HTTP_201_CREATED,
response_model=schemas.Project,
dependencies=[Depends(has_privilege("Snapshot.Restore"))]
)
async def restore_snapshot(snapshot_id: UUID, project: Project = Depends(dep_project)) -> schemas.Project:
"""
Restore a snapshot.
Required privilege: Snapshot.Restore
"""
snapshot = project.get_snapshot(str(snapshot_id))

View File

@ -29,7 +29,7 @@ from gns3server.controller import Controller
from gns3server import schemas
from gns3server.controller.controller_error import ControllerError, ControllerNotFoundError
from .dependencies.authentication import get_current_active_user
from .dependencies.rbac import has_privilege
import logging
@ -39,19 +39,32 @@ log = logging.getLogger(__name__)
router = APIRouter()
@router.get("")
def get_symbols() -> List[str]:
@router.get(
"",
dependencies=[Depends(has_privilege("Symbol.Audit"))]
)
def get_symbols() -> List[dict]:
"""
Return all symbols.
Required privilege: Symbol.Audit
"""
controller = Controller.instance()
return controller.symbols.list()
@router.get(
"/{symbol_id:path}/raw", responses={404: {"model": schemas.ErrorMessage, "description": "Could not find symbol"}}
"/{symbol_id:path}/raw",
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find symbol"}},
# FIXME: this is a temporary workaround due to a bug in the web-ui: https://github.com/GNS3/gns3-web-ui/issues/1466
# dependencies=[Depends(has_privilege("Symbol.Audit"))]
)
async def get_symbol(symbol_id: str) -> FileResponse:
"""
Download a symbol file.
Required privilege: Symbol.Audit
"""
controller = Controller.instance()
@ -65,10 +78,13 @@ async def get_symbol(symbol_id: str) -> FileResponse:
@router.get(
"/{symbol_id:path}/dimensions",
responses={404: {"model": schemas.ErrorMessage, "description": "Could not find symbol"}},
dependencies=[Depends(has_privilege("Symbol.Audit"))]
)
async def get_symbol_dimensions(symbol_id: str) -> dict:
"""
Get a symbol dimensions.
Required privilege: Symbol.Audit
"""
controller = Controller.instance()
@ -80,10 +96,12 @@ async def get_symbol_dimensions(symbol_id: str) -> dict:
raise ControllerNotFoundError(f"Could not get symbol file: {e}")
@router.get("/default_symbols")
@router.get("/default_symbols", dependencies=[Depends(has_privilege("Symbol.Audit"))])
def get_default_symbols() -> dict:
"""
Return all default symbols.
Required privilege: Symbol.Audit
"""
controller = Controller.instance()
@ -92,12 +110,14 @@ def get_default_symbols() -> dict:
@router.post(
"/{symbol_id:path}/raw",
dependencies=[Depends(get_current_active_user)],
status_code=status.HTTP_204_NO_CONTENT
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Symbol.Allocate"))]
)
async def upload_symbol(symbol_id: str, request: Request) -> None:
"""
Upload a symbol file.
Required privilege: Symbol.Allocate
"""
controller = Controller.instance()
@ -111,4 +131,3 @@ async def upload_symbol(symbol_id: str, request: Request) -> None:
# Reset the symbol list
controller.symbols.list()

View File

@ -36,6 +36,7 @@ from gns3server.db.repositories.rbac import RbacRepository
from gns3server.db.repositories.images import ImagesRepository
from .dependencies.authentication import get_current_active_user
from .dependencies.rbac import has_privilege
from .dependencies.database import get_repository
responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find template"}}
@ -43,24 +44,33 @@ responses = {404: {"model": schemas.ErrorMessage, "description": "Could not find
router = APIRouter(responses=responses)
@router.post("", response_model=schemas.Template, status_code=status.HTTP_201_CREATED)
@router.post(
"",
response_model=schemas.Template,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(has_privilege("Template.Allocate"))]
)
async def create_template(
template_create: schemas.TemplateCreate,
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
current_user: schemas.User = Depends(get_current_active_user),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository))
) -> schemas.Template:
"""
Create a new template.
Required privilege: Template.Allocate
"""
template = await TemplatesService(templates_repo).create_template(template_create)
template_id = template.get("template_id")
await rbac_repo.add_permission_to_user_with_path(current_user.user_id, f"/templates/{template_id}/*")
return template
@router.get("/{template_id}", response_model=schemas.Template, response_model_exclude_unset=True)
@router.get(
"/{template_id}",
response_model=schemas.Template,
response_model_exclude_unset=True,
dependencies=[Depends(get_current_active_user)],
#dependencies=[Depends(has_privilege("Template.Audit"))] # FIXME: this is a temporary workaround due to a bug in the web-ui
)
async def get_template(
template_id: UUID,
request: Request,
@ -69,6 +79,8 @@ async def get_template(
) -> schemas.Template:
"""
Return a template.
Required privilege: Template.Audit
"""
request_etag = request.headers.get("If-None-Match", "")
@ -82,7 +94,12 @@ async def get_template(
return template
@router.put("/{template_id}", response_model=schemas.Template, response_model_exclude_unset=True)
@router.put(
"/{template_id}",
response_model=schemas.Template,
response_model_exclude_unset=True,
dependencies=[Depends(has_privilege("Template.Modify"))]
)
async def update_template(
template_id: UUID,
template_update: schemas.TemplateUpdate,
@ -90,12 +107,18 @@ async def update_template(
) -> schemas.Template:
"""
Update a template.
Required privilege: Template.Modify
"""
return await TemplatesService(templates_repo).update_template(template_id, template_update)
@router.delete("/{template_id}", status_code=status.HTTP_204_NO_CONTENT)
@router.delete(
"/{template_id}",
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("Template.Allocate"))]
)
async def delete_template(
template_id: UUID,
prune_images: Optional[bool] = False,
@ -105,15 +128,23 @@ async def delete_template(
) -> None:
"""
Delete a template.
Required privilege: Template.Allocate
"""
await TemplatesService(templates_repo).delete_template(template_id)
await rbac_repo.delete_all_permissions_with_path(f"/templates/{template_id}")
await rbac_repo.delete_all_ace_starting_with_path(f"/templates/{template_id}")
if prune_images:
await images_repo.prune_images()
@router.get("", response_model=List[schemas.Template], response_model_exclude_unset=True)
@router.get(
"",
response_model=List[schemas.Template],
response_model_exclude_unset=True,
dependencies=[Depends(get_current_active_user)],
#dependencies=[Depends(has_privilege("Template.Audit"))] # FIXME: this is a temporary workaround due to a bug in the web-ui
)
async def get_templates(
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
current_user: schemas.User = Depends(get_current_active_user),
@ -121,6 +152,8 @@ async def get_templates(
) -> List[schemas.Template]:
"""
Return all templates.
Required privilege: Template.Audit
"""
templates = await TemplatesService(templates_repo).get_templates()
@ -129,27 +162,31 @@ async def get_templates(
else:
user_templates = []
for template in templates:
if template.get("builtin") is True:
user_templates.append(template)
continue
template_id = template.get("template_id")
authorized = await rbac_repo.check_user_is_authorized(
current_user.user_id, "GET", f"/templates/{template_id}")
if authorized:
user_templates.append(template)
# if template.get("builtin") is True:
# user_templates.append(template)
# continue
# template_id = template.get("template_id")
# authorized = await rbac_repo.check_user_is_authorized(
# current_user.user_id, "GET", f"/templates/{template_id}")
# if authorized:
user_templates.append(template)
return user_templates
@router.post("/{template_id}/duplicate", response_model=schemas.Template, status_code=status.HTTP_201_CREATED)
@router.post(
"/{template_id}/duplicate",
response_model=schemas.Template,
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(has_privilege("Template.Allocate"))]
)
async def duplicate_template(
template_id: UUID, templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)),
current_user: schemas.User = Depends(get_current_active_user),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
template_id: UUID, templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository))
) -> schemas.Template:
"""
Duplicate a template.
Required privilege: Template.Allocate
"""
template = await TemplatesService(templates_repo).duplicate_template(template_id)
await rbac_repo.add_permission_to_user_with_path(current_user.user_id, f"/templates/{template_id}/*")
return template

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python
#
# Copyright (C) 2020 GNS3 Technologies Inc.
# Copyright (C) 2023 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -38,6 +38,7 @@ from gns3server.services import auth_service
from .dependencies.authentication import get_current_active_user
from .dependencies.database import get_repository
from .dependencies.rbac import has_privilege
import logging
@ -115,12 +116,18 @@ async def update_logged_in_user(
return await users_repo.update_user(current_user.user_id, user_update)
@router.get("", response_model=List[schemas.User], dependencies=[Depends(get_current_active_user)])
@router.get(
"",
response_model=List[schemas.User],
dependencies=[Depends(has_privilege("User.Audit"))]
)
async def get_users(
users_repo: UsersRepository = Depends(get_repository(UsersRepository))
) -> List[schemas.User]:
"""
Get all users.
Required privilege: User.Audit
"""
return await users_repo.get_users()
@ -129,8 +136,8 @@ async def get_users(
@router.post(
"",
response_model=schemas.User,
dependencies=[Depends(get_current_active_user)],
status_code=status.HTTP_201_CREATED
status_code=status.HTTP_201_CREATED,
dependencies=[Depends(has_privilege("User.Allocate"))]
)
async def create_user(
user_create: schemas.UserCreate,
@ -138,6 +145,8 @@ async def create_user(
) -> schemas.User:
"""
Create a new user.
Required privilege: User.Allocate
"""
if await users_repo.get_user_by_username(user_create.username):
@ -149,13 +158,19 @@ async def create_user(
return await users_repo.create_user(user_create)
@router.get("/{user_id}", dependencies=[Depends(get_current_active_user)], response_model=schemas.User)
@router.get(
"/{user_id}",
response_model=schemas.User,
dependencies=[Depends(has_privilege("User.Audit"))]
)
async def get_user(
user_id: UUID,
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
) -> schemas.User:
"""
Get an user.
Get a user.
Required privilege: User.Audit
"""
user = await users_repo.get_user(user_id)
@ -164,14 +179,20 @@ async def get_user(
return user
@router.put("/{user_id}", dependencies=[Depends(get_current_active_user)], response_model=schemas.User)
@router.put(
"/{user_id}",
response_model=schemas.User,
dependencies=[Depends(has_privilege("User.Modify"))]
)
async def update_user(
user_id: UUID,
user_update: schemas.UserUpdate,
users_repo: UsersRepository = Depends(get_repository(UsersRepository))
) -> schemas.User:
"""
Update an user.
Update a user.
Required privilege: User.Modify
"""
if user_update.username and await users_repo.get_user_by_username(user_update.username):
@ -188,15 +209,18 @@ async def update_user(
@router.delete(
"/{user_id}",
dependencies=[Depends(get_current_active_user)],
status_code=status.HTTP_204_NO_CONTENT
status_code=status.HTTP_204_NO_CONTENT,
dependencies=[Depends(has_privilege("User.Allocate"))]
)
async def delete_user(
user_id: UUID,
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
user_id: UUID,
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> None:
"""
Delete an user.
Delete a user.
Required privilege: User.Allocate
"""
user = await users_repo.get_user(user_id)
@ -209,12 +233,13 @@ async def delete_user(
success = await users_repo.delete_user(user_id)
if not success:
raise ControllerError(f"User '{user_id}' could not be deleted")
await rbac_repo.delete_all_ace_starting_with_path(f"/users/{user_id}")
@router.get(
"/{user_id}/groups",
dependencies=[Depends(get_current_active_user)],
response_model=List[schemas.UserGroup]
response_model=List[schemas.UserGroup],
dependencies=[Depends(has_privilege("Group.Audit"))]
)
async def get_user_memberships(
user_id: UUID,
@ -222,68 +247,8 @@ async def get_user_memberships(
) -> List[schemas.UserGroup]:
"""
Get user memberships.
Required privilege: Group.Audit
"""
return await users_repo.get_user_memberships(user_id)
@router.get(
"/{user_id}/permissions",
dependencies=[Depends(get_current_active_user)],
response_model=List[schemas.Permission]
)
async def get_user_permissions(
user_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> List[schemas.Permission]:
"""
Get user permissions.
"""
return await rbac_repo.get_user_permissions(user_id)
@router.put(
"/{user_id}/permissions/{permission_id}",
dependencies=[Depends(get_current_active_user)],
status_code=status.HTTP_204_NO_CONTENT
)
async def add_permission_to_user(
user_id: UUID,
permission_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
) -> None:
"""
Add a permission to an user.
"""
permission = await rbac_repo.get_permission(permission_id)
if not permission:
raise ControllerNotFoundError(f"Permission '{permission_id}' not found")
user = await rbac_repo.add_permission_to_user(user_id, permission)
if not user:
raise ControllerNotFoundError(f"User '{user_id}' not found")
@router.delete(
"/{user_id}/permissions/{permission_id}",
dependencies=[Depends(get_current_active_user)],
status_code=status.HTTP_204_NO_CONTENT
)
async def remove_permission_from_user(
user_id: UUID,
permission_id: UUID,
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
) -> None:
"""
Remove permission from an user.
"""
permission = await rbac_repo.get_permission(permission_id)
if not permission:
raise ControllerNotFoundError(f"Permission '{permission_id}' not found")
user = await rbac_repo.remove_permission_from_user(user_id, permission)
if not user:
raise ControllerNotFoundError(f"User '{user_id}' not found")

View File

@ -55,6 +55,9 @@ async def web_ui(file_path: str):
if static is None or not os.path.exists(static):
static = get_resource(os.path.join("static", "web-ui", "index.html"))
if static is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
# guesstype prefers to have text/html type than application/javascript
# which results with warnings in Firefox 66 on Windows
# Ref. gns3-server#1559

View File

@ -21,9 +21,10 @@ FastAPI app
import time
from fastapi import FastAPI, Request, HTTPException
from fastapi import FastAPI, Request, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from sqlalchemy.exc import SQLAlchemyError
from uvicorn.main import Server as UvicornServer
@ -87,54 +88,54 @@ UvicornServer.handle_exit = handle_exit
@app.exception_handler(ControllerError)
async def controller_error_handler(request: Request, exc: ControllerError):
log.error(f"Controller error: {exc}")
log.error(f"Controller error in {request.url.path} ({request.method}): {exc}")
return JSONResponse(
status_code=409,
status_code=status.HTTP_409_CONFLICT,
content={"message": str(exc)},
)
@app.exception_handler(ControllerTimeoutError)
async def controller_timeout_error_handler(request: Request, exc: ControllerTimeoutError):
log.error(f"Controller timeout error: {exc}")
log.error(f"Controller timeout error in {request.url.path} ({request.method}): {exc}")
return JSONResponse(
status_code=408,
status_code=status.HTTP_408_REQUEST_TIMEOUT,
content={"message": str(exc)},
)
@app.exception_handler(ControllerUnauthorizedError)
async def controller_unauthorized_error_handler(request: Request, exc: ControllerUnauthorizedError):
log.error(f"Controller unauthorized error: {exc}")
log.error(f"Controller unauthorized error in {request.url.path} ({request.method}): {exc}")
return JSONResponse(
status_code=401,
status_code=status.HTTP_401_UNAUTHORIZED,
content={"message": str(exc)},
)
@app.exception_handler(ControllerForbiddenError)
async def controller_forbidden_error_handler(request: Request, exc: ControllerForbiddenError):
log.error(f"Controller forbidden error: {exc}")
log.error(f"Controller forbidden error in {request.url.path} ({request.method}): {exc}")
return JSONResponse(
status_code=403,
status_code=status.HTTP_403_FORBIDDEN,
content={"message": str(exc)},
)
@app.exception_handler(ControllerNotFoundError)
async def controller_not_found_error_handler(request: Request, exc: ControllerNotFoundError):
log.error(f"Controller not found error: {exc}")
log.error(f"Controller not found error in {request.url.path} ({request.method}): {exc}")
return JSONResponse(
status_code=404,
status_code=status.HTTP_404_NOT_FOUND,
content={"message": str(exc)},
)
@app.exception_handler(ControllerBadRequestError)
async def controller_bad_request_error_handler(request: Request, exc: ControllerBadRequestError):
log.error(f"Controller bad request error: {exc}")
log.error(f"Controller bad request error in {request.url.path} ({request.method}): {exc}")
return JSONResponse(
status_code=400,
status_code=status.HTTP_400_BAD_REQUEST,
content={"message": str(exc)},
)
@ -143,7 +144,7 @@ async def controller_bad_request_error_handler(request: Request, exc: Controller
async def compute_conflict_error_handler(request: Request, exc: ComputeConflictError):
log.error(f"Controller received error from compute for request '{exc.url()}': {exc}")
return JSONResponse(
status_code=409,
status_code=status.HTTP_409_CONFLICT,
content={"message": str(exc)},
)
@ -160,12 +161,21 @@ async def http_exception_handler(request: Request, exc: HTTPException):
@app.exception_handler(SQLAlchemyError)
async def sqlalchemry_error_handler(request: Request, exc: SQLAlchemyError):
log.error(f"Controller database error: {exc}")
log.error(f"Controller database error in {request.url.path} ({request.method}): {exc}")
return JSONResponse(
status_code=500,
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={"message": "Database error detected, please check logs to find details"},
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
log.error(f"Request validation error in {request.url.path} ({request.method}): {exc}")
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={"message": str(exc)}
)
# FIXME: do not use this middleware since it creates issue when using StreamingResponse
# see https://starlette-context.readthedocs.io/en/latest/middleware.html#why-are-there-two-middlewares-that-do-the-same-thing

View File

@ -7,7 +7,7 @@
"vendor_url": "https://www.kali.org/",
"documentation_url": "http://www.ipcop.org/docs.html",
"product_name": "IP Cop",
"registry_version": 3,
"registry_version": 4,
"status": "stable",
"maintainer": "Brent Stewart",
"maintainer_email": "brent@stewart.tc",

View File

@ -7,8 +7,8 @@
"vendor_url": "https://www.a10networks.com/",
"documentation_url": "https://www.a10networks.com/support",
"product_name": "A10 vThunder",
"product_url": "https://www.a10networks.com/products/thunder-series-appliances/vthunder-virtualized-application_delivery_controller/",
"registry_version": 3,
"product_url": "https://www.a10networks.com/products/vthunder-trial/",
"registry_version": 4,
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -6,7 +6,7 @@
"vendor_name": "Ubuntu",
"vendor_url": "https://www.ubuntu.com/",
"product_name": "AAA",
"registry_version": 3,
"registry_version": 4,
"status": "stable",
"maintainer": "Andras Dosztal",
"maintainer_email": "developers@gns3.net",

View File

@ -8,7 +8,7 @@
"documentation_url": "https://www.alcatel-lucent.com/support",
"product_name": "Alcatel 7750",
"product_url": "https://www.alcatel-lucent.com/products/7750-service-router",
"registry_version": 3,
"registry_version": 4,
"status": "experimental",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -21,30 +21,62 @@
"hda_disk_interface": "sata",
"arch": "x86_64",
"console_type": "telnet",
"kvm": "allow"
"kvm": "allow",
"options": "-cpu host -nographic"
},
"images": [
{
"filename": "AlmaLinux-8-GenericCloud-8.5-20211119.x86_64.qcow2",
"version": "8.5",
"md5sum": "a64ece809ae06180ac59cfa622d98af0",
"filesize": 561774592,
"filename": "AlmaLinux-9-GenericCloud-9.2-20230513.x86_64.qcow2",
"version": "9.2",
"md5sum": "c5bc76e8c95ac9f810a3482c80a54cc7",
"filesize": 563347456,
"download_url": "https://repo.almalinux.org/almalinux/9/cloud/x86_64/images/",
"direct_download_url": "https://repo.almalinux.org/almalinux/9/cloud/x86_64/images/AlmaLinux-9-GenericCloud-9.2-20230513.x86_64.qcow2"
},
{
"filename": "AlmaLinux-8-GenericCloud-8.8-20230524.x86_64.qcow2",
"version": "8.8",
"md5sum": "3958c5fc25770ef63cf97aa5d93f0a0b",
"filesize": 565444608,
"download_url": "https://repo.almalinux.org/almalinux/8/cloud/x86_64/images/",
"direct_download_url": "https://repo.almalinux.org/almalinux/8/cloud/x86_64/images/AlmaLinux-8-GenericCloud-8.5-20211119.x86_64.qcow2"
"direct_download_url": "https://repo.almalinux.org/almalinux/8/cloud/x86_64/images/AlmaLinux-8-GenericCloud-8.8-20230524.x86_64.qcow2"
},
{
"filename": "AlmaLinux-8-GenericCloud-8.7-20221111.x86_64.qcow2",
"version": "8.7",
"md5sum": "b2b8c7fd3b6869362f3f8ed47549c804",
"filesize": 566231040,
"download_url": "https://repo.almalinux.org/almalinux/8/cloud/x86_64/images/",
"direct_download_url": "https://repo.almalinux.org/almalinux/8/cloud/x86_64/images/AlmaLinux-8-GenericCloud-8.7-20221111.x86_64.qcow2"
},
{
"filename": "almalinux-cloud-init-data.iso",
"version": "1.0",
"md5sum": "72fb52af76e9561d125dd99224e2c1d1",
"filesize": 374784,
"download_url": "https://github.com/GNS3/gns3-registry/raw/master/cloud-init/AlmaLinux/almalinux-cloud-init-data.iso"
"download_url": "https://github.com/GNS3/gns3-registry/tree/master/cloud-init/AlmaLinux",
"direct_download_url": "https://github.com/GNS3/gns3-registry/raw/master/cloud-init/AlmaLinux/almalinux-cloud-init-data.iso"
}
],
"versions": [
{
"name": "8.5",
"name": "9.2",
"images": {
"hda_disk_image": "AlmaLinux-8-GenericCloud-8.5-20211119.x86_64.qcow2",
"hda_disk_image": "AlmaLinux-9-GenericCloud-9.2-20230513.x86_64.qcow2",
"cdrom_image": "almalinux-cloud-init-data.iso"
}
},
{
"name": "8.8",
"images": {
"hda_disk_image": "AlmaLinux-8-GenericCloud-8.8-20230524.x86_64.qcow2",
"cdrom_image": "almalinux-cloud-init-data.iso"
}
},
{
"name": "8.7",
"images": {
"hda_disk_image": "AlmaLinux-8-GenericCloud-8.7-20221111.x86_64.qcow2",
"cdrom_image": "almalinux-cloud-init-data.iso"
}
}

View File

@ -0,0 +1,59 @@
{
"appliance_id": "3da5c614-772c-4963-af86-f24e058c9216",
"name": "Alpine Linux Virt",
"category": "guest",
"description": "Alpine Linux is a security-oriented, lightweight Linux distribution based on musl libc and busybox.\n\nThis is the qemu version of Alpine Linux, stripped down to the maximum, only the default packages are installed without an SSH server.",
"vendor_name": "Alpine Linux Development Team",
"vendor_url": "http://alpinelinux.org",
"documentation_url": "http://wiki.alpinelinux.org",
"product_name": "Alpine Linux Virt",
"registry_version": 4,
"status": "stable",
"availability": "free",
"maintainer": "Adnan RIHAN",
"maintainer_email": "adnan@rihan.fr",
"usage": "Autologin is enabled as \"root\" with no password.\n\nThe network interfaces aren't configured, you can do either of the following:\n- Use alpine's DHCP client: `udhcpc`\n- Configure them manually (ip address add \u2026, ip route add \u2026)\n- Modify interfaces file in /etc/network/interfaces\n- Use alpine's wizard: `setup-interfaces`",
"symbol": "alpine-virt-qemu.svg",
"port_name_format": "eth{0}",
"qemu": {
"adapter_type": "virtio-net-pci",
"adapters": 1,
"ram": 128,
"hda_disk_interface": "virtio",
"arch": "x86_64",
"console_type": "telnet",
"kvm": "allow"
},
"images": [
{
"filename": "alpine-virt-3.18.4.qcow2",
"version": "3.18.4",
"md5sum": "99d393c16c870e12c4215aadd82ca998",
"filesize": 51066880,
"download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/alpine-virt-3.18.4.qcow2/download"
},
{
"filename": "alpine-virt-3.16.img",
"version": "3.16",
"md5sum": "ce90ff64b8f8e5860c49ea4a038e54cc",
"filesize": 96468992,
"download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/alpine-virt-3.16.img/download"
}
],
"versions": [
{
"name": "3.18.4",
"images": {
"hda_disk_image": "alpine-virt-3.18.4.qcow2"
}
},
{
"name": "3.16",
"images": {
"hda_disk_image": "alpine-virt-3.16.img"
}
}
]
}

View File

@ -5,9 +5,10 @@
"description": "Alpine Linux is a security-oriented, lightweight Linux distribution based on musl libc and busybox.",
"vendor_name": "Alpine Linux Development Team",
"vendor_url": "http://alpinelinux.org",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/Alpine Linux.png",
"documentation_url": "http://wiki.alpinelinux.org",
"product_name": "Alpine Linux",
"registry_version": 3,
"registry_version": 4,
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -6,7 +6,7 @@
"vendor_name": "Arista",
"vendor_url": "http://www.arista.com/",
"product_name": "cEOS",
"registry_version": 3,
"registry_version": 4,
"status": "experimental",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -8,7 +8,7 @@
"documentation_url": "https://www.arista.com/assets/data/docs/Manuals/EOS-4.17.2F-Manual.pdf",
"product_name": "vEOS",
"product_url": "https://eos.arista.com/",
"registry_version": 3,
"registry_version": 4,
"status": "experimental",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -2,10 +2,12 @@
"appliance_id": "8f074218-9d61-4e99-ab89-35ca19ad44ee",
"name": "ArubaOS-CX Simulation Software",
"category": "multilayer_switch",
"description": "The ArubaOS-CX Simulation Software is a virtual platform to enable simulation of the ArubaOS-CX Network Operating System. Simulated networks can be created using many of the protocols in the ArubaOS-CX Operating system like OSPF, BGP (inc. EVPN). Key features like the Aruba Network Analytics Engine and the REST API can be simulated, providing a lightweight development platform to building the modern network.",
"description": "The Aruba AOS-CX Switch Simulator is a virtual platform to enable simulation of the Aruba AOS-CX Network Operating System. Simulated networks can be created using many of the protocols in the ArubaOS-CX Operating system like OSPF, BGP (inc. EVPN). Key features like the Aruba Network Analytics Engine and the REST API can be simulated, providing a lightweight development platform to building the modern network.",
"vendor_name": "HPE Aruba",
"vendor_url": "https://www.arubanetworks.com",
"product_name": "ArubaOS-CX Simulation Software",
"documentation_url": "https://asp.arubanetworks.com/downloads;search=Aruba%20AOS%20CX%20Switch%20Simulator;products=Aruba%20Switches",
"product_name": "Aruba AOS-CX Switch Simulator",
"product_url": "https://www.arubanetworks.com/products/switches/",
"registry_version": 4,
"status": "stable",
"availability": "service-contract",
@ -30,6 +32,27 @@
"process_priority": "normal"
},
"images": [
{
"filename": "arubaoscx-disk-image-genericx86-p4-20230531220439.vmdk",
"version": "10.12.0006",
"md5sum": "c4f80fecd02ef93b431b75dd610e0063",
"filesize": 384638464,
"download_url": "https://asp.arubanetworks.com/"
},
{
"filename": "arubaoscx-disk-image-genericx86-p4-20221130174651.vmdk",
"version": "10.11.0001",
"md5sum": "ed5434173c898f47f19bfda51000611a",
"filesize": 364597760,
"download_url": "https://asp.arubanetworks.com/"
},
{
"filename": "arubaoscx-disk-image-genericx86-p4-20220815162137.vmdk",
"version": "10.10.1000",
"md5sum": "40f9ddf1e12640376af443b5d982f2f6",
"filesize": 356162560,
"download_url": "https://asp.arubanetworks.com/"
},
{
"filename": "arubaoscx-disk-image-genericx86-p4-20220616193419.vmdk",
"version": "10.10.0002",
@ -95,6 +118,24 @@
}
],
"versions": [
{
"name": "10.12.0006",
"images": {
"hda_disk_image": "arubaoscx-disk-image-genericx86-p4-20230531220439.vmdk"
}
},
{
"name": "10.11.0001",
"images": {
"hda_disk_image": "arubaoscx-disk-image-genericx86-p4-20221130174651.vmdk"
}
},
{
"name": "10.10.1000",
"images": {
"hda_disk_image": "arubaoscx-disk-image-genericx86-p4-20220815162137.vmdk"
}
},
{
"name": "10.10.0002",
"images": {

View File

@ -8,7 +8,7 @@
"documentation_url": "https://wiki.asterisk.org/wiki/display/AST/Installing+AsteriskNOW",
"product_name": "AsteriskNOW / FreePBX",
"product_url": "http://www.asterisk.org/downloads/asterisknow",
"registry_version": 3,
"registry_version": 4,
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -8,7 +8,7 @@
"documentation_url": "http://www.bigswitch.com/support",
"product_name": "Big Cloud Fabric",
"product_url": "http://www.bigswitch.com/sdn-products/big-cloud-fabrictm",
"registry_version": 3,
"registry_version": 4,
"status": "experimental",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -7,7 +7,7 @@
"vendor_url": "http://bird.network.cz/",
"documentation_url": "http://bird.network.cz/?get_doc&f=bird.html",
"product_name": "BIRD internet routing daemon",
"registry_version": 3,
"registry_version": 4,
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -0,0 +1,43 @@
{
"appliance_id": "8fecbf89-5cd1-4aea-b735-5f36cf0efbb7",
"name": "BIRD2",
"category": "router",
"description": "The BIRD project aims to develop a fully functional dynamic IP routing daemon primarily targeted on (but not limited to) Linux, FreeBSD and other UNIX-like systems and distributed under the GNU General Public License.",
"vendor_name": "CZ.NIC Labs",
"vendor_url": "https://bird.network.cz",
"documentation_url": "https://bird.network.cz/?get_doc&f=bird.html&v=20",
"product_name": "BIRD internet routing daemon",
"registry_version": 4,
"status": "stable",
"maintainer": "Bernhard Ehlers",
"maintainer_email": "dev-ehlers@mailbox.org",
"usage": "Username:\tgns3\nPassword:\tgns3\nTo become root, use \"sudo -s\".\n\nNetwork configuration:\nsudo nano /etc/network/interfaces\nsudo systemctl restart networking\n\nBIRD:\nRestart: sudo systemctl restart bird\nReconfigure: birdc configure",
"port_name_format": "eth{0}",
"qemu": {
"adapter_type": "virtio-net-pci",
"adapters": 4,
"ram": 512,
"hda_disk_interface": "scsi",
"arch": "x86_64",
"console_type": "telnet",
"kvm": "allow"
},
"images": [
{
"filename": "bird2-debian-2.0.12.qcow2",
"version": "2.0.12",
"md5sum": "435218a2e90cba921cc7fde1d64a9419",
"filesize": 287965184,
"download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/",
"direct_download_url": "https://downloads.sourceforge.net/project/gns-3/Qemu%20Appliances/bird2-debian-2.0.12.qcow2"
}
],
"versions": [
{
"name": "2.0.12",
"images": {
"hda_disk_image": "bird2-debian-2.0.12.qcow2"
}
}
]
}

View File

@ -6,7 +6,7 @@
"vendor_name": "Brocade",
"vendor_url": "https://www.brocade.com",
"product_name": "Virtual ADX",
"registry_version": 3,
"registry_version": 4,
"status": "experimental",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -8,7 +8,7 @@
"documentation_url": "http://www.brocade.com/en/products-services/software-networking/network-functions-virtualization/vrouter.html",
"product_name": "vRouter",
"product_url": "http://www.brocade.com/en/products-services/software-networking/network-functions-virtualization/vrouter.html",
"registry_version": 3,
"registry_version": 4,
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -8,7 +8,7 @@
"documentation_url": "http://www.brocade.com/en/products-services/software-networking/application-delivery-controllers/virtual-traffic-manager.html",
"product_name": "vTM DE",
"product_url": "http://www.brocade.com/en/products-services/software-networking/application-delivery-controllers/virtual-traffic-manager.html",
"registry_version": 3,
"registry_version": 4,
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -6,7 +6,7 @@
"vendor_name": "Olivier Cochard-Labbe",
"vendor_url": "https://bsdrp.net/",
"product_name": "BSDRP",
"registry_version": 3,
"registry_version": 4,
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -8,11 +8,11 @@
"documentation_url": "https://wiki.centos.org/Documentation",
"product_name": "Centos Cloud",
"product_url": "https://wiki.centos.org/Cloud",
"registry_version": 3,
"registry_version": 4,
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",
"usage": "Username: centos\nPassword: centos",
"usage": "Username: centos or cloud-user\nPassword: centos",
"port_name_format": "Ethernet{0}",
"qemu": {
"adapter_type": "virtio-net-pci",
@ -23,39 +23,73 @@
"console_type": "telnet",
"boot_priority": "c",
"kvm": "require",
"options": "-nographic"
"options": "-cpu host -nographic"
},
"images": [
{
"filename": "CentOS-7-x86_64-GenericCloud-2111.qcow2",
"version": "7 (2111)",
"md5sum": "730b8662695831670721c8245be61dac",
"filesize": 897384448,
"download_url": "https://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud-2111.qcow2"
"filename": "CentOS-Stream-GenericCloud-9-20230704.1.x86_64.qcow2",
"version": "Stream-9 (20230704.1)",
"md5sum": "e04511e019325a97837edd9eafe02b48",
"filesize": 1087868416,
"download_url": "https://cloud.centos.org/centos/9-stream/x86_64/images",
"direct_download_url": "https://cloud.centos.org/centos/9-stream/x86_64/images/CentOS-Stream-GenericCloud-9-20230704.1.x86_64.qcow2"
},
{
"filename": "CentOS-7-x86_64-GenericCloud-1809.qcow2",
"version": "7 (1809)",
"md5sum": "da79108d1324b27bd1759362b82fbe40",
"filesize": 914948096,
"download_url": "https://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud-1809.qcow2"
"filename": "CentOS-Stream-GenericCloud-8-20230710.0.x86_64.qcow2",
"version": "Stream-8 (20230710.0)",
"md5sum": "83e02ce98c29753c86fb7be7d802aa75",
"filesize": 1676164096,
"download_url": "https://cloud.centos.org/centos/8-stream/x86_64/images",
"direct_download_url": "https://cloud.centos.org/centos/8-stream/x86_64/images/CentOS-Stream-GenericCloud-8-20230710.0.x86_64.qcow2"
},
{
"filename": "CentOS-8-GenericCloud-8.4.2105-20210603.0.x86_64.qcow2",
"version": "8.4 (2105)",
"md5sum": "032eed270415526546eac07628905a62",
"filesize": 1309652992,
"download_url": "https://cloud.centos.org/centos/8/x86_64/images/CentOS-8-GenericCloud-8.4.2105-20210603.0.x86_64.qcow2"
"download_url": "https://cloud.centos.org/centos/8/x86_64/images",
"direct_download_url": "https://cloud.centos.org/centos/8/x86_64/images/CentOS-8-GenericCloud-8.4.2105-20210603.0.x86_64.qcow2"
},
{
"filename": "CentOS-7-x86_64-GenericCloud-2111.qcow2",
"version": "7 (2111)",
"md5sum": "730b8662695831670721c8245be61dac",
"filesize": 897384448,
"download_url": "https://cloud.centos.org/centos/7/images",
"direct_download_url": "https://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud-2111.qcow2"
},
{
"filename": "CentOS-7-x86_64-GenericCloud-1809.qcow2",
"version": "7 (1809)",
"md5sum": "da79108d1324b27bd1759362b82fbe40",
"filesize": 914948096,
"download_url": "https://cloud.centos.org/centos/7/images",
"direct_download_url": "https://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud-1809.qcow2"
},
{
"filename": "centos-cloud-init-data.iso",
"version": "1.0",
"md5sum": "15ca60c12db6d13b8eeae1a19613fd6e",
"filesize": 378880,
"download_url": "https://github.com/asenci/gns3-centos-cloud-init-data/raw/master/centos-cloud-init-data.iso"
"version": "1.1",
"md5sum": "59ea8223fd659d8bce9081ff175912e9",
"filesize": 374784,
"download_url": "https://github.com/GNS3/gns3-registry/tree/master/cloud-init/centos-cloud",
"direct_download_url": "https://github.com/GNS3/gns3-registry/raw/master/cloud-init/centos-cloud/centos-cloud-init-data.iso"
}
],
"versions": [
{
"name": "Stream-9 (20230704.1)",
"images": {
"hda_disk_image": "CentOS-Stream-GenericCloud-9-20230704.1.x86_64.qcow2",
"cdrom_image": "centos-cloud-init-data.iso"
}
},
{
"name": "Stream-8 (20230710.0)",
"images": {
"hda_disk_image": "CentOS-Stream-GenericCloud-8-20230710.0.x86_64.qcow2",
"cdrom_image": "centos-cloud-init-data.iso"
}
},
{
"name": "8.4 (2105)",
"images": {

View File

@ -5,8 +5,9 @@
"description": "The chromium browser",
"vendor_name": "Chromium",
"vendor_url": "https://www.chromium.org/",
"vendor_logo_url": "https://raw.githubusercontent.com/GNS3/gns3-registry/master/vendor-logos/Chromium.jpg",
"product_name": "Chromium",
"registry_version": 3,
"registry_version": 4,
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -7,7 +7,7 @@
"vendor_url": "http://www.cisco.com",
"documentation_url": "http://www.cisco.com/c/en/us/support/index.html",
"product_name": "1700",
"registry_version": 3,
"registry_version": 4,
"status": "experimental",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -7,7 +7,7 @@
"vendor_url": "http://www.cisco.com",
"documentation_url": "http://www.cisco.com/c/en/us/support/index.html",
"product_name": "2600",
"registry_version": 3,
"registry_version": 4,
"status": "experimental",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -7,7 +7,7 @@
"vendor_url": "http://www.cisco.com",
"documentation_url": "http://www.cisco.com/c/en/us/support/index.html",
"product_name": "2691",
"registry_version": 3,
"registry_version": 4,
"status": "experimental",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -7,7 +7,7 @@
"vendor_url": "http://www.cisco.com",
"documentation_url": "http://www.cisco.com/c/en/us/support/index.html",
"product_name": "3620",
"registry_version": 3,
"registry_version": 4,
"status": "experimental",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -7,7 +7,7 @@
"vendor_url": "http://www.cisco.com",
"documentation_url": "http://www.cisco.com/c/en/us/support/index.html",
"product_name": "3640",
"registry_version": 3,
"registry_version": 4,
"status": "experimental",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -7,7 +7,7 @@
"vendor_url": "http://www.cisco.com",
"documentation_url": "http://www.cisco.com/c/en/us/support/index.html",
"product_name": "3660",
"registry_version": 3,
"registry_version": 4,
"status": "experimental",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -7,7 +7,7 @@
"vendor_url": "http://www.cisco.com",
"documentation_url": "http://www.cisco.com/c/en/us/support/index.html",
"product_name": "3725",
"registry_version": 3,
"registry_version": 4,
"status": "experimental",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",
@ -21,14 +21,14 @@
"images": [
{
"filename": "c3725-adventerprisek9-mz.124-15.T14.image",
"version": "124-25.T14",
"version": "124-15.T14",
"md5sum": "64f8c427ed48fd21bd02cf1ff254c4eb",
"filesize": 97859480
}
],
"versions": [
{
"name": "124-25.T14",
"name": "124-15.T14",
"idlepc": "0x60c09aa0",
"images": {
"image": "c3725-adventerprisek9-mz.124-15.T14.image"

View File

@ -7,7 +7,7 @@
"vendor_url": "http://www.cisco.com",
"documentation_url": "http://www.cisco.com/c/en/us/support/routers/3745-multiservice-access-router/model.html",
"product_name": "3745",
"registry_version": 3,
"registry_version": 4,
"status": "experimental",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -7,7 +7,7 @@
"vendor_url": "http://www.cisco.com",
"documentation_url": "http://www.cisco.com/c/en/us/products/routers/7200-series-routers/index.html",
"product_name": "7200",
"registry_version": 3,
"registry_version": 4,
"status": "experimental",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",
@ -21,6 +21,18 @@
"npe": "npe-400"
},
"images": [
{
"filename": "c7200-adventerprisek9-mz.153-3.XB12.image",
"version": "153-3.XB12",
"md5sum": "3d234a3793331c972776354531f87221",
"filesize": 131471340
},
{
"filename": "c7200-advipservicesk9-mz.152-4.S5.image",
"version": "152-4.S5",
"md5sum": "cbbbea66a253f1dac0fcf81274dc778d",
"filesize": 87756936
},
{
"filename": "c7200-adventerprisek9-mz.124-24.T5.image",
"version": "124-24.T5",
@ -29,6 +41,20 @@
}
],
"versions": [
{
"name": "153-3.XB12",
"idlepc": "0x60630d08",
"images": {
"image": "c7200-adventerprisek9-mz.153-3.XB12.image"
}
},
{
"name": "152-4.S5",
"idlepc": "0x62cc930c",
"images": {
"image": "c7200-advipservicesk9-mz.152-4.S5.image"
}
},
{
"name": "124-24.T5",
"idlepc": "0x606df838",

View File

@ -7,7 +7,7 @@
"vendor_url": "http://www.cisco.com/",
"product_name": "ASA",
"product_url": "http://www.cisco.com/c/en/us/products/security/adaptive-security-appliance-asa-software/index.html",
"registry_version": 3,
"registry_version": 4,
"status": "broken",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -8,7 +8,7 @@
"documentation_url": "http://www.cisco.com/c/en/us/support/security/virtual-adaptive-security-appliance-firewall/products-installation-guides-list.html",
"product_name": "ASAv",
"product_url": "http://www.cisco.com/c/en/us/products/security/virtual-adaptive-security-appliance-firewall/index.html",
"registry_version": 3,
"registry_version": 4,
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",
@ -26,7 +26,7 @@
"kvm": "require"
},
"images": [
{
{
"filename": "asav9-16-2.qcow2",
"version": "9.16.2 CML",
"md5sum": "1f8db97063a7f738fddc81ac880a906c",

View File

@ -8,7 +8,7 @@
"documentation_url": "https://www.cisco.com/c/en/us/td/docs/routers/C8000V/Configuration/c8000v-installation-configuration-guide.html",
"product_name": "c8000v",
"product_url": "https://www.cisco.com/c/en/us/support/routers/catalyst-8000v-edge-software/series.html",
"registry_version": 3,
"registry_version": 4,
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",
@ -24,10 +24,17 @@
"kvm": "require"
},
"images": [
{
"filename": "c8000v-universalk9_8G_serial.17.06.05.qcow2",
"version": "17.06.05 8G",
"md5sum": "aeb15ab8e1cbd0cd76f7260a81442f98",
"filesize": 1777795072,
"download_url": "https://software.cisco.com/download/home/286327102/type/282046477/release/Bengaluru-17.6.5"
},
{
"filename": "c8000v-universalk9_8G_serial.17.06.01a.qcow2",
"version": "17.06.01a 8G",
"md5sum": "d8b8ae633d953ec1b6d8f18a09a4f4e7",
"md5sum": "e278fa644295c703976a86f7f1c1cd65",
"filesize": 1595277312,
"download_url": "https://software.cisco.com/download/home/286327102/type/282046477/release/Bengaluru-17.6.1a"
},
@ -47,6 +54,12 @@
}
],
"versions": [
{
"name": "17.06.05 8G",
"images": {
"hda_disk_image": "c8000v-universalk9_8G_serial.17.06.05.qcow2"
}
},
{
"name": "17.06.01a 8G",
"images": {

View File

@ -0,0 +1,45 @@
{
"appliance_id": "57a85f0e-b8ae-4820-bd2b-816b2cceb842",
"name": "Cisco CAT IOS-XE 9000v",
"category": "multilayer_switch",
"description": "Cisco IOS-XE 9000v. This appliance requires 16GB of memory to run! Recommend 2 or more vCPUs for faster boot performance",
"vendor_name": "Cisco",
"vendor_url": "http://www.cisco.com/",
"documentation_url": "https://developer.cisco.com/docs/modeling-labs/2-5/#!cml-release-notes",
"product_name": "Cisco CAT IOS-XE 9000v",
"product_url": "http://virl.cisco.com/",
"registry_version": 4,
"status": "experimental",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",
"usage": "There is no default configuration present. Virtual Switch and Interfaces may take several minutes to be usable after appliance boot.",
"first_port_name": "GigabitEthernet0/0",
"port_name_format": "GigabitEthernet1/0/{port1}",
"qemu": {
"adapter_type": "virtio-net-pci",
"adapters": 9,
"ram": 16384,
"cpus": 2,
"hda_disk_interface": "virtio",
"arch": "x86_64",
"console_type": "telnet",
"kvm": "require"
},
"images": [
{
"filename": "cat9kv-prd-17.10.01prd7.qcow2",
"version": "17.10(1)",
"md5sum": "ffdbace33d31deae33e2a920a96b79ef",
"filesize": 2155806720,
"download_url": "https://learningnetworkstore.cisco.com/myaccount"
}
],
"versions": [
{
"name": "17.10(1)",
"images": {
"hda_disk_image": "cat9kv-prd-17.10.01prd7.qcow2"
}
}
]
}

View File

@ -8,7 +8,7 @@
"documentation_url": "http://www.cisco.com/c/en/us/support/routers/cloud-services-router-1000v-series/products-installation-and-configuration-guides-list.html",
"product_name": "CSR1000v",
"product_url": "http://www.cisco.com/c/en/us/support/routers/cloud-services-router-1000v-series/tsd-products-support-series-home.html",
"registry_version": 3,
"registry_version": 4,
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -8,7 +8,7 @@
"documentation_url": "http://www.cisco.com/c/en/us/support/cloud-systems-management/data-center-network-manager-10/model.html",
"product_name": "DCNM",
"product_url": "http://www.cisco.com/c/en/us/products/cloud-systems-management/prime-data-center-network-manager/index.html",
"registry_version": 3,
"registry_version": 4,
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -77,6 +77,13 @@
"md5sum": "4cf5b7fd68075b6f7ee0dd41a4029ca0",
"filesize": 2150017536,
"download_url": "https://software.cisco.com/download/"
},
{
"filename": "Cisco_Firepower_Management_Center_Virtual-6.2.2-81.qcow2",
"version": "6.2.2 (81)",
"md5sum": "2f75c9c6c18a6fbb5516f6f451aef3a4",
"filesize": 2112356352,
"download_url": "https://software.cisco.com/download/"
}
],
"versions": [
@ -121,6 +128,12 @@
"images": {
"hda_disk_image": "Cisco_Firepower_Management_Center_Virtual_VMware-6.2.1-342-disk1.vmdk"
}
},
{
"name": "6.2.2 (81)",
"images": {
"hda_disk_image": "Cisco_Firepower_Management_Center_Virtual-6.2.2-81.qcow2"
}
}
]
}

View File

@ -7,7 +7,7 @@
"vendor_url": "http://www.cisco.com/",
"product_name": "IOSv",
"product_url": "http://virl.cisco.com/",
"registry_version": 3,
"registry_version": 4,
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",
@ -32,6 +32,13 @@
"download_url": "https://sourceforge.net/projects/gns-3/files",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/IOSv_startup_config.img/download"
},
{
"filename": "vios-adventerprisek9-m.spa.159-3.m6.qcow2",
"version": "15.9(3)M6",
"md5sum": "49a6977977263b2774bebc56e4e678ff",
"filesize": 57309696,
"download_url": "https://learningnetworkstore.cisco.com/myaccount"
},
{
"filename": "vios-adventerprisek9-m.spa.159-3.m4.qcow2",
"version": "15.9(3)M4",
@ -90,6 +97,13 @@
}
],
"versions": [
{
"name": "15.9(3)M6",
"images": {
"hda_disk_image": "vios-adventerprisek9-m.spa.159-3.m6.qcow2",
"hdb_disk_image": "IOSv_startup_config.img"
}
},
{
"name": "15.9(3)M4",
"images": {

View File

@ -7,7 +7,7 @@
"vendor_url": "http://www.cisco.com/",
"product_name": "IOSvL2",
"product_url": "http://virl.cisco.com/",
"registry_version": 3,
"registry_version": 4,
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -8,7 +8,7 @@
"documentation_url": "http://www.cisco.com/c/en/us/td/docs/ios_xr_sw/ios_xrv/release/notes/xrv-rn.html",
"product_name": "IOS XRv",
"product_url": "http://virl.cisco.com/",
"registry_version": 3,
"registry_version": 4,
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -8,7 +8,7 @@
"documentation_url": "http://www.cisco.com/c/en/us/td/docs/ios_xr_sw/ios_xrv/release/notes/xrv-rn.html",
"product_name": "IOS XRv 9000",
"product_url": "http://virl.cisco.com/",
"registry_version": 3,
"registry_version": 4,
"status": "experimental",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -6,7 +6,7 @@
"vendor_name": "Cisco",
"vendor_url": "http://www.cisco.com",
"product_name": "Cisco IOU L2",
"registry_version": 3,
"registry_version": 4,
"status": "experimental",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -6,7 +6,7 @@
"vendor_name": "Cisco",
"vendor_url": "http://www.cisco.com",
"product_name": "Cisco IOU L3",
"registry_version": 3,
"registry_version": 4,
"status": "experimental",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -7,7 +7,7 @@
"vendor_url": "http://www.cisco.com/",
"product_name": "NX-OSv",
"product_url": "http://virl.cisco.com/",
"registry_version": 3,
"registry_version": 4,
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -33,6 +33,27 @@
"filesize": 1592000512,
"download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/10.1(1)"
},
{
"filename": "nexus9300v.10.1.1.qcow2",
"version": "9300v 10.1.1",
"md5sum": "4051bdb96aff6e54b72b7e3b06c9d6eb",
"filesize": 1990983680,
"download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/10.1(1)"
},
{
"filename": "nexus9500v.9.3.9.qcow2",
"version": "9500v 9.3.9",
"md5sum": "30c25039927f89aebe73ea20d15abd6d",
"filesize": 1980760064,
"download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/9.3(9)"
},
{
"filename": "nexus9300v.9.3.9.qcow2",
"version": "9300v 9.3.9",
"md5sum": "e807005cb7d2d2957b4af0e59f368b36",
"filesize": 1980563456,
"download_url": "https://software.cisco.com/download/home/286312239/type/282088129/release/9.3(9)"
},
{
"filename": "nexus9300v.9.3.8.qcow2",
"version": "9300v 9.3.8",
@ -167,12 +188,12 @@
"download_url": "https://software.cisco.com/download/"
},
{
"filename": "OVMF-20160813.fd",
"version": "16.08.13",
"md5sum": "8ff0ef1ec56345db5b6bda1a8630e3c6",
"filesize": 2097152,
"download_url": "",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/OVMF-20160813.fd.zip/download",
"filename": "OVMF-edk2-stable202305.fd",
"version": "stable202305",
"md5sum": "6c4cf1519fec4a4b95525d9ae562963a",
"filesize": 4194304,
"download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/OVMF-edk2-stable202305.fd.zip/download",
"compression": "zip"
}
],
@ -180,140 +201,161 @@
{
"name": "9500v 10.1.1",
"images": {
"bios_image": "OVMF-20160813.fd",
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nexus9500v64.10.1.1.qcow2"
}
},
{
{
"name": "9300v 10.1.1",
"images": {
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nexus9300v.10.1.1.qcow2"
}
},
{
"name": "9500v 9.3.9",
"images": {
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nexus9500v.9.3.9.qcow2"
}
},
{
"name": "9300v 9.3.9",
"images": {
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nexus9300v.9.3.9.qcow2"
}
},
{
"name": "9300v 9.3.8",
"images": {
"bios_image": "OVMF-20160813.fd",
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nexus9300v.9.3.8.qcow2"
}
},
{
"name": "9500v 9.3.7",
"images": {
"bios_image": "OVMF-20160813.fd",
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nexus9500v.9.3.7.qcow2"
}
},
{
"name": "9500v 9.3.3",
"images": {
"bios_image": "OVMF-20160813.fd",
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nexus9500v.9.3.3.qcow2"
}
},
{
"name": "9300v 9.3.3",
"images": {
"bios_image": "OVMF-20160813.fd",
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nexus9300v.9.3.3.qcow2"
}
},
{
"name": "9.3.1",
"images": {
"bios_image": "OVMF-20160813.fd",
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nxosv.9.3.1.qcow2"
}
},
{
"name": "9.2.3",
"images": {
"bios_image": "OVMF-20160813.fd",
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nxosv-final.9.2.3.qcow2"
}
},
{
"name": "9.2.2",
"images": {
"bios_image": "OVMF-20160813.fd",
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nxosv-final.9.2.2.qcow2"
}
},
{
"name": "9.2.1",
"images": {
"bios_image": "OVMF-20160813.fd",
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nxosv-final.9.2.1.qcow2"
}
},
{
"name": "7.0.3.I7.9",
"images": {
"bios_image": "OVMF-20160813.fd",
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nxosv-final.7.0.3.I7.9.qcow2"
}
},
{
"name": "7.0.3.I7.7",
"images": {
"bios_image": "OVMF-20160813.fd",
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nxosv-final.7.0.3.I7.7.qcow2"
}
},
{
"name": "7.0.3.I7.6",
"images": {
"bios_image": "OVMF-20160813.fd",
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nxosv-final.7.0.3.I7.6.qcow2"
}
},
{
"name": "7.0.3.I7.5",
"images": {
"bios_image": "OVMF-20160813.fd",
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nxosv-final.7.0.3.I7.5.qcow2"
}
},
{
"name": "7.0.3.I7.4",
"images": {
"bios_image": "OVMF-20160813.fd",
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nxosv-final.7.0.3.I7.4.qcow2"
}
},
{
"name": "7.0.3.I7.3",
"images": {
"bios_image": "OVMF-20160813.fd",
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nxosv-final.7.0.3.I7.3.qcow2"
}
},
{
"name": "7.0.3.I7.2",
"images": {
"bios_image": "OVMF-20160813.fd",
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nxosv-final.7.0.3.I7.2.qcow2"
}
},
{
"name": "7.0.3.I7.1",
"images": {
"bios_image": "OVMF-20160813.fd",
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nxosv-final.7.0.3.I7.1.qcow2"
}
},
{
"name": "7.0.3.I6.1",
"images": {
"bios_image": "OVMF-20160813.fd",
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nxosv-final.7.0.3.I6.1.qcow2"
}
},
{
"name": "7.0.3.I5.2",
"images": {
"bios_image": "OVMF-20160813.fd",
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nxosv-final.7.0.3.I5.2.qcow2"
}
},
{
"name": "7.0.3.I5.1",
"images": {
"bios_image": "OVMF-20160813.fd",
"bios_image": "OVMF-edk2-stable202305.fd",
"hda_disk_image": "nxosv-final.7.0.3.I5.1.qcow2"
}
}

View File

@ -0,0 +1,19 @@
{
"appliance_id": "d9ce131e-ecdc-49d2-be7d-d883d3919a06",
"name": "Cisco PyATS",
"category": "guest",
"description": "pyATS is an end-to-end DevOps automation ecosystem. Agnostic by design, pyATS enable network engineers to automate their day-to-day DevOps activities, perform stateful validation of their device operational status, build a safety-net of scalable, data-driven and reusable tests around their network, and visualize everything in a modern, easy to use dashboard.",
"vendor_name": "Cisco",
"vendor_url": "https://cisco.com",
"product_name": "PyATS",
"product_url": "https://developer.cisco.com/pyats/",
"registry_version": 4,
"status": "stable",
"maintainer": "Xander Petty",
"maintainer_email": "Xander.Petty@protonmail.com",
"docker": {
"adapters": 1,
"image": "gns3/pyats:latest",
"console_type": "telnet"
}
}

View File

@ -8,7 +8,7 @@
"documentation_url": "http://www.cisco.com/c/en/us/products/wireless/wireless-lan-controller/index.html",
"product_name": "Virtual Wireless LAN Controller",
"product_url": "http://www.cisco.com/c/en/us/support/wireless/virtual-wireless-controller/tsd-products-support-series-home.html",
"registry_version": 3,
"registry_version": 4,
"status": "experimental",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -8,7 +8,7 @@
"documentation_url": "http://www.cisco.com/c/en/us/support/security/web-security-appliance/tsd-products-support-series-home.html",
"product_name": "Web Security Virtual Appliance",
"product_url": "http://www.cisco.com/c/en/us/products/security/web-security-appliance/index.html",
"registry_version": 3,
"registry_version": 4,
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -8,7 +8,7 @@
"documentation_url": "https://www.citrix.com/products/netscaler-adc/support.html",
"product_name": "NetScaler VPX",
"product_url": "https://www.citrix.com/products/netscaler-adc/",
"registry_version": 3,
"registry_version": 4,
"status": "stable",
"maintainer": "GNS3 Team",
"maintainer_email": "developers@gns3.net",

View File

@ -2,7 +2,7 @@
"appliance_id": "2d5634dc-ad39-46cf-a2fd-17b291abab91",
"name": "Citrix SD-WAN",
"category": "router",
"description": "A software-defined wide area network (SD-WAN) is a virtual WAN architecture, in which any blend of network transport types not only multiprotocol label switching (MPLS) but also broadband internet, cellular, and satellite can be virtualized and bonded then centrally managed in software, to securely connect users to applications and desktops in accordance with policy. Essentially, SD-WAN is software-defined networking (SDN) for the WAN.",
"description": "A software-defined wide area network (SD-WAN) is a virtual WAN architecture, in which any blend of network transport types \u2014 not only multiprotocol label switching (MPLS) but also broadband internet, cellular, and satellite \u2014 can be virtualized and bonded then centrally managed in software, to securely connect users to applications and desktops in accordance with policy. Essentially, SD-WAN is software-defined networking (SDN) for the WAN.",
"vendor_name": "Citrix",
"vendor_url": "http://www.citrix.com/",
"documentation_url": "https://docs.citrix.com/en-us/citrix-sd-wan",

View File

@ -18,7 +18,7 @@
"adapter_type": "virtio-net-pci",
"adapters": 4,
"ram": 8192,
"cpus": 4,
"cpus": 4,
"hda_disk_interface": "ide",
"arch": "x86_64",
"console_type": "telnet",

View File

@ -18,7 +18,7 @@
"qemu": {
"adapter_type": "virtio-net-pci",
"adapters": 4,
"ram": 1024,
"ram": 1024,
"hda_disk_interface": "virtio",
"arch": "x86_64",
"console_type": "telnet",
@ -37,10 +37,10 @@
],
"versions": [
{
"name": "cOS Stream 3.80.09",
"images": {
"hda_disk_image": "clavister-cos-stream-3.80.09.01-virtual-x64-generic.qcow2"
},
"name": "cOS Stream 3.80.09"
}
}
]
}

Some files were not shown because too many files have changed in this diff Show More