feat: update terminal support messaging and enhance guest agent provisioning logic
This commit is contained in:
+2
-2
@@ -71,8 +71,8 @@ class GuestSession:
|
||||
if connection is not None:
|
||||
connection.close()
|
||||
raise GuestAgentUnavailableError(
|
||||
"guest agent is unavailable or not ready; install it with POST /vms/{vm_id}/agent/install "
|
||||
"while stopped, then start the VM (vsock-capable kernel required)"
|
||||
"guest agent is unavailable or not ready; wait for guest startup, or restart the VM "
|
||||
"to apply automatic vsock guest-agent setup (vsock-capable kernel required)"
|
||||
) from error
|
||||
|
||||
def execute(self, command: str, *, sudo: bool, timeout_seconds: float) -> ExecResult:
|
||||
|
||||
@@ -38,8 +38,8 @@ class GuestTerminal(GuestSession):
|
||||
except (OSError, EOFError, ProtocolError) as error:
|
||||
self.close()
|
||||
raise GuestAgentUnavailableError(
|
||||
"Interactive terminal support is unavailable. Stop this VM, use Install / update terminal support "
|
||||
"in its Sessions view, then start it and launch a new session."
|
||||
"Interactive terminal support is unavailable. Restart this VM to apply automatic guest-agent setup, "
|
||||
"then wait for the agent to start and launch a new session."
|
||||
) from error
|
||||
|
||||
async def _receive_exact(self, size: int) -> bytes:
|
||||
|
||||
+40
-11
@@ -304,10 +304,12 @@ class ImageStore:
|
||||
return profile
|
||||
|
||||
def provision_guest_agent(self, disk: Path, vm_id: str, username: str, *, install: bool = False) -> bool:
|
||||
"""Configure new templates, or explicitly install on a proven-offline disk.
|
||||
"""Ensure every guest has its vsock agent before booting its private disk.
|
||||
|
||||
The lifecycle lock and confirmed VMM exit are required. A checked sparse
|
||||
copy preserves the original disk on failure and makes retries atomic.
|
||||
Return False only when the managed files and service are already current.
|
||||
An explicit install request always stages and rechecks the installation.
|
||||
"""
|
||||
parse_username(username)
|
||||
if re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,80}", vm_id) is None:
|
||||
@@ -319,13 +321,8 @@ class ImageStore:
|
||||
raise UvmError("private VM disk is unsafe for guest agent installation")
|
||||
with tempfile.TemporaryDirectory(prefix="uvm-guest-agent-") as name:
|
||||
files = Path(name)
|
||||
if not install:
|
||||
marker = self._read_guest_file(disk, "/etc/uvm-image", files / "marker",
|
||||
required=False, regular_only=True)
|
||||
if marker is None or marker.path.read_text(encoding="utf-8") not in {
|
||||
"uvm-ubuntu-jammy-v4\n", "uvm-ubuntu-jammy-v5\n",
|
||||
}:
|
||||
return False
|
||||
if not install and self._guest_agent_current(disk, files, vm_id, username):
|
||||
return False
|
||||
descriptor, candidate = tempfile.mkstemp(prefix=".rootfs-agent-", suffix=".ext4", dir=disk.parent)
|
||||
os.close(descriptor)
|
||||
temporary = Path(candidate)
|
||||
@@ -352,16 +349,48 @@ class ImageStore:
|
||||
except (OSError, UnicodeError) as error:
|
||||
raise UvmError(f"could not provision guest agent: {error}") from error
|
||||
|
||||
@staticmethod
|
||||
def _agent_payloads(vm_id: str, username: str) -> dict[str, bytes]:
|
||||
payloads = guest_agent_files()
|
||||
payloads["/etc/uvm-agent.json"] = (json.dumps({"vm_id": vm_id, "username": username}) + "\n").encode("utf-8")
|
||||
return payloads
|
||||
|
||||
def _guest_agent_current(self, disk: Path, files: Path, vm_id: str, username: str) -> bool:
|
||||
"""Read-only fast path; changed installations are checked again after journal replay."""
|
||||
payloads = self._agent_payloads(vm_id, username)
|
||||
for index, (path, content) in enumerate(payloads.items()):
|
||||
existing = self._agent_file(disk, path, files / f"current-{index}")
|
||||
mode = 0o600 if path.endswith(".json") else 0o644
|
||||
if (existing is None or (existing.mode, existing.gid) != (mode, 0)
|
||||
or existing.path.read_bytes() != content):
|
||||
return False
|
||||
manifest = self._agent_file(disk, "/etc/uvm-agent-files.json", files / "current-manifest")
|
||||
if manifest is None or (manifest.mode, manifest.gid) != (0o600, 0):
|
||||
return False
|
||||
try:
|
||||
hashes = json.loads(manifest.path.read_bytes())
|
||||
except (ValueError, UnicodeError):
|
||||
return False
|
||||
if hashes != {path: hashlib.sha256(content).hexdigest() for path, content in payloads.items()}:
|
||||
return False
|
||||
service = self._runner.run((
|
||||
"debugfs", "-R", "stat /etc/systemd/system/multi-user.target.wants/uvm-agent.service", disk,
|
||||
), capture=True)
|
||||
return (re.search(r"Type:\s+symlink\b", service.stdout) is not None
|
||||
and 'Fast link dest: "/etc/systemd/system/uvm-agent.service"' in service.stdout)
|
||||
|
||||
def _install_agent_files(self, disk: Path, files: Path, vm_id: str, username: str) -> None:
|
||||
for binary in ("/usr/lib/systemd/systemd", "/usr/bin/apt-get"):
|
||||
result = self._runner.run(("debugfs", "-R", f"stat {binary}", disk), capture=True)
|
||||
if re.search(r"Type:\s+regular\b", result.stdout) is None:
|
||||
raise UvmError("guest agent installation requires a systemd/apt image (Ubuntu 22.04 or newer)")
|
||||
raise UvmError(
|
||||
"automatic vsock guest-agent setup requires a systemd/apt image (Ubuntu 22.04 or newer); "
|
||||
"use the default Ubuntu rootfs or a compatible custom image"
|
||||
)
|
||||
for directory in ("/etc", "/usr", "/usr/lib", "/usr/lib/uvm", "/etc/systemd",
|
||||
"/etc/systemd/system", "/etc/systemd/system/multi-user.target.wants"):
|
||||
self._agent_directory(disk, directory)
|
||||
payloads = guest_agent_files()
|
||||
payloads["/etc/uvm-agent.json"] = (json.dumps({"vm_id": vm_id, "username": username}) + "\n").encode("utf-8")
|
||||
payloads = self._agent_payloads(vm_id, username)
|
||||
manifest_path = "/etc/uvm-agent-files.json"
|
||||
previous = self._agent_file(disk, manifest_path, files / "manifest")
|
||||
old_hashes = {}
|
||||
|
||||
@@ -198,6 +198,7 @@ class LifecycleService:
|
||||
self._cleanup_runtime(vm)
|
||||
self._apply_pending_resize(vm)
|
||||
self._images.configure_guest_network(Path(vm.disk), vm.guest_ip)
|
||||
self._images.provision_guest_agent(Path(vm.disk), vm.id, vm.username)
|
||||
vm.network, vm.bridge = str(self._settings.network), self._settings.bridge
|
||||
self._boot(vm, kernel)
|
||||
return vm
|
||||
|
||||
Reference in New Issue
Block a user