Merge branch '3.1' into 3.1

This commit is contained in:
Jeremy Grossmann authored and GitHub committed 2026-10-19 18:11:29 +02:00
commit 68b5e89bfa
51 files changed
+3155 -183

No files matched your search

+78 -11
View File
@@ -20,18 +20,33 @@ import copy
import html
import logging
import os
import re
import uuid
from ..config import Config
from ..utils import int_to_macaddress, macaddress_to_int
from ..utils.application_id import is_iol_runner_environment
from ..utils.images import images_directories
from ..utils.vxlan import vxlan_device_name
from .controller_error import ComputeConflictError, ComputeError, ControllerError, ControllerTimeoutError
from .node_types import BUILTIN_NODE_TYPES
from .ports.port_factory import DynamipsPortFactory, PortFactory, StandardPortFactory
log = logging.getLogger(__name__)
# The compute's error when it could not create a VXLAN tunnel device — the
# wording is a same-repo contract with KernelDatapathMixin._kernel_error
# (only computes running this codebase carry VXLAN links: the capability
# gate keeps everything else on the relay), and the captured device name
# identifies the link (a pure function of link id + node id).
_VXLAN_CONFLICT_RE = re.compile(r"Could not create VXLAN device (?P<device>\S+)")
# Node-start self-heal rounds over a refused VXLAN VNI: the first re-wires
# the link on a fresh VNI, the second degrades it to the relay (whose NIOs
# cannot hit the conflict) — a start must not fail permanently because
# another link already registered the VNI on the shared dstport socket.
_VXLAN_START_HEALS = 2
# Image properties of a node, mapped by node type. The value is the image
# "type" used by the image manager (see utils.images). Docker images are not
@@ -43,6 +58,7 @@ IMAGE_PROPERTIES_BY_NODE_TYPE = {
"hdc_disk_image": "qemu",
"hdd_disk_image": "qemu",
"cdrom_image": "qemu",
"cdrom2_image": "qemu",
"initrd": "qemu",
"kernel_image": "qemu",
"bios_image": "qemu",
@@ -885,6 +901,7 @@ class Node:
"hdc_disk_image",
"hdd_disk_image",
"cdrom_image",
"cdrom2_image",
"bios_image",
"initrd",
"kernel_image",
@@ -955,18 +972,32 @@ class Node:
f"Cannot start node '{self._name}': {len(failed_links)} deferred link(s) "
"could not be restored. Please try again."
)
try:
# For IOU: we need to send the licence everytime we start a node
if self.node_type == "iou":
license_check = self._project.controller.iou_license.get("license_check", True)
iourc_content = self._project.controller.iou_license.get("iourc_content", None)
await self.post(
"/start", timeout=240, data={"license_check": license_check, "iourc_content": iourc_content}
for heal in range(_VXLAN_START_HEALS + 1):
try:
await self._start_on_compute(data)
break
except asyncio.TimeoutError:
raise ControllerTimeoutError(f"Timeout when starting {self._name}")
except ComputeError as e:
conflicted = self._vxlan_conflicted_link(e)
if conflicted is None or heal == _VXLAN_START_HEALS:
raise
degrade = heal > 0 # a second conflict takes the relay
log.warning(
"Node '%s': start refused over a VXLAN VNI conflict on link %s — %s and retrying",
self._name,
conflicted.id,
"degrading the link to the uBridge relay" if degrade else "re-wiring the link on a fresh VNI",
)
else:
await self.post("/start", data=data, timeout=240)
except TimeoutError:
raise ControllerTimeoutError(f"Timeout when starting {self._name}")
# The refused start leaves the node half-started (the
# emulator process runs, the wiring partial) — stop it
# first: a start on a running container early-returns
# without wiring anything.
await self.stop()
if await self._project._redispatch_vxlan_links([conflicted], downgrade=degrade) and not degrade:
# The fresh VNI was already refused before the start
# could even retry — degrade the pair to the relay now.
await self._project._redispatch_vxlan_links([conflicted], downgrade=True)
self._confirm_status("started")
# A started node may have brought kernel anchors into existence that
# a link needs to finish wiring (the Ethernet-switch fast path joins
@@ -974,6 +1005,42 @@ class Node:
for link in self._links:
await link.node_started(self)
async def _start_on_compute(self, data=None):
"""
The compute-side start request (IOU carries its licence along).
"""
# For IOU: we need to send the licence everytime we start a node
if self.node_type == "iou":
license_check = self._project.controller.iou_license.get("license_check", True)
iourc_content = self._project.controller.iou_license.get("iourc_content", None)
await self.post(
"/start", timeout=240, data={"license_check": license_check, "iourc_content": iourc_content}
)
else:
await self.post("/start", data=data, timeout=240)
def _vxlan_conflicted_link(self, error):
"""
The link whose VXLAN device a failed node start could not create,
or None for every other failure.
The compute's error names the device it could not create and the
name is a pure function of (link id, node id), so it identifies the
link with no extra API surface. An unmatched message — another
failure, a future wording change — simply leaves the error to
surface unchanged, which is exactly the behaviour without the heal.
"""
match = _VXLAN_CONFLICT_RE.search(str(error))
if match is None:
return None
device = match.group("device")
for link in self._links:
if vxlan_device_name(link.id, self.id) == device and any(nio.get("vxlan") for nio in link._link_data):
return link
return None
async def stop(self, strict=False):
"""
Stop a node