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.

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 signal
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 discover_images, check_valid_image_header, read_image_info, default_images_directory, InvalidImageError
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 image_filter(change: Change, path: str) -> bool:
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.8
271
- if sys.version_info < (3, 8, 0):
272
- raise SystemExit("Python 3.8 or higher is required")
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 jose import JWTError, jwt
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
- encoded_jwt = jwt.encode(to_encode, secret_key, algorithm=algorithm)
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
- payload = jwt.decode(token, secret_key, algorithms=[algorithm])
77
- username: str = payload.get("sub")
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 (JWTError, ValidationError):
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.e55eeff5c0ba1cf4.js" type="module"></script>
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>