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

+35 -4
View File
@@ -19,7 +19,7 @@
from enum import Enum
from typing import Annotated, List, Literal, Optional, Union
from uuid import UUID
from pydantic import AnyUrl, BaseModel, Discriminator, EmailStr, Field, Tag
from pydantic import AnyUrl, BaseModel, Discriminator, EmailStr, Field, Tag, model_validator
from ..common import ExtraConfig
@@ -567,14 +567,23 @@ class QemuPropertiesV8(BaseModel):
title='Optional define the disk boot priory. Refer to -boot option in qemu manual for more details.',
)
kernel_command_line: Optional[str] = Field(None, title='Command line parameters send to the kernel')
kvm: Optional[Kvm] = Field(None, title='KVM requirements')
options: Optional[str] = Field(None, title='Optional additional qemu command line options')
cpu_throttling: Optional[Annotated[float, Field(ge=0.0, le=100.0)]] = Field(None, title='Throttle the CPU')
cpu_throttling: Optional[Annotated[int, Field(ge=0, le=800)]] = Field(None, title='Throttle the CPU')
tpm: Optional[bool] = Field(None, title='Enable the Trusted Platform Module (TPM)')
uefi: Optional[bool] = Field(None, title='Enable the UEFI boot mode')
on_close: Optional[QemuOnClose] = Field(None, title='Action to execute on the VM is closed')
process_priority: Optional[QemuProcessPriority] = Field(None, title='Process priority for QEMU')
_V8_PROPERTIES_MODELS = {
TemplateType.qemu: QemuPropertiesV8,
TemplateType.dynamips: DynamipsPropertiesV8,
TemplateType.iou: IouPropertiesV8,
TemplateType.docker: DockerPropertiesV8,
}
class TemplateSetting(BaseModel):
"""Emulator settings configuration (v8)"""
@@ -587,12 +596,34 @@ class TemplateSetting(BaseModel):
title='Properties for the template'
)
@model_validator(mode='before')
@classmethod
def _validate_template_properties(cls, data):
"""
Validate template_properties against the model matching template_type.
The template_type discriminator lives at the settings level (not inside
template_properties), so the union cannot be discriminated by pydantic
alone and would misroute properties between the per-type models.
"""
if isinstance(data, dict):
# work on a copy: replacing template_properties with the validated
# model must not mutate the caller's data
data = data.copy()
template_type = data.get("template_type")
template_properties = data.get("template_properties")
model = _V8_PROPERTIES_MODELS.get(template_type)
if model is not None and isinstance(template_properties, dict):
data["template_properties"] = model.model_validate(template_properties)
return data
class ApplianceVersionV8(BaseModel):
"""Appliance version definition (v8)"""
name: str = Field(..., title='Name of the version')
settings: Optional[str] = Field(None, title='Template settings to use to run the version')
idlepc: Optional[str] = Field(None, pattern=r'^0x[0-9a-f]{8}')
category: Optional[Category] = Field(None, title='Category of the version')
installation_instructions: Optional[str] = Field(None, title='Optional installation instructions for the version')
usage: Optional[str] = Field(None, title='Optional instructions about using the version')
@@ -633,7 +664,7 @@ class ApplianceV1_6(BaseModel):
usage: Optional[str] = Field(None, title='How to use the appliance')
symbol: Optional[str] = Field(None, title='An optional symbol for the appliance')
netmiko_device_type: Optional[str] = Field(
None, title='Device type for Netmiko-based automation tools', pattern=r'^[a-z0-9_]+$'
None, title='Device type for Netmiko-based automation tools', pattern=r'^[a-z0-9_]+$|^$'
)
first_port_name: Optional[str] = Field(None, title='Optional name of the first networking port example: eth0')
port_name_format: Optional[str] = Field(None, title='Optional formating of the networking port example: eth{0}')
@@ -689,7 +720,7 @@ class ApplianceV8(BaseModel):
default_password: Optional[str] = Field(None, title='Default password for the appliance')
symbol: Optional[str] = Field(None, title='An optional symbol for the appliance')
netmiko_device_type: Optional[str] = Field(
None, title='Device type for Netmiko-based automation tools', pattern=r'^[a-z0-9_]+$'
None, title='Device type for Netmiko-based automation tools', pattern=r'^[a-z0-9_]+$|^$'
)
tags: Optional[List[str]] = Field(None, title='User-defined metadata tags for the appliance')
settings: List[TemplateSetting] = Field(..., title='Settings for running the appliance')
+1 -1
View File
@@ -119,7 +119,7 @@ class NodeBase(BaseModel):
netmiko_device_type: Optional[str] = Field(
None,
description="Device type for Netmiko-based automation tools, overrides the template value",
pattern=r"^[a-z0-9_]+$",
pattern=r"^[a-z0-9_]+$|^$",
)
aux: Optional[int] = Field(None, gt=0, le=65535, description="Auxiliary console TCP port")
aux_type: Optional[ConsoleType] = None
@@ -51,7 +51,7 @@ class TemplateBase(BaseModel):
netmiko_device_type: Optional[str] = Field(
None,
description="Device type for Netmiko-based automation tools (e.g. 'cisco_xr' or 'nokia_srl')",
pattern=r"^[a-z0-9_]+$",
pattern=r"^[a-z0-9_]+$|^$",
)
tags: Optional[List[str]] = Field(
default_factory=list,