gns3-server 3.0.0b3__py3-none-any.whl → 3.0.0rc1__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 (47) hide show
  1. {gns3_server-3.0.0b3.dist-info → gns3_server-3.0.0rc1.dist-info}/METADATA +31 -31
  2. {gns3_server-3.0.0b3.dist-info → gns3_server-3.0.0rc1.dist-info}/RECORD +47 -47
  3. {gns3_server-3.0.0b3.dist-info → gns3_server-3.0.0rc1.dist-info}/WHEEL +1 -1
  4. gns3server/api/routes/controller/projects.py +2 -0
  5. gns3server/appliances/aruba-arubaoscx.gns3a +39 -0
  6. gns3server/appliances/cisco-iou-l2.gns3a +2 -2
  7. gns3server/appliances/cisco-iou-l3.gns3a +2 -2
  8. gns3server/appliances/debian.gns3a +28 -0
  9. gns3server/appliances/fortiadc.gns3a +46 -4
  10. gns3server/appliances/fortianalyzer.gns3a +42 -0
  11. gns3server/appliances/fortiauthenticator.gns3a +58 -2
  12. gns3server/appliances/fortigate.gns3a +42 -0
  13. gns3server/appliances/fortimanager.gns3a +42 -0
  14. gns3server/appliances/fortiweb.gns3a +56 -0
  15. gns3server/appliances/juniper-junos-space.gns3a +3 -2
  16. gns3server/appliances/juniper-vmx-legacy.gns3a +1 -1
  17. gns3server/appliances/juniper-vmx-vcp.gns3a +1 -1
  18. gns3server/appliances/juniper-vmx-vfp.gns3a +2 -1
  19. gns3server/appliances/juniper-vqfx-pfe.gns3a +1 -1
  20. gns3server/appliances/juniper-vqfx-re.gns3a +2 -1
  21. gns3server/appliances/juniper-vrr.gns3a +1 -1
  22. gns3server/appliances/juniper-vsrx.gns3a +2 -1
  23. gns3server/appliances/pan-vm-fw.gns3a +26 -0
  24. gns3server/appliances/security-onion.gns3a +27 -3
  25. gns3server/appliances/ubuntu-docker.gns3a +1 -1
  26. gns3server/compute/docker/__init__.py +8 -2
  27. gns3server/compute/qemu/qemu_vm.py +26 -10
  28. gns3server/config_samples/gns3_server.conf +13 -3
  29. gns3server/configs/iou_l2_base_startup-config.txt +1 -1
  30. gns3server/configs/iou_l3_base_startup-config.txt +1 -1
  31. gns3server/controller/__init__.py +12 -7
  32. gns3server/controller/appliance_manager.py +7 -2
  33. gns3server/controller/export_project.py +9 -9
  34. gns3server/controller/import_project.py +3 -3
  35. gns3server/controller/project.py +8 -4
  36. gns3server/controller/snapshot.py +3 -8
  37. gns3server/controller/topology.py +30 -1
  38. gns3server/crash_report.py +1 -1
  39. gns3server/schemas/config.py +3 -0
  40. gns3server/static/web-ui/index.html +3 -3
  41. gns3server/static/web-ui/{main.f3840f9b1c0240e6.js → main.4185a8e61824af0d.js} +1 -1
  42. gns3server/utils/__init__.py +20 -0
  43. gns3server/utils/hostname.py +53 -0
  44. gns3server/version.py +1 -1
  45. {gns3_server-3.0.0b3.dist-info → gns3_server-3.0.0rc1.dist-info}/LICENSE +0 -0
  46. {gns3_server-3.0.0b3.dist-info → gns3_server-3.0.0rc1.dist-info}/entry_points.txt +0 -0
  47. {gns3_server-3.0.0b3.dist-info → gns3_server-3.0.0rc1.dist-info}/top_level.txt +0 -0
@@ -45,7 +45,7 @@ from ..nios.nio_tap import NIOTAP
45
45
  from ..base_node import BaseNode
46
46
  from ...utils.asyncio import monitor_process
47
47
  from ...utils.images import md5sum
48
- from ...utils import macaddress_to_int, int_to_macaddress
48
+ from ...utils import macaddress_to_int, int_to_macaddress, is_ipv6_enabled
49
49
  from ...utils.hostname import is_rfc1123_hostname_valid
50
50
 
51
51
  from gns3server.schemas.compute.qemu_nodes import Qemu, QemuPlatform
@@ -54,6 +54,12 @@ import logging
54
54
 
55
55
  log = logging.getLogger(__name__)
56
56
 
57
+ # forbidden additional options
58
+ FORBIDDEN_OPTIONS = {"-blockdev", "-drive", "-hda", "-hdb", "-hdc", "-hdd",
59
+ "-fsdev", "-virtfs", "-nic", "-netdev"}
60
+ FORBIDDEN_OPTIONS |= {"-" + opt for opt in FORBIDDEN_OPTIONS
61
+ if opt.startswith("-") and not opt.startswith("--")}
62
+
57
63
 
58
64
  class QemuVM(BaseNode):
59
65
  module_name = "qemu"
@@ -1855,14 +1861,17 @@ class QemuVM(BaseNode):
1855
1861
  if port:
1856
1862
  console_host = self._manager.port_manager.console_host
1857
1863
  if console_host == "0.0.0.0":
1858
- if socket.has_ipv6:
1859
- # to fix an issue with Qemu when IPv4 is not enabled
1860
- # see https://github.com/GNS3/gns3-gui/issues/2352
1861
- # FIXME: consider making this more global (not just for Qemu + SPICE)
1862
- console_host = "::"
1863
- else:
1864
- raise QemuError("IPv6 must be enabled in order to use the SPICE console")
1865
- return ["-spice", f"addr={console_host},port={port},disable-ticketing", "-vga", "qxl"]
1864
+ try:
1865
+ if is_ipv6_enabled():
1866
+ # to fix an issue with Qemu when IPv4 is not enabled
1867
+ # see https://github.com/GNS3/gns3-gui/issues/2352
1868
+ # FIXME: consider making this more global (not just for Qemu + SPICE)
1869
+ console_host = "::"
1870
+ except OSError as e:
1871
+ raise QemuError("Could not check if IPv6 is enabled: {}".format(e))
1872
+ return ["-spice",
1873
+ f"addr={console_host},port={port},disable-ticketing",
1874
+ "-vga", "qxl"]
1866
1875
  else:
1867
1876
  return []
1868
1877
 
@@ -2640,9 +2649,16 @@ class QemuVM(BaseNode):
2640
2649
  command.extend(self._tpm_options())
2641
2650
  if additional_options:
2642
2651
  try:
2643
- command.extend(shlex.split(additional_options))
2652
+ additional_opt_list = shlex.split(additional_options)
2644
2653
  except ValueError as e:
2645
2654
  raise QemuError(f"Invalid additional options: {additional_options} error {e}")
2655
+ allow_unsafe_options = self.manager.config.settings.Qemu.allow_unsafe_options
2656
+ if allow_unsafe_options is False:
2657
+ for opt in additional_opt_list:
2658
+ if opt in FORBIDDEN_OPTIONS:
2659
+ raise QemuError("Forbidden additional option: {}".format(opt))
2660
+ command.extend(additional_opt_list)
2661
+
2646
2662
  # avoiding mouse offset (see https://github.com/GNS3/gns3-server/issues/2335)
2647
2663
  if self._console_type == "vnc":
2648
2664
  command.extend(['-machine', 'usb=on', '-device', 'usb-tablet'])
@@ -34,7 +34,7 @@ enable_ssl = False
34
34
  certfile = /home/gns3/.config/GNS3/ssl/server.cert
35
35
  certkey = /home/gns3/.config/GNS3/ssl/server.key
36
36
 
37
- ; Path where devices images are stored
37
+ ; Path where binary images are stored
38
38
  images_path = /home/gns3/GNS3/images
39
39
 
40
40
  ; Additional paths to look for images
@@ -43,15 +43,20 @@ additional_images_paths = /opt/images;/mnt/disk1/images
43
43
  ; Path where user projects are stored
44
44
  projects_path = /home/gns3/GNS3/projects
45
45
 
46
- ; Path where user appliances are stored
46
+ ; Path where custom user appliances are stored
47
47
  appliances_path = /home/gns3/GNS3/appliances
48
48
 
49
- ; Path where custom device symbols are stored
49
+ ; Path where custom user symbols are stored
50
50
  symbols_path = /home/gns3/GNS3/symbols
51
51
 
52
52
  ; Path where custom configs are stored
53
53
  configs_path = /home/gns3/GNS3/configs
54
54
 
55
+ ; Path where files like built-in appliances and Docker resources are stored
56
+ ; The default path is the local user data directory
57
+ ; (Linux: "~/.local/share/GNS3", macOS: "~/Library/Application Support/GNS3", Windows: "%APPDATA%\GNS3")
58
+ ; resources_path = /home/gns3/GNS3/resources
59
+
55
60
  ; Default symbol theme
56
61
  ; Currently available themes are "Classic", Affinity-square-blue", "Affinity-square-red"
57
62
  ; "Affinity-square-gray", "Affinity-circle-blue", "Affinity-circle-red" and "Affinity-circle-gray"
@@ -102,6 +107,9 @@ default_nat_interface = vmnet10
102
107
  ; Enable the built-in templates
103
108
  enable_builtin_templates = True
104
109
 
110
+ ; Install built-in appliances
111
+ install_builtin_appliances = True
112
+
105
113
  ; check if hardware virtualization is used by other emulators (KVM, VMware or VirtualBox)
106
114
  hardware_virtualization_check = True
107
115
 
@@ -148,3 +156,5 @@ monitor_host = 127.0.0.1
148
156
  enable_hardware_acceleration = True
149
157
  ; Require hardware acceleration in order to start VMs
150
158
  require_hardware_acceleration = False
159
+ ; Allow unsafe additional command line options
160
+ allow_unsafe_options = False
@@ -15,7 +15,7 @@ no ip icmp rate-limit unreachable
15
15
  !
16
16
  ! due to some bugs with IOU, try to change the following line to 'ip cef' if your routing does not work
17
17
  no ip cef
18
- no ip domain-lookup
18
+ no ip domain lookup
19
19
  !
20
20
  !
21
21
  !
@@ -14,7 +14,7 @@ no ip icmp rate-limit unreachable
14
14
  !
15
15
  ! due to some bugs with IOU, try to change the following line to 'ip cef' if your routing does not work
16
16
  no ip cef
17
- no ip domain-lookup
17
+ no ip domain lookup
18
18
  !
19
19
  !
20
20
  ip tcp synwait-time 5
@@ -270,13 +270,18 @@ class Controller:
270
270
  log.error(f"Cannot read IOU license file '{iourc_path}': {e}")
271
271
  self._iou_license_settings["license_check"] = iou_config.license_check
272
272
 
273
- previous_version = controller_vars.get("version")
274
- log.info("Comparing controller version {} with config version {}".format(__version__, previous_version))
275
- if not previous_version or \
276
- parse_version(__version__.split("+")[0]) > parse_version(previous_version.split("+")[0]):
277
- self._appliance_manager.install_builtin_appliances()
278
- elif not os.listdir(self._appliance_manager.builtin_appliances_path()):
279
- self._appliance_manager.install_builtin_appliances()
273
+ # install the built-in appliances if needed
274
+ if Config.instance().settings.Server.install_builtin_appliances:
275
+ previous_version = controller_vars.get("version")
276
+ log.info("Comparing controller version {} with config version {}".format(__version__, previous_version))
277
+ builtin_appliances_path = self._appliance_manager.builtin_appliances_path()
278
+ if not previous_version or \
279
+ parse_version(__version__.split("+")[0]) > parse_version(previous_version.split("+")[0]):
280
+ self._appliance_manager.install_builtin_appliances()
281
+ elif not os.listdir(builtin_appliances_path):
282
+ self._appliance_manager.install_builtin_appliances()
283
+ else:
284
+ log.info(f"Built-in appliances are installed in '{builtin_appliances_path}'")
280
285
 
281
286
  self._appliance_manager.appliances_etag = controller_vars.get("appliances_etag")
282
287
  self._appliance_manager.load_appliances()
@@ -100,8 +100,13 @@ class ApplianceManager:
100
100
  Get the built-in appliance storage directory
101
101
  """
102
102
 
103
- appname = vendor = "GNS3"
104
- appliances_dir = os.path.join(platformdirs.user_data_dir(appname, vendor, roaming=True), "appliances")
103
+ resources_path = Config.instance().settings.Server.resources_path
104
+ if not resources_path:
105
+ appname = vendor = "GNS3"
106
+ resources_path = platformdirs.user_data_dir(appname, vendor, roaming=True)
107
+ else:
108
+ resources_path = os.path.expanduser(resources_path)
109
+ appliances_dir = os.path.join(resources_path, "appliances")
105
110
  if delete_first:
106
111
  shutil.rmtree(appliances_dir, ignore_errors=True)
107
112
  os.makedirs(appliances_dir, exist_ok=True)
@@ -39,7 +39,7 @@ async def export_project(
39
39
  temporary_dir,
40
40
  include_images=False,
41
41
  include_snapshots=False,
42
- keep_compute_id=False,
42
+ keep_compute_ids=False,
43
43
  allow_all_nodes=False,
44
44
  reset_mac_addresses=False,
45
45
  ):
@@ -54,9 +54,9 @@ async def export_project(
54
54
  :param temporary_dir: A temporary dir where to store intermediate data
55
55
  :param include_images: save OS images to the zip file
56
56
  :param include_snapshots: save snapshots to the zip file
57
- :param keep_compute_id: If false replace all compute id by local (standard behavior for .gns3project to make it portable)
58
- :param allow_all_nodes: Allow all nodes type to be include in the zip even if not portable
59
- :param reset_mac_addresses: Reset MAC addresses for every nodes.
57
+ :param keep_compute_ids: If false replace all compute IDs by local (standard behavior for .gns3project to make it portable)
58
+ :param allow_all_nodes: Allow all nodes type to be included in the zip even if not portable
59
+ :param reset_mac_addresses: Reset MAC addresses for each node.
60
60
  """
61
61
 
62
62
  # To avoid issue with data not saved we disallow the export of a running project
@@ -77,7 +77,7 @@ async def export_project(
77
77
  os.path.join(project._path, file),
78
78
  zstream,
79
79
  include_images,
80
- keep_compute_id,
80
+ keep_compute_ids,
81
81
  allow_all_nodes,
82
82
  temporary_dir,
83
83
  reset_mac_addresses,
@@ -193,7 +193,7 @@ def _is_exportable(path, include_snapshots=False):
193
193
 
194
194
 
195
195
  async def _patch_project_file(
196
- project, path, zstream, include_images, keep_compute_id, allow_all_nodes, temporary_dir, reset_mac_addresses
196
+ project, path, zstream, include_images, keep_compute_ids, allow_all_nodes, temporary_dir, reset_mac_addresses
197
197
  ):
198
198
  """
199
199
  Patch a project file (.gns3) to export a project.
@@ -225,7 +225,7 @@ async def _patch_project_file(
225
225
  if not allow_all_nodes and node["node_type"] in ["virtualbox", "vmware"]:
226
226
  raise ControllerError("Projects with a {} node cannot be exported".format(node["node_type"]))
227
227
 
228
- if not keep_compute_id:
228
+ if not keep_compute_ids:
229
229
  node["compute_id"] = "local" # To make project portable all node by default run on local
230
230
 
231
231
  if "properties" in node and node["node_type"] != "docker":
@@ -243,13 +243,13 @@ async def _patch_project_file(
243
243
  if value is None or value.strip() == "":
244
244
  continue
245
245
 
246
- if not keep_compute_id: # If we keep the original compute we can keep the image path
246
+ if not keep_compute_ids: # If we keep the original compute we can keep the image path
247
247
  node["properties"][prop] = os.path.basename(value)
248
248
 
249
249
  if include_images is True:
250
250
  images.append({"compute_id": compute_id, "image": value, "image_type": node["node_type"]})
251
251
 
252
- if not keep_compute_id:
252
+ if not keep_compute_ids:
253
253
  topology["topology"][
254
254
  "computes"
255
255
  ] = [] # Strip compute information because could contain secret info like password
@@ -40,7 +40,7 @@ Handle the import of project from a .gns3project
40
40
  """
41
41
 
42
42
 
43
- async def import_project(controller, project_id, stream, location=None, name=None, keep_compute_id=False,
43
+ async def import_project(controller, project_id, stream, location=None, name=None, keep_compute_ids=False,
44
44
  auto_start=False, auto_open=False, auto_close=True):
45
45
  """
46
46
  Import a project contain in a zip file
@@ -52,7 +52,7 @@ async def import_project(controller, project_id, stream, location=None, name=Non
52
52
  :param stream: A io.BytesIO of the zipfile
53
53
  :param location: Directory for the project if None put in the default directory
54
54
  :param name: Wanted project name, generate one from the .gns3 if None
55
- :param keep_compute_id: If true do not touch the compute id
55
+ :param keep_compute_ids: keep compute IDs unchanged
56
56
 
57
57
  :returns: Project
58
58
  """
@@ -126,7 +126,7 @@ async def import_project(controller, project_id, stream, location=None, name=Non
126
126
  drawing["drawing_id"] = str(uuid.uuid4())
127
127
 
128
128
  # Modify the compute id of the node depending of compute capacity
129
- if not keep_compute_id:
129
+ if not keep_compute_ids:
130
130
  # For some VM type we move them to the GNS3 VM if possible
131
131
  # unless it's a linux host without GNS3 VM
132
132
  if not sys.platform.startswith("linux") or controller.has_compute("vm"):
@@ -210,7 +210,11 @@ class Project:
210
210
  if os.path.exists(snapshot_dir):
211
211
  for snap in os.listdir(snapshot_dir):
212
212
  if snap.endswith(".gns3project"):
213
- snapshot = Snapshot(self, filename=snap)
213
+ try:
214
+ snapshot = Snapshot(self, filename=snap)
215
+ except ValueError:
216
+ log.error("Invalid snapshot file: {}".format(snap))
217
+ continue
214
218
  self._snapshots[snapshot.id] = snapshot
215
219
 
216
220
  # Create the project on demand on the compute node
@@ -491,7 +495,7 @@ class Project:
491
495
 
492
496
  if base_name is None:
493
497
  return None
494
- base_name = re.sub(r"[ ]", "", base_name)
498
+ base_name = re.sub(r"[ ]", "", base_name) # remove spaces in node name
495
499
  if base_name in self._allocated_node_names:
496
500
  base_name = re.sub(r"[0-9]+$", "{0}", base_name)
497
501
 
@@ -1087,7 +1091,7 @@ class Project:
1087
1091
  zstream,
1088
1092
  self,
1089
1093
  tmpdir,
1090
- keep_compute_id=True,
1094
+ keep_compute_ids=True,
1091
1095
  allow_all_nodes=True,
1092
1096
  reset_mac_addresses=reset_mac_addresses,
1093
1097
  )
@@ -1106,7 +1110,7 @@ class Project:
1106
1110
  str(uuid.uuid4()),
1107
1111
  f,
1108
1112
  name=name,
1109
- keep_compute_id=True
1113
+ keep_compute_ids=True
1110
1114
  )
1111
1115
 
1112
1116
  log.info(f"Project '{project.name}' duplicated in {time.time() - begin:.4f} seconds")
@@ -59,14 +59,9 @@ class Snapshot:
59
59
  + ".gns3project"
60
60
  )
61
61
  else:
62
- self._name = filename.split("_")[0]
62
+ self._name = filename.rsplit("_", 2)[0]
63
63
  datestring = filename.replace(self._name + "_", "").split(".")[0]
64
- try:
65
- self._created_at = (
66
- datetime.strptime(datestring, "%d%m%y_%H%M%S").replace(tzinfo=timezone.utc).timestamp()
67
- )
68
- except ValueError:
69
- self._created_at = datetime.now(timezone.utc)
64
+ self._created_at = (datetime.strptime(datestring, "%d%m%y_%H%M%S").replace(tzinfo=timezone.utc).timestamp())
70
65
  self._path = os.path.join(project.path, "snapshots", filename)
71
66
 
72
67
  @property
@@ -104,7 +99,7 @@ class Snapshot:
104
99
  with tempfile.TemporaryDirectory(dir=snapshot_directory) as tmpdir:
105
100
  # Do not compress the snapshots
106
101
  with aiozipstream.ZipFile(compression=zipfile.ZIP_STORED) as zstream:
107
- await export_project(zstream, self._project, tmpdir, keep_compute_id=True, allow_all_nodes=True)
102
+ await export_project(zstream, self._project, tmpdir, keep_compute_ids=True, allow_all_nodes=True)
108
103
  async with aiofiles.open(self.path, "wb") as f:
109
104
  async for chunk in zstream:
110
105
  await f.write(chunk)
@@ -35,6 +35,7 @@ from .drawing import Drawing
35
35
  from .node import Node
36
36
  from .link import Link
37
37
 
38
+ from gns3server.utils.hostname import is_ios_hostname_valid, is_rfc1123_hostname_valid, to_rfc1123_hostname, to_ios_hostname
38
39
  from gns3server.schemas.controller.topology import Topology
39
40
  from gns3server.schemas.compute.dynamips_nodes import DynamipsCreate
40
41
 
@@ -43,7 +44,7 @@ import logging
43
44
  log = logging.getLogger(__name__)
44
45
 
45
46
 
46
- GNS3_FILE_FORMAT_REVISION = 9
47
+ GNS3_FILE_FORMAT_REVISION = 10
47
48
 
48
49
 
49
50
  class DynamipsNodeValidation(DynamipsCreate):
@@ -186,6 +187,10 @@ def load_topology(path):
186
187
  if variables:
187
188
  topo["variables"] = [var for var in variables if var.get("name")]
188
189
 
190
+ # Version before GNS3 3.0
191
+ if topo["revision"] < 10:
192
+ topo = _convert_2_2_0(topo, path)
193
+
189
194
  try:
190
195
  _check_topology_schema(topo, path)
191
196
  except ControllerError as e:
@@ -201,6 +206,30 @@ def load_topology(path):
201
206
  return topo
202
207
 
203
208
 
209
+ def _convert_2_2_0(topo, topo_path):
210
+ """
211
+ Convert topologies from GNS3 2.2.x to 3.0
212
+
213
+ Changes:
214
+ * Convert Qemu and Docker node names to be a valid RFC1123 hostnames.
215
+ * Convert Dynamips and IOU node names to be a valid IOS hostnames.
216
+ """
217
+
218
+ topo["revision"] = 10
219
+
220
+ for node in topo.get("topology", {}).get("nodes", []):
221
+ if "properties" in node:
222
+ if node["node_type"] in ("qemu", "docker") and not is_rfc1123_hostname_valid(node["name"]):
223
+ new_name = to_rfc1123_hostname(node["name"])
224
+ log.info(f"Convert node name {node['name']} to {new_name} (RFC1123)")
225
+ node["name"] = new_name
226
+ if node["node_type"] in ("dynamips", "iou") and not is_ios_hostname_valid(node["name"] ):
227
+ new_name = to_ios_hostname(node["name"])
228
+ log.info(f"Convert node name {node['name']} to {new_name} (IOS)")
229
+ node["name"] = new_name
230
+ return topo
231
+
232
+
204
233
  def _convert_2_1_0(topo, topo_path):
205
234
  """
206
235
  Convert topologies from GNS3 2.1.x to 2.2
@@ -58,7 +58,7 @@ class CrashReport:
58
58
  Report crash to a third party service
59
59
  """
60
60
 
61
- DSN = "https://99870c759d1c1d62ceb091d59dbcfa78@o19455.ingest.us.sentry.io/38482"
61
+ DSN = "https://1ae6f3c9d64e75bf8ad39295723da722@o19455.ingest.us.sentry.io/38482"
62
62
  _instance = None
63
63
 
64
64
  def __init__(self):
@@ -69,6 +69,7 @@ class QemuSettings(BaseModel):
69
69
  monitor_host: str = "127.0.0.1"
70
70
  enable_hardware_acceleration: bool = True
71
71
  require_hardware_acceleration: bool = False
72
+ allow_unsafe_options: bool = False
72
73
  model_config = ConfigDict(validate_assignment=True, str_strip_whitespace=True)
73
74
 
74
75
 
@@ -126,6 +127,7 @@ class ServerSettings(BaseModel):
126
127
  appliances_path: str = "~/GNS3/appliances"
127
128
  symbols_path: str = "~/GNS3/symbols"
128
129
  configs_path: str = "~/GNS3/configs"
130
+ resources_path: str = None
129
131
  default_symbol_theme: BuiltinSymbolTheme = BuiltinSymbolTheme.affinity_square_blue
130
132
  allow_raw_images: bool = True
131
133
  auto_discover_images: bool = True
@@ -144,6 +146,7 @@ class ServerSettings(BaseModel):
144
146
  default_nat_interface: str = None
145
147
  allow_remote_console: bool = False
146
148
  enable_builtin_templates: bool = True
149
+ install_builtin_appliances: bool = True
147
150
  model_config = ConfigDict(validate_assignment=True, str_strip_whitespace=True, use_enum_values=True)
148
151
 
149
152
  @field_validator("additional_images_paths", mode="before")
@@ -36,7 +36,7 @@
36
36
  <body class="mat-app-background" oncontextmenu="return false;">
37
37
  <app-root></app-root>
38
38
  <!-- Global site tag (gtag.js) - Google Analytics -->
39
- <script async="" src="https://www.googletagmanager.com/gtag/js?id=G-5D6FZL9923"></script>
39
+ <script async="" src="https://www.googletagmanager.com/gtag/js?id=G-0BT7QQV1W1"></script>
40
40
  <script>
41
41
  window.dataLayer = window.dataLayer || [];
42
42
  function gtag() {
@@ -44,8 +44,8 @@
44
44
  }
45
45
  gtag('js', new Date());
46
46
 
47
- gtag('config', 'G-5D6FZL9923');
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.f3840f9b1c0240e6.js" type="module"></script>
49
+ <script src="runtime.24fa95b7061d7056.js" type="module"></script><script src="polyfills.319c79dd175e50d0.js" type="module"></script><script src="main.4185a8e61824af0d.js" type="module"></script>
50
50
 
51
51
  </body></html>