gns3-server 3.0.0rc1__py3-none-any.whl → 3.0.1__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 (89) hide show
  1. {gns3_server-3.0.0rc1.dist-info → gns3_server-3.0.1.dist-info}/METADATA +20 -19
  2. {gns3_server-3.0.0rc1.dist-info → gns3_server-3.0.1.dist-info}/RECORD +89 -79
  3. {gns3_server-3.0.0rc1.dist-info → gns3_server-3.0.1.dist-info}/WHEEL +1 -1
  4. gns3server/api/routes/compute/cloud_nodes.py +1 -1
  5. gns3server/api/routes/compute/docker_nodes.py +3 -0
  6. gns3server/api/routes/compute/nat_nodes.py +1 -1
  7. gns3server/api/routes/compute/vmware_nodes.py +1 -1
  8. gns3server/api/routes/compute/vpcs_nodes.py +10 -4
  9. gns3server/api/routes/controller/projects.py +29 -3
  10. gns3server/api/routes/controller/users.py +2 -2
  11. gns3server/api/routes/index.py +3 -3
  12. gns3server/api/server.py +38 -3
  13. gns3server/appliances/almalinux.gns3a +6 -6
  14. gns3server/appliances/arista-veos.gns3a +20 -514
  15. gns3server/appliances/cisco-7200.gns3a +26 -0
  16. gns3server/appliances/cisco-asav.gns3a +14 -1
  17. gns3server/appliances/cisco-csr1000v.gns3a +28 -2
  18. gns3server/appliances/cisco-iou-l2.gns3a +16 -4
  19. gns3server/appliances/cisco-iou-l3.gns3a +16 -4
  20. gns3server/appliances/cisco-vWLC.gns3a +29 -1
  21. gns3server/appliances/fortigate.gns3a +3 -3
  22. gns3server/appliances/hbcd-pe.gns3a +62 -0
  23. gns3server/appliances/innovaphone-app.gns3a +50 -0
  24. gns3server/appliances/innovaphone-ipva.gns3a +78 -0
  25. gns3server/appliances/mikrotik-chr.gns3a +30 -99
  26. gns3server/appliances/nixos.gns3a +52 -0
  27. gns3server/appliances/opnsense.gns3a +13 -0
  28. gns3server/appliances/pfsense.gns3a +14 -0
  29. gns3server/appliances/reactos.gns3a +10 -10
  30. gns3server/appliances/truenas.gns3a +104 -0
  31. gns3server/appliances/ubuntu-cloud.gns3a +35 -20
  32. gns3server/appliances/ubuntu-gui.gns3a +13 -0
  33. gns3server/appliances/viptela-edge-genericx86-64.gns3a +28 -2
  34. gns3server/appliances/viptela-smart-genericx86-64.gns3a +27 -1
  35. gns3server/appliances/viptela-vmanage-genericx86-64.gns3a +32 -4
  36. gns3server/appliances/vyos.gns3a +95 -98
  37. gns3server/compute/base_node.py +1 -0
  38. gns3server/compute/docker/docker_vm.py +56 -2
  39. gns3server/compute/docker/resources/init.sh +5 -2
  40. gns3server/compute/dynamips/__init__.py +0 -4
  41. gns3server/compute/dynamips/nodes/router.py +20 -0
  42. gns3server/compute/iou/iou_vm.py +22 -12
  43. gns3server/compute/notification_manager.py +2 -2
  44. gns3server/compute/qemu/qemu_vm.py +0 -5
  45. gns3server/controller/__init__.py +35 -25
  46. gns3server/controller/appliance_manager.py +2 -4
  47. gns3server/controller/compute.py +1 -1
  48. gns3server/controller/export_project.py +18 -14
  49. gns3server/controller/import_project.py +21 -0
  50. gns3server/controller/node.py +10 -8
  51. gns3server/controller/notification.py +4 -4
  52. gns3server/controller/project.py +88 -5
  53. gns3server/controller/symbols.py +1 -1
  54. gns3server/controller/topology.py +1 -1
  55. gns3server/crash_report.py +1 -1
  56. gns3server/db/models/templates.py +1 -0
  57. gns3server/db/repositories/pools.py +1 -1
  58. gns3server/db/tasks.py +1 -1
  59. gns3server/db_migrations/versions/9a5292aa4389_add_mac_address_field_in_docker_.py +27 -0
  60. gns3server/disks/empty100G.qcow2 +0 -0
  61. gns3server/disks/empty200G.qcow2 +0 -0
  62. gns3server/disks/empty30G.qcow2 +0 -0
  63. gns3server/disks/empty8G.qcow2 +0 -0
  64. gns3server/schemas/compute/docker_nodes.py +1 -0
  65. gns3server/schemas/compute/ethernet_switch_nodes.py +1 -1
  66. gns3server/schemas/config.py +1 -1
  67. gns3server/schemas/controller/templates/cloud_templates.py +2 -2
  68. gns3server/schemas/controller/templates/docker_templates.py +4 -3
  69. gns3server/schemas/controller/templates/dynamips_templates.py +5 -5
  70. gns3server/schemas/controller/templates/ethernet_hub_templates.py +1 -1
  71. gns3server/schemas/controller/templates/ethernet_switch_templates.py +2 -2
  72. gns3server/schemas/controller/templates/iou_templates.py +2 -2
  73. gns3server/schemas/controller/templates/qemu_templates.py +12 -12
  74. gns3server/schemas/controller/templates/virtualbox_templates.py +4 -5
  75. gns3server/schemas/controller/templates/vmware_templates.py +4 -4
  76. gns3server/schemas/controller/templates/vpcs_templates.py +2 -2
  77. gns3server/static/favicon.ico +0 -0
  78. gns3server/static/redoc.standalone.js +1782 -0
  79. gns3server/static/swagger-ui-bundle.js +2 -0
  80. gns3server/static/swagger-ui.css +3 -0
  81. gns3server/static/web-ui/index.html +1 -1
  82. gns3server/static/web-ui/{main.4185a8e61824af0d.js → main.e55eeff5c0ba1cf4.js} +1 -1
  83. gns3server/utils/__init__.py +12 -0
  84. gns3server/utils/asyncio/aiozipstream.py +15 -11
  85. gns3server/utils/images.py +45 -35
  86. gns3server/version.py +2 -2
  87. {gns3_server-3.0.0rc1.dist-info → gns3_server-3.0.1.dist-info}/LICENSE +0 -0
  88. {gns3_server-3.0.0rc1.dist-info → gns3_server-3.0.1.dist-info}/entry_points.txt +0 -0
  89. {gns3_server-3.0.0rc1.dist-info → gns3_server-3.0.1.dist-info}/top_level.txt +0 -0
@@ -30,7 +30,7 @@ except ImportError:
30
30
 
31
31
 
32
32
  from ..config import Config
33
- from ..utils import parse_version
33
+ from ..utils import parse_version, md5sum
34
34
  from ..utils.images import default_images_directory
35
35
 
36
36
  from .project import Project
@@ -64,7 +64,7 @@ class Controller:
64
64
  self.gns3vm = GNS3VM(self)
65
65
  self.symbols = Symbols()
66
66
  self._appliance_manager = ApplianceManager()
67
- self._iou_license_settings = {"iourc_content": "", "license_check": True}
67
+ self._iou_license_settings = {"iourc_content": "", "license_check": False}
68
68
  self._vars_loaded = False
69
69
  self._vars_file = Config.instance().controller_vars
70
70
  log.info(f'Loading controller vars file "{self._vars_file}"')
@@ -91,7 +91,7 @@ class Controller:
91
91
  if server_config.enable_ssl:
92
92
  self._ssl_context = self._create_ssl_context(server_config)
93
93
 
94
- protocol = server_config.protocol
94
+ protocol = server_config.protocol.value
95
95
  if self._ssl_context and protocol != "https":
96
96
  log.warning(f"Protocol changed to 'https' for local compute because SSL is enabled")
97
97
  protocol = "https"
@@ -134,7 +134,9 @@ class Controller:
134
134
  log.warning(str(e))
135
135
 
136
136
  await self.load_projects()
137
- await self._project_auto_open()
137
+
138
+ # start to auto open projects (if configured) 5 seconds after the controller has started
139
+ asyncio.get_event_loop().call_later(5, asyncio.create_task, self._project_auto_open())
138
140
 
139
141
  def _create_ssl_context(self, server_config):
140
142
 
@@ -208,19 +210,15 @@ class Controller:
208
210
  if self._vars_loaded:
209
211
  controller_vars = {
210
212
  "appliances_etag": self._appliance_manager.appliances_etag,
213
+ "iou_license_check": self._iou_license_settings["license_check"],
211
214
  "version": __version__
212
215
  }
213
216
 
214
217
  if self._iou_license_settings["iourc_content"]:
215
218
 
216
- iou_config = Config.instance().settings.IOU
217
219
  server_config = Config.instance().settings.Server
218
-
219
- if iou_config.iourc_path:
220
- iourc_path = iou_config.iourc_path
221
- else:
222
- os.makedirs(server_config.secrets_dir, exist_ok=True)
223
- iourc_path = os.path.join(server_config.secrets_dir, "gns3_iourc_license")
220
+ os.makedirs(server_config.secrets_dir, exist_ok=True)
221
+ iourc_path = os.path.join(server_config.secrets_dir, "iou_license")
224
222
 
225
223
  try:
226
224
  with open(iourc_path, "w+") as f:
@@ -251,15 +249,11 @@ class Controller:
251
249
  return []
252
250
 
253
251
  # load the IOU license settings
254
- iou_config = Config.instance().settings.IOU
255
252
  server_config = Config.instance().settings.Server
256
253
 
257
- if iou_config.iourc_path:
258
- iourc_path = iou_config.iourc_path
259
- else:
260
- if not server_config.secrets_dir:
261
- server_config.secrets_dir = os.path.dirname(Config.instance().server_config)
262
- iourc_path = os.path.join(server_config.secrets_dir, "gns3_iourc_license")
254
+ if not server_config.secrets_dir:
255
+ server_config.secrets_dir = os.path.dirname(Config.instance().server_config)
256
+ iourc_path = os.path.join(server_config.secrets_dir, "iou_license")
263
257
 
264
258
  if os.path.exists(iourc_path):
265
259
  try:
@@ -268,7 +262,10 @@ class Controller:
268
262
  log.info(f"iourc file '{iourc_path}' loaded")
269
263
  except OSError as e:
270
264
  log.error(f"Cannot read IOU license file '{iourc_path}': {e}")
271
- self._iou_license_settings["license_check"] = iou_config.license_check
265
+
266
+ # IOU license check is disabled by default
267
+ self._iou_license_settings["license_check"] = controller_vars.get("iou_license_check", False)
268
+ log.info("IOU license check is {} on the controller".format("enabled" if self._iou_license_settings["license_check"] else "disabled"))
272
269
 
273
270
  # install the built-in appliances if needed
274
271
  if Config.instance().settings.Server.install_builtin_appliances:
@@ -308,12 +305,21 @@ class Controller:
308
305
  except OSError as e:
309
306
  log.error(str(e))
310
307
 
308
+
311
309
  @staticmethod
312
- def install_resource_files(dst_path, resource_name):
310
+ def install_resource_files(dst_path, resource_name, upgrade_resources=True):
313
311
  """
314
312
  Install files from resources to user's file system
315
313
  """
316
314
 
315
+ def should_copy(src, dst, upgrade_resources):
316
+ if not os.path.exists(dst):
317
+ return True
318
+ if upgrade_resources is False:
319
+ return False
320
+ # copy the resource if it is different
321
+ return md5sum(src) != md5sum(dst)
322
+
317
323
  if hasattr(sys, "frozen") and sys.platform.startswith("win"):
318
324
  resource_path = os.path.normpath(os.path.join(os.path.dirname(sys.executable), resource_name))
319
325
  for filename in os.listdir(resource_path):
@@ -322,7 +328,7 @@ class Controller:
322
328
  else:
323
329
  for entry in importlib_resources.files('gns3server').joinpath(resource_name).iterdir():
324
330
  full_path = os.path.join(dst_path, entry.name)
325
- if entry.is_file() and not os.path.exists(full_path):
331
+ if entry.is_file() and should_copy(str(entry), full_path, upgrade_resources):
326
332
  log.debug(f'Installing {resource_name} resource file "{entry.name}" to "{full_path}"')
327
333
  shutil.copy(str(entry), os.path.join(dst_path, entry.name))
328
334
  elif entry.is_dir():
@@ -338,7 +344,8 @@ class Controller:
338
344
  dst_path = self.configs_path()
339
345
  log.info(f"Installing base configs in '{dst_path}'")
340
346
  try:
341
- Controller.install_resource_files(dst_path, "configs")
347
+ # 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)
342
349
  except OSError as e:
343
350
  log.error(f"Could not install base config files to {dst_path}: {e}")
344
351
 
@@ -591,9 +598,12 @@ class Controller:
591
598
  Auto open the project with auto open enable
592
599
  """
593
600
 
594
- for project in self._projects.values():
595
- if project.auto_open:
596
- await project.open()
601
+ try:
602
+ for project in self._projects.values():
603
+ if project.auto_open:
604
+ await project.open()
605
+ except ControllerError as e:
606
+ log.error(f"Could not auto open projects: {e}")
597
607
 
598
608
  def get_free_project_name(self, base_name):
599
609
  """
@@ -95,7 +95,7 @@ class ApplianceManager:
95
95
  os.makedirs(appliances_path, exist_ok=True)
96
96
  return appliances_path
97
97
 
98
- def builtin_appliances_path(self, delete_first=False):
98
+ def builtin_appliances_path(self):
99
99
  """
100
100
  Get the built-in appliance storage directory
101
101
  """
@@ -107,8 +107,6 @@ class ApplianceManager:
107
107
  else:
108
108
  resources_path = os.path.expanduser(resources_path)
109
109
  appliances_dir = os.path.join(resources_path, "appliances")
110
- if delete_first:
111
- shutil.rmtree(appliances_dir, ignore_errors=True)
112
110
  os.makedirs(appliances_dir, exist_ok=True)
113
111
  return appliances_dir
114
112
 
@@ -117,7 +115,7 @@ class ApplianceManager:
117
115
  At startup we copy the built-in appliances files.
118
116
  """
119
117
 
120
- dst_path = self.builtin_appliances_path(delete_first=True)
118
+ dst_path = self.builtin_appliances_path()
121
119
  log.info(f"Installing built-in appliances in '{dst_path}'")
122
120
  from . import Controller
123
121
  try:
@@ -502,7 +502,7 @@ class Compute:
502
502
  """ Returns URL for specific path at Compute"""
503
503
  return self._getUrl(path)
504
504
 
505
- async def _run_http_query(self, method, path, data=None, timeout=20, raw=False):
505
+ async def _run_http_query(self, method, path, data=None, timeout=120, raw=False):
506
506
  async with async_timeout.timeout(delay=timeout):
507
507
  url = self._getUrl(path)
508
508
  headers = {"content-type": "application/json"}
@@ -16,6 +16,7 @@
16
16
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
17
 
18
18
  import os
19
+ import sys
19
20
  import json
20
21
  import asyncio
21
22
  import aiofiles
@@ -89,14 +90,15 @@ async def export_project(
89
90
  files = [f for f in files if _is_exportable(os.path.join(root, f), include_snapshots)]
90
91
  for file in files:
91
92
  path = os.path.join(root, file)
92
- # check if we can export the file
93
- try:
94
- open(path).close()
95
- except OSError as e:
96
- msg = f"Could not export file {path}: {e}"
97
- log.warning(msg)
98
- project.emit_notification("log.warning", {"message": msg})
99
- continue
93
+ if not os.path.islink(path):
94
+ try:
95
+ # check if we can export the file
96
+ open(path).close()
97
+ except OSError as e:
98
+ msg = f"Could not export file {path}: {e}"
99
+ log.warning(msg)
100
+ project.emit_notification("log.warning", {"message": msg})
101
+ continue
100
102
  # ignore the .gns3 file
101
103
  if file.endswith(".gns3"):
102
104
  continue
@@ -150,7 +152,10 @@ def _patch_mtime(path):
150
152
  :param path: file path
151
153
  """
152
154
 
153
- st = os.stat(path)
155
+ if sys.platform.startswith("win"):
156
+ # only UNIX type platforms
157
+ return
158
+ st = os.stat(path, follow_symlinks=False)
154
159
  file_date = datetime.fromtimestamp(st.st_mtime)
155
160
  if file_date.year < 1980:
156
161
  new_mtime = file_date.replace(year=1980).timestamp()
@@ -166,10 +171,6 @@ def _is_exportable(path, include_snapshots=False):
166
171
  if include_snapshots is False and path.endswith("snapshots"):
167
172
  return False
168
173
 
169
- # do not export symlinks
170
- if os.path.islink(path):
171
- return False
172
-
173
174
  # do not export directories of snapshots
174
175
  if include_snapshots is False and "{sep}snapshots{sep}".format(sep=os.path.sep) in path:
175
176
  return False
@@ -228,13 +229,16 @@ async def _patch_project_file(
228
229
  if not keep_compute_ids:
229
230
  node["compute_id"] = "local" # To make project portable all node by default run on local
230
231
 
231
- if "properties" in node and node["node_type"] != "docker":
232
+ if "properties" in node:
232
233
  for prop, value in node["properties"].items():
233
234
 
234
235
  # reset the MAC address
235
236
  if reset_mac_addresses and prop in ("mac_addr", "mac_address"):
236
237
  node["properties"][prop] = None
237
238
 
239
+ if node["node_type"] == "docker":
240
+ continue
241
+
238
242
  if node["node_type"] == "iou":
239
243
  if not prop == "path":
240
244
  continue
@@ -17,6 +17,7 @@
17
17
 
18
18
  import os
19
19
  import sys
20
+ import stat
20
21
  import json
21
22
  import uuid
22
23
  import shutil
@@ -95,6 +96,7 @@ async def import_project(controller, project_id, stream, location=None, name=Non
95
96
  try:
96
97
  with zipfile_zstd.ZipFile(stream) as zip_file:
97
98
  await wait_run_in_executor(zip_file.extractall, path)
99
+ _create_symbolic_links(zip_file, path)
98
100
  except zipfile_zstd.BadZipFile:
99
101
  raise ControllerError("Cannot extract files from GNS3 project (invalid zip)")
100
102
 
@@ -184,6 +186,24 @@ async def import_project(controller, project_id, stream, location=None, name=Non
184
186
  project = await controller.load_project(dot_gns3_path, load=False)
185
187
  return project
186
188
 
189
+ def _create_symbolic_links(zip_file, path):
190
+ """
191
+ Manually create symbolic links (if any) because ZipFile does not support it.
192
+
193
+ :param zip_file: ZipFile instance
194
+ :param path: project location
195
+ """
196
+
197
+ for zip_info in zip_file.infolist():
198
+ if stat.S_ISLNK(zip_info.external_attr >> 16):
199
+ symlink_target = zip_file.read(zip_info.filename).decode()
200
+ symlink_path = os.path.join(path, zip_info.filename)
201
+ try:
202
+ # remove the regular file and replace it by a symbolic link
203
+ os.remove(symlink_path)
204
+ os.symlink(symlink_target, symlink_path)
205
+ except OSError as e:
206
+ raise ControllerError(f"Cannot create symbolic link: {e}")
187
207
 
188
208
  def _move_node_file(path, old_id, new_id):
189
209
  """
@@ -269,6 +289,7 @@ async def _import_snapshots(snapshots_path, project_name, project_id):
269
289
  with open(snapshot_path, "rb") as f:
270
290
  with zipfile_zstd.ZipFile(f) as zip_file:
271
291
  await wait_run_in_executor(zip_file.extractall, tmpdir)
292
+ _create_symbolic_links(zip_file, tmpdir)
272
293
  except OSError as e:
273
294
  raise ControllerError(f"Cannot open snapshot '{os.path.basename(snapshot)}': {e}")
274
295
  except zipfile_zstd.BadZipFile:
@@ -29,8 +29,8 @@ from .controller_error import (
29
29
  )
30
30
  from .ports.port_factory import PortFactory, StandardPortFactory, DynamipsPortFactory
31
31
  from ..utils.images import images_directories
32
+ from ..utils import macaddress_to_int, int_to_macaddress
32
33
  from ..config import Config
33
- from ..utils.qt import qt_font_to_style
34
34
 
35
35
 
36
36
  import logging
@@ -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
- # if license_check and not iourc_content:
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:
@@ -758,7 +754,13 @@ class Node:
758
754
  break
759
755
  port_name = f"eth{adapter_number}"
760
756
  port_name = custom_adapter_settings.get("port_name", port_name)
761
- self._ports.append(PortFactory(port_name, 0, adapter_number, 0, "ethernet", short_name=port_name))
757
+ mac_address = custom_adapter_settings.get("mac_address")
758
+ if not mac_address and "mac_address" in self._properties:
759
+ mac_address = int_to_macaddress(macaddress_to_int(self._properties["mac_address"]) + adapter_number)
760
+
761
+ port = PortFactory(port_name, 0, adapter_number, 0, "ethernet", short_name=port_name)
762
+ port.mac_address = mac_address
763
+ self._ports.append(port)
762
764
  elif self._node_type in ("ethernet_switch", "ethernet_hub"):
763
765
  # Basic node we don't want to have adapter number
764
766
  port_number = 0
@@ -15,7 +15,7 @@
15
15
  # You should have received a copy of the GNU General Public License
16
16
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
17
 
18
- import os
18
+ import asyncio
19
19
  from contextlib import contextmanager
20
20
 
21
21
  from gns3server.utils.notification_queue import NotificationQueue
@@ -73,7 +73,7 @@ class Notification:
73
73
  """
74
74
 
75
75
  for controller_listener in self._controller_listeners:
76
- controller_listener.put_nowait((action, event, {}))
76
+ asyncio.get_running_loop().call_soon_threadsafe(controller_listener.put_nowait, (action, event, {}))
77
77
 
78
78
  def project_has_listeners(self, project_id):
79
79
  """
@@ -134,7 +134,7 @@ class Notification:
134
134
  except KeyError:
135
135
  return
136
136
  for listener in project_listeners:
137
- listener.put_nowait((action, event, {}))
137
+ asyncio.get_running_loop().call_soon_threadsafe(listener.put_nowait, (action, event, {}))
138
138
 
139
139
  def _send_event_to_all_projects(self, action, event):
140
140
  """
@@ -146,4 +146,4 @@ class Notification:
146
146
  """
147
147
  for project_listeners in self._project_listeners.values():
148
148
  for listener in project_listeners:
149
- listener.put_nowait((action, event, {}))
149
+ asyncio.get_running_loop().call_soon_threadsafe(listener.put_nowait, (action, event, {}))
@@ -15,6 +15,7 @@
15
15
  # You should have received a copy of the GNU General Public License
16
16
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
17
 
18
+ import sys
18
19
  import re
19
20
  import os
20
21
  import json
@@ -26,6 +27,7 @@ import asyncio
26
27
  import aiofiles
27
28
  import tempfile
28
29
  import zipfile
30
+ import pathlib
29
31
 
30
32
  from uuid import UUID, uuid4
31
33
 
@@ -41,8 +43,9 @@ from ..utils.application_id import get_next_application_id
41
43
  from ..utils.asyncio.pool import Pool
42
44
  from ..utils.asyncio import locking
43
45
  from ..utils.asyncio import aiozipstream
46
+ from ..utils.asyncio import wait_run_in_executor
44
47
  from .export_project import export_project
45
- from .import_project import import_project
48
+ from .import_project import import_project, _move_node_file
46
49
  from .controller_error import ControllerError, ControllerForbiddenError, ControllerNotFoundError
47
50
 
48
51
  import logging
@@ -592,7 +595,7 @@ class Project:
592
595
 
593
596
  if node_type == "iou":
594
597
  async with self._iou_id_lock:
595
- # wait for a IOU node to be completely created before adding a new one
598
+ # wait for an IOU node to be completely created before adding a new one
596
599
  # this is important otherwise we allocate the same application ID (used
597
600
  # to generate MAC addresses) when creating multiple IOU node at the same time
598
601
  if "properties" in kwargs.keys():
@@ -1062,13 +1065,15 @@ class Project:
1062
1065
  """
1063
1066
  Duplicate a project
1064
1067
 
1065
- It's the save as feature of the 1.X. It's implemented on top of the
1066
- export / import features. It will generate a gns3p and reimport it.
1067
- It's a little slower but we have only one implementation to maintain.
1068
+ Implemented on top of the export / import features. It will generate a gns3p and reimport it.
1069
+
1070
+ NEW: fast duplication is used if possible (when there are no remote computes).
1071
+ If not, the project is exported and reimported as explained above.
1068
1072
 
1069
1073
  :param name: Name of the new project. A new one will be generated in case of conflicts
1070
1074
  :param reset_mac_addresses: Reset MAC addresses for the new project
1071
1075
  """
1076
+
1072
1077
  # If the project was not open we open it temporary
1073
1078
  previous_status = self._status
1074
1079
  if self._status == "closed":
@@ -1076,6 +1081,18 @@ class Project:
1076
1081
 
1077
1082
  self.dump()
1078
1083
  assert self._status != "closed"
1084
+
1085
+ try:
1086
+ proj = await self._fast_duplication(name, reset_mac_addresses)
1087
+ if proj:
1088
+ if previous_status == "closed":
1089
+ await self.close()
1090
+ return proj
1091
+ else:
1092
+ log.info("Fast duplication failed, fallback to normal duplication")
1093
+ except Exception as e:
1094
+ raise ControllerError(f"Cannot duplicate project: {str(e)}")
1095
+
1079
1096
  try:
1080
1097
  begin = time.time()
1081
1098
 
@@ -1314,3 +1331,69 @@ class Project:
1314
1331
 
1315
1332
  def __repr__(self):
1316
1333
  return f"<gns3server.controller.Project {self._name} {self._id}>"
1334
+
1335
+ async def _fast_duplication(self, name=None, reset_mac_addresses=True):
1336
+ """
1337
+ Fast duplication of a project.
1338
+
1339
+ Copy the project files directly rather than in an import-export fashion.
1340
+
1341
+ :param name: Name of the new project. A new one will be generated in case of conflicts
1342
+ :param reset_mac_addresses: Reset MAC addresses for the new project
1343
+ """
1344
+
1345
+ # remote replication is not supported with remote computes
1346
+ for compute in self.computes:
1347
+ if compute.id != "local":
1348
+ log.warning("Fast duplication is not supported with remote compute: '{}'".format(compute.id))
1349
+ return None
1350
+ # work dir
1351
+ p_work = pathlib.Path(self.path).parent.absolute()
1352
+ t0 = time.time()
1353
+ new_project_id = str(uuid.uuid4())
1354
+ new_project_path = p_work.joinpath(new_project_id)
1355
+ # copy dir
1356
+ await wait_run_in_executor(shutil.copytree, self.path, new_project_path.as_posix(), symlinks=True, ignore_dangling_symlinks=True)
1357
+ log.info("Project content copied from '{}' to '{}' in {}s".format(self.path, new_project_path, time.time() - t0))
1358
+ topology = json.loads(new_project_path.joinpath('{}.gns3'.format(self.name)).read_bytes())
1359
+ project_name = name or topology["name"]
1360
+ # If the project name is already used we generate a new one
1361
+ project_name = self.controller.get_free_project_name(project_name)
1362
+ topology["name"] = project_name
1363
+ # To avoid unexpected behavior (project start without manual operations just after import)
1364
+ topology["auto_start"] = False
1365
+ topology["auto_open"] = False
1366
+ topology["auto_close"] = False
1367
+ # change node ID
1368
+ node_old_to_new = {}
1369
+ for node in topology["topology"]["nodes"]:
1370
+ new_node_id = str(uuid.uuid4())
1371
+ if "node_id" in node:
1372
+ node_old_to_new[node["node_id"]] = new_node_id
1373
+ _move_node_file(new_project_path, node["node_id"], new_node_id)
1374
+ node["node_id"] = new_node_id
1375
+ if reset_mac_addresses:
1376
+ if "properties" in node:
1377
+ for prop, value in node["properties"].items():
1378
+ # reset the MAC address
1379
+ if prop in ("mac_addr", "mac_address"):
1380
+ node["properties"][prop] = None
1381
+ # change link ID
1382
+ for link in topology["topology"]["links"]:
1383
+ link["link_id"] = str(uuid.uuid4())
1384
+ for node in link["nodes"]:
1385
+ node["node_id"] = node_old_to_new[node["node_id"]]
1386
+ # Generate new drawings id
1387
+ for drawing in topology["topology"]["drawings"]:
1388
+ drawing["drawing_id"] = str(uuid.uuid4())
1389
+
1390
+ # And we dump the updated.gns3
1391
+ dot_gns3_path = new_project_path.joinpath('{}.gns3'.format(project_name))
1392
+ topology["project_id"] = new_project_id
1393
+ with open(dot_gns3_path, "w+") as f:
1394
+ json.dump(topology, f, indent=4)
1395
+
1396
+ os.remove(new_project_path.joinpath('{}.gns3'.format(self.name)))
1397
+ project = await self.controller.load_project(dot_gns3_path, load=False)
1398
+ log.info("Project '{}' fast duplicated in {:.4f} seconds".format(project.name, time.time() - t0))
1399
+ return project
@@ -45,7 +45,7 @@ class Symbols:
45
45
  self._symbol_size_cache = {}
46
46
 
47
47
  self._server_config = Config.instance().settings.Server
48
- self._current_theme = self._server_config.default_symbol_theme
48
+ self._current_theme = self._server_config.default_symbol_theme.value
49
49
  self._themes = BUILTIN_SYMBOL_THEMES
50
50
 
51
51
  @property
@@ -194,7 +194,7 @@ def load_topology(path):
194
194
  try:
195
195
  _check_topology_schema(topo, path)
196
196
  except ControllerError as e:
197
- log.error("Can't load the topology %s", path)
197
+ log.error("Can't load the topology %s, please check using the debug mode...", path)
198
198
  raise e
199
199
 
200
200
  if changed:
@@ -58,7 +58,7 @@ class CrashReport:
58
58
  Report crash to a third party service
59
59
  """
60
60
 
61
- DSN = "https://1ae6f3c9d64e75bf8ad39295723da722@o19455.ingest.us.sentry.io/38482"
61
+ DSN = "https://847198b87dbd50ef8962901641918a08@o19455.ingest.us.sentry.io/38482"
62
62
  _instance = None
63
63
 
64
64
  def __init__(self):
@@ -66,6 +66,7 @@ class DockerTemplate(Template):
66
66
  template_id = Column(GUID, ForeignKey("templates.template_id", ondelete="CASCADE"), primary_key=True)
67
67
  image = Column(String)
68
68
  adapters = Column(Integer)
69
+ mac_address = Column(String)
69
70
  start_command = Column(String)
70
71
  environment = Column(String)
71
72
  console_type = Column(String)
@@ -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.UserGroup]:
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
  """
gns3server/db/tasks.py CHANGED
@@ -77,7 +77,7 @@ async def connect_to_db(app: FastAPI) -> None:
77
77
 
78
78
  db_path = os.path.join(Config.instance().config_dir, "gns3_controller.db")
79
79
  db_url = os.environ.get("GNS3_DATABASE_URI", f"sqlite+aiosqlite:///{db_path}")
80
- engine = create_async_engine(db_url, connect_args={"check_same_thread": False}, future=True)
80
+ engine = create_async_engine(db_url, connect_args={"check_same_thread": False, "timeout": 20}, future=True)
81
81
  alembic_cfg = config.Config()
82
82
  alembic_cfg.set_main_option("script_location", "gns3server:db_migrations")
83
83
  #alembic_cfg.set_main_option('sqlalchemy.url', db_url)
@@ -0,0 +1,27 @@
1
+ """add mac_address field in Docker templates table
2
+
3
+ Revision ID: 9a5292aa4389
4
+ Revises: 7ceeddd9c9a8
5
+ Create Date: 2024-09-18 17:52:53.429522
6
+
7
+ """
8
+ from alembic import op
9
+ import sqlalchemy as sa
10
+
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = '9a5292aa4389'
14
+ down_revision = '7ceeddd9c9a8'
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def upgrade() -> None:
20
+
21
+ op.add_column('docker_templates', sa.Column('mac_address', sa.String()))
22
+
23
+
24
+ def downgrade() -> None:
25
+
26
+ op.drop_column('docker_templates', 'mac_address')
27
+
Binary file
Binary file
Binary file
Binary file
@@ -39,6 +39,7 @@ class DockerBase(BaseModel):
39
39
  usage: Optional[str] = Field(None, description="How to use the Docker container")
40
40
  start_command: Optional[str] = Field(None, description="Docker CMD entry")
41
41
  adapters: Optional[int] = Field(None, ge=0, le=99, description="Number of adapters")
42
+ mac_address: Optional[str] = Field(None, description="Base MAC address", pattern="^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$")
42
43
  environment: Optional[str] = Field(None, description="Docker environment variables")
43
44
  extra_hosts: Optional[str] = Field(None, description="Docker extra hosts (added to /etc/hosts)")
44
45
  extra_volumes: Optional[List[str]] = Field(None, description="Additional directories to make persistent")
@@ -43,7 +43,7 @@ class EthernetSwitchPort(BaseModel):
43
43
  port_number: int
44
44
  type: EthernetSwitchPortType = Field(..., description="Port type")
45
45
  vlan: int = Field(..., ge=1, le=4094, description="VLAN number")
46
- ethertype: Optional[EthernetSwitchEtherType] = Field("0x8100", description="QinQ Ethertype")
46
+ ethertype: Optional[EthernetSwitchEtherType] = Field(EthernetSwitchEtherType.ethertype_8021q, description="QinQ Ethertype")
47
47
 
48
48
  @model_validator(mode="after")
49
49
  def check_ethertype(self) -> "EthernetSwitchPort":
@@ -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, use_enum_values=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