mirror of
https://github.com/vdsm/virtual-dsm.git
synced 2026-08-29 20:26:57 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c70b93c2b6 | ||
|
|
4c700e87fe | ||
|
|
030255004f | ||
|
|
e64f9f2c39 | ||
|
|
537fc97d53 | ||
|
|
438e992653 | ||
|
|
615fad2a0d | ||
|
|
aac543e5c7 | ||
|
|
fec4f876e7 | ||
|
|
dced815c06 | ||
|
|
51d6f3aeca | ||
|
|
4544620d48 | ||
|
|
de8468161f | ||
|
|
18d6be8210 | ||
|
|
e5b8cf3bf8 | ||
|
|
ec12039f43 | ||
|
|
3c7c3ca1b1 | ||
|
|
2b27f32cd4 | ||
|
|
55d1d50284 | ||
|
|
784b73b5b5 | ||
|
|
85f00bb9cc | ||
|
|
efe8732e47 | ||
|
|
f6871dd61b | ||
|
|
a713b728ff | ||
|
|
0d74c6ac80 | ||
|
|
63e4529eff | ||
|
|
afd848cebb | ||
|
|
695b075130 | ||
|
|
7eff53e722 | ||
|
|
bcaf0e5980 | ||
|
|
faa820c2cc | ||
|
|
81e477fcc4 | ||
|
|
abd3a1c3df | ||
|
|
3b59bcd284 |
@@ -1,4 +1,5 @@
|
|||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: docker
|
||||||
directory: /
|
directory: /
|
||||||
@@ -6,9 +7,15 @@ updates:
|
|||||||
interval: weekly
|
interval: weekly
|
||||||
cooldown:
|
cooldown:
|
||||||
default-days: 7
|
default-days: 7
|
||||||
|
|
||||||
- package-ecosystem: github-actions
|
- package-ecosystem: github-actions
|
||||||
directory: /
|
directory: /
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: weekly
|
||||||
cooldown:
|
cooldown:
|
||||||
default-days: 7
|
default-days: 7
|
||||||
|
ignore:
|
||||||
|
- dependency-name: "*"
|
||||||
|
update-types:
|
||||||
|
- version-update:semver-minor
|
||||||
|
- version-update:semver-patch
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
-e SC2153
|
-e SC2153
|
||||||
-
|
-
|
||||||
name: Lint Dockerfile
|
name: Lint Dockerfile
|
||||||
uses: hadolint/hadolint-action@v3.3.0
|
uses: hadolint/hadolint-action@v3.4.0
|
||||||
with:
|
with:
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
ignore: DL3008
|
ignore: DL3008
|
||||||
|
|||||||
+11
-15
@@ -7,8 +7,9 @@ ARG TARGETARCH
|
|||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
ARG VERSION_ARG="0.0"
|
ARG VERSION_ARG="0.0"
|
||||||
|
ARG VERSION_WSD="0.4.2"
|
||||||
ARG VERSION_CSTRUCT="4.7"
|
ARG VERSION_CSTRUCT="4.7"
|
||||||
ARG VERSION_PASST="2026_07_16"
|
ARG VERSION_PASST="2026_07_28"
|
||||||
|
|
||||||
ARG DEBCONF_NOWARNINGS="yes"
|
ARG DEBCONF_NOWARNINGS="yes"
|
||||||
ARG DEBIAN_FRONTEND="noninteractive"
|
ARG DEBIAN_FRONTEND="noninteractive"
|
||||||
@@ -29,10 +30,6 @@ RUN <<EOF
|
|||||||
procps \
|
procps \
|
||||||
ipcalc \
|
ipcalc \
|
||||||
ethtool \
|
ethtool \
|
||||||
python3 \
|
|
||||||
python3-pip \
|
|
||||||
python3-msgpack \
|
|
||||||
python3-pysodium \
|
|
||||||
xz-utils \
|
xz-utils \
|
||||||
iptables \
|
iptables \
|
||||||
iproute2 \
|
iproute2 \
|
||||||
@@ -43,30 +40,29 @@ RUN <<EOF
|
|||||||
e2fsprogs \
|
e2fsprogs \
|
||||||
diffutils \
|
diffutils \
|
||||||
qemu-utils \
|
qemu-utils \
|
||||||
websocketd \
|
|
||||||
iputils-ping \
|
iputils-ping \
|
||||||
inotify-tools \
|
inotify-tools \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
netcat-openbsd \
|
netcat-openbsd \
|
||||||
qemu-system-x86
|
qemu-system-x86 \
|
||||||
|
python3 \
|
||||||
|
python3-pip \
|
||||||
|
python3-msgpack \
|
||||||
|
python3-pysodium
|
||||||
|
|
||||||
# Install Passt package
|
# Install Passt package
|
||||||
wget "https://github.com/qemus/passt/releases/download/v${VERSION_PASST}/passt_${VERSION_PASST}_${TARGETARCH}.deb" -O /tmp/passt.deb -q --timeout=10
|
wget "https://github.com/qemus/passt/releases/download/v${VERSION_PASST}/passt_${VERSION_PASST}_${TARGETARCH}.deb" -O /tmp/passt.deb -q --timeout=10
|
||||||
dpkg -i /tmp/passt.deb
|
dpkg -i /tmp/passt.deb
|
||||||
|
|
||||||
|
# Install Websocketd package
|
||||||
|
wget "https://github.com/qemus/websocketd/releases/download/v${VERSION_WSD}/websocketd-${VERSION_WSD}_${TARGETARCH}.deb" -O /tmp/wsd.deb -q --timeout=10
|
||||||
|
dpkg -i /tmp/wsd.deb
|
||||||
|
|
||||||
apt-get clean
|
apt-get clean
|
||||||
|
|
||||||
# Install Python dependencies
|
# Install Python dependencies
|
||||||
pip3 install --no-cache-dir --break-system-packages --root-user-action=ignore "dissect.cstruct==$VERSION_CSTRUCT"
|
pip3 install --no-cache-dir --break-system-packages --root-user-action=ignore "dissect.cstruct==$VERSION_CSTRUCT"
|
||||||
|
|
||||||
# Configure QEMU
|
|
||||||
mkdir -p /etc/qemu
|
|
||||||
echo "allow br0" > /etc/qemu/bridge.conf
|
|
||||||
|
|
||||||
# Configure nginx
|
|
||||||
unlink /etc/nginx/sites-enabled/default
|
|
||||||
sed -i 's/^worker_processes.*/worker_processes 1;/' /etc/nginx/nginx.conf
|
|
||||||
|
|
||||||
# Set version file
|
# Set version file
|
||||||
echo "$VERSION_ARG" > /etc/version
|
echo "$VERSION_ARG" > /etc/version
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ An empty default means the variable is unset and its value is determined automat
|
|||||||
| `DISK_DISCARD` | `unmap` | Discard/TRIM mode for the primary disk. |
|
| `DISK_DISCARD` | `unmap` | Discard/TRIM mode for the primary disk. |
|
||||||
| `DISK_ROTATION` | `1` | Rotation rate reported to the guest. Use `1` to identify the disk as an SSD. |
|
| `DISK_ROTATION` | `1` | Rotation rate reported to the guest. Use `1` to identify the disk as an SSD. |
|
||||||
| `DISK_FLAGS` | | Additional options used when creating `qcow2` disks. |
|
| `DISK_FLAGS` | | Additional options used when creating `qcow2` disks. |
|
||||||
|
| `DISK_OPTIONS` | | Additional options appended to QEMU disk devices. |
|
||||||
| `ALLOCATE` | `N` | Preallocates space for the data disks. |
|
| `ALLOCATE` | `N` | Preallocates space for the data disks. |
|
||||||
| `STORAGE` | `/storage` | Storage directory used for disks, settings, and downloads. |
|
| `STORAGE` | `/storage` | Storage directory used for disks, settings, and downloads. |
|
||||||
|
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/m
|
|||||||
|
|
||||||
- Docker or Podman on a Linux host with KVM support.
|
- Docker or Podman on a Linux host with KVM support.
|
||||||
- Docker Desktop or Podman (Desktop) on Windows 11 with nested virtualization enabled.
|
- Docker Desktop or Podman (Desktop) on Windows 11 with nested virtualization enabled.
|
||||||
- At least 2 GB of available RAM.
|
- At least 1 GB of available RAM.
|
||||||
- At least 32 GB of free disk space.
|
- At least 16 GB of free disk space.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> Docker Desktop on Linux, macOS, and Windows 10 does not currently provide KVM access to containers and is therefore not supported.
|
> Docker Desktop on Linux, macOS, and Windows 10 does not currently provide KVM access to containers and is therefore not supported.
|
||||||
|
|||||||
+5
-1
@@ -15,12 +15,16 @@ file="/run/shm/dsm.url"
|
|||||||
address="/run/shm/qemu.ip"
|
address="/run/shm/qemu.ip"
|
||||||
gateway="/run/shm/qemu.gw"
|
gateway="/run/shm/qemu.gw"
|
||||||
|
|
||||||
[ ! -s "$file" ] && echo "DSM has not enabled networking yet..." && exit 1
|
# dsm.url is written only after the guest agent reports both the DSM
|
||||||
|
# address and its configured HTTP port.
|
||||||
|
[ ! -s "$file" ] && echo "DSM has not enabled networking yet..." && exit 0
|
||||||
|
|
||||||
location=$(<"$file")
|
location=$(<"$file")
|
||||||
|
|
||||||
if ! curl -m 20 -ILfSs "http://$location/" > /dev/null; then
|
if ! curl -m 20 -ILfSs "http://$location/" > /dev/null; then
|
||||||
|
|
||||||
|
# In DHCP mode the firewall must allow the container address; with port
|
||||||
|
# forwarding it must allow the internal gateway used to reach the guest.
|
||||||
if enabled "$DHCP"; then
|
if enabled "$DHCP"; then
|
||||||
ip=$(<"$address")
|
ip=$(<"$address")
|
||||||
echo "Failed to reach DSM at http://$location"
|
echo "Failed to reach DSM at http://$location"
|
||||||
|
|||||||
+13
-2
@@ -6,6 +6,8 @@ DEV_OPTS=""
|
|||||||
|
|
||||||
configureProcessor() {
|
configureProcessor() {
|
||||||
|
|
||||||
|
# Expose one thread per core in a single socket; DSM licensing and topology
|
||||||
|
# reporting are more predictable with this fixed layout.
|
||||||
CPU_OPTS="-cpu $CPU_FLAGS"
|
CPU_OPTS="-cpu $CPU_FLAGS"
|
||||||
CPU_OPTS+=" -smp $CPU_CORES,sockets=1,dies=1,cores=$CPU_CORES,threads=1"
|
CPU_OPTS+=" -smp $CPU_CORES,sockets=1,dies=1,cores=$CPU_CORES,threads=1"
|
||||||
|
|
||||||
@@ -29,6 +31,8 @@ configureMonitor() {
|
|||||||
|
|
||||||
configureMachine() {
|
configureMachine() {
|
||||||
|
|
||||||
|
# Disable firmware and chipset features that Virtual DSM does not use and
|
||||||
|
# that can introduce extra devices or timing differences.
|
||||||
MAC_OPTS="-machine type=$MACHINE,smm=off,usb=off"
|
MAC_OPTS="-machine type=$MACHINE,smm=off,usb=off"
|
||||||
MAC_OPTS+=",vmport=off,dump-guest-core=off,hpet=off${KVM_OPTS}"
|
MAC_OPTS+=",vmport=off,dump-guest-core=off,hpet=off${KVM_OPTS}"
|
||||||
|
|
||||||
@@ -37,9 +41,12 @@ configureMachine() {
|
|||||||
|
|
||||||
configureVirtioDevices() {
|
configureVirtioDevices() {
|
||||||
|
|
||||||
DEV_OPTS="-device virtio-balloon-pci,id=balloon0,bus=pcie.0,addr=0x4"
|
local bus
|
||||||
|
bus=$(getPciBus)
|
||||||
|
|
||||||
|
DEV_OPTS="-device virtio-balloon-pci,id=balloon0,bus=$bus,addr=0x4"
|
||||||
DEV_OPTS+=" -object rng-random,id=objrng0,filename=/dev/urandom"
|
DEV_OPTS+=" -object rng-random,id=objrng0,filename=/dev/urandom"
|
||||||
DEV_OPTS+=" -device virtio-rng-pci,rng=objrng0,id=rng0,bus=pcie.0,addr=0x1c"
|
DEV_OPTS+=" -device virtio-rng-pci,rng=objrng0,id=rng0,bus=$bus,addr=0x1c"
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -47,11 +54,15 @@ configureVirtioDevices() {
|
|||||||
buildArguments() {
|
buildArguments() {
|
||||||
|
|
||||||
ARGS="$DEF_OPTS $CPU_OPTS $RAM_OPTS $MAC_OPTS $DISPLAY_OPTS $MON_OPTS $SERIAL_OPTS $NET_OPTS $DISK_OPTS $DEV_OPTS $ARGUMENTS"
|
ARGS="$DEF_OPTS $CPU_OPTS $RAM_OPTS $MAC_OPTS $DISPLAY_OPTS $MON_OPTS $SERIAL_OPTS $NET_OPTS $DISK_OPTS $DEV_OPTS $ARGUMENTS"
|
||||||
|
# Collapse whitespace after optional argument groups are assembled so empty
|
||||||
|
# features cannot leave malformed spacing in the final QEMU command.
|
||||||
ARGS=$(echo "$ARGS" | sed 's/\t/ /g' | tr -s ' ')
|
ARGS=$(echo "$ARGS" | sed 's/\t/ /g' | tr -s ' ')
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
finalizeMemory
|
||||||
|
|
||||||
configureMemory
|
configureMemory
|
||||||
configureMonitor
|
configureMonitor
|
||||||
configureMachine
|
configureMachine
|
||||||
|
|||||||
+62
-24
@@ -7,6 +7,7 @@ set -Eeuo pipefail
|
|||||||
: "${DISK_FMT:="raw"}" # Disk file format, 'raw' by default for best performance
|
: "${DISK_FMT:="raw"}" # Disk file format, 'raw' by default for best performance
|
||||||
: "${DISK_TYPE:=""}" # Device type to be used, "sata", "nvme", "blk" or "scsi"
|
: "${DISK_TYPE:=""}" # Device type to be used, "sata", "nvme", "blk" or "scsi"
|
||||||
: "${DISK_FLAGS:=""}" # Specifies the options for use with the qcow2 disk format
|
: "${DISK_FLAGS:=""}" # Specifies the options for use with the qcow2 disk format
|
||||||
|
: "${DISK_OPTIONS:=""}" # Specifies additional options for the QEMU disk device
|
||||||
: "${DISK_CACHE:="none"}" # Caching mode, can be set to 'writeback' for better performance
|
: "${DISK_CACHE:="none"}" # Caching mode, can be set to 'writeback' for better performance
|
||||||
: "${DISK_DISCARD:="unmap"}" # Controls whether unmap (TRIM) commands are passed to the host.
|
: "${DISK_DISCARD:="unmap"}" # Controls whether unmap (TRIM) commands are passed to the host.
|
||||||
: "${DISK_ROTATION:="1"}" # Rotation rate, set to 1 for SSD storage and increase for HDD
|
: "${DISK_ROTATION:="1"}" # Rotation rate, set to 1 for SSD storage and increase for HDD
|
||||||
@@ -16,6 +17,7 @@ DISK_IO=$(strip "$DISK_IO")
|
|||||||
DISK_FMT=$(strip "$DISK_FMT")
|
DISK_FMT=$(strip "$DISK_FMT")
|
||||||
DISK_TYPE=$(strip "$DISK_TYPE")
|
DISK_TYPE=$(strip "$DISK_TYPE")
|
||||||
DISK_FLAGS=$(strip "$DISK_FLAGS")
|
DISK_FLAGS=$(strip "$DISK_FLAGS")
|
||||||
|
DISK_OPTIONS=$(strip "$DISK_OPTIONS")
|
||||||
DISK_CACHE=$(strip "$DISK_CACHE")
|
DISK_CACHE=$(strip "$DISK_CACHE")
|
||||||
DISK_DISCARD=$(strip "$DISK_DISCARD")
|
DISK_DISCARD=$(strip "$DISK_DISCARD")
|
||||||
DISK_ROTATION=$(strip "$DISK_ROTATION")
|
DISK_ROTATION=$(strip "$DISK_ROTATION")
|
||||||
@@ -23,6 +25,8 @@ DISK_ROTATION=$(strip "$DISK_ROTATION")
|
|||||||
BOOT="$STORAGE/$BASE.boot.img"
|
BOOT="$STORAGE/$BASE.boot.img"
|
||||||
SYSTEM="$STORAGE/$BASE.system.img"
|
SYSTEM="$STORAGE/$BASE.system.img"
|
||||||
|
|
||||||
|
# The boot and system images are installation artifacts, not optional data
|
||||||
|
# disks, and must exist before any user storage is attached.
|
||||||
[ ! -s "$BOOT" ] && error "Virtual DSM boot-image does not exist ($BOOT)" && exit 81
|
[ ! -s "$BOOT" ] && error "Virtual DSM boot-image does not exist ($BOOT)" && exit 81
|
||||||
[ ! -s "$SYSTEM" ] && error "Virtual DSM system-image does not exist ($SYSTEM)" && exit 82
|
[ ! -s "$SYSTEM" ] && error "Virtual DSM system-image does not exist ($SYSTEM)" && exit 82
|
||||||
|
|
||||||
@@ -125,6 +129,8 @@ allocateRaw() {
|
|||||||
return $?
|
return $?
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Prefer real allocation, retry with zero-range allocation where supported,
|
||||||
|
# and fall back to a sparse file when the host filesystem rejects both.
|
||||||
fallocate -l "$dataSize" "$diskFile" &>/dev/null && return 0
|
fallocate -l "$dataSize" "$diskFile" &>/dev/null && return 0
|
||||||
fallocate -l -x "$dataSize" "$diskFile" && return 0
|
fallocate -l -x "$dataSize" "$diskFile" && return 0
|
||||||
truncate -s "$dataSize" "$diskFile" || return 1
|
truncate -s "$dataSize" "$diskFile" || return 1
|
||||||
@@ -154,9 +160,11 @@ normalizeSize() {
|
|||||||
local diskDesc="$2"
|
local diskDesc="$2"
|
||||||
local dir="$3"
|
local dir="$3"
|
||||||
|
|
||||||
local gb free space
|
local free dataSize
|
||||||
local dataSize spare=1073741824
|
local spare=1073741824
|
||||||
|
|
||||||
|
# Dynamic sizes are resolved once from current free space. max reserves one
|
||||||
|
# GiB for host metadata and container activity; half uses half the space.
|
||||||
if [[ "${diskSpace,,}" == "max" || "${diskSpace,,}" == "half" ]]; then
|
if [[ "${diskSpace,,}" == "max" || "${diskSpace,,}" == "half" ]]; then
|
||||||
|
|
||||||
free=$(df --output=avail -B 1 "$dir" | tail -n 1)
|
free=$(df --output=avail -B 1 "$dir" | tail -n 1)
|
||||||
@@ -168,12 +176,12 @@ normalizeSize() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
(( free < spare )) && free="$spare"
|
(( free < spare )) && free="$spare"
|
||||||
gb=$(( free / 1073741825 ))
|
local gb=$(( free / 1073741825 ))
|
||||||
diskSpace="${gb}G"
|
diskSpace="${gb}G"
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
space="${diskSpace// /}"
|
local space="${diskSpace// /}"
|
||||||
[ -z "$space" ] && space="256G"
|
[ -z "$space" ] && space="256G"
|
||||||
[ -z "${space//[0-9. ]}" ] && space="${space}G"
|
[ -z "${space//[0-9. ]}" ] && space="${space}G"
|
||||||
space=$(echo "${space^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
|
space=$(echo "${space^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
|
||||||
@@ -419,6 +427,8 @@ convertDisk() {
|
|||||||
if [[ "$destinationFmt" == "raw" ]]; then
|
if [[ "$destinationFmt" == "raw" ]]; then
|
||||||
if ! disabled "$ALLOCATE"; then
|
if ! disabled "$ALLOCATE"; then
|
||||||
|
|
||||||
|
# qemu-img may leave converted raw output sparse despite requested
|
||||||
|
# preallocation, so allocate its final length explicitly afterward.
|
||||||
# Work around qemu-img bug
|
# Work around qemu-img bug
|
||||||
if ! currentSize=$(stat -c%s "$tmpFile"); then
|
if ! currentSize=$(stat -c%s "$tmpFile"); then
|
||||||
error "Failed to determine converted image size: $tmpFile"
|
error "Failed to determine converted image size: $tmpFile"
|
||||||
@@ -433,6 +443,8 @@ convertDisk() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Publish the converted image before deleting the original so a failed
|
||||||
|
# conversion or rename never destroys the only usable disk.
|
||||||
if ! mv "$tmpFile" "$destinationFile"; then
|
if ! mv "$tmpFile" "$destinationFile"; then
|
||||||
error "Failed to move converted $diskDesc image to $destinationFile."
|
error "Failed to move converted $diskDesc image to $destinationFile."
|
||||||
exit 79
|
exit 79
|
||||||
@@ -477,6 +489,8 @@ checkFS () {
|
|||||||
warn "the filesystem of $base is FUSE, this extra layer will negatively affect performance!"
|
warn "the filesystem of $base is FUSE, this extra layer will negatively affect performance!"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Filesystems without O_DIRECT support require threaded I/O and writeback
|
||||||
|
# caching; native AIO with cache=none would fail at runtime.
|
||||||
if ! supportsDirect "$fs"; then
|
if ! supportsDirect "$fs"; then
|
||||||
warn "the filesystem of $base is $fs, which does not support O_DIRECT mode, adjusting settings..."
|
warn "the filesystem of $base is $fs, which does not support O_DIRECT mode, adjusting settings..."
|
||||||
fi
|
fi
|
||||||
@@ -504,9 +518,12 @@ createDevice () {
|
|||||||
local diskCache="$7"
|
local diskCache="$7"
|
||||||
local diskSerial="$8"
|
local diskSerial="$8"
|
||||||
local diskSectors="$9"
|
local diskSectors="$9"
|
||||||
local bus="${PCI_BUS:-pcie.0}"
|
|
||||||
|
|
||||||
[[ -z "${PCI_BUS:-}" && ( "${MACHINE,,}" == pc || "${MACHINE,,}" == pc-i440fx* ) ]] && bus="pci.0"
|
local bus
|
||||||
|
bus=$(getPciBus)
|
||||||
|
|
||||||
|
local options=""
|
||||||
|
[ -n "$DISK_OPTIONS" ] && options=",${DISK_OPTIONS#,}"
|
||||||
|
|
||||||
local bootIndex=""
|
local bootIndex=""
|
||||||
local diskId="data$diskIndex"
|
local diskId="data$diskIndex"
|
||||||
@@ -520,29 +537,29 @@ createDevice () {
|
|||||||
;;
|
;;
|
||||||
"usb" )
|
"usb" )
|
||||||
result+=",if=none \
|
result+=",if=none \
|
||||||
-device usb-storage,drive=${diskId}${bootIndex}${diskSerial}${diskSectors}"
|
-device usb-storage,drive=${diskId}${bootIndex}${diskSerial}${diskSectors}${options}"
|
||||||
echo "$result"
|
echo "$result"
|
||||||
;;
|
;;
|
||||||
"nvme" )
|
"nvme" )
|
||||||
result+=",if=none \
|
result+=",if=none \
|
||||||
-device nvme,drive=${diskId}${bootIndex},serial=deadbeaf${diskIndex}${diskSerial}${diskSectors}"
|
-device nvme,drive=${diskId}${bootIndex},serial=deadbeaf${diskIndex}${diskSerial}${diskSectors}${options}"
|
||||||
echo "$result"
|
echo "$result"
|
||||||
;;
|
;;
|
||||||
"ide" | "sata" )
|
"ide" | "sata" )
|
||||||
result+=",if=none \
|
result+=",if=none \
|
||||||
-device ich9-ahci,id=ahci${diskIndex},addr=$diskAddress \
|
-device ich9-ahci,id=ahci${diskIndex},addr=$diskAddress \
|
||||||
-device ide-hd,drive=${diskId},bus=ahci$diskIndex.0,rotation_rate=$DISK_ROTATION${bootIndex}${diskSerial}${diskSectors}"
|
-device ide-hd,drive=${diskId},bus=ahci$diskIndex.0,rotation_rate=$DISK_ROTATION${bootIndex}${diskSerial}${diskSectors}${options}"
|
||||||
echo "$result"
|
echo "$result"
|
||||||
;;
|
;;
|
||||||
"blk" | "virtio-blk" )
|
"blk" | "virtio-blk" )
|
||||||
result+=",if=none \
|
result+=",if=none \
|
||||||
-device virtio-blk-pci,drive=${diskId},bus=$bus,addr=$diskAddress,iothread=io2${bootIndex}${diskSerial}${diskSectors}"
|
-device virtio-blk-pci,drive=${diskId},bus=$bus,addr=$diskAddress,iothread=io2${bootIndex}${diskSerial}${diskSectors}${options}"
|
||||||
echo "$result"
|
echo "$result"
|
||||||
;;
|
;;
|
||||||
"scsi" | "virtio-scsi" )
|
"scsi" | "virtio-scsi" )
|
||||||
result+=",if=none \
|
result+=",if=none \
|
||||||
-device virtio-scsi-pci,id=${diskId}b,bus=$bus,addr=$diskAddress,iothread=io2,hotplug=off \
|
-device virtio-scsi-pci,id=${diskId}b,bus=$bus,addr=$diskAddress,iothread=io2,hotplug=off \
|
||||||
-device scsi-hd,drive=${diskId},bus=${diskId}b.0,channel=0,scsi-id=0,lun=0,rotation_rate=$DISK_ROTATION${bootIndex}${diskSerial}${diskSectors}"
|
-device scsi-hd,drive=${diskId},bus=${diskId}b.0,channel=0,scsi-id=0,lun=0,rotation_rate=$DISK_ROTATION${bootIndex}${diskSerial}${diskSectors}${options}"
|
||||||
echo "$result"
|
echo "$result"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -554,6 +571,8 @@ finishDisks () {
|
|||||||
|
|
||||||
case "${DISK_TYPE,,}" in
|
case "${DISK_TYPE,,}" in
|
||||||
"blk" | "scsi" | "virtio-blk" | "virtio-scsi" )
|
"blk" | "scsi" | "virtio-blk" | "virtio-scsi" )
|
||||||
|
# VirtIO block and SCSI devices share one dedicated I/O thread, which
|
||||||
|
# must be declared exactly once regardless of disk count.
|
||||||
[[ "$DISK_OPTS" != *" -object iothread,id=io2"* ]] && DISK_OPTS+=" -object iothread,id=io2" ;;
|
[[ "$DISK_OPTS" != *" -object iothread,id=io2"* ]] && DISK_OPTS+=" -object iothread,id=io2" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
@@ -573,13 +592,12 @@ addDisk () {
|
|||||||
local diskCache="$9"
|
local diskCache="$9"
|
||||||
|
|
||||||
local fs dir used space
|
local fs dir used space
|
||||||
local diskExt diskFile
|
local diskExt dataSize
|
||||||
local dataSize missing
|
|
||||||
local available currentSize
|
local available currentSize
|
||||||
local previousExt previousFmt
|
local previousExt
|
||||||
|
|
||||||
diskExt=$(fmt2ext "$diskFmt")
|
diskExt=$(fmt2ext "$diskFmt")
|
||||||
diskFile="$diskBase.$diskExt"
|
local diskFile="$diskBase.$diskExt"
|
||||||
|
|
||||||
dir=$(dirname "$diskFile")
|
dir=$(dirname "$diskFile")
|
||||||
[ ! -d "$dir" ] && return 0
|
[ ! -d "$dir" ] && return 0
|
||||||
@@ -587,7 +605,11 @@ addDisk () {
|
|||||||
space=$(normalizeSize "$diskSpace" "$diskDesc" "$dir")
|
space=$(normalizeSize "$diskSpace" "$diskDesc" "$dir")
|
||||||
dataSize=$(numfmt --from=iec "$space")
|
dataSize=$(numfmt --from=iec "$space")
|
||||||
|
|
||||||
fs=$(stat -f -c %T "$dir")
|
if ! fs=$(stat -f -c %T "$dir"); then
|
||||||
|
error "Failed to determine filesystem type of \"$dir\" !"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
checkFS "$fs" "$diskFile" "$diskDesc" || exit $?
|
checkFS "$fs" "$diskFile" "$diskDesc" || exit $?
|
||||||
|
|
||||||
if ! supportsDirect "$fs"; then
|
if ! supportsDirect "$fs"; then
|
||||||
@@ -595,23 +617,26 @@ addDisk () {
|
|||||||
diskCache="writeback"
|
diskCache="writeback"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -s "$diskFile" ] ; then
|
if [ ! -f "$diskFile" ] || [ ! -s "$diskFile" ]; then
|
||||||
|
|
||||||
if [[ "${diskFmt,,}" != "raw" ]]; then
|
if [[ "${diskFmt,,}" != "raw" ]]; then
|
||||||
previousFmt="raw"
|
local previousFmt="raw"
|
||||||
else
|
else
|
||||||
previousFmt="qcow2"
|
local previousFmt="qcow2"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
previousExt=$(fmt2ext "$previousFmt")
|
previousExt=$(fmt2ext "$previousFmt")
|
||||||
|
|
||||||
if [ -s "$diskBase.$previousExt" ] ; then
|
# Treat a disk in the other supported format as the same logical disk and
|
||||||
|
# convert it automatically instead of creating an empty replacement.
|
||||||
|
if [ -f "$diskBase.$previousExt" ] &&
|
||||||
|
[ -s "$diskBase.$previousExt" ]; then
|
||||||
convertDisk "$diskBase.$previousExt" "$previousFmt" "$diskFile" "$diskFmt" "$diskBase" "$diskDesc" "$fs" || exit $?
|
convertDisk "$diskBase.$previousExt" "$previousFmt" "$diskFile" "$diskFmt" "$diskBase" "$diskDesc" "$fs" || exit $?
|
||||||
fi
|
fi
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -s "$diskFile" ]; then
|
if [ -f "$diskFile" ] && [ -s "$diskFile" ]; then
|
||||||
|
|
||||||
currentSize=$(getSize "$diskFile") || exit 71
|
currentSize=$(getSize "$diskFile") || exit 71
|
||||||
|
|
||||||
@@ -636,23 +661,25 @@ addDisk () {
|
|||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Sparse disks can promise more guest capacity than the host can currently
|
||||||
|
# satisfy, so report the future shortfall without blocking startup.
|
||||||
if [ -f "$diskFile" ] && disabled "$ALLOCATE"; then
|
if [ -f "$diskFile" ] && disabled "$ALLOCATE"; then
|
||||||
|
|
||||||
currentSize=$(getSize "$diskFile") || exit 73
|
currentSize=$(getSize "$diskFile") || exit 73
|
||||||
used=$(du -sB 1 "$diskFile" | cut -f1)
|
used=$(du -sB 1 "$diskFile" | cut -f1)
|
||||||
available=$(df --output=avail -B 1 "$dir" | tail -n 1)
|
available=$(df --output=avail -B 1 "$dir" | tail -n 1)
|
||||||
missing=$(( currentSize - used - available ))
|
local missing=$(( currentSize - used - available ))
|
||||||
(( missing < 0 )) && missing=0
|
(( missing < 0 )) && missing=0
|
||||||
|
|
||||||
if (( missing > 0 )); then
|
if (( missing > 0 )); then
|
||||||
|
|
||||||
local gb base msg
|
local gb base
|
||||||
|
|
||||||
gb=$(formatBytes "$available")
|
gb=$(formatBytes "$available")
|
||||||
base=$(baseDir "$dir")
|
base=$(baseDir "$dir")
|
||||||
missing=$(formatBytes "$missing")
|
missing=$(formatBytes "$missing")
|
||||||
currentSize=$(formatBytes "$currentSize")
|
currentSize=$(formatBytes "$currentSize")
|
||||||
msg="The virtual size of the ${diskDesc,,} is $currentSize"
|
local msg="The virtual size of the ${diskDesc,,} is $currentSize"
|
||||||
|
|
||||||
if [ -n "$used" ] && [[ "$used" != "0" ]]; then
|
if [ -n "$used" ] && [[ "$used" != "0" ]]; then
|
||||||
used=$(formatBytes "$used")
|
used=$(formatBytes "$used")
|
||||||
@@ -690,6 +717,8 @@ addDevice () {
|
|||||||
[ -z "$diskDev" ] && return 0
|
[ -z "$diskDev" ] && return 0
|
||||||
[ ! -b "$diskDev" ] && error "Device $diskDev cannot be found! Please add it to the 'devices' section of your compose file." && exit 55
|
[ ! -b "$diskDev" ] && error "Device $diskDev cannot be found! Please add it to the 'devices' section of your compose file." && exit 55
|
||||||
|
|
||||||
|
# DSM may reject whole-disk passthrough when QEMU is given explicit sector
|
||||||
|
# geometry; partitions need it to preserve non-512-byte host geometry.
|
||||||
# Only detect and apply sector sizes for partitions, not whole disks.
|
# Only detect and apply sector sizes for partitions, not whole disks.
|
||||||
# Whole disk passthrough with explicit sector sizes causes DSM not to recognize the disk.
|
# Whole disk passthrough with explicit sector sizes causes DSM not to recognize the disk.
|
||||||
if [[ "$devType" == "part" ]]; then
|
if [[ "$devType" == "part" ]]; then
|
||||||
@@ -767,6 +796,11 @@ if [[ "$DISK_FLAGS" =~ [[:space:]] ]]; then
|
|||||||
exit 78
|
exit 78
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "$DISK_OPTIONS" =~ [[:space:]] ]]; then
|
||||||
|
error "Invalid DISK_OPTIONS value '$DISK_OPTIONS', spaces are not allowed."
|
||||||
|
exit 78
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -z "$ALLOCATE" ]; then
|
if [ -z "$ALLOCATE" ]; then
|
||||||
ALLOCATE="N"
|
ALLOCATE="N"
|
||||||
fi
|
fi
|
||||||
@@ -779,6 +813,8 @@ else
|
|||||||
DISK_ALLOC="preallocation=falloc"
|
DISK_ALLOC="preallocation=falloc"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Reserve the first two boot indexes and PCI addresses for the managed boot
|
||||||
|
# and system images; user disks begin at index 3.
|
||||||
DISK_OPTS+=$(createDevice "$BOOT" "$DISK_TYPE" "1" "0xa" "raw" "$DISK_IO" "$DISK_CACHE" "" "")
|
DISK_OPTS+=$(createDevice "$BOOT" "$DISK_TYPE" "1" "0xa" "raw" "$DISK_IO" "$DISK_CACHE" "" "")
|
||||||
DISK_OPTS+=$(createDevice "$SYSTEM" "$DISK_TYPE" "2" "0xb" "raw" "$DISK_IO" "$DISK_CACHE" "" "")
|
DISK_OPTS+=$(createDevice "$SYSTEM" "$DISK_TYPE" "2" "0xb" "raw" "$DISK_IO" "$DISK_CACHE" "" "")
|
||||||
|
|
||||||
@@ -818,6 +854,8 @@ DISK_DEVICES=( "$DEVICE" "$DEVICE2" "$DEVICE3" "$DEVICE4" )
|
|||||||
DISK_INDEXES=( "3" "4" "5" "6" )
|
DISK_INDEXES=( "3" "4" "5" "6" )
|
||||||
DISK_ADDRESSES=( "0xc" "0xd" "0xe" "0xf" )
|
DISK_ADDRESSES=( "0xc" "0xd" "0xe" "0xf" )
|
||||||
|
|
||||||
|
# A passed-through block device takes precedence over the image-file slot
|
||||||
|
# with the same number.
|
||||||
for i in "${!DISK_FILES[@]}"; do
|
for i in "${!DISK_FILES[@]}"; do
|
||||||
|
|
||||||
if [ -n "${DISK_DEVICES[i]}" ]; then
|
if [ -n "${DISK_DEVICES[i]}" ]; then
|
||||||
|
|||||||
@@ -17,8 +17,12 @@ RENDERNODE=$(strip "$RENDERNODE")
|
|||||||
|
|
||||||
CPU_VENDOR=$(lscpu | awk '/Vendor ID/{print $3}')
|
CPU_VENDOR=$(lscpu | awk '/Vendor ID/{print $3}')
|
||||||
|
|
||||||
|
# The accelerated Intel render-node path is restricted to x86 Intel hosts;
|
||||||
|
# other platforms retain the normal QEMU display backend.
|
||||||
if ! enabled "$GPU" || isAmdCpu || [[ "$ARCH" != "amd64" ]]; then
|
if ! enabled "$GPU" || isAmdCpu || [[ "$ARCH" != "amd64" ]]; then
|
||||||
|
|
||||||
|
# A disabled frontend also removes the emulated VGA device to keep the guest
|
||||||
|
# hardware layout headless.
|
||||||
[[ "${DISPLAY,,}" == "none" ]] && VGA="none"
|
[[ "${DISPLAY,,}" == "none" ]] && VGA="none"
|
||||||
|
|
||||||
if enabled "$LOSSY" && [[ "${DISPLAY,,}" == vnc=* ]]; then
|
if enabled "$LOSSY" && [[ "${DISPLAY,,}" == vnc=* ]]; then
|
||||||
@@ -40,6 +44,8 @@ DISPLAY_OPTS+=" -vga $VGA"
|
|||||||
[ ! -d /dev/dri ] && mkdir -m 755 /dev/dri
|
[ ! -d /dev/dri ] && mkdir -m 755 /dev/dri
|
||||||
|
|
||||||
# Extract the card number from the render node
|
# Extract the card number from the render node
|
||||||
|
# Linux renderD128 corresponds to card0; derive both device minors because
|
||||||
|
# container device bindings may expose only the render node.
|
||||||
CARD_NUMBER=$(echo "$RENDERNODE" | grep -oP '(?<=renderD)\d+')
|
CARD_NUMBER=$(echo "$RENDERNODE" | grep -oP '(?<=renderD)\d+')
|
||||||
CARD_DEVICE="/dev/dri/card$((CARD_NUMBER - 128))"
|
CARD_DEVICE="/dev/dri/card$((CARD_NUMBER - 128))"
|
||||||
|
|
||||||
@@ -59,6 +65,8 @@ if [ ! -c "$RENDERNODE" ] || [ ! -r "$RENDERNODE" ] || [ ! -w "$RENDERNODE" ]; t
|
|||||||
warn "render device '${RENDERNODE}' is unavailable or inaccessible."
|
warn "render device '${RENDERNODE}' is unavailable or inaccessible."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Install acceleration packages lazily so non-GPU deployments keep the base
|
||||||
|
# image small and do not require OpenGL modules.
|
||||||
addPackage "xserver-xorg-video-intel" "Intel GPU drivers"
|
addPackage "xserver-xorg-video-intel" "Intel GPU drivers"
|
||||||
addPackage "qemu-system-modules-opengl" "OpenGL module"
|
addPackage "qemu-system-modules-opengl" "OpenGL module"
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -9,7 +9,8 @@ cd /run
|
|||||||
|
|
||||||
. start.sh # Startup hook
|
. start.sh # Startup hook
|
||||||
. utils.sh # Load functions
|
. utils.sh # Load functions
|
||||||
. reset.sh # Initialize system
|
. init.sh # Initialize system
|
||||||
|
. memory.sh # Check memory
|
||||||
. server.sh # Start webserver
|
. server.sh # Start webserver
|
||||||
. install.sh # Run installation
|
. install.sh # Run installation
|
||||||
. disk.sh # Initialize disks
|
. disk.sh # Initialize disks
|
||||||
@@ -18,7 +19,6 @@ cd /run
|
|||||||
. proc.sh # Initialize processor
|
. proc.sh # Initialize processor
|
||||||
. serial.sh # Initialize serialport
|
. serial.sh # Initialize serialport
|
||||||
. power.sh # Configure shutdown
|
. power.sh # Configure shutdown
|
||||||
. memory.sh # Check available memory
|
|
||||||
. config.sh # Configure arguments
|
. config.sh # Configure arguments
|
||||||
. finish.sh # Finish initialization
|
. finish.sh # Finish initialization
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -2,7 +2,10 @@
|
|||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
if enabled "$DEBUG"; then
|
if enabled "$DEBUG"; then
|
||||||
printf "QEMU arguments:\n\n%s\n\n" "${ARGS// -/$'\n-'}"
|
printf "QEMU arguments:\n\n %s\n\n" "${ARGS// -/$'\n -'}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Must always remain the very last command
|
||||||
|
enableTrap
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
+41
-107
@@ -41,8 +41,10 @@ detectEngine() {
|
|||||||
|
|
||||||
detectRootless() {
|
detectRootless() {
|
||||||
|
|
||||||
local uid_map=""
|
local uid_map
|
||||||
|
|
||||||
|
# A full identity UID map indicates a rootful container; any remapping is
|
||||||
|
# treated as rootless even though the process itself runs as UID 0.
|
||||||
uid_map=$(awk '{$1=$1; print}' /proc/self/uid_map 2>/dev/null || true)
|
uid_map=$(awk '{$1=$1; print}' /proc/self/uid_map 2>/dev/null || true)
|
||||||
|
|
||||||
if [[ "$uid_map" == "0 0 4294967295" ]]; then
|
if [[ "$uid_map" == "0 0 4294967295" ]]; then
|
||||||
@@ -58,8 +60,6 @@ checkPrivileged() {
|
|||||||
|
|
||||||
local cap_bnd
|
local cap_bnd
|
||||||
local last_cap
|
local last_cap
|
||||||
local max_cap
|
|
||||||
|
|
||||||
# Get the capability bounding set
|
# Get the capability bounding set
|
||||||
cap_bnd=$(grep '^CapBnd:' /proc/$$/status | awk '{print $2}')
|
cap_bnd=$(grep '^CapBnd:' /proc/$$/status | awk '{print $2}')
|
||||||
cap_bnd=$(printf "%d" "0x${cap_bnd}")
|
cap_bnd=$(printf "%d" "0x${cap_bnd}")
|
||||||
@@ -68,7 +68,9 @@ checkPrivileged() {
|
|||||||
last_cap=$(cat /proc/sys/kernel/cap_last_cap)
|
last_cap=$(cat /proc/sys/kernel/cap_last_cap)
|
||||||
|
|
||||||
# Calculate the maximum capability value
|
# Calculate the maximum capability value
|
||||||
max_cap=$(((1 << (last_cap + 1)) - 1))
|
# Compare the bounding set with every capability supported by this kernel;
|
||||||
|
# checking only a few known capabilities would misclassify newer kernels.
|
||||||
|
local max_cap=$(((1 << (last_cap + 1)) - 1))
|
||||||
|
|
||||||
if [ "$cap_bnd" -eq "$max_cap" ]; then
|
if [ "$cap_bnd" -eq "$max_cap" ]; then
|
||||||
PRIVILEGED="Y"
|
PRIVILEGED="Y"
|
||||||
@@ -77,15 +79,15 @@ checkPrivileged() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizeCpuCores() {
|
checkCores() {
|
||||||
|
|
||||||
CPU_CORES=$(strip "$CPU_CORES")
|
CPU_CORES=$(strip "$CPU_CORES")
|
||||||
[ -z "$CPU_CORES" ] && CPU_CORES=2
|
[ -z "$CPU_CORES" ] && CPU_CORES=2
|
||||||
[[ "${CPU_CORES,,}" == "max" ]] && CPU_CORES="$CORES"
|
[[ "${CPU_CORES,,}" == "max" ]] && CPU_CORES="$CORES"
|
||||||
[[ "${CPU_CORES,,}" == "half" ]] && CPU_CORES=$(( CORES / 2 ))
|
[[ "${CPU_CORES,,}" == "half" ]] && CPU_CORES=$(( CORES / 2 ))
|
||||||
[ -z "${CPU_CORES##*[!0-9]*}" ] && error "Invalid amount of CPU_CORES: $CPU_CORES" && exit 15
|
[ -z "${CPU_CORES##*[!0-9]*}" ] && error "Invalid amount of CPU_CORES: $CPU_CORES" && exit 15
|
||||||
|
|
||||||
[ "$CPU_CORES" -lt "1" ] && CPU_CORES=1
|
[ "$CPU_CORES" -lt "1" ] && CPU_CORES=1
|
||||||
|
|
||||||
if [ "$CPU_CORES" -gt "$CORES" ]; then
|
if [ "$CPU_CORES" -gt "$CORES" ]; then
|
||||||
warn "The amount for CPU_CORES (${CPU_CORES}) exceeds the amount of logical cores available (${CORES}) and will be limited."
|
warn "The amount for CPU_CORES (${CPU_CORES}) exceeds the amount of logical cores available (${CORES}) and will be limited."
|
||||||
CPU_CORES="$CORES"
|
CPU_CORES="$CORES"
|
||||||
@@ -94,6 +96,20 @@ normalizeCpuCores() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkSockets() {
|
||||||
|
|
||||||
|
local lscpu_out
|
||||||
|
lscpu_out=$(lscpu 2>/dev/null || true)
|
||||||
|
|
||||||
|
if grep -qi "socket(s)" <<< "$lscpu_out"; then
|
||||||
|
SOCKETS=$(grep -m 1 -i 'socket(s)' <<< "$lscpu_out" | awk '{print $2}')
|
||||||
|
[ -z "${SOCKETS##*[!0-9]*}" ] && SOCKETS=1
|
||||||
|
[ "$SOCKETS" -lt "1" ] && SOCKETS=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
checkStorage() {
|
checkStorage() {
|
||||||
|
|
||||||
# Check system
|
# Check system
|
||||||
@@ -103,6 +119,8 @@ checkStorage() {
|
|||||||
if [ ! -d "/dev/shm" ]; then
|
if [ ! -d "/dev/shm" ]; then
|
||||||
error "Directory /dev/shm not found!" && exit 14
|
error "Directory /dev/shm not found!" && exit 14
|
||||||
else
|
else
|
||||||
|
# Keep runtime sockets and PID files on shared memory even on images where
|
||||||
|
# /run/shm is absent but /dev/shm is available.
|
||||||
[ ! -d "$QEMU_DIR" ] && ln -s /dev/shm "$QEMU_DIR"
|
[ ! -d "$QEMU_DIR" ] && ln -s /dev/shm "$QEMU_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -111,7 +129,9 @@ checkStorage() {
|
|||||||
# Check folder
|
# Check folder
|
||||||
|
|
||||||
if [[ "${STORAGE,,}" != "/storage" ]]; then
|
if [[ "${STORAGE,,}" != "/storage" ]]; then
|
||||||
mkdir -p "$STORAGE"
|
if ! mkdir -p -- "$STORAGE"; then
|
||||||
|
error "Cannot create storage folder ($STORAGE)!" && exit 13
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -d "$STORAGE" ]; then
|
if [ ! -d "$STORAGE" ]; then
|
||||||
@@ -127,11 +147,9 @@ checkStorage() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
checkFilesystem() {
|
checkHost() {
|
||||||
|
|
||||||
# Check filesystem
|
# Check filesystem
|
||||||
FS=$(stat -f -c %T "$STORAGE")
|
|
||||||
|
|
||||||
if [[ "${FS,,}" == "ecryptfs" || "${FS,,}" == "tmpfs" ]]; then
|
if [[ "${FS,,}" == "ecryptfs" || "${FS,,}" == "tmpfs" ]]; then
|
||||||
DISK_IO="threads"
|
DISK_IO="threads"
|
||||||
DISK_CACHE="writeback"
|
DISK_CACHE="writeback"
|
||||||
@@ -140,91 +158,6 @@ checkFilesystem() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
finiteMemoryLimit() {
|
|
||||||
|
|
||||||
local limit="$1"
|
|
||||||
local sentinel="4611686018427387904"
|
|
||||||
local i=0
|
|
||||||
local left=""
|
|
||||||
local right=""
|
|
||||||
|
|
||||||
[[ "$limit" =~ ^[0-9]+$ ]] || return 1
|
|
||||||
|
|
||||||
(( ${#limit} < ${#sentinel} )) && return 0
|
|
||||||
(( ${#limit} > ${#sentinel} )) && return 1
|
|
||||||
|
|
||||||
for (( i=0; i<${#sentinel}; i++ )); do
|
|
||||||
left="${limit:i:1}"
|
|
||||||
right="${sentinel:i:1}"
|
|
||||||
|
|
||||||
(( left < right )) && return 0
|
|
||||||
(( left > right )) && return 1
|
|
||||||
done
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
getMemoryInfo() {
|
|
||||||
|
|
||||||
local host_total=""
|
|
||||||
local host_avail=""
|
|
||||||
local limit=""
|
|
||||||
local current=""
|
|
||||||
local available=""
|
|
||||||
|
|
||||||
host_total=$(free -b | awk '/^Mem:/ {print $2; exit}')
|
|
||||||
host_avail=$(free -b | awk '/^Mem:/ {print $7; exit}')
|
|
||||||
|
|
||||||
RAM_TOTAL="$host_total"
|
|
||||||
RAM_AVAIL="$host_avail"
|
|
||||||
|
|
||||||
if [ -r /sys/fs/cgroup/memory.max ] && [ -r /sys/fs/cgroup/memory.current ]; then
|
|
||||||
limit=$(< /sys/fs/cgroup/memory.max)
|
|
||||||
current=$(< /sys/fs/cgroup/memory.current)
|
|
||||||
elif [ -r /sys/fs/cgroup/memory/memory.limit_in_bytes ] && [ -r /sys/fs/cgroup/memory/memory.usage_in_bytes ]; then
|
|
||||||
limit=$(< /sys/fs/cgroup/memory/memory.limit_in_bytes)
|
|
||||||
current=$(< /sys/fs/cgroup/memory/memory.usage_in_bytes)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if finiteMemoryLimit "$limit" && [[ "$current" =~ ^[0-9]+$ ]]; then
|
|
||||||
(( limit < RAM_TOTAL )) && RAM_TOTAL="$limit"
|
|
||||||
|
|
||||||
available=$(( limit - current ))
|
|
||||||
(( available < 0 )) && available=0
|
|
||||||
(( available < RAM_AVAIL )) && RAM_AVAIL="$available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
normalizeRamSize() {
|
|
||||||
|
|
||||||
# Read host and container memory limits.
|
|
||||||
getMemoryInfo
|
|
||||||
|
|
||||||
RAM_SPARE=500000000
|
|
||||||
RAM_MINIMUM=136314880
|
|
||||||
|
|
||||||
RAM_SIZE=$(strip "$RAM_SIZE")
|
|
||||||
RAM_SIZE="${RAM_SIZE// /}"
|
|
||||||
[ -z "$RAM_SIZE" ] && RAM_SIZE="2G"
|
|
||||||
|
|
||||||
if [[ "${RAM_SIZE,,}" != "max" && "${RAM_SIZE,,}" != "half" ]]; then
|
|
||||||
|
|
||||||
if [ -z "${RAM_SIZE//[0-9. ]}" ]; then
|
|
||||||
[ "${RAM_SIZE%%.*}" -lt "130" ] && RAM_SIZE="${RAM_SIZE}G" || RAM_SIZE="${RAM_SIZE}M"
|
|
||||||
fi
|
|
||||||
|
|
||||||
RAM_SIZE=$(echo "${RAM_SIZE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
|
|
||||||
! numfmt --from=iec "$RAM_SIZE" &>/dev/null && error "Invalid RAM_SIZE: $RAM_SIZE" && exit 16
|
|
||||||
wanted=$(numfmt --from=iec "$RAM_SIZE")
|
|
||||||
[ "$wanted" -lt "$RAM_MINIMUM" ] && error "RAM_SIZE is too low: $RAM_SIZE" && exit 16
|
|
||||||
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
checkKvm() {
|
checkKvm() {
|
||||||
|
|
||||||
# Check KVM support
|
# Check KVM support
|
||||||
@@ -238,6 +171,8 @@ checkKvm() {
|
|||||||
if disabled "$KVM"; then
|
if disabled "$KVM"; then
|
||||||
warn "KVM acceleration is disabled, this will cause the machine to run about 10 times slower!"
|
warn "KVM acceleration is disabled, this will cause the machine to run about 10 times slower!"
|
||||||
else
|
else
|
||||||
|
# KVM accelerates only matching host and guest instruction sets; cross-
|
||||||
|
# architecture execution must fall back to software emulation.
|
||||||
if [[ "${ARCH,,}" != "$TARGET" ]]; then
|
if [[ "${ARCH,,}" != "$TARGET" ]]; then
|
||||||
KVM="N"
|
KVM="N"
|
||||||
warn "your CPU architecture is ${ARCH^^} and cannot provide KVM acceleration for ${PLATFORM^^} instructions, so the machine will run about 10 times slower."
|
warn "your CPU architecture is ${ARCH^^} and cannot provide KVM acceleration for ${PLATFORM^^} instructions, so the machine will run about 10 times slower."
|
||||||
@@ -261,7 +196,7 @@ checkKvm() {
|
|||||||
fi
|
fi
|
||||||
if ! grep -qw "sse4_2" <<< "$flags"; then
|
if ! grep -qw "sse4_2" <<< "$flags"; then
|
||||||
error "Your CPU does not have the SSE4 instruction set that Virtual DSM requires!"
|
error "Your CPU does not have the SSE4 instruction set that Virtual DSM requires!"
|
||||||
! enabled "$DEBUG" && exit 88
|
enabled "$DEBUG" || exit 88
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -282,7 +217,7 @@ checkKvm() {
|
|||||||
error "KVM acceleration is not available $KVM_ERR, this will cause the machine to run about 10 times slower."
|
error "KVM acceleration is not available $KVM_ERR, this will cause the machine to run about 10 times slower."
|
||||||
error "See the FAQ for possible causes, or disable acceleration by adding the \"KVM=N\" variable (not recommended)." ;;
|
error "See the FAQ for possible causes, or disable acceleration by adding the \"KVM=N\" variable (not recommended)." ;;
|
||||||
esac
|
esac
|
||||||
! enabled "$DEBUG" && exit 88
|
enabled "$DEBUG" || exit 88
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -295,6 +230,7 @@ checkKvm() {
|
|||||||
TZ=$(strip "$TZ")
|
TZ=$(strip "$TZ")
|
||||||
STORAGE=$(strip "$STORAGE")
|
STORAGE=$(strip "$STORAGE")
|
||||||
COUNTRY=$(strip "$COUNTRY")
|
COUNTRY=$(strip "$COUNTRY")
|
||||||
|
MACHINE=$(strip "${MACHINE,,}")
|
||||||
DISK_SIZE=$(strip "$DISK_SIZE")
|
DISK_SIZE=$(strip "$DISK_SIZE")
|
||||||
|
|
||||||
# Helper variables
|
# Helper variables
|
||||||
@@ -322,22 +258,17 @@ SOCKETS=1
|
|||||||
CPU=$(cpu)
|
CPU=$(cpu)
|
||||||
SYS=$(uname -r)
|
SYS=$(uname -r)
|
||||||
ARCH=$(dpkg --print-architecture)
|
ARCH=$(dpkg --print-architecture)
|
||||||
IFS=. read -r KERNEL MINOR _ <<< "$SYS"
|
|
||||||
CORES=$(grep -c '^processor' /proc/cpuinfo)
|
CORES=$(grep -c '^processor' /proc/cpuinfo)
|
||||||
|
IFS=. read -r KERNEL MINOR _ <<< "$SYS"
|
||||||
|
|
||||||
if grep -qi "socket(s)" <<< "$(lscpu)"; then
|
checkSockets
|
||||||
SOCKETS=$(lscpu | grep -m 1 -i 'socket(s)' | awk '{print $2}')
|
checkCores
|
||||||
[ -z "${SOCKETS##*[!0-9]*}" ] && SOCKETS=1
|
|
||||||
[ "$SOCKETS" -lt "1" ] && SOCKETS=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
normalizeCpuCores
|
|
||||||
checkStorage
|
checkStorage
|
||||||
checkFilesystem
|
getMemoryInfo
|
||||||
normalizeRamSize
|
|
||||||
|
|
||||||
# Print system info
|
# Print system info
|
||||||
SYS="${SYS/-generic/}"
|
SYS="${SYS/-generic/}"
|
||||||
|
FS=$(stat -f -c %T "$STORAGE")
|
||||||
FS="${FS/UNKNOWN //}"
|
FS="${FS/UNKNOWN //}"
|
||||||
FS="${FS/ext2\/ext3/ext4}"
|
FS="${FS/ext2\/ext3/ext4}"
|
||||||
FS=$(echo "$FS" | sed 's/[)(]//g')
|
FS=$(echo "$FS" | sed 's/[)(]//g')
|
||||||
@@ -349,8 +280,11 @@ TOTAL_MEM=$(formatBytes "$RAM_TOTAL" "up")
|
|||||||
echo "❯ CPU: ${CPU} | RAM: ${AVAIL_MEM/ GB/}/$TOTAL_MEM | DISK: $SPACE_GB (${FS}) | KERNEL: ${SYS}"
|
echo "❯ CPU: ${CPU} | RAM: ${AVAIL_MEM/ GB/}/$TOTAL_MEM | DISK: $SPACE_GB (${FS}) | KERNEL: ${SYS}"
|
||||||
echo
|
echo
|
||||||
|
|
||||||
|
checkHost
|
||||||
checkKvm
|
checkKvm
|
||||||
|
|
||||||
|
# Runtime state is intentionally discarded at each container start; persistent
|
||||||
|
# machine and disk identity lives under STORAGE instead.
|
||||||
# Cleanup files
|
# Cleanup files
|
||||||
rm -f "$QEMU_DIR"/dsm.url
|
rm -f "$QEMU_DIR"/dsm.url
|
||||||
rm -f "$QEMU_DIR"/{qemu.*,*.{pid,sock,pipe}}
|
rm -f "$QEMU_DIR"/{qemu.*,*.{pid,sock,pipe}}
|
||||||
+28
-4
@@ -3,6 +3,8 @@ set -Eeuo pipefail
|
|||||||
|
|
||||||
: "${URL:=""}" # URL of the PAT file to be downloaded.
|
: "${URL:=""}" # URL of the PAT file to be downloaded.
|
||||||
|
|
||||||
|
# Persist the exact PAT base name so future starts reopen the matching boot,
|
||||||
|
# system, and cached installation files.
|
||||||
if [ -f "$STORAGE/dsm.ver" ]; then
|
if [ -f "$STORAGE/dsm.ver" ]; then
|
||||||
BASE=$(<"$STORAGE/dsm.ver")
|
BASE=$(<"$STORAGE/dsm.ver")
|
||||||
BASE="${BASE//[![:print:]]/}"
|
BASE="${BASE//[![:print:]]/}"
|
||||||
@@ -16,6 +18,8 @@ FN="boot.pat"
|
|||||||
DIR=$(find / -maxdepth 1 -type d -iname "$FN" -print -quit)
|
DIR=$(find / -maxdepth 1 -type d -iname "$FN" -print -quit)
|
||||||
[ ! -d "$DIR" ] && DIR=$(find "$STORAGE" -maxdepth 1 -type d -iname "$FN" -print -quit)
|
[ ! -d "$DIR" ] && DIR=$(find "$STORAGE" -maxdepth 1 -type d -iname "$FN" -print -quit)
|
||||||
|
|
||||||
|
# A boot.pat directory bind represents already extracted boot and system
|
||||||
|
# images and therefore takes precedence over PAT file or URL discovery.
|
||||||
if [ -d "$DIR" ]; then
|
if [ -d "$DIR" ]; then
|
||||||
BASE="DSM_VirtualDSM" && URL="file://$DIR"
|
BASE="DSM_VirtualDSM" && URL="file://$DIR"
|
||||||
if [[ ! -s "$STORAGE/$BASE.boot.img" || ! -s "$STORAGE/$BASE.system.img" ]]; then
|
if [[ ! -s "$STORAGE/$BASE.boot.img" || ! -s "$STORAGE/$BASE.system.img" ]]; then
|
||||||
@@ -29,6 +33,8 @@ FILE=$(find / -maxdepth 1 -type f -iname "$FN" -print -quit)
|
|||||||
|
|
||||||
URL=$(strip "$URL")
|
URL=$(strip "$URL")
|
||||||
|
|
||||||
|
# Derive a filesystem-safe identity from the URL only when no local boot.pat
|
||||||
|
# source was supplied; preserve an existing system image identity if present.
|
||||||
if [ -n "$URL" ] && [ ! -s "$FILE" ] && [ ! -d "$DIR" ]; then
|
if [ -n "$URL" ] && [ ! -s "$FILE" ] && [ ! -d "$DIR" ]; then
|
||||||
BASE=$(basename "$URL" .pat)
|
BASE=$(basename "$URL" .pat)
|
||||||
if [ ! -s "$STORAGE/$BASE.system.img" ]; then
|
if [ ! -s "$STORAGE/$BASE.system.img" ]; then
|
||||||
@@ -42,6 +48,8 @@ if [ -n "$URL" ] && [ ! -s "$FILE" ] && [ ! -d "$DIR" ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# A complete matching image pair is the installation marker; the cached PAT
|
||||||
|
# itself is optional after installation.
|
||||||
if [[ -s "$STORAGE/$BASE.boot.img" && -s "$STORAGE/$BASE.system.img" ]]; then
|
if [[ -s "$STORAGE/$BASE.boot.img" && -s "$STORAGE/$BASE.system.img" ]]; then
|
||||||
return 0 # Previous installation found
|
return 0 # Previous installation found
|
||||||
fi
|
fi
|
||||||
@@ -55,6 +63,8 @@ DL_GLOBAL="https://global.synologydownload.com/download/DSM"
|
|||||||
[[ "${URL,,}" == *"cndl.synology"* ]] && DL="$DL_CHINA"
|
[[ "${URL,,}" == *"cndl.synology"* ]] && DL="$DL_CHINA"
|
||||||
[[ "${URL,,}" == *"global.synology"* ]] && DL="$DL_GLOBAL"
|
[[ "${URL,,}" == *"global.synology"* ]] && DL="$DL_GLOBAL"
|
||||||
|
|
||||||
|
# Honor an explicitly selected Synology mirror first, otherwise choose the
|
||||||
|
# China or global endpoint from the detected country.
|
||||||
if [ -z "$DL" ]; then
|
if [ -z "$DL" ]; then
|
||||||
[ -z "$COUNTRY" ] && setCountry
|
[ -z "$COUNTRY" ] && setCountry
|
||||||
[ -z "$COUNTRY" ] && info "Warning: could not detect country to select mirror!"
|
[ -z "$COUNTRY" ] && info "Warning: could not detect country to select mirror!"
|
||||||
@@ -98,6 +108,8 @@ if [[ "${FS,,}" == "fat"* || "${FS,,}" == "vfat"* || "${FS,,}" == "msdos"* ]]; t
|
|||||||
error "Unable to install on $FS filesystems, please use a different filesystem for /storage." && exit 61
|
error "Unable to install on $FS filesystems, please use a different filesystem for /storage." && exit 61
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Extract beside storage on Unix filesystems to avoid container-space limits;
|
||||||
|
# use /tmp for filesystems that cannot safely host the installer workspace.
|
||||||
if [[ "${FS,,}" != "exfat"* && "${FS,,}" != "ntfs"* && "${FS,,}" != "unknown"* ]]; then
|
if [[ "${FS,,}" != "exfat"* && "${FS,,}" != "ntfs"* && "${FS,,}" != "unknown"* ]]; then
|
||||||
TMP="$STORAGE/tmp"
|
TMP="$STORAGE/tmp"
|
||||||
rm -rf "$TMP"
|
rm -rf "$TMP"
|
||||||
@@ -206,6 +218,8 @@ fi
|
|||||||
|
|
||||||
SIZE=$(stat -c%s "$PAT")
|
SIZE=$(stat -c%s "$PAT")
|
||||||
|
|
||||||
|
# Full Virtual DSM PAT files are substantially larger than update packs;
|
||||||
|
# reject undersized inputs before attempting destructive image preparation.
|
||||||
if ((SIZE<250000000)); then
|
if ((SIZE<250000000)); then
|
||||||
error "The specified PAT file is probably an update pack as it's too small." && exit 62
|
error "The specified PAT file is probably an update pack as it's too small." && exit 62
|
||||||
fi
|
fi
|
||||||
@@ -213,6 +227,8 @@ fi
|
|||||||
MSG="Extracting installation image..."
|
MSG="Extracting installation image..."
|
||||||
info "Install: $MSG" && html "$MSG"
|
info "Install: $MSG" && html "$MSG"
|
||||||
|
|
||||||
|
# Newer PAT files are normal tar archives; older encrypted/proprietary forms
|
||||||
|
# require the bundled extractor as a compatibility fallback.
|
||||||
if { tar tf "$PAT"; } >/dev/null 2>&1; then
|
if { tar tf "$PAT"; } >/dev/null 2>&1; then
|
||||||
|
|
||||||
tar xpf "$PAT" -C "$TMP/."
|
tar xpf "$PAT" -C "$TMP/."
|
||||||
@@ -231,6 +247,8 @@ fi
|
|||||||
MSG="Preparing system partition..."
|
MSG="Preparing system partition..."
|
||||||
info "Install: $MSG" && html "$MSG"
|
info "Install: $MSG" && html "$MSG"
|
||||||
|
|
||||||
|
# The PAT boot archive becomes the persistent QEMU boot disk after its
|
||||||
|
# companion system partition has been assembled.
|
||||||
BOOT=$(find "$TMP" -name "*.bin.zip" -print -quit)
|
BOOT=$(find "$TMP" -name "*.bin.zip" -print -quit)
|
||||||
[ -z "$BOOT" ] && error "The PAT file contains no boot image." && exit 67
|
[ -z "$BOOT" ] && error "The PAT file contains no boot image." && exit 67
|
||||||
[ ! -s "$BOOT" ] && error "The PAT boot image archive is empty." && exit 67
|
[ ! -s "$BOOT" ] && error "The PAT boot image archive is empty." && exit 67
|
||||||
@@ -254,7 +272,7 @@ if ! touch "$SYSTEM"; then
|
|||||||
error "Could not create file $SYSTEM for the system disk." && exit 98
|
error "Could not create file $SYSTEM for the system disk." && exit 98
|
||||||
fi
|
fi
|
||||||
|
|
||||||
! setOwner "$SYSTEM" && warn "failed to set the owner for \"$SYSTEM\" !"
|
setOwner "$SYSTEM" || warn "failed to set the owner for \"$SYSTEM\" !"
|
||||||
|
|
||||||
if [[ "${FS,,}" == "btrfs" ]]; then
|
if [[ "${FS,,}" == "btrfs" ]]; then
|
||||||
{ chattr +C "$SYSTEM"; } || :
|
{ chattr +C "$SYSTEM"; } || :
|
||||||
@@ -273,6 +291,8 @@ if ! fallocate -l "$SYSTEM_SIZE" "$SYSTEM" &>/dev/null; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Recreate Synology's expected DOS partition layout inside the fixed 10 GiB
|
||||||
|
# system image before populating the ext4 root partition.
|
||||||
PART="$TMP/partition.fdisk"
|
PART="$TMP/partition.fdisk"
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -320,6 +340,8 @@ OFFSET="1048576" # 2048 * 512
|
|||||||
NUMBLOCKS="2097152" # (16777216 * 512) / 4096
|
NUMBLOCKS="2097152" # (16777216 * 512) / 4096
|
||||||
MSG="Installing system partition..."
|
MSG="Installing system partition..."
|
||||||
|
|
||||||
|
# Build the ext4 filesystem directly from the extracted tree under fakeroot,
|
||||||
|
# preserving archive ownership without mounting a loop device.
|
||||||
fakeroot -- bash -c "set -Eeu;\
|
fakeroot -- bash -c "set -Eeu;\
|
||||||
[ -s $HDP.txz ] && tar xpfJ $HDP.txz --absolute-names -C $MOUNT/;\
|
[ -s $HDP.txz ] && tar xpfJ $HDP.txz --absolute-names -C $MOUNT/;\
|
||||||
[ -s $IDB.txz ] && tar xpfJ $IDB.txz --absolute-names -C $INDEX_DB/;\
|
[ -s $IDB.txz ] && tar xpfJ $IDB.txz --absolute-names -C $INDEX_DB/;\
|
||||||
@@ -329,8 +351,10 @@ fakeroot -- bash -c "set -Eeu;\
|
|||||||
|
|
||||||
rm -rf "$MOUNT"
|
rm -rf "$MOUNT"
|
||||||
echo "$BASE" > "$STORAGE/dsm.ver"
|
echo "$BASE" > "$STORAGE/dsm.ver"
|
||||||
! setOwner "$STORAGE/dsm.ver" && warn "failed to set the owner for \"$STORAGE/dsm.ver\" !"
|
setOwner "$STORAGE/dsm.ver" || warn "failed to set the owner for \"$STORAGE/dsm.ver\" !"
|
||||||
|
|
||||||
|
# Do not keep a second copy when the source PAT already lives in storage;
|
||||||
|
# downloaded or externally mounted sources are cached for later reuse.
|
||||||
if [[ "$URL" == "file://$STORAGE/$BASE.pat" ]]; then
|
if [[ "$URL" == "file://$STORAGE/$BASE.pat" ]]; then
|
||||||
rm -f "$PAT"
|
rm -f "$PAT"
|
||||||
else
|
else
|
||||||
@@ -338,11 +362,11 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f "$STORAGE/$BASE.pat" ]; then
|
if [ -f "$STORAGE/$BASE.pat" ]; then
|
||||||
! setOwner "$STORAGE/$BASE.pat" && warn "failed to set the owner for \"$STORAGE/$BASE.pat\" !"
|
setOwner "$STORAGE/$BASE.pat" || warn "failed to set the owner for \"$STORAGE/$BASE.pat\" !"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mv -f "$BOOT" "$STORAGE/$BASE.boot.img"
|
mv -f "$BOOT" "$STORAGE/$BASE.boot.img"
|
||||||
! setOwner "$STORAGE/$BASE.boot.img" && warn "failed to set the owner for \"$STORAGE/$BASE.boot.img\" !"
|
setOwner "$STORAGE/$BASE.boot.img" || warn "failed to set the owner for \"$STORAGE/$BASE.boot.img\" !"
|
||||||
|
|
||||||
rm -rf "$TMP"
|
rm -rf "$TMP"
|
||||||
|
|
||||||
|
|||||||
+152
-29
@@ -1,78 +1,144 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
msg="Checking memory..."
|
normalizeMemory() {
|
||||||
enabled "$DEBUG" && echo "$msg"
|
|
||||||
|
local wanted
|
||||||
|
|
||||||
|
RAM_SPARE=500000000
|
||||||
|
RAM_MINIMUM="${RAM_MINIMUM:-1073741824}"
|
||||||
|
|
||||||
|
RAM_MINIMUM=$(strip "$RAM_MINIMUM")
|
||||||
|
RAM_MINIMUM="${RAM_MINIMUM// /}"
|
||||||
|
RAM_MINIMUM=$(echo "${RAM_MINIMUM^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
|
||||||
|
numfmt --from=iec "$RAM_MINIMUM" &>/dev/null || {
|
||||||
|
error "Invalid RAM_MINIMUM: $RAM_MINIMUM"
|
||||||
|
exit 16
|
||||||
|
}
|
||||||
|
RAM_MINIMUM=$(numfmt --from=iec "$RAM_MINIMUM")
|
||||||
|
|
||||||
|
RAM_SIZE=$(strip "$RAM_SIZE")
|
||||||
|
RAM_SIZE="${RAM_SIZE// /}"
|
||||||
|
[ -z "$RAM_SIZE" ] && RAM_SIZE="2G"
|
||||||
|
|
||||||
|
if [[ "${RAM_SIZE,,}" != "max" && "${RAM_SIZE,,}" != "half" ]]; then
|
||||||
|
|
||||||
|
# Bare values below 130 are interpreted as GiB for convenience; larger bare
|
||||||
|
# values are treated as MiB to preserve historical configurations.
|
||||||
|
if [ -z "${RAM_SIZE//[0-9. ]}" ]; then
|
||||||
|
[ "${RAM_SIZE%%.*}" -lt "130" ] && RAM_SIZE="${RAM_SIZE}G" || RAM_SIZE="${RAM_SIZE}M"
|
||||||
|
fi
|
||||||
|
|
||||||
|
RAM_SIZE=$(echo "${RAM_SIZE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
|
||||||
|
numfmt --from=iec "$RAM_SIZE" &>/dev/null || {
|
||||||
|
error "Invalid RAM_SIZE: $RAM_SIZE"
|
||||||
|
exit 16
|
||||||
|
}
|
||||||
|
|
||||||
|
wanted=$(numfmt --from=iec "$RAM_SIZE")
|
||||||
|
|
||||||
|
if [ "$wanted" -lt "$RAM_MINIMUM" ]; then
|
||||||
|
error "$(app) requires at least $(formatBytes "$RAM_MINIMUM") of RAM, but RAM_SIZE is set to $(formatBytes "$wanted")."
|
||||||
|
exit 16
|
||||||
|
fi
|
||||||
|
|
||||||
|
# QEMU requires a whole-number memory value, so convert decimal sizes to MiB.
|
||||||
|
if [[ "$RAM_SIZE" == *.* ]]; then
|
||||||
|
RAM_SIZE="$(( wanted / 1048576 ))M"
|
||||||
|
fi
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
app() {
|
|
||||||
echo "Virtual DSM"
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
checkConfiguredMemory() {
|
checkConfiguredMemory() {
|
||||||
|
|
||||||
|
local final="$1"
|
||||||
|
|
||||||
if disabled "$RAM_CHECK" || [[ "${RAM_SIZE,,}" == "max" || "${RAM_SIZE,,}" == "half" ]]; then
|
if disabled "$RAM_CHECK" || [[ "${RAM_SIZE,,}" == "max" || "${RAM_SIZE,,}" == "half" ]]; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local wanted msg avail_mem
|
local wanted avail_mem
|
||||||
wanted=$(numfmt --from=iec "$RAM_SIZE")
|
wanted=$(numfmt --from=iec "$RAM_SIZE")
|
||||||
avail_mem=$(formatBytes "$RAM_AVAIL")
|
avail_mem=$(formatBytes "$RAM_AVAIL")
|
||||||
|
|
||||||
if (( (wanted + RAM_SPARE) > RAM_AVAIL )); then
|
if (( (wanted + RAM_SPARE) > RAM_AVAIL )); then
|
||||||
msg="Your configured RAM_SIZE of ${RAM_SIZE/G/ GB} is too high for the $avail_mem of free memory available,"
|
|
||||||
|
local msg="Your configured RAM_SIZE of ${RAM_SIZE/G/ GB} is too high for the $avail_mem of free memory available,"
|
||||||
|
|
||||||
|
# ZFS ARC can release cached memory under pressure, so this free-memory
|
||||||
|
# heuristic remains informational instead of rewriting RAM_SIZE.
|
||||||
if [[ "${FS,,}" == "zfs" ]]; then
|
if [[ "${FS,,}" == "zfs" ]]; then
|
||||||
info "$msg but since ZFS is active this will be ignored."
|
|
||||||
|
enabled "$final" && info "$msg but since ZFS is active this will be ignored."
|
||||||
|
|
||||||
else
|
else
|
||||||
|
|
||||||
RAM_SIZE="max"
|
RAM_SIZE="max"
|
||||||
warn "$msg it will automatically be adjusted to a lower amount."
|
RAM_WARNING="$msg it will automatically be adjusted to a lower amount."
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
else
|
else
|
||||||
|
|
||||||
if (( (wanted + (RAM_SPARE * 3)) > RAM_AVAIL )); then
|
if (( (wanted + (RAM_SPARE * 3)) > RAM_AVAIL )); then
|
||||||
msg="your configured RAM_SIZE of ${RAM_SIZE/G/ GB} is very close to the $avail_mem of free memory available,"
|
|
||||||
|
local msg="your configured RAM_SIZE of ${RAM_SIZE/G/ GB} is very close to the $avail_mem of free memory available,"
|
||||||
|
|
||||||
if [[ "${FS,,}" == "zfs" ]]; then
|
if [[ "${FS,,}" == "zfs" ]]; then
|
||||||
info "$msg but since ZFS is active this will be ignored."
|
enabled "$final" && info "$msg but since ZFS is active this will be ignored."
|
||||||
else
|
else
|
||||||
warn "$msg please consider a lower amount."
|
enabled "$final" && warn "$msg please consider a lower amount."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
configureHalfMemory() {
|
configureHalfMemory() {
|
||||||
local wanted
|
|
||||||
|
|
||||||
if [[ "${RAM_SIZE,,}" != "half" ]]; then
|
if [[ "${RAM_SIZE,,}" != "half" ]]; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if (( (RAM_AVAIL / 2) > RAM_SPARE )); then
|
if (( (RAM_AVAIL / 2) > RAM_SPARE )); then
|
||||||
wanted=$(( (RAM_AVAIL / 2) / 1048577 ))
|
|
||||||
RAM_SIZE="${wanted}M"
|
local wanted=$(( RAM_AVAIL / 2 ))
|
||||||
info "Allocated $wanted MB of RAM for $(app)."
|
|
||||||
|
# Divide by one byte more than a MiB to round down
|
||||||
|
local target=$(( wanted / 1048577 ))
|
||||||
|
RAM_SIZE="${target}M"
|
||||||
|
RAM_ALLOCATION="$wanted"
|
||||||
|
|
||||||
else
|
else
|
||||||
|
|
||||||
RAM_SIZE="max"
|
RAM_SIZE="max"
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
configureMaxMemory() {
|
configureMaxMemory() {
|
||||||
local wanted
|
|
||||||
|
|
||||||
if [[ "${RAM_SIZE,,}" != "max" ]]; then
|
if [[ "${RAM_SIZE,,}" != "max" ]]; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# max keeps a host reserve when possible, but on very small systems falls back
|
||||||
|
# to half the available memory to avoid starving the container.
|
||||||
if (( RAM_AVAIL < (RAM_SPARE * 2) )); then
|
if (( RAM_AVAIL < (RAM_SPARE * 2) )); then
|
||||||
|
|
||||||
wanted=$(( RAM_AVAIL / 2 ))
|
local wanted=$(( RAM_AVAIL / 2 ))
|
||||||
|
|
||||||
else
|
else
|
||||||
|
|
||||||
wanted=$(( RAM_AVAIL - (RAM_SPARE * 3) ))
|
local wanted=$(( RAM_AVAIL - (RAM_SPARE * 3) ))
|
||||||
|
|
||||||
if (( wanted < (RAM_SPARE * 6) )); then
|
if (( wanted < (RAM_SPARE * 6) )); then
|
||||||
wanted=$(( RAM_AVAIL - RAM_SPARE ))
|
wanted=$(( RAM_AVAIL - RAM_SPARE ))
|
||||||
@@ -80,33 +146,90 @@ configureMaxMemory() {
|
|||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
wanted=$(( wanted / 1048577 ))
|
# Divide by one byte more than a MiB to round down
|
||||||
RAM_SIZE="${wanted}M"
|
local target=$(( wanted / 1048577 ))
|
||||||
|
RAM_SIZE="${target}M"
|
||||||
|
RAM_ALLOCATION="$wanted"
|
||||||
|
|
||||||
info "Allocated $wanted MB of RAM for $(app)."
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
showMemoryLimitHint() {
|
||||||
|
|
||||||
|
local kernel
|
||||||
|
kernel=$(uname -r)
|
||||||
|
|
||||||
|
if [[ "${kernel,,}" == *-wsl2* ]]; then
|
||||||
|
echo
|
||||||
|
info "Docker Desktop (WSL2) is detected, follow these instructions:"
|
||||||
|
info ""
|
||||||
|
info "Increase the memory limit in \"%UserProfile%\\.wslconfig\" by setting \"memory=<size>\" under \"[wsl2]\"."
|
||||||
|
info "Then run \"wsl --shutdown\" in PowerShell and restart Docker Desktop for the new limit to take effect."
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
checkMinimumMemory() {
|
checkMinimumMemory() {
|
||||||
local wanted
|
|
||||||
|
|
||||||
|
local wanted
|
||||||
wanted=$(numfmt --from=iec "$RAM_SIZE")
|
wanted=$(numfmt --from=iec "$RAM_SIZE")
|
||||||
|
|
||||||
if [ "$wanted" -lt "$RAM_MINIMUM" ]; then
|
if [ "$wanted" -lt "$RAM_MINIMUM" ]; then
|
||||||
wanted=$(( wanted / 1048577 ))
|
|
||||||
error "Not enough memory available, there is only $wanted MB left!"
|
error "$(app) requires at least $(formatBytes "$RAM_MINIMUM") of RAM, but only $(formatBytes "$wanted") can be allocated."
|
||||||
|
|
||||||
|
showMemoryLimitHint
|
||||||
|
|
||||||
exit 16
|
exit 16
|
||||||
fi
|
fi
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
getMemoryInfo
|
checkMemoryAllocation() {
|
||||||
|
|
||||||
checkConfiguredMemory
|
local final="${1:-N}"
|
||||||
configureHalfMemory
|
local configured
|
||||||
configureMaxMemory
|
|
||||||
checkMinimumMemory
|
normalizeMemory
|
||||||
|
configured="$RAM_SIZE"
|
||||||
|
|
||||||
|
RAM_WARNING=""
|
||||||
|
RAM_ALLOCATION=""
|
||||||
|
|
||||||
|
getMemoryInfo
|
||||||
|
checkConfiguredMemory "$final"
|
||||||
|
|
||||||
|
configureHalfMemory
|
||||||
|
configureMaxMemory
|
||||||
|
checkMinimumMemory
|
||||||
|
|
||||||
|
if enabled "$final"; then
|
||||||
|
[ -n "$RAM_WARNING" ] && warn "$RAM_WARNING"
|
||||||
|
[ -n "$RAM_ALLOCATION" ] && info "Allocated $(formatBytes "$RAM_ALLOCATION") of RAM for $(app)."
|
||||||
|
else
|
||||||
|
RAM_SIZE="$configured"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
checkMemoryRequirement() {
|
||||||
|
|
||||||
|
checkMemoryAllocation "N"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
finalizeMemory() {
|
||||||
|
|
||||||
|
checkMemoryAllocation "Y"
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
checkMemoryRequirement
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
+306
-136
@@ -84,8 +84,7 @@ getMTU() {
|
|||||||
|
|
||||||
minMTU() {
|
minMTU() {
|
||||||
|
|
||||||
local mtu=""
|
local mtu min=""
|
||||||
local min=""
|
|
||||||
|
|
||||||
for mtu in "$@"; do
|
for mtu in "$@"; do
|
||||||
[[ -z "$mtu" || "$mtu" == "0" ]] && continue
|
[[ -z "$mtu" || "$mtu" == "0" ]] && continue
|
||||||
@@ -124,7 +123,7 @@ gatewayMAC() {
|
|||||||
maskToCIDR() {
|
maskToCIDR() {
|
||||||
|
|
||||||
local mask="$1"
|
local mask="$1"
|
||||||
local prefix=""
|
local prefix
|
||||||
|
|
||||||
if ! command -v ipcalc > /dev/null 2>&1; then
|
if ! command -v ipcalc > /dev/null 2>&1; then
|
||||||
error "Required command 'ipcalc' is not installed!"
|
error "Required command 'ipcalc' is not installed!"
|
||||||
@@ -154,7 +153,7 @@ maskToCIDR() {
|
|||||||
networkCIDR() {
|
networkCIDR() {
|
||||||
|
|
||||||
local ip="$1"
|
local ip="$1"
|
||||||
local network=""
|
local network
|
||||||
|
|
||||||
network=$(ipcalc -n -b "$ip/$MASK" 2>/dev/null | awk '
|
network=$(ipcalc -n -b "$ip/$MASK" 2>/dev/null | awk '
|
||||||
/^Network:/ {
|
/^Network:/ {
|
||||||
@@ -172,6 +171,37 @@ networkCIDR() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
upstreamIP() {
|
||||||
|
|
||||||
|
local subnet="$1"
|
||||||
|
local guest="$2"
|
||||||
|
local gateway="$3"
|
||||||
|
local broadcast candidate last
|
||||||
|
|
||||||
|
broadcast=$(ipcalc -n -b "$subnet" 2>/dev/null | awk '
|
||||||
|
/^Broadcast:/ {
|
||||||
|
print $2
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
')
|
||||||
|
|
||||||
|
if [[ ! "$broadcast" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
last="${broadcast##*.}"
|
||||||
|
|
||||||
|
for (( last--; last>=2; last-- )); do
|
||||||
|
candidate="${broadcast%.*}.$last"
|
||||||
|
[[ "$candidate" == "$guest" || "$candidate" == "$gateway" ]] && continue
|
||||||
|
|
||||||
|
echo "$candidate"
|
||||||
|
return 0
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
detectInterface() {
|
detectInterface() {
|
||||||
|
|
||||||
if [ -n "$DEV" ]; then
|
if [ -n "$DEV" ]; then
|
||||||
@@ -207,16 +237,23 @@ formatAddress() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defaultGateway() {
|
||||||
|
|
||||||
|
ip -4 route list default dev "$1" 2>/dev/null |
|
||||||
|
awk '$1 == "default" { for (i = 1; i < NF; i++) if ($i == "via") { print $(i + 1); exit } }' || :
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
detectAddresses() {
|
detectAddresses() {
|
||||||
|
|
||||||
GATEWAY=$(ip route list dev "$DEV" | awk ' /^default/ {print $3}' | head -n 1)
|
GATEWAY=$(defaultGateway "$DEV")
|
||||||
{ UPLINK=$(ip address show dev "$DEV" | grep inet | awk '/inet / { print $2 }' | cut -f1 -d/ | head -n 1); } 2>/dev/null || :
|
{ UPLINK=$(ip address show dev "$DEV" | grep inet | awk '/inet / { print $2 }' | cut -f1 -d/ | head -n 1); } 2>/dev/null || :
|
||||||
|
|
||||||
IP6=""
|
IP6=""
|
||||||
|
|
||||||
if [ -f /proc/net/if_inet6 ] && [[ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null)" != "1" ]]; then
|
if [ -f /proc/net/if_inet6 ] && [[ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null)" != "1" ]]; then
|
||||||
local rc=0
|
{ IP6=$(ip -6 addr show dev "$DEV" scope global up); local rc=$?; } 2>/dev/null || :
|
||||||
{ IP6=$(ip -6 addr show dev "$DEV" scope global up); rc=$?; } 2>/dev/null || :
|
|
||||||
(( rc != 0 )) && IP6=""
|
(( rc != 0 )) && IP6=""
|
||||||
[ -n "$IP6" ] && IP6=$(echo "$IP6" | sed -e's/^.*inet6 \([^ ]*\)\/.*$/\1/;t;d' | head -n 1)
|
[ -n "$IP6" ] && IP6=$(echo "$IP6" | sed -e's/^.*inet6 \([^ ]*\)\/.*$/\1/;t;d' | head -n 1)
|
||||||
fi
|
fi
|
||||||
@@ -226,7 +263,7 @@ detectAddresses() {
|
|||||||
|
|
||||||
detectAdapter() {
|
detectAdapter() {
|
||||||
|
|
||||||
local result=""
|
local result
|
||||||
|
|
||||||
NIC=""
|
NIC=""
|
||||||
BUS=""
|
BUS=""
|
||||||
@@ -252,7 +289,7 @@ detectAdapter() {
|
|||||||
|
|
||||||
containerID() {
|
containerID() {
|
||||||
|
|
||||||
local id=""
|
local id
|
||||||
|
|
||||||
id=$(hostname -s 2>/dev/null || true)
|
id=$(hostname -s 2>/dev/null || true)
|
||||||
|
|
||||||
@@ -260,7 +297,7 @@ containerID() {
|
|||||||
id=$(< /etc/machine-id)
|
id=$(< /etc/machine-id)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$id" ] && [ -s /proc/sys/kernel/random/boot_id ]; then
|
if [ -z "$id" ] && [ -r /proc/sys/kernel/random/boot_id ]; then
|
||||||
id=$(< /proc/sys/kernel/random/boot_id)
|
id=$(< /proc/sys/kernel/random/boot_id)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -270,6 +307,27 @@ containerID() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canBindPrivilegedPort() {
|
||||||
|
|
||||||
|
local port="$1"
|
||||||
|
local proto="${2:-tcp}"
|
||||||
|
local start="1024"
|
||||||
|
local rc=1
|
||||||
|
|
||||||
|
[ -r /proc/sys/net/ipv4/ip_unprivileged_port_start ] &&
|
||||||
|
start=$(< /proc/sys/net/ipv4/ip_unprivileged_port_start)
|
||||||
|
|
||||||
|
(( port >= start )) && return 0
|
||||||
|
|
||||||
|
if [[ "$proto" == "udp" ]]; then
|
||||||
|
{ timeout 0.1 nc -4 -n -d -u -l 127.0.0.1 "$port" > /dev/null 2>&1; rc=$?; } || :
|
||||||
|
else
|
||||||
|
{ timeout 0.1 nc -4 -n -d -l 127.0.0.1 "$port" > /dev/null 2>&1; rc=$?; } || :
|
||||||
|
fi
|
||||||
|
|
||||||
|
(( rc == 124 ))
|
||||||
|
}
|
||||||
|
|
||||||
disableIPv6() {
|
disableIPv6() {
|
||||||
|
|
||||||
local dev="$1"
|
local dev="$1"
|
||||||
@@ -286,7 +344,7 @@ disableIPv6() {
|
|||||||
subnetInUse() {
|
subnetInUse() {
|
||||||
|
|
||||||
local subnet="$1"
|
local subnet="$1"
|
||||||
local broader="" narrower="" routes=""
|
local broader narrower routes
|
||||||
|
|
||||||
if ! broader=$(ip -4 route show table all match "$subnet" 2>/dev/null); then
|
if ! broader=$(ip -4 route show table all match "$subnet" 2>/dev/null); then
|
||||||
error "Failed to inspect existing routes for subnet $subnet."
|
error "Failed to inspect existing routes for subnet $subnet."
|
||||||
@@ -324,17 +382,18 @@ guestIP() {
|
|||||||
natGuestIP() {
|
natGuestIP() {
|
||||||
|
|
||||||
local ip="$1"
|
local ip="$1"
|
||||||
local start="" guest="" subnet=""
|
local guest subnet second third
|
||||||
local second="" third="" rc=""
|
|
||||||
|
|
||||||
third=$(cut -d. -f3 <<< "$ip")
|
third=$(cut -d. -f3 <<< "$ip")
|
||||||
|
|
||||||
if [[ "$ip" == "172.30."* ]]; then
|
if [[ "$ip" == "172.30."* ]]; then
|
||||||
start="31"
|
local start="31"
|
||||||
else
|
else
|
||||||
start="30"
|
local start="30"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Scan adjacent 172.30/31 through 172.254 subnets to avoid Docker routes
|
||||||
|
# while retaining the original third octet and guest host number.
|
||||||
for (( second=start; second<=254; second++ )); do
|
for (( second=start; second<=254; second++ )); do
|
||||||
guest=$(guestIP "172.$second.$third.0" 2)
|
guest=$(guestIP "172.$second.$third.0" 2)
|
||||||
subnet=$(networkCIDR "$guest") || return 1
|
subnet=$(networkCIDR "$guest") || return 1
|
||||||
@@ -342,7 +401,7 @@ natGuestIP() {
|
|||||||
if subnetInUse "$subnet"; then
|
if subnetInUse "$subnet"; then
|
||||||
continue
|
continue
|
||||||
else
|
else
|
||||||
rc=$?
|
local rc=$?
|
||||||
(( rc == 1 )) || return 1
|
(( rc == 1 )) || return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -357,7 +416,7 @@ natGuestIP() {
|
|||||||
if subnetInUse "$subnet"; then
|
if subnetInUse "$subnet"; then
|
||||||
continue
|
continue
|
||||||
else
|
else
|
||||||
rc=$?
|
local rc=$?
|
||||||
(( rc == 1 )) || return 1
|
(( rc == 1 )) || return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -369,36 +428,6 @@ natGuestIP() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
kernelAtLeast() {
|
|
||||||
|
|
||||||
local major="$1"
|
|
||||||
local minor="${2:-0}"
|
|
||||||
|
|
||||||
(( KERNEL > major || (KERNEL == major && MINOR >= minor) ))
|
|
||||||
}
|
|
||||||
|
|
||||||
canBindToDevice() {
|
|
||||||
|
|
||||||
local dev="$1"
|
|
||||||
[ -n "$dev" ] || return 1
|
|
||||||
|
|
||||||
kernelAtLeast 5 7 || return 1
|
|
||||||
[ -d "/sys/class/net/$dev" ] || return 1
|
|
||||||
command -v python3 > /dev/null 2>&1 || return 0
|
|
||||||
|
|
||||||
python3 - "$dev" > /dev/null 2>&1 <<'PY'
|
|
||||||
import socket
|
|
||||||
import sys
|
|
||||||
|
|
||||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
||||||
sock.setsockopt(
|
|
||||||
socket.SOL_SOCKET,
|
|
||||||
socket.SO_BINDTODEVICE,
|
|
||||||
sys.argv[1].encode() + b"\0",
|
|
||||||
)
|
|
||||||
PY
|
|
||||||
}
|
|
||||||
|
|
||||||
# ######################################
|
# ######################################
|
||||||
# DNS / port helpers
|
# DNS / port helpers
|
||||||
# ######################################
|
# ######################################
|
||||||
@@ -411,7 +440,9 @@ configureDNS() {
|
|||||||
local host="$4"
|
local host="$4"
|
||||||
local mask="$5"
|
local mask="$5"
|
||||||
local gateway="$6"
|
local gateway="$6"
|
||||||
local arguments="$DNSMASQ_OPTS" rc
|
local upstream="${7:-}"
|
||||||
|
local arguments="$DNSMASQ_OPTS"
|
||||||
|
local pid
|
||||||
|
|
||||||
if ! echo "$gateway" > /run/shm/qemu.gw; then
|
if ! echo "$gateway" > /run/shm/qemu.gw; then
|
||||||
error "Failed to write gateway file."
|
error "Failed to write gateway file."
|
||||||
@@ -421,7 +452,10 @@ configureDNS() {
|
|||||||
enabled "${DNSMASQ_DISABLE:-}" && return 0
|
enabled "${DNSMASQ_DISABLE:-}" && return 0
|
||||||
enabled "$DEBUG" && echo "Starting dnsmasq daemon..."
|
enabled "$DEBUG" && echo "Starting dnsmasq daemon..."
|
||||||
|
|
||||||
[ -s "$DNSMASQ_PID" ] && pKill "$(<"$DNSMASQ_PID")"
|
if readPidFile pid "$DNSMASQ_PID"; then
|
||||||
|
pKill "$pid"
|
||||||
|
fi
|
||||||
|
|
||||||
rm -f "$DNSMASQ_PID"
|
rm -f "$DNSMASQ_PID"
|
||||||
|
|
||||||
if isNAT; then
|
if isNAT; then
|
||||||
@@ -459,6 +493,11 @@ configureDNS() {
|
|||||||
# Add DNS entry for container
|
# Add DNS entry for container
|
||||||
arguments+=" --address=/host.lan/$gateway"
|
arguments+=" --address=/host.lan/$gateway"
|
||||||
|
|
||||||
|
# Add DNS entry for the upstream gateway.
|
||||||
|
if isNAT && [ -n "$upstream" ]; then
|
||||||
|
arguments+=" --address=/system.lan/$upstream"
|
||||||
|
fi
|
||||||
|
|
||||||
# Avoid returning IPv6 records when the active network mode is IPv4-only.
|
# Avoid returning IPv6 records when the active network mode is IPv4-only.
|
||||||
if isNAT || [ -z "$IP6" ]; then
|
if isNAT || [ -z "$IP6" ]; then
|
||||||
arguments+=" --filter-AAAA"
|
arguments+=" --filter-AAAA"
|
||||||
@@ -476,9 +515,9 @@ configureDNS() {
|
|||||||
arguments+=" --log-facility=$log"
|
arguments+=" --log-facility=$log"
|
||||||
|
|
||||||
arguments=$(echo "$arguments" | sed 's/\t/ /g' | tr -s ' ' | sed 's/^ *//')
|
arguments=$(echo "$arguments" | sed 's/\t/ /g' | tr -s ' ' | sed 's/^ *//')
|
||||||
enabled "$DEBUG" && printf "Dnsmasq arguments:\n\n%s\n\n" "${arguments// -/$'\n-'}"
|
enabled "$DEBUG" && printf "Dnsmasq arguments:\n\n %s\n\n" "${arguments// -/$'\n -'}"
|
||||||
|
|
||||||
{ $DNSMASQ ${arguments:+ $arguments}; rc=$?; } || :
|
{ $DNSMASQ ${arguments:+ $arguments}; local rc=$?; } || :
|
||||||
|
|
||||||
if (( rc != 0 )); then
|
if (( rc != 0 )); then
|
||||||
|
|
||||||
@@ -499,11 +538,12 @@ configureDNS() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
getHostPorts() {
|
normalizePorts() {
|
||||||
|
|
||||||
local port="" ports=""
|
local list="$1"
|
||||||
local num="" mode="${1:-tcp}"
|
local mode="${2:-tcp}"
|
||||||
local list="${HOST_PORTS// /},"
|
local port num
|
||||||
|
local ports=""
|
||||||
|
|
||||||
for port in ${list//,/ }; do
|
for port in ${list//,/ }; do
|
||||||
|
|
||||||
@@ -513,6 +553,7 @@ getHostPorts() {
|
|||||||
"tcp" )
|
"tcp" )
|
||||||
[[ "$port" == *"/udp" ]] && continue
|
[[ "$port" == *"/udp" ]] && continue
|
||||||
num="${port%/tcp}"
|
num="${port%/tcp}"
|
||||||
|
[ -n "$num" ] && ports+="$num,"
|
||||||
;;
|
;;
|
||||||
"all" )
|
"all" )
|
||||||
if [[ "$port" == *"/udp" ]]; then
|
if [[ "$port" == *"/udp" ]]; then
|
||||||
@@ -522,39 +563,82 @@ getHostPorts() {
|
|||||||
num="${port%/tcp}"
|
num="${port%/tcp}"
|
||||||
[ -n "$num" ] && ports+="$num/tcp,"
|
[ -n "$num" ] && ports+="$num/tcp,"
|
||||||
fi
|
fi
|
||||||
continue
|
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
return 1
|
return 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
[ -n "$num" ] && ports+="$num,"
|
|
||||||
|
|
||||||
done
|
done
|
||||||
|
|
||||||
# Remove duplicates
|
# Remove duplicates
|
||||||
ports=$(echo "${ports//,,/,}," | awk 'BEGIN{RS=ORS=","} !seen[$0]++' | sed 's/,*$//g')
|
echo "${ports//,,/,}," | awk 'BEGIN{RS=ORS=","} !seen[$0]++' | sed 's/,*$//g'
|
||||||
|
|
||||||
echo "$ports"
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getReservedPorts() {
|
||||||
|
|
||||||
|
local list=""
|
||||||
|
local mode="${1:-tcp}"
|
||||||
|
|
||||||
|
# Reserve the DNS port while the internal dnsmasq resolver is active.
|
||||||
|
if ! enabled "${DNSMASQ_DISABLE:-}" && ! isNAT; then
|
||||||
|
list+="53/tcp,53/udp,"
|
||||||
|
fi
|
||||||
|
|
||||||
|
normalizePorts "$list" "$mode"
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
|
||||||
|
getCustomHostPorts() {
|
||||||
|
|
||||||
|
local mode="${1:-tcp}"
|
||||||
|
local reserved user port
|
||||||
|
local ports=""
|
||||||
|
|
||||||
|
reserved=$(getReservedPorts "all")
|
||||||
|
user=$(normalizePorts "$HOST_PORTS" "all")
|
||||||
|
|
||||||
|
for port in ${user//,/ }; do
|
||||||
|
[[ ",$reserved," == *",$port,"* ]] && continue
|
||||||
|
ports+="$port,"
|
||||||
|
done
|
||||||
|
|
||||||
|
normalizePorts "$ports" "$mode"
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
|
||||||
|
getHostPorts() {
|
||||||
|
|
||||||
|
local mode="${1:-tcp}"
|
||||||
|
local reserved custom
|
||||||
|
|
||||||
|
# Merge internal reservations with user-defined host ports without mutating HOST_PORTS.
|
||||||
|
# User entries already covered by an internal reservation are silently ignored.
|
||||||
|
reserved=$(getReservedPorts "all")
|
||||||
|
custom=$(getCustomHostPorts "all")
|
||||||
|
normalizePorts "$reserved,$custom" "$mode"
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
|
||||||
getUserPorts() {
|
getUserPorts() {
|
||||||
|
|
||||||
|
# User-mode networking forwards DSM management and SSH ports by default;
|
||||||
|
# internal container reservations and HOST_PORTS are removed below.
|
||||||
local defaults="22/tcp,5000/tcp,5001/tcp"
|
local defaults="22/tcp,5000/tcp,5001/tcp"
|
||||||
local list="$defaults,${USER_PORTS// /},"
|
local list="$defaults,${USER_PORTS// /},"
|
||||||
|
|
||||||
local num="" ports="" proto=""
|
local ports=""
|
||||||
local userport="" hostport=""
|
local userport hostport exclude reserved
|
||||||
|
|
||||||
local exclude=""
|
reserved=$(getReservedPorts "all")
|
||||||
exclude=$(getHostPorts "all")
|
exclude=$(getHostPorts "all")
|
||||||
|
|
||||||
for userport in ${list//,/ }; do
|
for userport in ${list//,/ }; do
|
||||||
|
|
||||||
proto="tcp"
|
local proto="tcp"
|
||||||
num="$userport"
|
local num="$userport"
|
||||||
|
|
||||||
if [[ "$userport" == *"/udp" ]]; then
|
if [[ "$userport" == *"/udp" ]]; then
|
||||||
proto="udp"
|
proto="udp"
|
||||||
@@ -570,7 +654,9 @@ getUserPorts() {
|
|||||||
|
|
||||||
if [[ "$num/$proto" == "$hostport" ]]; then
|
if [[ "$num/$proto" == "$hostport" ]]; then
|
||||||
|
|
||||||
if [[ "$hostport" != "${WEB_PORT:-}/tcp" ]]; then
|
if [[ ",$reserved," == *",$hostport,"* ]]; then
|
||||||
|
warn "Could not assign port $hostport to \"USER_PORTS\" because it is reserved by the container!"
|
||||||
|
elif [[ "$hostport" != "${WEB_PORT:-}/tcp" ]]; then
|
||||||
warn "Could not assign port $hostport to \"USER_PORTS\" because it is already in \"HOST_PORTS\"!"
|
warn "Could not assign port $hostport to \"USER_PORTS\" because it is already in \"HOST_PORTS\"!"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -580,8 +666,14 @@ getUserPorts() {
|
|||||||
|
|
||||||
done
|
done
|
||||||
|
|
||||||
[ -n "$num" ] && ports+="$num/$proto,"
|
[ -z "$num" ] && continue
|
||||||
|
|
||||||
|
if ! canBindPrivilegedPort "$num" "$proto"; then
|
||||||
|
warn "Could not assign port $num/$proto to \"USER_PORTS\" because it cannot be bound by the current user!"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
ports+="$num/$proto,"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Remove duplicates
|
# Remove duplicates
|
||||||
@@ -593,7 +685,7 @@ getUserPorts() {
|
|||||||
getSlirp() {
|
getSlirp() {
|
||||||
|
|
||||||
local ip="$1"
|
local ip="$1"
|
||||||
local args="" list=""
|
local args="" list
|
||||||
|
|
||||||
list=$(getUserPorts)
|
list=$(getUserPorts)
|
||||||
|
|
||||||
@@ -617,9 +709,8 @@ getSlirp() {
|
|||||||
|
|
||||||
getPasst() {
|
getPasst() {
|
||||||
|
|
||||||
local args="" list="" port=""
|
local list port
|
||||||
local num="" tcp="" udp=""
|
local tcp="" udp="" args=""
|
||||||
local bind="$UPLINK"
|
|
||||||
|
|
||||||
list=$(getUserPorts)
|
list=$(getUserPorts)
|
||||||
|
|
||||||
@@ -629,12 +720,12 @@ getPasst() {
|
|||||||
|
|
||||||
if [[ "$port" == *"/udp" ]]; then
|
if [[ "$port" == *"/udp" ]]; then
|
||||||
|
|
||||||
num="${port%/udp}"
|
local num="${port%/udp}"
|
||||||
[ -n "$num" ] && udp+="$num,"
|
[ -n "$num" ] && udp+="$num,"
|
||||||
|
|
||||||
elif [[ "$port" == *"/tcp" ]]; then
|
elif [[ "$port" == *"/tcp" ]]; then
|
||||||
|
|
||||||
num="${port%/tcp}"
|
local num="${port%/tcp}"
|
||||||
[ -n "$num" ] && tcp+="$num,"
|
[ -n "$num" ] && tcp+="$num,"
|
||||||
|
|
||||||
else
|
else
|
||||||
@@ -648,12 +739,8 @@ getPasst() {
|
|||||||
tcp="${tcp%,}"
|
tcp="${tcp%,}"
|
||||||
udp="${udp%,}"
|
udp="${udp%,}"
|
||||||
|
|
||||||
if canBindToDevice "$DEV"; then
|
[ -n "$tcp" ] && args+=" -t $tcp"
|
||||||
bind="%$DEV"
|
[ -n "$udp" ] && args+=" -u $udp"
|
||||||
fi
|
|
||||||
|
|
||||||
[ -n "$tcp" ] && args+=" -t $bind/$tcp"
|
|
||||||
[ -n "$udp" ] && args+=" -u $bind/$udp"
|
|
||||||
|
|
||||||
echo "$args"
|
echo "$args"
|
||||||
return 0
|
return 0
|
||||||
@@ -665,8 +752,7 @@ getPasst() {
|
|||||||
|
|
||||||
configureVTAP() {
|
configureVTAP() {
|
||||||
|
|
||||||
local msg=""
|
local msg dev
|
||||||
local rc
|
|
||||||
|
|
||||||
enabled "$DEBUG" && echo "Configuring MACVTAP networking..."
|
enabled "$DEBUG" && echo "Configuring MACVTAP networking..."
|
||||||
|
|
||||||
@@ -678,7 +764,7 @@ configureVTAP() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Create a macvtap network for the VM guest
|
# Create a macvtap network for the VM guest
|
||||||
{ msg=$(ip link add link "$DEV" name "$TAP" address "$MAC" type macvtap mode bridge 2>&1); rc=$?; } || :
|
{ msg=$(ip link add link "$DEV" name "$TAP" address "$MAC" type macvtap mode bridge 2>&1); local rc=$?; } || :
|
||||||
|
|
||||||
case "$msg" in
|
case "$msg" in
|
||||||
"RTNETLINK answers: File exists"* )
|
"RTNETLINK answers: File exists"* )
|
||||||
@@ -711,15 +797,35 @@ configureVTAP() {
|
|||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
|
|
||||||
local TAP_NR TAP_PATH MAJOR MINOR
|
local TAP_NR MAJOR MINOR
|
||||||
TAP_NR=$(</sys/class/net/"$TAP"/ifindex)
|
|
||||||
TAP_PATH="/dev/tap${TAP_NR}"
|
if ! dev=$(cat /sys/devices/virtual/net/"$TAP"/tap*/dev); then
|
||||||
|
error "Failed to determine device numbers for MACVTAP interface \"$TAP\" !"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS=: read -r MAJOR MINOR <<< "$dev"
|
||||||
|
|
||||||
|
if [[ ! "$MAJOR" =~ ^[0-9]+$ || ! "$MINOR" =~ ^[0-9]+$ ]]; then
|
||||||
|
error "Failed to parse device numbers for MACVTAP interface \"$TAP\" !"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( MAJOR < 1 )); then
|
||||||
|
error "Cannot find: sys/devices/virtual/net/$TAP"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! TAP_NR=$(<"/sys/class/net/$TAP/ifindex"); then
|
||||||
|
error "Failed to determine interface index of MACVTAP interface \"$TAP\" !"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Create dev file (there is no udev in container: need to be done manually)
|
# Create dev file (there is no udev in container: need to be done manually)
|
||||||
IFS=: read -r MAJOR MINOR < <(cat /sys/devices/virtual/net/"$TAP"/tap*/dev)
|
local TAP_PATH="/dev/tap${TAP_NR}"
|
||||||
(( MAJOR < 1)) && error "Cannot find: sys/devices/virtual/net/$TAP" && return 1
|
|
||||||
|
|
||||||
[[ ! -e "$TAP_PATH" && -e "/dev0/${TAP_PATH##*/}" ]] && ln -s "/dev0/${TAP_PATH##*/}" "$TAP_PATH"
|
[[ ! -e "$TAP_PATH" && -e "/dev0/${TAP_PATH##*/}" ]] &&
|
||||||
|
ln -s "/dev0/${TAP_PATH##*/}" "$TAP_PATH"
|
||||||
|
|
||||||
if [[ ! -e "$TAP_PATH" ]]; then
|
if [[ ! -e "$TAP_PATH" ]]; then
|
||||||
{ mknod "$TAP_PATH" c "$MAJOR" "$MINOR"; rc=$?; } || :
|
{ mknod "$TAP_PATH" c "$MAJOR" "$MINOR"; rc=$?; } || :
|
||||||
@@ -753,7 +859,7 @@ configureSlirp() {
|
|||||||
|
|
||||||
ip=$(guestIP "$ip" 4)
|
ip=$(guestIP "$ip" 4)
|
||||||
local gateway="${ip%.*}.1"
|
local gateway="${ip%.*}.1"
|
||||||
local subnet=""
|
local subnet
|
||||||
subnet=$(networkCIDR "$ip") || return 1
|
subnet=$(networkCIDR "$ip") || return 1
|
||||||
|
|
||||||
local ipv6="ipv6=off,"
|
local ipv6="ipv6=off,"
|
||||||
@@ -761,7 +867,7 @@ configureSlirp() {
|
|||||||
|
|
||||||
NET_OPTS="-netdev user,id=hostnet0,ipv4=on,host=$gateway,net=$subnet,dhcpstart=$ip,${ipv6}hostname=$HOST"
|
NET_OPTS="-netdev user,id=hostnet0,ipv4=on,host=$gateway,net=$subnet,dhcpstart=$ip,${ipv6}hostname=$HOST"
|
||||||
|
|
||||||
local forward=""
|
local forward
|
||||||
forward=$(getSlirp "$ip")
|
forward=$(getSlirp "$ip")
|
||||||
[ -n "$forward" ] && NET_OPTS+=",$forward"
|
[ -n "$forward" ] && NET_OPTS+=",$forward"
|
||||||
|
|
||||||
@@ -818,12 +924,13 @@ configurePasst() {
|
|||||||
# Pass an explicit MTU to passt.
|
# Pass an explicit MTU to passt.
|
||||||
PASST_OPTS+=" -m $passt_mtu"
|
PASST_OPTS+=" -m $passt_mtu"
|
||||||
|
|
||||||
local forward=""
|
local forward
|
||||||
forward=$(getPasst)
|
forward=$(getPasst)
|
||||||
[ -n "$forward" ] && PASST_OPTS+="$forward"
|
[ -n "$forward" ] && PASST_OPTS+="$forward"
|
||||||
|
|
||||||
PASST_OPTS+=" -H $HOST"
|
PASST_OPTS+=" -H $HOST"
|
||||||
PASST_OPTS+=" -M $GATEWAY_MAC"
|
PASST_OPTS+=" -M $GATEWAY_MAC"
|
||||||
|
PASST_OPTS+=" --runas $EUID:$(id -g)"
|
||||||
PASST_OPTS+=" -P $PASST_PID"
|
PASST_OPTS+=" -P $PASST_PID"
|
||||||
PASST_OPTS+=" -s $PASST_SOCKET"
|
PASST_OPTS+=" -s $PASST_SOCKET"
|
||||||
PASST_OPTS+=" -l $log"
|
PASST_OPTS+=" -l $log"
|
||||||
@@ -854,11 +961,10 @@ configurePasst() {
|
|||||||
|
|
||||||
if ! "$PASST" ${PASST_OPTS:+$PASST_OPTS} >/dev/null 2>&1; then
|
if ! "$PASST" ${PASST_OPTS:+$PASST_OPTS} >/dev/null 2>&1; then
|
||||||
|
|
||||||
local rc=0
|
|
||||||
rm -f "$log"
|
rm -f "$log"
|
||||||
|
|
||||||
PASST_OPTS="${PASST_OPTS/ -q/}"
|
PASST_OPTS="${PASST_OPTS/ -q/}"
|
||||||
{ "$PASST" ${PASST_OPTS:+$PASST_OPTS}; rc=$?; } || :
|
{ "$PASST" ${PASST_OPTS:+$PASST_OPTS}; local rc=$?; } || :
|
||||||
|
|
||||||
if (( rc != 0 )); then
|
if (( rc != 0 )); then
|
||||||
[ -f "$log" ] && [ -s "$log" ] && cat "$log"
|
[ -f "$log" ] && [ -s "$log" ] && cat "$log"
|
||||||
@@ -886,13 +992,24 @@ configurePasst() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configureBridge() {
|
||||||
|
|
||||||
|
local file="/etc/qemu/bridge.conf"
|
||||||
|
|
||||||
|
[ -e "$file" ] && return 0
|
||||||
|
mkdir -p "${file%/*}" || return 0
|
||||||
|
echo "allow br0" > "$file" || return 0
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
createBridge() {
|
createBridge() {
|
||||||
|
|
||||||
local gateway="$1"
|
local gateway="$1"
|
||||||
local rc msg=""
|
local msg
|
||||||
|
|
||||||
# Create a bridge with a static IP for the VM guest
|
# Create a bridge with a static IP for the VM guest
|
||||||
{ msg=$(ip link add dev "$BRIDGE" type bridge 2>&1); rc=$?; } || :
|
{ msg=$(ip link add dev "$BRIDGE" type bridge 2>&1); local rc=$?; } || :
|
||||||
|
|
||||||
if (( rc != 0 )); then
|
if (( rc != 0 )); then
|
||||||
enabled "$ROOTLESS" && ! enabled "$DEBUG" && return 1
|
enabled "$ROOTLESS" && ! enabled "$DEBUG" && return 1
|
||||||
@@ -930,10 +1047,10 @@ createBridge() {
|
|||||||
createTap() {
|
createTap() {
|
||||||
|
|
||||||
local tuntap="$1"
|
local tuntap="$1"
|
||||||
local rc msg=""
|
local msg
|
||||||
|
|
||||||
# Set tap to the bridge created
|
# Set tap to the bridge created
|
||||||
{ msg=$(ip tuntap add dev "$TAP" mode tap 2>&1); rc=$?; } || :
|
{ msg=$(ip tuntap add dev "$TAP" mode tap 2>&1); local rc=$?; } || :
|
||||||
|
|
||||||
if (( rc != 0 )); then
|
if (( rc != 0 )); then
|
||||||
enabled "$ROOTLESS" && ! enabled "$DEBUG" && return 1
|
enabled "$ROOTLESS" && ! enabled "$DEBUG" && return 1
|
||||||
@@ -976,7 +1093,7 @@ hasTable() {
|
|||||||
|
|
||||||
getTablesBackend() {
|
getTablesBackend() {
|
||||||
|
|
||||||
local version=""
|
local version
|
||||||
version=$(iptables --version 2>/dev/null || true)
|
version=$(iptables --version 2>/dev/null || true)
|
||||||
|
|
||||||
case "$version" in
|
case "$version" in
|
||||||
@@ -989,7 +1106,7 @@ getTablesBackend() {
|
|||||||
setTables() {
|
setTables() {
|
||||||
|
|
||||||
local mode="$1"
|
local mode="$1"
|
||||||
local path=""
|
local path
|
||||||
|
|
||||||
path=$(command -v "iptables-$mode" 2>/dev/null || true)
|
path=$(command -v "iptables-$mode" 2>/dev/null || true)
|
||||||
[ -z "$path" ] && return 1
|
[ -z "$path" ] && return 1
|
||||||
@@ -1003,7 +1120,7 @@ showRules() {
|
|||||||
local chain="$2"
|
local chain="$2"
|
||||||
local label="$3"
|
local label="$3"
|
||||||
local rule_tag="$4"
|
local rule_tag="$4"
|
||||||
local rules=""
|
local rules
|
||||||
local own_rule="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)"
|
local own_rule="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)"
|
||||||
|
|
||||||
enabled "$DEBUG" || return 0
|
enabled "$DEBUG" || return 0
|
||||||
@@ -1022,7 +1139,7 @@ showRules() {
|
|||||||
|
|
||||||
checkExistingTables() {
|
checkExistingTables() {
|
||||||
|
|
||||||
local msg="" rules="" conflicts=""
|
local rules conflicts
|
||||||
local rule_tag="QEMU_DNAT"
|
local rule_tag="QEMU_DNAT"
|
||||||
local own_rule="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)"
|
local own_rule="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)"
|
||||||
|
|
||||||
@@ -1040,7 +1157,7 @@ checkExistingTables() {
|
|||||||
<<< "$rules" || true)
|
<<< "$rules" || true)
|
||||||
|
|
||||||
if [ -n "$conflicts" ]; then
|
if [ -n "$conflicts" ]; then
|
||||||
msg="your existing NAT rules may take precedence over VM port forwarding"
|
local msg="your existing NAT rules may take precedence over VM port forwarding"
|
||||||
|
|
||||||
if enabled "$DEBUG"; then
|
if enabled "$DEBUG"; then
|
||||||
warn "${msg}."
|
warn "${msg}."
|
||||||
@@ -1060,7 +1177,7 @@ checkExistingTables() {
|
|||||||
<<< "$rules" || true)
|
<<< "$rules" || true)
|
||||||
|
|
||||||
if [ -n "$conflicts" ]; then
|
if [ -n "$conflicts" ]; then
|
||||||
msg="your existing firewall rules may block traffic forwarded to or from the VM"
|
local msg="your existing firewall rules may block traffic forwarded to or from the VM"
|
||||||
|
|
||||||
if enabled "$DEBUG"; then
|
if enabled "$DEBUG"; then
|
||||||
warn "${msg}."
|
warn "${msg}."
|
||||||
@@ -1088,13 +1205,13 @@ runTableRule() {
|
|||||||
|
|
||||||
local silent="$1"
|
local silent="$1"
|
||||||
local result="$2"
|
local result="$2"
|
||||||
local rc msg=""
|
local msg
|
||||||
|
|
||||||
shift 2
|
shift 2
|
||||||
|
|
||||||
printf -v "$result" '%s' ""
|
printf -v "$result" '%s' ""
|
||||||
|
|
||||||
{ msg=$("$@" 2>&1); rc=$?; } || :
|
{ msg=$("$@" 2>&1); local rc=$?; } || :
|
||||||
(( rc == 0 )) && return 0
|
(( rc == 0 )) && return 0
|
||||||
|
|
||||||
printf -v "$result" '%s' "$msg"
|
printf -v "$result" '%s' "$msg"
|
||||||
@@ -1154,8 +1271,8 @@ applyTables() {
|
|||||||
local ip="$1"
|
local ip="$1"
|
||||||
local subnet="$2"
|
local subnet="$2"
|
||||||
local silent="${3:-N}"
|
local silent="${3:-N}"
|
||||||
local exclude="" port=""
|
local exclude port
|
||||||
local table_error=""
|
local table_error
|
||||||
local dnat_chain="QEMU_DNAT"
|
local dnat_chain="QEMU_DNAT"
|
||||||
local rule_tag="$dnat_chain"
|
local rule_tag="$dnat_chain"
|
||||||
|
|
||||||
@@ -1283,8 +1400,8 @@ applyTables() {
|
|||||||
|
|
||||||
clearTables() {
|
clearTables() {
|
||||||
|
|
||||||
local table="" line="" chain=""
|
local line
|
||||||
local rules="" remaining="" message=""
|
local rules remaining message
|
||||||
local dnat_chain="QEMU_DNAT"
|
local dnat_chain="QEMU_DNAT"
|
||||||
local rule_tag="$dnat_chain"
|
local rule_tag="$dnat_chain"
|
||||||
local own_rule="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)"
|
local own_rule="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)"
|
||||||
@@ -1309,15 +1426,15 @@ clearTables() {
|
|||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
|
|
||||||
case "$line" in
|
case "$line" in
|
||||||
\*nat ) table="nat" ;;
|
\*nat ) local table="nat" ;;
|
||||||
\*filter ) table="filter" ;;
|
\*filter ) local table="filter" ;;
|
||||||
\*mangle ) table="mangle" ;;
|
\*mangle ) local table="mangle" ;;
|
||||||
\*raw ) table="raw" ;;
|
\*raw ) local table="raw" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [[ "$line" == -A* ]] && [[ "$line" =~ $own_rule ]]; then
|
if [[ "$line" == -A* ]] && [[ "$line" =~ $own_rule ]]; then
|
||||||
|
|
||||||
chain="${line#-A }"
|
local chain="${line#-A }"
|
||||||
chain="${chain%% *}"
|
chain="${chain%% *}"
|
||||||
|
|
||||||
# Rules inside this chain are removed together by the flush below.
|
# Rules inside this chain are removed together by the flush below.
|
||||||
@@ -1383,7 +1500,7 @@ clearTables() {
|
|||||||
hasTaggedRules() {
|
hasTaggedRules() {
|
||||||
|
|
||||||
local save="$1"
|
local save="$1"
|
||||||
local rules=""
|
local rules
|
||||||
local dnat_chain="QEMU_DNAT"
|
local dnat_chain="QEMU_DNAT"
|
||||||
local rule_tag="$dnat_chain"
|
local rule_tag="$dnat_chain"
|
||||||
local own_rule="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)"
|
local own_rule="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)"
|
||||||
@@ -1405,9 +1522,8 @@ configureTables() {
|
|||||||
|
|
||||||
local ip="$1"
|
local ip="$1"
|
||||||
local subnet="$2"
|
local subnet="$2"
|
||||||
local preferred=""
|
local preferred
|
||||||
local alternate=""
|
local alternate_save
|
||||||
local alternate_save=""
|
|
||||||
local preferred_clean="N"
|
local preferred_clean="N"
|
||||||
local alternate_dirty="N"
|
local alternate_dirty="N"
|
||||||
|
|
||||||
@@ -1418,8 +1534,8 @@ configureTables() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "$preferred" in
|
case "$preferred" in
|
||||||
"nft" ) alternate="legacy" ;;
|
"nft" ) local alternate="legacy" ;;
|
||||||
"legacy" ) alternate="nft" ;;
|
"legacy" ) local alternate="nft" ;;
|
||||||
* )
|
* )
|
||||||
enabled "$ROOTLESS" && ! enabled "$DEBUG" && return 1
|
enabled "$ROOTLESS" && ! enabled "$DEBUG" && return 1
|
||||||
warn "unsupported IP tables backend: $preferred"
|
warn "unsupported IP tables backend: $preferred"
|
||||||
@@ -1605,10 +1721,46 @@ configureTables() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addUpstream() {
|
||||||
|
|
||||||
|
local upstream="$1"
|
||||||
|
local table_error
|
||||||
|
local rule_tag="QEMU_DNAT"
|
||||||
|
|
||||||
|
[ -n "$upstream" ] || return 1
|
||||||
|
[ -n "$GATEWAY" ] || return 1
|
||||||
|
|
||||||
|
if ! ip address add "$upstream/32" dev "$BRIDGE"; then
|
||||||
|
if ! enabled "$ROOTLESS" || enabled "$DEBUG"; then
|
||||||
|
warn "failed to add the system.lan address; access through that name will be unavailable."
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! runTableRule "Y" table_error \
|
||||||
|
iptables -t nat -A PREROUTING \
|
||||||
|
-i "$BRIDGE" \
|
||||||
|
-d "$upstream" \
|
||||||
|
-m comment --comment "$rule_tag" \
|
||||||
|
-j DNAT --to-destination "$GATEWAY"; then
|
||||||
|
|
||||||
|
ip address del "$upstream/32" dev "$BRIDGE" > /dev/null 2>&1 || :
|
||||||
|
|
||||||
|
if ! enabled "$ROOTLESS" || enabled "$DEBUG"; then
|
||||||
|
[ -n "$table_error" ] && echo "$table_error" >&2
|
||||||
|
warn "failed to configure system.lan forwarding; access through that name will be unavailable."
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
configureNAT() {
|
configureNAT() {
|
||||||
|
|
||||||
local tuntap="TUN device is missing. $ADD_ERR --device /dev/net/tun"
|
local tuntap="TUN device is missing. $ADD_ERR --device /dev/net/tun"
|
||||||
local rc ip subnet msg="" forwarding=""
|
local ip subnet upstream="" forwarding=""
|
||||||
|
|
||||||
enabled "$DEBUG" && echo "Configuring NAT networking..."
|
enabled "$DEBUG" && echo "Configuring NAT networking..."
|
||||||
|
|
||||||
@@ -1616,7 +1768,8 @@ configureNAT() {
|
|||||||
if [ ! -c /dev/net/tun ]; then
|
if [ ! -c /dev/net/tun ]; then
|
||||||
[ ! -d /dev/net ] && mkdir -m 755 /dev/net > /dev/null 2>&1 || :
|
[ ! -d /dev/net ] && mkdir -m 755 /dev/net > /dev/null 2>&1 || :
|
||||||
|
|
||||||
{ msg=$(mknod /dev/net/tun c 10 200 2>&1); rc=$?; } || :
|
local msg
|
||||||
|
{ msg=$(mknod /dev/net/tun c 10 200 2>&1); local rc=$?; } || :
|
||||||
|
|
||||||
if (( rc == 0 )); then
|
if (( rc == 0 )); then
|
||||||
chmod 666 /dev/net/tun
|
chmod 666 /dev/net/tun
|
||||||
@@ -1635,7 +1788,7 @@ configureNAT() {
|
|||||||
forwarding=$(< /proc/sys/net/ipv4/ip_forward)
|
forwarding=$(< /proc/sys/net/ipv4/ip_forward)
|
||||||
|
|
||||||
if [[ "$forwarding" != "1" ]]; then
|
if [[ "$forwarding" != "1" ]]; then
|
||||||
{ sysctl -w net.ipv4.ip_forward=1 > /dev/null 2>&1; rc=$?; } || :
|
{ sysctl -w net.ipv4.ip_forward=1 > /dev/null 2>&1; local rc=$?; } || :
|
||||||
|
|
||||||
forwarding=""
|
forwarding=""
|
||||||
[ -r /proc/sys/net/ipv4/ip_forward ] &&
|
[ -r /proc/sys/net/ipv4/ip_forward ] &&
|
||||||
@@ -1657,11 +1810,15 @@ configureNAT() {
|
|||||||
local gateway="${ip%.*}.1"
|
local gateway="${ip%.*}.1"
|
||||||
subnet=$(networkCIDR "$ip") || return 1
|
subnet=$(networkCIDR "$ip") || return 1
|
||||||
|
|
||||||
|
if [ -n "$GATEWAY" ]; then
|
||||||
|
upstream=$(upstreamIP "$subnet" "$ip" "$gateway") || upstream=""
|
||||||
|
fi
|
||||||
|
|
||||||
if subnetInUse "$subnet"; then
|
if subnetInUse "$subnet"; then
|
||||||
error "VM subnet $subnet conflicts with an existing route inside the container."
|
error "VM subnet $subnet conflicts with an existing route inside the container."
|
||||||
return 1
|
return 1
|
||||||
else
|
else
|
||||||
rc=$?
|
local rc=$?
|
||||||
(( rc == 1 )) || return 1
|
(( rc == 1 )) || return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -1675,16 +1832,20 @@ configureNAT() {
|
|||||||
|
|
||||||
configureTables "$ip" "$subnet" || return 1
|
configureTables "$ip" "$subnet" || return 1
|
||||||
|
|
||||||
|
if [ -n "$upstream" ] && ! addUpstream "$upstream"; then
|
||||||
|
upstream=""
|
||||||
|
fi
|
||||||
|
|
||||||
NET_OPTS="-netdev tap,id=hostnet0,ifname=$TAP"
|
NET_OPTS="-netdev tap,id=hostnet0,ifname=$TAP"
|
||||||
|
|
||||||
if [ -c /dev/vhost-net ]; then
|
if [ -c /dev/vhost-net ]; then
|
||||||
{ exec 40>>/dev/vhost-net; rc=$?; } 2>/dev/null || :
|
{ exec 40>>/dev/vhost-net; local rc=$?; } 2>/dev/null || :
|
||||||
(( rc == 0 )) && NET_OPTS+=",vhost=on,vhostfd=40"
|
(( rc == 0 )) && NET_OPTS+=",vhost=on,vhostfd=40"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
NET_OPTS+=",script=no,downscript=no"
|
NET_OPTS+=",script=no,downscript=no"
|
||||||
|
|
||||||
configureDNS "$BRIDGE" "$ip" "$MAC" "$HOST" "$MASK" "$gateway" || return 1
|
configureDNS "$BRIDGE" "$ip" "$MAC" "$HOST" "$MASK" "$gateway" "$upstream" || return 1
|
||||||
|
|
||||||
IP="$ip"
|
IP="$ip"
|
||||||
return 0
|
return 0
|
||||||
@@ -1740,7 +1901,7 @@ closeNetwork() {
|
|||||||
checkOS() {
|
checkOS() {
|
||||||
|
|
||||||
local iface="macvlan"
|
local iface="macvlan"
|
||||||
local os="" kernel=""
|
local os="" kernel
|
||||||
|
|
||||||
kernel=$(uname -a)
|
kernel=$(uname -a)
|
||||||
|
|
||||||
@@ -1798,7 +1959,10 @@ validateHost() {
|
|||||||
|
|
||||||
validateHostPorts() {
|
validateHostPorts() {
|
||||||
|
|
||||||
if isNAT && [[ "${HOST_PORTS,,}" == *"/udp"* ]]; then
|
local custom
|
||||||
|
custom=$(getCustomHostPorts "all")
|
||||||
|
|
||||||
|
if isNAT && [[ "$custom" == *"/udp"* ]]; then
|
||||||
warn "UDP ports in \"HOST_PORTS\" are not yet implemented for NAT networking."
|
warn "UDP ports in \"HOST_PORTS\" are not yet implemented for NAT networking."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -1891,14 +2055,12 @@ configureMTU() {
|
|||||||
|
|
||||||
configureMAC() {
|
configureMAC() {
|
||||||
|
|
||||||
local container=""
|
local container
|
||||||
local file=""
|
|
||||||
|
|
||||||
container=$(containerID)
|
container=$(containerID)
|
||||||
|
|
||||||
if [ -z "$MAC" ]; then
|
if [ -z "$MAC" ]; then
|
||||||
|
|
||||||
file="$STORAGE/dsm.mac"
|
local file="$STORAGE/dsm.mac"
|
||||||
|
|
||||||
if [ -s "$file" ]; then
|
if [ -s "$file" ]; then
|
||||||
if ! MAC=$(readFile "$file"); then
|
if ! MAC=$(readFile "$file"); then
|
||||||
@@ -1941,7 +2103,7 @@ configureMAC() {
|
|||||||
|
|
||||||
showHostInfo() {
|
showHostInfo() {
|
||||||
|
|
||||||
local mtu="" host="" uplink="" prefix=""
|
local mtu host uplink prefix
|
||||||
|
|
||||||
prefix=$(ip -4 -o address show dev "$DEV" scope global 2>/dev/null |
|
prefix=$(ip -4 -o address show dev "$DEV" scope global 2>/dev/null |
|
||||||
awk -v ip="$UPLINK" '
|
awk -v ip="$UPLINK" '
|
||||||
@@ -2010,6 +2172,7 @@ showHostInfo() {
|
|||||||
echo "❯ DNS: $nameservers"
|
echo "❯ DNS: $nameservers"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
enabled "$DEBUG" && echo
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2028,10 +2191,10 @@ showGuestInfo() {
|
|||||||
|
|
||||||
local mode="${NETWORK,,}"
|
local mode="${NETWORK,,}"
|
||||||
|
|
||||||
if isNAT; then
|
if enabled "$DHCP"; then
|
||||||
mode="NAT"
|
|
||||||
elif enabled "$DHCP"; then
|
|
||||||
mode="DHCP"
|
mode="DHCP"
|
||||||
|
elif isNAT; then
|
||||||
|
mode="NAT"
|
||||||
elif isUserMode; then
|
elif isUserMode; then
|
||||||
mode="User ($mode)"
|
mode="User ($mode)"
|
||||||
elif [ -z "$mode" ]; then
|
elif [ -z "$mode" ]; then
|
||||||
@@ -2064,6 +2227,7 @@ initializeNetwork() {
|
|||||||
|
|
||||||
configureMTU
|
configureMTU
|
||||||
configureMAC
|
configureMAC
|
||||||
|
configureBridge
|
||||||
|
|
||||||
showHostInfo
|
showHostInfo
|
||||||
|
|
||||||
@@ -2117,6 +2281,8 @@ else
|
|||||||
if ! configureNAT; then
|
if ! configureNAT; then
|
||||||
|
|
||||||
closeInterfaces
|
closeInterfaces
|
||||||
|
# NAT setup failure is recoverable: tear down partial interfaces and
|
||||||
|
# continue with the default user-mode backend.
|
||||||
NETWORK="user"
|
NETWORK="user"
|
||||||
|
|
||||||
if ! enabled "$ROOTLESS" || enabled "$DEBUG"; then
|
if ! enabled "$ROOTLESS" || enabled "$DEBUG"; then
|
||||||
@@ -2164,6 +2330,8 @@ else
|
|||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Suppress the adapter option ROM because firmware network boot is unused and
|
||||||
|
# would otherwise alter boot order and startup timing.
|
||||||
NET_OPTS+=" -device $ADAPTER,id=net0,netdev=hostnet0,romfile=,mac=$MAC"
|
NET_OPTS+=" -device $ADAPTER,id=net0,netdev=hostnet0,romfile=,mac=$MAC"
|
||||||
|
|
||||||
if [[ "$GUEST_MTU" != "0" && "$GUEST_MTU" != "1500" ]]; then
|
if [[ "$GUEST_MTU" != "0" && "$GUEST_MTU" != "1500" ]]; then
|
||||||
@@ -2174,6 +2342,8 @@ if [[ "$GUEST_MTU" != "0" && "$GUEST_MTU" != "1500" ]]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Publish the container address and detected driver for the healthcheck and
|
||||||
|
# post-boot login-message helper.
|
||||||
if ! echo "$UPLINK" > "$QEMU_DIR"/qemu.ip; then
|
if ! echo "$UPLINK" > "$QEMU_DIR"/qemu.ip; then
|
||||||
error "Failed to write QEMU IP file!"
|
error "Failed to write QEMU IP file!"
|
||||||
exit 24
|
exit 24
|
||||||
|
|||||||
+47
-34
@@ -67,16 +67,10 @@ displayReason() {
|
|||||||
|
|
||||||
readQemuPid() {
|
readQemuPid() {
|
||||||
|
|
||||||
local -n _pid="$1"
|
# Interactive startup uses a wrapper-created PID file before QEMU writes its
|
||||||
local file
|
# own pidfile, so accept either during startup and shutdown races.
|
||||||
|
readPidFile "$1" "$QEMU_START_PID" && return 0
|
||||||
for file in "$QEMU_START_PID" "$QEMU_PID"; do
|
readPidFile "$1" "$QEMU_PID"
|
||||||
if [ -s "$file" ] && read -r _pid < "$file"; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
qemuPidFile() {
|
qemuPidFile() {
|
||||||
@@ -92,7 +86,7 @@ qemuPidFile() {
|
|||||||
waitQemuExit() {
|
waitQemuExit() {
|
||||||
|
|
||||||
local timeout="${1:-10}"
|
local timeout="${1:-10}"
|
||||||
local file=""
|
local file
|
||||||
|
|
||||||
qemuPidFile file
|
qemuPidFile file
|
||||||
waitPidFile "$file" "$timeout"
|
waitPidFile "$file" "$timeout"
|
||||||
@@ -100,26 +94,24 @@ waitQemuExit() {
|
|||||||
|
|
||||||
waitQemuPid() {
|
waitQemuPid() {
|
||||||
|
|
||||||
local -n _pid="$1"
|
local cnt=0
|
||||||
local cnt=0 value=""
|
|
||||||
|
|
||||||
while ! readQemuPid value; do
|
while ! readQemuPid "$1"; do
|
||||||
sleep 0.02
|
sleep 0.02
|
||||||
cnt=$((cnt + 1))
|
cnt=$((cnt + 1))
|
||||||
(( cnt >= 50 )) && return 1
|
(( cnt >= 50 )) && return 1
|
||||||
done
|
done
|
||||||
|
|
||||||
_pid="$value"
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
forceKillQemu() {
|
forceKillQemu() {
|
||||||
|
|
||||||
local reason="$1"
|
local reason="$1"
|
||||||
local pid="" display
|
local pid display
|
||||||
|
|
||||||
! readQemuPid pid && return 0
|
readQemuPid pid || return 0
|
||||||
! isAlive "$pid" && return 0
|
isAlive "$pid" || return 0
|
||||||
|
|
||||||
display=$(displayReason "$reason")
|
display=$(displayReason "$reason")
|
||||||
error "Forcefully terminating $(app), reason: $display..."
|
error "Forcefully terminating $(app), reason: $display..."
|
||||||
@@ -145,7 +137,7 @@ cleanupHelpers() {
|
|||||||
startConsole() {
|
startConsole() {
|
||||||
|
|
||||||
local output="${1:-/dev/tty}"
|
local output="${1:-/dev/tty}"
|
||||||
local cnt=0 pid=""
|
local cnt=0
|
||||||
|
|
||||||
rm -f -- "$CONSOLE_SOCKET" "$CONSOLE_PID"
|
rm -f -- "$CONSOLE_SOCKET" "$CONSOLE_PID"
|
||||||
|
|
||||||
@@ -159,7 +151,7 @@ startConsole() {
|
|||||||
exec nc -lU "$CONSOLE_SOCKET" </dev/tty >"$output"
|
exec nc -lU "$CONSOLE_SOCKET" </dev/tty >"$output"
|
||||||
) &
|
) &
|
||||||
|
|
||||||
pid=$!
|
local pid="$!"
|
||||||
echo "$pid" > "$CONSOLE_PID"
|
echo "$pid" > "$CONSOLE_PID"
|
||||||
|
|
||||||
while [ ! -S "$CONSOLE_SOCKET" ]; do
|
while [ ! -S "$CONSOLE_SOCKET" ]; do
|
||||||
@@ -194,6 +186,8 @@ startQemu() {
|
|||||||
|
|
||||||
rm -f -- "$QEMU_START_PID"
|
rm -f -- "$QEMU_START_PID"
|
||||||
|
|
||||||
|
# Launch QEMU in a separate session while recording the real child PID;
|
||||||
|
# setsid's wrapper PID is not suitable for guest shutdown or forced cleanup.
|
||||||
(
|
(
|
||||||
trap '' INT QUIT
|
trap '' INT QUIT
|
||||||
|
|
||||||
@@ -246,14 +240,16 @@ finish() {
|
|||||||
sendGuestShutdown() {
|
sendGuestShutdown() {
|
||||||
|
|
||||||
local pid="$1"
|
local pid="$1"
|
||||||
local response url
|
local response
|
||||||
|
|
||||||
|
# Virtual DSM ignores ACPI powerdown, so graceful shutdown must go through
|
||||||
|
# the qemu-host guest API exposed on the Unix socket.
|
||||||
# Don't send the powerdown signal because vDSM ignores ACPI signals
|
# Don't send the powerdown signal because vDSM ignores ACPI signals
|
||||||
# nc -q 1 -w 1 -U "$QEMU_DIR/monitor.sock" &> /dev/null <<<'system_powerdown' || :
|
# nc -q 1 -w 1 -U "$QEMU_DIR/monitor.sock" &> /dev/null <<<'system_powerdown' || :
|
||||||
|
|
||||||
# Send shutdown command to guest agent via serial port
|
# Send shutdown command to guest agent via serial port
|
||||||
API_TIMEOUT=$(strip "$API_TIMEOUT")
|
API_TIMEOUT=$(strip "$API_TIMEOUT")
|
||||||
url="http://localhost/read?command=$API_CMD&timeout=$API_TIMEOUT"
|
local url="http://localhost/read?command=$API_CMD&timeout=$API_TIMEOUT"
|
||||||
response=$(curl --unix-socket "$HOST_API_SOCKET" -sk -m "$(( API_TIMEOUT+2 ))" -S "$url" 2>&1)
|
response=$(curl --unix-socket "$HOST_API_SOCKET" -sk -m "$(( API_TIMEOUT+2 ))" -S "$url" 2>&1)
|
||||||
|
|
||||||
if [[ "$response" =~ "\"success\"" ]]; then
|
if [[ "$response" =~ "\"success\"" ]]; then
|
||||||
@@ -275,6 +271,8 @@ sendGuestShutdown() {
|
|||||||
|
|
||||||
normalizeTimeout() {
|
normalizeTimeout() {
|
||||||
|
|
||||||
|
# Divide the remaining timeout into guest wait, SIGTERM grace, and final
|
||||||
|
# cleanup instead of allowing the API call to consume the entire budget.
|
||||||
local term_grace=3 # seconds before loop ends to send SIGTERM
|
local term_grace=3 # seconds before loop ends to send SIGTERM
|
||||||
local cleanup_grace=3 # seconds reserved after the loop for cleanup
|
local cleanup_grace=3 # seconds reserved after the loop for cleanup
|
||||||
|
|
||||||
@@ -315,8 +313,10 @@ waitForShutdown() {
|
|||||||
local slp=$!
|
local slp=$!
|
||||||
|
|
||||||
# Stop waiting if the process has exited
|
# Stop waiting if the process has exited
|
||||||
! isAlive "$pid" && break
|
isAlive "$pid" || break
|
||||||
|
|
||||||
|
# The process state is authoritative, but disappearance of both pidfiles
|
||||||
|
# also ends the wait when a wrapper exits before process reaping completes.
|
||||||
# Workaround for stale/zombie QEMU pid file
|
# Workaround for stale/zombie QEMU pid file
|
||||||
[ ! -s "$QEMU_START_PID" ] && [ ! -s "$QEMU_PID" ] && break
|
[ ! -s "$QEMU_START_PID" ] && [ ! -s "$QEMU_PID" ] && break
|
||||||
|
|
||||||
@@ -335,17 +335,19 @@ waitForShutdown() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
graceful_shutdown() {
|
gracefulShutdown() {
|
||||||
|
|
||||||
local sig="$1"
|
local sig="$1"
|
||||||
local pid="" code=0
|
local pid code
|
||||||
|
|
||||||
[[ $BASHPID != "$TRAP_PID" ]] && return
|
[[ $BASHPID != "$TRAP_PID" ]] && return
|
||||||
|
|
||||||
code=$(signalCode "$sig")
|
code=$(signalCode "$sig")
|
||||||
|
|
||||||
if [ -f "$QEMU_END" ]; then
|
if (( SHUTDOWN_SIGNAL != 0 )); then
|
||||||
|
|
||||||
|
# A second Ctrl-C is the explicit user request to skip the remaining
|
||||||
|
# graceful-shutdown wait and proceed to forced cleanup.
|
||||||
if (( code == 130 && SHUTDOWN_SIGNAL == code )); then
|
if (( code == 130 && SHUTDOWN_SIGNAL == code )); then
|
||||||
SHUTDOWN_SKIP=1
|
SHUTDOWN_SKIP=1
|
||||||
echo && info "Received SIGINT again, forcing shutdown..."
|
echo && info "Received SIGINT again, forcing shutdown..."
|
||||||
@@ -356,11 +358,14 @@ graceful_shutdown() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
set +e
|
|
||||||
start=$SECONDS
|
start=$SECONDS
|
||||||
SHUTDOWN_SIGNAL=$code
|
SHUTDOWN_SIGNAL=$code
|
||||||
|
|
||||||
|
# Shutdown handlers must continue through missing processes and failed cleanup
|
||||||
|
# commands instead of being aborted by errexit.
|
||||||
|
set +e
|
||||||
touch "$QEMU_END"
|
touch "$QEMU_END"
|
||||||
|
|
||||||
echo && info "Received $sig signal, sending shutdown command..."
|
echo && info "Received $sig signal, sending shutdown command..."
|
||||||
|
|
||||||
if ! readQemuPid pid; then
|
if ! readQemuPid pid; then
|
||||||
@@ -382,13 +387,21 @@ graceful_shutdown() {
|
|||||||
finish "$code"
|
finish "$code"
|
||||||
}
|
}
|
||||||
|
|
||||||
! enabled "$SHUTDOWN" && return 0
|
enableTrap() {
|
||||||
|
|
||||||
|
enabled "$SHUTDOWN" || return 0
|
||||||
|
|
||||||
|
# Keep Ctrl-C available to interactive users without installing an unnecessary
|
||||||
|
# SIGINT handler for background/container execution.
|
||||||
|
if interactive; then
|
||||||
|
_trap gracefulShutdown SIGINT
|
||||||
|
fi
|
||||||
|
|
||||||
|
_trap gracefulShutdown SIGTERM SIGHUP SIGABRT SIGQUIT
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
[ -n "${QEMU_TIMEOUT:-}" ] && TIMEOUT="$QEMU_TIMEOUT"
|
[ -n "${QEMU_TIMEOUT:-}" ] && TIMEOUT="$QEMU_TIMEOUT"
|
||||||
|
|
||||||
if interactive; then
|
|
||||||
_trap graceful_shutdown SIGINT
|
|
||||||
fi
|
|
||||||
|
|
||||||
_trap graceful_shutdown SIGTERM SIGHUP SIGABRT SIGQUIT
|
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
+33
-13
@@ -9,6 +9,7 @@ cd /run
|
|||||||
. utils.sh # Load functions
|
. utils.sh # Load functions
|
||||||
|
|
||||||
info () { printf "%b%s%b" "\E[1;34m❯ \E[1;36m" "$1" "\E[0m\n" >&2; }
|
info () { printf "%b%s%b" "\E[1;34m❯ \E[1;36m" "$1" "\E[0m\n" >&2; }
|
||||||
|
warn () { printf "%b%s%b" "\E[1;33m❯ " "WARNING: $1" "\E[0m\n" >&2; }
|
||||||
error () { printf "%b%s%b" "\E[1;31m❯ " "ERROR: $1" "\E[0m\n" >&2; }
|
error () { printf "%b%s%b" "\E[1;31m❯ " "ERROR: $1" "\E[0m\n" >&2; }
|
||||||
|
|
||||||
disabled "$NETWORK" && exit 0
|
disabled "$NETWORK" && exit 0
|
||||||
@@ -36,9 +37,9 @@ exitIfShuttingDown() {
|
|||||||
|
|
||||||
queryGuest() {
|
queryGuest() {
|
||||||
|
|
||||||
local rc
|
# Query DSM through the qemu-host sidecar rather than the guest network,
|
||||||
|
# which may not be configured yet.
|
||||||
{ json=$(curl --unix-socket "$socket" -m 20 -sk "$url"); rc=$?; } || :
|
{ json=$(curl --unix-socket "$socket" -m 20 -sk "$url"); local rc=$?; } || :
|
||||||
|
|
||||||
exitIfShuttingDown
|
exitIfShuttingDown
|
||||||
|
|
||||||
@@ -54,9 +55,8 @@ readJsonField() {
|
|||||||
|
|
||||||
local query="$1"
|
local query="$1"
|
||||||
local result
|
local result
|
||||||
local rc
|
|
||||||
|
|
||||||
{ result=$(jq -r "$query" <<< "$json"); rc=$?; } || :
|
{ result=$(jq -r "$query" <<< "$json"); local rc=$?; } || :
|
||||||
|
|
||||||
if (( rc != 0 )); then
|
if (( rc != 0 )); then
|
||||||
error "$jq_err $rc ( $json )"
|
error "$jq_err $rc ( $json )"
|
||||||
@@ -74,12 +74,12 @@ readJsonField() {
|
|||||||
|
|
||||||
readGuestStatus() {
|
readGuestStatus() {
|
||||||
|
|
||||||
local result msg rc
|
local result msg
|
||||||
|
|
||||||
result=$(readJsonField '.status') || return 1
|
result=$(readJsonField '.status') || return 1
|
||||||
|
|
||||||
if [[ "$result" != "success" ]]; then
|
if [[ "$result" != "success" ]]; then
|
||||||
{ msg=$(jq -r '.message // empty' <<< "$json"); rc=$?; } || :
|
{ msg=$(jq -r '.message // empty' <<< "$json"); local rc=$?; } || :
|
||||||
|
|
||||||
if (( rc != 0 )); then
|
if (( rc != 0 )); then
|
||||||
error "$jq_err $rc ( $json )"
|
error "$jq_err $rc ( $json )"
|
||||||
@@ -118,6 +118,8 @@ writeDsmLocation() {
|
|||||||
|
|
||||||
pollGuestLocation() {
|
pollGuestLocation() {
|
||||||
|
|
||||||
|
# Keep polling until the guest reports a usable address, but stop promptly
|
||||||
|
# when container shutdown begins.
|
||||||
while [ ! -s "$file" ]; do
|
while [ ! -s "$file" ]; do
|
||||||
|
|
||||||
# Check if not shutting down
|
# Check if not shutting down
|
||||||
@@ -140,14 +142,29 @@ pollGuestLocation() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkAddressConflict() {
|
||||||
|
|
||||||
|
local guest_ip="${location%:*}"
|
||||||
|
local container_ip=""
|
||||||
|
|
||||||
|
[ -s "$address" ] && container_ip=$(<"$address")
|
||||||
|
[ -z "$container_ip" ] && return 0
|
||||||
|
[[ "$guest_ip" != "$container_ip" ]] && return 0
|
||||||
|
|
||||||
|
warn "DSM is using the same IP as the container, this will cause connectivity issues."
|
||||||
|
warn "change the container's macvlan IP or assign DSM a different address in your router."
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
writeDhcpPage() {
|
writeDhcpPage() {
|
||||||
|
|
||||||
local title body script html
|
local html
|
||||||
|
|
||||||
msg="http://$location"
|
msg="http://$location"
|
||||||
title="<title>Virtual DSM</title>"
|
local title="<title>Virtual DSM</title>"
|
||||||
body="The location of DSM is <a href='http://$location'>http://$location</a>"
|
local body="The location of DSM is <a href='http://$location'>http://$location</a>"
|
||||||
script="<script>setTimeout(function(){ window.location.assign('http://$location'); }, 3000);</script>"
|
local script="<script>setTimeout(function(){ window.location.assign('http://$location'); }, 3000);</script>"
|
||||||
|
|
||||||
html=$(<"$template")
|
html=$(<"$template")
|
||||||
html="${html/\[1\]/$title}"
|
html="${html/\[1\]/$title}"
|
||||||
@@ -164,12 +181,14 @@ writeDhcpPage() {
|
|||||||
|
|
||||||
buildStaticMessage() {
|
buildStaticMessage() {
|
||||||
|
|
||||||
local nic ip port
|
local nic ip
|
||||||
|
|
||||||
nic=$(<"$driver")
|
nic=$(<"$driver")
|
||||||
ip=$(<"$address")
|
ip=$(<"$address")
|
||||||
port="${location##*:}"
|
local port="${location##*:}"
|
||||||
|
|
||||||
|
# NAT and user-mode networking are reached through a forwarded host port;
|
||||||
|
# macvlan exposes DSM directly on the container-facing LAN address.
|
||||||
if [[ "${nic,,}" != "macvlan" ]]; then
|
if [[ "${nic,,}" != "macvlan" ]]; then
|
||||||
msg="port $port"
|
msg="port $port"
|
||||||
else
|
else
|
||||||
@@ -196,6 +215,7 @@ exitIfShuttingDown
|
|||||||
location=$(<"$file")
|
location=$(<"$file")
|
||||||
|
|
||||||
if enabled "$DHCP"; then
|
if enabled "$DHCP"; then
|
||||||
|
checkAddressConflict
|
||||||
writeDhcpPage
|
writeDhcpPage
|
||||||
else
|
else
|
||||||
buildStaticMessage
|
buildStaticMessage
|
||||||
|
|||||||
+11
-1
@@ -47,7 +47,7 @@ checkSse42() {
|
|||||||
|
|
||||||
if ! hasFlag "sse4_2"; then
|
if ! hasFlag "sse4_2"; then
|
||||||
error "Your CPU does not have the SSE4 instruction set that Virtual DSM requires!"
|
error "Your CPU does not have the SSE4 instruction set that Virtual DSM requires!"
|
||||||
! enabled "$DEBUG" && exit 88
|
enabled "$DEBUG" || exit 88
|
||||||
fi
|
fi
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
@@ -66,6 +66,8 @@ trimSpaces() {
|
|||||||
|
|
||||||
removeCpuArgument() {
|
removeCpuArgument() {
|
||||||
|
|
||||||
|
# CPU configuration has dedicated variables. Remove raw -cpu arguments so
|
||||||
|
# option ordering cannot silently override the validated model and flags.
|
||||||
local args=" ${ARGUMENTS:-} "
|
local args=" ${ARGUMENTS:-} "
|
||||||
|
|
||||||
while [[ "$args" =~ [[:space:]]-cpu([[:space:]][^[:space:]]+|=[^[:space:]]+)? ]]; do
|
while [[ "$args" =~ [[:space:]]-cpu([[:space:]][^[:space:]]+|=[^[:space:]]+)? ]]; do
|
||||||
@@ -94,6 +96,8 @@ configureKvmCpuModel() {
|
|||||||
|
|
||||||
appendKvmInvtscFeature() {
|
appendKvmInvtscFeature() {
|
||||||
|
|
||||||
|
# invtsc is safe only when the active accelerator can scale the host TSC;
|
||||||
|
# AMD and Intel expose that capability through different host flags.
|
||||||
if hasFlag "svm"; then
|
if hasFlag "svm"; then
|
||||||
|
|
||||||
# AMD processor
|
# AMD processor
|
||||||
@@ -131,6 +135,8 @@ configureTcgCpuModel() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# TCG uses the broad max model on native x86, but qemu64 is the compatible
|
||||||
|
# cross-architecture fallback.
|
||||||
if [[ "$ARCH" == "amd64" ]]; then
|
if [[ "$ARCH" == "amd64" ]]; then
|
||||||
CPU_MODEL="max"
|
CPU_MODEL="max"
|
||||||
CPU_FEATURES+=",migratable=no"
|
CPU_FEATURES+=",migratable=no"
|
||||||
@@ -158,6 +164,8 @@ configureTcg() {
|
|||||||
|
|
||||||
composeCpuFlags() {
|
composeCpuFlags() {
|
||||||
|
|
||||||
|
# Compose one -cpu value in precedence order: model, required features,
|
||||||
|
# then user-provided overrides.
|
||||||
CPU_FLAGS="${CPU_MODEL}${CPU_FEATURES:+,$CPU_FEATURES}${CPU_FLAGS:+,$CPU_FLAGS}"
|
CPU_FLAGS="${CPU_MODEL}${CPU_FEATURES:+,$CPU_FEATURES}${CPU_FLAGS:+,$CPU_FLAGS}"
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
@@ -170,6 +178,8 @@ configureHostCpuName() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$HOST_CPU" ]; then
|
if [ -n "$HOST_CPU" ]; then
|
||||||
|
# qemu-host expects a comma-separated CPU description with empty family
|
||||||
|
# and suffix fields, not QEMU's -cpu syntax.
|
||||||
HOST_CPU="${HOST_CPU%%,*},,"
|
HOST_CPU="${HOST_CPU%%,*},,"
|
||||||
else
|
else
|
||||||
HOST_CPU="QEMU, Virtual CPU,"
|
HOST_CPU="QEMU, Virtual CPU,"
|
||||||
|
|||||||
+26
-2
@@ -22,6 +22,8 @@ writeInfo() {
|
|||||||
|
|
||||||
local content="$1"
|
local content="$1"
|
||||||
|
|
||||||
|
# Replace the web status atomically so websocket readers never observe a
|
||||||
|
# partially written HTML fragment.
|
||||||
if ! printf '%s\n' "$content" > "$info_tmp"; then
|
if ! printf '%s\n' "$content" > "$info_tmp"; then
|
||||||
rm -f -- "$info_tmp"
|
rm -f -- "$info_tmp"
|
||||||
return 1
|
return 1
|
||||||
@@ -153,6 +155,16 @@ printSizeProgress() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stopProgress() {
|
||||||
|
|
||||||
|
if [ -z "$status_file" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
stopping="Y"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
finishProgress() {
|
finishProgress() {
|
||||||
|
|
||||||
rm -f -- "$info_tmp"
|
rm -f -- "$info_tmp"
|
||||||
@@ -202,13 +214,17 @@ printed="N"
|
|||||||
next_percent=10
|
next_percent=10
|
||||||
next_bytes="$step_bytes"
|
next_bytes="$step_bytes"
|
||||||
log_mode="percent"
|
log_mode="percent"
|
||||||
|
stopping="N"
|
||||||
|
|
||||||
if [ -z "$total" ] || [[ "$total" == "0" ]]; then
|
if [ -z "$total" ] || [[ "$total" == "0" ]]; then
|
||||||
log_mode="size"
|
log_mode="size"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
trap finishProgress EXIT
|
trap finishProgress EXIT
|
||||||
trap 'exit 0' HUP INT QUIT TERM
|
trap 'exit 0' HUP INT QUIT
|
||||||
|
# SIGTERM requests one final measurement and web update rather than
|
||||||
|
# terminating between progress samples.
|
||||||
|
trap stopProgress TERM
|
||||||
|
|
||||||
if [[ "$body" == *"..." ]]; then
|
if [[ "$body" == *"..." ]]; then
|
||||||
body="<p class=\"loading\">${body::-3}</p>"
|
body="<p class=\"loading\">${body::-3}</p>"
|
||||||
@@ -216,9 +232,12 @@ fi
|
|||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
|
|
||||||
|
final_pass="${stopping:-}"
|
||||||
bytes=$(getBytes "$path" "$mode")
|
bytes=$(getBytes "$path" "$mode")
|
||||||
effective_total="$total"
|
effective_total="$total"
|
||||||
|
|
||||||
|
# An external downloader may provide authoritative completed and total byte
|
||||||
|
# counters; use them instead of filesystem size when available.
|
||||||
if [ -n "$status_file" ] && status=$(getStatus "$status_file"); then
|
if [ -n "$status_file" ] && status=$(getStatus "$status_file"); then
|
||||||
read -r status_bytes status_total <<< "$status"
|
read -r status_bytes status_total <<< "$status"
|
||||||
bytes="$status_bytes"
|
bytes="$status_bytes"
|
||||||
@@ -255,6 +274,8 @@ while true; do
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
|
# Floor the percentage rather than rounding so displayed completion
|
||||||
|
# never gets ahead of bytes actually written.
|
||||||
# Truncate to one decimal so progress is never reported early.
|
# Truncate to one decimal so progress is never reported early.
|
||||||
progress=$((bytes * 1000 / effective_total))
|
progress=$((bytes * 1000 / effective_total))
|
||||||
(( progress > 1000 )) && progress=1000
|
(( progress > 1000 )) && progress=1000
|
||||||
@@ -282,5 +303,8 @@ while true; do
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sleep 1 & wait $!
|
[[ "$final_pass" == "Y" ]] && break
|
||||||
|
|
||||||
|
sleep 1 &
|
||||||
|
wait $! || :
|
||||||
done
|
done
|
||||||
|
|||||||
+18
-8
@@ -42,6 +42,8 @@ validateHostMac() {
|
|||||||
|
|
||||||
buildHostArguments() {
|
buildHostArguments() {
|
||||||
|
|
||||||
|
# qemu-host is a sidecar that bridges DSM's proprietary serial agent to
|
||||||
|
# Unix sockets used by shutdown and post-boot discovery helpers.
|
||||||
HOST_ARGS=()
|
HOST_ARGS=()
|
||||||
HOST_ARGS+=("-cpu=$CPU_CORES")
|
HOST_ARGS+=("-cpu=$CPU_CORES")
|
||||||
HOST_ARGS+=("-cpu_arch=$HOST_CPU")
|
HOST_ARGS+=("-cpu_arch=$HOST_CPU")
|
||||||
@@ -60,6 +62,8 @@ startHostBinary() {
|
|||||||
|
|
||||||
local pid
|
local pid
|
||||||
|
|
||||||
|
# Remove stale sockets and pid state before starting the sidecar; a Unix
|
||||||
|
# socket path cannot be rebound while an old filesystem entry remains.
|
||||||
rm -f -- "$HOST_PID" "$HOST_API_SOCKET" "$HOST_AGENT_SOCKET" || return 1
|
rm -f -- "$HOST_PID" "$HOST_API_SOCKET" "$HOST_AGENT_SOCKET" || return 1
|
||||||
|
|
||||||
if enabled "$HOST_DEBUG"; then
|
if enabled "$HOST_DEBUG"; then
|
||||||
@@ -73,7 +77,7 @@ startHostBinary() {
|
|||||||
pid=$!
|
pid=$!
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$pid" > "$HOST_PID"
|
printf '%s\n' "$pid" > "$HOST_PID"
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -82,23 +86,24 @@ waitForSocket() {
|
|||||||
|
|
||||||
local socket="$1"
|
local socket="$1"
|
||||||
local exit_code="$2"
|
local exit_code="$2"
|
||||||
local pid cnt=0
|
local timeout=5 pid
|
||||||
|
local deadline=$((SECONDS + timeout))
|
||||||
|
|
||||||
|
# Do not start QEMU until both sidecar sockets are ready; otherwise the
|
||||||
|
# VirtIO serial channel or API client may race initial creation.
|
||||||
while [ ! -S "$socket" ]; do
|
while [ ! -S "$socket" ]; do
|
||||||
|
|
||||||
if ! read -r pid < "$HOST_PID" || ! isAlive "$pid"; then
|
if ! readPidFile pid "$HOST_PID" || ! isAlive "$pid"; then
|
||||||
error "qemu-host exited unexpectedly!"
|
error "qemu-host exited unexpectedly!"
|
||||||
exit "$exit_code"
|
exit "$exit_code"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sleep 0.1
|
if (( SECONDS >= deadline )); then
|
||||||
cnt=$((cnt + 1))
|
|
||||||
|
|
||||||
if (( cnt > 50 )); then
|
|
||||||
error "Failed to create qemu-host socket: $socket"
|
error "Failed to create qemu-host socket: $socket"
|
||||||
exit "$exit_code"
|
exit "$exit_code"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
sleep 0.1
|
||||||
done
|
done
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
@@ -106,6 +111,11 @@ waitForSocket() {
|
|||||||
|
|
||||||
configureSerialPorts() {
|
configureSerialPorts() {
|
||||||
|
|
||||||
|
local bus
|
||||||
|
bus=$(getPciBus)
|
||||||
|
|
||||||
|
# Managed interactive mode separates the console and QEMU monitor into
|
||||||
|
# reconnecting sockets; other runs keep the simple combined stdio monitor.
|
||||||
if enabled "${SHUTDOWN:-Y}" && interactive; then
|
if enabled "${SHUTDOWN:-Y}" && interactive; then
|
||||||
|
|
||||||
CONSOLE_SOCKET="$QEMU_DIR/console.sock"
|
CONSOLE_SOCKET="$QEMU_DIR/console.sock"
|
||||||
@@ -123,7 +133,7 @@ configureSerialPorts() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
SERIAL_OPTS+=" \
|
SERIAL_OPTS+=" \
|
||||||
-device virtio-serial-pci,id=virtio-serial0,bus=pcie.0,addr=0x3 \
|
-device virtio-serial-pci,id=virtio-serial0,bus=$bus,addr=0x3 \
|
||||||
-chardev socket,id=charchannel0,path=$HOST_AGENT_SOCKET,reconnect-ms=1000 \
|
-chardev socket,id=charchannel0,path=$HOST_AGENT_SOCKET,reconnect-ms=1000 \
|
||||||
-device virtserialport,bus=virtio-serial0.0,nr=1,chardev=charchannel0,id=channel0,name=vchannel"
|
-device virtserialport,bus=virtio-serial0.0,nr=1,chardev=charchannel0,id=channel0,name=vchannel"
|
||||||
|
|
||||||
|
|||||||
+95
-20
@@ -2,20 +2,19 @@
|
|||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
: "${WEB_PORT:="5000"}" # Webserver port
|
: "${WEB_PORT:="5000"}" # Webserver port
|
||||||
: "${WSD_PORT:="8004"}" # Websockets port
|
|
||||||
|
|
||||||
# Sanitize port variables
|
# Sanitize port variables
|
||||||
WEB_PORT=$(strip "$WEB_PORT")
|
WEB_PORT=$(strip "$WEB_PORT")
|
||||||
WSD_PORT=$(strip "$WSD_PORT")
|
|
||||||
|
|
||||||
WEB_PID="/run/nginx.pid"
|
WEB_PID="/run/nginx.pid"
|
||||||
WSD_LOG="/var/log/websocketd.log"
|
WSD_LOG="/var/log/websocketd.log"
|
||||||
WSD_PID="$QEMU_DIR/websocketd.pid"
|
WSD_PID="$QEMU_DIR/websocketd.pid"
|
||||||
|
WSD_SOCKET="$QEMU_DIR/status-ws.sock"
|
||||||
|
|
||||||
prepareWebFiles() {
|
prepareWebFiles() {
|
||||||
|
|
||||||
cp -r /var/www/* "$QEMU_DIR" || return 1
|
cp -r /var/www/* "$QEMU_DIR" || return 1
|
||||||
rm -f "$WSD_PID" "$WEB_PID" "$WSD_LOG" || return 1
|
rm -f -- "$WSD_PID" "$WSD_SOCKET" "$WEB_PID" "$WSD_LOG" || return 1
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -24,9 +23,8 @@ configureWebPorts() {
|
|||||||
|
|
||||||
if ! sed -i \
|
if ! sed -i \
|
||||||
-e "s|listen 5000 default_server;|listen $WEB_PORT default_server;|g" \
|
-e "s|listen 5000 default_server;|listen $WEB_PORT default_server;|g" \
|
||||||
-e "s|proxy_pass http://127.0.0.1:8004/;|proxy_pass http://127.0.0.1:$WSD_PORT/;|g" \
|
|
||||||
/etc/nginx/sites-enabled/web.conf; then
|
/etc/nginx/sites-enabled/web.conf; then
|
||||||
error "Failed to configure webserver ports!"
|
error "Failed to configure webserver port!"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -35,6 +33,8 @@ configureWebPorts() {
|
|||||||
|
|
||||||
configureIpv6Listen() {
|
configureIpv6Listen() {
|
||||||
|
|
||||||
|
# Use one dual-stack listener when IPv6 is active, avoiding separate IPv4
|
||||||
|
# and IPv6 sockets that can conflict on the same port.
|
||||||
if [ -f /proc/net/if_inet6 ] && [[ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null)" != "1" ]]; then
|
if [ -f /proc/net/if_inet6 ] && [[ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null)" != "1" ]]; then
|
||||||
|
|
||||||
if ! sed -i \
|
if ! sed -i \
|
||||||
@@ -49,17 +49,56 @@ configureIpv6Listen() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configureNginx() {
|
||||||
|
|
||||||
|
mkdir -p /etc/nginx/sites-enabled || return 1
|
||||||
|
rm -f /etc/nginx/sites-enabled/default || return 1
|
||||||
|
|
||||||
|
# TODO: Use setfacl to grant www-data access to the Unix sockets
|
||||||
|
# and restore unprivileged nginx workers.
|
||||||
|
if ! sed -i \
|
||||||
|
-e 's/^user .*/user root;/' \
|
||||||
|
-e 's/^worker_processes.*/worker_processes 1;/' \
|
||||||
|
/etc/nginx/nginx.conf; then
|
||||||
|
error "Failed to configure nginx!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! cp /etc/nginx/default.conf /etc/nginx/sites-enabled/web.conf; then
|
||||||
|
error "Failed to copy nginx config!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
configureWebServer() {
|
configureWebServer() {
|
||||||
|
|
||||||
mkdir -p /etc/nginx/sites-enabled
|
configureNginx || return 1
|
||||||
cp /etc/nginx/default.conf /etc/nginx/sites-enabled/web.conf
|
|
||||||
|
|
||||||
configureWebPorts || return 1
|
configureWebPorts || return 1
|
||||||
configureIpv6Listen || return 1
|
configureIpv6Listen || return 1
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stopWebServer() {
|
||||||
|
|
||||||
|
local pid
|
||||||
|
|
||||||
|
if readPidFile pid "$WEB_PID"; then
|
||||||
|
pKill "$pid" 2
|
||||||
|
|
||||||
|
# Escalate only after the normal termination grace period; stale nginx
|
||||||
|
# processes would otherwise keep the configured web port occupied.
|
||||||
|
if isAlive "$pid"; then
|
||||||
|
kill -9 -- "$pid" 2>/dev/null || :
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f -- "$WEB_PID"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
startWebServer() {
|
startWebServer() {
|
||||||
|
|
||||||
# Start webserver
|
# Start webserver
|
||||||
@@ -68,12 +107,27 @@ startWebServer() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stopWebsocketServer() {
|
||||||
|
|
||||||
|
local pid
|
||||||
|
|
||||||
|
if readPidFile pid "$WSD_PID"; then
|
||||||
|
pKill "$pid" 2
|
||||||
|
|
||||||
|
if isAlive "$pid"; then
|
||||||
|
kill -9 -- "$pid" 2>/dev/null || :
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f -- "$WSD_PID" "$WSD_SOCKET"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
startWebsocketServer() {
|
startWebsocketServer() {
|
||||||
|
|
||||||
# Start websocket server
|
# Start websocket server
|
||||||
websocketd \
|
websocketd \
|
||||||
--address 127.0.0.1 \
|
--unixsocket="$WSD_SOCKET" \
|
||||||
--port="$WSD_PORT" \
|
|
||||||
/run/socket.sh \
|
/run/socket.sh \
|
||||||
>"$WSD_LOG" 2>&1 &
|
>"$WSD_LOG" 2>&1 &
|
||||||
|
|
||||||
@@ -81,19 +135,36 @@ startWebsocketServer() {
|
|||||||
|
|
||||||
if ! echo "$pid" > "$WSD_PID"; then
|
if ! echo "$pid" > "$WSD_PID"; then
|
||||||
kill "$pid" 2>/dev/null || :
|
kill "$pid" 2>/dev/null || :
|
||||||
|
rm -f -- "$WSD_PID"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sleep 0.1
|
local i
|
||||||
|
for (( i = 1; i <= 50; i++ )); do
|
||||||
|
|
||||||
if ! isAlive "$pid"; then
|
if ! isAlive "$pid"; then
|
||||||
rm -f "$WSD_PID"
|
rm -f -- "$WSD_PID" "$WSD_SOCKET"
|
||||||
[ -s "$WSD_LOG" ] && cat "$WSD_LOG" >&2
|
[ -s "$WSD_LOG" ] && cat "$WSD_LOG" >&2
|
||||||
error "Failed to start websocket server!"
|
error "Failed to start websocket server!"
|
||||||
return 1
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -S "$WSD_SOCKET" ] && return 0
|
||||||
|
|
||||||
|
sleep 0.1
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
pKill "$pid" 2
|
||||||
|
|
||||||
|
if isAlive "$pid"; then
|
||||||
|
kill -9 -- "$pid" 2>/dev/null || :
|
||||||
fi
|
fi
|
||||||
|
|
||||||
return 0
|
rm -f -- "$WSD_PID" "$WSD_SOCKET"
|
||||||
|
[ -s "$WSD_LOG" ] && cat "$WSD_LOG" >&2
|
||||||
|
error "Websocket server did not create its socket!"
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareWebFiles
|
prepareWebFiles
|
||||||
@@ -104,7 +175,11 @@ disabled "${WEB:-}" && return 0
|
|||||||
|
|
||||||
configureWebServer
|
configureWebServer
|
||||||
|
|
||||||
startWebServer
|
if startWebServer && startWebsocketServer; then
|
||||||
startWebsocketServer
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
return 0
|
stopWebsocketServer || :
|
||||||
|
stopWebServer || :
|
||||||
|
|
||||||
|
return 1
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ refresh() {
|
|||||||
[[ "$msg" == "$lastmsg" ]] && return 0
|
[[ "$msg" == "$lastmsg" ]] && return 0
|
||||||
|
|
||||||
lastmsg="$msg"
|
lastmsg="$msg"
|
||||||
|
# websocketd clients interpret s: as a status update and c: as a command;
|
||||||
|
# suppress unchanged status to avoid redundant browser work.
|
||||||
echo "s: $msg"
|
echo "s: $msg"
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
@@ -37,6 +39,8 @@ inotifywait \
|
|||||||
case "${event,,}" in
|
case "${event,,}" in
|
||||||
"delete"* )
|
"delete"* )
|
||||||
echo "c: vnc" ;;
|
echo "c: vnc" ;;
|
||||||
|
# moved_to covers the atomic replacement used by html()/writeAtomic(),
|
||||||
|
# while close_write handles direct writers.
|
||||||
"close_write"* | "moved_to"* )
|
"close_write"* | "moved_to"* )
|
||||||
refresh ;;
|
refresh ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
+133
-27
@@ -7,6 +7,32 @@ info () { printf "%b%s%b" "\E[1;34m❯ \E[1;36m" "${1:-}" "\E[0m\n"; }
|
|||||||
error () { printf "%b%s%b" "\E[1;31m❯ " "ERROR: ${1:-}" "\E[0m\n" >&2; }
|
error () { printf "%b%s%b" "\E[1;31m❯ " "ERROR: ${1:-}" "\E[0m\n" >&2; }
|
||||||
warn () { printf "%b%s%b" "\E[1;31m❯ " "Warning: ${1:-}" "\E[0m\n" >&2; }
|
warn () { printf "%b%s%b" "\E[1;31m❯ " "Warning: ${1:-}" "\E[0m\n" >&2; }
|
||||||
|
|
||||||
|
app() {
|
||||||
|
|
||||||
|
echo "Virtual DSM"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
readPidFile() {
|
||||||
|
|
||||||
|
local -n _pid="$1"
|
||||||
|
_pid=""
|
||||||
|
|
||||||
|
if ! _pid=$(cat -- "$2" 2>/dev/null); then
|
||||||
|
_pid=""
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Reject empty, zero, or nonnumeric pidfiles so cleanup can never signal an
|
||||||
|
# unintended process group.
|
||||||
|
if [[ ! "$_pid" =~ ^[1-9][0-9]*$ ]]; then
|
||||||
|
_pid=""
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
hasFlag() {
|
hasFlag() {
|
||||||
|
|
||||||
# Match a whitespace-delimited token in /proc/cpuinfo
|
# Match a whitespace-delimited token in /proc/cpuinfo
|
||||||
@@ -29,8 +55,27 @@ isAmdCpu() {
|
|||||||
[[ "$vendor" == "AuthenticAMD" ]]
|
[[ "$vendor" == "AuthenticAMD" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPciBus() {
|
||||||
|
|
||||||
|
local machine="${1:-${MACHINE:-q35}}"
|
||||||
|
|
||||||
|
if [ -n "${PCI_BUS:-}" ]; then
|
||||||
|
echo "$PCI_BUS"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${machine,,}" in
|
||||||
|
pc|pc-i440fx*) echo "pci.0" ;;
|
||||||
|
*) echo "pcie.0" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
interactive() {
|
interactive() {
|
||||||
|
|
||||||
|
# A TTY on stdin is insufficient when /dev/tty is unavailable; require both
|
||||||
|
# before enabling interactive console handling.
|
||||||
[ -t 0 ] && : 2>/dev/null </dev/tty >/dev/tty
|
[ -t 0 ] && : 2>/dev/null </dev/tty >/dev/tty
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -118,14 +163,13 @@ isAlive() {
|
|||||||
|
|
||||||
waitPid() {
|
waitPid() {
|
||||||
|
|
||||||
local i=0
|
|
||||||
local pid="$1"
|
local pid="$1"
|
||||||
local timeout="${2:-10}"
|
local timeout="${2:-10}"
|
||||||
|
local deadline=$((SECONDS + timeout))
|
||||||
|
|
||||||
while [ -n "$pid" ] && isAlive "$pid"; do
|
while [ -n "$pid" ] && isAlive "$pid"; do
|
||||||
|
(( SECONDS >= deadline )) && return 1
|
||||||
sleep 0.2
|
sleep 0.2
|
||||||
i=$((i + 1))
|
|
||||||
(( i >= timeout * 5 )) && return 1
|
|
||||||
done
|
done
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
@@ -133,19 +177,16 @@ waitPid() {
|
|||||||
|
|
||||||
waitPidFile() {
|
waitPidFile() {
|
||||||
|
|
||||||
local i=0
|
local pid
|
||||||
local pid=""
|
|
||||||
local file="$1"
|
local file="$1"
|
||||||
local timeout="${2:-10}"
|
local timeout="${2:-10}"
|
||||||
|
local deadline=$((SECONDS + timeout))
|
||||||
|
|
||||||
[ ! -s "$file" ] && return 0
|
readPidFile pid "$file" || return 0
|
||||||
! read -r pid <"$file" && return 0
|
|
||||||
[ -z "$pid" ] && return 0
|
|
||||||
|
|
||||||
while [ -s "$file" ] && isAlive "$pid"; do
|
while [ -s "$file" ] && isAlive "$pid"; do
|
||||||
|
(( SECONDS >= deadline )) && return 1
|
||||||
sleep 0.2
|
sleep 0.2
|
||||||
i=$((i + 1))
|
|
||||||
(( i >= timeout * 5 )) && return 1
|
|
||||||
done
|
done
|
||||||
|
|
||||||
rm -f -- "$file"
|
rm -f -- "$file"
|
||||||
@@ -168,19 +209,19 @@ pKill() {
|
|||||||
|
|
||||||
fWait() {
|
fWait() {
|
||||||
|
|
||||||
local i=0
|
|
||||||
local name="$1"
|
local name="$1"
|
||||||
local timeout="${2:-10}"
|
local timeout="${2:-10}"
|
||||||
|
local deadline=$((SECONDS + timeout))
|
||||||
|
|
||||||
[ -z "$name" ] && return 0
|
[ -z "$name" ] && return 0
|
||||||
|
|
||||||
while pgrep -f -l "$name" >/dev/null; do
|
while pgrep -f -l "$name" >/dev/null; do
|
||||||
sleep 0.2
|
if (( SECONDS >= deadline )); then
|
||||||
i=$((i + 1))
|
|
||||||
if (( i >= timeout * 5 )); then
|
|
||||||
warn "Timed out while waiting for process: $name"
|
warn "Timed out while waiting for process: $name"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
sleep 0.2
|
||||||
done
|
done
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
@@ -201,12 +242,10 @@ fKill() {
|
|||||||
|
|
||||||
sKill() {
|
sKill() {
|
||||||
|
|
||||||
local pid=""
|
local pid
|
||||||
local file="$1"
|
local file="$1"
|
||||||
|
|
||||||
[ ! -s "$file" ] && return 0
|
readPidFile pid "$file" || return 0
|
||||||
! read -r pid <"$file" && return 0
|
|
||||||
[ -z "$pid" ] && return 0
|
|
||||||
|
|
||||||
if isAlive "$pid"; then
|
if isAlive "$pid"; then
|
||||||
{ kill -15 -- "$pid" || :; } 2>/dev/null
|
{ kill -15 -- "$pid" || :; } 2>/dev/null
|
||||||
@@ -240,11 +279,13 @@ setOwner() {
|
|||||||
|
|
||||||
[ ! -f "$file" ] && return 1
|
[ ! -f "$file" ] && return 1
|
||||||
|
|
||||||
|
# Match generated files to the owner of their bind-mounted parent directory
|
||||||
|
# instead of assuming a fixed container or host UID.
|
||||||
dir=$(dirname -- "$file")
|
dir=$(dirname -- "$file")
|
||||||
uid=$(stat -c '%u' "$dir") || return 1
|
uid=$(stat -c '%u' "$dir") || return 1
|
||||||
gid=$(stat -c '%g' "$dir") || return 1
|
gid=$(stat -c '%g' "$dir") || return 1
|
||||||
|
|
||||||
! chown "$uid:$gid" "$file" && return 1
|
chown "$uid:$gid" "$file" || return 1
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -255,7 +296,7 @@ makeDir() {
|
|||||||
local dir uid gid
|
local dir uid gid
|
||||||
|
|
||||||
[ -d "$path" ] && return 0
|
[ -d "$path" ] && return 0
|
||||||
! mkdir -p "$path" && return 1
|
mkdir -p "$path" || return 1
|
||||||
|
|
||||||
dir=$(dirname -- "$path")
|
dir=$(dirname -- "$path")
|
||||||
|
|
||||||
@@ -272,6 +313,64 @@ makeDir() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
finiteMemoryLimit() {
|
||||||
|
|
||||||
|
local limit="$1"
|
||||||
|
# cgroup v1 commonly reports this enormous sentinel for an unlimited memory
|
||||||
|
# limit; compare as decimal strings to avoid shell integer overflow.
|
||||||
|
local sentinel="4611686018427387904"
|
||||||
|
local i
|
||||||
|
|
||||||
|
[[ "$limit" =~ ^[0-9]+$ ]] || return 1
|
||||||
|
|
||||||
|
(( ${#limit} < ${#sentinel} )) && return 0
|
||||||
|
(( ${#limit} > ${#sentinel} )) && return 1
|
||||||
|
|
||||||
|
for (( i=0; i<${#sentinel}; i++ )); do
|
||||||
|
local left="${limit:i:1}"
|
||||||
|
local right="${sentinel:i:1}"
|
||||||
|
|
||||||
|
(( left < right )) && return 0
|
||||||
|
(( left > right )) && return 1
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
getMemoryInfo() {
|
||||||
|
|
||||||
|
local host_total
|
||||||
|
local host_avail
|
||||||
|
local limit=""
|
||||||
|
local current=""
|
||||||
|
|
||||||
|
host_total=$(free -b | awk '/^Mem:/ {print $2; exit}')
|
||||||
|
host_avail=$(free -b | awk '/^Mem:/ {print $7; exit}')
|
||||||
|
|
||||||
|
RAM_TOTAL="$host_total"
|
||||||
|
RAM_AVAIL="$host_avail"
|
||||||
|
|
||||||
|
if [ -r /sys/fs/cgroup/memory.max ] && [ -r /sys/fs/cgroup/memory.current ]; then
|
||||||
|
limit=$(< /sys/fs/cgroup/memory.max)
|
||||||
|
current=$(< /sys/fs/cgroup/memory.current)
|
||||||
|
elif [ -r /sys/fs/cgroup/memory/memory.limit_in_bytes ] && [ -r /sys/fs/cgroup/memory/memory.usage_in_bytes ]; then
|
||||||
|
limit=$(< /sys/fs/cgroup/memory/memory.limit_in_bytes)
|
||||||
|
current=$(< /sys/fs/cgroup/memory/memory.usage_in_bytes)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use the tighter of host availability and the container's remaining cgroup
|
||||||
|
# allowance so RAM sizing cannot exceed either boundary.
|
||||||
|
if finiteMemoryLimit "$limit" && [[ "$current" =~ ^[0-9]+$ ]]; then
|
||||||
|
(( limit < RAM_TOTAL )) && RAM_TOTAL="$limit"
|
||||||
|
|
||||||
|
local available=$(( limit - current ))
|
||||||
|
(( available < 0 )) && available=0
|
||||||
|
(( available < RAM_AVAIL )) && RAM_AVAIL="$available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
stateFile() {
|
stateFile() {
|
||||||
|
|
||||||
local name="$1"
|
local name="$1"
|
||||||
@@ -304,6 +403,8 @@ writeAtomic() {
|
|||||||
|
|
||||||
local path="$1"
|
local path="$1"
|
||||||
local content="$2"
|
local content="$2"
|
||||||
|
# Use a per-process temporary file and rename so readers see either the old
|
||||||
|
# complete value or the new complete value.
|
||||||
local tmp="${path}.${BASHPID}.tmp"
|
local tmp="${path}.${BASHPID}.tmp"
|
||||||
|
|
||||||
if ! printf '%s\n' "$content" > "$tmp"; then
|
if ! printf '%s\n' "$content" > "$tmp"; then
|
||||||
@@ -368,6 +469,8 @@ restoreState() {
|
|||||||
local prefix="${4:-$PROCESS}"
|
local prefix="${4:-$PROCESS}"
|
||||||
local value
|
local value
|
||||||
|
|
||||||
|
# Persistent state fills only unset variables unless force is requested,
|
||||||
|
# preserving explicit environment overrides.
|
||||||
if ! enabled "$force"; then
|
if ! enabled "$force"; then
|
||||||
[ -z "${!var:-}" ] || return 0
|
[ -z "${!var:-}" ] || return 0
|
||||||
fi
|
fi
|
||||||
@@ -381,8 +484,7 @@ restoreState() {
|
|||||||
|
|
||||||
escape () {
|
escape () {
|
||||||
|
|
||||||
local s
|
local s=${1//&/\&}
|
||||||
s=${1//&/\&}
|
|
||||||
s=${s//</\<}
|
s=${s//</\<}
|
||||||
s=${s//>/\>}
|
s=${s//>/\>}
|
||||||
s=${s//'"'/\"}
|
s=${s//'"'/\"}
|
||||||
@@ -408,7 +510,7 @@ html() {
|
|||||||
|
|
||||||
local title
|
local title
|
||||||
local body
|
local body
|
||||||
local script
|
local script="${2:-}"
|
||||||
local footer
|
local footer
|
||||||
|
|
||||||
title=$(escape "$APP")
|
title=$(escape "$APP")
|
||||||
@@ -420,8 +522,6 @@ html() {
|
|||||||
body="<p class=\"loading\">${body/.../}</p>"
|
body="<p class=\"loading\">${body/.../}</p>"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[ -n "${2:-}" ] && script="$2" || script=""
|
|
||||||
|
|
||||||
local HTML
|
local HTML
|
||||||
HTML=$(<"$TEMPLATE")
|
HTML=$(<"$TEMPLATE")
|
||||||
HTML="${HTML/\[1\]/$title}"
|
HTML="${HTML/\[1\]/$title}"
|
||||||
@@ -430,6 +530,8 @@ html() {
|
|||||||
HTML="${HTML/\[4\]/$footer}"
|
HTML="${HTML/\[4\]/$footer}"
|
||||||
HTML="${HTML/\[5\]/$FOOTER2}"
|
HTML="${HTML/\[5\]/$FOOTER2}"
|
||||||
|
|
||||||
|
# Publish both the full page and websocket fragment atomically because nginx
|
||||||
|
# and websocketd may read them concurrently.
|
||||||
writeAtomic "$PAGE" "$HTML" || return 1
|
writeAtomic "$PAGE" "$HTML" || return 1
|
||||||
writeAtomic "$INFO" "$body" || return 1
|
writeAtomic "$INFO" "$body" || return 1
|
||||||
|
|
||||||
@@ -478,9 +580,9 @@ getCountry() {
|
|||||||
|
|
||||||
local url=$1
|
local url=$1
|
||||||
local query=$2
|
local query=$2
|
||||||
local rc json result
|
local json result
|
||||||
|
|
||||||
{ json=$(curl -m 5 -H "Accept: application/json" -sfk "$url"); rc=$?; } || :
|
{ json=$(curl -m 5 -H "Accept: application/json" -sfk "$url"); local rc=$?; } || :
|
||||||
(( rc != 0 )) && return 0
|
(( rc != 0 )) && return 0
|
||||||
|
|
||||||
{ result=$(echo "$json" | jq -r "$query" 2> /dev/null); rc=$?; } || :
|
{ result=$(echo "$json" | jq -r "$query" 2> /dev/null); rc=$?; } || :
|
||||||
@@ -503,6 +605,8 @@ setCountry() {
|
|||||||
[[ "${TZ,,}" == "asia/shanghai" ]] && COUNTRY="CN"
|
[[ "${TZ,,}" == "asia/shanghai" ]] && COUNTRY="CN"
|
||||||
[[ "${TZ,,}" == "asia/chongqing" ]] && COUNTRY="CN"
|
[[ "${TZ,,}" == "asia/chongqing" ]] && COUNTRY="CN"
|
||||||
|
|
||||||
|
# Country detection is best-effort and tries independent services in order;
|
||||||
|
# failure leaves mirror selection at its global default.
|
||||||
[ -z "$COUNTRY" ] && getCountry "https://api.ipapi.is" ".location.country_code"
|
[ -z "$COUNTRY" ] && getCountry "https://api.ipapi.is" ".location.country_code"
|
||||||
[ -z "$COUNTRY" ] && getCountry "https://ifconfig.co/json" ".country_iso"
|
[ -z "$COUNTRY" ] && getCountry "https://ifconfig.co/json" ".country_iso"
|
||||||
[ -z "$COUNTRY" ] && getCountry "https://api.ip2location.io" ".country_code"
|
[ -z "$COUNTRY" ] && getCountry "https://api.ip2location.io" ".country_code"
|
||||||
@@ -527,6 +631,8 @@ addPackage() {
|
|||||||
|
|
||||||
[ -z "$COUNTRY" ] && setCountry
|
[ -z "$COUNTRY" ] && setCountry
|
||||||
|
|
||||||
|
# Use a mainland mirror only for on-demand package installation, avoiding
|
||||||
|
# slow or inaccessible Debian endpoints in that region.
|
||||||
if [[ "${COUNTRY^^}" == "CN" ]]; then
|
if [[ "${COUNTRY^^}" == "CN" ]]; then
|
||||||
sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources
|
sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources
|
||||||
fi
|
fi
|
||||||
|
|||||||
+1
-1
@@ -39,6 +39,6 @@ server {
|
|||||||
proxy_read_timeout 3600s;
|
proxy_read_timeout 3600s;
|
||||||
proxy_send_timeout 3600s;
|
proxy_send_timeout 3600s;
|
||||||
|
|
||||||
proxy_pass http://127.0.0.1:8004/;
|
proxy_pass http://unix:/run/shm/status-ws.sock:/;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-3
@@ -71,6 +71,13 @@ function processInfo() {
|
|||||||
var response = request;
|
var response = request;
|
||||||
request = null;
|
request = null;
|
||||||
|
|
||||||
|
var status = response.status;
|
||||||
|
|
||||||
|
if (status == 502 || status == 503 || status == 504) {
|
||||||
|
schedule();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
var msg = response.responseText;
|
var msg = response.responseText;
|
||||||
if (msg == null || msg.length == 0) {
|
if (msg == null || msg.length == 0) {
|
||||||
|
|
||||||
@@ -83,9 +90,9 @@ function processInfo() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var notFound = (response.status == 404);
|
var notFound = (status == 404);
|
||||||
|
|
||||||
if (response.status == 200) {
|
if (status == 200) {
|
||||||
if (msg.toLowerCase().indexOf("<html>") !== -1) {
|
if (msg.toLowerCase().indexOf("<html>") !== -1) {
|
||||||
notFound = true;
|
notFound = true;
|
||||||
} else {
|
} else {
|
||||||
@@ -103,7 +110,7 @@ function processInfo() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
setError("Error: Received statuscode " + response.status);
|
setError("Error: Received statuscode " + status);
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user