gns3-server 3.0.1__py3-none-any.whl → 3.0.3__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.3.dist-info}/METADATA +22 -21
- {gns3_server-3.0.1.dist-info → gns3_server-3.0.3.dist-info}/RECORD +29 -27
- {gns3_server-3.0.1.dist-info → gns3_server-3.0.3.dist-info}/WHEEL +1 -1
- gns3server/api/routes/compute/iou_nodes.py +1 -1
- gns3server/api/routes/controller/images.py +58 -21
- gns3server/api/routes/controller/templates.py +23 -2
- gns3server/appliances/alpine-cloud.gns3a +56 -0
- gns3server/appliances/stormshield-eva.gns3a +50 -0
- gns3server/compute/docker/__init__.py +1 -1
- gns3server/compute/virtualbox/virtualbox_vm.py +29 -25
- 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/main.py +40 -2
- gns3server/server.py +26 -41
- gns3server/services/authentication.py +9 -6
- gns3server/static/web-ui/index.html +1 -1
- gns3server/static/web-ui/main.2e807eb4bc32f838.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.3.dist-info}/LICENSE +0 -0
- {gns3_server-3.0.1.dist-info → gns3_server-3.0.3.dist-info}/entry_points.txt +0 -0
- {gns3_server-3.0.1.dist-info → gns3_server-3.0.3.dist-info}/top_level.txt +0 -0
|
@@ -28,10 +28,10 @@ try:
|
|
|
28
28
|
except ImportError:
|
|
29
29
|
from importlib import resources as importlib_resources
|
|
30
30
|
|
|
31
|
-
|
|
32
31
|
from ..config import Config
|
|
33
32
|
from ..utils import parse_version, md5sum
|
|
34
33
|
from ..utils.images import default_images_directory
|
|
34
|
+
from ..utils.asyncio import wait_run_in_executor
|
|
35
35
|
|
|
36
36
|
from .project import Project
|
|
37
37
|
from .appliance import Appliance
|
|
@@ -43,6 +43,7 @@ from .topology import load_topology
|
|
|
43
43
|
from .gns3vm import GNS3VM
|
|
44
44
|
from .gns3vm.gns3_vm_error import GNS3VMError
|
|
45
45
|
from .controller_error import ControllerError, ControllerNotFoundError
|
|
46
|
+
from ..db.tasks import update_disk_checksums
|
|
46
47
|
from ..version import __version__
|
|
47
48
|
|
|
48
49
|
import logging
|
|
@@ -72,8 +73,11 @@ class Controller:
|
|
|
72
73
|
async def start(self, computes=None):
|
|
73
74
|
|
|
74
75
|
log.info("Controller is starting")
|
|
75
|
-
self._install_base_configs()
|
|
76
|
-
self._install_builtin_disks()
|
|
76
|
+
await self._install_base_configs()
|
|
77
|
+
installed_disks = await self._install_builtin_disks()
|
|
78
|
+
if installed_disks:
|
|
79
|
+
await update_disk_checksums(installed_disks)
|
|
80
|
+
|
|
77
81
|
server_config = Config.instance().settings.Server
|
|
78
82
|
Config.instance().listen_for_config_changes(self._update_config)
|
|
79
83
|
name = server_config.name
|
|
@@ -86,7 +90,7 @@ class Controller:
|
|
|
86
90
|
if host == "0.0.0.0":
|
|
87
91
|
host = "127.0.0.1"
|
|
88
92
|
|
|
89
|
-
self._load_controller_vars()
|
|
93
|
+
await self._load_controller_vars()
|
|
90
94
|
|
|
91
95
|
if server_config.enable_ssl:
|
|
92
96
|
self._ssl_context = self._create_ssl_context(server_config)
|
|
@@ -190,7 +194,7 @@ class Controller:
|
|
|
190
194
|
async def reload(self):
|
|
191
195
|
|
|
192
196
|
log.info("Controller is reloading")
|
|
193
|
-
self._load_controller_vars()
|
|
197
|
+
await self._load_controller_vars()
|
|
194
198
|
|
|
195
199
|
# remove all projects deleted from disk.
|
|
196
200
|
for project in self._projects.copy().values():
|
|
@@ -234,7 +238,7 @@ class Controller:
|
|
|
234
238
|
except OSError as e:
|
|
235
239
|
log.error(f"Cannot write controller vars file '{self._vars_file}': {e}")
|
|
236
240
|
|
|
237
|
-
def _load_controller_vars(self):
|
|
241
|
+
async def _load_controller_vars(self):
|
|
238
242
|
"""
|
|
239
243
|
Reload the controller vars from disk
|
|
240
244
|
"""
|
|
@@ -274,9 +278,9 @@ class Controller:
|
|
|
274
278
|
builtin_appliances_path = self._appliance_manager.builtin_appliances_path()
|
|
275
279
|
if not previous_version or \
|
|
276
280
|
parse_version(__version__.split("+")[0]) > parse_version(previous_version.split("+")[0]):
|
|
277
|
-
self._appliance_manager.install_builtin_appliances()
|
|
281
|
+
await self._appliance_manager.install_builtin_appliances()
|
|
278
282
|
elif not os.listdir(builtin_appliances_path):
|
|
279
|
-
self._appliance_manager.install_builtin_appliances()
|
|
283
|
+
await self._appliance_manager.install_builtin_appliances()
|
|
280
284
|
else:
|
|
281
285
|
log.info(f"Built-in appliances are installed in '{builtin_appliances_path}'")
|
|
282
286
|
|
|
@@ -307,18 +311,21 @@ class Controller:
|
|
|
307
311
|
|
|
308
312
|
|
|
309
313
|
@staticmethod
|
|
310
|
-
def install_resource_files(dst_path, resource_name, upgrade_resources=True):
|
|
314
|
+
async def install_resource_files(dst_path, resource_name, upgrade_resources=True):
|
|
311
315
|
"""
|
|
312
316
|
Install files from resources to user's file system
|
|
313
317
|
"""
|
|
314
318
|
|
|
315
|
-
|
|
319
|
+
installed_resources = []
|
|
320
|
+
async def should_copy(src, dst, upgrade_resources):
|
|
316
321
|
if not os.path.exists(dst):
|
|
317
322
|
return True
|
|
318
323
|
if upgrade_resources is False:
|
|
319
324
|
return False
|
|
320
325
|
# copy the resource if it is different
|
|
321
|
-
|
|
326
|
+
src_md5 = await wait_run_in_executor(md5sum, src)
|
|
327
|
+
dst_md5 = await wait_run_in_executor(md5sum, dst)
|
|
328
|
+
return src_md5 != dst_md5
|
|
322
329
|
|
|
323
330
|
if hasattr(sys, "frozen") and sys.platform.startswith("win"):
|
|
324
331
|
resource_path = os.path.normpath(os.path.join(os.path.dirname(sys.executable), resource_name))
|
|
@@ -328,14 +335,16 @@ class Controller:
|
|
|
328
335
|
else:
|
|
329
336
|
for entry in importlib_resources.files('gns3server').joinpath(resource_name).iterdir():
|
|
330
337
|
full_path = os.path.join(dst_path, entry.name)
|
|
331
|
-
if entry.is_file() and should_copy(str(entry), full_path, upgrade_resources):
|
|
338
|
+
if entry.is_file() and await should_copy(str(entry), full_path, upgrade_resources):
|
|
332
339
|
log.debug(f'Installing {resource_name} resource file "{entry.name}" to "{full_path}"')
|
|
333
|
-
shutil.copy(str(entry), os.path.join(
|
|
340
|
+
shutil.copy(str(entry), os.path.join(full_path))
|
|
341
|
+
installed_resources.append(full_path)
|
|
334
342
|
elif entry.is_dir():
|
|
335
343
|
os.makedirs(full_path, exist_ok=True)
|
|
336
|
-
Controller.install_resource_files(full_path, os.path.join(resource_name, entry.name))
|
|
344
|
+
await Controller.install_resource_files(full_path, os.path.join(resource_name, entry.name))
|
|
345
|
+
return installed_resources
|
|
337
346
|
|
|
338
|
-
def _install_base_configs(self):
|
|
347
|
+
async def _install_base_configs(self):
|
|
339
348
|
"""
|
|
340
349
|
At startup we copy base configs to the user location to allow
|
|
341
350
|
them to customize it
|
|
@@ -345,11 +354,11 @@ class Controller:
|
|
|
345
354
|
log.info(f"Installing base configs in '{dst_path}'")
|
|
346
355
|
try:
|
|
347
356
|
# do not overwrite base configs because they may have been customized by the user
|
|
348
|
-
Controller.install_resource_files(dst_path, "configs", upgrade_resources=False)
|
|
357
|
+
await Controller.install_resource_files(dst_path, "configs", upgrade_resources=False)
|
|
349
358
|
except OSError as e:
|
|
350
359
|
log.error(f"Could not install base config files to {dst_path}: {e}")
|
|
351
360
|
|
|
352
|
-
def _install_builtin_disks(self):
|
|
361
|
+
async def _install_builtin_disks(self):
|
|
353
362
|
"""
|
|
354
363
|
At startup we copy built-in Qemu disks to the user location to allow
|
|
355
364
|
them to use with appliances
|
|
@@ -358,7 +367,7 @@ class Controller:
|
|
|
358
367
|
dst_path = self.disks_path()
|
|
359
368
|
log.info(f"Installing built-in disks in '{dst_path}'")
|
|
360
369
|
try:
|
|
361
|
-
Controller.install_resource_files(dst_path, "disks")
|
|
370
|
+
return await Controller.install_resource_files(dst_path, "disks")
|
|
362
371
|
except OSError as e:
|
|
363
372
|
log.error(f"Could not install disk files to {dst_path}: {e}")
|
|
364
373
|
|
|
@@ -110,7 +110,7 @@ class ApplianceManager:
|
|
|
110
110
|
os.makedirs(appliances_dir, exist_ok=True)
|
|
111
111
|
return appliances_dir
|
|
112
112
|
|
|
113
|
-
def install_builtin_appliances(self):
|
|
113
|
+
async def install_builtin_appliances(self):
|
|
114
114
|
"""
|
|
115
115
|
At startup we copy the built-in appliances files.
|
|
116
116
|
"""
|
|
@@ -119,7 +119,7 @@ class ApplianceManager:
|
|
|
119
119
|
log.info(f"Installing built-in appliances in '{dst_path}'")
|
|
120
120
|
from . import Controller
|
|
121
121
|
try:
|
|
122
|
-
Controller.instance().install_resource_files(dst_path, "appliances")
|
|
122
|
+
await Controller.instance().install_resource_files(dst_path, "appliances")
|
|
123
123
|
except OSError as e:
|
|
124
124
|
log.error(f"Could not install built-in appliance files to {dst_path}: {e}")
|
|
125
125
|
|
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 (
|
|
@@ -503,7 +508,7 @@ class Compute:
|
|
|
503
508
|
return self._getUrl(path)
|
|
504
509
|
|
|
505
510
|
async def _run_http_query(self, method, path, data=None, timeout=120, raw=False):
|
|
506
|
-
async with
|
|
511
|
+
async with asynctimeout(delay=timeout):
|
|
507
512
|
url = self._getUrl(path)
|
|
508
513
|
headers = {"content-type": "application/json"}
|
|
509
514
|
chunked = None
|
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://2c96fa0280f82c48108f122b87cd902c@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
|
|
@@ -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/main.py
CHANGED
|
@@ -30,6 +30,7 @@ import gns3server.utils.get_resource
|
|
|
30
30
|
import os
|
|
31
31
|
import sys
|
|
32
32
|
import asyncio
|
|
33
|
+
import argparse
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
def daemonize():
|
|
@@ -59,6 +60,42 @@ def daemonize():
|
|
|
59
60
|
print("Second fork failed: %d (%s)\n" % (e.errno, e.strerror), file=sys.stderr)
|
|
60
61
|
sys.exit(1)
|
|
61
62
|
|
|
63
|
+
def parse_arguments(argv):
|
|
64
|
+
"""
|
|
65
|
+
Parse command line arguments
|
|
66
|
+
|
|
67
|
+
:param argv: Array of command line arguments
|
|
68
|
+
"""
|
|
69
|
+
from gns3server.version import __version__
|
|
70
|
+
parser = argparse.ArgumentParser(description=f"GNS3 server version {__version__}")
|
|
71
|
+
parser.add_argument("-v", "--version", help="show the version", action="version", version=__version__)
|
|
72
|
+
parser.add_argument("--host", help="run on the given host/IP address")
|
|
73
|
+
parser.add_argument("--port", help="run on the given port", type=int)
|
|
74
|
+
parser.add_argument("--ssl", action="store_true", help="run in SSL mode")
|
|
75
|
+
parser.add_argument("--config", help="Configuration file")
|
|
76
|
+
parser.add_argument("--certfile", help="SSL cert file")
|
|
77
|
+
parser.add_argument("--certkey", help="SSL key file")
|
|
78
|
+
parser.add_argument("-L", "--local", action="store_true", help="local mode (allows some insecure operations)")
|
|
79
|
+
parser.add_argument(
|
|
80
|
+
"-A", "--allow", action="store_true", help="allow remote connections to local console ports"
|
|
81
|
+
)
|
|
82
|
+
parser.add_argument("-q", "--quiet", default=False, action="store_true", help="do not show logs on stdout")
|
|
83
|
+
parser.add_argument("-d", "--debug", default=False, action="store_true", help="show debug logs")
|
|
84
|
+
parser.add_argument("--logfile", "--log", help="send output to logfile instead of console")
|
|
85
|
+
parser.add_argument("--logmaxsize", default=10000000, help="maximum logfile size in bytes (default is 10MB)")
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"--logbackupcount", default=10, help="number of historical log files to keep (default is 10)"
|
|
88
|
+
)
|
|
89
|
+
parser.add_argument(
|
|
90
|
+
"--logcompression", default=False, action="store_true", help="compress inactive (historical) logs"
|
|
91
|
+
)
|
|
92
|
+
parser.add_argument("--daemon", action="store_true", help="start as a daemon")
|
|
93
|
+
parser.add_argument("--pid", help="store process pid")
|
|
94
|
+
parser.add_argument("--profile", help="Settings profile (blank will use default settings files)")
|
|
95
|
+
|
|
96
|
+
args = parser.parse_args(argv)
|
|
97
|
+
return parser, args
|
|
98
|
+
|
|
62
99
|
|
|
63
100
|
def main():
|
|
64
101
|
"""
|
|
@@ -69,10 +106,11 @@ def main():
|
|
|
69
106
|
raise SystemExit("Windows is not a supported platform to run the GNS3 server")
|
|
70
107
|
if "--daemon" in sys.argv:
|
|
71
108
|
daemonize()
|
|
72
|
-
from gns3server.server import Server
|
|
73
109
|
|
|
74
110
|
try:
|
|
75
|
-
|
|
111
|
+
parser, args = parse_arguments(sys.argv[1:])
|
|
112
|
+
from gns3server.server import Server
|
|
113
|
+
asyncio.run(Server().run(parser, args))
|
|
76
114
|
except KeyboardInterrupt:
|
|
77
115
|
pass
|
|
78
116
|
|