diff options
| -rw-r--r-- | recipes-containers/container-registry/container-oci-registry-config.bb | 145 | ||||
| -rw-r--r-- | recipes-containers/container-registry/docker-registry-config.bb | 24 | ||||
| -rwxr-xr-x | recipes-containers/vcontainer/files/vdkr.sh | 26 | ||||
| -rwxr-xr-x | recipes-containers/vcontainer/files/vpdmn.sh | 26 | ||||
| -rwxr-xr-x | recipes-containers/vcontainer/files/vrunner.sh | 7 | ||||
| -rw-r--r-- | recipes-containers/vcontainer/files/vxn-oci-runtime | 148 | ||||
| -rw-r--r-- | recipes-core/vxn/README.md | 159 | ||||
| -rw-r--r-- | recipes-core/vxn/vxn_1.0.bb | 36 |
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 |
| 65 | CONTAINER_REGISTRY_AUTHFILE ?= "" | 65 | CONTAINER_REGISTRY_AUTHFILE ?= "" |
| 66 | 66 | ||
| 67 | # OCI runtime configuration for Podman | ||
| 68 | # Runtime name (e.g. "vxn") — creates a containers.conf.d drop-in | ||
| 69 | PODMAN_OCI_RUNTIME ?= "" | ||
| 70 | # Path to OCI runtime binary (e.g. "/usr/bin/vxn-oci-runtime") | ||
| 71 | PODMAN_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 |
| 69 | python() { | 75 | python() { |
| 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 | ||
| 168 | FILES:${PN} = " \ | 194 | FILES:${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 |
| 61 | CONTAINER_REGISTRY_AUTHFILE ?= "" | 61 | CONTAINER_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"}}' | ||
| 66 | DOCKER_OCI_RUNTIMES ?= "" | ||
| 67 | # Default OCI runtime name (must be a key in DOCKER_OCI_RUNTIMES or "runc") | ||
| 68 | DOCKER_DEFAULT_RUNTIME ?= "" | ||
| 69 | |||
| 63 | def get_insecure_registries(d): | 70 | def 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" | |||
| 21 | VCONTAINER_OTHER_PREFIX="VPDMN" | 21 | VCONTAINER_OTHER_PREFIX="VPDMN" |
| 22 | VCONTAINER_VERSION="3.4.0" | 22 | VCONTAINER_VERSION="3.4.0" |
| 23 | 23 | ||
| 24 | # Source common implementation | 24 | # Auto-detect Xen if not explicitly set |
| 25 | source "$(dirname "${BASH_SOURCE[0]}")/vcontainer-common.sh" "$@" | 25 | if [ -z "${VCONTAINER_HYPERVISOR:-}" ]; then |
| 26 | if command -v xl >/dev/null 2>&1; then | ||
| 27 | export VCONTAINER_HYPERVISOR="xen" | ||
| 28 | fi | ||
| 29 | fi | ||
| 30 | |||
| 31 | # Fall back to vxn blob dir on Dom0 | ||
| 32 | if [ -z "${VDKR_BLOB_DIR:-}" ] && [ -d "/usr/share/vxn" ]; then | ||
| 33 | export VDKR_BLOB_DIR="/usr/share/vxn" | ||
| 34 | fi | ||
| 35 | |||
| 36 | # Two-phase lib lookup: script dir (dev), then /usr/lib/vxn (target) | ||
| 37 | SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")" | ||
| 38 | if [ -f "${SCRIPT_DIR}/vcontainer-common.sh" ]; then | ||
| 39 | export VCONTAINER_LIBDIR="${SCRIPT_DIR}" | ||
| 40 | source "${SCRIPT_DIR}/vcontainer-common.sh" "$@" | ||
| 41 | elif [ -f "/usr/lib/vxn/vcontainer-common.sh" ]; then | ||
| 42 | export VCONTAINER_LIBDIR="/usr/lib/vxn" | ||
| 43 | source "/usr/lib/vxn/vcontainer-common.sh" "$@" | ||
| 44 | else | ||
| 45 | echo "Error: vcontainer-common.sh not found" >&2 | ||
| 46 | exit 1 | ||
| 47 | fi | ||
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" | |||
| 21 | VCONTAINER_OTHER_PREFIX="VDKR" | 21 | VCONTAINER_OTHER_PREFIX="VDKR" |
| 22 | VCONTAINER_VERSION="1.2.0" | 22 | VCONTAINER_VERSION="1.2.0" |
| 23 | 23 | ||
| 24 | # Source common implementation | 24 | # Auto-detect Xen if not explicitly set |
| 25 | source "$(dirname "${BASH_SOURCE[0]}")/vcontainer-common.sh" "$@" | 25 | if [ -z "${VCONTAINER_HYPERVISOR:-}" ]; then |
| 26 | if command -v xl >/dev/null 2>&1; then | ||
| 27 | export VCONTAINER_HYPERVISOR="xen" | ||
| 28 | fi | ||
| 29 | fi | ||
| 30 | |||
| 31 | # Fall back to vxn blob dir on Dom0 | ||
| 32 | if [ -z "${VPDMN_BLOB_DIR:-}" ] && [ -d "/usr/share/vxn" ]; then | ||
| 33 | export VPDMN_BLOB_DIR="/usr/share/vxn" | ||
| 34 | fi | ||
| 35 | |||
| 36 | # Two-phase lib lookup: script dir (dev), then /usr/lib/vxn (target) | ||
| 37 | SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")" | ||
| 38 | if [ -f "${SCRIPT_DIR}/vcontainer-common.sh" ]; then | ||
| 39 | export VCONTAINER_LIBDIR="${SCRIPT_DIR}" | ||
| 40 | source "${SCRIPT_DIR}/vcontainer-common.sh" "$@" | ||
| 41 | elif [ -f "/usr/lib/vxn/vcontainer-common.sh" ]; then | ||
| 42 | export VCONTAINER_LIBDIR="/usr/lib/vxn" | ||
| 43 | source "/usr/lib/vxn/vcontainer-common.sh" "$@" | ||
| 44 | else | ||
| 45 | echo "Error: vcontainer-common.sh not found" >&2 | ||
| 46 | exit 1 | ||
| 47 | fi | ||
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 | |||
| 515 | fi | 515 | fi |
| 516 | source "$HV_BACKEND" | 516 | source "$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. | ||
| 521 | if [ "$VCONTAINER_HYPERVISOR" = "xen" ]; then | ||
| 522 | CMDLINE_PREFIX="docker" | ||
| 523 | fi | ||
| 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" | |||
| 37 | LOG_FILE="/var/log/vxn-oci-runtime.log" | 37 | LOG_FILE="/var/log/vxn-oci-runtime.log" |
| 38 | VXN_LOG="/var/log/vxn-oci-runtime.log" | 38 | VXN_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 | |||
| 40 | log() { | 49 | log() { |
| 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 | ||
| 50 | die() { | 60 | die() { |
| 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 | ||
| 464 | cmd_kill() { | 521 | cmd_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 | |||
| 3 | vxn runs OCI containers as Xen DomU guests. The VM IS the container — no | ||
| 4 | Docker/Podman daemon runs inside the guest. The guest boots a minimal Linux, | ||
| 5 | mounts 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 | |||
| 21 | No additional packages needed beyond `vxn`. containerd is configured | ||
| 22 | automatically via `/etc/containerd/config.toml`. | ||
| 23 | |||
| 24 | ```bash | ||
| 25 | ctr image pull docker.io/library/alpine:latest | ||
| 26 | vctr run --rm docker.io/library/alpine:latest test1 /bin/echo hello | ||
| 27 | ctr 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 | |||
| 32 | Install `vxn-vdkr` or `vxn-vpdmn`. These are standalone frontends that | ||
| 33 | auto-detect Xen (via `xl`) and manage containers without any daemon process. | ||
| 34 | They handle OCI image pull/unpack on the host via skopeo. | ||
| 35 | |||
| 36 | ```bash | ||
| 37 | vdkr run --rm alpine echo hello # Docker-like | ||
| 38 | vpdmn run --rm alpine echo hello # Podman-like | ||
| 39 | ``` | ||
| 40 | |||
| 41 | Persistent DomU (memres) for faster subsequent runs: | ||
| 42 | ```bash | ||
| 43 | vdkr vmemres start # Boot persistent DomU (~10s) | ||
| 44 | vdkr run --rm alpine echo hello # Hot-plug container (~1s) | ||
| 45 | vdkr vmemres stop # Shutdown DomU | ||
| 46 | ``` | ||
| 47 | |||
| 48 | ### 3. Native Docker with vxn runtime | ||
| 49 | |||
| 50 | Install `vxn-docker-config` to register vxn-oci-runtime as Docker's default | ||
| 51 | OCI runtime. Docker manages images (pull/tag/rmi) natively. | ||
| 52 | |||
| 53 | ```bash | ||
| 54 | docker run --rm --network=none alpine echo hello | ||
| 55 | docker run --rm --network=host alpine echo hello | ||
| 56 | ``` | ||
| 57 | |||
| 58 | **IMPORTANT: Networking** — Docker's default bridge networking is incompatible | ||
| 59 | with VM-based runtimes. Docker tries to create veth pairs and move them into | ||
| 60 | a Linux network namespace, but vxn containers are Xen DomUs with their own | ||
| 61 | kernel network stack. You MUST use `--network=none` or `--network=host`. | ||
| 62 | |||
| 63 | This is the same limitation as kata-containers. The long-term fix is a TAP | ||
| 64 | bridge that connects Docker's network namespace to the DomU's vif (see TODO). | ||
| 65 | |||
| 66 | For selective use (keep runc as default, use vxn per-run): | ||
| 67 | ```bash | ||
| 68 | docker run --rm --runtime=vxn --network=none alpine echo hello | ||
| 69 | ``` | ||
| 70 | |||
| 71 | ### 4. Native Podman with vxn runtime | ||
| 72 | |||
| 73 | Install `vxn-podman-config` to register vxn-oci-runtime as Podman's default | ||
| 74 | OCI runtime. Same networking constraints as Docker. | ||
| 75 | |||
| 76 | ```bash | ||
| 77 | podman run --rm --network=none alpine echo hello | ||
| 78 | ``` | ||
| 79 | |||
| 80 | ## Build Instructions | ||
| 81 | |||
| 82 | ```bash | ||
| 83 | # Prerequisites in local.conf: | ||
| 84 | DISTRO_FEATURES:append = " xen virtualization vcontainer" | ||
| 85 | BBMULTICONFIG = "vruntime-aarch64 vruntime-x86-64" | ||
| 86 | |||
| 87 | # Build (mcdepends auto-builds vruntime blobs) | ||
| 88 | bitbake vxn | ||
| 89 | |||
| 90 | # Dom0 image with containerd + Docker-like CLI | ||
| 91 | IMAGE_INSTALL:append = " vxn vxn-vdkr" | ||
| 92 | |||
| 93 | # Dom0 image with native Docker integration | ||
| 94 | IMAGE_INSTALL:append = " vxn vxn-docker-config docker" | ||
| 95 | |||
| 96 | bitbake xen-image-minimal | ||
| 97 | ``` | ||
| 98 | |||
| 99 | ## Architecture | ||
| 100 | |||
| 101 | ``` | ||
| 102 | Docker/Podman/containerd → vxn-oci-runtime → xl create/unpause/destroy → Xen DomU | ||
| 103 | ↓ | ||
| 104 | vxn-init.sh | ||
| 105 | mount rootfs | ||
| 106 | chroot + exec | ||
| 107 | ``` | ||
| 108 | |||
| 109 | The OCI runtime (`/usr/bin/vxn-oci-runtime`) implements the standard | ||
| 110 | create/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 | |||
| 123 | Docker and Podman's default bridge networking creates Linux veth pairs and | ||
| 124 | moves one end into a container network namespace. This is fundamentally | ||
| 125 | incompatible with VM-based runtimes where the "container" is a VM with its | ||
| 126 | own 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):** | ||
| 133 | TAP bridge integration — read Docker's network namespace config from | ||
| 134 | config.json, create a TAP device bridged to the DomU's vif. This is the | ||
| 135 | approach kata-containers uses to provide Docker-compatible networking with | ||
| 136 | VM 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) | ||
| 146 | cat /var/log/vxn-oci-runtime.log | ||
| 147 | |||
| 148 | # Per-container console capture (persists after container exit) | ||
| 149 | ls /var/log/vxn-oci-runtime/containers/ | ||
| 150 | |||
| 151 | # Xen domain status | ||
| 152 | xl list | ||
| 153 | |||
| 154 | # Watch domain console | ||
| 155 | xl console <domname> | ||
| 156 | |||
| 157 | # Kill stuck domain | ||
| 158 | xl 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 | ||
| 72 | FILESEXTRAPATHS:prepend := "${THISDIR}/../../recipes-containers/vcontainer/files:" | 74 | FILESEXTRAPATHS: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 | ||
| 277 | PACKAGES =+ "${PN}-vdkr ${PN}-vpdmn ${PN}-docker-config ${PN}-podman-config" | ||
| 278 | |||
| 279 | FILES:${PN}-vdkr = "${bindir}/vdkr" | ||
| 280 | FILES:${PN}-vpdmn = "${bindir}/vpdmn" | ||
| 281 | FILES:${PN}-docker-config = "${sysconfdir}/docker/daemon.json" | ||
| 282 | FILES:${PN}-podman-config = "${sysconfdir}/containers/containers.conf.d/50-vxn-runtime.conf" | ||
| 283 | |||
| 284 | RDEPENDS:${PN}-vdkr = "${PN} bash" | ||
| 285 | RDEPENDS:${PN}-vpdmn = "${PN} bash" | ||
| 286 | RDEPENDS:${PN}-docker-config = "${PN} docker" | ||
| 287 | RDEPENDS:${PN}-podman-config = "${PN} podman" | ||
| 288 | |||
| 289 | # daemon.json conflicts with docker-registry-config (only one provider) | ||
| 290 | RCONFLICTS:${PN}-docker-config = "docker-registry-config" | ||
| 291 | |||
| 256 | FILES:${PN} = "\ | 292 | FILES:${PN} = "\ |
| 257 | ${bindir}/vxn \ | 293 | ${bindir}/vxn \ |
| 258 | ${bindir}/vxn-oci-runtime \ | 294 | ${bindir}/vxn-oci-runtime \ |
