summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--recipes-containers/container-registry/container-oci-registry-config.bb145
-rw-r--r--recipes-containers/container-registry/docker-registry-config.bb24
-rwxr-xr-xrecipes-containers/vcontainer/files/vdkr.sh26
-rwxr-xr-xrecipes-containers/vcontainer/files/vpdmn.sh26
-rwxr-xr-xrecipes-containers/vcontainer/files/vrunner.sh7
-rw-r--r--recipes-containers/vcontainer/files/vxn-oci-runtime148
-rw-r--r--recipes-core/vxn/README.md159
-rw-r--r--recipes-core/vxn/vxn_1.0.bb36
8 files changed, 470 insertions, 101 deletions
diff --git a/recipes-containers/container-registry/container-oci-registry-config.bb b/recipes-containers/container-registry/container-oci-registry-config.bb
index 294defc3..5161f0a3 100644
--- a/recipes-containers/container-registry/container-oci-registry-config.bb
+++ b/recipes-containers/container-registry/container-oci-registry-config.bb
@@ -64,12 +64,19 @@ CONTAINER_REGISTRY_SEARCH_FIRST ?= "1"
64# NOT stored in bitbake - should point to external file 64# NOT stored in bitbake - should point to external file
65CONTAINER_REGISTRY_AUTHFILE ?= "" 65CONTAINER_REGISTRY_AUTHFILE ?= ""
66 66
67# OCI runtime configuration for Podman
68# Runtime name (e.g. "vxn") — creates a containers.conf.d drop-in
69PODMAN_OCI_RUNTIME ?= ""
70# Path to OCI runtime binary (e.g. "/usr/bin/vxn-oci-runtime")
71PODMAN_OCI_RUNTIME_PATH ?= ""
72
67# Skip recipe entirely if not configured 73# Skip recipe entirely if not configured
68# User must explicitly set CONTAINER_REGISTRY_URL to enable 74# User must explicitly set CONTAINER_REGISTRY_URL to enable
69python() { 75python() {
70 registry = d.getVar('CONTAINER_REGISTRY_URL') 76 registry = d.getVar('CONTAINER_REGISTRY_URL')
71 if not registry: 77 oci_runtime = (d.getVar('PODMAN_OCI_RUNTIME') or "").strip()
72 raise bb.parse.SkipRecipe("CONTAINER_REGISTRY_URL not set - recipe is opt-in only") 78 if not registry and not oci_runtime:
79 raise bb.parse.SkipRecipe("No registry or OCI runtime configured - recipe is opt-in only")
73 80
74 # Check for conflicting settings 81 # Check for conflicting settings
75 secure = d.getVar('CONTAINER_REGISTRY_SECURE') == '1' 82 secure = d.getVar('CONTAINER_REGISTRY_SECURE') == '1'
@@ -94,63 +101,85 @@ python do_install() {
94 search_first = d.getVar('CONTAINER_REGISTRY_SEARCH_FIRST') == "1" 101 search_first = d.getVar('CONTAINER_REGISTRY_SEARCH_FIRST') == "1"
95 ca_cert = d.getVar('CONTAINER_REGISTRY_CA_CERT') 102 ca_cert = d.getVar('CONTAINER_REGISTRY_CA_CERT')
96 authfile = d.getVar('CONTAINER_REGISTRY_AUTHFILE') or '' 103 authfile = d.getVar('CONTAINER_REGISTRY_AUTHFILE') or ''
97 104 oci_runtime = (d.getVar('PODMAN_OCI_RUNTIME') or "").strip()
98 # Extract registry host (strip any path) 105 oci_runtime_path = (d.getVar('PODMAN_OCI_RUNTIME_PATH') or "").strip()
99 registry_host = registry.split('/')[0] if '/' in registry else registry
100 106
101 dest = d.getVar('D') 107 dest = d.getVar('D')
102 confdir = os.path.join(dest, d.getVar('sysconfdir').lstrip('/'),
103 'containers', 'registries.conf.d')
104 os.makedirs(confdir, exist_ok=True)
105 108
106 # Install CA cert in secure mode 109 # --- Registry configuration ---
107 if secure: 110 if registry:
108 if os.path.exists(ca_cert): 111 # Extract registry host (strip any path)
109 cert_dir = os.path.join(dest, 'etc/containers/certs.d', registry_host) 112 registry_host = registry.split('/')[0] if '/' in registry else registry
110 os.makedirs(cert_dir, exist_ok=True) 113
111 shutil.copy(ca_cert, os.path.join(cert_dir, 'ca.crt')) 114 confdir = os.path.join(dest, d.getVar('sysconfdir').lstrip('/'),
112 bb.note(f"Installed CA certificate for registry: {registry_host}") 115 'containers', 'registries.conf.d')
113 else: 116 os.makedirs(confdir, exist_ok=True)
114 bb.warn(f"Secure mode enabled but CA certificate not found at {ca_cert}") 117
115 bb.warn("Run 'container-registry.sh start' to generate PKI, then rebuild this package") 118 # Install CA cert in secure mode
116
117 # In secure mode, insecure should be false
118 if secure:
119 insecure = False
120
121 # Generate drop-in config
122 # Filename starts with 50- so it's processed after base config but
123 # can be overridden by higher-numbered files
124 config_path = os.path.join(confdir, '50-custom-registry.conf')
125
126 with open(config_path, 'w') as f:
127 f.write(f"# Custom container registry: {registry}\n")
128 f.write(f"# Generated by container-oci-registry-config recipe\n")
129 f.write(f"# This is ADDITIVE - base registries.conf is unchanged\n")
130 f.write(f"# Public registries (docker.io, quay.io) remain accessible\n")
131 f.write(f"#\n")
132 if secure: 119 if secure:
133 f.write(f"# Mode: secure (TLS with CA certificate verification)\n") 120 if os.path.exists(ca_cert):
134 f.write(f"# CA cert: /etc/containers/certs.d/{registry_host}/ca.crt\n") 121 cert_dir = os.path.join(dest, 'etc/containers/certs.d', registry_host)
135 else: 122 os.makedirs(cert_dir, exist_ok=True)
136 f.write(f"# Mode: insecure (HTTP or untrusted TLS)\n") 123 shutil.copy(ca_cert, os.path.join(cert_dir, 'ca.crt'))
137 f.write(f"#\n") 124 bb.note("Installed CA certificate for registry: %s" % registry_host)
138 f.write(f"# To remove: uninstall container-oci-registry-config package\n") 125 else:
139 f.write(f"# or delete this file\n\n") 126 bb.warn("Secure mode enabled but CA certificate not found at %s" % ca_cert)
140 127 bb.warn("Run 'container-registry.sh start' to generate PKI, then rebuild this package")
141 if search_first: 128
142 # Add to unqualified-search-registries 129 # In secure mode, insecure should be false
143 # This means short names like "myapp:latest" will search here first 130 if secure:
144 f.write(f"# Search this registry for unqualified image names\n") 131 insecure = False
145 f.write(f'unqualified-search-registries = ["{registry}"]\n\n') 132
146 133 # Generate drop-in config
147 # Always create registry entry to set insecure flag explicitly 134 config_path = os.path.join(confdir, '50-custom-registry.conf')
148 f.write(f'[[registry]]\n') 135
149 f.write(f'location = "{registry_host}"\n') 136 with open(config_path, 'w') as f:
150 if insecure: 137 f.write("# Custom container registry: %s\n" % registry)
151 f.write(f'insecure = true\n') 138 f.write("# Generated by container-oci-registry-config recipe\n")
152 else: 139 f.write("# This is ADDITIVE - base registries.conf is unchanged\n")
153 f.write(f'insecure = false\n') 140 f.write("# Public registries (docker.io, quay.io) remain accessible\n")
141 f.write("#\n")
142 if secure:
143 f.write("# Mode: secure (TLS with CA certificate verification)\n")
144 f.write("# CA cert: /etc/containers/certs.d/%s/ca.crt\n" % registry_host)
145 else:
146 f.write("# Mode: insecure (HTTP or untrusted TLS)\n")
147 f.write("#\n")
148 f.write("# To remove: uninstall container-oci-registry-config package\n")
149 f.write("# or delete this file\n\n")
150
151 if search_first:
152 f.write("# Search this registry for unqualified image names\n")
153 f.write('unqualified-search-registries = ["%s"]\n\n' % registry)
154
155 f.write('[[registry]]\n')
156 f.write('location = "%s"\n' % registry_host)
157 if insecure:
158 f.write('insecure = true\n')
159 else:
160 f.write('insecure = false\n')
161
162 mode = "secure" if secure else ("insecure" if insecure else "default")
163 bb.note("Created registry config for %s (mode=%s)" % (registry, mode))
164
165 # --- OCI runtime configuration ---
166 if oci_runtime:
167 dropin_dir = os.path.join(dest, d.getVar('sysconfdir').lstrip('/'),
168 'containers', 'containers.conf.d')
169 os.makedirs(dropin_dir, exist_ok=True)
170
171 runtime_path = oci_runtime_path if oci_runtime_path else "/usr/bin/%s" % oci_runtime
172 dropin_path = os.path.join(dropin_dir, '50-oci-runtime.conf')
173
174 with open(dropin_path, 'w') as f:
175 f.write("# OCI runtime configuration\n")
176 f.write("# Generated by container-oci-registry-config recipe\n\n")
177 f.write("[engine]\n")
178 f.write('runtime = "%s"\n\n' % oci_runtime)
179 f.write("[engine.runtimes]\n")
180 f.write('%s = ["%s"]\n' % (oci_runtime, runtime_path))
181
182 bb.note("Created OCI runtime drop-in for %s (%s)" % (oci_runtime, runtime_path))
154 183
155 # Install authfile if provided (for baked credentials) 184 # Install authfile if provided (for baked credentials)
156 if authfile and os.path.exists(authfile): 185 if authfile and os.path.exists(authfile):
@@ -159,14 +188,12 @@ python do_install() {
159 auth_json = os.path.join(containers_dir, 'auth.json') 188 auth_json = os.path.join(containers_dir, 'auth.json')
160 shutil.copy(authfile, auth_json) 189 shutil.copy(authfile, auth_json)
161 os.chmod(auth_json, 0o600) 190 os.chmod(auth_json, 0o600)
162 bb.note(f"Installed OCI auth config from {authfile}") 191 bb.note("Installed OCI auth config from %s" % authfile)
163
164 mode = "secure" if secure else ("insecure" if insecure else "default")
165 bb.note(f"Created registry config for {registry} (mode={mode})")
166} 192}
167 193
168FILES:${PN} = " \ 194FILES:${PN} = " \
169 ${sysconfdir}/containers/registries.conf.d \ 195 ${sysconfdir}/containers/registries.conf.d \
196 ${sysconfdir}/containers/containers.conf.d \
170 ${sysconfdir}/containers/certs.d/*/ca.crt \ 197 ${sysconfdir}/containers/certs.d/*/ca.crt \
171 ${sysconfdir}/containers/auth.json \ 198 ${sysconfdir}/containers/auth.json \
172" 199"
diff --git a/recipes-containers/container-registry/docker-registry-config.bb b/recipes-containers/container-registry/docker-registry-config.bb
index 0e8d66ad..e558cccb 100644
--- a/recipes-containers/container-registry/docker-registry-config.bb
+++ b/recipes-containers/container-registry/docker-registry-config.bb
@@ -60,6 +60,13 @@ DOCKER_REGISTRY_INSECURE ?= ""
60# NOT stored in bitbake - should point to external file 60# NOT stored in bitbake - should point to external file
61CONTAINER_REGISTRY_AUTHFILE ?= "" 61CONTAINER_REGISTRY_AUTHFILE ?= ""
62 62
63# OCI runtime configuration for Docker daemon
64# JSON object mapping runtime names to paths, e.g.:
65# DOCKER_OCI_RUNTIMES = '{"vxn": {"path": "/usr/bin/vxn-oci-runtime"}}'
66DOCKER_OCI_RUNTIMES ?= ""
67# Default OCI runtime name (must be a key in DOCKER_OCI_RUNTIMES or "runc")
68DOCKER_DEFAULT_RUNTIME ?= ""
69
63def get_insecure_registries(d): 70def get_insecure_registries(d):
64 """Get insecure registries from either Docker-specific or generic config""" 71 """Get insecure registries from either Docker-specific or generic config"""
65 # Prefer explicit DOCKER_REGISTRY_INSECURE if set 72 # Prefer explicit DOCKER_REGISTRY_INSECURE if set
@@ -87,8 +94,10 @@ python() {
87 bb.fatal("CONTAINER_REGISTRY_SECURE='1' conflicts with insecure registry settings. " 94 bb.fatal("CONTAINER_REGISTRY_SECURE='1' conflicts with insecure registry settings. "
88 "Use secure mode (TLS+auth) OR insecure mode (HTTP), not both.") 95 "Use secure mode (TLS+auth) OR insecure mode (HTTP), not both.")
89 96
90 if not secure and not registries: 97 oci_runtimes = (d.getVar('DOCKER_OCI_RUNTIMES') or "").strip()
91 raise bb.parse.SkipRecipe("No registry configured - recipe is opt-in only") 98
99 if not secure and not registries and not oci_runtimes:
100 raise bb.parse.SkipRecipe("No registry or OCI runtime configured - recipe is opt-in only")
92 101
93 # In secure mode, depend on PKI generation 102 # In secure mode, depend on PKI generation
94 if secure: 103 if secure:
@@ -137,6 +146,17 @@ python do_install() {
137 config["insecure-registries"] = registries 146 config["insecure-registries"] = registries
138 bb.note(f"Created Docker config with insecure registries: {registries}") 147 bb.note(f"Created Docker config with insecure registries: {registries}")
139 148
149 # OCI runtime configuration
150 oci_runtimes = (d.getVar('DOCKER_OCI_RUNTIMES') or "").strip()
151 default_runtime = (d.getVar('DOCKER_DEFAULT_RUNTIME') or "").strip()
152
153 if oci_runtimes:
154 config["runtimes"] = json.loads(oci_runtimes)
155 bb.note("Added OCI runtimes to Docker config: %s" % oci_runtimes)
156 if default_runtime:
157 config["default-runtime"] = default_runtime
158 bb.note("Set default Docker runtime: %s" % default_runtime)
159
140 # Install authfile if provided (for baked credentials) 160 # Install authfile if provided (for baked credentials)
141 if authfile and os.path.exists(authfile): 161 if authfile and os.path.exists(authfile):
142 docker_dir = os.path.join(dest, 'root/.docker') 162 docker_dir = os.path.join(dest, 'root/.docker')
diff --git a/recipes-containers/vcontainer/files/vdkr.sh b/recipes-containers/vcontainer/files/vdkr.sh
index 818c831e..29f08877 100755
--- a/recipes-containers/vcontainer/files/vdkr.sh
+++ b/recipes-containers/vcontainer/files/vdkr.sh
@@ -21,5 +21,27 @@ VCONTAINER_STATE_FILE="docker-state.img"
21VCONTAINER_OTHER_PREFIX="VPDMN" 21VCONTAINER_OTHER_PREFIX="VPDMN"
22VCONTAINER_VERSION="3.4.0" 22VCONTAINER_VERSION="3.4.0"
23 23
24# Source common implementation 24# Auto-detect Xen if not explicitly set
25source "$(dirname "${BASH_SOURCE[0]}")/vcontainer-common.sh" "$@" 25if [ -z "${VCONTAINER_HYPERVISOR:-}" ]; then
26 if command -v xl >/dev/null 2>&1; then
27 export VCONTAINER_HYPERVISOR="xen"
28 fi
29fi
30
31# Fall back to vxn blob dir on Dom0
32if [ -z "${VDKR_BLOB_DIR:-}" ] && [ -d "/usr/share/vxn" ]; then
33 export VDKR_BLOB_DIR="/usr/share/vxn"
34fi
35
36# Two-phase lib lookup: script dir (dev), then /usr/lib/vxn (target)
37SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
38if [ -f "${SCRIPT_DIR}/vcontainer-common.sh" ]; then
39 export VCONTAINER_LIBDIR="${SCRIPT_DIR}"
40 source "${SCRIPT_DIR}/vcontainer-common.sh" "$@"
41elif [ -f "/usr/lib/vxn/vcontainer-common.sh" ]; then
42 export VCONTAINER_LIBDIR="/usr/lib/vxn"
43 source "/usr/lib/vxn/vcontainer-common.sh" "$@"
44else
45 echo "Error: vcontainer-common.sh not found" >&2
46 exit 1
47fi
diff --git a/recipes-containers/vcontainer/files/vpdmn.sh b/recipes-containers/vcontainer/files/vpdmn.sh
index 30775d35..6f0f56d8 100755
--- a/recipes-containers/vcontainer/files/vpdmn.sh
+++ b/recipes-containers/vcontainer/files/vpdmn.sh
@@ -21,5 +21,27 @@ VCONTAINER_STATE_FILE="podman-state.img"
21VCONTAINER_OTHER_PREFIX="VDKR" 21VCONTAINER_OTHER_PREFIX="VDKR"
22VCONTAINER_VERSION="1.2.0" 22VCONTAINER_VERSION="1.2.0"
23 23
24# Source common implementation 24# Auto-detect Xen if not explicitly set
25source "$(dirname "${BASH_SOURCE[0]}")/vcontainer-common.sh" "$@" 25if [ -z "${VCONTAINER_HYPERVISOR:-}" ]; then
26 if command -v xl >/dev/null 2>&1; then
27 export VCONTAINER_HYPERVISOR="xen"
28 fi
29fi
30
31# Fall back to vxn blob dir on Dom0
32if [ -z "${VPDMN_BLOB_DIR:-}" ] && [ -d "/usr/share/vxn" ]; then
33 export VPDMN_BLOB_DIR="/usr/share/vxn"
34fi
35
36# Two-phase lib lookup: script dir (dev), then /usr/lib/vxn (target)
37SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
38if [ -f "${SCRIPT_DIR}/vcontainer-common.sh" ]; then
39 export VCONTAINER_LIBDIR="${SCRIPT_DIR}"
40 source "${SCRIPT_DIR}/vcontainer-common.sh" "$@"
41elif [ -f "/usr/lib/vxn/vcontainer-common.sh" ]; then
42 export VCONTAINER_LIBDIR="/usr/lib/vxn"
43 source "/usr/lib/vxn/vcontainer-common.sh" "$@"
44else
45 echo "Error: vcontainer-common.sh not found" >&2
46 exit 1
47fi
diff --git a/recipes-containers/vcontainer/files/vrunner.sh b/recipes-containers/vcontainer/files/vrunner.sh
index aaaaeb61..3f0b3448 100755
--- a/recipes-containers/vcontainer/files/vrunner.sh
+++ b/recipes-containers/vcontainer/files/vrunner.sh
@@ -515,6 +515,13 @@ if [ ! -f "$HV_BACKEND" ]; then
515fi 515fi
516source "$HV_BACKEND" 516source "$HV_BACKEND"
517 517
518# Xen backend uses vxn-init.sh which is a unified init (no Docker/Podman
519# daemon in guest). It always parses docker_* kernel parameters regardless
520# of which frontend (vdkr/vpdmn) invoked us.
521if [ "$VCONTAINER_HYPERVISOR" = "xen" ]; then
522 CMDLINE_PREFIX="docker"
523fi
524
518# Daemon mode handling 525# Daemon mode handling
519# Set default socket directory based on architecture 526# Set default socket directory based on architecture
520# If --state-dir was provided, use it for daemon files too 527# If --state-dir was provided, use it for daemon files too
diff --git a/recipes-containers/vcontainer/files/vxn-oci-runtime b/recipes-containers/vcontainer/files/vxn-oci-runtime
index 57144aea..02b94fb5 100644
--- a/recipes-containers/vcontainer/files/vxn-oci-runtime
+++ b/recipes-containers/vcontainer/files/vxn-oci-runtime
@@ -37,18 +37,32 @@ BLOB_DIR="/usr/share/vxn"
37LOG_FILE="/var/log/vxn-oci-runtime.log" 37LOG_FILE="/var/log/vxn-oci-runtime.log"
38VXN_LOG="/var/log/vxn-oci-runtime.log" 38VXN_LOG="/var/log/vxn-oci-runtime.log"
39 39
40# Write a JSON log entry to the shim's --log file (runc-compatible format).
41# containerd-shim-runc-v2 parses this to extract error messages on failure.
42_log_json() {
43 local level="$1" msg="$2" dest="$3"
44 local ts
45 ts=$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || echo "1970-01-01T00:00:00Z")
46 printf '{"level":"%s","msg":"%s","time":"%s"}\n' "$level" "$msg" "$ts" >> "$dest" 2>/dev/null || true
47}
48
40log() { 49log() {
41 local ts 50 local ts
42 ts=$(date '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "-") 51 ts=$(date '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "-")
43 # Always write to our own log (shim overrides LOG_FILE via --log) 52 # Always write plain text to our own log for human debugging
44 echo "[$ts] $*" >> "$VXN_LOG" 2>/dev/null || true 53 echo "[$ts] $*" >> "$VXN_LOG" 2>/dev/null || true
54 # Write JSON to shim's log file (if different from our log)
45 if [ "$LOG_FILE" != "$VXN_LOG" ]; then 55 if [ "$LOG_FILE" != "$VXN_LOG" ]; then
46 echo "[$ts] $*" >> "$LOG_FILE" 2>/dev/null || true 56 _log_json "info" "$*" "$LOG_FILE"
47 fi 57 fi
48} 58}
49 59
50die() { 60die() {
51 log "FATAL: $*" 61 log "FATAL: $*"
62 # Write JSON error to shim log so Docker can extract the message
63 if [ "$LOG_FILE" != "$VXN_LOG" ]; then
64 _log_json "error" "$*" "$LOG_FILE"
65 fi
52 echo "vxn-oci-runtime: $*" >&2 66 echo "vxn-oci-runtime: $*" >&2
53 exit 1 67 exit 1
54} 68}
@@ -200,10 +214,26 @@ cmd_create() {
200 214
201 log " entrypoint='$entrypoint' cwd='$cwd' terminal=$terminal" 215 log " entrypoint='$entrypoint' cwd='$cwd' terminal=$terminal"
202 216
203 # Create ext4 disk image from bundle/rootfs/ 217 # Read rootfs path from config.json (OCI spec: root.path)
204 local rootfs_dir="$bundle/rootfs" 218 local rootfs_path=""
219 if command -v jq >/dev/null 2>&1; then
220 rootfs_path=$(jq -r '.root.path // "rootfs"' "$config" 2>/dev/null)
221 else
222 rootfs_path=$(grep -o '"path"[[:space:]]*:[[:space:]]*"[^"]*"' "$config" 2>/dev/null | \
223 head -1 | sed 's/.*"path"[[:space:]]*:[[:space:]]*"//;s/"$//')
224 [ -z "$rootfs_path" ] && rootfs_path="rootfs"
225 fi
226 # Resolve relative paths against bundle directory
227 case "$rootfs_path" in
228 /*) ;;
229 *) rootfs_path="$bundle/$rootfs_path" ;;
230 esac
231
232 local rootfs_dir="$rootfs_path"
205 local input_img="$dir/input.img" 233 local input_img="$dir/input.img"
206 234
235 log " rootfs_dir=$rootfs_dir"
236
207 if [ -d "$rootfs_dir" ] && [ -n "$(ls -A "$rootfs_dir" 2>/dev/null)" ]; then 237 if [ -d "$rootfs_dir" ] && [ -n "$(ls -A "$rootfs_dir" 2>/dev/null)" ]; then
208 # Calculate size: rootfs size + 50% headroom, minimum 64MB 238 # Calculate size: rootfs size + 50% headroom, minimum 64MB
209 local rootfs_size_kb 239 local rootfs_size_kb
@@ -213,8 +243,14 @@ cmd_create() {
213 243
214 log " Creating ext4 image: ${img_size_kb}KB from $rootfs_dir" 244 log " Creating ext4 image: ${img_size_kb}KB from $rootfs_dir"
215 mke2fs -t ext4 -d "$rootfs_dir" -b 4096 "$input_img" "${img_size_kb}K" \ 245 mke2fs -t ext4 -d "$rootfs_dir" -b 4096 "$input_img" "${img_size_kb}K" \
216 >> "$LOG_FILE" 2>&1 || die "create: failed to create ext4 image" 246 >> "$VXN_LOG" 2>&1 || die "create: failed to create ext4 image"
217 else 247 else
248 # Diagnostics: log what we actually see
249 log " DIAG: bundle contents: $(ls -la "$bundle/" 2>&1)"
250 log " DIAG: rootfs_dir exists=$([ -d "$rootfs_dir" ] && echo yes || echo no)"
251 log " DIAG: rootfs_dir contents: $(ls -la "$rootfs_dir" 2>&1)"
252 log " DIAG: mounts at bundle: $(mount 2>/dev/null | grep "$(dirname "$bundle")" || echo none)"
253 log " DIAG: config.json root: $(grep -o '"root"[^}]*}' "$config" 2>/dev/null)"
218 die "create: $rootfs_dir is empty or does not exist" 254 die "create: $rootfs_dir is empty or does not exist"
219 fi 255 fi
220 256
@@ -271,7 +307,7 @@ XENEOF
271 log " Xen config written to $config_cfg" 307 log " Xen config written to $config_cfg"
272 308
273 # Create domain in paused state (OCI spec: create does not start) 309 # Create domain in paused state (OCI spec: create does not start)
274 xl create -p "$config_cfg" >> "$LOG_FILE" 2>&1 || die "create: xl create -p failed" 310 xl create -p "$config_cfg" >> "$VXN_LOG" 2>&1 || die "create: xl create -p failed"
275 311
276 log " Domain $domname created (paused)" 312 log " Domain $domname created (paused)"
277 313
@@ -327,33 +363,54 @@ DBGEOF
327 363
328 # Monitor process: tracks domain lifecycle and captures output. 364 # Monitor process: tracks domain lifecycle and captures output.
329 # 365 #
330 # Non-terminal mode: xl console captures the domain's serial output. 366 # The shim monitors the PID written to --pid-file. The monitor MUST stay
331 # When the domain dies, xl console exits (PTY closes). We immediately 367 # alive through the full create→start→run→exit lifecycle. If the monitor
332 # extract content between OUTPUT_START/END markers and write to stdout. 368 # dies before start is called, the shim skips start and goes to cleanup.
333 # stdout is the shim's pipe → containerd copies to client FIFO → ctr. 369 #
370 # Non-terminal mode: we poll xl list to wait for the domain to be
371 # unpaused and to run to completion. Once the domain dies, we attach
372 # xl console to read the console ring buffer, extract OUTPUT_START/END
373 # markers, and relay the output to stdout (the shim's pipe).
334 # 374 #
335 # CRITICAL: We use "wait" on xl console instead of polling xl list. 375 # IMPORTANT: We cannot run xl console on a paused domain — it exits
336 # Polling with sleep 5 was too slow — the shim detected "stopped" and 376 # immediately with no output. Instead we wait for the domain to finish,
337 # killed the monitor before it had a chance to output. Using wait gives 377 # then read the console ring buffer post-mortem via xl console -r (dmesg).
338 # us instant reaction when the domain dies. 378 # However, xl console on a destroyed domain also fails. So we use a
379 # two-phase approach: poll for domain to start running, then attach
380 # xl console which will block until the domain dies.
339 # 381 #
340 # Terminal mode (console-socket): the shim owns the PTY exclusively. 382 # Terminal mode (console-socket): the shim owns the PTY exclusively.
341 # We just wait for the domain to exit without capturing console. 383 # We just wait for the domain to exit without capturing console.
342 local _dn="$domname" _logdir="$logdir" _csock="$console_socket" 384 local _dn="$domname" _logdir="$logdir" _csock="$console_socket"
343 ( 385 (
344 if [ -z "$_csock" ]; then 386 if [ -z "$_csock" ]; then
345 # Non-terminal: capture console to persistent log dir 387 # Non-terminal: stay alive until domain finishes, then capture output.
346 xl console "$_dn" > "$_logdir/console.log" 2>&1 & 388 #
347 _cpid=$! 389 # Phase 1: Wait for domain to exist and be unpaused (start called).
348 390 # The domain is created paused — xl console would exit immediately.
349 # Wait for xl console to exit — domain death closes the PTY, 391 # Poll until it transitions from 'p' (paused) to running, or dies.
350 # which causes xl console to exit immediately. No polling delay. 392 while xl list "$_dn" >/dev/null 2>&1; do
351 wait $_cpid 2>/dev/null 393 # Check if domain is still paused
352 394 local _state
353 # Extract output between markers and write to stdout. 395 _state=$(xl list "$_dn" 2>/dev/null | awk -v dn="$_dn" '$1 == dn {print $5}')
354 # stdout IS the shim's pipe (confirmed: fd1=pipe). The shim's 396 # States: r=running, b=blocked, p=paused, s=shutdown, c=crashed, d=dying
355 # io.Copy goroutine reads from this pipe and writes to the 397 case "$_state" in
356 # containerd client FIFO. ctr reads from the FIFO. 398 p) sleep 0.2; continue ;; # Still paused, keep waiting
399 *) break ;; # Running/blocked/other — proceed
400 esac
401 done
402
403 # Phase 2: Domain is running (or already dead). Attach xl console
404 # to capture serial output. xl console blocks until PTY closes
405 # (domain death), then exits.
406 if xl list "$_dn" >/dev/null 2>&1; then
407 xl console "$_dn" > "$_logdir/console.log" 2>&1 || true
408 fi
409
410 # Phase 3: Extract output between markers and write to stdout.
411 # stdout IS the shim's pipe (fd1=pipe). The shim's io.Copy
412 # goroutine reads from this pipe and writes to the containerd
413 # client FIFO. ctr reads from the FIFO.
357 if [ -f "$_logdir/console.log" ]; then 414 if [ -f "$_logdir/console.log" ]; then
358 _relay=false 415 _relay=false
359 while IFS= read -r _line; do 416 while IFS= read -r _line; do
@@ -409,7 +466,7 @@ cmd_start() {
409 xl list "$domname" >/dev/null 2>&1 || die "start: domain $domname not found" 466 xl list "$domname" >/dev/null 2>&1 || die "start: domain $domname not found"
410 467
411 # Unpause the domain 468 # Unpause the domain
412 xl unpause "$domname" >> "$LOG_FILE" 2>&1 || die "start: xl unpause failed" 469 xl unpause "$domname" >> "$VXN_LOG" 2>&1 || die "start: xl unpause failed"
413 470
414 # Update state 471 # Update state
415 local pid bundle created 472 local pid bundle created
@@ -462,11 +519,30 @@ EOF
462} 519}
463 520
464cmd_kill() { 521cmd_kill() {
465 local container_id="$1" 522 local container_id=""
466 local signal="${2:-SIGTERM}" 523 local signal="SIGTERM"
524 local kill_all=false
525
526 # Parse arguments: runc accepts `kill [flags] <container-id> [signal]`
527 # Docker sends: kill --all <container-id> <signal>
528 while [ $# -gt 0 ]; do
529 case "$1" in
530 --all|-a) kill_all=true; shift ;;
531 -*) shift ;; # skip unknown flags
532 *)
533 if [ -z "$container_id" ]; then
534 container_id="$1"
535 else
536 signal="$1"
537 fi
538 shift
539 ;;
540 esac
541 done
542
467 [ -n "$container_id" ] || die "kill: container ID required" 543 [ -n "$container_id" ] || die "kill: container ID required"
468 544
469 log "KILL: id=$container_id signal=$signal" 545 log "KILL: id=$container_id signal=$signal all=$kill_all"
470 load_state "$container_id" 546 load_state "$container_id"
471 547
472 local dir 548 local dir
@@ -477,24 +553,24 @@ cmd_kill() {
477 # Normalize signal: accept both numeric and symbolic forms 553 # Normalize signal: accept both numeric and symbolic forms
478 case "$signal" in 554 case "$signal" in
479 9|SIGKILL|KILL) 555 9|SIGKILL|KILL)
480 xl destroy "$domname" >> "$LOG_FILE" 2>&1 || true 556 xl destroy "$domname" >> "$VXN_LOG" 2>&1 || true
481 ;; 557 ;;
482 2|SIGINT|INT) 558 2|SIGINT|INT)
483 xl destroy "$domname" >> "$LOG_FILE" 2>&1 || true 559 xl destroy "$domname" >> "$VXN_LOG" 2>&1 || true
484 ;; 560 ;;
485 15|SIGTERM|TERM|"") 561 15|SIGTERM|TERM|"")
486 xl shutdown "$domname" >> "$LOG_FILE" 2>&1 || true 562 xl shutdown "$domname" >> "$VXN_LOG" 2>&1 || true
487 # Wait briefly for graceful shutdown, then force destroy 563 # Wait briefly for graceful shutdown, then force destroy
488 local i 564 local i
489 for i in 1 2 3 4 5 6 7 8 9 10; do 565 for i in 1 2 3 4 5 6 7 8 9 10; do
490 xl list "$domname" >/dev/null 2>&1 || break 566 xl list "$domname" >/dev/null 2>&1 || break
491 sleep 1 567 sleep 1
492 done 568 done
493 xl destroy "$domname" >> "$LOG_FILE" 2>&1 || true 569 xl destroy "$domname" >> "$VXN_LOG" 2>&1 || true
494 ;; 570 ;;
495 *) 571 *)
496 # Unknown signal — treat as SIGTERM 572 # Unknown signal — treat as SIGTERM
497 xl shutdown "$domname" >> "$LOG_FILE" 2>&1 || true 573 xl shutdown "$domname" >> "$VXN_LOG" 2>&1 || true
498 ;; 574 ;;
499 esac 575 esac
500 576
@@ -542,7 +618,7 @@ cmd_delete() {
542 local domname 618 local domname
543 domname=$(cat "$dir/domname") 619 domname=$(cat "$dir/domname")
544 if xl list "$domname" >/dev/null 2>&1; then 620 if xl list "$domname" >/dev/null 2>&1; then
545 xl destroy "$domname" >> "$LOG_FILE" 2>&1 || true 621 xl destroy "$domname" >> "$VXN_LOG" 2>&1 || true
546 fi 622 fi
547 fi 623 fi
548 624
diff --git a/recipes-core/vxn/README.md b/recipes-core/vxn/README.md
new file mode 100644
index 00000000..35802e31
--- /dev/null
+++ b/recipes-core/vxn/README.md
@@ -0,0 +1,159 @@
1# vxn — Docker CLI for Xen DomU Containers
2
3vxn runs OCI containers as Xen DomU guests. The VM IS the container — no
4Docker/Podman daemon runs inside the guest. The guest boots a minimal Linux,
5mounts the container's filesystem, and directly executes the entrypoint.
6
7## Packages
8
9| Package | Contents | Usage |
10|---------|----------|-------|
11| `vxn` | CLI, OCI runtime, blobs, containerd config | Base package (required) |
12| `vxn-vdkr` | `vdkr` — Docker-like CLI frontend | `IMAGE_INSTALL:append = " vxn-vdkr"` |
13| `vxn-vpdmn` | `vpdmn` — Podman-like CLI frontend | `IMAGE_INSTALL:append = " vxn-vpdmn"` |
14| `vxn-docker-config` | `/etc/docker/daemon.json` (vxn as default runtime) | `IMAGE_INSTALL:append = " vxn-docker-config"` |
15| `vxn-podman-config` | `/etc/containers/containers.conf.d/50-vxn-runtime.conf` | `IMAGE_INSTALL:append = " vxn-podman-config"` |
16
17## Execution Paths
18
19### 1. containerd (vctr/ctr) — recommended
20
21No additional packages needed beyond `vxn`. containerd is configured
22automatically via `/etc/containerd/config.toml`.
23
24```bash
25ctr image pull docker.io/library/alpine:latest
26vctr run --rm docker.io/library/alpine:latest test1 /bin/echo hello
27ctr run -t --rm --runtime io.containerd.vxn.v2 docker.io/library/alpine:latest tty1 /bin/sh
28```
29
30### 2. vdkr/vpdmn (Docker/Podman-like CLI, no daemon)
31
32Install `vxn-vdkr` or `vxn-vpdmn`. These are standalone frontends that
33auto-detect Xen (via `xl`) and manage containers without any daemon process.
34They handle OCI image pull/unpack on the host via skopeo.
35
36```bash
37vdkr run --rm alpine echo hello # Docker-like
38vpdmn run --rm alpine echo hello # Podman-like
39```
40
41Persistent DomU (memres) for faster subsequent runs:
42```bash
43vdkr vmemres start # Boot persistent DomU (~10s)
44vdkr run --rm alpine echo hello # Hot-plug container (~1s)
45vdkr vmemres stop # Shutdown DomU
46```
47
48### 3. Native Docker with vxn runtime
49
50Install `vxn-docker-config` to register vxn-oci-runtime as Docker's default
51OCI runtime. Docker manages images (pull/tag/rmi) natively.
52
53```bash
54docker run --rm --network=none alpine echo hello
55docker run --rm --network=host alpine echo hello
56```
57
58**IMPORTANT: Networking** — Docker's default bridge networking is incompatible
59with VM-based runtimes. Docker tries to create veth pairs and move them into
60a Linux network namespace, but vxn containers are Xen DomUs with their own
61kernel network stack. You MUST use `--network=none` or `--network=host`.
62
63This is the same limitation as kata-containers. The long-term fix is a TAP
64bridge that connects Docker's network namespace to the DomU's vif (see TODO).
65
66For selective use (keep runc as default, use vxn per-run):
67```bash
68docker run --rm --runtime=vxn --network=none alpine echo hello
69```
70
71### 4. Native Podman with vxn runtime
72
73Install `vxn-podman-config` to register vxn-oci-runtime as Podman's default
74OCI runtime. Same networking constraints as Docker.
75
76```bash
77podman run --rm --network=none alpine echo hello
78```
79
80## Build Instructions
81
82```bash
83# Prerequisites in local.conf:
84DISTRO_FEATURES:append = " xen virtualization vcontainer"
85BBMULTICONFIG = "vruntime-aarch64 vruntime-x86-64"
86
87# Build (mcdepends auto-builds vruntime blobs)
88bitbake vxn
89
90# Dom0 image with containerd + Docker-like CLI
91IMAGE_INSTALL:append = " vxn vxn-vdkr"
92
93# Dom0 image with native Docker integration
94IMAGE_INSTALL:append = " vxn vxn-docker-config docker"
95
96bitbake xen-image-minimal
97```
98
99## Architecture
100
101```
102Docker/Podman/containerd → vxn-oci-runtime → xl create/unpause/destroy → Xen DomU
103
104 vxn-init.sh
105 mount rootfs
106 chroot + exec
107```
108
109The OCI runtime (`/usr/bin/vxn-oci-runtime`) implements the standard
110create/start/state/kill/delete lifecycle by mapping to xl commands:
111
112| OCI Command | xl Equivalent |
113|-------------|---------------|
114| create | xl create -p (paused) |
115| start | xl unpause |
116| state | xl list + monitor PID check |
117| kill SIGTERM | xl shutdown (10s grace) + xl destroy |
118| kill SIGKILL | xl destroy |
119| delete | xl destroy + cleanup state |
120
121## Networking Constraints (Native Docker/Podman)
122
123Docker and Podman's default bridge networking creates Linux veth pairs and
124moves one end into a container network namespace. This is fundamentally
125incompatible with VM-based runtimes where the "container" is a VM with its
126own kernel networking.
127
128**Current workarounds:**
129- `--network=none` — DomU uses its own xenbr0 networking
130- `--network=host` — Tells Docker/Podman to skip namespace setup
131
132**Future fix (TODO):**
133TAP bridge integration — read Docker's network namespace config from
134config.json, create a TAP device bridged to the DomU's vif. This is the
135approach kata-containers uses to provide Docker-compatible networking with
136VM isolation.
137
138**Not affected:**
139- `vctr`/`ctr` (containerd) — CNI is separate and opt-in
140- `vdkr`/`vpdmn` — Handle networking independently via xenbr0
141
142## Debugging
143
144```bash
145# OCI runtime log (all invocations)
146cat /var/log/vxn-oci-runtime.log
147
148# Per-container console capture (persists after container exit)
149ls /var/log/vxn-oci-runtime/containers/
150
151# Xen domain status
152xl list
153
154# Watch domain console
155xl console <domname>
156
157# Kill stuck domain
158xl destroy <domname>
159```
diff --git a/recipes-core/vxn/vxn_1.0.bb b/recipes-core/vxn/vxn_1.0.bb
index d16cbc77..a08dac09 100644
--- a/recipes-core/vxn/vxn_1.0.bb
+++ b/recipes-core/vxn/vxn_1.0.bb
@@ -67,6 +67,8 @@ SRC_URI = "\
67 file://containerd-config-vxn.toml \ 67 file://containerd-config-vxn.toml \
68 file://containerd-shim-vxn-v2 \ 68 file://containerd-shim-vxn-v2 \
69 file://vctr \ 69 file://vctr \
70 file://vdkr.sh \
71 file://vpdmn.sh \
70" 72"
71 73
72FILESEXTRAPATHS:prepend := "${THISDIR}/../../recipes-containers/vcontainer/files:" 74FILESEXTRAPATHS:prepend := "${THISDIR}/../../recipes-containers/vcontainer/files:"
@@ -221,6 +223,24 @@ do_install() {
221 # Install vctr convenience wrapper 223 # Install vctr convenience wrapper
222 install -m 0755 ${S}/vctr ${D}${bindir}/vctr 224 install -m 0755 ${S}/vctr ${D}${bindir}/vctr
223 225
226 # Docker/Podman CLI frontends (sub-packages)
227 install -m 0755 ${S}/vdkr.sh ${D}${bindir}/vdkr
228 install -m 0755 ${S}/vpdmn.sh ${D}${bindir}/vpdmn
229
230 # Docker daemon config: register vxn-oci-runtime (vxn-docker-config sub-package)
231 # no-new-privileges=false is needed because vxn ignores Linux security features.
232 # Users must use --network=none or --network=host with vxn containers since
233 # Xen DomUs have their own kernel network stack and Docker's veth/namespace
234 # setup is incompatible with VM-based runtimes.
235 install -d ${D}${sysconfdir}/docker
236 printf '{\n "runtimes": {\n "vxn": {\n "path": "/usr/bin/vxn-oci-runtime"\n }\n },\n "default-runtime": "vxn"\n}\n' \
237 > ${D}${sysconfdir}/docker/daemon.json
238
239 # Podman config: register vxn-oci-runtime (vxn-podman-config sub-package)
240 install -d ${D}${sysconfdir}/containers/containers.conf.d
241 printf '[engine]\nruntime = "vxn"\n\n[engine.runtimes]\nvxn = ["/usr/bin/vxn-oci-runtime"]\n' \
242 > ${D}${sysconfdir}/containers/containers.conf.d/50-vxn-runtime.conf
243
224 # Install shared scripts into libdir 244 # Install shared scripts into libdir
225 install -d ${D}${libdir}/vxn 245 install -d ${D}${libdir}/vxn
226 install -m 0755 ${S}/vrunner.sh ${D}${libdir}/vxn/ 246 install -m 0755 ${S}/vrunner.sh ${D}${libdir}/vxn/
@@ -253,6 +273,22 @@ do_install() {
253 fi 273 fi
254} 274}
255 275
276# Sub-packages for CLI frontends and native runtime config
277PACKAGES =+ "${PN}-vdkr ${PN}-vpdmn ${PN}-docker-config ${PN}-podman-config"
278
279FILES:${PN}-vdkr = "${bindir}/vdkr"
280FILES:${PN}-vpdmn = "${bindir}/vpdmn"
281FILES:${PN}-docker-config = "${sysconfdir}/docker/daemon.json"
282FILES:${PN}-podman-config = "${sysconfdir}/containers/containers.conf.d/50-vxn-runtime.conf"
283
284RDEPENDS:${PN}-vdkr = "${PN} bash"
285RDEPENDS:${PN}-vpdmn = "${PN} bash"
286RDEPENDS:${PN}-docker-config = "${PN} docker"
287RDEPENDS:${PN}-podman-config = "${PN} podman"
288
289# daemon.json conflicts with docker-registry-config (only one provider)
290RCONFLICTS:${PN}-docker-config = "docker-registry-config"
291
256FILES:${PN} = "\ 292FILES:${PN} = "\
257 ${bindir}/vxn \ 293 ${bindir}/vxn \
258 ${bindir}/vxn-oci-runtime \ 294 ${bindir}/vxn-oci-runtime \