fix: address appliance v8 install review findings

- install: resolve the image directory from the version's settings type and
  skip image handling for docker appliances; guard appliance.images
- appliance schema: validate template_properties against template_type,
  align cpu_throttling with the qemu template, add kvm and version idlepc
- conversion: map IOU image to path, kvm disable to accel=tcg, inherit only
  same-type default settings, symbol fallback from the effective category,
  template_properties cannot override structural fields
- allow clearing netmiko_device_type with an empty string
- download the template symbol regardless of the level it is defined at and
  give qemu guests a default symbol
This commit is contained in:
YueGuobin committed 2026-08-21 21:41:42 +08:00
1 parent 435aa4c257
commit 1c68a52856
10 files changed
+603 -39

No files matched your search

+21 -13
View File
@@ -174,7 +174,7 @@ class ApplianceManager:
version_images = version.get("images")
if version_images:
for appliance_key, appliance_file in version_images.items():
for image in appliance.images:
for image in appliance.images or []:
if appliance_file == image.get("filename"):
image_checksum = image.get("md5sum")
image_in_db = await images_repo.get_image_by_checksum(image_checksum)
@@ -225,12 +225,15 @@ class ApplianceManager:
from . import Controller
# downloading missing custom symbol for this appliance
if appliance.symbol and not appliance.symbol.startswith(":/symbols/"):
destination_path = os.path.join(Controller.instance().symbols.symbols_path(), appliance.symbol)
template_data = ApplianceToTemplate().new_template(appliance.asdict(), version, "local") # FIXME: "local"
# download the custom symbol used by the template if it is missing;
# the symbol can be defined at the appliance, version or settings level
symbol = template_data.get("symbol")
if symbol and not symbol.startswith(":/symbols/"):
destination_path = os.path.join(Controller.instance().symbols.symbols_path(), symbol)
if not os.path.exists(destination_path):
await self._download_symbol(appliance.symbol, destination_path)
return ApplianceToTemplate().new_template(appliance.asdict(), version, "local") # FIXME: "local"
await self._download_symbol(symbol, destination_path)
return template_data
async def install_appliances_from_image(
self,
@@ -290,11 +293,14 @@ class ApplianceManager:
if not appliance.versions:
raise ControllerBadRequestError(message=f"Appliance '{appliance_id}' do not have versions")
image_dir = default_images_directory(appliance.type)
for appliance_version_info in appliance.versions:
if appliance_version_info.get("name") == version:
try:
await self._find_appliance_version_images(appliance, appliance_version_info, images_repo, image_dir)
template_type = ApplianceToTemplate().get_template_type(appliance.asdict(), appliance_version_info)
if template_type != "docker":
# docker appliances have no image files to find or download
image_dir = default_images_directory(template_type)
await self._find_appliance_version_images(appliance, appliance_version_info, images_repo, image_dir)
except InvalidImageError as e:
raise ControllerError(message=f"Image error: {e}")
template_data = await self._appliance_to_template(appliance, appliance_version_info)
@@ -363,12 +369,14 @@ class ApplianceManager:
category = appliance["category"]
if category == "guest":
if appliance.get("registry_version", 0) >= 8:
# registry version 8: look for a Docker settings set instead of a top-level block
settings_types = [
settings.get("template_type") for settings in appliance.get("settings") or []
]
if "docker" in settings_types:
# registry version 8: the emulator type comes from the default
# settings set (or the only one present), not a top-level block
settings = appliance.get("settings") or []
selected = next((s for s in settings if s.get("default")), settings[0] if settings else None)
if selected and selected.get("template_type") == "docker":
return controller.symbols.get_default_symbol("docker_guest", symbol_theme)
if selected:
return controller.symbols.get_default_symbol("qemu_guest", symbol_theme)
elif "docker" in appliance:
return controller.symbols.get_default_symbol("docker_guest", symbol_theme)
elif "qemu" in appliance:
+48 -7
View File
@@ -168,9 +168,23 @@ class ApplianceToTemplate:
elif appliance_config.get(prop) is not None:
properties[prop] = appliance_config[prop]
if properties.get("category") == "multilayer_switch":
category_before_remap = properties.get("category")
if category_before_remap == "multilayer_switch":
properties["category"] = "switch"
if settings["template_type"] == "qemu":
# kvm is not a valid template property: convert it to the
# equivalent qemu options like for registry versions 1-6
kvm = properties.pop("kvm", None) or "allow"
options = properties.get("options") or ""
if kvm == "disable" and "-machine accel=tcg" not in options:
options += " -machine accel=tcg"
properties["options"] = options.strip()
# template_properties must not override the structural fields
for reserved in ("template_type", "compute_id", "version"):
properties.pop(reserved, None)
new_template.update(properties)
if "tags" in appliance_config:
new_template["tags"] = appliance_config.get("tags")
@@ -179,8 +193,8 @@ class ApplianceToTemplate:
new_template["netmiko_device_type"] = appliance_config["netmiko_device_type"]
if not new_template.get("symbol"):
# apply a default symbol based on the category and template type
if appliance_config["category"] == "guest":
# apply a default symbol based on the effective category and template type
if category_before_remap == "guest":
if settings["template_type"] == "docker":
new_template["symbol"] = ":/symbols/docker_guest.svg"
else:
@@ -192,13 +206,38 @@ class ApplianceToTemplate:
"multilayer_switch": ":/symbols/multilayer_switch.svg",
"firewall": ":/symbols/firewall.svg",
}
new_template["symbol"] = symbols.get(appliance_config["category"])
new_template["symbol"] = symbols.get(category_before_remap)
if version and version.get("images"):
new_template.update(version["images"])
if settings["template_type"] == "iou":
# IOU templates take the image path, not an image name
new_template["path"] = version["images"].get("image")
else:
new_template.update(version["images"])
if version and settings["template_type"] == "dynamips" and version.get("idlepc"):
# settings level idlepc takes precedence over the version level
new_template.setdefault("idlepc", version["idlepc"])
return new_template
def get_template_type(self, appliance_config, version):
"""
Returns the template type of the settings set used to install the given
version: for registry versions 1-6 it comes from the emulator block, for
version 8 from the settings set selected for the version.
"""
if appliance_config.get("registry_version", 0) >= 8:
return self._select_v8_settings(appliance_config, version)["template_type"]
if "iou" in appliance_config:
return "iou"
if "dynamips" in appliance_config:
return "dynamips"
if "docker" in appliance_config:
return "docker"
return "qemu"
def _select_v8_settings(self, appliance_config, version):
"""
Selects the settings set to use: the one referenced by the version,
@@ -235,13 +274,15 @@ class ApplianceToTemplate:
"""
Merges the template properties of the selected settings with the default
settings properties, unless inheritance is disabled or the default set
is selected.
is selected. Only a default set of the same emulator type is inherited
from, so properties of a different type never pollute the template.
"""
properties = {}
if not settings.get("default") and settings.get("inherit_default_properties", True):
for other_settings in appliance_config.get("settings") or []:
if other_settings.get("default"):
if other_settings.get("default") and other_settings.get("template_type") == settings["template_type"]:
properties.update(other_settings.get("template_properties") or {})
break
properties.update(settings.get("template_properties") or {})
return properties