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.

Files changed (30) hide show
  1. {gns3_server-3.0.1.dist-info → gns3_server-3.0.3.dist-info}/METADATA +22 -21
  2. {gns3_server-3.0.1.dist-info → gns3_server-3.0.3.dist-info}/RECORD +29 -27
  3. {gns3_server-3.0.1.dist-info → gns3_server-3.0.3.dist-info}/WHEEL +1 -1
  4. gns3server/api/routes/compute/iou_nodes.py +1 -1
  5. gns3server/api/routes/controller/images.py +58 -21
  6. gns3server/api/routes/controller/templates.py +23 -2
  7. gns3server/appliances/alpine-cloud.gns3a +56 -0
  8. gns3server/appliances/stormshield-eva.gns3a +50 -0
  9. gns3server/compute/docker/__init__.py +1 -1
  10. gns3server/compute/virtualbox/virtualbox_vm.py +29 -25
  11. gns3server/controller/__init__.py +27 -18
  12. gns3server/controller/appliance_manager.py +2 -2
  13. gns3server/controller/compute.py +7 -2
  14. gns3server/crash_report.py +1 -1
  15. gns3server/db/repositories/images.py +22 -3
  16. gns3server/db/repositories/templates.py +11 -0
  17. gns3server/db/tasks.py +120 -79
  18. gns3server/main.py +40 -2
  19. gns3server/server.py +26 -41
  20. gns3server/services/authentication.py +9 -6
  21. gns3server/static/web-ui/index.html +1 -1
  22. gns3server/static/web-ui/main.2e807eb4bc32f838.js +1 -0
  23. gns3server/utils/asyncio/__init__.py +4 -12
  24. gns3server/utils/asyncio/pool.py +1 -4
  25. gns3server/utils/images.py +17 -4
  26. gns3server/version.py +2 -2
  27. gns3server/static/web-ui/main.e55eeff5c0ba1cf4.js +0 -1
  28. {gns3_server-3.0.1.dist-info → gns3_server-3.0.3.dist-info}/LICENSE +0 -0
  29. {gns3_server-3.0.1.dist-info → gns3_server-3.0.3.dist-info}/entry_points.txt +0 -0
  30. {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
- def should_copy(src, dst, upgrade_resources):
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
- return md5sum(src) != md5sum(dst)
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(dst_path, entry.name))
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
 
@@ -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 async_timeout.timeout(delay=timeout):
511
+ async with asynctimeout(delay=timeout):
507
512
  url = self._getUrl(path)
508
513
  headers = {"content-type": "application/json"}
509
514
  chunked = None
@@ -58,7 +58,7 @@ class CrashReport:
58
58
  Report crash to a third party service
59
59
  """
60
60
 
61
- DSN = "https://847198b87dbd50ef8962901641918a08@o19455.ingest.us.sentry.io/38482"
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.filename):
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 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/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
- asyncio.run(Server().run())
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