gns3-server 3.0.1__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.1.dist-info → gns3_server-3.0.2.dist-info}/METADATA +17 -16
- {gns3_server-3.0.1.dist-info → gns3_server-3.0.2.dist-info}/RECORD +24 -24
- gns3server/api/routes/controller/images.py +58 -21
- gns3server/api/routes/controller/templates.py +23 -2
- gns3server/compute/docker/__init__.py +1 -1
- gns3server/controller/__init__.py +27 -18
- gns3server/controller/appliance_manager.py +2 -2
- gns3server/controller/compute.py +7 -2
- gns3server/crash_report.py +1 -1
- gns3server/db/repositories/images.py +22 -3
- gns3server/db/repositories/templates.py +11 -0
- gns3server/db/tasks.py +120 -79
- gns3server/server.py +3 -3
- gns3server/services/authentication.py +9 -6
- 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 +17 -4
- gns3server/version.py +2 -2
- gns3server/static/web-ui/main.e55eeff5c0ba1cf4.js +0 -1
- {gns3_server-3.0.1.dist-info → gns3_server-3.0.2.dist-info}/LICENSE +0 -0
- {gns3_server-3.0.1.dist-info → gns3_server-3.0.2.dist-info}/WHEEL +0 -0
- {gns3_server-3.0.1.dist-info → gns3_server-3.0.2.dist-info}/entry_points.txt +0 -0
- {gns3_server-3.0.1.dist-info → gns3_server-3.0.2.dist-info}/top_level.txt +0 -0
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/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
|
|
@@ -46,6 +46,6 @@
|
|
|
46
46
|
|
|
47
47
|
gtag('config', 'G-0BT7QQV1W1');
|
|
48
48
|
</script>
|
|
49
|
-
<script src="runtime.24fa95b7061d7056.js" type="module"></script><script src="polyfills.319c79dd175e50d0.js" type="module"></script><script src="main.
|
|
49
|
+
<script src="runtime.24fa95b7061d7056.js" type="module"></script><script src="polyfills.319c79dd175e50d0.js" type="module"></script><script src="main.62c99707e4709a56.js" type="module"></script>
|
|
50
50
|
|
|
51
51
|
</body></html>
|