YueGuobin be62e8c022
feat: keep Docker images on computes consistent with the controller host
Only relying on the image name lets a moved tag (e.g. a newer :latest)
silently serve stale content from a compute that already has an image
under the same name. When creating a Docker node, the controller now
pins the image id (Id from the Docker daemon on the controller host)
into the create payload. A compute holding a different image under the
same tag reports the image as missing, which routes it through the
image sync added by the previous commit and re-aligns the tag.

No new template fields or database changes: the controller host daemon
remains the source of truth and the pin is resolved per creation. When
the image is not available on the controller host the pin is omitted
and behavior is unchanged (the compute pulls from the repository).
2026-09-14 00:55:18 +08:00

103 lines
4.9 KiB
Python

#
# Copyright (C) 2020 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from pydantic import BaseModel, Field, field_validator
from typing import Optional, List
from uuid import UUID
from ..common import NodeStatus, CustomAdapter, ConsoleType, AuxType, ExtraConfig
class DockerBase(BaseModel):
"""
Common Docker node properties.
"""
@field_validator("start_command", "environment", "extra_hosts", "startup_config_content", mode="before")
@classmethod
def _empty_string_to_none(cls, value):
# Web clients serialize empty form fields as "" while unset values are
# stored as None on the node: normalize before the update diff runs,
# otherwise every full PUT would see a phantom change and recreate
# the container for nothing.
return value or None
@field_validator("console_http_path", mode="before")
@classmethod
def _empty_string_to_root_path(cls, value):
# the canonical "no path" value is "/" (the creation default)
return value or "/"
name: str
image: str = Field(..., description="Docker image name")
node_id: Optional[UUID] = None
console: Optional[int] = Field(None, gt=0, le=65535, description="Console TCP port")
console_type: Optional[ConsoleType] = Field(None, description="Console type")
console_resolution: Optional[str] = Field(None, pattern="^[0-9]+x[0-9]+$", description="Console resolution for VNC")
console_http_port: Optional[int] = Field(None, description="Internal port in the container for the HTTP server")
console_http_path: Optional[str] = Field(None, description="Path of the web interface")
aux: Optional[int] = Field(None, gt=0, le=65535, description="Auxiliary TCP port")
aux_type: Optional[AuxType] = Field(None, description="Auxiliary console type")
usage: Optional[str] = Field(None, description="How to use the Docker container")
start_command: Optional[str] = Field(None, description="Docker CMD entry")
adapters: Optional[int] = Field(None, ge=0, le=99, description="Number of adapters")
mac_address: Optional[str] = Field(None, description="Base MAC address", pattern="^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$")
environment: Optional[str] = Field(None, description="Docker environment variables")
extra_hosts: Optional[str] = Field(None, description="Docker extra hosts (added to /etc/hosts)")
extra_volumes: Optional[List[str]] = Field(None, description="Additional directories to make persistent")
extra_configs: Optional[List[ExtraConfig]] = Field(None, description="Configuration files injected into the container (bind-mounted read-only)")
startup_config_content: Optional[str] = Field(
None, description="Startup-config content (IOL runner images: materialized into the node's NVRAM at start)"
)
memory: Optional[int] = Field(None, ge=0, description="Maximum amount of memory the container can use in MB")
cpus: Optional[float] = Field(None, ge=0, description="Maximum amount of CPU resources the container can use")
custom_adapters: Optional[List[CustomAdapter]] = Field(None, description="Custom adapters")
class DockerCreate(DockerBase):
"""
Properties to create a Docker node.
"""
application_id: Optional[int] = Field(
None, ge=1, le=1022, description="IOL application ID for iol-runner images (allocated by the controller)"
)
image_digest: Optional[str] = Field(
None, pattern=r"^sha256:[a-f0-9]{64}$",
description="Image id the controller expects for 'image' (sha256:<hex>, resolved from the Docker "
"daemon on the controller host). When set and the compute holds a different image "
"under the same tag, the image is reported as missing so the controller re-syncs it"
)
class DockerUpdate(DockerBase):
"""
Properties to update a Docker node.
"""
name: Optional[str] = None
image: Optional[str] = Field(None, description="Docker image name")
class Docker(DockerBase):
container_id: str = Field(
..., min_length=12, max_length=64, pattern="^[a-f0-9]+$", description="Docker container ID (read only)"
)
project_id: UUID = Field(..., description="Project ID")
node_directory: str = Field(..., description="Path to the node working directory (read only)")
status: NodeStatus = Field(..., description="Container status (read only)")