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.

Files changed (40) hide show
  1. gns3_server-3.0.5.dist-info/METADATA +202 -0
  2. {gns3_server-3.0.4.dist-info → gns3_server-3.0.5.dist-info}/RECORD +38 -33
  3. {gns3_server-3.0.4.dist-info → gns3_server-3.0.5.dist-info}/WHEEL +1 -1
  4. gns3server/api/routes/compute/cloud_nodes.py +1 -1
  5. gns3server/api/routes/controller/images.py +0 -5
  6. gns3server/api/routes/controller/links.py +52 -1
  7. gns3server/appliances/almalinux.gns3a +30 -0
  8. gns3server/appliances/arista-veos.gns3a +15 -15
  9. gns3server/appliances/aruba-arubaoscx.gns3a +39 -0
  10. gns3server/appliances/asterfusion-vAsterNOS-campus.gns3a +50 -0
  11. gns3server/appliances/asterfusion-vAsterNOS.gns3a +1 -1
  12. gns3server/appliances/centos-cloud.gns3a +24 -54
  13. gns3server/appliances/exos.gns3a +13 -0
  14. gns3server/appliances/fedora-cloud.gns3a +30 -0
  15. gns3server/appliances/infix.gns3a +48 -5
  16. gns3server/appliances/juniper-vJunos-router.gns3a +75 -0
  17. gns3server/appliances/nethsecurity.gns3a +44 -0
  18. gns3server/appliances/oracle-linux-cloud.gns3a +31 -1
  19. gns3server/appliances/rhel.gns3a +57 -1
  20. gns3server/appliances/rockylinux.gns3a +15 -0
  21. gns3server/compute/builtin/nodes/nat.py +1 -1
  22. gns3server/compute/docker/__init__.py +8 -9
  23. gns3server/compute/docker/docker_vm.py +2 -1
  24. gns3server/compute/qemu/qemu_vm.py +69 -28
  25. gns3server/controller/compute.py +4 -3
  26. gns3server/controller/gns3vm/virtualbox_gns3_vm.py +55 -28
  27. gns3server/crash_report.py +1 -1
  28. gns3server/disks/OVMF_CODE_4M.fd +0 -0
  29. gns3server/disks/OVMF_VARS_4M.fd +0 -0
  30. gns3server/schemas/__init__.py +1 -1
  31. gns3server/schemas/controller/links.py +21 -0
  32. gns3server/static/web-ui/index.html +1 -1
  33. gns3server/static/web-ui/main.fd9d76d279fa7d5e.js +1 -0
  34. gns3server/utils/images.py +10 -0
  35. gns3server/version.py +2 -2
  36. gns3_server-3.0.4.dist-info/METADATA +0 -869
  37. gns3server/static/web-ui/main.87178dd64c9c79ba.js +0 -1
  38. {gns3_server-3.0.4.dist-info → gns3_server-3.0.5.dist-info}/entry_points.txt +0 -0
  39. {gns3_server-3.0.4.dist-info → gns3_server-3.0.5.dist-info/licenses}/LICENSE +0 -0
  40. {gns3_server-3.0.4.dist-info → gns3_server-3.0.5.dist-info}/top_level.txt +0 -0
@@ -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.3/x86_64/product-software attach/customize rhel-cloud-init.iso and start.\nusername: cloud-user\npassword: redhat",
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 body and len(body):
179
- if response.headers.get('CONTENT-TYPE') == 'application/json':
180
- body = json.loads(body.decode("utf-8"))
181
- else:
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 docker hub")
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 Hub, "
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 docker hub")
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(f"Timeout while pulling Docker image '{image}' from docker hub")
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(f"Image '{self._image}' is missing, pulling it from Docker hub...")
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{self.read_stdout()}"},
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
- # get the OVMF firmware from the images directory
2292
- ovmf_firmware_path = self.manager.get_abs_image_path("OVMF_CODE.fd")
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
- ovmf_vars_node_path = os.path.join(self.working_dir, "OVMF_VARS.fd")
2298
- if not os.path.exists(ovmf_vars_node_path):
2299
- try:
2300
- shutil.copyfile(self.manager.get_abs_image_path("OVMF_VARS.fd"), ovmf_vars_node_path)
2301
- except OSError as e:
2302
- raise QemuError("Cannot copy OVMF_VARS.fd file to the node working directory: {}".format(e))
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
- pci_devices = 4 + len(self._ethernet_adapters) # 4 PCI devices are use by default by qemu
2400
- pci_bridges = math.floor(pci_devices / 32)
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
- if pci_bridges >= 1:
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()
@@ -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, event, project_id=project_id, compute_id=self.id
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
- self.ip_address = await self._get_ip(interface_number, api_port)
365
- log.info("GNS3 VM has been started with IP {}".format(self.ip_address))
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 _get_ip(self, hostonly_interface_number, api_port):
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 = 300
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:
@@ -58,7 +58,7 @@ class CrashReport:
58
58
  Report crash to a third party service
59
59
  """
60
60
 
61
- DSN = "https://0d64280ffb5ae409d448f255b9956a88@o19455.ingest.us.sentry.io/38482"
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
@@ -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.87178dd64c9c79ba.js" type="module"></script>
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>