gns3-server 3.0.0rc2__py3-none-any.whl → 3.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of gns3-server might be problematic. Click here for more details.
- {gns3_server-3.0.0rc2.dist-info → gns3_server-3.0.2.dist-info}/METADATA +19 -18
- {gns3_server-3.0.0rc2.dist-info → gns3_server-3.0.2.dist-info}/RECORD +48 -42
- {gns3_server-3.0.0rc2.dist-info → gns3_server-3.0.2.dist-info}/WHEEL +1 -1
- gns3server/api/routes/controller/images.py +58 -21
- gns3server/api/routes/controller/projects.py +20 -4
- gns3server/api/routes/controller/templates.py +23 -2
- gns3server/api/routes/index.py +3 -3
- gns3server/api/server.py +38 -3
- gns3server/appliances/arista-veos.gns3a +20 -514
- gns3server/appliances/cisco-7200.gns3a +26 -0
- gns3server/appliances/cisco-asav.gns3a +14 -1
- gns3server/appliances/cisco-iou-l2.gns3a +16 -4
- gns3server/appliances/cisco-iou-l3.gns3a +16 -4
- gns3server/appliances/innovaphone-app.gns3a +50 -0
- gns3server/appliances/innovaphone-ipva.gns3a +78 -0
- gns3server/appliances/pfsense.gns3a +14 -0
- gns3server/compute/docker/__init__.py +1 -1
- gns3server/compute/iou/iou_vm.py +22 -12
- gns3server/controller/__init__.py +48 -38
- gns3server/controller/appliance_manager.py +2 -2
- gns3server/controller/compute.py +8 -3
- gns3server/controller/node.py +2 -6
- gns3server/controller/project.py +1 -2
- gns3server/crash_report.py +1 -1
- gns3server/db/repositories/images.py +22 -3
- gns3server/db/repositories/pools.py +1 -1
- gns3server/db/repositories/templates.py +11 -0
- gns3server/db/tasks.py +120 -79
- gns3server/disks/empty100G.qcow2 +0 -0
- gns3server/disks/empty200G.qcow2 +0 -0
- gns3server/disks/empty30G.qcow2 +0 -0
- gns3server/disks/empty8G.qcow2 +0 -0
- gns3server/schemas/config.py +1 -1
- gns3server/server.py +3 -3
- gns3server/services/authentication.py +9 -6
- gns3server/static/favicon.ico +0 -0
- gns3server/static/redoc.standalone.js +1782 -0
- gns3server/static/swagger-ui-bundle.js +2 -0
- gns3server/static/swagger-ui.css +3 -0
- gns3server/static/web-ui/index.html +1 -1
- gns3server/static/web-ui/main.62c99707e4709a56.js +1 -0
- gns3server/utils/asyncio/__init__.py +4 -12
- gns3server/utils/asyncio/pool.py +1 -4
- gns3server/utils/images.py +62 -39
- gns3server/version.py +2 -2
- gns3server/static/web-ui/main.ed82697b58d803e7.js +0 -1
- {gns3_server-3.0.0rc2.dist-info → gns3_server-3.0.2.dist-info}/LICENSE +0 -0
- {gns3_server-3.0.0rc2.dist-info → gns3_server-3.0.2.dist-info}/entry_points.txt +0 -0
- {gns3_server-3.0.0rc2.dist-info → gns3_server-3.0.2.dist-info}/top_level.txt +0 -0
gns3server/controller/compute.py
CHANGED
|
@@ -18,14 +18,19 @@
|
|
|
18
18
|
import ipaddress
|
|
19
19
|
import aiohttp
|
|
20
20
|
import asyncio
|
|
21
|
-
import async_timeout
|
|
22
21
|
import socket
|
|
23
22
|
import json
|
|
24
23
|
import sys
|
|
25
24
|
import io
|
|
25
|
+
|
|
26
26
|
from fastapi import HTTPException
|
|
27
27
|
from aiohttp import web
|
|
28
28
|
|
|
29
|
+
if sys.version_info >= (3, 11):
|
|
30
|
+
from asyncio import timeout as asynctimeout
|
|
31
|
+
else:
|
|
32
|
+
from async_timeout import timeout as asynctimeout
|
|
33
|
+
|
|
29
34
|
from ..utils import parse_version
|
|
30
35
|
from ..utils.asyncio import locking
|
|
31
36
|
from ..controller.controller_error import (
|
|
@@ -502,8 +507,8 @@ class Compute:
|
|
|
502
507
|
""" Returns URL for specific path at Compute"""
|
|
503
508
|
return self._getUrl(path)
|
|
504
509
|
|
|
505
|
-
async def _run_http_query(self, method, path, data=None, timeout=
|
|
506
|
-
async with
|
|
510
|
+
async def _run_http_query(self, method, path, data=None, timeout=120, raw=False):
|
|
511
|
+
async with asynctimeout(delay=timeout):
|
|
507
512
|
url = self._getUrl(path)
|
|
508
513
|
headers = {"content-type": "application/json"}
|
|
509
514
|
chunked = None
|
gns3server/controller/node.py
CHANGED
|
@@ -572,15 +572,11 @@ class Node:
|
|
|
572
572
|
Start a node
|
|
573
573
|
"""
|
|
574
574
|
try:
|
|
575
|
-
# For IOU we need to send the licence everytime
|
|
575
|
+
# For IOU: we need to send the licence everytime we start a node
|
|
576
576
|
if self.node_type == "iou":
|
|
577
577
|
license_check = self._project.controller.iou_license.get("license_check", True)
|
|
578
578
|
iourc_content = self._project.controller.iou_license.get("iourc_content", None)
|
|
579
|
-
|
|
580
|
-
# raise aiohttp.web.HTTPConflict(text="IOU licence is not configured")
|
|
581
|
-
await self.post(
|
|
582
|
-
"/start", timeout=240, data={"license_check": license_check, "iourc_content": iourc_content}
|
|
583
|
-
)
|
|
579
|
+
await self.post("/start", timeout=240, data={"license_check": license_check, "iourc_content": iourc_content})
|
|
584
580
|
else:
|
|
585
581
|
await self.post("/start", data=data, timeout=240)
|
|
586
582
|
except asyncio.TimeoutError:
|
gns3server/controller/project.py
CHANGED
|
@@ -595,7 +595,7 @@ class Project:
|
|
|
595
595
|
|
|
596
596
|
if node_type == "iou":
|
|
597
597
|
async with self._iou_id_lock:
|
|
598
|
-
# wait for
|
|
598
|
+
# wait for an IOU node to be completely created before adding a new one
|
|
599
599
|
# this is important otherwise we allocate the same application ID (used
|
|
600
600
|
# to generate MAC addresses) when creating multiple IOU node at the same time
|
|
601
601
|
if "properties" in kwargs.keys():
|
|
@@ -1339,7 +1339,6 @@ class Project:
|
|
|
1339
1339
|
Copy the project files directly rather than in an import-export fashion.
|
|
1340
1340
|
|
|
1341
1341
|
:param name: Name of the new project. A new one will be generated in case of conflicts
|
|
1342
|
-
:param location: Parent directory of the new project
|
|
1343
1342
|
:param reset_mac_addresses: Reset MAC addresses for the new project
|
|
1344
1343
|
"""
|
|
1345
1344
|
|
gns3server/crash_report.py
CHANGED
|
@@ -58,7 +58,7 @@ class CrashReport:
|
|
|
58
58
|
Report crash to a third party service
|
|
59
59
|
"""
|
|
60
60
|
|
|
61
|
-
DSN = "https://
|
|
61
|
+
DSN = "https://9cf53e6b9adfe49b867f1847b7cc4d72@o19455.ingest.us.sentry.io/38482"
|
|
62
62
|
_instance = None
|
|
63
63
|
|
|
64
64
|
def __init__(self):
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import os
|
|
19
19
|
|
|
20
20
|
from typing import Optional, List
|
|
21
|
-
from sqlalchemy import select, delete
|
|
21
|
+
from sqlalchemy import select, delete, update
|
|
22
22
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
23
23
|
|
|
24
24
|
from .base import BaseRepository
|
|
@@ -103,6 +103,22 @@ class ImagesRepository(BaseRepository):
|
|
|
103
103
|
await self._db_session.refresh(db_image)
|
|
104
104
|
return db_image
|
|
105
105
|
|
|
106
|
+
async def update_image(self, image_path: str, checksum: str, checksum_algorithm: str) -> models.Image:
|
|
107
|
+
"""
|
|
108
|
+
Update an image.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
query = update(models.Image).\
|
|
112
|
+
where(models.Image.path == image_path).\
|
|
113
|
+
values(checksum=checksum, checksum_algorithm=checksum_algorithm)
|
|
114
|
+
|
|
115
|
+
await self._db_session.execute(query)
|
|
116
|
+
await self._db_session.commit()
|
|
117
|
+
image_db = await self.get_image_by_checksum(checksum)
|
|
118
|
+
if image_db:
|
|
119
|
+
await self._db_session.refresh(image_db) # force refresh of updated_at value
|
|
120
|
+
return image_db
|
|
121
|
+
|
|
106
122
|
async def delete_image(self, image_path: str) -> bool:
|
|
107
123
|
"""
|
|
108
124
|
Delete an image.
|
|
@@ -119,7 +135,7 @@ class ImagesRepository(BaseRepository):
|
|
|
119
135
|
await self._db_session.commit()
|
|
120
136
|
return result.rowcount > 0
|
|
121
137
|
|
|
122
|
-
async def prune_images(self) -> int:
|
|
138
|
+
async def prune_images(self, skip_images: list[str] = None) -> int:
|
|
123
139
|
"""
|
|
124
140
|
Prune images not attached to any template.
|
|
125
141
|
"""
|
|
@@ -130,12 +146,15 @@ class ImagesRepository(BaseRepository):
|
|
|
130
146
|
images = result.scalars().all()
|
|
131
147
|
images_deleted = 0
|
|
132
148
|
for image in images:
|
|
149
|
+
if skip_images and image.filename in skip_images:
|
|
150
|
+
log.debug(f"Skipping image '{image.path}' for pruning")
|
|
151
|
+
continue
|
|
133
152
|
try:
|
|
134
153
|
log.debug(f"Deleting image '{image.path}'")
|
|
135
154
|
os.remove(image.path)
|
|
136
155
|
except OSError:
|
|
137
156
|
log.warning(f"Could not delete image file {image.path}")
|
|
138
|
-
if await self.delete_image(image.
|
|
157
|
+
if await self.delete_image(image.path):
|
|
139
158
|
images_deleted += 1
|
|
140
159
|
log.info(f"{images_deleted} image(s) have been deleted")
|
|
141
160
|
return images_deleted
|
|
@@ -80,7 +80,7 @@ class ResourcePoolsRepository(BaseRepository):
|
|
|
80
80
|
await self._db_session.commit()
|
|
81
81
|
return result.rowcount > 0
|
|
82
82
|
|
|
83
|
-
async def get_resource_memberships(self, resource_id: UUID) -> List[models.
|
|
83
|
+
async def get_resource_memberships(self, resource_id: UUID) -> List[models.ResourcePool]:
|
|
84
84
|
"""
|
|
85
85
|
Get all resource memberships in resource pools.
|
|
86
86
|
"""
|
|
@@ -170,3 +170,14 @@ class TemplatesRepository(BaseRepository):
|
|
|
170
170
|
await self._db_session.commit()
|
|
171
171
|
await self._db_session.refresh(template_in_db)
|
|
172
172
|
return template_in_db
|
|
173
|
+
|
|
174
|
+
async def get_template_images(self, template_id: UUID) -> List[models.Image]:
|
|
175
|
+
"""
|
|
176
|
+
Return all images attached to a template.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
query = select(models.Image).\
|
|
180
|
+
join(models.Image.templates).\
|
|
181
|
+
filter(models.Template.template_id == template_id)
|
|
182
|
+
result = await self._db_session.execute(query)
|
|
183
|
+
return result.scalars().all()
|
gns3server/db/tasks.py
CHANGED
|
@@ -16,13 +16,11 @@
|
|
|
16
16
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
import asyncio
|
|
19
|
-
import
|
|
19
|
+
import time
|
|
20
20
|
import os
|
|
21
21
|
|
|
22
22
|
from fastapi import FastAPI
|
|
23
23
|
from pydantic import ValidationError
|
|
24
|
-
from watchfiles import awatch, Change
|
|
25
|
-
|
|
26
24
|
from typing import List
|
|
27
25
|
from sqlalchemy import event
|
|
28
26
|
from sqlalchemy.engine import Engine
|
|
@@ -32,10 +30,13 @@ from alembic import command, config
|
|
|
32
30
|
from alembic.script import ScriptDirectory
|
|
33
31
|
from alembic.runtime.migration import MigrationContext
|
|
34
32
|
from alembic.util.exc import CommandError
|
|
33
|
+
from watchdog.observers import Observer
|
|
34
|
+
from watchdog.events import FileSystemEvent, PatternMatchingEventHandler
|
|
35
35
|
|
|
36
36
|
from gns3server.db.repositories.computes import ComputesRepository
|
|
37
37
|
from gns3server.db.repositories.images import ImagesRepository
|
|
38
|
-
from gns3server.utils.images import
|
|
38
|
+
from gns3server.utils.images import md5sum, discover_images, read_image_info, InvalidImageError
|
|
39
|
+
from gns3server.utils.asyncio import wait_run_in_executor
|
|
39
40
|
from gns3server import schemas
|
|
40
41
|
|
|
41
42
|
from .models import Base
|
|
@@ -130,81 +131,7 @@ async def get_computes(app: FastAPI) -> List[dict]:
|
|
|
130
131
|
return computes
|
|
131
132
|
|
|
132
133
|
|
|
133
|
-
def
|
|
134
|
-
|
|
135
|
-
if change == Change.added and os.path.isfile(path):
|
|
136
|
-
if path.endswith(".tmp") or path.endswith(".md5sum") or path.startswith("."):
|
|
137
|
-
return False
|
|
138
|
-
if "/lib/" in path or "/lib64/" in path:
|
|
139
|
-
# ignore custom IOU libraries
|
|
140
|
-
return False
|
|
141
|
-
header_magic_len = 7
|
|
142
|
-
with open(path, "rb") as f:
|
|
143
|
-
image_header = f.read(header_magic_len) # read the first 7 bytes of the file
|
|
144
|
-
if len(image_header) >= header_magic_len:
|
|
145
|
-
try:
|
|
146
|
-
check_valid_image_header(image_header)
|
|
147
|
-
except InvalidImageError as e:
|
|
148
|
-
log.debug(f"New image '{path}': {e}")
|
|
149
|
-
return False
|
|
150
|
-
else:
|
|
151
|
-
log.debug(f"New image '{path}': size is too small to be valid")
|
|
152
|
-
return False
|
|
153
|
-
return True
|
|
154
|
-
# FIXME: should we support image deletion?
|
|
155
|
-
# elif change == Change.deleted:
|
|
156
|
-
# return True
|
|
157
|
-
return False
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
async def monitor_images_on_filesystem(app: FastAPI):
|
|
161
|
-
|
|
162
|
-
directories_to_monitor = []
|
|
163
|
-
for image_type in ("qemu", "ios", "iou"):
|
|
164
|
-
image_dir = default_images_directory(image_type)
|
|
165
|
-
if os.path.isdir(image_dir):
|
|
166
|
-
log.debug(f"Monitoring for new images in '{image_dir}'")
|
|
167
|
-
directories_to_monitor.append(image_dir)
|
|
168
|
-
|
|
169
|
-
try:
|
|
170
|
-
async for changes in awatch(
|
|
171
|
-
*directories_to_monitor,
|
|
172
|
-
watch_filter=image_filter,
|
|
173
|
-
raise_interrupt=True
|
|
174
|
-
):
|
|
175
|
-
async with AsyncSession(app.state._db_engine) as db_session:
|
|
176
|
-
images_repository = ImagesRepository(db_session)
|
|
177
|
-
for change in changes:
|
|
178
|
-
change_type, image_path = change
|
|
179
|
-
if change_type == Change.added:
|
|
180
|
-
try:
|
|
181
|
-
image = await read_image_info(image_path)
|
|
182
|
-
except InvalidImageError as e:
|
|
183
|
-
log.warning(str(e))
|
|
184
|
-
continue
|
|
185
|
-
try:
|
|
186
|
-
if await images_repository.get_image(image_path):
|
|
187
|
-
continue
|
|
188
|
-
await images_repository.add_image(**image)
|
|
189
|
-
log.info(f"Discovered image '{image_path}' has been added to the database")
|
|
190
|
-
except SQLAlchemyError as e:
|
|
191
|
-
log.warning(f"Error while adding image '{image_path}' to the database: {e}")
|
|
192
|
-
# if change_type == Change.deleted:
|
|
193
|
-
# try:
|
|
194
|
-
# if await images_repository.get_image(image_path):
|
|
195
|
-
# success = await images_repository.delete_image(image_path)
|
|
196
|
-
# if not success:
|
|
197
|
-
# log.warning(f"Could not delete image '{image_path}' from the database")
|
|
198
|
-
# else:
|
|
199
|
-
# log.info(f"Image '{image_path}' has been deleted from the database")
|
|
200
|
-
# except SQLAlchemyError as e:
|
|
201
|
-
# log.warning(f"Error while deleting image '{image_path}' from the database: {e}")
|
|
202
|
-
except KeyboardInterrupt:
|
|
203
|
-
# send SIGTERM to the server PID so uvicorn can shutdown the process
|
|
204
|
-
os.kill(os.getpid(), signal.SIGTERM)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
async def discover_images_on_filesystem(app: FastAPI):
|
|
134
|
+
async def discover_images_on_filesystem(app: FastAPI) -> None:
|
|
208
135
|
|
|
209
136
|
async with AsyncSession(app.state._db_engine) as db_session:
|
|
210
137
|
images_repository = ImagesRepository(db_session)
|
|
@@ -228,3 +155,117 @@ async def discover_images_on_filesystem(app: FastAPI):
|
|
|
228
155
|
|
|
229
156
|
# monitor if images have been manually added
|
|
230
157
|
asyncio.create_task(monitor_images_on_filesystem(app))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
async def update_disk_checksums(updated_disks: List[str]) -> None:
|
|
161
|
+
"""
|
|
162
|
+
Update the checksum of a list of disks in the database.
|
|
163
|
+
|
|
164
|
+
:param updated_disks: list of updated disks
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
from gns3server.api.server import app
|
|
168
|
+
async with AsyncSession(app.state._db_engine) as db_session:
|
|
169
|
+
images_repository = ImagesRepository(db_session)
|
|
170
|
+
for path in updated_disks:
|
|
171
|
+
image = await images_repository.get_image(path)
|
|
172
|
+
if image:
|
|
173
|
+
log.info(f"Updating image '{path}' in the database")
|
|
174
|
+
checksum = await wait_run_in_executor(md5sum, path, cache_to_md5file=False)
|
|
175
|
+
if image.checksum != checksum:
|
|
176
|
+
await images_repository.update_image(path, checksum, "md5")
|
|
177
|
+
|
|
178
|
+
class EventHandler(PatternMatchingEventHandler):
|
|
179
|
+
"""
|
|
180
|
+
Watchdog event handler.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
def __init__(self, queue: asyncio.Queue, loop: asyncio.BaseEventLoop, **kwargs):
|
|
184
|
+
|
|
185
|
+
self._loop = loop
|
|
186
|
+
self._queue = queue
|
|
187
|
+
|
|
188
|
+
# ignore temporary files, md5sum files, hidden files and directories
|
|
189
|
+
super().__init__(ignore_patterns=["*.tmp", "*.md5sum", ".*"], ignore_directories = True, **kwargs)
|
|
190
|
+
|
|
191
|
+
def on_closed(self, event: FileSystemEvent) -> None:
|
|
192
|
+
# monitor for closed files (e.g. when a file has finished to be copied)
|
|
193
|
+
if "/lib/" in event.src_path or "/lib64/" in event.src_path:
|
|
194
|
+
return # ignore custom IOU libraries
|
|
195
|
+
self._loop.call_soon_threadsafe(self._queue.put_nowait, event)
|
|
196
|
+
|
|
197
|
+
class EventIterator(object):
|
|
198
|
+
"""
|
|
199
|
+
Watchdog Event iterator.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
def __init__(self, queue: asyncio.Queue):
|
|
203
|
+
self.queue = queue
|
|
204
|
+
|
|
205
|
+
def __aiter__(self):
|
|
206
|
+
return self
|
|
207
|
+
|
|
208
|
+
async def __anext__(self):
|
|
209
|
+
|
|
210
|
+
item = await self.queue.get()
|
|
211
|
+
if item is None:
|
|
212
|
+
raise StopAsyncIteration
|
|
213
|
+
return item
|
|
214
|
+
|
|
215
|
+
async def monitor_images_on_filesystem(app: FastAPI):
|
|
216
|
+
|
|
217
|
+
def watchdog(
|
|
218
|
+
path: str,
|
|
219
|
+
queue: asyncio.Queue,
|
|
220
|
+
loop: asyncio.BaseEventLoop,
|
|
221
|
+
app: FastAPI, recursive: bool = False
|
|
222
|
+
) -> None:
|
|
223
|
+
"""
|
|
224
|
+
Thread to monitor a directory for new images.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
handler = EventHandler(queue, loop)
|
|
228
|
+
observer = Observer()
|
|
229
|
+
observer.schedule(handler, str(path), recursive=recursive)
|
|
230
|
+
observer.start()
|
|
231
|
+
log.info(f"Monitoring for new images in '{path}'")
|
|
232
|
+
while True:
|
|
233
|
+
time.sleep(1)
|
|
234
|
+
# stop when the app is exiting
|
|
235
|
+
if app.state.exiting:
|
|
236
|
+
observer.stop()
|
|
237
|
+
observer.join(10)
|
|
238
|
+
log.info(f"Stopping monitoring for new images in '{path}'")
|
|
239
|
+
loop.call_soon_threadsafe(queue.put_nowait, None)
|
|
240
|
+
break
|
|
241
|
+
|
|
242
|
+
queue = asyncio.Queue()
|
|
243
|
+
loop = asyncio.get_event_loop()
|
|
244
|
+
server_config = Config.instance().settings.Server
|
|
245
|
+
image_dir = os.path.expanduser(server_config.images_path)
|
|
246
|
+
asyncio.get_event_loop().run_in_executor(None, watchdog,image_dir, queue, loop, app, True)
|
|
247
|
+
|
|
248
|
+
async for filesystem_event in EventIterator(queue):
|
|
249
|
+
# read the file system event from the queue
|
|
250
|
+
image_path = filesystem_event.src_path
|
|
251
|
+
expected_image_type = None
|
|
252
|
+
if "IOU" in image_path:
|
|
253
|
+
expected_image_type = "iou"
|
|
254
|
+
elif "QEMU" in image_path:
|
|
255
|
+
expected_image_type = "qemu"
|
|
256
|
+
elif "IOS" in image_path:
|
|
257
|
+
expected_image_type = "ios"
|
|
258
|
+
async with AsyncSession(app.state._db_engine) as db_session:
|
|
259
|
+
images_repository = ImagesRepository(db_session)
|
|
260
|
+
try:
|
|
261
|
+
image = await read_image_info(image_path, expected_image_type)
|
|
262
|
+
except InvalidImageError as e:
|
|
263
|
+
log.warning(str(e))
|
|
264
|
+
continue
|
|
265
|
+
try:
|
|
266
|
+
if await images_repository.get_image(image_path):
|
|
267
|
+
continue
|
|
268
|
+
await images_repository.add_image(**image)
|
|
269
|
+
log.info(f"Discovered image '{image_path}' has been added to the database")
|
|
270
|
+
except SQLAlchemyError as e:
|
|
271
|
+
log.warning(f"Error while adding image '{image_path}' to the database: {e}")
|
gns3server/disks/empty100G.qcow2
CHANGED
|
Binary file
|
gns3server/disks/empty200G.qcow2
CHANGED
|
Binary file
|
gns3server/disks/empty30G.qcow2
CHANGED
|
Binary file
|
gns3server/disks/empty8G.qcow2
CHANGED
|
Binary file
|
gns3server/schemas/config.py
CHANGED
|
@@ -147,7 +147,7 @@ class ServerSettings(BaseModel):
|
|
|
147
147
|
allow_remote_console: bool = False
|
|
148
148
|
enable_builtin_templates: bool = True
|
|
149
149
|
install_builtin_appliances: bool = True
|
|
150
|
-
model_config = ConfigDict(validate_assignment=True, str_strip_whitespace=True
|
|
150
|
+
model_config = ConfigDict(validate_assignment=True, str_strip_whitespace=True)
|
|
151
151
|
|
|
152
152
|
@field_validator("additional_images_paths", mode="before")
|
|
153
153
|
@classmethod
|
gns3server/server.py
CHANGED
|
@@ -267,9 +267,9 @@ class Server:
|
|
|
267
267
|
else:
|
|
268
268
|
log.info(f"Compute authentication is enabled with username '{config.Server.compute_username}'")
|
|
269
269
|
|
|
270
|
-
# we only support Python 3 version >= 3.
|
|
271
|
-
if sys.version_info < (3,
|
|
272
|
-
raise SystemExit("Python 3.
|
|
270
|
+
# we only support Python 3 version >= 3.9
|
|
271
|
+
if sys.version_info < (3, 9, 0):
|
|
272
|
+
raise SystemExit("Python 3.9 or higher is required")
|
|
273
273
|
|
|
274
274
|
log.info(
|
|
275
275
|
"Running with Python {major}.{minor}.{micro} and has PID {pid}".format(
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
# You should have received a copy of the GNU General Public License
|
|
15
15
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
from
|
|
17
|
+
from joserfc import jwt
|
|
18
|
+
from joserfc.jwk import OctKey
|
|
19
|
+
from joserfc.errors import JoseError
|
|
19
20
|
from datetime import datetime, timedelta, timezone
|
|
20
21
|
import bcrypt
|
|
21
22
|
|
|
@@ -56,7 +57,8 @@ class AuthService:
|
|
|
56
57
|
secret_key = DEFAULT_JWT_SECRET_KEY
|
|
57
58
|
log.error("A JWT secret key must be configured to secure the server, using an unsecured default key!")
|
|
58
59
|
algorithm = Config.instance().settings.Controller.jwt_algorithm
|
|
59
|
-
|
|
60
|
+
key = OctKey.import_key(secret_key)
|
|
61
|
+
encoded_jwt = jwt.encode({"alg": algorithm}, to_encode, key)
|
|
60
62
|
return encoded_jwt
|
|
61
63
|
|
|
62
64
|
def get_username_from_token(self, token: str, secret_key: str = None) -> Optional[str]:
|
|
@@ -73,11 +75,12 @@ class AuthService:
|
|
|
73
75
|
secret_key = DEFAULT_JWT_SECRET_KEY
|
|
74
76
|
log.error("A JWT secret key must be configured to secure the server, using an unsecured default key!")
|
|
75
77
|
algorithm = Config.instance().settings.Controller.jwt_algorithm
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
key = OctKey.import_key(secret_key)
|
|
79
|
+
payload = jwt.decode(token, key, algorithms=[algorithm])
|
|
80
|
+
username: str = payload.claims.get("sub")
|
|
78
81
|
if username is None:
|
|
79
82
|
raise credentials_exception
|
|
80
83
|
token_data = TokenData(username=username)
|
|
81
|
-
except (
|
|
84
|
+
except (JoseError, ValidationError, ValueError):
|
|
82
85
|
raise credentials_exception
|
|
83
86
|
return token_data.username
|
|
Binary file
|