gns3-server 3.0.4__py3-none-any.whl → 3.0.5__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.5.dist-info/METADATA +202 -0
- {gns3_server-3.0.4.dist-info → gns3_server-3.0.5.dist-info}/RECORD +38 -33
- {gns3_server-3.0.4.dist-info → gns3_server-3.0.5.dist-info}/WHEEL +1 -1
- gns3server/api/routes/compute/cloud_nodes.py +1 -1
- gns3server/api/routes/controller/images.py +0 -5
- gns3server/api/routes/controller/links.py +52 -1
- gns3server/appliances/almalinux.gns3a +30 -0
- gns3server/appliances/arista-veos.gns3a +15 -15
- gns3server/appliances/aruba-arubaoscx.gns3a +39 -0
- gns3server/appliances/asterfusion-vAsterNOS-campus.gns3a +50 -0
- gns3server/appliances/asterfusion-vAsterNOS.gns3a +1 -1
- gns3server/appliances/centos-cloud.gns3a +24 -54
- gns3server/appliances/exos.gns3a +13 -0
- gns3server/appliances/fedora-cloud.gns3a +30 -0
- gns3server/appliances/infix.gns3a +48 -5
- gns3server/appliances/juniper-vJunos-router.gns3a +75 -0
- gns3server/appliances/nethsecurity.gns3a +44 -0
- gns3server/appliances/oracle-linux-cloud.gns3a +31 -1
- gns3server/appliances/rhel.gns3a +57 -1
- gns3server/appliances/rockylinux.gns3a +15 -0
- gns3server/compute/builtin/nodes/nat.py +1 -1
- gns3server/compute/docker/__init__.py +8 -9
- gns3server/compute/docker/docker_vm.py +2 -1
- gns3server/compute/qemu/qemu_vm.py +69 -28
- gns3server/controller/compute.py +4 -3
- gns3server/controller/gns3vm/virtualbox_gns3_vm.py +55 -28
- gns3server/crash_report.py +1 -1
- gns3server/disks/OVMF_CODE_4M.fd +0 -0
- gns3server/disks/OVMF_VARS_4M.fd +0 -0
- gns3server/schemas/__init__.py +1 -1
- gns3server/schemas/controller/links.py +21 -0
- gns3server/static/web-ui/index.html +1 -1
- gns3server/static/web-ui/main.fd9d76d279fa7d5e.js +1 -0
- gns3server/utils/images.py +10 -0
- gns3server/version.py +2 -2
- gns3_server-3.0.4.dist-info/METADATA +0 -869
- gns3server/static/web-ui/main.87178dd64c9c79ba.js +0 -1
- {gns3_server-3.0.4.dist-info → gns3_server-3.0.5.dist-info}/entry_points.txt +0 -0
- {gns3_server-3.0.4.dist-info → gns3_server-3.0.5.dist-info/licenses}/LICENSE +0 -0
- {gns3_server-3.0.4.dist-info → gns3_server-3.0.5.dist-info}/top_level.txt +0 -0
gns3server/appliances/rhel.gns3a
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"availability": "service-contract",
|
|
14
14
|
"maintainer": "Da-Geek",
|
|
15
15
|
"maintainer_email": "dageek@dageeks-geeks.gg",
|
|
16
|
-
"usage": "You should download Red Hat Enterprise Linux KVM Guest Image from https://access.redhat.com/downloads/content/479/ver=/rhel---9/9.
|
|
16
|
+
"usage": "You should download Red Hat Enterprise Linux KVM Guest Image from https://access.redhat.com/downloads/content/479/ver=/rhel---9/9.5/x86_64/product-software attach/customize rhel-cloud-init.iso and start.\nusername: cloud-user\npassword: redhat",
|
|
17
17
|
"qemu": {
|
|
18
18
|
"adapter_type": "virtio-net-pci",
|
|
19
19
|
"adapters": 1,
|
|
@@ -26,6 +26,20 @@
|
|
|
26
26
|
"options": "-cpu host -nographic"
|
|
27
27
|
},
|
|
28
28
|
"images": [
|
|
29
|
+
{
|
|
30
|
+
"filename": "rhel-9.5-x86_64-kvm.qcow2",
|
|
31
|
+
"version": "9.5",
|
|
32
|
+
"md5sum": "8174396d5cb47727c59dd04dd9a05418",
|
|
33
|
+
"filesize": 974389248,
|
|
34
|
+
"download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---9/9.5/x86_64/product-software"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"filename": "rhel-9.4-x86_64-kvm.qcow2",
|
|
38
|
+
"version": "9.4",
|
|
39
|
+
"md5sum": "77a2ca9a4cb0448260e04f0d2ebf9807",
|
|
40
|
+
"filesize": 957218816,
|
|
41
|
+
"download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---9/9.4/x86_64/product-software"
|
|
42
|
+
},
|
|
29
43
|
{
|
|
30
44
|
"filename": "rhel-9.3-x86_64-kvm.qcow2",
|
|
31
45
|
"version": "9.3",
|
|
@@ -54,6 +68,20 @@
|
|
|
54
68
|
"filesize": 696582144,
|
|
55
69
|
"download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---8/9.0/x86_64/product-software"
|
|
56
70
|
},
|
|
71
|
+
{
|
|
72
|
+
"filename": "rhel-8.10-x86_64-kvm.qcow2",
|
|
73
|
+
"version": "8.10",
|
|
74
|
+
"md5sum": "5fda99fcab47e3b235c6ccdb6e80d362",
|
|
75
|
+
"filesize": 1065091072,
|
|
76
|
+
"download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---8/8.10/x86_64/product-software"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"filename": "rhel-8.9-x86_64-kvm.qcow2",
|
|
80
|
+
"version": "8.9",
|
|
81
|
+
"md5sum": "23295fe508678cbdebfbdbd41ef6e6e2",
|
|
82
|
+
"filesize": 971833344,
|
|
83
|
+
"download_url": "https://access.redhat.com/downloads/content/479/ver=/rhel---8/8.9/x86_64/product-software"
|
|
84
|
+
},
|
|
57
85
|
{
|
|
58
86
|
"filename": "rhel-8.8-x86_64-kvm.qcow2",
|
|
59
87
|
"version": "8.8",
|
|
@@ -119,6 +147,20 @@
|
|
|
119
147
|
}
|
|
120
148
|
],
|
|
121
149
|
"versions": [
|
|
150
|
+
{
|
|
151
|
+
"name": "9.5",
|
|
152
|
+
"images": {
|
|
153
|
+
"hda_disk_image": "rhel-9.5-x86_64-kvm.qcow2",
|
|
154
|
+
"cdrom_image": "rhel-cloud-init.iso"
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"name": "9.4",
|
|
159
|
+
"images": {
|
|
160
|
+
"hda_disk_image": "rhel-9.4-x86_64-kvm.qcow2",
|
|
161
|
+
"cdrom_image": "rhel-cloud-init.iso"
|
|
162
|
+
}
|
|
163
|
+
},
|
|
122
164
|
{
|
|
123
165
|
"name": "9.3",
|
|
124
166
|
"images": {
|
|
@@ -147,6 +189,20 @@
|
|
|
147
189
|
"cdrom_image": "rhel-cloud-init.iso"
|
|
148
190
|
}
|
|
149
191
|
},
|
|
192
|
+
{
|
|
193
|
+
"name": "8.10",
|
|
194
|
+
"images": {
|
|
195
|
+
"hda_disk_image": "rhel-8.10-x86_64-kvm.qcow2",
|
|
196
|
+
"cdrom_image": "rhel-cloud-init.iso"
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"name": "8.9",
|
|
201
|
+
"images": {
|
|
202
|
+
"hda_disk_image": "rhel-8.9-x86_64-kvm.qcow2",
|
|
203
|
+
"cdrom_image": "rhel-cloud-init.iso"
|
|
204
|
+
}
|
|
205
|
+
},
|
|
150
206
|
{
|
|
151
207
|
"name": "8.8",
|
|
152
208
|
"images": {
|
|
@@ -26,6 +26,14 @@
|
|
|
26
26
|
"options": "-nographic -cpu host"
|
|
27
27
|
},
|
|
28
28
|
"images": [
|
|
29
|
+
{
|
|
30
|
+
"filename": "Rocky-9-GenericCloud-Base-9.5-20241118.0.x86_64.qcow2",
|
|
31
|
+
"version": "9.5",
|
|
32
|
+
"md5sum": "880eccf788301bb9f34669faebe09276",
|
|
33
|
+
"filesize": 609812480,
|
|
34
|
+
"download_url": "https://download.rockylinux.org/pub/rocky/9/images/x86_64/",
|
|
35
|
+
"direct_download_url": "https://download.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud-Base-9.5-20241118.0.x86_64.qcow2"
|
|
36
|
+
},
|
|
29
37
|
{
|
|
30
38
|
"filename": "Rocky-9-GenericCloud-Base-9.3-20231113.0.x86_64.qcow2",
|
|
31
39
|
"version": "9.3",
|
|
@@ -68,6 +76,13 @@
|
|
|
68
76
|
}
|
|
69
77
|
],
|
|
70
78
|
"versions": [
|
|
79
|
+
{
|
|
80
|
+
"name": "9.5",
|
|
81
|
+
"images": {
|
|
82
|
+
"hda_disk_image": "Rocky-9-GenericCloud-Base-9.5-20241118.0.x86_64.qcow2",
|
|
83
|
+
"cdrom_image": "rocky-cloud-init-data.iso"
|
|
84
|
+
}
|
|
85
|
+
},
|
|
71
86
|
{
|
|
72
87
|
"name": "9.3",
|
|
73
88
|
"images": {
|
|
@@ -37,7 +37,7 @@ class Nat(Cloud):
|
|
|
37
37
|
def __init__(self, name, node_id, project, manager, ports=None):
|
|
38
38
|
|
|
39
39
|
allowed_interfaces = Config.instance().settings.Server.allowed_interfaces
|
|
40
|
-
if allowed_interfaces:
|
|
40
|
+
if allowed_interfaces and isinstance(allowed_interfaces, str):
|
|
41
41
|
allowed_interfaces = allowed_interfaces.split(',')
|
|
42
42
|
if sys.platform.startswith("linux"):
|
|
43
43
|
nat_interface = Config.instance().settings.Server.default_nat_interface
|
|
@@ -175,11 +175,10 @@ class Docker(BaseManager):
|
|
|
175
175
|
response = await self.http_query(method, path, data=data, params=params)
|
|
176
176
|
body = await response.read()
|
|
177
177
|
response.close()
|
|
178
|
-
if
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
body = body.decode("utf-8")
|
|
178
|
+
if response.headers.get('CONTENT-TYPE') == 'application/json':
|
|
179
|
+
body = json.loads(body.decode("utf-8", errors="ignore"))
|
|
180
|
+
else:
|
|
181
|
+
body = body.decode("utf-8", errors="ignore")
|
|
183
182
|
log.debug("Query Docker %s %s params=%s data=%s Response: %s", method, path, params, data, body)
|
|
184
183
|
return body
|
|
185
184
|
|
|
@@ -267,12 +266,12 @@ class Docker(BaseManager):
|
|
|
267
266
|
pass
|
|
268
267
|
|
|
269
268
|
if progress_callback:
|
|
270
|
-
progress_callback(f"Pulling '{image}' from
|
|
269
|
+
progress_callback(f"Pulling '{image}' from Docker repository")
|
|
271
270
|
try:
|
|
272
271
|
response = await self.http_query("POST", "images/create", params={"fromImage": image}, timeout=None)
|
|
273
272
|
except DockerError as e:
|
|
274
273
|
raise DockerError(
|
|
275
|
-
f"Could not pull the '{image}' image from Docker
|
|
274
|
+
f"Could not pull the '{image}' image from Docker repository, "
|
|
276
275
|
f"please check your Internet connection (original error: {e})"
|
|
277
276
|
)
|
|
278
277
|
# The pull api will stream status via an HTTP JSON stream
|
|
@@ -281,10 +280,10 @@ class Docker(BaseManager):
|
|
|
281
280
|
try:
|
|
282
281
|
chunk = await response.content.read(CHUNK_SIZE)
|
|
283
282
|
except aiohttp.ServerDisconnectedError:
|
|
284
|
-
log.error(f"Disconnected from server while pulling Docker image '{image}' from
|
|
283
|
+
log.error(f"Disconnected from server while pulling Docker image '{image}' from Docker repository")
|
|
285
284
|
break
|
|
286
285
|
except asyncio.TimeoutError:
|
|
287
|
-
log.error(
|
|
286
|
+
log.error("Timeout while pulling Docker image '{}' from Docker repository".format(image))
|
|
288
287
|
break
|
|
289
288
|
if not chunk:
|
|
290
289
|
break
|
|
@@ -437,7 +437,7 @@ class DockerVM(BaseNode):
|
|
|
437
437
|
try:
|
|
438
438
|
image_infos = await self._get_image_information()
|
|
439
439
|
except DockerHttp404Error:
|
|
440
|
-
log.info(
|
|
440
|
+
log.info("Image '{}' is missing, pulling it from Docker repository...".format(self._image))
|
|
441
441
|
await self.pull_image(self._image)
|
|
442
442
|
image_infos = await self._get_image_information()
|
|
443
443
|
|
|
@@ -617,6 +617,7 @@ class DockerVM(BaseNode):
|
|
|
617
617
|
await self._clean_servers()
|
|
618
618
|
|
|
619
619
|
await self.manager.query("POST", f"containers/{self._cid}/start")
|
|
620
|
+
await asyncio.sleep(0.5) # give the Docker container some time to start
|
|
620
621
|
self._namespace = await self._get_namespace()
|
|
621
622
|
|
|
622
623
|
await self._start_ubridge(require_privileged_access=True)
|
|
@@ -32,6 +32,7 @@ import subprocess
|
|
|
32
32
|
import time
|
|
33
33
|
import json
|
|
34
34
|
import shlex
|
|
35
|
+
import psutil
|
|
35
36
|
|
|
36
37
|
from gns3server.utils import parse_version
|
|
37
38
|
from gns3server.utils.asyncio import subprocess_check_output, cancellable_wait_run_in_executor
|
|
@@ -265,17 +266,10 @@ class QemuVM(BaseNode):
|
|
|
265
266
|
if qemu_bin == "qemu":
|
|
266
267
|
self._platform = "i386"
|
|
267
268
|
else:
|
|
268
|
-
self._platform = re.sub(r'^qemu-system-(\w+).*$', r'\1', qemu_bin, re.IGNORECASE)
|
|
269
|
-
|
|
270
|
-
try:
|
|
271
|
-
QemuPlatform(self._platform.split(".")[0])
|
|
272
|
-
except ValueError:
|
|
269
|
+
self._platform = re.sub(r'^qemu-system-(\w+).*$', r'\1', qemu_bin, flags=re.IGNORECASE)
|
|
270
|
+
if self._platform.split(".")[0] not in list(QemuPlatform):
|
|
273
271
|
raise QemuError(f"Platform {self._platform} is unknown")
|
|
274
|
-
log.info(
|
|
275
|
-
'QEMU VM "{name}" [{id}] has set the QEMU path to {qemu_path}'.format(
|
|
276
|
-
name=self._name, id=self._id, qemu_path=qemu_path
|
|
277
|
-
)
|
|
278
|
-
)
|
|
272
|
+
log.info(f'QEMU VM "{self._name}" [{self._name}] has set the QEMU path to {qemu_path}')
|
|
279
273
|
|
|
280
274
|
def _check_qemu_path(self, qemu_path):
|
|
281
275
|
|
|
@@ -1225,6 +1219,21 @@ class QemuVM(BaseNode):
|
|
|
1225
1219
|
except OSError as e:
|
|
1226
1220
|
raise QemuError(f"Could not start Telnet QEMU console {e}\n")
|
|
1227
1221
|
|
|
1222
|
+
def _find_partition_for_path(self, path):
|
|
1223
|
+
"""
|
|
1224
|
+
Finds the disk partition for a given path.
|
|
1225
|
+
"""
|
|
1226
|
+
|
|
1227
|
+
path = os.path.abspath(path)
|
|
1228
|
+
partitions = psutil.disk_partitions()
|
|
1229
|
+
# find the partition with the longest matching mount point
|
|
1230
|
+
matching_partition = None
|
|
1231
|
+
for partition in partitions:
|
|
1232
|
+
if path.startswith(partition.mountpoint):
|
|
1233
|
+
if matching_partition is None or len(partition.mountpoint) > len(matching_partition.mountpoint):
|
|
1234
|
+
matching_partition = partition
|
|
1235
|
+
return matching_partition
|
|
1236
|
+
|
|
1228
1237
|
async def _termination_callback(self, returncode):
|
|
1229
1238
|
"""
|
|
1230
1239
|
Called when the process has stopped.
|
|
@@ -1236,9 +1245,19 @@ class QemuVM(BaseNode):
|
|
|
1236
1245
|
log.info("QEMU process has stopped, return code: %d", returncode)
|
|
1237
1246
|
await self.stop()
|
|
1238
1247
|
if returncode != 0:
|
|
1248
|
+
qemu_stdout = self.read_stdout()
|
|
1249
|
+
# additional permissions need to be configured for swtpm in AppArmor if the working dir
|
|
1250
|
+
# is located on a different partition than the partition for the root directory
|
|
1251
|
+
if "TPM result for CMD_INIT" in qemu_stdout:
|
|
1252
|
+
partition = self._find_partition_for_path(self.project.path)
|
|
1253
|
+
if partition and partition.mountpoint != "/":
|
|
1254
|
+
qemu_stdout += "\nTPM error: the project directory is not on the same partition as the root directory which can be a problem when using AppArmor.\n" \
|
|
1255
|
+
"Please try to execute the following commands on the server:\n\n" \
|
|
1256
|
+
"echo 'owner {}/** rwk,' | sudo tee /etc/apparmor.d/local/usr.bin.swtpm > /dev/null\n" \
|
|
1257
|
+
"sudo service apparmor restart".format(os.path.dirname(self.project.path))
|
|
1239
1258
|
self.project.emit(
|
|
1240
1259
|
"log.error",
|
|
1241
|
-
{"message": f"QEMU process has stopped, return code: {returncode}\n{
|
|
1260
|
+
{"message": f"QEMU process has stopped, return code: {returncode}\n{qemu_stdout}"},
|
|
1242
1261
|
)
|
|
1243
1262
|
|
|
1244
1263
|
async def stop(self):
|
|
@@ -2287,19 +2306,42 @@ class QemuVM(BaseNode):
|
|
|
2287
2306
|
else:
|
|
2288
2307
|
raise QemuError(f"bios image '{self._bios_image}' is not accessible")
|
|
2289
2308
|
options.extend(["-bios", self._bios_image.replace(",", ",,")])
|
|
2309
|
+
|
|
2290
2310
|
elif self._uefi:
|
|
2291
|
-
|
|
2292
|
-
|
|
2311
|
+
|
|
2312
|
+
old_ovmf_vars_path = os.path.join(self.working_dir, "OVMF_VARS.fd")
|
|
2313
|
+
if os.path.exists(old_ovmf_vars_path):
|
|
2314
|
+
# the node has its own UEFI variables store already, we must also use the old UEFI firmware
|
|
2315
|
+
ovmf_firmware_path = self.manager.get_abs_image_path("OVMF_CODE.fd")
|
|
2316
|
+
else:
|
|
2317
|
+
system_ovmf_firmware_path = "/usr/share/OVMF/OVMF_CODE_4M.fd"
|
|
2318
|
+
if os.path.exists(system_ovmf_firmware_path):
|
|
2319
|
+
ovmf_firmware_path = system_ovmf_firmware_path
|
|
2320
|
+
else:
|
|
2321
|
+
# otherwise, get the UEFI firmware from the images directory
|
|
2322
|
+
ovmf_firmware_path = self.manager.get_abs_image_path("OVMF_CODE_4M.fd")
|
|
2323
|
+
|
|
2293
2324
|
log.info("Configuring UEFI boot mode using OVMF file: '{}'".format(ovmf_firmware_path))
|
|
2294
2325
|
options.extend(["-drive", "if=pflash,format=raw,readonly,file={}".format(ovmf_firmware_path)])
|
|
2295
2326
|
|
|
2327
|
+
# try to use the UEFI variables store from the system first
|
|
2328
|
+
system_ovmf_vars_path = "/usr/share/OVMF/OVMF_VARS_4M.fd"
|
|
2329
|
+
if os.path.exists(system_ovmf_vars_path):
|
|
2330
|
+
ovmf_vars_path = system_ovmf_vars_path
|
|
2331
|
+
else:
|
|
2332
|
+
# otherwise, get the UEFI variables store from the images directory
|
|
2333
|
+
ovmf_vars_path = self.manager.get_abs_image_path("OVMF_VARS_4M.fd")
|
|
2334
|
+
|
|
2296
2335
|
# the node should have its own copy of OVMF_VARS.fd (the UEFI variables store)
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2336
|
+
if os.path.exists(old_ovmf_vars_path):
|
|
2337
|
+
ovmf_vars_node_path = old_ovmf_vars_path
|
|
2338
|
+
else:
|
|
2339
|
+
ovmf_vars_node_path = os.path.join(self.working_dir, "OVMF_VARS_4M.fd")
|
|
2340
|
+
if not os.path.exists(ovmf_vars_node_path):
|
|
2341
|
+
try:
|
|
2342
|
+
shutil.copyfile(ovmf_vars_path, ovmf_vars_node_path)
|
|
2343
|
+
except OSError as e:
|
|
2344
|
+
raise QemuError("Cannot copy OVMF_VARS_4M.fd file to the node working directory: {}".format(e))
|
|
2303
2345
|
options.extend(["-drive", "if=pflash,format=raw,file={}".format(ovmf_vars_node_path)])
|
|
2304
2346
|
return options
|
|
2305
2347
|
|
|
@@ -2396,16 +2438,13 @@ class QemuVM(BaseNode):
|
|
|
2396
2438
|
) # we do not want any user networking back-end if no adapter is connected.
|
|
2397
2439
|
|
|
2398
2440
|
# Each 32 PCI device we need to add a PCI bridge with max 9 bridges
|
|
2399
|
-
|
|
2400
|
-
|
|
2441
|
+
# Reserve 32 devices on root pci_bridge,
|
|
2442
|
+
# since the number of devices used by templates may differ significantly
|
|
2443
|
+
# and pci_bridges also consume IDs.
|
|
2444
|
+
# Move network devices to their own bridge
|
|
2445
|
+
pci_devices_reserved = 32
|
|
2401
2446
|
pci_bridges_created = 0
|
|
2402
|
-
|
|
2403
|
-
if self._qemu_version and parse_version(self._qemu_version) < parse_version("2.4.0"):
|
|
2404
|
-
raise QemuError(
|
|
2405
|
-
"Qemu version 2.4 or later is required to run this VM with a large number of network adapters"
|
|
2406
|
-
)
|
|
2407
|
-
|
|
2408
|
-
pci_device_id = 4 + pci_bridges # Bridge consume PCI ports
|
|
2447
|
+
pci_device_id = pci_devices_reserved
|
|
2409
2448
|
for adapter_number, adapter in enumerate(self._ethernet_adapters):
|
|
2410
2449
|
mac = int_to_macaddress(macaddress_to_int(self._mac_address) + adapter_number)
|
|
2411
2450
|
|
|
@@ -2596,6 +2635,8 @@ class QemuVM(BaseNode):
|
|
|
2596
2635
|
"""
|
|
2597
2636
|
|
|
2598
2637
|
self._qemu_version = await self.manager.get_qemu_version(self.qemu_path)
|
|
2638
|
+
if self._qemu_version and parse_version(self._qemu_version) < parse_version("2.4.0"):
|
|
2639
|
+
raise QemuError("Qemu version 2.4 or later is required to run Qemu VMs")
|
|
2599
2640
|
vm_name = self._name.replace(",", ",,")
|
|
2600
2641
|
project_path = self.project.path.replace(",", ",,")
|
|
2601
2642
|
additional_options = self._options.strip()
|
gns3server/controller/compute.py
CHANGED
|
@@ -458,10 +458,11 @@ class Compute:
|
|
|
458
458
|
# FIXME: slow down number of compute events
|
|
459
459
|
self._controller.notification.controller_emit("compute.updated", self.asdict())
|
|
460
460
|
else:
|
|
461
|
-
if action == "log.error":
|
|
462
|
-
log.error(event.pop("message"))
|
|
463
461
|
await self._controller.notification.dispatch(
|
|
464
|
-
action,
|
|
462
|
+
action,
|
|
463
|
+
event,
|
|
464
|
+
project_id=project_id,
|
|
465
|
+
compute_id=self.id
|
|
465
466
|
)
|
|
466
467
|
else:
|
|
467
468
|
if response.type == aiohttp.WSMsgType.CLOSE:
|
|
@@ -249,6 +249,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
|
|
|
249
249
|
return True
|
|
250
250
|
return False
|
|
251
251
|
|
|
252
|
+
|
|
252
253
|
async def list(self):
|
|
253
254
|
"""
|
|
254
255
|
List all VirtualBox VMs
|
|
@@ -269,8 +270,8 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
|
|
|
269
270
|
|
|
270
271
|
# get a NAT interface number
|
|
271
272
|
nat_interface_number = await self._look_for_interface("nat")
|
|
272
|
-
if nat_interface_number < 0:
|
|
273
|
-
raise GNS3VMError(f'VM "{self.vmname}" must have a NAT interface configured in order to start')
|
|
273
|
+
if nat_interface_number < 0 and await self._look_for_interface("natnetwork") < 0:
|
|
274
|
+
raise GNS3VMError(f'VM "{self.vmname}" must have a NAT interface or NAT Network configured in order to start')
|
|
274
275
|
|
|
275
276
|
if sys.platform.startswith("darwin") and parse_version(self._system_properties["API version"]) >= parse_version("7_0"):
|
|
276
277
|
# VirtualBox 7.0+ on macOS requires a host-only network interface
|
|
@@ -339,42 +340,68 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
|
|
|
339
340
|
elif vm_state == "paused":
|
|
340
341
|
args = [self._vmname, "resume"]
|
|
341
342
|
await self._execute("controlvm", args)
|
|
342
|
-
ip_address = "127.0.0.1"
|
|
343
|
-
try:
|
|
344
|
-
# get a random port on localhost
|
|
345
|
-
with socket.socket() as s:
|
|
346
|
-
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
347
|
-
s.bind((ip_address, 0))
|
|
348
|
-
api_port = s.getsockname()[1]
|
|
349
|
-
except OSError as e:
|
|
350
|
-
raise GNS3VMError(f"Error while getting random port: {e}")
|
|
351
|
-
|
|
352
|
-
if await self._check_vbox_port_forwarding():
|
|
353
|
-
# delete the GNS3VM NAT port forwarding rule if it exists
|
|
354
|
-
log.info(f"Removing GNS3VM NAT port forwarding rule from interface {nat_interface_number}")
|
|
355
|
-
await self._execute("controlvm", [self._vmname, f"natpf{nat_interface_number}", "delete", "GNS3VM"])
|
|
356
|
-
|
|
357
|
-
# add a GNS3VM NAT port forwarding rule to redirect 127.0.0.1 with random port to the port in the VM
|
|
358
|
-
log.info(f"Adding GNS3VM NAT port forwarding rule with port {api_port} to interface {nat_interface_number}")
|
|
359
|
-
await self._execute(
|
|
360
|
-
"controlvm",
|
|
361
|
-
[self._vmname, f"natpf{nat_interface_number}", f"GNS3VM,tcp,{ip_address},{api_port},,{self.port}"],
|
|
362
|
-
)
|
|
363
343
|
|
|
364
|
-
|
|
365
|
-
|
|
344
|
+
log.info("Retrieving IP address from GNS3 VM...")
|
|
345
|
+
ip = await self._get_ip_from_guest_property()
|
|
346
|
+
if ip:
|
|
347
|
+
self.ip_address = ip
|
|
348
|
+
else:
|
|
349
|
+
# if we can't get the IP address from the guest property, we try to get it from the GNS3 server (a NAT interface is required)
|
|
350
|
+
if nat_interface_number < 0:
|
|
351
|
+
raise GNS3VMError("Could not find guest IP address for {}".format(self.vmname))
|
|
352
|
+
log.warning("Could not find IP address from guest property, trying to get it from GNS3 server")
|
|
353
|
+
ip_address = "127.0.0.1"
|
|
354
|
+
try:
|
|
355
|
+
# get a random port on localhost
|
|
356
|
+
with socket.socket() as s:
|
|
357
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
358
|
+
s.bind((ip_address, 0))
|
|
359
|
+
api_port = s.getsockname()[1]
|
|
360
|
+
except OSError as e:
|
|
361
|
+
raise GNS3VMError("Error while getting random port: {}".format(e))
|
|
362
|
+
|
|
363
|
+
if await self._check_vbox_port_forwarding():
|
|
364
|
+
# delete the GNS3VM NAT port forwarding rule if it exists
|
|
365
|
+
log.info("Removing GNS3VM NAT port forwarding rule from interface {}".format(nat_interface_number))
|
|
366
|
+
await self._execute("controlvm", [self._vmname, "natpf{}".format(nat_interface_number), "delete", "GNS3VM"])
|
|
367
|
+
|
|
368
|
+
# add a GNS3VM NAT port forwarding rule to redirect 127.0.0.1 with random port to the port in the VM
|
|
369
|
+
log.info("Adding GNS3VM NAT port forwarding rule with port {} to interface {}".format(api_port, nat_interface_number))
|
|
370
|
+
await self._execute("controlvm", [self._vmname, "natpf{}".format(nat_interface_number),
|
|
371
|
+
"GNS3VM,tcp,{},{},,{}".format(ip_address, api_port, self.port)])
|
|
372
|
+
|
|
373
|
+
self.ip_address = await self._get_ip_from_server(interface_number, api_port)
|
|
374
|
+
|
|
375
|
+
log.info("GNS3 VM has been started with IP '{}'".format(self.ip_address))
|
|
366
376
|
self.running = True
|
|
367
377
|
|
|
368
|
-
async def
|
|
378
|
+
async def _get_ip_from_guest_property(self):
|
|
379
|
+
"""
|
|
380
|
+
Get the IP from VirtualBox by retrieving the guest property (Guest Additions must be installed).
|
|
381
|
+
"""
|
|
382
|
+
|
|
383
|
+
remaining_try = 180 # try for 3 minutes
|
|
384
|
+
while remaining_try > 0:
|
|
385
|
+
result = await self._execute("guestproperty", ["get", self._vmname, "/VirtualBox/GuestInfo/Net/0/V4/IP"])
|
|
386
|
+
for info in result.splitlines():
|
|
387
|
+
if ':' in info:
|
|
388
|
+
name, value = info.split(':', 1)
|
|
389
|
+
if name == "Value":
|
|
390
|
+
return value.strip()
|
|
391
|
+
remaining_try -= 1
|
|
392
|
+
await asyncio.sleep(1)
|
|
393
|
+
return None
|
|
394
|
+
|
|
395
|
+
async def _get_ip_from_server(self, hostonly_interface_number, api_port):
|
|
369
396
|
"""
|
|
370
|
-
Get the IP from VirtualBox.
|
|
397
|
+
Get the IP from VirtualBox by sending a request to the GNS3 server.
|
|
371
398
|
|
|
372
399
|
Due to VirtualBox limitation the only way is to send request each
|
|
373
400
|
second to a GNS3 endpoint in order to get the list of the interfaces and
|
|
374
401
|
their IP and after that match it with VirtualBox host only.
|
|
375
402
|
"""
|
|
376
403
|
|
|
377
|
-
remaining_try =
|
|
404
|
+
remaining_try = 180 # try for 3 minutes
|
|
378
405
|
while remaining_try > 0:
|
|
379
406
|
try:
|
|
380
407
|
async with HTTPClient.get(f"http://127.0.0.1:{api_port}/v3/compute/network/interfaces") as resp:
|
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://61bb46252cabeebd49ee1e09fb8ba72e@o19455.ingest.us.sentry.io/38482"
|
|
62
62
|
_instance = None
|
|
63
63
|
|
|
64
64
|
def __init__(self):
|
|
Binary file
|
|
Binary file
|
gns3server/schemas/__init__.py
CHANGED
|
@@ -20,7 +20,7 @@ from .common import ErrorMessage
|
|
|
20
20
|
from .version import Version
|
|
21
21
|
|
|
22
22
|
# Controller schemas
|
|
23
|
-
from .controller.links import LinkCreate, LinkUpdate, Link
|
|
23
|
+
from .controller.links import LinkCreate, LinkUpdate, Link, UDPPortInfo, EthernetPortInfo
|
|
24
24
|
from .controller.computes import ComputeCreate, ComputeUpdate, ComputeVirtualBoxVM, ComputeVMwareVM, ComputeDockerImage, AutoIdlePC, Compute
|
|
25
25
|
from .controller.templates import TemplateCreate, TemplateUpdate, TemplateUsage, Template
|
|
26
26
|
from .controller.images import Image, ImageType
|
|
@@ -92,3 +92,24 @@ class Link(LinkBase):
|
|
|
92
92
|
None,
|
|
93
93
|
description="Read only property. The compute identifier where a capture is running"
|
|
94
94
|
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class UDPPortInfo(BaseModel):
|
|
98
|
+
"""
|
|
99
|
+
UDP port information.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
node_id: UUID
|
|
103
|
+
lport: int
|
|
104
|
+
rhost: str
|
|
105
|
+
rport: int
|
|
106
|
+
type: str
|
|
107
|
+
|
|
108
|
+
class EthernetPortInfo(BaseModel):
|
|
109
|
+
"""
|
|
110
|
+
Ethernet port information.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
node_id: UUID
|
|
114
|
+
interface: str
|
|
115
|
+
type: str
|
|
@@ -46,6 +46,6 @@
|
|
|
46
46
|
|
|
47
47
|
gtag('config', 'G-0BT7QQV1W1');
|
|
48
48
|
</script>
|
|
49
|
-
<script src="runtime.24fa95b7061d7056.js" type="module"></script><script src="polyfills.319c79dd175e50d0.js" type="module"></script><script src="main.
|
|
49
|
+
<script src="runtime.24fa95b7061d7056.js" type="module"></script><script src="polyfills.319c79dd175e50d0.js" type="module"></script><script src="main.fd9d76d279fa7d5e.js" type="module"></script>
|
|
50
50
|
|
|
51
51
|
</body></html>
|