mirror of
https://github.com/vdsm/virtual-dsm.git
synced 2026-08-28 11:56:03 +00:00
build: Switched to QEMU base image (#1363)
This commit is contained in:
@@ -30,7 +30,7 @@ jobs:
|
||||
uses: hadolint/hadolint-action@v3.4.0
|
||||
with:
|
||||
dockerfile: Dockerfile
|
||||
ignore: DL3008
|
||||
ignore: DL3008,DL3067
|
||||
failure-threshold: warning
|
||||
-
|
||||
name: Validate JSON and YML files
|
||||
|
||||
+5
-42
@@ -1,15 +1,12 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM qemux/qemu-host:2.06 AS builder
|
||||
FROM debian:trixie-slim
|
||||
FROM qemux/qemu-host:2.06 AS host
|
||||
FROM scratch
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG TARGETPLATFORM
|
||||
COPY --from=qemux/qemu:7.45 / /
|
||||
|
||||
ARG VERSION_ARG="0.0"
|
||||
ARG VERSION_WSD="0.4.2"
|
||||
ARG VERSION_CSTRUCT="4.7"
|
||||
ARG VERSION_PASST="2026_07_28"
|
||||
|
||||
ARG DEBCONF_NOWARNINGS="yes"
|
||||
ARG DEBIAN_FRONTEND="noninteractive"
|
||||
@@ -20,44 +17,10 @@ RUN <<EOF
|
||||
|
||||
apt-get update
|
||||
apt-get --no-install-recommends -y install \
|
||||
jq \
|
||||
tini \
|
||||
curl \
|
||||
wget \
|
||||
fdisk \
|
||||
unzip \
|
||||
nginx \
|
||||
procps \
|
||||
ipcalc \
|
||||
ethtool \
|
||||
xz-utils \
|
||||
iptables \
|
||||
iproute2 \
|
||||
dnsmasq \
|
||||
fakeroot \
|
||||
apt-utils \
|
||||
net-tools \
|
||||
e2fsprogs \
|
||||
diffutils \
|
||||
qemu-utils \
|
||||
iputils-ping \
|
||||
inotify-tools \
|
||||
ca-certificates \
|
||||
netcat-openbsd \
|
||||
qemu-system-x86 \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-msgpack \
|
||||
python3-pysodium
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
# Install Python dependencies
|
||||
@@ -71,12 +34,12 @@ EOF
|
||||
|
||||
COPY --chmod=755 ./src /run/
|
||||
COPY --chmod=755 ./web /var/www/
|
||||
COPY --chmod=755 --from=builder /qemu-host.bin /run/host.bin
|
||||
COPY --chmod=755 --from=host /qemu-host.bin /run/host.bin
|
||||
COPY --chmod=744 ./web/conf/nginx.conf /etc/nginx/default.conf
|
||||
ADD --chmod=775 https://raw.githubusercontent.com/sud0woodo/patology/refs/heads/main/patology.py /run/extract.py
|
||||
|
||||
VOLUME /storage
|
||||
EXPOSE 22 139 445 5000
|
||||
EXPOSE 445 5000
|
||||
|
||||
ENV RAM_SIZE="2G"
|
||||
ENV CPU_CORES="2"
|
||||
|
||||
+17
-5
@@ -56,7 +56,7 @@ An empty default means the variable is unset and its value is determined automat
|
||||
| `DEV` | `eth0` | Container network interface used as the uplink. |
|
||||
| `MTU` | | MTU assigned to the guest network interface. |
|
||||
| `MASK` | `255.255.255.0` | IPv4 netmask for guest network. |
|
||||
| `TAP` | `dsm` | TAP or macvtap interface name. |
|
||||
| `TAP` | `qemu` | TAP or macvtap interface name. |
|
||||
| `BRIDGE` | `docker` | Bridge name used for NAT networking. |
|
||||
| `HOST_PORTS` | | Ports excluded from guest forwarding. |
|
||||
| `USER_PORTS` | | Additional ports to forward to DSM when using user-mode networking. |
|
||||
@@ -76,19 +76,30 @@ An empty default means the variable is unset and its value is determined automat
|
||||
| `GPU` | `N` | Enables Intel iGPU acceleration. |
|
||||
| `RENDERNODE` | `/dev/dri/renderD128` | Render node used for GPU acceleration. |
|
||||
|
||||
## ⚙️ System
|
||||
## 🎈 Memory Ballooning
|
||||
|
||||
Also see [Dynamic memory allocation](https://github.com/qemus/qemu/blob/master/docs/ballooning.md) for usage instructions and important caveats.
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `MACHINE` | `q35` | QEMU machine type. |
|
||||
| `ARGUMENTS` | | Additional raw arguments appended to the QEMU command line. |
|
||||
| `BALLOONING` | `N` | Enables dynamic memory ballooning. |
|
||||
| `BALLOONING_MIN_MEM` | `33%` | Minimum amount of memory retained by the VM. |
|
||||
| `BALLOONING_RAM_THRESHOLD` | `80.0` | Host RAM usage percentage at which ballooning begins adjusting memory. |
|
||||
| `BALLOONING_RAM_THRESHOLD_HARD` | `90.0` | Host RAM usage percentage at which ballooning becomes more aggressive. |
|
||||
| `BALLOONING_PSI_PRESSURE` | `10.0` | PSI memory pressure level at which ballooning becomes more aggressive. |
|
||||
| `BALLOONING_PSI_PRESSURE_MAX` | `50.0` | PSI memory pressure level at which ballooning reaches its strongest response. |
|
||||
| `BALLOONING_HYSTERESIS` | `128M` | Minimum memory change before the balloon target is updated. |
|
||||
| `BALLOONING_KP` | `0.5` | Proportional gain used by the ballooning controller. |
|
||||
| `BALLOONING_KI` | `0.05` | Integral gain used by the ballooning controller. |
|
||||
| `BALLOONING_INTERVAL` | `5` | Polling interval in seconds. |
|
||||
| `BALLOONING_DEBUG` | `N` | Enables debug output for the ballooning monitor. |
|
||||
|
||||
## 🔌 Shutdown
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `SHUTDOWN` | `Y` | Enables graceful shutdown. |
|
||||
| `TIMEOUT` | `115` | Maximum time, in seconds, to wait before forcing DSM to stop. |
|
||||
| `TIMEOUT` | `105` | Maximum time, in seconds, to wait before forcing DSM to stop. |
|
||||
| `API_TIMEOUT` | `90` | Maximum time, in seconds, to wait for the shutdown API call. |
|
||||
|
||||
## 🐞 Debugging
|
||||
@@ -100,3 +111,4 @@ An empty default means the variable is unset and its value is determined automat
|
||||
| `HOST_DEBUG` | `N` | Enables debug output for the DSM host helper. |
|
||||
| `MONITOR` | | QEMU monitor configuration. |
|
||||
| `QMP` | | QEMU Machine Protocol configuration. |
|
||||
| `ARGUMENTS` | | Additional raw arguments appended to the QEMU command line. |
|
||||
|
||||
@@ -22,6 +22,7 @@ Virtual DSM in a Docker container.
|
||||
- Near-native performance with KVM acceleration
|
||||
- Customizable CPU, memory, and storage allocation
|
||||
- Supports multiple disks and physical disk passthrough
|
||||
- Dynamic memory allocation with memory ballooning
|
||||
- Supports NAT, user-mode, macvlan, and macvtap networking
|
||||
|
||||
## Usage 🐳
|
||||
@@ -214,6 +215,12 @@ kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/m
|
||||
> [!NOTE]
|
||||
> This can be used to enable the facial recognition function in Synology Photos, but does not provide hardware transcoding for video.
|
||||
|
||||
### How do I enable dynamic memory allocation?
|
||||
|
||||
By default, the DSM is allocated the full amount of RAM configured via `RAM_SIZE` for its entire lifetime.
|
||||
|
||||
However, you can enable [memory ballooning](https://github.com/qemus/qemu/blob/master/docs/ballooning.md) if you want the container to dynamically reclaim unused guest RAM based on host memory pressure.
|
||||
|
||||
### How do I install a specific version of vDSM?
|
||||
|
||||
By default, version 7.2 will be installed, but if you prefer an older version, you can add the download URL of the `.pat` file to your compose file as follows:
|
||||
|
||||
+7
-3
@@ -8,20 +8,24 @@ cd /run
|
||||
: "${NETWORK:="Y"}"
|
||||
|
||||
[ -f "/run/shm/qemu.end" ] && echo "QEMU is shutting down..." && exit 1
|
||||
|
||||
# Treat the startup window as healthy so container health checks do not restart
|
||||
# the service before QEMU has had time to publish its PID.
|
||||
[ ! -s "/run/shm/qemu.pid" ] && echo "QEMU is not running yet..." && exit 0
|
||||
|
||||
disabled "$NETWORK" && echo "Networking is disabled." && exit 0
|
||||
|
||||
file="/run/shm/dsm.url"
|
||||
address="/run/shm/qemu.ip"
|
||||
address="/run/shm/qemu.host"
|
||||
gateway="/run/shm/qemu.gw"
|
||||
|
||||
# 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
|
||||
[ ! -s "$file" ] && echo "DSM has not enabled networking yet..." && exit 0
|
||||
|
||||
location=$(<"$file")
|
||||
|
||||
if ! curl -m 20 -ILfSs "http://$location/" > /dev/null; then
|
||||
if ! curl -m 20 -LfSs -o /dev/null "http://$location/"; 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.
|
||||
|
||||
+20
-3
@@ -2,6 +2,7 @@
|
||||
set -Eeuo pipefail
|
||||
|
||||
: "${QMP:=""}"
|
||||
: "${UUID:=""}"
|
||||
: "${MONITOR:=""}"
|
||||
|
||||
DEF_OPTS="-nodefaults -boot strict=on"
|
||||
@@ -39,11 +40,18 @@ configureMonitor() {
|
||||
|
||||
configureMachine() {
|
||||
|
||||
local smm="off"
|
||||
enabled "${SMM:-}" && smm="on"
|
||||
|
||||
# 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=$smm,usb=off"
|
||||
MAC_OPTS+=",vmport=off,dump-guest-core=off,hpet=off${KVM_OPTS}"
|
||||
|
||||
UUID=$(strip "$UUID")
|
||||
[ -n "$UUID" ] && MAC_OPTS+=" -uuid $UUID"
|
||||
[ -n "${SM_BIOS:-}" ] && MAC_OPTS+=" $SM_BIOS"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -52,7 +60,15 @@ configureVirtioDevices() {
|
||||
local bus
|
||||
bus=$(getPciBus)
|
||||
|
||||
DEV_OPTS="-device virtio-balloon-pci,id=balloon0,bus=$bus,addr=0x4"
|
||||
# Keep the existing balloon device by default. When dynamic ballooning is
|
||||
# enabled, expose guest statistics and a dedicated QMP control socket.
|
||||
if ! enabled "${BALLOONING:-}"; then
|
||||
DEV_OPTS="-device virtio-balloon-pci,id=balloon0,bus=$bus,addr=0x4"
|
||||
else
|
||||
MON_OPTS+=" -qmp unix:${BALLOONING_SOCKET},server=on,wait=off"
|
||||
DEV_OPTS="-device virtio-balloon-pci,free-page-reporting=on,guest-stats-polling-interval=1,id=balloon0,bus=$bus,addr=0x4"
|
||||
fi
|
||||
|
||||
DEV_OPTS+=" -object rng-random,id=objrng0,filename=/dev/urandom"
|
||||
DEV_OPTS+=" -device virtio-rng-pci,rng=objrng0,id=rng0,bus=$bus,addr=0x1c"
|
||||
|
||||
@@ -61,7 +77,8 @@ configureVirtioDevices() {
|
||||
|
||||
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 $BOOT_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 ' ')
|
||||
|
||||
-855
@@ -1,855 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
# Docker environment variables
|
||||
|
||||
: "${DISK_IO:="native"}" # I/O Mode, can be set to 'native', 'threads' or 'io_uring'
|
||||
: "${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_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_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
|
||||
|
||||
# Sanitize all variables
|
||||
DISK_IO=$(strip "$DISK_IO")
|
||||
DISK_FMT=$(strip "$DISK_FMT")
|
||||
DISK_TYPE=$(strip "$DISK_TYPE")
|
||||
DISK_FLAGS=$(strip "$DISK_FLAGS")
|
||||
DISK_OPTIONS=$(strip "$DISK_OPTIONS")
|
||||
DISK_CACHE=$(strip "$DISK_CACHE")
|
||||
DISK_DISCARD=$(strip "$DISK_DISCARD")
|
||||
DISK_ROTATION=$(strip "$DISK_ROTATION")
|
||||
|
||||
BOOT="$STORAGE/$BASE.boot.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 "$SYSTEM" ] && error "Virtual DSM system-image does not exist ($SYSTEM)" && exit 82
|
||||
|
||||
if ! setOwner "$BOOT"; then
|
||||
warn "failed to set the owner for \"$BOOT\" !"
|
||||
fi
|
||||
|
||||
if ! setOwner "$SYSTEM"; then
|
||||
warn "failed to set the owner for \"$SYSTEM\" !"
|
||||
fi
|
||||
|
||||
fmt2ext() {
|
||||
local diskFmt="$1"
|
||||
|
||||
case "${diskFmt,,}" in
|
||||
qcow2) echo "qcow2" ;;
|
||||
raw) echo "img" ;;
|
||||
*) error "Unrecognized disk format: $diskFmt" && exit 78 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
ext2fmt() {
|
||||
local diskExt="$1"
|
||||
|
||||
case "${diskExt,,}" in
|
||||
qcow2) echo "qcow2" ;;
|
||||
img) echo "raw" ;;
|
||||
*) error "Unrecognized file extension: .$diskExt" && exit 78 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
getSize() {
|
||||
|
||||
local diskFile="$1"
|
||||
local diskExt diskFmt size
|
||||
|
||||
diskExt=$(echo "${diskFile//*./}" | sed 's/^.*\.//')
|
||||
diskFmt=$(ext2fmt "$diskExt")
|
||||
|
||||
case "${diskFmt,,}" in
|
||||
raw)
|
||||
stat -c%s "$diskFile"
|
||||
;;
|
||||
|
||||
qcow2)
|
||||
size=$(qemu-img info --output=json -f "$diskFmt" "$diskFile" | jq -r '."virtual-size" // empty')
|
||||
if [[ ! "$size" =~ ^[0-9]+$ ]]; then
|
||||
error "Failed to determine virtual size of $diskFile"
|
||||
exit 78
|
||||
fi
|
||||
echo "$size"
|
||||
;;
|
||||
|
||||
*)
|
||||
error "Unrecognized disk format: $diskFmt"
|
||||
exit 78
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
isCow() {
|
||||
local fs="$1"
|
||||
|
||||
if [[ "${fs,,}" == "btrfs" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
supportsDirect() {
|
||||
local fs="$1"
|
||||
|
||||
if [[ "${fs,,}" == "ecryptfs" || "${fs,,}" == "tmpfs" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
validDiskType() {
|
||||
|
||||
case "${1,,}" in
|
||||
"ide" | "sata" | "nvme" | "usb" | "scsi" | "blk" | \
|
||||
"virtio-blk" | "virtio-scsi" | "auto" | "none" )
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
allocateRaw() {
|
||||
|
||||
local diskFile="$1"
|
||||
local dataSize="$2"
|
||||
|
||||
if disabled "$ALLOCATE"; then
|
||||
truncate -s "$dataSize" "$diskFile"
|
||||
return $?
|
||||
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 -x "$dataSize" "$diskFile" && return 0
|
||||
truncate -s "$dataSize" "$diskFile" || return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
getDiskOptions() {
|
||||
|
||||
local fs="$1"
|
||||
local diskFmt="$2"
|
||||
local diskParam="$DISK_ALLOC"
|
||||
|
||||
isCow "$fs" && diskParam+=",nocow=on"
|
||||
|
||||
if [[ "${diskFmt,,}" != "raw" ]]; then
|
||||
[ -n "$DISK_FLAGS" ] && diskParam+=",$DISK_FLAGS"
|
||||
fi
|
||||
|
||||
echo "$diskParam"
|
||||
return 0
|
||||
}
|
||||
|
||||
normalizeSize() {
|
||||
|
||||
local diskSpace="$1"
|
||||
local diskDesc="$2"
|
||||
local dir="$3"
|
||||
|
||||
local free dataSize
|
||||
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
|
||||
|
||||
free=$(df --output=avail -B 1 "$dir" | tail -n 1)
|
||||
|
||||
if [[ "${diskSpace,,}" == "max" ]]; then
|
||||
free=$(( free - spare ))
|
||||
else
|
||||
free=$(( free / 2 ))
|
||||
fi
|
||||
|
||||
(( free < spare )) && free="$spare"
|
||||
local gb=$(( free / 1073741825 ))
|
||||
diskSpace="${gb}G"
|
||||
|
||||
fi
|
||||
|
||||
local space="${diskSpace// /}"
|
||||
[ -z "$space" ] && space="256G"
|
||||
[ -z "${space//[0-9. ]}" ] && space="${space}G"
|
||||
space=$(echo "${space^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
|
||||
|
||||
if ! numfmt --from=iec "$space" &>/dev/null; then
|
||||
error "Invalid value for ${diskDesc^^}_SIZE: $diskSpace" && exit 73
|
||||
fi
|
||||
|
||||
dataSize=$(numfmt --from=iec "$space")
|
||||
|
||||
if (( dataSize < 6442450944 )); then
|
||||
error "Please increase the ${diskDesc^^}_SIZE variable to at least 6 GB." && exit 73
|
||||
fi
|
||||
|
||||
echo "$space"
|
||||
return 0
|
||||
}
|
||||
|
||||
freeSpace() {
|
||||
|
||||
local path="$1"
|
||||
|
||||
local base
|
||||
base=$(baseDir "$path")
|
||||
|
||||
if ! available=$(df --output=avail -B 1 "$path" | tail -n 1); then
|
||||
error "Failed to check free space in $base."
|
||||
exit 76
|
||||
fi
|
||||
|
||||
if [[ ! "$available" =~ ^[0-9]+$ ]]; then
|
||||
error "Failed to check free space in $base."
|
||||
exit 76
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
createDisk() {
|
||||
|
||||
local diskFile="$1"
|
||||
local diskSpace="$2"
|
||||
local diskDesc="$3"
|
||||
local diskFmt="$4"
|
||||
local fs="$5"
|
||||
|
||||
local gb dir base
|
||||
local attributes available
|
||||
|
||||
rm -f "$diskFile"
|
||||
|
||||
local dataSize
|
||||
dataSize=$(numfmt --from=iec "$diskSpace")
|
||||
|
||||
if ! disabled "$ALLOCATE"; then
|
||||
|
||||
# Check free diskspace
|
||||
dir=$(dirname "$diskFile")
|
||||
base=$(baseDir "$dir")
|
||||
|
||||
freeSpace "$dir"
|
||||
|
||||
if (( dataSize > available )); then
|
||||
gb=$(formatBytes "$available")
|
||||
error "Not enough free space to create a $diskDesc of ${diskSpace/G/ GB} in $base, it has only $gb available..."
|
||||
error "Please specify a smaller ${diskDesc^^}_SIZE or disable preallocation by setting ALLOCATE=N." && exit 76
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
html "Creating a $diskDesc image..."
|
||||
info "Creating a ${diskSpace/G/ GB} $DISK_STYLE $diskDesc image in $diskFmt format..."
|
||||
|
||||
local failure="Could not create a $DISK_STYLE $diskFmt $diskDesc image of ${diskSpace/G/ GB} ($diskFile)"
|
||||
|
||||
case "${diskFmt,,}" in
|
||||
raw)
|
||||
|
||||
if isCow "$fs"; then
|
||||
if ! touch "$diskFile"; then
|
||||
error "$failure" && exit 77
|
||||
fi
|
||||
{ chattr +C "$diskFile"; } || :
|
||||
fi
|
||||
|
||||
if ! allocateRaw "$diskFile" "$dataSize"; then
|
||||
rm -f "$diskFile"
|
||||
error "$failure" && exit 77
|
||||
fi
|
||||
;;
|
||||
qcow2)
|
||||
|
||||
local diskParam
|
||||
diskParam=$(getDiskOptions "$fs" "$diskFmt")
|
||||
|
||||
if ! qemu-img create -f "$diskFmt" -o "$diskParam" -- "$diskFile" "$dataSize" ; then
|
||||
rm -f "$diskFile"
|
||||
error "$failure" && exit 70
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if isCow "$fs"; then
|
||||
attributes=$(lsattr "$diskFile")
|
||||
if [[ "$attributes" != *"C"* ]]; then
|
||||
error "Failed to disable COW for $diskDesc image $diskFile on ${fs^^} filesystem (returned $attributes)"
|
||||
fi
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
resizeDisk() {
|
||||
|
||||
local diskFile="$1"
|
||||
local diskSpace="$2"
|
||||
local diskDesc="$3"
|
||||
local diskFmt="$4"
|
||||
local fs="$5"
|
||||
|
||||
local gb dir base dataSize
|
||||
local available currentSize
|
||||
|
||||
currentSize=$(getSize "$diskFile") || exit 71
|
||||
dataSize=$(numfmt --from=iec "$diskSpace")
|
||||
local required=$(( dataSize - currentSize ))
|
||||
(( required < 1 )) && error "Shrinking disks is not supported yet, please increase ${diskDesc^^}_SIZE." && exit 71
|
||||
|
||||
if ! disabled "$ALLOCATE"; then
|
||||
|
||||
# Check free diskspace
|
||||
dir=$(dirname "$diskFile")
|
||||
base=$(baseDir "$dir")
|
||||
|
||||
freeSpace "$dir"
|
||||
|
||||
if (( required > available )); then
|
||||
gb=$(formatBytes "$available")
|
||||
error "Not enough free space to resize $diskDesc to ${diskSpace/G/ GB} in $base, it has only $gb available.."
|
||||
error "Please specify a smaller ${diskDesc^^}_SIZE or disable preallocation by setting ALLOCATE=N." && exit 74
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
gb=$(formatBytes "$currentSize")
|
||||
MSG="Resizing $diskDesc from $gb to ${diskSpace/G/ GB}..."
|
||||
info "$MSG" && html "$MSG"
|
||||
|
||||
local failure="Could not resize the $DISK_STYLE $diskFmt $diskDesc image from ${gb} to ${diskSpace/G/ GB} ($diskFile)"
|
||||
|
||||
case "${diskFmt,,}" in
|
||||
raw)
|
||||
|
||||
if ! allocateRaw "$diskFile" "$dataSize"; then
|
||||
error "$failure" && exit 75
|
||||
fi
|
||||
;;
|
||||
qcow2)
|
||||
|
||||
if ! qemu-img resize -f "$diskFmt" "--$DISK_ALLOC" "$diskFile" "$dataSize" ; then
|
||||
error "$failure" && exit 72
|
||||
fi
|
||||
|
||||
;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
convertDisk() {
|
||||
|
||||
local sourceFile="$1"
|
||||
local sourceFmt="$2"
|
||||
local destinationFile="$3"
|
||||
local destinationFmt="$4"
|
||||
local diskBase="$5"
|
||||
local diskDesc="$6"
|
||||
local fs="$7"
|
||||
local tmpFile="$diskBase.tmp"
|
||||
|
||||
local gb dir base attributes
|
||||
local available currentSize
|
||||
|
||||
[ -f "$destinationFile" ] && error "Conversion failed, destination file $destinationFile already exists?" && exit 79
|
||||
[ ! -f "$sourceFile" ] && error "Conversion failed, source file $sourceFile does not exist?" && exit 79
|
||||
|
||||
rm -f "$tmpFile"
|
||||
|
||||
dir=$(dirname "$tmpFile")
|
||||
base=$(baseDir "$dir")
|
||||
|
||||
if ! disabled "$ALLOCATE"; then
|
||||
|
||||
# Check free diskspace
|
||||
currentSize=$(getSize "$sourceFile") || exit 79
|
||||
|
||||
freeSpace "$dir"
|
||||
|
||||
if (( currentSize > available )); then
|
||||
gb=$(formatBytes "$available")
|
||||
error "Not enough free space to convert $diskDesc to $destinationFmt in $base, it has only $gb available..."
|
||||
error "Please free up some disk space or disable preallocation by setting ALLOCATE=N." && exit 76
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
local msg="Converting $diskDesc to $destinationFmt"
|
||||
html "$msg..."
|
||||
info "$msg, please wait until completed..."
|
||||
|
||||
local convertFlags="-p"
|
||||
local diskParam
|
||||
diskParam=$(getDiskOptions "$fs" "$destinationFmt")
|
||||
|
||||
if [[ "$destinationFmt" != "raw" ]]; then
|
||||
if disabled "$ALLOCATE"; then
|
||||
convertFlags+=" -c"
|
||||
fi
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
if ! qemu-img convert -f "$sourceFmt" $convertFlags -o "$diskParam" -O "$destinationFmt" -- "$sourceFile" "$tmpFile"; then
|
||||
rm -f "$tmpFile"
|
||||
error "Failed to convert $DISK_STYLE $diskDesc image to $destinationFmt format in $base, is there enough space available?" && exit 79
|
||||
fi
|
||||
|
||||
if [[ "$destinationFmt" == "raw" ]]; 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
|
||||
if ! currentSize=$(stat -c%s "$tmpFile"); then
|
||||
error "Failed to determine converted image size: $tmpFile"
|
||||
exit 79
|
||||
fi
|
||||
|
||||
if ! fallocate -l "$currentSize" "$tmpFile" &>/dev/null; then
|
||||
if ! fallocate -l -x "$currentSize" "$tmpFile"; then
|
||||
error "Failed to allocate $currentSize bytes for $diskDesc image $tmpFile"
|
||||
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
|
||||
error "Failed to move converted $diskDesc image to $destinationFile."
|
||||
exit 79
|
||||
fi
|
||||
|
||||
if ! rm -f "$sourceFile"; then
|
||||
error "Failed to remove old $diskDesc image $sourceFile."
|
||||
exit 79
|
||||
fi
|
||||
|
||||
if isCow "$fs"; then
|
||||
attributes=$(lsattr "$destinationFile")
|
||||
if [[ "$attributes" != *"C"* ]]; then
|
||||
error "Failed to disable COW for $diskDesc image $destinationFile on ${fs^^} filesystem (returned $attributes)"
|
||||
fi
|
||||
fi
|
||||
|
||||
msg="Conversion of $diskDesc"
|
||||
info "$msg to $destinationFmt completed successfully!"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
checkFS () {
|
||||
|
||||
local fs="$1"
|
||||
local diskFile="$2"
|
||||
local diskDesc="$3"
|
||||
|
||||
local dir base
|
||||
local attributes
|
||||
|
||||
dir=$(dirname "$diskFile")
|
||||
base=$(baseDir "$dir")
|
||||
[ ! -d "$dir" ] && return 0
|
||||
|
||||
if [[ "${fs,,}" == "overlay"* && "${ENGINE,,}" == "docker" ]]; then
|
||||
warn "the filesystem of $base is OverlayFS, this usually means it was binded to an invalid path!"
|
||||
fi
|
||||
|
||||
if [[ "${fs,,}" == "fuse"* ]]; then
|
||||
warn "the filesystem of $base is FUSE, this extra layer will negatively affect performance!"
|
||||
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
|
||||
warn "the filesystem of $base is $fs, which does not support O_DIRECT mode, adjusting settings..."
|
||||
fi
|
||||
|
||||
if isCow "$fs"; then
|
||||
if [ -f "$diskFile" ]; then
|
||||
attributes=$(lsattr "$diskFile")
|
||||
if [[ "$attributes" != *"C"* ]]; then
|
||||
warn "COW (copy on write) is not disabled for $diskDesc image file $diskFile, this is recommended on ${fs^^} filesystems!"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
createDevice () {
|
||||
|
||||
local diskFile="$1"
|
||||
local diskType="$2"
|
||||
local diskIndex="$3"
|
||||
local diskAddress="$4"
|
||||
local diskFmt="$5"
|
||||
local diskIo="$6"
|
||||
local diskCache="$7"
|
||||
local diskSerial="$8"
|
||||
local diskSectors="$9"
|
||||
|
||||
local bus
|
||||
bus=$(getPciBus)
|
||||
|
||||
local options=""
|
||||
[ -n "$DISK_OPTIONS" ] && options=",${DISK_OPTIONS#,}"
|
||||
|
||||
local bootIndex=""
|
||||
local diskId="data$diskIndex"
|
||||
[ -n "$diskIndex" ] && bootIndex=",bootindex=$diskIndex"
|
||||
local result=" -drive file=$diskFile,id=$diskId,format=$diskFmt,cache=$diskCache,aio=$diskIo,discard=$DISK_DISCARD,detect-zeroes=on"
|
||||
|
||||
case "${diskType,,}" in
|
||||
"none" ) ;;
|
||||
"auto" )
|
||||
echo "$result"
|
||||
;;
|
||||
"usb" )
|
||||
result+=",if=none \
|
||||
-device usb-storage,drive=${diskId}${bootIndex}${diskSerial}${diskSectors}${options}"
|
||||
echo "$result"
|
||||
;;
|
||||
"nvme" )
|
||||
result+=",if=none \
|
||||
-device nvme,drive=${diskId}${bootIndex},serial=deadbeaf${diskIndex}${diskSerial}${diskSectors}${options}"
|
||||
echo "$result"
|
||||
;;
|
||||
"ide" | "sata" )
|
||||
result+=",if=none \
|
||||
-device ich9-ahci,id=ahci${diskIndex},addr=$diskAddress \
|
||||
-device ide-hd,drive=${diskId},bus=ahci$diskIndex.0,rotation_rate=$DISK_ROTATION${bootIndex}${diskSerial}${diskSectors}${options}"
|
||||
echo "$result"
|
||||
;;
|
||||
"blk" | "virtio-blk" )
|
||||
result+=",if=none \
|
||||
-device virtio-blk-pci,drive=${diskId},bus=$bus,addr=$diskAddress,iothread=io2${bootIndex}${diskSerial}${diskSectors}${options}"
|
||||
echo "$result"
|
||||
;;
|
||||
"scsi" | "virtio-scsi" )
|
||||
result+=",if=none \
|
||||
-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}${options}"
|
||||
echo "$result"
|
||||
;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
finishDisks () {
|
||||
|
||||
case "${DISK_TYPE,,}" in
|
||||
"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" ;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
addDisk () {
|
||||
|
||||
local diskBase="$1"
|
||||
local diskType="$2"
|
||||
local diskDesc="$3"
|
||||
local diskSpace="$4"
|
||||
local diskIndex="$5"
|
||||
local diskAddress="$6"
|
||||
local diskFmt="$7"
|
||||
local diskIo="$8"
|
||||
local diskCache="$9"
|
||||
|
||||
local fs dir used space
|
||||
local diskExt dataSize
|
||||
local available currentSize
|
||||
local previousExt
|
||||
|
||||
diskExt=$(fmt2ext "$diskFmt")
|
||||
local diskFile="$diskBase.$diskExt"
|
||||
|
||||
dir=$(dirname "$diskFile")
|
||||
[ ! -d "$dir" ] && return 0
|
||||
|
||||
space=$(normalizeSize "$diskSpace" "$diskDesc" "$dir")
|
||||
dataSize=$(numfmt --from=iec "$space")
|
||||
|
||||
if ! fs=$(stat -f -c %T "$dir"); then
|
||||
error "Failed to determine filesystem type of \"$dir\" !"
|
||||
return 1
|
||||
fi
|
||||
|
||||
checkFS "$fs" "$diskFile" "$diskDesc" || exit $?
|
||||
|
||||
if ! supportsDirect "$fs"; then
|
||||
diskIo="threads"
|
||||
diskCache="writeback"
|
||||
fi
|
||||
|
||||
if [ ! -f "$diskFile" ] || [ ! -s "$diskFile" ]; then
|
||||
|
||||
if [[ "${diskFmt,,}" != "raw" ]]; then
|
||||
local previousFmt="raw"
|
||||
else
|
||||
local previousFmt="qcow2"
|
||||
fi
|
||||
|
||||
previousExt=$(fmt2ext "$previousFmt")
|
||||
|
||||
# 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 $?
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
if [ -f "$diskFile" ] && [ -s "$diskFile" ]; then
|
||||
|
||||
currentSize=$(getSize "$diskFile") || exit 71
|
||||
|
||||
if (( dataSize > currentSize )); then
|
||||
|
||||
resizeDisk "$diskFile" "$space" "$diskDesc" "$diskFmt" "$fs" || exit $?
|
||||
|
||||
else
|
||||
|
||||
if (( dataSize < currentSize )); then
|
||||
|
||||
if [[ "${diskSpace,,}" != "max" && "${diskSpace,,}" != "half" ]]; then
|
||||
info "You decreased the ${diskDesc^^}_SIZE variable to ${diskSpace/G/ GB} but shrinking disks is not supported, will be ignored..."
|
||||
fi
|
||||
|
||||
fi
|
||||
fi
|
||||
|
||||
else
|
||||
|
||||
createDisk "$diskFile" "$space" "$diskDesc" "$diskFmt" "$fs" || exit $?
|
||||
|
||||
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
|
||||
|
||||
currentSize=$(getSize "$diskFile") || exit 73
|
||||
used=$(du -sB 1 "$diskFile" | cut -f1)
|
||||
available=$(df --output=avail -B 1 "$dir" | tail -n 1)
|
||||
local missing=$(( currentSize - used - available ))
|
||||
(( missing < 0 )) && missing=0
|
||||
|
||||
if (( missing > 0 )); then
|
||||
|
||||
local gb base
|
||||
|
||||
gb=$(formatBytes "$available")
|
||||
base=$(baseDir "$dir")
|
||||
missing=$(formatBytes "$missing")
|
||||
currentSize=$(formatBytes "$currentSize")
|
||||
local msg="The virtual size of the ${diskDesc,,} is $currentSize"
|
||||
|
||||
if [ -n "$used" ] && [[ "$used" != "0" ]]; then
|
||||
used=$(formatBytes "$used")
|
||||
msg+=" (of which $used is used)"
|
||||
fi
|
||||
|
||||
info "$msg, but there is only $gb of free space remaining in $base now."
|
||||
info "Please consider making at least $missing more space available in $base for future expansions."
|
||||
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "$diskFile" ]; then
|
||||
if ! setOwner "$diskFile"; then
|
||||
warn "failed to set the owner for \"$diskFile\" !"
|
||||
fi
|
||||
fi
|
||||
|
||||
DISK_OPTS+=$(createDevice "$diskFile" "$diskType" "$diskIndex" "$diskAddress" "$diskFmt" "$diskIo" "$diskCache" "" "")
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
addDevice () {
|
||||
|
||||
local diskDev="$1"
|
||||
local diskType="$2"
|
||||
local diskIndex="$3"
|
||||
local diskAddress="$4"
|
||||
|
||||
local sectors=""
|
||||
local devType
|
||||
devType=$(lsblk -no TYPE "$diskDev" 2>/dev/null | head -n1)
|
||||
|
||||
[ -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
|
||||
|
||||
# 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.
|
||||
# Whole disk passthrough with explicit sector sizes causes DSM not to recognize the disk.
|
||||
if [[ "$devType" == "part" ]]; then
|
||||
|
||||
local result
|
||||
local logical="" physical=""
|
||||
|
||||
result=$(fdisk -l "$diskDev" 2>/dev/null | grep -m 1 -o "(logical/physical): .*" | cut -c 21- || true)
|
||||
|
||||
if [ -n "$result" ]; then
|
||||
logical="${result%% *}"
|
||||
physical=$(echo "$result" | grep -m 1 -o "/ .*" | cut -c 3- || true)
|
||||
physical="${physical%% *}"
|
||||
fi
|
||||
|
||||
if [ -z "$logical" ] || [ -z "$physical" ]; then
|
||||
warn "Failed to determine the sector size for $diskDev"
|
||||
elif [[ "$physical" != "512" ]]; then
|
||||
sectors=",logical_block_size=$logical,physical_block_size=$physical"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
DISK_OPTS+=$(createDevice "$diskDev" "$diskType" "$diskIndex" "$diskAddress" "raw" "$DISK_IO" "$DISK_CACHE" "" "$sectors")
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
[ -z "${DISK_OPTS:-}" ] && DISK_OPTS=""
|
||||
[ -z "${DISK_TYPE:-}" ] && DISK_TYPE="scsi"
|
||||
[ -z "${DISK_NAME:-}" ] && DISK_NAME="data"
|
||||
[ -z "${DISK_DISABLE:-}" ] && DISK_DISABLE=""
|
||||
|
||||
if ! enabled "$DISK_DISABLE"; then
|
||||
msg="Initializing disks..."
|
||||
enabled "$DEBUG" && echo "$msg"
|
||||
fi
|
||||
|
||||
if [[ "${DISK_IO,,}" == "native" && "${DISK_CACHE,,}" != "none" && "${DISK_CACHE,,}" != "directsync" ]]; then
|
||||
warn "DISK_IO=native requires direct I/O caching, using DISK_IO=threads with DISK_CACHE=$DISK_CACHE."
|
||||
DISK_IO="threads"
|
||||
fi
|
||||
|
||||
case "${DISK_DISCARD,,}" in
|
||||
"y" | "yes" | "true" | "1" | "on" | "unmap" )
|
||||
DISK_DISCARD="unmap" ;;
|
||||
|
||||
"n" | "no" | "false" | "0" | "off" | "ignore" )
|
||||
DISK_DISCARD="ignore" ;;
|
||||
|
||||
* )
|
||||
warn "Invalid DISK_DISCARD value '$DISK_DISCARD', using 'unmap'."
|
||||
DISK_DISCARD="unmap" ;;
|
||||
esac
|
||||
|
||||
if [[ ! "$DISK_ROTATION" =~ ^[0-9]+$ ]]; then
|
||||
warn "Invalid DISK_ROTATION value '$DISK_ROTATION', using 1."
|
||||
DISK_ROTATION="1"
|
||||
fi
|
||||
|
||||
DISK_FMT="${DISK_FMT,,}"
|
||||
|
||||
case "$DISK_FMT" in
|
||||
"raw" | "qcow2" ) ;;
|
||||
* ) error "Invalid DISK_FMT specified, value \"$DISK_FMT\" is not recognized!" && exit 78 ;;
|
||||
esac
|
||||
|
||||
if ! validDiskType "$DISK_TYPE"; then
|
||||
error "Invalid DISK_TYPE specified, value \"$DISK_TYPE\" is not recognized!"
|
||||
exit 80
|
||||
fi
|
||||
|
||||
if [[ "$DISK_FLAGS" =~ [[:space:]] ]]; then
|
||||
error "Invalid DISK_FLAGS value '$DISK_FLAGS', spaces are not allowed."
|
||||
exit 78
|
||||
fi
|
||||
|
||||
if [[ "$DISK_OPTIONS" =~ [[:space:]] ]]; then
|
||||
error "Invalid DISK_OPTIONS value '$DISK_OPTIONS', spaces are not allowed."
|
||||
exit 78
|
||||
fi
|
||||
|
||||
if [ -z "$ALLOCATE" ]; then
|
||||
ALLOCATE="N"
|
||||
fi
|
||||
|
||||
if disabled "$ALLOCATE"; then
|
||||
DISK_STYLE="growable"
|
||||
DISK_ALLOC="preallocation=off"
|
||||
else
|
||||
DISK_STYLE="preallocated"
|
||||
DISK_ALLOC="preallocation=falloc"
|
||||
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 "$SYSTEM" "$DISK_TYPE" "2" "0xb" "raw" "$DISK_IO" "$DISK_CACHE" "" "")
|
||||
|
||||
if enabled "$DISK_DISABLE"; then
|
||||
finishDisks && return 0
|
||||
fi
|
||||
|
||||
DISK1_FILE="$STORAGE/${DISK_NAME}"
|
||||
DISK2_FILE="/storage2/${DISK_NAME}2"
|
||||
DISK3_FILE="/storage3/${DISK_NAME}3"
|
||||
DISK4_FILE="/storage4/${DISK_NAME}4"
|
||||
|
||||
: "${DISK2_SIZE:=""}"
|
||||
: "${DISK3_SIZE:=""}"
|
||||
: "${DISK4_SIZE:=""}"
|
||||
|
||||
: "${DEVICE:=""}" # Docker variables to passthrough a block device, like /dev/vdc1.
|
||||
: "${DEVICE2:=""}"
|
||||
: "${DEVICE3:=""}"
|
||||
: "${DEVICE4:=""}"
|
||||
|
||||
[ -z "$DEVICE" ] && [ -b "/disk" ] && DEVICE="/disk"
|
||||
[ -z "$DEVICE" ] && [ -b "/disk1" ] && DEVICE="/disk1"
|
||||
[ -z "$DEVICE2" ] && [ -b "/disk2" ] && DEVICE2="/disk2"
|
||||
[ -z "$DEVICE3" ] && [ -b "/disk3" ] && DEVICE3="/disk3"
|
||||
[ -z "$DEVICE4" ] && [ -b "/disk4" ] && DEVICE4="/disk4"
|
||||
|
||||
[ -z "$DEVICE" ] && [ -b "/dev/disk1" ] && DEVICE="/dev/disk1"
|
||||
[ -z "$DEVICE2" ] && [ -b "/dev/disk2" ] && DEVICE2="/dev/disk2"
|
||||
[ -z "$DEVICE3" ] && [ -b "/dev/disk3" ] && DEVICE3="/dev/disk3"
|
||||
[ -z "$DEVICE4" ] && [ -b "/dev/disk4" ] && DEVICE4="/dev/disk4"
|
||||
|
||||
DISK_FILES=( "$DISK1_FILE" "$DISK2_FILE" "$DISK3_FILE" "$DISK4_FILE" )
|
||||
DISK_DESCS=( "disk" "disk2" "disk3" "disk4" )
|
||||
DISK_SIZES=( "$DISK_SIZE" "$DISK2_SIZE" "$DISK3_SIZE" "$DISK4_SIZE" )
|
||||
DISK_DEVICES=( "$DEVICE" "$DEVICE2" "$DEVICE3" "$DEVICE4" )
|
||||
DISK_INDEXES=( "3" "4" "5" "6" )
|
||||
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
|
||||
|
||||
if [ -n "${DISK_DEVICES[i]}" ]; then
|
||||
addDevice "${DISK_DEVICES[i]}" "$DISK_TYPE" "${DISK_INDEXES[i]}" "${DISK_ADDRESSES[i]}" || exit $?
|
||||
else
|
||||
addDisk "${DISK_FILES[i]}" "$DISK_TYPE" "${DISK_DESCS[i]}" "${DISK_SIZES[i]}" "${DISK_INDEXES[i]}" "${DISK_ADDRESSES[i]}" "$DISK_FMT" "$DISK_IO" "$DISK_CACHE" || exit $?
|
||||
fi
|
||||
|
||||
done
|
||||
|
||||
finishDisks
|
||||
|
||||
return 0
|
||||
+20
-2
@@ -65,9 +65,27 @@ if [ ! -c "$RENDERNODE" ] || [ ! -r "$RENDERNODE" ] || [ ! -w "$RENDERNODE" ]; t
|
||||
warn "render device '${RENDERNODE}' is unavailable or inaccessible."
|
||||
fi
|
||||
|
||||
addDsmPackage() {
|
||||
|
||||
local pkg=$1
|
||||
local desc=$2
|
||||
|
||||
if ! apt-mark showinstall | grep -qx "$pkg"; then
|
||||
[ -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
|
||||
sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources
|
||||
fi
|
||||
fi
|
||||
|
||||
addPackage "$pkg" "$desc"
|
||||
}
|
||||
|
||||
# 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 "qemu-system-modules-opengl" "OpenGL module"
|
||||
addDsmPackage "xserver-xorg-video-intel" "Intel GPU drivers"
|
||||
addDsmPackage "qemu-system-modules-opengl" "OpenGL module"
|
||||
|
||||
return 0
|
||||
|
||||
@@ -5,6 +5,17 @@ set -Eeuo pipefail
|
||||
: "${APP:="Virtual DSM"}"
|
||||
: "${SUPPORT:="https://github.com/vdsm/virtual-dsm"}"
|
||||
|
||||
: "${USB:="N"}"
|
||||
: "${AUDIO:="N"}"
|
||||
: "${VGA:="none"}"
|
||||
: "${DISPLAY:="none"}"
|
||||
: "${WEB_PORT:="5000"}"
|
||||
: "${DISK_OFFSET:="2"}"
|
||||
: "${DISK_SIZE:="256G"}"
|
||||
: "${RAM_MINIMUM:="1G"}"
|
||||
: "${DISK_MINIMUM:="6G"}"
|
||||
: "${BOOT_MODE:="legacy"}"
|
||||
|
||||
cd /run
|
||||
|
||||
. start.sh # Startup hook
|
||||
@@ -15,10 +26,13 @@ cd /run
|
||||
. install.sh # Run installation
|
||||
. disk.sh # Initialize disks
|
||||
. display.sh # Initialize graphics
|
||||
. prepare.sh # Prepare for launch
|
||||
. network.sh # Initialize network
|
||||
. boot.sh # Configure boot
|
||||
. proc.sh # Initialize processor
|
||||
. serial.sh # Initialize serialport
|
||||
. power.sh # Configure shutdown
|
||||
. balloon.sh # Initialize ballooning
|
||||
. config.sh # Configure arguments
|
||||
. finish.sh # Finish initialization
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
if [ -s "$QEMU_DIR/qemu.host" ] && [[ "$(<"$QEMU_DIR/qemu.host")" == "172.17."* ]]; then
|
||||
warn "your container IP starts with 172.17.* which will cause conflicts when you install the Container Manager package inside DSM!"
|
||||
fi
|
||||
|
||||
if enabled "$DEBUG"; then
|
||||
echo
|
||||
printf "QEMU arguments:\n\n %s\n\n" "${ARGS// -/$'\n -'}"
|
||||
fi
|
||||
|
||||
|
||||
-296
@@ -1,296 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
trap 'error "Status $? while: $BASH_COMMAND (line $LINENO/$BASH_LINENO)"' ERR
|
||||
enabled "${TRACE:-}" && set -o functrace && trap 'echo "# $BASH_COMMAND" >&2' DEBUG
|
||||
|
||||
[ ! -f "/run/entry.sh" ] && error "Script must be run inside the container!" && exit 11
|
||||
[ "$(id -u)" -ne "0" ] && error "Script must be executed with root privileges." && exit 12
|
||||
|
||||
# Docker environment variables
|
||||
|
||||
: "${TZ:=""}" # System timezone
|
||||
: "${KVM:="Y"}" # KVM acceleration
|
||||
: "${DEBUG:="N"}" # Disable debugging mode
|
||||
: "${COUNTRY:=""}" # Country code for mirror
|
||||
: "${MACHINE:="q35"}" # Machine type selection
|
||||
: "${ALLOCATE:=""}" # Preallocate diskspace
|
||||
: "${ARGUMENTS:=""}" # Extra QEMU parameters
|
||||
: "${CPU_CORES:="2"}" # Amount of CPU cores
|
||||
: "${RAM_SIZE:="2G"}" # Maximum RAM amount
|
||||
: "${RAM_CHECK:="Y"}" # Check available RAM
|
||||
: "${DISK_SIZE:="16G"}" # Initial data disk size
|
||||
: "${STORAGE:="/storage"}" # Storage folder location
|
||||
|
||||
detectEngine() {
|
||||
|
||||
if [ -f "/run/.containerenv" ]; then
|
||||
ENGINE="${container:-}"
|
||||
|
||||
if [[ "${ENGINE,,}" == *"podman"* ]]; then
|
||||
ENGINE="Podman"
|
||||
else
|
||||
[ -z "$ENGINE" ] && ENGINE="Kubernetes"
|
||||
fi
|
||||
elif [ -f "/.dockerenv" ]; then
|
||||
ENGINE="Docker"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
detectRootless() {
|
||||
|
||||
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)
|
||||
|
||||
if [[ "$uid_map" == "0 0 4294967295" ]]; then
|
||||
ROOTLESS="N"
|
||||
else
|
||||
ROOTLESS="Y"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
checkPrivileged() {
|
||||
|
||||
local cap_bnd
|
||||
local last_cap
|
||||
# Get the capability bounding set
|
||||
cap_bnd=$(grep '^CapBnd:' /proc/$$/status | awk '{print $2}')
|
||||
cap_bnd=$(printf "%d" "0x${cap_bnd}")
|
||||
|
||||
# Get the last capability number
|
||||
last_cap=$(cat /proc/sys/kernel/cap_last_cap)
|
||||
|
||||
# Calculate the maximum capability value
|
||||
# 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
|
||||
PRIVILEGED="Y"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
checkCores() {
|
||||
|
||||
CPU_CORES=$(strip "$CPU_CORES")
|
||||
[ -z "$CPU_CORES" ] && CPU_CORES=2
|
||||
[[ "${CPU_CORES,,}" == "max" ]] && CPU_CORES="$CORES"
|
||||
[[ "${CPU_CORES,,}" == "half" ]] && CPU_CORES=$(( CORES / 2 ))
|
||||
[ -z "${CPU_CORES##*[!0-9]*}" ] && error "Invalid amount of CPU_CORES: $CPU_CORES" && exit 15
|
||||
[ "$CPU_CORES" -lt "1" ] && CPU_CORES=1
|
||||
|
||||
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."
|
||||
CPU_CORES="$CORES"
|
||||
fi
|
||||
|
||||
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() {
|
||||
|
||||
# Check system
|
||||
|
||||
QEMU_DIR="/run/shm"
|
||||
|
||||
if [ ! -d "/dev/shm" ]; then
|
||||
error "Directory /dev/shm not found!" && exit 14
|
||||
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"
|
||||
fi
|
||||
|
||||
QEMU_PID="$QEMU_DIR/qemu.pid"
|
||||
|
||||
# Check folder
|
||||
|
||||
if [[ "${STORAGE,,}" != "/storage" ]]; then
|
||||
if ! mkdir -p -- "$STORAGE"; then
|
||||
error "Cannot create storage folder ($STORAGE)!" && exit 13
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -d "$STORAGE" ]; then
|
||||
error "Storage folder ($STORAGE) not found!" && exit 13
|
||||
fi
|
||||
|
||||
if [ ! -w "$STORAGE" ]; then
|
||||
msg="Storage folder ($STORAGE) is not writeable!"
|
||||
msg+=" If SELinux is active, you need to add the \":Z\" flag to the bind mount."
|
||||
error "$msg" && exit 13
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
checkHost() {
|
||||
|
||||
# Check filesystem
|
||||
if [[ "${FS,,}" == "ecryptfs" || "${FS,,}" == "tmpfs" ]]; then
|
||||
DISK_IO="threads"
|
||||
DISK_CACHE="writeback"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
checkKvm() {
|
||||
|
||||
# Check KVM support
|
||||
|
||||
if [[ "${PLATFORM,,}" == "x64" ]]; then
|
||||
TARGET="amd64"
|
||||
else
|
||||
TARGET="arm64"
|
||||
fi
|
||||
|
||||
if disabled "$KVM"; then
|
||||
warn "KVM acceleration is disabled, this will cause the machine to run about 10 times slower!"
|
||||
else
|
||||
# KVM accelerates only matching host and guest instruction sets; cross-
|
||||
# architecture execution must fall back to software emulation.
|
||||
if [[ "${ARCH,,}" != "$TARGET" ]]; then
|
||||
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."
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! disabled "$KVM"; then
|
||||
|
||||
KVM_ERR=""
|
||||
|
||||
if [ ! -e /dev/kvm ]; then
|
||||
KVM_ERR="(/dev/kvm is missing)"
|
||||
else
|
||||
if ! sh -c 'echo -n > /dev/kvm' &> /dev/null; then
|
||||
KVM_ERR="(/dev/kvm is unwriteable)"
|
||||
else
|
||||
if [[ "${PLATFORM,,}" == "x64" ]]; then
|
||||
flags=$(sed -ne '/^flags/s/^.*: //p' /proc/cpuinfo)
|
||||
if ! grep -qw "vmx\|svm" <<< "$flags"; then
|
||||
KVM_ERR="(not enabled in BIOS)"
|
||||
fi
|
||||
if ! grep -qw "sse4_2" <<< "$flags"; then
|
||||
error "Your CPU does not have the SSE4 instruction set that Virtual DSM requires!"
|
||||
enabled "$DEBUG" || exit 88
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$KVM_ERR" ]; then
|
||||
KVM="N"
|
||||
if [[ "$OSTYPE" =~ ^darwin ]]; then
|
||||
warn "you are using macOS which has no KVM support, so the machine will run about 10 times slower."
|
||||
else
|
||||
kernel=$(uname -a)
|
||||
case "${kernel,,}" in
|
||||
*"microsoft"* )
|
||||
error "Please bind '/dev/kvm' as a volume in the optional container settings when using Docker Desktop." ;;
|
||||
*"synology"* )
|
||||
error "Please make sure that Synology VMM (Virtual Machine Manager) is installed and that '/dev/kvm' is binded to this container." ;;
|
||||
*)
|
||||
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)." ;;
|
||||
esac
|
||||
enabled "$DEBUG" || exit 88
|
||||
fi
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Sanitize variables
|
||||
TZ=$(strip "$TZ")
|
||||
STORAGE=$(strip "$STORAGE")
|
||||
COUNTRY=$(strip "$COUNTRY")
|
||||
MACHINE=$(strip "${MACHINE,,}")
|
||||
DISK_SIZE=$(strip "$DISK_SIZE")
|
||||
|
||||
# Helper variables
|
||||
ROOTLESS="N"
|
||||
PRIVILEGED="N"
|
||||
ENGINE="Docker"
|
||||
PROCESS="${APP,,}"
|
||||
PROCESS="${PROCESS// /-}"
|
||||
|
||||
detectEngine
|
||||
detectRootless
|
||||
|
||||
echo "❯ Starting $APP for $ENGINE v$(</etc/version)..."
|
||||
echo "❯ For support visit $SUPPORT"
|
||||
|
||||
checkPrivileged
|
||||
|
||||
INFO="/run/shm/msg.html"
|
||||
PAGE="/run/shm/index.html"
|
||||
TEMPLATE="/var/www/index.html"
|
||||
FOOTER1="$APP for $ENGINE v$(</etc/version)"
|
||||
FOOTER2="<a href='$SUPPORT'>$SUPPORT</a>"
|
||||
|
||||
SOCKETS=1
|
||||
CPU=$(cpu)
|
||||
SYS=$(uname -r)
|
||||
ARCH=$(dpkg --print-architecture)
|
||||
CORES=$(grep -c '^processor' /proc/cpuinfo)
|
||||
IFS=. read -r KERNEL MINOR _ <<< "$SYS"
|
||||
|
||||
checkSockets
|
||||
checkCores
|
||||
checkStorage
|
||||
getMemoryInfo
|
||||
|
||||
# Print system info
|
||||
SYS="${SYS/-generic/}"
|
||||
FS=$(stat -f -c %T "$STORAGE")
|
||||
FS="${FS/UNKNOWN //}"
|
||||
FS="${FS/ext2\/ext3/ext4}"
|
||||
FS=$(echo "$FS" | sed 's/[)(]//g')
|
||||
SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
|
||||
SPACE_GB=$(formatBytes "$SPACE" "down")
|
||||
AVAIL_MEM=$(formatBytes "$RAM_AVAIL" "down")
|
||||
TOTAL_MEM=$(formatBytes "$RAM_TOTAL" "up")
|
||||
|
||||
echo "❯ CPU: ${CPU} | RAM: ${AVAIL_MEM/ GB/}/$TOTAL_MEM | DISK: $SPACE_GB (${FS}) | KERNEL: ${SYS}"
|
||||
echo
|
||||
|
||||
checkHost
|
||||
checkKvm
|
||||
|
||||
# Runtime state is intentionally discarded at each container start; persistent
|
||||
# machine and disk identity lives under STORAGE instead.
|
||||
# Cleanup files
|
||||
rm -f "$QEMU_DIR"/dsm.url
|
||||
rm -f "$QEMU_DIR"/{qemu.*,*.{pid,sock,pipe}}
|
||||
|
||||
# Cleanup dirs
|
||||
rm -rf /tmp/dsm
|
||||
rm -rf "$STORAGE/tmp"
|
||||
|
||||
return 0
|
||||
+581
-324
@@ -1,378 +1,635 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
: "${URL:=""}" # URL of the PAT file to be downloaded.
|
||||
: "${TZ:=""}" # System timezone
|
||||
: "${COUNTRY:=""}" # Country code for mirror
|
||||
: "${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
|
||||
BASE=$(<"$STORAGE/dsm.ver")
|
||||
BASE="${BASE//[![:print:]]/}"
|
||||
[ -z "$BASE" ] && BASE="DSM_VirtualDSM_69057"
|
||||
else
|
||||
# Fallback for old installs
|
||||
BASE="DSM_VirtualDSM_42962"
|
||||
fi
|
||||
TZ=$(strip "$TZ")
|
||||
COUNTRY=$(strip "$COUNTRY")
|
||||
|
||||
FN="boot.pat"
|
||||
DIR=$(find / -maxdepth 1 -type d -iname "$FN" -print -quit)
|
||||
[ ! -d "$DIR" ] && DIR=$(find "$STORAGE" -maxdepth 1 -type d -iname "$FN" -print -quit)
|
||||
checkDsmFilesystem() {
|
||||
|
||||
# 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
|
||||
BASE="DSM_VirtualDSM" && URL="file://$DIR"
|
||||
if [[ ! -s "$STORAGE/$BASE.boot.img" || ! -s "$STORAGE/$BASE.system.img" ]]; then
|
||||
error "The bind $DIR maps to a file that does not exist!" && exit 65
|
||||
fi
|
||||
fi
|
||||
local fs="$1"
|
||||
|
||||
FILE=$(find / -maxdepth 1 -type f -iname "$FN" -print -quit)
|
||||
[ ! -s "$FILE" ] && FILE=$(find "$STORAGE" -maxdepth 1 -type f -iname "$FN" -print -quit)
|
||||
[ -s "$FILE" ] && BASE="DSM_VirtualDSM" && URL="file://$FILE"
|
||||
|
||||
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
|
||||
BASE=$(basename "$URL" .pat)
|
||||
if [ ! -s "$STORAGE/$BASE.system.img" ]; then
|
||||
BASE=$(basename "${URL%%\?*}" .pat)
|
||||
printf -v BASE '%b' "${BASE//%/\\x}"
|
||||
BASE="${BASE//[!A-Za-z0-9._-]/_}"
|
||||
fi
|
||||
if [[ "${URL,,}" != "http"* && "${URL,,}" != "file:"* ]]; then
|
||||
[ ! -s "$STORAGE/$BASE.pat" ] && error "Invalid URL: $URL" && exit 65
|
||||
URL="file://$STORAGE/$BASE.pat"
|
||||
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
|
||||
return 0 # Previous installation found
|
||||
fi
|
||||
|
||||
html "Please wait while Virtual DSM is being installed..."
|
||||
|
||||
DL=""
|
||||
DL_CHINA="https://cndl.synology.cn/download/DSM"
|
||||
DL_GLOBAL="https://global.synologydownload.com/download/DSM"
|
||||
|
||||
[[ "${URL,,}" == *"cndl.synology"* ]] && DL="$DL_CHINA"
|
||||
[[ "${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
|
||||
[ -z "$COUNTRY" ] && setCountry
|
||||
[ -z "$COUNTRY" ] && info "Warning: could not detect country to select mirror!"
|
||||
[[ "${COUNTRY^^}" == "CN" ]] && DL="$DL_CHINA" || DL="$DL_GLOBAL"
|
||||
fi
|
||||
|
||||
if [ -z "$URL" ]; then
|
||||
URL="$DL/release/7.2.2/72806/DSM_VirtualDSM_72806.pat"
|
||||
fi
|
||||
|
||||
if [ ! -s "$FILE" ]; then
|
||||
BASE=$(basename "${URL%%\?*}" .pat)
|
||||
printf -v BASE '%b' "${BASE//%/\\x}"
|
||||
BASE="${BASE//[!A-Za-z0-9._-]/_}"
|
||||
fi
|
||||
|
||||
if [[ "$URL" != "file://$STORAGE/$BASE.pat" ]]; then
|
||||
rm -f "$STORAGE/$BASE.pat"
|
||||
fi
|
||||
|
||||
rm -f "$STORAGE/$BASE.agent"
|
||||
rm -f "$STORAGE/$BASE.boot.img"
|
||||
rm -f "$STORAGE/$BASE.system.img"
|
||||
|
||||
# Check filesystem
|
||||
FS=$(stat -f -c %T "$STORAGE")
|
||||
|
||||
if [[ "${FS,,}" == "overlay"* && "${ENGINE,,}" == "docker" ]]; then
|
||||
warn "the filesystem of $STORAGE is OverlayFS, this usually means it was binded to an invalid path!"
|
||||
fi
|
||||
|
||||
if [[ "${FS,,}" == "fuse"* ]]; then
|
||||
warn "the filesystem of $STORAGE is FUSE, this extra layer will negatively affect performance!"
|
||||
fi
|
||||
|
||||
if [[ "${FS,,}" == "ecryptfs" || "${FS,,}" == "tmpfs" ]]; then
|
||||
warn "the filesystem of $STORAGE is $FS, which does not support O_DIRECT mode, adjusting settings..."
|
||||
fi
|
||||
|
||||
if [[ "${FS,,}" == "fat"* || "${FS,,}" == "vfat"* || "${FS,,}" == "msdos"* ]]; then
|
||||
error "Unable to install on $FS filesystems, please use a different filesystem for /storage." && exit 61
|
||||
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
|
||||
TMP="$STORAGE/tmp"
|
||||
rm -rf "$TMP"
|
||||
if ! makeDir "$TMP"; then
|
||||
error "Failed to create directory \"$TMP\" !" && exit 93
|
||||
fi
|
||||
else
|
||||
TMP="/tmp/dsm"
|
||||
TMP_SPACE=2147483648
|
||||
SPACE=$(df --output=avail -B 1 /tmp | tail -n 1)
|
||||
SPACE_MB=$(formatBytes "$SPACE")
|
||||
if (( TMP_SPACE > SPACE )); then
|
||||
error "Not enough free space inside the container, have $SPACE_MB available but need at least 2 GB." && exit 93
|
||||
fi
|
||||
rm -rf "$TMP" && mkdir -p "$TMP"
|
||||
fi
|
||||
|
||||
# Check free diskspace
|
||||
ROOT_SPACE=536870912
|
||||
SPACE=$(df --output=avail -B 1 / | tail -n 1)
|
||||
SPACE_MB=$(formatBytes "$SPACE" "down")
|
||||
(( ROOT_SPACE > SPACE )) && error "Not enough free space inside the container, have $SPACE_MB available but need at least 500 MB." && exit 96
|
||||
|
||||
MIN_SPACE=15032385536
|
||||
SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
|
||||
SPACE_GB=$(formatBytes "$SPACE")
|
||||
(( MIN_SPACE > SPACE )) && error "Not enough free space for installation in $STORAGE, have $SPACE_GB available but need at least 14 GB." && exit 94
|
||||
|
||||
if [[ "$URL" == "file://"* ]]; then
|
||||
MSG="Copying DSM"
|
||||
ERR="Failed to copy ${URL:7}"
|
||||
info "Install: Copying installation image..."
|
||||
else
|
||||
MSG="Downloading DSM"
|
||||
ERR="Failed to download $URL"
|
||||
info "Install: Downloading $BASE.pat..."
|
||||
fi
|
||||
|
||||
html "$MSG..."
|
||||
|
||||
PAT="/$BASE.pat"
|
||||
rm -f "$PAT"
|
||||
|
||||
if [[ "$URL" == "file://"* ]]; then
|
||||
|
||||
if [ ! -f "${URL:7}" ]; then
|
||||
error "File '${URL:7}' does not exist!" && exit 65
|
||||
if [[ "${fs,,}" == "overlay"* && "${ENGINE,,}" == "docker" ]]; then
|
||||
warn "the filesystem of $STORAGE is OverlayFS, this usually means it was binded to an invalid path!"
|
||||
fi
|
||||
|
||||
cp "${URL:7}" "$PAT"
|
||||
if [[ "${fs,,}" == "fuse"* ]]; then
|
||||
warn "the filesystem of $STORAGE is FUSE, this extra layer will negatively affect performance!"
|
||||
fi
|
||||
|
||||
else
|
||||
if [[ "${fs,,}" == "ecryptfs" || "${fs,,}" == "tmpfs" ]]; then
|
||||
warn "the filesystem of $STORAGE is $fs, which does not support O_DIRECT mode, adjusting settings..."
|
||||
fi
|
||||
|
||||
SIZE=0
|
||||
REASON=""
|
||||
PROGRESS=()
|
||||
OUTPUT=""
|
||||
LOG=$(mktemp)
|
||||
if [[ "${fs,,}" == "fat"* || "${fs,,}" == "vfat"* || "${fs,,}" == "msdos"* ]]; then
|
||||
error "Unable to install on $fs filesystems, please use a different filesystem for /storage."
|
||||
return 61
|
||||
fi
|
||||
|
||||
[[ "${URL,,}" == *"_72806.pat" ]] && SIZE=361010261
|
||||
[[ "${URL,,}" == *"_69057.pat" ]] && SIZE=363837333
|
||||
[[ "${URL,,}" == *"_42218.pat" ]] && SIZE=379637760
|
||||
return 0
|
||||
}
|
||||
|
||||
checkDsmSpace() {
|
||||
|
||||
local rootSpace=536870912
|
||||
local minSpace=15032385536
|
||||
local space available
|
||||
|
||||
space=$(df --output=avail -B 1 / | tail -n 1) || return $?
|
||||
available=$(formatBytes "$space" "down") || return $?
|
||||
|
||||
if (( rootSpace > space )); then
|
||||
error "Not enough free space inside the container, have $available available but need at least 500 MB."
|
||||
return 96
|
||||
fi
|
||||
|
||||
space=$(df --output=avail -B 1 "$STORAGE" | tail -n 1) || return $?
|
||||
available=$(formatBytes "$space") || return $?
|
||||
|
||||
if (( minSpace > space )); then
|
||||
error "Not enough free space for installation in $STORAGE, have $available available but need at least 14 GB."
|
||||
return 94
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
downloadFile() {
|
||||
|
||||
local url="$1"
|
||||
local pat="$2"
|
||||
local msg="$3"
|
||||
local connections="${4:-1}"
|
||||
|
||||
downloadToFile \
|
||||
"$url" \
|
||||
"$pat" \
|
||||
"$msg" \
|
||||
"0" \
|
||||
"$connections" \
|
||||
"Y"
|
||||
}
|
||||
|
||||
downloadPat() {
|
||||
|
||||
local pat="$1"
|
||||
local msg err
|
||||
|
||||
if [[ "$URL" == "file://"* ]]; then
|
||||
msg="Copying DSM"
|
||||
err="Failed to copy ${URL:7}"
|
||||
info "Install: Copying installation image..."
|
||||
html "$msg..." || return $?
|
||||
|
||||
rm -f "$pat" || return $?
|
||||
|
||||
if [ ! -f "${URL:7}" ]; then
|
||||
error "File '${URL:7}' does not exist!"
|
||||
return 65
|
||||
fi
|
||||
|
||||
cp "${URL:7}" "$pat" || return $?
|
||||
|
||||
# Use Wget's progress bar in a terminal and progress.sh in container logs.
|
||||
if [ -t 1 ]; then
|
||||
PROGRESS=( --show-progress --progress=bar:noscroll )
|
||||
else
|
||||
OUTPUT="log"
|
||||
|
||||
msg="Downloading DSM"
|
||||
err="Failed to download $URL"
|
||||
info "Install: Downloading $BASE.pat..."
|
||||
|
||||
downloadRetry \
|
||||
"$pat" \
|
||||
"$CONNECTIONS" \
|
||||
"5" \
|
||||
"$BASE.pat" \
|
||||
"0" \
|
||||
"$URL" \
|
||||
"$pat" \
|
||||
"$msg" || return 69
|
||||
fi
|
||||
|
||||
/run/progress.sh "$PAT" "$SIZE" "$MSG ([P])..." "$OUTPUT" 52428800 &
|
||||
if [ ! -s "$pat" ]; then
|
||||
error "$err"
|
||||
return 69
|
||||
fi
|
||||
|
||||
{
|
||||
LC_ALL=C wget "$URL" -O "$PAT" --no-verbose --no-check-certificate \
|
||||
--timeout=30 --no-http-keep-alive "${PROGRESS[@]}" \
|
||||
--output-file="$LOG"
|
||||
rc=$?
|
||||
} || :
|
||||
return 0
|
||||
}
|
||||
|
||||
extractPat() {
|
||||
|
||||
local pat="$1"
|
||||
local tmp="$2"
|
||||
local size="$3"
|
||||
local msg="Extracting installation image..."
|
||||
local rc
|
||||
|
||||
info "Install: $msg" && html "$msg" || return $?
|
||||
/run/progress.sh "$tmp" "$size" "$msg ([P])..." &
|
||||
|
||||
# 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
|
||||
|
||||
tar xpf "$pat" -C "$tmp/." || {
|
||||
rc=$?
|
||||
fKill "progress.sh"
|
||||
return "$rc"
|
||||
}
|
||||
|
||||
else
|
||||
|
||||
{ (cd "$tmp" && python3 /run/extract.py -i "$pat" -d 2>/run/extract.log); rc=$?; } || :
|
||||
|
||||
if (( rc != 0 )); then
|
||||
fKill "progress.sh"
|
||||
cat /run/extract.log
|
||||
error "Failed to extract PAT file, reason $rc"
|
||||
return 63
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
fKill "progress.sh"
|
||||
return 0
|
||||
}
|
||||
|
||||
if (( rc != 0 )); then
|
||||
REASON=$(sed -n \
|
||||
-e 's/^wget: //p' \
|
||||
-e 's/^[0-9-]\{10\} [0-9:]\{8\} ERROR //p' \
|
||||
"$LOG" | tail -n 1)
|
||||
createSystemImage() {
|
||||
|
||||
local tmp="$1"
|
||||
local fs="$2"
|
||||
local msg="Preparing system partition..."
|
||||
|
||||
info "Install: $msg" && html "$msg" || return $?
|
||||
|
||||
# The PAT boot archive becomes the persistent QEMU boot disk after its
|
||||
# companion system partition has been assembled.
|
||||
DSM_BOOT=$(find "$tmp" -name "*.bin.zip" -print -quit) || return $?
|
||||
|
||||
if [ -z "$DSM_BOOT" ]; then
|
||||
error "The PAT file contains no boot image."
|
||||
return 67
|
||||
fi
|
||||
|
||||
rm -f "$LOG"
|
||||
|
||||
if (( rc == 3 )); then
|
||||
error "$ERR because the file could not be written (disk full?)."
|
||||
exit 69
|
||||
elif (( rc != 0 )); then
|
||||
if [ -n "$REASON" ]; then
|
||||
error "$ERR: ${REASON%.}."
|
||||
else
|
||||
error "$ERR with exit status $rc."
|
||||
fi
|
||||
exit 69
|
||||
if [ ! -s "$DSM_BOOT" ]; then
|
||||
error "The PAT boot image archive is empty."
|
||||
return 67
|
||||
fi
|
||||
|
||||
fi
|
||||
7z x -y -o"$tmp" "$DSM_BOOT" >/dev/null || return $?
|
||||
DSM_BOOT="${DSM_BOOT%.zip}"
|
||||
|
||||
[ ! -s "$PAT" ] && error "$ERR" && exit 69
|
||||
SYSTEM="$STORAGE/$BASE.system.img"
|
||||
rm -f "$SYSTEM" || return $?
|
||||
|
||||
SIZE=$(stat -c%s "$PAT")
|
||||
# Check free diskspace
|
||||
local systemSize=10738466816
|
||||
local space available
|
||||
|
||||
# Full Virtual DSM PAT files are substantially larger than update packs;
|
||||
# reject undersized inputs before attempting destructive image preparation.
|
||||
if ((SIZE<250000000)); then
|
||||
error "The specified PAT file is probably an update pack as it's too small." && exit 62
|
||||
fi
|
||||
space=$(df --output=avail -B 1 "$STORAGE" | tail -n 1) || return $?
|
||||
available=$(formatBytes "$space") || return $?
|
||||
|
||||
MSG="Extracting installation image..."
|
||||
info "Install: $MSG" && html "$MSG"
|
||||
|
||||
/run/progress.sh "$TMP" "$SIZE" "$MSG ([P])..." &
|
||||
|
||||
# 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
|
||||
|
||||
tar xpf "$PAT" -C "$TMP/."
|
||||
|
||||
else
|
||||
|
||||
{ (cd "$TMP" && python3 /run/extract.py -i "$PAT" -d 2>/run/extract.log); rc=$?; } || :
|
||||
|
||||
if (( rc != 0 )); then
|
||||
fKill "progress.sh"
|
||||
cat /run/extract.log
|
||||
error "Failed to extract PAT file, reason $rc" && exit 63
|
||||
if (( systemSize > space )); then
|
||||
error "Not enough free space in $STORAGE to create a 10 GB system disk, have only $available available."
|
||||
return 97
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
fKill "progress.sh"
|
||||
|
||||
MSG="Preparing system partition..."
|
||||
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)
|
||||
[ -z "$BOOT" ] && error "The PAT file contains no boot image." && exit 67
|
||||
[ ! -s "$BOOT" ] && error "The PAT boot image archive is empty." && exit 67
|
||||
|
||||
unzip -q -o "$BOOT" -d "$TMP"
|
||||
BOOT="${BOOT%.zip}"
|
||||
|
||||
SYSTEM="$STORAGE/$BASE.system.img"
|
||||
rm -f "$SYSTEM"
|
||||
|
||||
# Check free diskspace
|
||||
SYSTEM_SIZE=10738466816
|
||||
SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
|
||||
SPACE_MB=$(formatBytes "$SPACE")
|
||||
|
||||
if (( SYSTEM_SIZE > SPACE )); then
|
||||
error "Not enough free space in $STORAGE to create a 10 GB system disk, have only $SPACE_MB available." && exit 97
|
||||
fi
|
||||
|
||||
if ! touch "$SYSTEM"; then
|
||||
error "Could not create file $SYSTEM for the system disk." && exit 98
|
||||
fi
|
||||
|
||||
setOwner "$SYSTEM" || warn "failed to set the owner for \"$SYSTEM\" !"
|
||||
|
||||
if [[ "${FS,,}" == "btrfs" ]]; then
|
||||
{ chattr +C "$SYSTEM"; } || :
|
||||
FA=$(lsattr "$SYSTEM")
|
||||
if [[ "$FA" != *"C"* ]]; then
|
||||
error "Failed to disable COW for system image $SYSTEM on ${FS^^} filesystem."
|
||||
if ! touch "$SYSTEM"; then
|
||||
error "Could not create file $SYSTEM for the system disk."
|
||||
return 98
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! fallocate -l "$SYSTEM_SIZE" "$SYSTEM" &>/dev/null; then
|
||||
if ! fallocate -l -x "$SYSTEM_SIZE" "$SYSTEM"; then
|
||||
if ! truncate -s "$SYSTEM_SIZE" "$SYSTEM"; then
|
||||
rm -f "$SYSTEM"
|
||||
error "Could not allocate file $SYSTEM for the system disk." && exit 98
|
||||
setOwner "$SYSTEM" || warn "failed to set the owner for \"$SYSTEM\" !"
|
||||
|
||||
if [[ "${fs,,}" == "btrfs" ]]; then
|
||||
{ chattr +C "$SYSTEM"; } || :
|
||||
local attrs
|
||||
attrs=$(lsattr "$SYSTEM") || return $?
|
||||
if [[ "$attrs" != *"C"* ]]; then
|
||||
error "Failed to disable COW for system image $SYSTEM on ${fs^^} filesystem."
|
||||
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"
|
||||
if ! fallocate -l "$systemSize" "$SYSTEM" &>/dev/null; then
|
||||
if ! fallocate -l -x "$systemSize" "$SYSTEM"; then
|
||||
if ! truncate -s "$systemSize" "$SYSTEM"; then
|
||||
rm -f "$SYSTEM"
|
||||
error "Could not allocate file $SYSTEM for the system disk."
|
||||
return 98
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
{
|
||||
echo "label: dos"
|
||||
echo "label-id: 0x6f9ee2e9"
|
||||
echo "device: $SYSTEM"
|
||||
echo "unit: sectors"
|
||||
echo "sector-size: 512"
|
||||
echo ""
|
||||
echo "${SYSTEM}1 : start= 2048, size= 16777216, type=83"
|
||||
echo "${SYSTEM}2 : start= 16779264, size= 4194304, type=82"
|
||||
} > "$PART"
|
||||
# Recreate Synology's expected DOS partition layout inside the fixed 10 GiB
|
||||
# system image before populating the ext4 root partition.
|
||||
local part="$tmp/partition.fdisk"
|
||||
|
||||
sfdisk -q "$SYSTEM" < "$PART"
|
||||
if ! {
|
||||
echo "label: dos"
|
||||
echo "label-id: 0x6f9ee2e9"
|
||||
echo "device: $SYSTEM"
|
||||
echo "unit: sectors"
|
||||
echo "sector-size: 512"
|
||||
echo ""
|
||||
echo "${SYSTEM}1 : start= 2048, size= 16777216, type=83"
|
||||
echo "${SYSTEM}2 : start= 16779264, size= 4194304, type=82"
|
||||
} > "$part"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
MOUNT="$TMP/system"
|
||||
rm -rf "$MOUNT"
|
||||
sfdisk -q "$SYSTEM" < "$part" || return $?
|
||||
|
||||
if ! makeDir "$MOUNT"; then
|
||||
error "Failed to create directory \"$MOUNT\" !" && exit 93
|
||||
fi
|
||||
local mount="$tmp/system"
|
||||
rm -rf "$mount" || return $?
|
||||
|
||||
MSG="Extracting system partition..."
|
||||
info "Install: $MSG" && html "$MSG"
|
||||
if ! makeDir "$mount"; then
|
||||
error "Failed to create directory \"$mount\" !"
|
||||
return 93
|
||||
fi
|
||||
|
||||
HDA="$TMP/hda1"
|
||||
IDB="$TMP/indexdb"
|
||||
PKG="$TMP/packages"
|
||||
HDP="$TMP/synohdpack_img"
|
||||
return 0
|
||||
}
|
||||
|
||||
[ ! -s "$HDA.tgz" ] && error "The PAT file contains no OS image." && exit 64
|
||||
mv "$HDA.tgz" "$HDA.txz"
|
||||
installSystemPartition() {
|
||||
|
||||
[ -d "$PKG" ] && mv "$PKG/" "$MOUNT/.SynoUpgradePackages/"
|
||||
rm -f "$MOUNT/.SynoUpgradePackages/ActiveInsight-"*
|
||||
local tmp="$1"
|
||||
local mount="$tmp/system"
|
||||
local msg="Extracting system partition..."
|
||||
|
||||
INDEX_DB="$MOUNT/usr/syno/synoman/indexdb"
|
||||
info "Install: $msg" && html "$msg" || return $?
|
||||
|
||||
if [ -s "$IDB.txz" ]; then
|
||||
mkdir -p "$INDEX_DB"
|
||||
fi
|
||||
local hda="$tmp/hda1"
|
||||
local idb="$tmp/indexdb"
|
||||
local pkg="$tmp/packages"
|
||||
local hdp="$tmp/synohdpack_img"
|
||||
|
||||
LABEL="1.44.1-42218"
|
||||
OFFSET="1048576" # 2048 * 512
|
||||
NUMBLOCKS="2097152" # (16777216 * 512) / 4096
|
||||
MSG="Installing system partition..."
|
||||
if [ ! -s "$hda.tgz" ]; then
|
||||
error "The PAT file contains no OS image."
|
||||
return 64
|
||||
fi
|
||||
|
||||
# Build the ext4 filesystem directly from the extracted tree under fakeroot,
|
||||
# preserving archive ownership without mounting a loop device.
|
||||
fakeroot -- bash -c "set -Eeu;\
|
||||
[ -s $HDP.txz ] && tar xpfJ $HDP.txz --absolute-names -C $MOUNT/;\
|
||||
[ -s $IDB.txz ] && tar xpfJ $IDB.txz --absolute-names -C $INDEX_DB/;\
|
||||
tar xpfJ $HDA.txz --absolute-names --skip-old-files -C $MOUNT/;\
|
||||
printf '%b%s%b' '\E[1;34m❯ \E[1;36m' 'Install: $MSG' '\E[0m\n';\
|
||||
mke2fs -q -t ext4 -b 4096 -d $MOUNT/ -L $LABEL -F -E offset=$OFFSET $SYSTEM $NUMBLOCKS"
|
||||
mv "$hda.tgz" "$hda.txz" || return $?
|
||||
|
||||
rm -rf "$MOUNT"
|
||||
echo "$BASE" > "$STORAGE/dsm.ver"
|
||||
setOwner "$STORAGE/dsm.ver" || warn "failed to set the owner for \"$STORAGE/dsm.ver\" !"
|
||||
if [ -d "$pkg" ]; then
|
||||
mv "$pkg/" "$mount/.SynoUpgradePackages/" || return $?
|
||||
fi
|
||||
|
||||
# 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
|
||||
rm -f "$PAT"
|
||||
else
|
||||
mv -f "$PAT" "$STORAGE/$BASE.pat"
|
||||
fi
|
||||
rm -f "$mount/.SynoUpgradePackages/ActiveInsight-"* || return $?
|
||||
|
||||
if [ -f "$STORAGE/$BASE.pat" ]; then
|
||||
setOwner "$STORAGE/$BASE.pat" || warn "failed to set the owner for \"$STORAGE/$BASE.pat\" !"
|
||||
fi
|
||||
local indexDb="$mount/usr/syno/synoman/indexdb"
|
||||
|
||||
mv -f "$BOOT" "$STORAGE/$BASE.boot.img"
|
||||
setOwner "$STORAGE/$BASE.boot.img" || warn "failed to set the owner for \"$STORAGE/$BASE.boot.img\" !"
|
||||
if [ -s "$idb.txz" ]; then
|
||||
mkdir -p "$indexDb" || return $?
|
||||
fi
|
||||
|
||||
rm -rf "$TMP"
|
||||
local label="1.44.1-42218"
|
||||
local offset="1048576" # 2048 * 512
|
||||
local numBlocks="2097152" # (16777216 * 512) / 4096
|
||||
local rc
|
||||
msg="Installing system partition..."
|
||||
|
||||
# Build the ext4 filesystem directly from the extracted tree under fakeroot,
|
||||
# preserving archive ownership without mounting a loop device.
|
||||
if fakeroot -- bash -c "set -Eeu;\
|
||||
[ -s $hdp.txz ] && tar xpfJ $hdp.txz --absolute-names -C $mount/;\
|
||||
[ -s $idb.txz ] && tar xpfJ $idb.txz --absolute-names -C $indexDb/;\
|
||||
tar xpfJ $hda.txz --absolute-names --skip-old-files -C $mount/;\
|
||||
printf '%b%s%b' '\E[1;34m❯ \E[1;36m' 'Install: $msg' '\E[0m\n';\
|
||||
mke2fs -q -t ext4 -b 4096 -d $mount/ -L $label -F -E offset=$offset $SYSTEM $numBlocks"; then
|
||||
:
|
||||
else
|
||||
rc=$?
|
||||
return "$rc"
|
||||
fi
|
||||
|
||||
rm -rf "$mount" || return $?
|
||||
return 0
|
||||
}
|
||||
|
||||
checkSse42() {
|
||||
|
||||
if disabled "$KVM" || [[ "${PLATFORM,,}" != "x64" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! hasFlag "sse4_2"; then
|
||||
error "Your CPU does not have the SSE4.2 instruction set that Virtual DSM requires!"
|
||||
enabled "$DEBUG" || return 88
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
sanitizePatBase() {
|
||||
|
||||
local source="$1"
|
||||
local base
|
||||
|
||||
base=$(basename "${source%%\?*}" .pat) || return 1
|
||||
printf -v base '%b' "${base//%/\\x}" || return 1
|
||||
base="${base//[!A-Za-z0-9._-]/_}"
|
||||
|
||||
printf '%s' "$base"
|
||||
}
|
||||
|
||||
reservePorts() {
|
||||
|
||||
local port ports=""
|
||||
local hostPorts="${HOST_PORTS:-}"
|
||||
|
||||
# Older configurations may reserve DSM's web ports for the container.
|
||||
# They now need to be forwarded to the guest instead.
|
||||
for port in ${hostPorts//,/ }; do
|
||||
|
||||
case "${port,,}" in
|
||||
5000|5000/tcp|5001|5001/tcp)
|
||||
continue ;;
|
||||
esac
|
||||
|
||||
ports+="${ports:+,}$port"
|
||||
|
||||
done
|
||||
|
||||
HOST_PORTS="$ports"
|
||||
|
||||
# NAT can fall back to user-mode networking after this point,
|
||||
# so always add the DSM web interface ports for non-DHCP networking.
|
||||
USER_PORTS="${USER_PORTS:+$USER_PORTS,}5000/tcp,5001/tcp,80/tcp,443/tcp,445/tcp"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
prepareMac() {
|
||||
|
||||
local file="$STORAGE/$PROCESS.mac"
|
||||
local legacy="$STORAGE/dsm.mac"
|
||||
local container mac=""
|
||||
|
||||
# An explicitly configured MAC remains network.sh's responsibility.
|
||||
[ -n "${MAC:-}" ] && return 0
|
||||
[ -s "$file" ] && return 0
|
||||
|
||||
# Preserve the address generated by older Virtual DSM releases.
|
||||
if [ -s "$legacy" ]; then
|
||||
mac=$(readFile "$legacy") || return $?
|
||||
fi
|
||||
|
||||
if [ -z "$mac" ]; then
|
||||
container=$(containerID) || return $?
|
||||
mac=$(echo "$container" | md5sum |
|
||||
sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\).*$/02:11:32:\3:\4:\5/') || return $?
|
||||
fi
|
||||
|
||||
if ! writeState "mac" "${mac^^}"; then
|
||||
error "Failed to write MAC address to \"$file\" !"
|
||||
return 28
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
getCountry() {
|
||||
|
||||
local url=$1
|
||||
local query=$2
|
||||
local json result
|
||||
|
||||
{ json=$(curl -m 5 -H "Accept: application/json" -sfk "$url"); local rc=$?; } || :
|
||||
(( rc != 0 )) && return 0
|
||||
|
||||
{ result=$(echo "$json" | jq -r "$query" 2> /dev/null); rc=$?; } || :
|
||||
(( rc != 0 )) && return 0
|
||||
|
||||
[[ ${#result} -ne 2 ]] && return 0
|
||||
[[ "${result^^}" == "XX" ]] && return 0
|
||||
|
||||
COUNTRY="${result^^}"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
setCountry() {
|
||||
|
||||
[[ "${TZ,,}" == "asia/harbin" ]] && COUNTRY="CN"
|
||||
[[ "${TZ,,}" == "asia/beijing" ]] && COUNTRY="CN"
|
||||
[[ "${TZ,,}" == "asia/urumqi" ]] && COUNTRY="CN"
|
||||
[[ "${TZ,,}" == "asia/kashgar" ]] && COUNTRY="CN"
|
||||
[[ "${TZ,,}" == "asia/shanghai" ]] && 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://ifconfig.co/json" ".country_iso"
|
||||
[ -z "$COUNTRY" ] && getCountry "https://api.ip2location.io" ".country_code"
|
||||
[ -z "$COUNTRY" ] && getCountry "https://ipinfo.io/json" ".country"
|
||||
[ -z "$COUNTRY" ] && getCountry "https://api.ipquery.io/?format=json" ".location.country_code"
|
||||
[ -z "$COUNTRY" ] && getCountry "https://api.myip.com" ".cc"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
finishDsmInstall() {
|
||||
|
||||
local pat="$1"
|
||||
local tmp="$2"
|
||||
|
||||
echo "$BASE" > "$STORAGE/dsm.ver" || return 1
|
||||
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
|
||||
rm -f "$pat" || return $?
|
||||
else
|
||||
mv -f "$pat" "$STORAGE/$BASE.pat" || return $?
|
||||
fi
|
||||
|
||||
if [ -f "$STORAGE/$BASE.pat" ]; then
|
||||
setOwner "$STORAGE/$BASE.pat" || warn "failed to set the owner for \"$STORAGE/$BASE.pat\" !"
|
||||
fi
|
||||
|
||||
mv -f "$DSM_BOOT" "$STORAGE/$BASE.boot.img" || return $?
|
||||
setOwner "$STORAGE/$BASE.boot.img" || warn "failed to set the owner for \"$STORAGE/$BASE.boot.img\" !"
|
||||
|
||||
rm -rf "$tmp" || return $?
|
||||
return 0
|
||||
}
|
||||
|
||||
installDSM() {
|
||||
|
||||
rm -rf /tmp/dsm || return $?
|
||||
rm -f "$QEMU_DIR/dsm.url" || return $?
|
||||
|
||||
checkSse42 || return $?
|
||||
reservePorts || return $?
|
||||
prepareMac || return $?
|
||||
|
||||
local patName="boot.pat"
|
||||
local patDir patFile
|
||||
|
||||
# Persist the exact PAT base name so future starts reopen the matching boot,
|
||||
# system, and cached installation files.
|
||||
if [ -f "$STORAGE/dsm.ver" ]; then
|
||||
BASE=$(<"$STORAGE/dsm.ver") || return $?
|
||||
BASE="${BASE//[![:print:]]/}"
|
||||
[ -z "$BASE" ] && BASE="DSM_VirtualDSM_69057"
|
||||
else
|
||||
# Fallback for old installs
|
||||
BASE="DSM_VirtualDSM_42962"
|
||||
fi
|
||||
|
||||
patDir=$(find / -maxdepth 1 -type d -iname "$patName" -print -quit) || return $?
|
||||
|
||||
if [ ! -d "$patDir" ]; then
|
||||
patDir=$(find "$STORAGE" -maxdepth 1 -type d -iname "$patName" -print -quit) || return $?
|
||||
fi
|
||||
|
||||
# A boot.pat directory bind represents already extracted boot and system
|
||||
# images and therefore takes precedence over PAT file or URL discovery.
|
||||
if [ -d "$patDir" ]; then
|
||||
BASE="DSM_VirtualDSM"
|
||||
URL="file://$patDir"
|
||||
|
||||
if [[ ! -s "$STORAGE/$BASE.boot.img" || ! -s "$STORAGE/$BASE.system.img" ]]; then
|
||||
error "The bind $patDir maps to a file that does not exist!"
|
||||
return 65
|
||||
fi
|
||||
fi
|
||||
|
||||
patFile=$(find / -maxdepth 1 -type f -iname "$patName" -print -quit) || return $?
|
||||
|
||||
if [ ! -s "$patFile" ]; then
|
||||
patFile=$(find "$STORAGE" -maxdepth 1 -type f -iname "$patName" -print -quit) || return $?
|
||||
fi
|
||||
|
||||
if [ -s "$patFile" ]; then
|
||||
BASE="DSM_VirtualDSM"
|
||||
URL="file://$patFile"
|
||||
fi
|
||||
|
||||
URL=$(strip "$URL") || return $?
|
||||
|
||||
# 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 "$patFile" ] && [ ! -d "$patDir" ]; then
|
||||
BASE=$(basename "$URL" .pat) || return $?
|
||||
|
||||
if [ ! -s "$STORAGE/$BASE.system.img" ]; then
|
||||
BASE=$(sanitizePatBase "$URL") || return $?
|
||||
fi
|
||||
|
||||
if [[ "${URL,,}" != "http"* && "${URL,,}" != "file:"* ]]; then
|
||||
if [ ! -s "$STORAGE/$BASE.pat" ]; then
|
||||
error "Invalid URL: $URL"
|
||||
return 65
|
||||
fi
|
||||
|
||||
URL="file://$STORAGE/$BASE.pat"
|
||||
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
|
||||
return 0 # Previous installation found
|
||||
fi
|
||||
|
||||
html "Please wait while Virtual DSM is being installed..." || return $?
|
||||
|
||||
local mirror=""
|
||||
local chinaMirror="https://cndl.synology.cn/download/DSM"
|
||||
local globalMirror="https://global.synologydownload.com/download/DSM"
|
||||
|
||||
[[ "${URL,,}" == *"cndl.synology"* ]] && mirror="$chinaMirror"
|
||||
[[ "${URL,,}" == *"global.synology"* ]] && mirror="$globalMirror"
|
||||
|
||||
# Honor an explicitly selected Synology mirror first, otherwise choose the
|
||||
# China or global endpoint from the detected country.
|
||||
if [ -z "$mirror" ]; then
|
||||
if [ -z "$COUNTRY" ]; then
|
||||
setCountry || return $?
|
||||
fi
|
||||
|
||||
[ -z "$COUNTRY" ] && info "Warning: could not detect country to select mirror!"
|
||||
[[ "${COUNTRY^^}" == "CN" ]] && mirror="$chinaMirror" || mirror="$globalMirror"
|
||||
fi
|
||||
|
||||
if [ -z "$URL" ]; then
|
||||
URL="$mirror/release/7.2.2/72806/DSM_VirtualDSM_72806.pat"
|
||||
fi
|
||||
|
||||
if [ ! -s "$patFile" ]; then
|
||||
BASE=$(sanitizePatBase "$URL") || return $?
|
||||
fi
|
||||
|
||||
if [[ "$URL" != "file://$STORAGE/$BASE.pat" ]]; then
|
||||
rm -f "$STORAGE/$BASE.pat" || return $?
|
||||
fi
|
||||
|
||||
rm -f "$STORAGE/$BASE.agent" || return $?
|
||||
rm -f "$STORAGE/$BASE.boot.img" || return $?
|
||||
rm -f "$STORAGE/$BASE.system.img" || return $?
|
||||
|
||||
# Check filesystem
|
||||
local fs
|
||||
fs=$(stat -f -c %T "$STORAGE") || return $?
|
||||
checkDsmFilesystem "$fs" || return $?
|
||||
|
||||
local tmp
|
||||
|
||||
# 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
|
||||
tmp="$STORAGE/tmp"
|
||||
rm -rf "$tmp" || return $?
|
||||
|
||||
if ! makeDir "$tmp"; then
|
||||
error "Failed to create directory \"$tmp\" !"
|
||||
return 93
|
||||
fi
|
||||
else
|
||||
tmp="/tmp/dsm"
|
||||
local tmpSpace=2147483648
|
||||
local space available
|
||||
|
||||
space=$(df --output=avail -B 1 /tmp | tail -n 1) || return $?
|
||||
available=$(formatBytes "$space") || return $?
|
||||
|
||||
if (( tmpSpace > space )); then
|
||||
error "Not enough free space inside the container, have $available available but need at least 2 GB."
|
||||
return 93
|
||||
fi
|
||||
|
||||
rm -rf "$tmp" || return $?
|
||||
mkdir -p "$tmp" || return $?
|
||||
fi
|
||||
|
||||
# Check free diskspace
|
||||
checkDsmSpace || return $?
|
||||
|
||||
local pat="/$BASE.pat"
|
||||
downloadPat "$pat" || return $?
|
||||
|
||||
local size
|
||||
size=$(stat -c%s "$pat") || return $?
|
||||
|
||||
# Full Virtual DSM PAT files are substantially larger than update packs;
|
||||
# reject undersized inputs before attempting destructive image preparation.
|
||||
if (( size < 250000000 )); then
|
||||
error "The specified PAT file is probably an update pack as it's too small."
|
||||
return 62
|
||||
fi
|
||||
|
||||
extractPat "$pat" "$tmp" "$size" || return $?
|
||||
createSystemImage "$tmp" "$fs" || return $?
|
||||
installSystemPartition "$tmp" || return $?
|
||||
finishDsmInstall "$pat" "$tmp" || return $?
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
installDSM || exit $?
|
||||
return 0
|
||||
|
||||
-235
@@ -1,235 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
normalizeMemory() {
|
||||
|
||||
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
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
checkConfiguredMemory() {
|
||||
|
||||
local final="$1"
|
||||
|
||||
if disabled "$RAM_CHECK" || [[ "${RAM_SIZE,,}" == "max" || "${RAM_SIZE,,}" == "half" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local wanted avail_mem
|
||||
wanted=$(numfmt --from=iec "$RAM_SIZE")
|
||||
avail_mem=$(formatBytes "$RAM_AVAIL")
|
||||
|
||||
if (( (wanted + RAM_SPARE) > RAM_AVAIL )); then
|
||||
|
||||
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
|
||||
|
||||
enabled "$final" && info "$msg but since ZFS is active this will be ignored."
|
||||
|
||||
else
|
||||
|
||||
RAM_SIZE="max"
|
||||
RAM_WARNING="$msg it will automatically be adjusted to a lower amount."
|
||||
|
||||
fi
|
||||
|
||||
else
|
||||
|
||||
if (( (wanted + (RAM_SPARE * 3)) > RAM_AVAIL )); then
|
||||
|
||||
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
|
||||
enabled "$final" && info "$msg but since ZFS is active this will be ignored."
|
||||
else
|
||||
enabled "$final" && warn "$msg please consider a lower amount."
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
configureHalfMemory() {
|
||||
|
||||
if [[ "${RAM_SIZE,,}" != "half" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if (( (RAM_AVAIL / 2) > RAM_SPARE )); then
|
||||
|
||||
local wanted=$(( RAM_AVAIL / 2 ))
|
||||
|
||||
# Divide by one byte more than a MiB to round down
|
||||
local target=$(( wanted / 1048577 ))
|
||||
RAM_SIZE="${target}M"
|
||||
RAM_ALLOCATION="$wanted"
|
||||
|
||||
else
|
||||
|
||||
RAM_SIZE="max"
|
||||
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
configureMaxMemory() {
|
||||
|
||||
if [[ "${RAM_SIZE,,}" != "max" ]]; then
|
||||
return 0
|
||||
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
|
||||
|
||||
local wanted=$(( RAM_AVAIL / 2 ))
|
||||
|
||||
else
|
||||
|
||||
local wanted=$(( RAM_AVAIL - (RAM_SPARE * 3) ))
|
||||
|
||||
if (( wanted < (RAM_SPARE * 6) )); then
|
||||
wanted=$(( RAM_AVAIL - RAM_SPARE ))
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# Divide by one byte more than a MiB to round down
|
||||
local target=$(( wanted / 1048577 ))
|
||||
RAM_SIZE="${target}M"
|
||||
RAM_ALLOCATION="$wanted"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
checkMinimumMemory() {
|
||||
|
||||
local wanted
|
||||
wanted=$(numfmt --from=iec "$RAM_SIZE")
|
||||
|
||||
if [ "$wanted" -lt "$RAM_MINIMUM" ]; then
|
||||
|
||||
error "$(app) requires at least $(formatBytes "$RAM_MINIMUM") of RAM, but only $(formatBytes "$wanted") can be allocated."
|
||||
|
||||
showMemoryLimitHint
|
||||
|
||||
exit 16
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
checkMemoryAllocation() {
|
||||
|
||||
local final="${1:-N}"
|
||||
local configured
|
||||
|
||||
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
|
||||
-2358
File diff suppressed because it is too large
Load Diff
+4
-193
@@ -17,198 +17,6 @@ CONSOLE_PID="$QEMU_DIR/console.pid"
|
||||
CONSOLE_SOCKET="$QEMU_DIR/console.sock"
|
||||
QEMU_START_PID="$QEMU_DIR/qemu.start.pid"
|
||||
|
||||
_trap() {
|
||||
|
||||
local func="$1"; shift
|
||||
local sig
|
||||
|
||||
TRAP_PID=$BASHPID
|
||||
|
||||
for sig; do
|
||||
# Capture the local callback and signal while registering the trap.
|
||||
# shellcheck disable=SC2064
|
||||
trap "$func $sig" "$sig"
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
signalCode() {
|
||||
|
||||
local sig="$1"
|
||||
|
||||
case "$sig" in
|
||||
SIGHUP) echo 129 ;;
|
||||
SIGINT) echo 130 ;;
|
||||
SIGQUIT) echo 131 ;;
|
||||
SIGABRT) echo 134 ;;
|
||||
SIGTERM) echo 143 ;;
|
||||
*) echo 0 ;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
displayReason() {
|
||||
|
||||
local reason="$1"
|
||||
|
||||
case "$reason" in
|
||||
129 ) echo "SIGHUP" ;;
|
||||
130 ) echo "SIGINT" ;;
|
||||
131 ) echo "SIGQUIT" ;;
|
||||
134 ) echo "SIGABRT" ;;
|
||||
143 ) echo "SIGTERM" ;;
|
||||
* ) echo "$reason" ;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
readQemuPid() {
|
||||
|
||||
# Interactive startup uses a wrapper-created PID file before QEMU writes its
|
||||
# own pidfile, so accept either during startup and shutdown races.
|
||||
readPidFile "$1" "$QEMU_START_PID" && return 0
|
||||
readPidFile "$1" "$QEMU_PID"
|
||||
}
|
||||
|
||||
qemuPidFile() {
|
||||
|
||||
local -n _file="$1"
|
||||
|
||||
_file="$QEMU_PID"
|
||||
[ -s "$QEMU_START_PID" ] && _file="$QEMU_START_PID"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
waitQemuExit() {
|
||||
|
||||
local timeout="${1:-10}"
|
||||
local file
|
||||
|
||||
qemuPidFile file
|
||||
waitPidFile "$file" "$timeout"
|
||||
}
|
||||
|
||||
waitQemuPid() {
|
||||
|
||||
local cnt=0
|
||||
|
||||
while ! readQemuPid "$1"; do
|
||||
sleep 0.02
|
||||
cnt=$((cnt + 1))
|
||||
(( cnt >= 50 )) && return 1
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
forceKillQemu() {
|
||||
|
||||
local reason="$1"
|
||||
local pid display
|
||||
|
||||
readQemuPid pid || return 0
|
||||
isAlive "$pid" || return 0
|
||||
|
||||
display=$(displayReason "$reason")
|
||||
error "Forcefully terminating $(app), reason: $display..."
|
||||
{ disown "$pid" || :; kill -9 -- "$pid" || :; } 2>/dev/null
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
cleanupHelpers() {
|
||||
|
||||
local pids=( "${HOST_PID:-}" "${WSD_PID:-}" "${CONSOLE_PID:-}" \
|
||||
"${WEB_PID:-}" "${PASST_PID:-}" "${DNSMASQ_PID:-}" )
|
||||
|
||||
mKill "${pids[@]}"
|
||||
fKill "print.sh"
|
||||
|
||||
rm -f -- "$HOST_API_SOCKET" "$HOST_AGENT_SOCKET"
|
||||
|
||||
closeNetwork
|
||||
return 0
|
||||
}
|
||||
|
||||
startConsole() {
|
||||
|
||||
local output="${1:-/dev/tty}"
|
||||
local cnt=0
|
||||
|
||||
rm -f -- "$CONSOLE_SOCKET" "$CONSOLE_PID"
|
||||
|
||||
if ! stty -icanon -echo isig -ixon min 1 time 0 </dev/tty; then
|
||||
error "Failed to configure serial console terminal!"
|
||||
return 1
|
||||
fi
|
||||
|
||||
(
|
||||
trap '' INT QUIT
|
||||
exec nc -lU "$CONSOLE_SOCKET" </dev/tty >"$output"
|
||||
) &
|
||||
|
||||
local pid="$!"
|
||||
echo "$pid" > "$CONSOLE_PID"
|
||||
|
||||
while [ ! -S "$CONSOLE_SOCKET" ]; do
|
||||
|
||||
if ! isAlive "$pid"; then
|
||||
rm -f -- "$CONSOLE_PID"
|
||||
error "Serial console relay exited unexpectedly!"
|
||||
return 1
|
||||
fi
|
||||
|
||||
sleep 0.02
|
||||
cnt=$((cnt + 1))
|
||||
|
||||
if (( cnt > 100 )); then
|
||||
error "Failed to start serial console relay!"
|
||||
return 1
|
||||
fi
|
||||
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
stopConsole() {
|
||||
|
||||
mKill "$CONSOLE_PID"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
startQemu() {
|
||||
|
||||
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
|
||||
|
||||
# shellcheck disable=SC2016
|
||||
exec setsid -f -w sh -c '
|
||||
file=$1
|
||||
shift
|
||||
|
||||
"$@" &
|
||||
pid=$!
|
||||
printf "%s\n" "$pid" > "$file" || exit 1
|
||||
|
||||
rc=0
|
||||
wait "$pid" 2>/dev/null || rc=$?
|
||||
exit "$rc"
|
||||
' sh "$QEMU_START_PID" "$@"
|
||||
) </dev/null &
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
finish() {
|
||||
|
||||
local reason=$1 failed=0
|
||||
@@ -220,7 +28,10 @@ finish() {
|
||||
touch "$QEMU_END" || :
|
||||
|
||||
forceKillQemu "$reason"
|
||||
cleanupHelpers
|
||||
cleanupHelpers "$HOST_PID"
|
||||
fKill "print.sh"
|
||||
|
||||
rm -f -- "$HOST_API_SOCKET" "$HOST_AGENT_SOCKET"
|
||||
|
||||
if ! waitQemuExit 10; then
|
||||
warn "Timed out while waiting for $(app) to exit!"
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
addSystemDisks() {
|
||||
|
||||
local boot="$STORAGE/$BASE.boot.img"
|
||||
local system="$STORAGE/$BASE.system.img"
|
||||
|
||||
[ ! -s "$boot" ] && error "Virtual DSM boot-image does not exist ($boot)" && return 81
|
||||
[ ! -s "$system" ] && error "Virtual DSM system-image does not exist ($system)" && return 82
|
||||
|
||||
setOwner "$boot" || warn "failed to set the owner for \"$boot\" !"
|
||||
setOwner "$system" || warn "failed to set the owner for \"$system\" !"
|
||||
|
||||
DISK_OPTS+=$(createDevice "$boot" "$DISK_TYPE" "1" "0xa" "raw" "$DISK_IO" "$DISK_CACHE" "" "" "synoboot")
|
||||
DISK_OPTS+=$(createDevice "$system" "$DISK_TYPE" "2" "0xb" "raw" "$DISK_IO" "$DISK_CACHE" "" "" "synosys")
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
closeWebserver() {
|
||||
|
||||
disabled "${NETWORK:-Y}" && return 0
|
||||
|
||||
MSG="Booting DSM instance..."
|
||||
html "$MSG" || return $?
|
||||
|
||||
if enabled "${DHCP:-N}" || disabled "${WEB:-}"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
writeAtomic "$WSD_COMMAND" "portal" || return $?
|
||||
sleep 1.2
|
||||
|
||||
local pids=( "${WEB_PID:-}" "${WSD_PID:-}" )
|
||||
mKill "${pids[@]}"
|
||||
|
||||
WEB="N"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
addSystemDisks || return $?
|
||||
closeWebserver || return $?
|
||||
|
||||
return 0
|
||||
+1
-1
@@ -18,7 +18,7 @@ file="/run/shm/dsm.url"
|
||||
msgs="/run/shm/msg.html"
|
||||
driver="/run/shm/qemu.nic"
|
||||
page="/run/shm/index.html"
|
||||
address="/run/shm/qemu.ip"
|
||||
address="/run/shm/qemu.host"
|
||||
shutdown="/run/shm/qemu.end"
|
||||
command="/run/shm/status.cmd"
|
||||
socket="/run/shm/qemu-host-api.sock"
|
||||
|
||||
-209
@@ -1,209 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
# Docker environment variables
|
||||
|
||||
: "${HOST_CPU:=""}"
|
||||
: "${CPU_FLAGS:=""}"
|
||||
: "${CPU_MODEL:=""}"
|
||||
|
||||
HOST_CPU=$(strip "$HOST_CPU")
|
||||
CPU_FLAGS=$(strip "$CPU_FLAGS")
|
||||
CPU_MODEL=$(strip "$CPU_MODEL")
|
||||
|
||||
selectClocksource() {
|
||||
|
||||
CLOCKSOURCE="tsc"
|
||||
[[ "${ARCH,,}" == "arm64" ]] && CLOCKSOURCE="arch_sys_counter"
|
||||
CLOCK="/sys/devices/system/clocksource/clocksource0/current_clocksource"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
checkClocksource() {
|
||||
|
||||
local result
|
||||
|
||||
if [ ! -f "$CLOCK" ]; then
|
||||
warn "file \"$CLOCK\" cannot be found?"
|
||||
return 0
|
||||
fi
|
||||
|
||||
result=$(<"$CLOCK")
|
||||
result="${result//[![:print:]]/}"
|
||||
|
||||
case "${result,,}" in
|
||||
"${CLOCKSOURCE,,}" ) ;;
|
||||
"kvm-clock" ) info "Nested KVM virtualization detected.." ;;
|
||||
"hyperv_clocksource_tsc_page" ) info "Nested Hyper-V virtualization detected.." ;;
|
||||
"hpet" ) warn "unsupported clock source detected: '$result'. Please set host clock source to '$CLOCKSOURCE'." ;;
|
||||
*) warn "unexpected clock source detected: '$result'. Please set host clock source to '$CLOCKSOURCE'." ;;
|
||||
esac
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
checkSse42() {
|
||||
|
||||
if ! hasFlag "sse4_2"; then
|
||||
error "Your CPU does not have the SSE4 instruction set that Virtual DSM requires!"
|
||||
enabled "$DEBUG" || exit 88
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
trimSpaces() {
|
||||
|
||||
local value="$1"
|
||||
|
||||
value="${value#"${value%%[![:space:]]*}"}"
|
||||
value="${value%"${value##*[![:space:]]}"}"
|
||||
|
||||
echo "$value"
|
||||
return 0
|
||||
}
|
||||
|
||||
removeCpuArgument() {
|
||||
|
||||
# CPU configuration has dedicated variables. Remove raw -cpu arguments so
|
||||
# option ordering cannot silently override the validated model and flags.
|
||||
local args=" ${ARGUMENTS:-} "
|
||||
|
||||
while [[ "$args" =~ [[:space:]]-cpu([[:space:]][^[:space:]]+|=[^[:space:]]+)? ]]; do
|
||||
local cpu="${BASH_REMATCH[0]}"
|
||||
args="${args/$cpu/ }"
|
||||
warn "Ignoring '${cpu#" "}' from ARGUMENTS, use CPU_MODEL and CPU_FLAGS instead."
|
||||
done
|
||||
|
||||
ARGUMENTS=$(trimSpaces "$args")
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
configureKvmCpuModel() {
|
||||
|
||||
CPU_FEATURES="kvm=on,l3-cache=on,+hypervisor"
|
||||
KVM_OPTS=",accel=kvm -enable-kvm -global kvm-pit.lost_tick_policy=discard"
|
||||
|
||||
if [ -z "$CPU_MODEL" ]; then
|
||||
CPU_MODEL="host"
|
||||
CPU_FEATURES+=",migratable=no"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
# AMD processor
|
||||
if hasFlag "tsc_scale"; then
|
||||
CPU_FEATURES+=",+invtsc"
|
||||
fi
|
||||
|
||||
else
|
||||
|
||||
# Intel processor
|
||||
local vmx
|
||||
vmx=$(sed -ne '/^vmx flags/s/^.*: //p' /proc/cpuinfo)
|
||||
|
||||
if grep -qw "tsc_scaling" <<< "$vmx"; then
|
||||
CPU_FEATURES+=",+invtsc"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
configureKvm() {
|
||||
|
||||
configureKvmCpuModel
|
||||
checkSse42
|
||||
appendKvmInvtscFeature
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
configureTcgCpuModel() {
|
||||
|
||||
if [ -n "$CPU_MODEL" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# TCG uses the broad max model on native x86, but qemu64 is the compatible
|
||||
# cross-architecture fallback.
|
||||
if [[ "$ARCH" == "amd64" ]]; then
|
||||
CPU_MODEL="max"
|
||||
CPU_FEATURES+=",migratable=no"
|
||||
else
|
||||
CPU_MODEL="qemu64"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
configureTcg() {
|
||||
|
||||
KVM_OPTS=""
|
||||
CPU_FEATURES="l3-cache=on,+hypervisor"
|
||||
|
||||
if [[ "$ARCH" == "amd64" ]]; then
|
||||
KVM_OPTS=" -accel tcg,thread=multi"
|
||||
fi
|
||||
|
||||
configureTcgCpuModel
|
||||
CPU_FEATURES+=",+ssse3,+sse4.1,+sse4.2"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
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}"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
configureHostCpuName() {
|
||||
|
||||
if [ -z "$HOST_CPU" ]; then
|
||||
[[ "${CPU,,}" != "unknown" ]] && HOST_CPU="$CPU"
|
||||
fi
|
||||
|
||||
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%%,*},,"
|
||||
else
|
||||
HOST_CPU="QEMU, Virtual CPU,"
|
||||
if [ "$ARCH" == "amd64" ]; then
|
||||
HOST_CPU+=" X86_64"
|
||||
else
|
||||
HOST_CPU+=" $ARCH"
|
||||
fi
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
selectClocksource
|
||||
checkClocksource
|
||||
|
||||
if ! disabled "$KVM"; then
|
||||
configureKvm
|
||||
else
|
||||
configureTcg
|
||||
fi
|
||||
|
||||
removeCpuArgument
|
||||
composeCpuFlags
|
||||
configureHostCpuName
|
||||
|
||||
return 0
|
||||
-310
@@ -1,310 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
info="/run/shm/msg.html"
|
||||
info_tmp="${info}.${BASHPID}.tmp"
|
||||
|
||||
escape() {
|
||||
|
||||
local s
|
||||
|
||||
s=${1//&/\&}
|
||||
s=${s//</\<}
|
||||
s=${s//>/\>}
|
||||
s=${s//'"'/\"}
|
||||
s=${s//"'"/\'}
|
||||
|
||||
printf '%s' "$s"
|
||||
return 0
|
||||
}
|
||||
|
||||
writeInfo() {
|
||||
|
||||
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
|
||||
rm -f -- "$info_tmp"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! mv -f -- "$info_tmp" "$info"; then
|
||||
rm -f -- "$info_tmp"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
getBytes() {
|
||||
|
||||
local path="$1"
|
||||
local mode="$2"
|
||||
local bytes="0"
|
||||
|
||||
if [[ "$mode" == "counter" ]]; then
|
||||
if [ -r "$path" ]; then
|
||||
read -r bytes < "$path" || bytes="0"
|
||||
fi
|
||||
|
||||
[[ "$bytes" =~ ^[0-9]+$ ]] || bytes="0"
|
||||
printf '%s\n' "$bytes"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ ! -s "$path" ] && [ ! -d "$path" ]; then
|
||||
printf '0\n'
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$mode" == "allocated" ]]; then
|
||||
bytes=$(du -sB1 -- "$path" 2>/dev/null | cut -f1) || bytes="0"
|
||||
else
|
||||
bytes=$(du -sb -- "$path" 2>/dev/null | cut -f1) || bytes="0"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$bytes"
|
||||
return 0
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
|
||||
local file="$1"
|
||||
local bytes total extra=""
|
||||
|
||||
[ -r "$file" ] || return 1
|
||||
read -r bytes total extra < "$file" || return 1
|
||||
|
||||
if [[ ! "$bytes" =~ ^[0-9]+$ ||
|
||||
! "$total" =~ ^[0-9]+$ ||
|
||||
-n "$extra" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s %s\n' "$bytes" "$total"
|
||||
return 0
|
||||
}
|
||||
|
||||
formatSize() {
|
||||
|
||||
local bytes="$1"
|
||||
local size
|
||||
|
||||
size=$(numfmt --to=iec --suffix=B "$bytes" |
|
||||
sed -r 's/([A-Z])/ \1/') ||
|
||||
size="${bytes} bytes"
|
||||
|
||||
printf '%s' "$size"
|
||||
return 0
|
||||
}
|
||||
|
||||
printPercentProgress() {
|
||||
|
||||
local percent="$1"
|
||||
|
||||
while (( next_percent <= percent && next_percent <= 100 )); do
|
||||
if [[ "$printed" == "Y" ]]; then
|
||||
printf ' → %s%%' "$next_percent"
|
||||
else
|
||||
printf '%s%%' "$next_percent"
|
||||
fi
|
||||
|
||||
printed="Y"
|
||||
next_percent=$((next_percent + 10))
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
printCurrentSize() {
|
||||
|
||||
local bytes="$1"
|
||||
local size
|
||||
|
||||
size=$(formatSize "$bytes")
|
||||
|
||||
if [[ "$printed" == "Y" ]]; then
|
||||
printf ' → %s' "$size"
|
||||
else
|
||||
printf '%s' "$size"
|
||||
fi
|
||||
|
||||
printed="Y"
|
||||
return 0
|
||||
}
|
||||
|
||||
printSizeProgress() {
|
||||
|
||||
local bytes="$1"
|
||||
local size
|
||||
|
||||
while (( bytes >= next_bytes )); do
|
||||
size=$(formatSize "$next_bytes")
|
||||
|
||||
if [[ "$printed" == "Y" ]]; then
|
||||
printf ' → %s' "$size"
|
||||
else
|
||||
printf '%s' "$size"
|
||||
fi
|
||||
|
||||
printed="Y"
|
||||
next_bytes=$((next_bytes + step_bytes))
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
stopProgress() {
|
||||
|
||||
if [ -z "$status_file" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
stopping="Y"
|
||||
return 0
|
||||
}
|
||||
|
||||
finishProgress() {
|
||||
|
||||
rm -f -- "$info_tmp"
|
||||
|
||||
if [[ "$output" == "log" && "$printed" == "Y" ]]; then
|
||||
printf '\n'
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
path="$1"
|
||||
total="$2"
|
||||
body=$(escape "$3")
|
||||
output="${4:-}"
|
||||
step_bytes="${5:-536870912}"
|
||||
mode="${6:-apparent}"
|
||||
status_file="${7:-}"
|
||||
|
||||
if [[ -n "$total" && ! "$total" =~ ^(0|[1-9][0-9]*)$ ]]; then
|
||||
printf 'Invalid total size: %s\n' "$total" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ ! "$step_bytes" =~ ^[1-9][0-9]*$ ]]; then
|
||||
printf 'Invalid progress interval: %s\n' "$step_bytes" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
case "$mode" in
|
||||
apparent | allocated | counter ) ;;
|
||||
* )
|
||||
printf 'Invalid progress mode: %s\n' "$mode" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$output" in
|
||||
"" | log ) ;;
|
||||
* )
|
||||
printf 'Invalid progress output: %s\n' "$output" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
printed="N"
|
||||
next_percent=10
|
||||
next_bytes="$step_bytes"
|
||||
log_mode="percent"
|
||||
stopping="N"
|
||||
|
||||
if [ -z "$total" ] || [[ "$total" == "0" ]]; then
|
||||
log_mode="size"
|
||||
fi
|
||||
|
||||
trap finishProgress EXIT
|
||||
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
|
||||
body="<p class=\"loading\">${body::-3}</p>"
|
||||
fi
|
||||
|
||||
while true; do
|
||||
|
||||
final_pass="${stopping:-}"
|
||||
bytes=$(getBytes "$path" "$mode")
|
||||
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
|
||||
read -r status_bytes status_total <<< "$status"
|
||||
bytes="$status_bytes"
|
||||
|
||||
if (( status_total > 0 )); then
|
||||
effective_total="$status_total"
|
||||
fi
|
||||
fi
|
||||
|
||||
# A real total may become available shortly after aria2 starts.
|
||||
if [[ "$log_mode" == "size" &&
|
||||
"$printed" == "N" &&
|
||||
-n "$effective_total" &&
|
||||
"$effective_total" != "0" ]]; then
|
||||
log_mode="percent"
|
||||
fi
|
||||
|
||||
if (( bytes > 4096 )); then
|
||||
|
||||
write_html="Y"
|
||||
|
||||
if [ -z "$effective_total" ] ||
|
||||
[[ "$effective_total" == "0" ]] ||
|
||||
(( bytes > effective_total )); then
|
||||
size=$(formatSize "$bytes")
|
||||
|
||||
if [[ "$output" == "log" ]]; then
|
||||
if [[ "$log_mode" == "percent" ]]; then
|
||||
printCurrentSize "$bytes"
|
||||
next_bytes=$(((bytes / step_bytes + 1) * step_bytes))
|
||||
log_mode="size"
|
||||
else
|
||||
printSizeProgress "$bytes"
|
||||
fi
|
||||
fi
|
||||
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.
|
||||
progress=$((bytes * 1000 / effective_total))
|
||||
(( progress > 1000 )) && progress=1000
|
||||
|
||||
percent=$((progress / 10))
|
||||
|
||||
printf -v size '%d.%d%%' \
|
||||
"$((progress / 10))" \
|
||||
"$((progress % 10))"
|
||||
|
||||
if [[ "$output" == "log" ]]; then
|
||||
if [[ "$log_mode" == "size" ]]; then
|
||||
printSizeProgress "$bytes"
|
||||
else
|
||||
printPercentProgress "$percent"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Do not update the web viewer until at least 0.1% is reached.
|
||||
(( progress == 0 )) && write_html="N"
|
||||
fi
|
||||
|
||||
if [[ "$write_html" == "Y" ]]; then
|
||||
writeInfo "${body//(\[P\])/($size)}"
|
||||
fi
|
||||
fi
|
||||
|
||||
[[ "$final_pass" == "Y" ]] && break
|
||||
|
||||
sleep 1 &
|
||||
wait $! || :
|
||||
done
|
||||
+28
-3
@@ -4,6 +4,7 @@ set -Eeuo pipefail
|
||||
# Docker environment variables
|
||||
|
||||
: "${HOST_MAC:=""}"
|
||||
: "${HOST_CPU:=""}"
|
||||
: "${HOST_DEBUG:=""}"
|
||||
: "${HOST_MODEL:=""}"
|
||||
: "${HOST_SERIAL:=""}"
|
||||
@@ -11,6 +12,7 @@ set -Eeuo pipefail
|
||||
|
||||
# Sanitize variables
|
||||
HOST_MAC=$(strip "$HOST_MAC")
|
||||
HOST_CPU=$(strip "$HOST_CPU")
|
||||
HOST_MODEL=$(strip "$HOST_MODEL")
|
||||
HOST_SERIAL=$(strip "$HOST_SERIAL")
|
||||
GUEST_SERIAL=$(strip "$GUEST_SERIAL")
|
||||
@@ -20,7 +22,6 @@ HOST_API_SOCKET="$QEMU_DIR/qemu-host-api.sock"
|
||||
HOST_AGENT_SOCKET="$QEMU_DIR/qemu-host-agent.sock"
|
||||
|
||||
validateHostMac() {
|
||||
local m
|
||||
|
||||
if [ -z "$HOST_MAC" ]; then
|
||||
return 0
|
||||
@@ -29,12 +30,35 @@ validateHostMac() {
|
||||
HOST_MAC="${HOST_MAC//-/:}"
|
||||
|
||||
if [[ ${#HOST_MAC} == 12 ]]; then
|
||||
m="$HOST_MAC"
|
||||
local m="$HOST_MAC"
|
||||
HOST_MAC="${m:0:2}:${m:2:2}:${m:4:2}:${m:6:2}:${m:8:2}:${m:10:2}"
|
||||
fi
|
||||
|
||||
if [[ ${#HOST_MAC} != 17 ]]; then
|
||||
error "Invalid HOST_MAC address: '$HOST_MAC', should be 12 or 17 digits long!" && exit 28
|
||||
error "Invalid HOST_MAC address: '$HOST_MAC', should be 12 or 17 digits long!"
|
||||
exit 28
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
configureHostCpuName() {
|
||||
|
||||
if [ -z "$HOST_CPU" ]; then
|
||||
[[ "${CPU,,}" != "unknown" ]] && HOST_CPU="$CPU"
|
||||
fi
|
||||
|
||||
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%%,*},,"
|
||||
else
|
||||
HOST_CPU="QEMU, Virtual CPU,"
|
||||
if [ "$ARCH" == "amd64" ]; then
|
||||
HOST_CPU+=" X86_64"
|
||||
else
|
||||
HOST_CPU+=" $ARCH"
|
||||
fi
|
||||
fi
|
||||
|
||||
return 0
|
||||
@@ -141,6 +165,7 @@ configureSerialPorts() {
|
||||
}
|
||||
|
||||
validateHostMac
|
||||
configureHostCpuName
|
||||
|
||||
buildHostArguments
|
||||
startHostBinary
|
||||
|
||||
-186
@@ -1,186 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
: "${WEB_PORT:="5000"}" # Webserver port
|
||||
|
||||
# Sanitize port variables
|
||||
WEB_PORT=$(strip "$WEB_PORT")
|
||||
|
||||
WEB_PID="/run/nginx.pid"
|
||||
WSD_LOG="/var/log/websocketd.log"
|
||||
WSD_COMMAND="$QEMU_DIR/status.cmd"
|
||||
WSD_PID="$QEMU_DIR/websocketd.pid"
|
||||
WSD_SOCKET="$QEMU_DIR/status-ws.sock"
|
||||
|
||||
prepareWebFiles() {
|
||||
|
||||
cp -r /var/www/* "$QEMU_DIR" || return 1
|
||||
rm -f -- "$WSD_PID" "$WSD_SOCKET" "$WSD_COMMAND" "$WEB_PID" "$WSD_LOG" || return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
configureWebPorts() {
|
||||
|
||||
if ! sed -i \
|
||||
-e "s|listen 5000 default_server;|listen $WEB_PORT default_server;|g" \
|
||||
/etc/nginx/sites-enabled/web.conf; then
|
||||
error "Failed to configure webserver port!"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
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 ! sed -i \
|
||||
"s/listen $WEB_PORT default_server;/listen [::]:$WEB_PORT default_server ipv6only=off;/g" \
|
||||
/etc/nginx/sites-enabled/web.conf; then
|
||||
error "Failed to configure IPv6 webserver listener!"
|
||||
return 1
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
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() {
|
||||
|
||||
configureNginx || return 1
|
||||
configureWebPorts || return 1
|
||||
configureIpv6Listen || return 1
|
||||
|
||||
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() {
|
||||
|
||||
# Start webserver
|
||||
nginx -e stderr || return 1
|
||||
|
||||
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() {
|
||||
|
||||
# Start websocket server
|
||||
websocketd \
|
||||
--unixsocket="$WSD_SOCKET" \
|
||||
/run/socket.sh \
|
||||
>"$WSD_LOG" 2>&1 &
|
||||
|
||||
local pid=$!
|
||||
|
||||
if ! echo "$pid" > "$WSD_PID"; then
|
||||
kill "$pid" 2>/dev/null || :
|
||||
rm -f -- "$WSD_PID"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local i
|
||||
for (( i = 1; i <= 50; i++ )); do
|
||||
|
||||
if ! isAlive "$pid"; then
|
||||
rm -f -- "$WSD_PID" "$WSD_SOCKET"
|
||||
[ -s "$WSD_LOG" ] && cat "$WSD_LOG" >&2
|
||||
error "Failed to start websocket server!"
|
||||
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
|
||||
|
||||
rm -f -- "$WSD_PID" "$WSD_SOCKET"
|
||||
[ -s "$WSD_LOG" ] && cat "$WSD_LOG" >&2
|
||||
error "Websocket server did not create its socket!"
|
||||
return 1
|
||||
}
|
||||
|
||||
prepareWebFiles
|
||||
|
||||
html "Starting $APP for $ENGINE..."
|
||||
|
||||
disabled "${WEB:-}" && return 0
|
||||
|
||||
configureWebServer
|
||||
|
||||
if startWebServer && startWebsocketServer; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
stopWebsocketServer || :
|
||||
stopWebServer || :
|
||||
|
||||
return 1
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
# You can override this hook to execute a script before startup!
|
||||
|
||||
return 0
|
||||
-662
@@ -1,662 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
# Helper functions
|
||||
|
||||
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; }
|
||||
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() {
|
||||
|
||||
# Match a whitespace-delimited token in /proc/cpuinfo
|
||||
grep -m1 '^flags[[:space:]]*:' /proc/cpuinfo | grep -Fqw -- "$1"
|
||||
|
||||
}
|
||||
|
||||
hasFeature() {
|
||||
|
||||
# Match a whitespace-delimited token in /proc/cpuinfo
|
||||
grep -m1 '^Features[[:space:]]*:' /proc/cpuinfo | grep -Fqw -- "$1"
|
||||
|
||||
}
|
||||
|
||||
isAmdCpu() {
|
||||
|
||||
local vendor
|
||||
vendor=$(awk -F ': *' '/^vendor_id/{print $2; exit}' /proc/cpuinfo)
|
||||
|
||||
[[ "$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() {
|
||||
|
||||
# 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
|
||||
|
||||
}
|
||||
|
||||
strip() {
|
||||
|
||||
local value="${1:-}"
|
||||
|
||||
# Remove surrounding whitespace
|
||||
value="${value#"${value%%[![:space:]]*}"}"
|
||||
value="${value%"${value##*[![:space:]]}"}"
|
||||
|
||||
# Remove leading/trailing single/double quotes
|
||||
value="${value%\"}"
|
||||
value="${value#\"}"
|
||||
value="${value%\'}"
|
||||
value="${value#\'}"
|
||||
|
||||
# Remove surrounding whitespace again
|
||||
value="${value#"${value%%[![:space:]]*}"}"
|
||||
value="${value%"${value##*[![:space:]]}"}"
|
||||
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
enabled() {
|
||||
|
||||
local value
|
||||
value=$(strip "${1:-}")
|
||||
|
||||
case "${value,,}" in
|
||||
y|yes|true|1|on|enable|enabled) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
disabled() {
|
||||
|
||||
local value
|
||||
value=$(strip "${1:-}")
|
||||
|
||||
case "${value,,}" in
|
||||
n|no|none|false|0|off|disable|disabled) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
isAlive() {
|
||||
|
||||
local pid="$1"
|
||||
[ -z "$pid" ] && return 1
|
||||
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
waitPid() {
|
||||
|
||||
local pid="$1"
|
||||
local timeout="${2:-10}"
|
||||
local deadline=$((SECONDS + timeout))
|
||||
|
||||
while [ -n "$pid" ] && isAlive "$pid"; do
|
||||
(( SECONDS >= deadline )) && return 1
|
||||
sleep 0.2
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
waitPidFile() {
|
||||
|
||||
local pid
|
||||
local file="$1"
|
||||
local timeout="${2:-10}"
|
||||
local deadline=$((SECONDS + timeout))
|
||||
|
||||
readPidFile pid "$file" || return 0
|
||||
|
||||
while [ -s "$file" ] && isAlive "$pid"; do
|
||||
(( SECONDS >= deadline )) && return 1
|
||||
sleep 0.2
|
||||
done
|
||||
|
||||
rm -f -- "$file"
|
||||
return 0
|
||||
}
|
||||
|
||||
pKill() {
|
||||
|
||||
local pid="$1"
|
||||
local timeout="${2:-10}"
|
||||
|
||||
{ kill -15 -- "$pid" || :; } 2>/dev/null
|
||||
|
||||
if ! waitPid "$pid" "$timeout"; then
|
||||
warn "Timed out while waiting for PID $pid"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
fWait() {
|
||||
|
||||
local name="$1"
|
||||
local timeout="${2:-10}"
|
||||
local deadline=$((SECONDS + timeout))
|
||||
|
||||
[ -z "$name" ] && return 0
|
||||
|
||||
while pgrep -f -l "$name" >/dev/null; do
|
||||
if (( SECONDS >= deadline )); then
|
||||
warn "Timed out while waiting for process: $name"
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 0.2
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
fKill() {
|
||||
|
||||
local name="$1"
|
||||
local timeout="${2:-10}"
|
||||
|
||||
[ -z "$name" ] && return 0
|
||||
|
||||
{ pkill -f "$name" || :; } 2>/dev/null
|
||||
fWait "$name" "$timeout"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
sKill() {
|
||||
|
||||
local pid
|
||||
local file="$1"
|
||||
|
||||
readPidFile pid "$file" || return 0
|
||||
|
||||
if isAlive "$pid"; then
|
||||
{ kill -15 -- "$pid" || :; } 2>/dev/null
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
mKill() {
|
||||
|
||||
local timeout=10
|
||||
local files=("$@")
|
||||
|
||||
for file in "${files[@]}"; do
|
||||
sKill "$file"
|
||||
done
|
||||
|
||||
for file in "${files[@]}"; do
|
||||
if ! waitPidFile "$file" "$timeout"; then
|
||||
warn "Timed out while waiting for PID file: $file"
|
||||
fi
|
||||
done
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
baseDir() {
|
||||
|
||||
local path="${1%/}"
|
||||
|
||||
[[ -z "$path" || "$path" == "/" ]] && {
|
||||
echo "/"
|
||||
return 0
|
||||
}
|
||||
|
||||
path="${path#/}"
|
||||
path="${path%%/*}"
|
||||
|
||||
echo "/$path"
|
||||
return 0
|
||||
}
|
||||
|
||||
formatBytes() {
|
||||
|
||||
local result
|
||||
|
||||
if ! result=$(numfmt --to=iec --suffix=B "$1" | sed -r 's/([A-Z])/ \1/' | sed 's/ B/ bytes/g;'); then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local unit="${result//[0-9. ]}"
|
||||
result="${result//[a-zA-Z ]/}"
|
||||
|
||||
if [[ "${2:-}" == "up" ]]; then
|
||||
if [[ "$result" == *"."* ]]; then
|
||||
result="${result%%.*}"
|
||||
result=$((result+1))
|
||||
fi
|
||||
else
|
||||
if [[ "${2:-}" == "down" ]]; then
|
||||
result="${result%%.*}"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$result $unit"
|
||||
return 0
|
||||
}
|
||||
|
||||
setOwner() {
|
||||
|
||||
local file="$1"
|
||||
local dir uid gid
|
||||
|
||||
[ ! -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")
|
||||
uid=$(stat -c '%u' "$dir") || return 1
|
||||
gid=$(stat -c '%g' "$dir") || return 1
|
||||
|
||||
chown "$uid:$gid" "$file" || return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
makeDir() {
|
||||
|
||||
local path="$1"
|
||||
local dir uid gid
|
||||
|
||||
[ -d "$path" ] && return 0
|
||||
mkdir -p "$path" || return 1
|
||||
|
||||
dir=$(dirname -- "$path")
|
||||
|
||||
if ! uid=$(stat -c '%u' "$dir") || ! gid=$(stat -c '%g' "$dir"); then
|
||||
warn "failed to determine the owner for \"$path\"."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! chown "$uid:$gid" "$path"; then
|
||||
warn "failed to set the owner for \"$path\"."
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
stateFile() {
|
||||
|
||||
local name="$1"
|
||||
local prefix="${2:-$PROCESS}"
|
||||
|
||||
[[ "$name" == */* ]] && printf '%s\n' "$name" && return 0
|
||||
|
||||
printf '%s/%s.%s\n' "$STORAGE" "$prefix" "$name"
|
||||
return 0
|
||||
}
|
||||
|
||||
writeFile() {
|
||||
|
||||
local txt="$1"
|
||||
local path="$2"
|
||||
|
||||
if ! printf '%s\n' "$txt" > "$path"; then
|
||||
error "Failed to write file \"$path\" !"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! setOwner "$path"; then
|
||||
warn "failed to set the owner for \"$path\"."
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
writeAtomic() {
|
||||
|
||||
local path="$1"
|
||||
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"
|
||||
|
||||
if ! printf '%s\n' "$content" > "$tmp"; then
|
||||
rm -f -- "$tmp"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! mv -f -- "$tmp" "$path"; then
|
||||
rm -f -- "$tmp"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
readFile() {
|
||||
|
||||
local path="$1"
|
||||
local value
|
||||
|
||||
[ -s "$path" ] || return 0
|
||||
|
||||
value=$(<"$path") || return 1
|
||||
value="${value//[![:print:]]/}"
|
||||
|
||||
printf '%s\n' "$value"
|
||||
return 0
|
||||
}
|
||||
|
||||
writeState() {
|
||||
|
||||
local name="$1"
|
||||
local value="$2"
|
||||
local prefix="${3:-$PROCESS}"
|
||||
local path
|
||||
|
||||
[ -z "$value" ] && return 0
|
||||
|
||||
path=$(stateFile "$name" "$prefix") || return 1
|
||||
writeFile "$value" "$path"
|
||||
|
||||
return $?
|
||||
}
|
||||
|
||||
readState() {
|
||||
|
||||
local name="$1"
|
||||
local prefix="${2:-$PROCESS}"
|
||||
local path
|
||||
|
||||
path=$(stateFile "$name" "$prefix") || return 1
|
||||
readFile "$path"
|
||||
|
||||
return $?
|
||||
}
|
||||
|
||||
restoreState() {
|
||||
|
||||
local var="$1"
|
||||
local name="$2"
|
||||
local force="${3:-N}"
|
||||
local prefix="${4:-$PROCESS}"
|
||||
local value
|
||||
|
||||
# Persistent state fills only unset variables unless force is requested,
|
||||
# preserving explicit environment overrides.
|
||||
if ! enabled "$force"; then
|
||||
[ -z "${!var:-}" ] || return 0
|
||||
fi
|
||||
|
||||
value=$(readState "$name" "$prefix") || return 1
|
||||
[ -n "$value" ] || return 0
|
||||
|
||||
printf -v "$var" '%s' "$value" || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
escape () {
|
||||
|
||||
local s=${1//&/\&}
|
||||
s=${s//</\<}
|
||||
s=${s//>/\>}
|
||||
s=${s//'"'/\"}
|
||||
|
||||
printf -- %s "$s"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
escapeXML() {
|
||||
|
||||
printf '%s' "$1" | sed \
|
||||
-e 's/&/\&/g' \
|
||||
-e 's/</\</g' \
|
||||
-e 's/>/\>/g' \
|
||||
-e 's/"/\"/g' \
|
||||
-e "s/'/\'/g"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
html() {
|
||||
|
||||
local title
|
||||
local body
|
||||
local script="${2:-}"
|
||||
local footer
|
||||
|
||||
title=$(escape "$APP")
|
||||
title="<title>$title</title>"
|
||||
footer=$(escape "$FOOTER1")
|
||||
|
||||
body=$(escape "$1")
|
||||
if [[ "$body" == *"..." ]]; then
|
||||
body="<p class=\"loading\">${body/.../}</p>"
|
||||
fi
|
||||
|
||||
local HTML
|
||||
HTML=$(<"$TEMPLATE")
|
||||
HTML="${HTML/\[1\]/$title}"
|
||||
HTML="${HTML/\[2\]/$script}"
|
||||
HTML="${HTML/\[3\]/$body}"
|
||||
HTML="${HTML/\[4\]/$footer}"
|
||||
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 "$INFO" "$body" || return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
cpu() {
|
||||
|
||||
local ret
|
||||
local cpu=""
|
||||
|
||||
ret=$(lscpu)
|
||||
|
||||
if grep -qi "model name" <<< "$ret"; then
|
||||
cpu=$(echo "$ret" | grep -m 1 -i 'model name' | cut -f 2 -d ":" | awk '{$1=$1}1' | sed 's# @.*##g' | sed s/"(R)"//g | sed 's/[^[:alnum:] ]\+/ /g' | sed 's/ */ /g')
|
||||
fi
|
||||
|
||||
if [ -z "${cpu// /}" ] && grep -qi "model:" <<< "$ret"; then
|
||||
cpu=$(echo "$ret" | grep -m 1 -i 'model:' | cut -f 2 -d ":" | awk '{$1=$1}1' | sed 's# @.*##g' | sed s/"(R)"//g | sed 's/[^[:alnum:] ]\+/ /g' | sed 's/ */ /g')
|
||||
fi
|
||||
|
||||
cpu="${cpu// CPU/}"
|
||||
cpu="${cpu// [0-9][0-9][0-9] Core}"
|
||||
cpu="${cpu// [0-9][0-9] Core}"
|
||||
cpu="${cpu// [0-9] Core}"
|
||||
cpu="${cpu//[0-9][0-9]th Gen }"
|
||||
cpu="${cpu//[0-9]th Gen }"
|
||||
cpu="${cpu// Processor/}"
|
||||
cpu="${cpu// Quad core/}"
|
||||
cpu="${cpu// Dual core/}"
|
||||
cpu="${cpu// Octa core/}"
|
||||
cpu="${cpu// Hexa core/}"
|
||||
cpu="${cpu// Core TM/ Core}"
|
||||
cpu="${cpu// with Radeon Graphics/}"
|
||||
cpu="${cpu// with Radeon Vega Graphics/}"
|
||||
cpu="${cpu// with Radeon Vega Mobile Gfx/}"
|
||||
cpu="${cpu// w Radeon [0-9][0-9][0-9]M Graphics/}"
|
||||
|
||||
[ -z "${cpu// /}" ] && cpu="Unknown"
|
||||
|
||||
echo "$cpu"
|
||||
return 0
|
||||
}
|
||||
|
||||
getCountry() {
|
||||
|
||||
local url=$1
|
||||
local query=$2
|
||||
local json result
|
||||
|
||||
{ json=$(curl -m 5 -H "Accept: application/json" -sfk "$url"); local rc=$?; } || :
|
||||
(( rc != 0 )) && return 0
|
||||
|
||||
{ result=$(echo "$json" | jq -r "$query" 2> /dev/null); rc=$?; } || :
|
||||
(( rc != 0 )) && return 0
|
||||
|
||||
[[ ${#result} -ne 2 ]] && return 0
|
||||
[[ "${result^^}" == "XX" ]] && return 0
|
||||
|
||||
COUNTRY="${result^^}"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
setCountry() {
|
||||
|
||||
[[ "${TZ,,}" == "asia/harbin" ]] && COUNTRY="CN"
|
||||
[[ "${TZ,,}" == "asia/beijing" ]] && COUNTRY="CN"
|
||||
[[ "${TZ,,}" == "asia/urumqi" ]] && COUNTRY="CN"
|
||||
[[ "${TZ,,}" == "asia/kashgar" ]] && COUNTRY="CN"
|
||||
[[ "${TZ,,}" == "asia/shanghai" ]] && 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://ifconfig.co/json" ".country_iso"
|
||||
[ -z "$COUNTRY" ] && getCountry "https://api.ip2location.io" ".country_code"
|
||||
[ -z "$COUNTRY" ] && getCountry "https://ipinfo.io/json" ".country"
|
||||
[ -z "$COUNTRY" ] && getCountry "https://api.ipquery.io/?format=json" ".location.country_code"
|
||||
[ -z "$COUNTRY" ] && getCountry "https://api.myip.com" ".cc"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
addPackage() {
|
||||
|
||||
local pkg=$1
|
||||
local desc=$2
|
||||
|
||||
if apt-mark showinstall | grep -qx "$pkg"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
MSG="Installing $desc..."
|
||||
info "$MSG" && html "$MSG"
|
||||
|
||||
[ -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
|
||||
sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources
|
||||
fi
|
||||
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -qq update || return 1
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -qq --no-install-recommends -y install "$pkg" > /dev/null || return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
return 0
|
||||
@@ -1,4 +1,5 @@
|
||||
server {
|
||||
|
||||
listen 5000 default_server;
|
||||
|
||||
autoindex on;
|
||||
@@ -9,6 +10,9 @@ server {
|
||||
error_log /dev/null;
|
||||
access_log /dev/null;
|
||||
|
||||
auth_basic off;
|
||||
auth_basic_user_file /etc/nginx/.htpasswd;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
|
||||
gzip on;
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
body {
|
||||
color: white;
|
||||
background-color: #125bdb;
|
||||
font-smoothing: antialiased;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-family: Verdana, Geneva, sans-serif;
|
||||
}
|
||||
|
||||
#info {
|
||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
#content {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
#progress {
|
||||
width: 0;
|
||||
min-width: 250px;
|
||||
max-width: 80vw;
|
||||
height: 15px;
|
||||
overflow: hidden;
|
||||
margin: 20px auto 0;
|
||||
border-radius: 999px;
|
||||
background: rgba(0, 0, 0, 0.17);
|
||||
box-shadow:
|
||||
inset 0 1px 2px rgba(0, 0, 0, 0.22),
|
||||
0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
#progress[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#progress-fill {
|
||||
position: relative;
|
||||
width: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
transition: width 0.25s ease;
|
||||
}
|
||||
|
||||
#progress-fill:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: -40%;
|
||||
width: 34%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 255, 255, 0),
|
||||
rgba(189, 227, 255, 0.15) 25%,
|
||||
rgba(255, 255, 255, 0.70) 50%,
|
||||
rgba(189, 227, 255, 0.15) 75%,
|
||||
rgba(255, 255, 255, 0)
|
||||
);
|
||||
animation: progress-sheen 1.9s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes progress-sheen {
|
||||
0% {
|
||||
left: -40%;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
12% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
60%,
|
||||
100% {
|
||||
left: 112%;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
width: 98%;
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
color: #0c8aeb;
|
||||
text-shadow: 0 0 1px #0c8aeb;
|
||||
}
|
||||
|
||||
#empty {
|
||||
height: 40px;
|
||||
/* Same height as footer */
|
||||
}
|
||||
|
||||
a,
|
||||
a:hover,
|
||||
a:active,
|
||||
a:visited {
|
||||
color: white;
|
||||
}
|
||||
|
||||
footer a:link,
|
||||
footer a:visited,
|
||||
footer a:active {
|
||||
color: #0c8aeb;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: #73e6ff;
|
||||
}
|
||||
|
||||
.loading:after {
|
||||
content: " .";
|
||||
animation: dots 1s steps(5, end) infinite;
|
||||
}
|
||||
|
||||
@keyframes dots {
|
||||
|
||||
0%,
|
||||
20% {
|
||||
color: rgba(0, 0, 0, 0);
|
||||
text-shadow: 0.25em 0 0 rgba(0, 0, 0, 0), 0.5em 0 0 rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
40% {
|
||||
color: white;
|
||||
text-shadow: 0.25em 0 0 rgba(0, 0, 0, 0), 0.5em 0 0 rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
60% {
|
||||
text-shadow: 0.25em 0 0 white, 0.5em 0 0 rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
80%,
|
||||
100% {
|
||||
text-shadow: 0.25em 0 0 white, 0.5em 0 0 white;
|
||||
}
|
||||
}
|
||||
|
||||
.spinner_LWk7 {
|
||||
animation: spinner_GWy6 1.2s linear infinite, spinner_BNNO 1.2s linear infinite
|
||||
}
|
||||
|
||||
.spinner_yOMU {
|
||||
animation: spinner_GWy6 1.2s linear infinite, spinner_pVqn 1.2s linear infinite;
|
||||
animation-delay: .15s
|
||||
}
|
||||
|
||||
.spinner_KS4S {
|
||||
animation: spinner_GWy6 1.2s linear infinite, spinner_6uKB 1.2s linear infinite;
|
||||
animation-delay: .3s
|
||||
}
|
||||
|
||||
.spinner_zVee {
|
||||
animation: spinner_GWy6 1.2s linear infinite, spinner_Qw4x 1.2s linear infinite;
|
||||
animation-delay: .45s
|
||||
}
|
||||
|
||||
@keyframes spinner_GWy6 {
|
||||
|
||||
0%,
|
||||
50% {
|
||||
width: 9px;
|
||||
height: 9px
|
||||
}
|
||||
|
||||
10% {
|
||||
width: 11px;
|
||||
height: 11px
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spinner_BNNO {
|
||||
|
||||
0%,
|
||||
50% {
|
||||
x: 1.5px;
|
||||
y: 1.5px
|
||||
}
|
||||
|
||||
10% {
|
||||
x: .5px;
|
||||
y: .5px
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spinner_pVqn {
|
||||
|
||||
0%,
|
||||
50% {
|
||||
x: 13.5px;
|
||||
y: 1.5px
|
||||
}
|
||||
|
||||
10% {
|
||||
x: 12.5px;
|
||||
y: .5px
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spinner_6uKB {
|
||||
|
||||
0%,
|
||||
50% {
|
||||
x: 13.5px;
|
||||
y: 13.5px
|
||||
}
|
||||
|
||||
10% {
|
||||
x: 12.5px;
|
||||
y: 12.5px
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spinner_Qw4x {
|
||||
|
||||
0%,
|
||||
50% {
|
||||
x: 1.5px;
|
||||
y: 13.5px
|
||||
}
|
||||
|
||||
10% {
|
||||
x: .5px;
|
||||
y: 12.5px
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<svg id="Capa_1" enable-background="new 0 0 511.962 511.962" height="512" viewBox="0 0 511.962 511.962" width="512" xmlns="http://www.w3.org/2000/svg"><g><path d="m489.965 120.063c0-5.77-3.31-11.028-8.512-13.524l-218.984-105.063c-4.102-1.967-8.875-1.967-12.977 0l-218.985 105.063c-5.202 2.496-8.511 7.755-8.511 13.524l-.003 271.834c0 5.77 3.31 11.028 8.512 13.524l218.989 105.064c2.051.983 4.27 1.476 6.488 1.476 2.219 0 4.438-.492 6.488-1.476l218.989-105.064c5.202-2.496 8.512-7.755 8.512-13.524z" fill="#4e6ba6"/><path d="m489.965 120.063c0-5.77-3.31-11.028-8.512-13.524l-218.984-105.063c-2.051-.984-4.269-1.476-6.488-1.476v511.962c2.219 0 4.438-.492 6.488-1.476l218.989-105.064c5.202-2.496 8.512-7.755 8.512-13.524z" fill="#28487a"/><path d="m425.812 160.441c0-2.27-.519-4.457-1.457-6.432l-336.701-.095c-.967 1.999-1.504 4.22-1.504 6.526l-.002 191.081c0 5.769 3.31 11.028 8.512 13.524l154.833 74.285c2.051.983 4.27 1.476 6.488 1.476 2.219 0 4.438-.492 6.488-1.476l154.834-74.285c5.202-2.496 8.512-7.755 8.512-13.524z" fill="#8dc2eb"/><path d="m424.354 154.009h-168.373v286.798c2.219 0 4.438-.492 6.488-1.476l154.834-74.285c5.202-2.496 8.512-7.755 8.512-13.524l-.003-191.081c0-2.27-.52-4.458-1.458-6.432z" fill="#5e9ff6"/><path d="m417.3 146.916-154.831-74.284c-4.102-1.967-8.875-1.967-12.977 0l-154.831 74.284c-3.122 1.498-5.555 3.996-7.007 6.998l168.328 80.812 168.374-80.717c-1.448-3.044-3.9-5.579-7.056-7.093z" fill="#ecf9fd"/><path d="m417.3 146.916-154.831-74.284c-2.051-.983-4.27-1.476-6.488-1.476v163.569l168.374-80.717c-1.447-3.043-3.899-5.578-7.055-7.092z" fill="#d9f3fc"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,37 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
[1]
|
||||
<meta http-equiv="Cache-Control" content="no-cache" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" type="text/css" href="css/style.css" />
|
||||
<link rel="icon" href="img/favicon.svg" type="image/x-icon">
|
||||
[2]
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page">
|
||||
<div id="content">
|
||||
<svg id="spinner" width="64" height="64" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect class="spinner_LWk7" fill="#0c8aeb" x="1.5" y="1.5" rx="1" width="9" height="9"/>
|
||||
<rect class="spinner_yOMU" fill="#0c8aeb" x="13.5" y="1.5" rx="1" width="9" height="9"/>
|
||||
<rect class="spinner_KS4S" fill="#0c8aeb" x="13.5" y="13.5" rx="1" width="9" height="9"/>
|
||||
<rect class="spinner_zVee" fill="#0c8aeb" x="1.5" y="13.5" rx="1" width="9" height="9"/>
|
||||
</svg>
|
||||
<h1 id="info">[3]</h1>
|
||||
<div id="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-labelledby="info" hidden>
|
||||
<div id="progress-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="empty">
|
||||
</div>
|
||||
<footer id="footer">
|
||||
[4]<br />
|
||||
[5]
|
||||
</footer>
|
||||
</div>
|
||||
<script type="text/javascript" src="js/script.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user