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.
- {gns3_server-3.0.0rc1.dist-info → gns3_server-3.0.1.dist-info}/METADATA +20 -19
- {gns3_server-3.0.0rc1.dist-info → gns3_server-3.0.1.dist-info}/RECORD +89 -79
- {gns3_server-3.0.0rc1.dist-info → gns3_server-3.0.1.dist-info}/WHEEL +1 -1
- gns3server/api/routes/compute/cloud_nodes.py +1 -1
- gns3server/api/routes/compute/docker_nodes.py +3 -0
- gns3server/api/routes/compute/nat_nodes.py +1 -1
- gns3server/api/routes/compute/vmware_nodes.py +1 -1
- gns3server/api/routes/compute/vpcs_nodes.py +10 -4
- gns3server/api/routes/controller/projects.py +29 -3
- gns3server/api/routes/controller/users.py +2 -2
- gns3server/api/routes/index.py +3 -3
- gns3server/api/server.py +38 -3
- gns3server/appliances/almalinux.gns3a +6 -6
- gns3server/appliances/arista-veos.gns3a +20 -514
- gns3server/appliances/cisco-7200.gns3a +26 -0
- gns3server/appliances/cisco-asav.gns3a +14 -1
- gns3server/appliances/cisco-csr1000v.gns3a +28 -2
- gns3server/appliances/cisco-iou-l2.gns3a +16 -4
- gns3server/appliances/cisco-iou-l3.gns3a +16 -4
- gns3server/appliances/cisco-vWLC.gns3a +29 -1
- gns3server/appliances/fortigate.gns3a +3 -3
- gns3server/appliances/hbcd-pe.gns3a +62 -0
- gns3server/appliances/innovaphone-app.gns3a +50 -0
- gns3server/appliances/innovaphone-ipva.gns3a +78 -0
- gns3server/appliances/mikrotik-chr.gns3a +30 -99
- gns3server/appliances/nixos.gns3a +52 -0
- gns3server/appliances/opnsense.gns3a +13 -0
- gns3server/appliances/pfsense.gns3a +14 -0
- gns3server/appliances/reactos.gns3a +10 -10
- gns3server/appliances/truenas.gns3a +104 -0
- gns3server/appliances/ubuntu-cloud.gns3a +35 -20
- gns3server/appliances/ubuntu-gui.gns3a +13 -0
- gns3server/appliances/viptela-edge-genericx86-64.gns3a +28 -2
- gns3server/appliances/viptela-smart-genericx86-64.gns3a +27 -1
- gns3server/appliances/viptela-vmanage-genericx86-64.gns3a +32 -4
- gns3server/appliances/vyos.gns3a +95 -98
- gns3server/compute/base_node.py +1 -0
- gns3server/compute/docker/docker_vm.py +56 -2
- gns3server/compute/docker/resources/init.sh +5 -2
- gns3server/compute/dynamips/__init__.py +0 -4
- gns3server/compute/dynamips/nodes/router.py +20 -0
- gns3server/compute/iou/iou_vm.py +22 -12
- gns3server/compute/notification_manager.py +2 -2
- gns3server/compute/qemu/qemu_vm.py +0 -5
- gns3server/controller/__init__.py +35 -25
- gns3server/controller/appliance_manager.py +2 -4
- gns3server/controller/compute.py +1 -1
- gns3server/controller/export_project.py +18 -14
- gns3server/controller/import_project.py +21 -0
- gns3server/controller/node.py +10 -8
- gns3server/controller/notification.py +4 -4
- gns3server/controller/project.py +88 -5
- gns3server/controller/symbols.py +1 -1
- gns3server/controller/topology.py +1 -1
- gns3server/crash_report.py +1 -1
- gns3server/db/models/templates.py +1 -0
- gns3server/db/repositories/pools.py +1 -1
- gns3server/db/tasks.py +1 -1
- gns3server/db_migrations/versions/9a5292aa4389_add_mac_address_field_in_docker_.py +27 -0
- gns3server/disks/empty100G.qcow2 +0 -0
- gns3server/disks/empty200G.qcow2 +0 -0
- gns3server/disks/empty30G.qcow2 +0 -0
- gns3server/disks/empty8G.qcow2 +0 -0
- gns3server/schemas/compute/docker_nodes.py +1 -0
- gns3server/schemas/compute/ethernet_switch_nodes.py +1 -1
- gns3server/schemas/config.py +1 -1
- gns3server/schemas/controller/templates/cloud_templates.py +2 -2
- gns3server/schemas/controller/templates/docker_templates.py +4 -3
- gns3server/schemas/controller/templates/dynamips_templates.py +5 -5
- gns3server/schemas/controller/templates/ethernet_hub_templates.py +1 -1
- gns3server/schemas/controller/templates/ethernet_switch_templates.py +2 -2
- gns3server/schemas/controller/templates/iou_templates.py +2 -2
- gns3server/schemas/controller/templates/qemu_templates.py +12 -12
- gns3server/schemas/controller/templates/virtualbox_templates.py +4 -5
- gns3server/schemas/controller/templates/vmware_templates.py +4 -4
- gns3server/schemas/controller/templates/vpcs_templates.py +2 -2
- gns3server/static/favicon.ico +0 -0
- gns3server/static/redoc.standalone.js +1782 -0
- gns3server/static/swagger-ui-bundle.js +2 -0
- gns3server/static/swagger-ui.css +3 -0
- gns3server/static/web-ui/index.html +1 -1
- gns3server/static/web-ui/{main.4185a8e61824af0d.js → main.e55eeff5c0ba1cf4.js} +1 -1
- gns3server/utils/__init__.py +12 -0
- gns3server/utils/asyncio/aiozipstream.py +15 -11
- gns3server/utils/images.py +45 -35
- gns3server/version.py +2 -2
- {gns3_server-3.0.0rc1.dist-info → gns3_server-3.0.1.dist-info}/LICENSE +0 -0
- {gns3_server-3.0.0rc1.dist-info → gns3_server-3.0.1.dist-info}/entry_points.txt +0 -0
- {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":
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
|
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(
|
|
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:
|
gns3server/controller/compute.py
CHANGED
|
@@ -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=
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
gns3server/controller/node.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
149
|
+
asyncio.get_running_loop().call_soon_threadsafe(listener.put_nowait, (action, event, {}))
|
gns3server/controller/project.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
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
|
gns3server/controller/symbols.py
CHANGED
|
@@ -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:
|
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://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.
|
|
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
|
+
|
gns3server/disks/empty100G.qcow2
CHANGED
|
Binary file
|
gns3server/disks/empty200G.qcow2
CHANGED
|
Binary file
|
gns3server/disks/empty30G.qcow2
CHANGED
|
Binary file
|
gns3server/disks/empty8G.qcow2
CHANGED
|
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(
|
|
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":
|
gns3server/schemas/config.py
CHANGED
|
@@ -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
|
|
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
|