summaryrefslogtreecommitdiffstats
path: root/recipes-containers/vcontainer
diff options
context:
space:
mode:
authorBruce Ashfield <bruce.ashfield@gmail.com>2026-01-08 04:54:11 +0000
committerBruce Ashfield <bruce.ashfield@gmail.com>2026-02-09 03:32:52 +0000
commit0ebf4ef7838d5c831a7e56a91981a53480ea5d5f (patch)
treeba776810b61bcd3898614b531724ff8db79d6e0d /recipes-containers/vcontainer
parent9e17cd687de956fd421454f1918131f824b95dbf (diff)
downloadmeta-virtualization-0ebf4ef7838d5c831a7e56a91981a53480ea5d5f.tar.gz
vcontainer: add dynamic port forwarding via QEMU QMP
Add dynamic port forwarding for containers using QEMU's QMP (QEMU Machine Protocol). This enables running multiple detached containers with different port mappings without needing to declare ports upfront. Key changes: - vrunner.sh: Add QMP socket (-qmp unix:...) to daemon startup - vcontainer-common.sh: * QMP helper functions (qmp_send, qmp_add_hostfwd, qmp_remove_hostfwd) * Port forward registry (port-forwards.txt) for tracking mappings * Parse -p, -d, --name flags in run command * Add port forwards via QMP for detached containers * Enhanced ps command to show host port forwards * Cleanup port forwards on stop/rm/vmemres stop Usage: vdkr run -d -p 8080:80 nginx # Adds 8080->80 forward vdkr run -d -p 3000:3000 myapp # Adds 3000->3000 forward vdkr ps # Shows containers + port forwards vdkr stop nginx # Removes 8080->80 forward This solves the problem where auto-started vmemres couldn't handle multiple containers with different port mappings. QMP allows adding and removing port forwards at runtime without restarting the daemon. Signed-off-by: Bruce Ashfield <bruce.ashfield@gmail.com>
Diffstat (limited to 'recipes-containers/vcontainer')
-rwxr-xr-xrecipes-containers/vcontainer/files/vcontainer-common.sh304
-rwxr-xr-xrecipes-containers/vcontainer/files/vrunner.sh4
2 files changed, 291 insertions, 17 deletions
diff --git a/recipes-containers/vcontainer/files/vcontainer-common.sh b/recipes-containers/vcontainer/files/vcontainer-common.sh
index 7fd62514..7d7c0ee6 100755
--- a/recipes-containers/vcontainer/files/vcontainer-common.sh
+++ b/recipes-containers/vcontainer/files/vcontainer-common.sh
@@ -355,17 +355,31 @@ ${BOLD}EXTENDED COMMANDS (${VCONTAINER_RUNTIME_NAME}-specific):${NC}
355 ${CYAN}clean${NC} ${YELLOW}[DEPRECATED]${NC} Use 'vstorage clean' instead 355 ${CYAN}clean${NC} ${YELLOW}[DEPRECATED]${NC} Use 'vstorage clean' instead
356 356
357${BOLD}MEMORY RESIDENT MODE (vmemres):${NC} 357${BOLD}MEMORY RESIDENT MODE (vmemres):${NC}
358 ${CYAN}vmemres start${NC} [-p port:port] Start memory resident VM in background 358 By default, vmemres auto-starts on the first command and stops after idle timeout.
359 This provides fast command execution without manual daemon management.
360
361 ${CYAN}vmemres start${NC} Start memory resident VM in background
359 ${CYAN}vmemres stop${NC} Stop memory resident VM 362 ${CYAN}vmemres stop${NC} Stop memory resident VM
360 ${CYAN}vmemres restart${NC} [--clean] Restart VM (optionally clean state first) 363 ${CYAN}vmemres restart${NC} [--clean] Restart VM (optionally clean state first)
361 ${CYAN}vmemres status${NC} Show memory resident VM status 364 ${CYAN}vmemres status${NC} Show memory resident VM status
362 ${CYAN}vmemres list${NC} List all running memres instances 365 ${CYAN}vmemres list${NC} List all running memres instances
363 (Note: 'memres' also works as an alias for 'vmemres') 366 (Note: 'memres' also works as an alias for 'vmemres')
364 367
365 Port forwarding with vmemres: 368 Auto-start and idle timeout:
366 -p <host_port>:<container_port>[/protocol] 369 - Daemon auto-starts when you run any command (configurable via vconfig auto-daemon)
367 Forward host port to container port (protocol: tcp or udp, default: tcp) 370 - Daemon auto-stops after 30 minutes of inactivity (configurable via vconfig idle-timeout)
368 Multiple -p options can be specified 371 - Use --no-daemon to run commands in ephemeral mode (no daemon)
372
373 ${BOLD}Dynamic Port Forwarding:${NC}
374 When running detached containers with -p, port forwards are added dynamically:
375 ${PROG_NAME} run -d -p 8080:80 nginx # Adds 8080->80 forward
376 ${PROG_NAME} run -d -p 3000:3000 myapp # Adds 3000->3000 forward
377 ${PROG_NAME} ps # Shows containers AND port forwards
378 ${PROG_NAME} stop nginx # Removes 8080->80 forward
379
380 Port format: -p <host_port>:<container_port>[/protocol]
381 - protocol: tcp (default) or udp
382 - Multiple -p options can be specified
369 383
370 ${YELLOW}NOTE:${NC} --network=host is used by default for all containers. 384 ${YELLOW}NOTE:${NC} --network=host is used by default for all containers.
371 Docker bridge networking is not available inside the VM. Host networking 385 Docker bridge networking is not available inside the VM. Host networking
@@ -688,6 +702,139 @@ daemon_is_running() {
688 return 1 702 return 1
689} 703}
690 704
705# ============================================================================
706# QMP (QEMU Machine Protocol) Helpers for Dynamic Port Forwarding
707# ============================================================================
708# These functions communicate with QEMU's QMP socket to add/remove port
709# forwards at runtime without restarting the daemon.
710
711# Get the QMP socket path
712get_qmp_socket() {
713 local state_dir="${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}"
714 echo "$state_dir/qmp.sock"
715}
716
717# Send a QMP command and get the response
718# Usage: qmp_send "command-line"
719qmp_send() {
720 local cmd="$1"
721 local qmp_socket=$(get_qmp_socket)
722
723 if [ ! -S "$qmp_socket" ]; then
724 echo "QMP socket not found: $qmp_socket" >&2
725 return 1
726 fi
727
728 # QMP requires a capabilities negotiation first, then human-monitor-command
729 # We use socat to send the command
730 {
731 echo '{"execute":"qmp_capabilities"}'
732 sleep 0.1
733 echo "{\"execute\":\"human-monitor-command\",\"arguments\":{\"command-line\":\"$cmd\"}}"
734 } | socat - "unix-connect:$qmp_socket" 2>/dev/null
735}
736
737# Add a port forward to the running daemon
738# Usage: qmp_add_hostfwd <host_port> <guest_port> [protocol]
739qmp_add_hostfwd() {
740 local host_port="$1"
741 local guest_port="$2"
742 local protocol="${3:-tcp}"
743
744 [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Adding port forward: ${host_port} -> ${guest_port}/${protocol}" >&2
745
746 local result=$(qmp_send "hostfwd_add user.0 ${protocol}::${host_port}-:${guest_port}")
747 if echo "$result" | grep -q '"error"'; then
748 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Failed to add port forward: $result" >&2
749 return 1
750 fi
751 return 0
752}
753
754# Remove a port forward from the running daemon
755# Usage: qmp_remove_hostfwd <host_port> [protocol]
756qmp_remove_hostfwd() {
757 local host_port="$1"
758 local protocol="${2:-tcp}"
759
760 [ "$VERBOSE" = "true" ] && echo -e "${CYAN}[$VCONTAINER_RUNTIME_NAME]${NC} Removing port forward: ${host_port}/${protocol}" >&2
761
762 local result=$(qmp_send "hostfwd_remove user.0 ${protocol}::${host_port}")
763 if echo "$result" | grep -q '"error"'; then
764 echo -e "${RED}[$VCONTAINER_RUNTIME_NAME]${NC} Failed to remove port forward: $result" >&2
765 return 1
766 fi
767 return 0
768}
769
770# ============================================================================
771# Port Forward Registry
772# ============================================================================
773# Track which ports are forwarded for which containers so we can clean up
774# when containers are stopped.
775
776PORT_FORWARD_FILE=""
777
778get_port_forward_file() {
779 if [ -z "$PORT_FORWARD_FILE" ]; then
780 local state_dir="${STATE_DIR:-$DEFAULT_STATE_DIR/$TARGET_ARCH}"
781 PORT_FORWARD_FILE="$state_dir/port-forwards.txt"
782 fi
783 echo "$PORT_FORWARD_FILE"
784}
785
786# Register a port forward for a container
787# Usage: register_port_forward <container_name> <host_port> <guest_port> [protocol]
788register_port_forward() {
789 local container_name="$1"
790 local host_port="$2"
791 local guest_port="$3"
792 local protocol="${4:-tcp}"
793
794 local pf_file=$(get_port_forward_file)
795 mkdir -p "$(dirname "$pf_file")"
796 echo "${container_name}:${host_port}:${guest_port}:${protocol}" >> "$pf_file"
797}
798
799# Unregister and remove port forwards for a container
800# Usage: unregister_port_forwards <container_name>
801unregister_port_forwards() {
802 local container_name="$1"
803 local pf_file=$(get_port_forward_file)
804
805 if [ ! -f "$pf_file" ]; then
806 return 0
807 fi
808
809 # Find and remove all port forwards for this container
810 local temp_file="${pf_file}.tmp"
811 while IFS=: read -r name host_port guest_port protocol; do
812 if [ "$name" = "$container_name" ]; then
813 qmp_remove_hostfwd "$host_port" "$protocol"
814 else
815 echo "${name}:${host_port}:${guest_port}:${protocol}"
816 fi
817 done < "$pf_file" > "$temp_file"
818 mv "$temp_file" "$pf_file"
819}
820
821# List all registered port forwards
822# Usage: list_port_forwards [container_name]
823list_port_forwards() {
824 local filter_name="$1"
825 local pf_file=$(get_port_forward_file)
826
827 if [ ! -f "$pf_file" ]; then
828 return 0
829 fi
830
831 while IFS=: read -r name host_port guest_port protocol; do
832 if [ -z "$filter_name" ] || [ "$name" = "$filter_name" ]; then
833 echo "0.0.0.0:${host_port}->${guest_port}/${protocol}"
834 fi
835 done < "$pf_file"
836}
837
691# Helper function to run command via daemon or regular mode 838# Helper function to run command via daemon or regular mode
692run_runtime_command() { 839run_runtime_command() {
693 local runtime_cmd="$1" 840 local runtime_cmd="$1"
@@ -1166,12 +1313,37 @@ case "$COMMAND" in
1166 1313
1167 # Container lifecycle commands 1314 # Container lifecycle commands
1168 ps) 1315 ps)
1169 # List containers 1316 # List containers and show port forwards if daemon is running
1170 run_runtime_command "$VCONTAINER_RUNTIME_CMD ps ${COMMAND_ARGS[*]}" 1317 run_runtime_command "$VCONTAINER_RUNTIME_CMD ps ${COMMAND_ARGS[*]}"
1318 PS_EXIT=$?
1319
1320 # Show host port forwards if daemon is running and we have any
1321 if daemon_is_running; then
1322 local pf_file=$(get_port_forward_file)
1323 if [ -f "$pf_file" ] && [ -s "$pf_file" ]; then
1324 echo ""
1325 echo -e "${CYAN}Host Port Forwards (QEMU):${NC}"
1326 printf " %-20s %-15s %-15s %-8s\n" "CONTAINER" "HOST PORT" "GUEST PORT" "PROTO"
1327 while IFS=: read -r name host_port guest_port protocol; do
1328 printf " %-20s %-15s %-15s %-8s\n" "$name" "0.0.0.0:$host_port" "$guest_port" "${protocol:-tcp}"
1329 done < "$pf_file"
1330 fi
1331 fi
1332 exit $PS_EXIT
1171 ;; 1333 ;;
1172 1334
1173 rm) 1335 rm)
1174 # Remove containers 1336 # Remove containers and cleanup any registered port forwards
1337 for arg in "${COMMAND_ARGS[@]}"; do
1338 # Skip flags like -f, --force, etc.
1339 case "$arg" in
1340 -*) continue ;;
1341 esac
1342 # This is a container name/id - clean up its port forwards
1343 if daemon_is_running; then
1344 unregister_port_forwards "$arg"
1345 fi
1346 done
1175 run_runtime_command "$VCONTAINER_RUNTIME_CMD rm ${COMMAND_ARGS[*]}" 1347 run_runtime_command "$VCONTAINER_RUNTIME_CMD rm ${COMMAND_ARGS[*]}"
1176 ;; 1348 ;;
1177 1349
@@ -1185,11 +1357,23 @@ case "$COMMAND" in
1185 run_runtime_command "$VCONTAINER_RUNTIME_CMD inspect ${COMMAND_ARGS[*]}" 1357 run_runtime_command "$VCONTAINER_RUNTIME_CMD inspect ${COMMAND_ARGS[*]}"
1186 ;; 1358 ;;
1187 1359
1188 start|stop|restart|kill|pause|unpause) 1360 start|restart|kill|pause|unpause)
1189 # Container state commands 1361 # Container state commands (no special handling needed)
1190 run_runtime_command "$VCONTAINER_RUNTIME_CMD $COMMAND ${COMMAND_ARGS[*]}" 1362 run_runtime_command "$VCONTAINER_RUNTIME_CMD $COMMAND ${COMMAND_ARGS[*]}"
1191 ;; 1363 ;;
1192 1364
1365 stop)
1366 # Stop container and cleanup any registered port forwards
1367 if [ ${#COMMAND_ARGS[@]} -ge 1 ]; then
1368 STOP_CONTAINER_NAME="${COMMAND_ARGS[0]}"
1369 # Remove port forwards for this container (if any)
1370 if daemon_is_running; then
1371 unregister_port_forwards "$STOP_CONTAINER_NAME"
1372 fi
1373 fi
1374 run_runtime_command "$VCONTAINER_RUNTIME_CMD stop ${COMMAND_ARGS[*]}"
1375 ;;
1376
1193 # Image commands 1377 # Image commands
1194 commit) 1378 commit)
1195 # Commit container to image 1379 # Commit container to image
@@ -1335,7 +1519,7 @@ case "$COMMAND" in
1335 1519
1336 vconfig) 1520 vconfig)
1337 # Configuration management (runs on host, not in VM) 1521 # Configuration management (runs on host, not in VM)
1338 VALID_KEYS="arch timeout state-dir verbose" 1522 VALID_KEYS="arch timeout state-dir verbose idle-timeout auto-daemon"
1339 1523
1340 if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then 1524 if [ ${#COMMAND_ARGS[@]} -lt 1 ]; then
1341 # Show all config 1525 # Show all config
@@ -1800,17 +1984,46 @@ case "$COMMAND" in
1800 exit 1 1984 exit 1
1801 fi 1985 fi
1802 1986
1803 # Check if any volume mounts are present and if user specified --network 1987 # Check if any volume mounts, network, port forwards, or detach are present
1804 RUN_HAS_VOLUMES=false 1988 RUN_HAS_VOLUMES=false
1805 RUN_HAS_NETWORK=false 1989 RUN_HAS_NETWORK=false
1990 RUN_HAS_PORT_FORWARDS=false
1991 RUN_IS_DETACHED=false
1992 RUN_CONTAINER_NAME=""
1993 RUN_PORT_FORWARDS=()
1994
1995 # Parse COMMAND_ARGS to extract relevant flags
1996 local i=0
1997 local prev_arg=""
1806 for arg in "${COMMAND_ARGS[@]}"; do 1998 for arg in "${COMMAND_ARGS[@]}"; do
1807 if [ "$arg" = "-v" ] || [ "$arg" = "--volume" ]; then
1808 RUN_HAS_VOLUMES=true
1809 fi
1810 # Check for explicit --network option (user override)
1811 case "$arg" in 1999 case "$arg" in
1812 --network=*|--net=*) RUN_HAS_NETWORK=true ;; 2000 -v|--volume)
2001 RUN_HAS_VOLUMES=true
2002 ;;
2003 --network=*|--net=*)
2004 RUN_HAS_NETWORK=true
2005 ;;
2006 -p|--publish)
2007 RUN_HAS_PORT_FORWARDS=true
2008 ;;
2009 -d|--detach)
2010 RUN_IS_DETACHED=true
2011 ;;
2012 --name=*)
2013 RUN_CONTAINER_NAME="${arg#--name=}"
2014 ;;
1813 esac 2015 esac
2016 # Check if previous arg was -p or --publish
2017 if [ "$prev_arg" = "-p" ] || [ "$prev_arg" = "--publish" ]; then
2018 # arg is the port specification (e.g., 8080:80)
2019 RUN_PORT_FORWARDS+=("$arg")
2020 fi
2021 # Check if previous arg was --name
2022 if [ "$prev_arg" = "--name" ]; then
2023 RUN_CONTAINER_NAME="$arg"
2024 fi
2025 prev_arg="$arg"
2026 i=$((i + 1))
1814 done 2027 done
1815 2028
1816 # Volume mounts require daemon mode 2029 # Volume mounts require daemon mode
@@ -1885,6 +2098,54 @@ case "$COMMAND" in
1885 fi 2098 fi
1886 else 2099 else
1887 # Non-interactive - use daemon mode when available 2100 # Non-interactive - use daemon mode when available
2101
2102 # For detached containers with port forwards, add them dynamically via QMP
2103 if [ "$RUN_IS_DETACHED" = "true" ] && [ "$RUN_HAS_PORT_FORWARDS" = "true" ] && daemon_is_running; then
2104 # Generate container name if not provided (needed for port tracking)
2105 if [ -z "$RUN_CONTAINER_NAME" ]; then
2106 # Generate a random name like docker does
2107 RUN_CONTAINER_NAME="$(cat /proc/sys/kernel/random/uuid | cut -c1-12)"
2108 # Update COMMAND_ARGS to include the generated name
2109 RUNTIME_CMD="$VCONTAINER_RUNTIME_CMD run --name=$RUN_CONTAINER_NAME $RUN_NETWORK_OPTS ${COMMAND_ARGS[*]}"
2110 fi
2111
2112 # Add port forwards via QMP and register them
2113 for port_spec in "${RUN_PORT_FORWARDS[@]}"; do
2114 # Parse port specification: [host_ip:]host_port:container_port[/protocol]
2115 # Examples: 8080:80, 127.0.0.1:8080:80, 8080:80/tcp, 8080:80/udp
2116 local spec="$port_spec"
2117 local protocol="tcp"
2118 local host_port=""
2119 local guest_port=""
2120
2121 # Extract protocol if present
2122 if echo "$spec" | grep -q '/'; then
2123 protocol="${spec##*/}"
2124 spec="${spec%/*}"
2125 fi
2126
2127 # Count colons to determine format
2128 local colon_count=$(echo "$spec" | tr -cd ':' | wc -c)
2129 if [ "$colon_count" -eq 2 ]; then
2130 # Format: host_ip:host_port:container_port (ignore host_ip for now)
2131 host_port=$(echo "$spec" | cut -d: -f2)
2132 guest_port=$(echo "$spec" | cut -d: -f3)
2133 else
2134 # Format: host_port:container_port
2135 host_port=$(echo "$spec" | cut -d: -f1)
2136 guest_port=$(echo "$spec" | cut -d: -f2)
2137 fi
2138
2139 if [ -n "$host_port" ] && [ -n "$guest_port" ]; then
2140 if qmp_add_hostfwd "$host_port" "$guest_port" "$protocol"; then
2141 register_port_forward "$RUN_CONTAINER_NAME" "$host_port" "$guest_port" "$protocol"
2142 else
2143 echo -e "${YELLOW}[$VCONTAINER_RUNTIME_NAME]${NC} Warning: Could not add port forward ${host_port}:${guest_port}" >&2
2144 fi
2145 fi
2146 done
2147 fi
2148
1888 run_runtime_command "$RUNTIME_CMD" 2149 run_runtime_command "$RUNTIME_CMD"
1889 RUN_EXIT=$? 2150 RUN_EXIT=$?
1890 2151
@@ -1970,10 +2231,19 @@ case "$COMMAND" in
1970 fi 2231 fi
1971 ;; 2232 ;;
1972 stop) 2233 stop)
2234 # Clear port forward registry when stopping daemon
2235 local pf_file=$(get_port_forward_file)
2236 if [ -f "$pf_file" ]; then
2237 rm -f "$pf_file"
2238 fi
1973 "$RUNNER" $RUNNER_ARGS --daemon-stop 2239 "$RUNNER" $RUNNER_ARGS --daemon-stop
1974 ;; 2240 ;;
1975 restart) 2241 restart)
1976 # Stop if running 2242 # Stop if running and clear port forward registry
2243 local pf_file=$(get_port_forward_file)
2244 if [ -f "$pf_file" ]; then
2245 rm -f "$pf_file"
2246 fi
1977 "$RUNNER" $RUNNER_ARGS --daemon-stop 2>/dev/null || true 2247 "$RUNNER" $RUNNER_ARGS --daemon-stop 2>/dev/null || true
1978 2248
1979 # Clean if --clean was passed 2249 # Clean if --clean was passed
diff --git a/recipes-containers/vcontainer/files/vrunner.sh b/recipes-containers/vcontainer/files/vrunner.sh
index cd504f7b..c9950618 100755
--- a/recipes-containers/vcontainer/files/vrunner.sh
+++ b/recipes-containers/vcontainer/files/vrunner.sh
@@ -1073,6 +1073,10 @@ if [ "$DAEMON_MODE" = "start" ]; then
1073 QEMU_OPTS="$QEMU_OPTS -device virtio-serial-pci" 1073 QEMU_OPTS="$QEMU_OPTS -device virtio-serial-pci"
1074 QEMU_OPTS="$QEMU_OPTS -device virtserialport,chardev=vdkr,name=vdkr" 1074 QEMU_OPTS="$QEMU_OPTS -device virtserialport,chardev=vdkr,name=vdkr"
1075 1075
1076 # Add QMP socket for dynamic control (port forwarding, etc.)
1077 QMP_SOCKET="$DAEMON_SOCKET_DIR/qmp.sock"
1078 QEMU_OPTS="$QEMU_OPTS -qmp unix:$QMP_SOCKET,server,nowait"
1079
1076 # Tell init script to run in daemon mode with idle timeout 1080 # Tell init script to run in daemon mode with idle timeout
1077 KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_daemon=1" 1081 KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_daemon=1"
1078 KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_idle_timeout=$IDLE_TIMEOUT" 1082 KERNEL_APPEND="$KERNEL_APPEND ${CMDLINE_PREFIX}_idle_timeout=$IDLE_TIMEOUT"