Compare commits

..
68 Commits
Author SHA1 Message Date
KroeseandGitHub f1b56f394a build: Create a detailed .gitignore file (#1321) 2026-07-23 15:42:59 +02:00
KroeseandGitHub 1a983ebcd1 feat: Improve error handling for webserver config (#1320) 2026-07-23 04:17:26 +02:00
KroeseandGitHub 41c76198fa feat: Use atomic writes for progress updates (#1319) 2026-07-23 04:10:09 +02:00
KroeseandGitHub 222b5649b0 fix: Prevent stale polling responses for web status (#1318) 2026-07-23 04:00:12 +02:00
KroeseandGitHub e14fd4b711 docs: Environment variables (#1317) 2026-07-21 13:14:32 +02:00
KroeseandGitHub 6fe19a8af4 feat: Improve download progress reporting (#1316) 2026-07-21 06:00:58 +02:00
KroeseandGitHub 072b5c3070 build: Update workflow (#1315) 2026-07-21 01:31:29 +02:00
KroeseandGitHub 89dbaeb2a4 feat: Improve download progress reporting (#1314) 2026-07-21 00:13:53 +02:00
KroeseandGitHub 02f76b44cd fix: Resolve shellcheck error (#1313) 2026-07-20 17:00:32 +02:00
KroeseandGitHub 99d6b8f830 feat: Preserve graceful shutdown for interactive console (#1312) 2026-07-20 16:31:21 +02:00
KroeseandGitHub e6b68b2f08 fix: Avoid shellcheck warning (#1311) 2026-07-20 14:32:03 +02:00
KroeseandGitHub 7a8f0041e7 build: Update workflow (#1310) 2026-07-20 12:31:13 +02:00
KroeseandGitHub 64809bb4d4 feat: Support progress tracking for segmented downloads (#1309) 2026-07-20 05:48:16 +02:00
KroeseandGitHub f08b2cbd99 fix: Decrease default timeout (#1307) 2026-07-19 09:17:51 +02:00
KroeseandGitHub 18d9ecd434 fix: Decrease default timeout (#1308) 2026-07-19 08:57:09 +02:00
KroeseandGitHub 6944293a32 fix: Add Recreate strategy to Kubernetes deployment (#1306) 2026-07-19 03:15:10 +02:00
KroeseandGitHub d807a365ff feat: Added optional lossy VNC compression (#1305) 2026-07-19 03:09:35 +02:00
KroeseandGitHub 066af83d07 fix: Decrease default timeout (#1304) 2026-07-19 01:03:21 +02:00
KroeseandGitHub 81536818df feat: Add escapeHTML function (#1303) 2026-07-18 22:00:45 +02:00
KroeseandGitHub 949c288565 fix: Set correct MTU option for dnsmasq (#1302) 2026-07-18 20:40:52 +02:00
KroeseandGitHub 5d5bbbcf4d feat: Improve NAT networking (#1300) 2026-07-18 15:34:46 +02:00
KroeseandGitHub 82a62c0240 build: Update workflow (#1301) 2026-07-18 15:32:07 +02:00
KroeseandGitHub 5399e1d463 feat: Use Unix sockets for qemu-host communication (#1299) 2026-07-18 11:31:20 +02:00
KroeseandGitHub c7e7b60f01 fix: Improve IP tables cleanup verification (#1298) 2026-07-18 10:54:20 +02:00
KroeseandGitHub f1defa6890 docs: Environment variables (#1297) 2026-07-18 00:28:24 +02:00
KroeseandGitHub a7fb161974 feat: Improve download progress readability (#1296) 2026-07-17 22:59:07 +02:00
KroeseandGitHub dcdf5e5293 fix: Improve interactive terminal detection (#1295) 2026-07-17 22:45:07 +02:00
KroeseandGitHub 1eb0db66ba feat: Improve download progress in container logs (#1294) 2026-07-17 22:29:10 +02:00
KroeseandGitHub 3eec8e03ed feat: Improve AMD detection (#1293) 2026-07-17 20:54:41 +02:00
KroeseandGitHub 5483981ed4 feat: Refactor config code (#1292) 2026-07-17 20:37:54 +02:00
KroeseandGitHub 62330a5bf4 fix: Cleanup stale pipe files (#1291) 2026-07-17 20:01:53 +02:00
KroeseandGitHub 2b0b59dfba feat: Preserve graceful shutdown for interactive console (#1290) 2026-07-17 19:56:05 +02:00
KroeseandGitHub b680ff9dd8 feat: Preserve graceful shutdown for interactive console (#1289) 2026-07-17 19:16:30 +02:00
KroeseandGitHub 5c889b272d fix: Store pid for serial debugging (#1288) 2026-07-17 14:44:49 +02:00
KroeseandGitHub 585ebb3f53 build: Update Passt to v2026_07_16 (#1287) 2026-07-17 13:49:26 +02:00
KroeseandGitHub 401307b981 build: Update QEMU host to v2.06 (#1286) 2026-07-17 13:24:34 +02:00
KroeseandGitHub 46820fe6eb build: Set dependabot cooldown period (#1284) 2026-07-17 13:20:19 +02:00
KroeseandGitHub 9452d48694 feat: Make machine type configurable (#1283) 2026-07-17 11:33:05 +02:00
KroeseandGitHub 5c3fed1387 fix: Clear service logs on startup (#1281) 2026-07-17 06:26:25 +02:00
KroeseandGitHub 797411ae7f fix: Show QEMU errors on unexpected exit (#1280) 2026-07-17 05:54:04 +02:00
KroeseandGitHub e81c509208 feat: Check RENDERNODE permissions (#1279) 2026-07-17 02:11:38 +02:00
KroeseandGitHub ace8614a50 feat: Refactor disk code (#1278) 2026-07-17 01:52:47 +02:00
KroeseandGitHub 8263b3a737 feat: Improve iptable backend selection (#1277) 2026-07-17 01:51:39 +02:00
KroeseandGitHub 61c6142988 feat: Remove unused functions (#1276) 2026-07-17 00:10:59 +02:00
KroeseandGitHub 5d26338bb3 feat: Improve detection of valid data disk (#1275) 2026-07-16 21:41:29 +02:00
KroeseandGitHub 084d475cde build: Add diffutils package (#1274) 2026-07-16 20:55:47 +02:00
KroeseandGitHub c36b0dcb00 docs: Environment variables (#1273) 2026-07-16 20:50:17 +02:00
KroeseandGitHub 6e9993742b feat: Limit wget progress output (#1272) 2026-07-15 23:01:39 +02:00
KroeseandGitHub 6946af0212 build: Update workflow (#1271) 2026-07-15 21:55:07 +02:00
KroeseandGitHub 0a9361d62f fix: Localize available memory formatting (#1270) 2026-07-15 21:13:51 +02:00
KroeseandGitHub 7cbfcd44a6 fix: Remove unnecessary guards (#1269) 2026-07-15 21:00:50 +02:00
KroeseandGitHub 6bcacf5e59 feat: Improve boolean helpers (#1268) 2026-07-15 19:35:16 +02:00
KroeseandGitHub 3bea87f087 feat: Suppress empty iptables debug sections (#1267) 2026-07-15 19:21:43 +02:00
KroeseandGitHub f01e1564f7 fix: Make setting owner non-fatal (#1266) 2026-07-15 18:00:24 +02:00
KroeseandGitHub cf8cdbf0c1 fix: Make failed chown non-fatal (#1265) 2026-07-15 17:39:05 +02:00
KroeseandGitHub 0813494ee0 feat: Use cgroup when calculating RAM (#1264) 2026-07-15 15:40:51 +02:00
KroeseandGitHub 0d71e30f50 feat: Add state helpers to utils (#1263) 2026-07-15 15:28:42 +02:00
KroeseandGitHub c203deab9a feat: Improve webserver configuration (#1262) 2026-07-15 15:27:24 +02:00
KroeseandGitHub 9cf31df266 fix: Validate subnet mask (#1261) 2026-07-15 14:17:01 +02:00
KroeseandGitHub 0cd08f2ee1 fix: Improve NAT port forwarding (#1260) 2026-07-15 12:24:25 +02:00
KroeseandGitHub a32d215e90 feat: Improve wget error reporting (#1259) 2026-07-15 11:34:55 +02:00
KroeseandGitHub c1f9a690f9 fix: Remove dots from info line (#1258) 2026-07-15 09:00:14 +02:00
KroeseandGitHub 5de504deb3 feat: Improve rootless container detection (#1256) 2026-07-15 06:02:12 +02:00
KroeseandGitHub 250eb18238 docs: Readme (#1255) 2026-07-14 12:50:07 +02:00
KroeseandGitHub ca80da59bd docs: Environment variables (#1254) 2026-07-14 11:29:10 +02:00
KroeseandGitHub 9cd86dfe82 fix: Use exec form for health check (#1253) 2026-07-13 19:40:30 +02:00
KroeseandGitHub 5bb93651f7 docs: Readme (#1252) 2026-07-13 00:38:58 +02:00
KroeseandGitHub 0c268308a4 feat: Preserve source image until conversion completes (#1251) 2026-07-12 23:33:10 +02:00
25 changed files with 2647 additions and 1010 deletions
+4
View File
@@ -4,7 +4,11 @@ updates:
directory: / directory: /
schedule: schedule:
interval: weekly interval: weekly
cooldown:
default-days: 7
- package-ecosystem: github-actions - package-ecosystem: github-actions
directory: / directory: /
schedule: schedule:
interval: weekly interval: weekly
cooldown:
default-days: 7
+27 -20
View File
@@ -1,5 +1,7 @@
on: [workflow_call] on: [workflow_call]
name: "Check" name: "Check"
permissions: {} permissions: {}
jobs: jobs:
@@ -7,24 +9,29 @@ jobs:
name: shellcheck name: shellcheck
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
-
name: Checkout
uses: actions/checkout@v7
- parallel:
- -
name: Run ShellCheck name: Checkout
uses: ludeeus/action-shellcheck@master uses: actions/checkout@v7
env: - parallel:
SHELLCHECK_OPTS: -x --source-path=src -e SC2001 -e SC2034 -e SC2064 -e SC2317 -e SC2153 -e SC2028 -
- name: Run ShellCheck
name: Lint Dockerfile uses: ludeeus/action-shellcheck@master
uses: hadolint/hadolint-action@v3.3.0 env:
with: SHELLCHECK_OPTS: >-
dockerfile: Dockerfile -x
ignore: DL3008,DL3003,DL3006,DL3013 --source-path=src
failure-threshold: warning -e SC1091
- -e SC2001
name: Validate JSON and YML files -e SC2034
uses: GrantBirki/json-yaml-validate@v5.0.0 -e SC2317
with: -e SC2153
yaml_exclude_regex: ".*\\kubernetes\\.yml$" -
name: Lint Dockerfile
uses: hadolint/hadolint-action@v3.3.0
with:
dockerfile: Dockerfile
ignore: DL3008
failure-threshold: warning
-
name: Validate JSON and YML files
uses: GrantBirki/json-yaml-validate@v5
+1 -58
View File
@@ -11,61 +11,4 @@ permissions:
jobs: jobs:
review: review:
name: review name: review
runs-on: ubuntu-latest uses: action-pack/.github/.github/workflows/review.yml@master
steps:
-
name: Checkout
uses: actions/checkout@v7
- parallel:
-
name: Spelling
uses: reviewdog/action-misspell@v1
with:
locale: "US"
level: warning
pattern: |
*.md
*.sh
reporter: github-pr-review
github_token: ${{ secrets.GITHUB_TOKEN }}
-
name: Hadolint
uses: reviewdog/action-hadolint@v1
with:
level: warning
fail_level: error
reporter: github-pr-review
hadolint_ignore: DL3008 DL3003 DL3006 DL3013
github_token: ${{ secrets.GITHUB_TOKEN }}
-
name: YamlLint
uses: reviewdog/action-yamllint@v1
with:
level: warning
reporter: github-pr-review
github_token: ${{ secrets.GITHUB_TOKEN }}
-
name: ActionLint
uses: reviewdog/action-actionlint@v1
with:
level: warning
reporter: github-pr-review
github_token: ${{ secrets.GITHUB_TOKEN }}
-
name: Shellformat
uses: reviewdog/action-shfmt@v1
if: false
with:
level: warning
fail_on_error: "true"
shfmt_flags: "-i 2 -ci -bn"
github_token: ${{ secrets.GITHUB_TOKEN }}
-
name: Shellcheck
uses: reviewdog/action-shellcheck@v1
with:
level: warning
fail_level: error
reporter: github-pr-review
shellcheck_flags: -x -e SC1091 -e SC2001 -e SC2034 -e SC2064 -e SC2317 -e SC2153 -e SC2028
github_token: ${{ secrets.GITHUB_TOKEN }}
+268 -1
View File
@@ -1 +1,268 @@
build.sh ##############################
# Operating System Files
##############################
.DS_Store
.AppleDouble
.LSOverride
Thumbs.db
ehthumbs.db
Desktop.ini
Icon?
$RECYCLE.BIN/
.Spotlight-V100/
.Trashes/
.fseventsd
##############################
# IDEs
##############################
.vscode/
.idea/
*.iml
*.ipr
*.iws
##############################
# VS Code
##############################
.history/
*.code-workspace
##############################
# Vim
##############################
*.swp
*.swo
Session.vim
##############################
# Sublime
##############################
*.sublime-workspace
*.sublime-project
##############################
# Temporary Files
##############################
*.tmp
*.temp
*.bak
*.old
*.orig
*.rej
*.save
##############################
# Logs
##############################
*.log
logs/
log/
*.out
*.err
*.trace
##############################
# Runtime
##############################
*.pid
*.seed
*.pid.lock
##############################
# Secrets
##############################
.env
.env.*
!.env.example
*.pem
*.key
*.crt
*.cer
*.p12
*.pfx
*.kdbx
*.secret
*.token
##############################
# SSH
##############################
.ssh/
##############################
# Docker
##############################
docker-compose.override.yml
docker-compose.local.yml
##############################
# VM Storage
##############################
storage/
windows/
downloads/
##############################
# Disk Images
##############################
*.qcow2
*.qcow
*.vhd
*.vhdx
*.vdi
*.raw
*.img
*.iso
*.bin
##############################
# QEMU
##############################
*.nvram
*.fd
*.efi
*.sock
*.monitor
*.serial
##############################
# Samba
##############################
shared/
share/
##############################
# Backups
##############################
backup/
backups/
*.backup
##############################
# Cache
##############################
.cache/
.cache-loader/
.tmp/
temp/
tmp/
##############################
# Python
##############################
__pycache__/
*.py[cod]
.pytest_cache/
.mypy_cache/
.venv/
venv/
##############################
# Node
##############################
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
##############################
# Build
##############################
dist/
build/
out/
release/
##############################
# Coverage
##############################
coverage/
.coverage
coverage.xml
##############################
# Archives
##############################
*.zip
*.tar
*.tar.gz
*.tgz
*.7z
*.rar
##############################
# Generated Config
##############################
config.local.*
settings.local.*
local.env
##############################
# Test Files
##############################
test-output/
playwright-report/
##############################
# macOS
##############################
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
##############################
# Linux
##############################
*~
.nfs*
##############################
# Windows
##############################
*.stackdump
##############################
# Misc
##############################
*.cache
*.lock
*.lock.json
*.bak.*
##############################
# Keep Examples
##############################
!.gitkeep
!.env.example
+7 -4
View File
@@ -1,12 +1,14 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
FROM qemux/qemu-host:2.05 AS builder FROM qemux/qemu-host:2.06 AS builder
FROM debian:trixie-slim FROM debian:trixie-slim
ARG TARGETARCH ARG TARGETARCH
ARG TARGETPLATFORM ARG TARGETPLATFORM
ARG VERSION_ARG="0.0" ARG VERSION_ARG="0.0"
ARG VERSION_PASST="2026_06_11" ARG VERSION_CSTRUCT="4.7"
ARG VERSION_PASST="2026_07_16"
ARG DEBCONF_NOWARNINGS="yes" ARG DEBCONF_NOWARNINGS="yes"
ARG DEBIAN_FRONTEND="noninteractive" ARG DEBIAN_FRONTEND="noninteractive"
@@ -39,6 +41,7 @@ RUN <<EOF
apt-utils \ apt-utils \
net-tools \ net-tools \
e2fsprogs \ e2fsprogs \
diffutils \
qemu-utils \ qemu-utils \
websocketd \ websocketd \
iputils-ping \ iputils-ping \
@@ -54,7 +57,7 @@ RUN <<EOF
apt-get clean apt-get clean
# Install Python dependencies # Install Python dependencies
pip3 install --no-cache-dir --break-system-packages --root-user-action=ignore dissect.cstruct pip3 install --no-cache-dir --break-system-packages --root-user-action=ignore "dissect.cstruct==$VERSION_CSTRUCT"
# Configure QEMU # Configure QEMU
mkdir -p /etc/qemu mkdir -p /etc/qemu
@@ -83,6 +86,6 @@ ENV RAM_SIZE="2G"
ENV CPU_CORES="2" ENV CPU_CORES="2"
ENV DISK_SIZE="256G" ENV DISK_SIZE="256G"
HEALTHCHECK --interval=60s --start-period=45s --retries=2 CMD /run/check.sh HEALTHCHECK --interval=60s --start-period=45s --retries=2 CMD ["/run/check.sh"]
ENTRYPOINT ["/usr/bin/tini", "-s", "/run/entry.sh"] ENTRYPOINT ["/usr/bin/tini", "-s", "/run/entry.sh"]
+46 -38
View File
@@ -2,13 +2,16 @@
This page lists all the environment variables that can be used to configure the container. This page lists all the environment variables that can be used to configure the container.
An empty default means the variable is unset and its value is determined automatically when applicable.
## 💽 Virtual DSM ## 💽 Virtual DSM
| Variable | Default | Description | | Variable | Default | Description |
|---|---|---| |---|---|---|
| `URL` | | URL or local path of the DSM `.pat` installation file. When unset, the default Virtual DSM image is downloaded automatically. | | `URL` | | URL or local path to a custom `.pat` installation file. |
| `COUNTRY` | | Country code used to select the Synology download mirror. |
| `HOST_MAC` | | MAC address reported to DSM. | | `HOST_MAC` | | MAC address reported to DSM. |
| `HOST_MODEL` | | Synology host device model reported to DSM. | | `HOST_MODEL` | | Synology host model reported to DSM. |
| `HOST_SERIAL` | | Synology host serial number reported to DSM. | | `HOST_SERIAL` | | Synology host serial number reported to DSM. |
| `GUEST_SERIAL` | | Synology guest serial number reported to DSM. | | `GUEST_SERIAL` | | Synology guest serial number reported to DSM. |
@@ -16,68 +19,76 @@ This page lists all the environment variables that can be used to configure the
| Variable | Default | Description | | Variable | Default | Description |
|---|---|---| |---|---|---|
| `CPU_CORES` | `2` | Number of CPU cores assigned to the VM. Can also be set to `max` or `half`. | | `CPU_CORES` | `2` | Number of virtual CPU cores, such as `4`, `half`, or `max`. |
| `CPU_MODEL` | `host` | QEMU CPU model to use. | | `CPU_MODEL` | `host` | QEMU CPU model. |
| `CPU_FLAGS` | | Additional QEMU CPU flags. | | `CPU_FLAGS` | | Additional QEMU CPU flags. |
| `HOST_CPU` | | CPU name reported to DSM. Automatically selected when unset. | | `HOST_CPU` | | CPU name reported to DSM. Selected automatically when unset. |
| `KVM` | `Y` | Enables KVM hardware acceleration. Set to `N` to disable. | | `KVM` | `Y` | Enables KVM hardware acceleration. |
| `RAM_SIZE` | `2G` | Amount of RAM assigned to the VM, for example `2G`, `4G`, `max`, or `half`. | | `RAM_SIZE` | `2G` | Amount of RAM assigned to DSM, such as `2G`, `4G`, `half`, or `max`. |
| `RAM_CHECK` | `Y` | Checks whether enough host memory is available before starting the VM. | | `RAM_CHECK` | `Y` | Checks whether enough host memory is available before starting DSM. |
## 💾 Storage ## 💾 Storage
| Variable | Default | Description | | Variable | Default | Description |
|---|---|---| |---|---|---|
| `DISK_SIZE` | `256G` | Size of the main data disk. | | `DISK_SIZE` | `256G` | Size of the main data disk. |
| `DISK_FMT` | `raw` | Disk image format, usually `raw` or `qcow2`. | | `DISK_FMT` | `raw` | Disk image format: `raw` or `qcow2`. |
| `DISK_TYPE` | `scsi` | Disk controller/device type, such as `sata`, `scsi`, `nvme`, or `blk`. | | `DISK_TYPE` | `scsi` | Disk device type, such as `sata`, `scsi`, `nvme`, or `blk`. |
| `DISK_CACHE` | `none` | QEMU disk cache mode, for example `none` or `writeback`. | | `DISK_CACHE` | `none` | Disk cache mode, such as `none` or `writeback`. |
| `DISK_IO` | `native` | QEMU disk I/O mode, for example `native`, `threads`, or `io_uring`. | | `DISK_IO` | `native` | Disk I/O mode, such as `native`, `threads`, or `io_uring`. |
| `DISK_DISCARD` | `unmap` | Enables TRIM/unmap support for the data disk. | | `DISK_DISCARD` | `unmap` | Discard/TRIM mode for the primary disk. |
| `DISK_ROTATION` | `1` | Rotation rate reported to the guest. Use `1` for SSD-like storage. | | `DISK_ROTATION` | `1` | Rotation rate reported to the guest. Use `1` to identify the disk as an SSD. |
| `DISK_FLAGS` | | Additional options used when creating qcow2 disks. | | `DISK_FLAGS` | | Additional options used when creating `qcow2` disks. |
| `ALLOCATE` | `N` | Preallocates disk space when creating the data disk. | | `ALLOCATE` | `N` | Preallocates space for the data disks. |
| `STORAGE` | `/storage` | Storage directory used for disks, firmware variables, and generated files. | | `STORAGE` | `/storage` | Storage directory used for disks, settings, and downloads. |
## 🌐 Networking ## 🌐 Networking
| Variable | Default | Description | | Variable | Default | Description |
|---|---|---| |---|---|---|
| `NETWORK` | `Y` | Network mode. Common values are `Y` for NAT, `passt`, `slirp`, or `N` to disable networking. | | `NETWORK` | | Network mode, such as `nat`, `user`, or `N` to disable networking. |
| `DHCP` | `N` | Enables DHCP/macvtap mode so the VM receives an address from the external LAN. | | `DHCP` | `N` | Enables macvtap networking so DSM receives a DHCP address. |
| `IP` | | Guest IP address override. | | `HOST` | | Hostname assigned to the machine on the network. |
| `IP` | | Overrides the automatically selected guest IPv4 address. |
| `MAC` | | Guest network adapter MAC address. | | `MAC` | | Guest network adapter MAC address. |
| `HOST` | `VirtualDSM` | Hostname assigned to the VM. |
| `DEV` | `eth0` | Host/container network interface to use. |
| `MTU` | | Network MTU to use for the guest interface. |
| `MASK` | `255.255.255.0` | IPv4 netmask. |
| `TAP` | `dsm` | TAP/macvtap interface name. |
| `BRIDGE` | `docker` | Bridge name used for NAT networking. |
| `ADAPTER` | `virtio-net-pci` | QEMU network adapter model. | | `ADAPTER` | `virtio-net-pci` | QEMU network adapter model. |
| `HOST_PORTS` | | Ports reserved for services running on the host/container side. | | `DEV` | `eth0` | Container network interface used as the uplink. |
| `USER_PORTS` | | Additional ports to forward to the VM when using user-mode networking. | | `MTU` | | MTU assigned to the guest network interface. |
| `DNSMASQ_OPTS` | | Additional dnsmasq options. | | `MASK` | `255.255.255.0` | IPv4 netmask for guest network. |
| `DNSMASQ_DEBUG` | `N` | Enables dnsmasq log tailing. | | `TAP` | `dsm` | 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. |
| `DNSMASQ_OPTS` | | Additional options passed to dnsmasq. |
| `DNSMASQ_DEBUG` | `N` | Enables dnsmasq debug output. |
| `DNSMASQ_DISABLE` | `N` | Disables the internal dnsmasq resolver. | | `DNSMASQ_DISABLE` | `N` | Disables the internal dnsmasq resolver. |
| `PASST_OPTS` | | Additional passt options. | | `PASST_OPTS` | | Additional options passed to passt. |
| `PASST_DEBUG` | `N` | Enables passt debug output. | | `PASST_DEBUG` | `N` | Enables passt debug output. |
## 🖥️ Display ## 🖥️ Display
| Variable | Default | Description | | Variable | Default | Description |
|---|---|---| |---|---|---|
| `DISPLAY` | `none` | QEMU display backend. Common values are `vnc`, `disabled`, or `none`. | | `DISPLAY` | `none` | Display backend, such as `vnc`, `disabled`, or `none`. |
| `LOSSY` | `N` | Enables lossy VNC compression to reduce bandwidth usage. |
| `VGA` | `none` | QEMU video adapter model. | | `VGA` | `none` | QEMU video adapter model. |
| `GPU` | `N` | Enables Intel iGPU acceleration. | | `GPU` | `N` | Enables Intel iGPU acceleration. |
| `RENDERNODE` | `/dev/dri/renderD128` | Render node used for GPU acceleration. | | `RENDERNODE` | `/dev/dri/renderD128` | Render node used for GPU acceleration. |
## ⚙️ System
| Variable | Default | Description |
|---|---|---|
| `MACHINE` | `q35` | QEMU machine type. |
| `ARGUMENTS` | | Additional raw arguments appended to the QEMU command line. |
## 🔌 Shutdown ## 🔌 Shutdown
| Variable | Default | Description | | Variable | Default | Description |
|---|---|---| |---|---|---|
| `SHUTDOWN` | `Y` | Enables graceful shutdown. | | `SHUTDOWN` | `Y` | Enables graceful shutdown. |
| `TIMEOUT` | `115` | Timeout used while waiting for DSM to shut down. | | `TIMEOUT` | `115` | Maximum time, in seconds, to wait before forcing DSM to stop. |
| `API_TIMEOUT` | `90` | Timeout used for the shutdown API call. | | `API_TIMEOUT` | `90` | Maximum time, in seconds, to wait for the shutdown API call. |
## 🐞 Debugging ## 🐞 Debugging
@@ -85,7 +96,4 @@ This page lists all the environment variables that can be used to configure the
|---|---|---| |---|---|---|
| `DEBUG` | `N` | Enables verbose debug output. | | `DEBUG` | `N` | Enables verbose debug output. |
| `TRACE` | `N` | Enables shell command tracing. | | `TRACE` | `N` | Enables shell command tracing. |
| `COM_PORT` | `2210` | Internal communication port used by the DSM host helper. | | `HOST_DEBUG` | `N` | Enables debug output for the DSM host helper. |
| `CHR_PORT` | `12345` | Internal character device port used by the DSM host helper. |
| `HOST_DEBUG` | `N` | Enables debug output for the host helper. |
| `ARGUMENTS` | | Additional raw QEMU arguments appended to the generated command line. |
+2
View File
@@ -18,6 +18,8 @@ metadata:
name: dsm name: dsm
spec: spec:
replicas: 1 replicas: 1
strategy:
type: Recreate
selector: selector:
matchLabels: matchLabels:
app: dsm app: dsm
+37 -33
View File
@@ -66,12 +66,13 @@ kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/m
## Requirements ⚙️ ## Requirements ⚙️
- A Linux host with KVM support, or Docker Desktop / Podman on Windows 11 with nested virtualization enabled. - Docker or Podman on a Linux host with KVM support.
- At least 2 GB of RAM available. - Docker Desktop or Podman (Desktop) on Windows 11 with nested virtualization enabled.
- At least 32 GB of free disk space. - At least 2 GB of available RAM.
- At least 32 GB of free disk space.
> [!NOTE] > [!NOTE]
> Docker Desktop on macOS and Windows 10 do not currently provide the required KVM support for this image. > Docker Desktop on Linux, macOS, and Windows 10 does not currently provide KVM access to containers and is therefore not supported.
## FAQ 💬 ## FAQ 💬
@@ -147,34 +148,6 @@ kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/m
CPU_CORES: "4" CPU_CORES: "4"
``` ```
### How do I verify if my system supports KVM?
First check if your software is compatible using this chart:
| **Product** | **Linux** | **Win11** | **Win10** | **macOS** |
|---|---|---|---|---|
| Docker CLI | ✅ | ✅ | ❌ | ❌ |
| Docker Desktop | ❌ | ✅ | ❌ | ❌ |
| Podman CLI | ✅ | ✅ | ❌ | ❌ |
| Podman Desktop | ✅ | ✅ | ❌ | ❌ |
After that you can run the following commands in Linux to check your system:
```bash
sudo apt install cpu-checker
sudo kvm-ok
```
If you receive an error from `kvm-ok` indicating that KVM cannot be used, please check whether:
- the virtualization extensions (`Intel VT-x` or `AMD SVM`) are enabled in your BIOS.
- you enabled "nested virtualization" if you are running the container inside a virtual machine.
- you are not using a cloud provider, as most of them do not allow nested virtualization for their VPSs.
If you did not receive any error from `kvm-ok` but the container still complains about a missing KVM device, it could help to add `privileged: true` to your compose file (or `sudo` to your `docker` command) to rule out any permission issue.
### How do I assign an individual IP address to the container? ### How do I assign an individual IP address to the container?
By default, the container uses bridge networking, which shares the IP address with the host. By default, the container uses bridge networking, which shares the IP address with the host.
@@ -263,7 +236,38 @@ kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/m
### Are these all available options? ### Are these all available options?
No. For a complete overview of all supported settings, see the [environment variables](docs/environment.md) page. No. For a complete overview of all supported settings, see the [environment variables](docs/environment.md) page.
### How do I verify that KVM is available?
First, make sure your platform and container runtime meet the [requirements](#requirements-) listed above.
On a Linux host, install `cpu-checker` and run:
```bash
sudo apt install cpu-checker
sudo kvm-ok
```
A working configuration should report:
```text
KVM acceleration can be used
```
You can also verify that the KVM device exists:
```bash
ls -l /dev/kvm
```
If KVM is unavailable, check whether:
- Hardware virtualization (`Intel VT-x` or `AMD-V`) is enabled in your BIOS or UEFI.
- Nested virtualization is enabled when the host itself is a virtual machine.
- Your VPS or cloud provider supports nested virtualization.
If `kvm-ok` succeeds but the container still reports that KVM is unavailable, you can temporarily add `privileged: true` to your Compose file to rule out a permission or device-access issue.
### What are the differences compared to the standard DSM? ### What are the differences compared to the standard DSM?
+56 -9
View File
@@ -2,15 +2,62 @@
set -Eeuo pipefail set -Eeuo pipefail
DEF_OPTS="-nodefaults -boot strict=on" DEF_OPTS="-nodefaults -boot strict=on"
RAM_OPTS=$(echo "-m ${RAM_SIZE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g') DEV_OPTS=""
MON_OPTS="-name $PROCESS,process=$PROCESS,debug-threads=on -pidfile $QEMU_PID"
CPU_OPTS="-cpu $CPU_FLAGS -smp $CPU_CORES,sockets=1,dies=1,cores=$CPU_CORES,threads=1"
MAC_OPTS="-machine type=q35,smm=off,usb=off,vmport=off,dump-guest-core=off,hpet=off${KVM_OPTS}"
DEV_OPTS="-device virtio-balloon-pci,id=balloon0,bus=pcie.0,addr=0x4"
DEV_OPTS+=" -object rng-random,id=objrng0,filename=/dev/urandom"
DEV_OPTS+=" -device virtio-rng-pci,rng=objrng0,id=rng0,bus=pcie.0,addr=0x1c"
ARGS="$DEF_OPTS $CPU_OPTS $RAM_OPTS $MAC_OPTS $DISPLAY_OPTS $MON_OPTS $SERIAL_OPTS $NET_OPTS $DISK_OPTS $DEV_OPTS $ARGUMENTS" configureProcessor() {
ARGS=$(echo "$ARGS" | sed 's/\t/ /g' | tr -s ' ')
CPU_OPTS="-cpu $CPU_FLAGS"
CPU_OPTS+=" -smp $CPU_CORES,sockets=1,dies=1,cores=$CPU_CORES,threads=1"
return 0
}
configureMemory() {
RAM_OPTS=$(echo "-m ${RAM_SIZE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
return 0
}
configureMonitor() {
MON_OPTS="-name $PROCESS,process=$PROCESS,debug-threads=on"
MON_OPTS+=" -pidfile $QEMU_PID"
return 0
}
configureMachine() {
MAC_OPTS="-machine type=$MACHINE,smm=off,usb=off"
MAC_OPTS+=",vmport=off,dump-guest-core=off,hpet=off${KVM_OPTS}"
return 0
}
configureVirtioDevices() {
DEV_OPTS="-device virtio-balloon-pci,id=balloon0,bus=pcie.0,addr=0x4"
DEV_OPTS+=" -object rng-random,id=objrng0,filename=/dev/urandom"
DEV_OPTS+=" -device virtio-rng-pci,rng=objrng0,id=rng0,bus=pcie.0,addr=0x1c"
return 0
}
buildArguments() {
ARGS="$DEF_OPTS $CPU_OPTS $RAM_OPTS $MAC_OPTS $DISPLAY_OPTS $MON_OPTS $SERIAL_OPTS $NET_OPTS $DISK_OPTS $DEV_OPTS $ARGUMENTS"
ARGS=$(echo "$ARGS" | sed 's/\t/ /g' | tr -s ' ')
return 0
}
configureMemory
configureMonitor
configureMachine
configureProcessor
configureVirtioDevices
buildArguments
return 0 return 0
+337 -307
View File
@@ -27,66 +27,66 @@ SYSTEM="$STORAGE/$BASE.system.img"
[ ! -s "$SYSTEM" ] && error "Virtual DSM system-image does not exist ($SYSTEM)" && exit 82 [ ! -s "$SYSTEM" ] && error "Virtual DSM system-image does not exist ($SYSTEM)" && exit 82
if ! setOwner "$BOOT"; then if ! setOwner "$BOOT"; then
error "Failed to set the owner for \"$BOOT\" !" warn "failed to set the owner for \"$BOOT\" !"
fi fi
if ! setOwner "$SYSTEM"; then if ! setOwner "$SYSTEM"; then
error "Failed to set the owner for \"$SYSTEM\" !" warn "failed to set the owner for \"$SYSTEM\" !"
fi fi
fmt2ext() { fmt2ext() {
local DISK_FMT="$1" local diskFmt="$1"
case "${DISK_FMT,,}" in case "${diskFmt,,}" in
qcow2) echo "qcow2" ;; qcow2) echo "qcow2" ;;
raw) echo "img" ;; raw) echo "img" ;;
*) error "Unrecognized disk format: $DISK_FMT" && exit 78 ;; *) error "Unrecognized disk format: $diskFmt" && exit 78 ;;
esac esac
} }
ext2fmt() { ext2fmt() {
local DISK_EXT="$1" local diskExt="$1"
case "${DISK_EXT,,}" in case "${diskExt,,}" in
qcow2) echo "qcow2" ;; qcow2) echo "qcow2" ;;
img) echo "raw" ;; img) echo "raw" ;;
*) error "Unrecognized file extension: .$DISK_EXT" && exit 78 ;; *) error "Unrecognized file extension: .$diskExt" && exit 78 ;;
esac esac
} }
getSize() { getSize() {
local DISK_FILE="$1" local diskFile="$1"
local DISK_EXT DISK_FMT size local diskExt diskFmt size
DISK_EXT=$(echo "${DISK_FILE//*./}" | sed 's/^.*\.//') diskExt=$(echo "${diskFile//*./}" | sed 's/^.*\.//')
DISK_FMT=$(ext2fmt "$DISK_EXT") diskFmt=$(ext2fmt "$diskExt")
case "${DISK_FMT,,}" in case "${diskFmt,,}" in
raw) raw)
stat -c%s "$DISK_FILE" stat -c%s "$diskFile"
;; ;;
qcow2) qcow2)
size=$(qemu-img info --output=json -f "$DISK_FMT" "$DISK_FILE" | jq -r '."virtual-size" // empty') size=$(qemu-img info --output=json -f "$diskFmt" "$diskFile" | jq -r '."virtual-size" // empty')
if [[ ! "$size" =~ ^[0-9]+$ ]]; then if [[ ! "$size" =~ ^[0-9]+$ ]]; then
error "Failed to determine virtual size of $DISK_FILE" error "Failed to determine virtual size of $diskFile"
exit 78 exit 78
fi fi
echo "$size" echo "$size"
;; ;;
*) *)
error "Unrecognized disk format: $DISK_FMT" error "Unrecognized disk format: $diskFmt"
exit 78 exit 78
;; ;;
esac esac
} }
isCow() { isCow() {
local FS="$1" local fs="$1"
if [[ "${FS,,}" == "btrfs" ]]; then if [[ "${fs,,}" == "btrfs" ]]; then
return 0 return 0
fi fi
@@ -94,92 +94,104 @@ isCow() {
} }
supportsDirect() { supportsDirect() {
local FS="$1" local fs="$1"
if [[ "${FS,,}" == "ecryptfs" || "${FS,,}" == "tmpfs" ]]; then if [[ "${fs,,}" == "ecryptfs" || "${fs,,}" == "tmpfs" ]]; then
return 1 return 1
fi fi
return 0 return 0
} }
validDiskType() {
case "${1,,}" in
"ide" | "sata" | "nvme" | "usb" | "scsi" | "blk" | \
"virtio-blk" | "virtio-scsi" | "auto" | "none" )
return 0
;;
esac
return 1
}
allocateRaw() { allocateRaw() {
local DISK_FILE="$1" local diskFile="$1"
local DATA_SIZE="$2" local dataSize="$2"
if disabled "$ALLOCATE"; then if disabled "$ALLOCATE"; then
truncate -s "$DATA_SIZE" "$DISK_FILE" truncate -s "$dataSize" "$diskFile"
return $? return $?
fi fi
fallocate -l "$DATA_SIZE" "$DISK_FILE" &>/dev/null && return 0 fallocate -l "$dataSize" "$diskFile" &>/dev/null && return 0
fallocate -l -x "$DATA_SIZE" "$DISK_FILE" && return 0 fallocate -l -x "$dataSize" "$diskFile" && return 0
truncate -s "$DATA_SIZE" "$DISK_FILE" || return 1 truncate -s "$dataSize" "$diskFile" || return 1
return 0 return 0
} }
getDiskOptions() { getDiskOptions() {
local FS="$1" local fs="$1"
local DISK_FMT="$2" local diskFmt="$2"
local DISK_PARAM="$DISK_ALLOC" local diskParam="$DISK_ALLOC"
isCow "$FS" && DISK_PARAM+=",nocow=on" isCow "$fs" && diskParam+=",nocow=on"
if [[ "${DISK_FMT,,}" != "raw" ]]; then if [[ "${diskFmt,,}" != "raw" ]]; then
[ -n "$DISK_FLAGS" ] && DISK_PARAM+=",$DISK_FLAGS" [ -n "$DISK_FLAGS" ] && diskParam+=",$DISK_FLAGS"
fi fi
echo "$DISK_PARAM" echo "$diskParam"
return 0 return 0
} }
normalizeSize() { normalizeSize() {
local DISK_SPACE="$1" local diskSpace="$1"
local DISK_DESC="$2" local diskDesc="$2"
local DIR="$3" local dir="$3"
local SPACE FREE GB DATA_SIZE
if [[ "${DISK_SPACE,,}" == "max" || "${DISK_SPACE,,}" == "half" ]]; then local gb free space
local dataSize spare=1073741824
local SPARE=1073741824 if [[ "${diskSpace,,}" == "max" || "${diskSpace,,}" == "half" ]]; then
FREE=$(df --output=avail -B 1 "$DIR" | tail -n 1)
if [[ "${DISK_SPACE,,}" == "max" ]]; then free=$(df --output=avail -B 1 "$dir" | tail -n 1)
FREE=$(( FREE - SPARE ))
if [[ "${diskSpace,,}" == "max" ]]; then
free=$(( free - spare ))
else else
FREE=$(( FREE / 2 )) free=$(( free / 2 ))
fi fi
(( FREE < SPARE )) && FREE="$SPARE" (( free < spare )) && free="$spare"
GB=$(( FREE / 1073741825 )) gb=$(( free / 1073741825 ))
DISK_SPACE="${GB}G" diskSpace="${gb}G"
fi fi
SPACE="${DISK_SPACE// /}" space="${diskSpace// /}"
[ -z "$SPACE" ] && SPACE="256G" [ -z "$space" ] && space="256G"
[ -z "${SPACE//[0-9. ]}" ] && SPACE="${SPACE}G" [ -z "${space//[0-9. ]}" ] && space="${space}G"
SPACE=$(echo "${SPACE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g') space=$(echo "${space^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
if ! numfmt --from=iec "$SPACE" &>/dev/null; then if ! numfmt --from=iec "$space" &>/dev/null; then
error "Invalid value for ${DISK_DESC^^}_SIZE: $DISK_SPACE" && exit 73 error "Invalid value for ${diskDesc^^}_SIZE: $diskSpace" && exit 73
fi fi
DATA_SIZE=$(numfmt --from=iec "$SPACE") dataSize=$(numfmt --from=iec "$space")
if (( DATA_SIZE < 6442450944 )); then if (( dataSize < 6442450944 )); then
error "Please increase the ${DISK_DESC^^}_SIZE variable to at least 6 GB." && exit 73 error "Please increase the ${diskDesc^^}_SIZE variable to at least 6 GB." && exit 73
fi fi
echo "$SPACE" echo "$space"
return 0 return 0
} }
baseDir() { baseDir() {
local path="${1%/}" local path="${1%/}"
@@ -195,74 +207,95 @@ baseDir() {
echo "/$path" echo "/$path"
return 0 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() { createDisk() {
local DISK_FILE="$1" local diskFile="$1"
local DISK_SPACE="$2" local diskSpace="$2"
local DISK_DESC="$3" local diskDesc="$3"
local DISK_FMT="$4" local diskFmt="$4"
local FS="$5" local fs="$5"
local DATA_SIZE DIR BASE_DIR SPACE GB FA
rm -f "$DISK_FILE" local gb dir base
local attributes available
DATA_SIZE=$(numfmt --from=iec "$DISK_SPACE") rm -f "$diskFile"
local dataSize
dataSize=$(numfmt --from=iec "$diskSpace")
if ! disabled "$ALLOCATE"; then if ! disabled "$ALLOCATE"; then
# Check free diskspace # Check free diskspace
DIR=$(dirname "$DISK_FILE") dir=$(dirname "$diskFile")
BASE_DIR=$(baseDir "$DIR") base=$(baseDir "$dir")
if ! SPACE=$(df --output=avail -B 1 "$DIR" | tail -n 1); then freeSpace "$dir"
error "Failed to check free space in $BASE_DIR."
exit 76
fi
if (( DATA_SIZE > SPACE )); then if (( dataSize > available )); then
GB=$(formatBytes "$SPACE") gb=$(formatBytes "$available")
error "Not enough free space to create a $DISK_DESC of ${DISK_SPACE/G/ GB} in $BASE_DIR, it has only $GB 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 ${DISK_DESC^^}_SIZE or disable preallocation by setting ALLOCATE=N." && exit 76 error "Please specify a smaller ${diskDesc^^}_SIZE or disable preallocation by setting ALLOCATE=N." && exit 76
fi fi
fi fi
html "Creating a $DISK_DESC image..." html "Creating a $diskDesc image..."
info "Creating a ${DISK_SPACE/G/ GB} $DISK_STYLE $DISK_DESC image in $DISK_FMT format..." info "Creating a ${diskSpace/G/ GB} $DISK_STYLE $diskDesc image in $diskFmt format..."
local FAIL="Could not create a $DISK_STYLE $DISK_FMT $DISK_DESC image of ${DISK_SPACE/G/ GB} ($DISK_FILE)" local failure="Could not create a $DISK_STYLE $diskFmt $diskDesc image of ${diskSpace/G/ GB} ($diskFile)"
case "${DISK_FMT,,}" in case "${diskFmt,,}" in
raw) raw)
if isCow "$FS"; then if isCow "$fs"; then
if ! touch "$DISK_FILE"; then if ! touch "$diskFile"; then
error "$FAIL" && exit 77 error "$failure" && exit 77
fi fi
{ chattr +C "$DISK_FILE"; } || : { chattr +C "$diskFile"; } || :
fi fi
if ! allocateRaw "$DISK_FILE" "$DATA_SIZE"; then if ! allocateRaw "$diskFile" "$dataSize"; then
rm -f "$DISK_FILE" rm -f "$diskFile"
error "$FAIL" && exit 77 error "$failure" && exit 77
fi fi
;; ;;
qcow2) qcow2)
local DISK_PARAM local diskParam
DISK_PARAM=$(getDiskOptions "$FS" "$DISK_FMT") diskParam=$(getDiskOptions "$fs" "$diskFmt")
if ! qemu-img create -f "$DISK_FMT" -o "$DISK_PARAM" -- "$DISK_FILE" "$DATA_SIZE" ; then if ! qemu-img create -f "$diskFmt" -o "$diskParam" -- "$diskFile" "$dataSize" ; then
rm -f "$DISK_FILE" rm -f "$diskFile"
error "$FAIL" && exit 70 error "$failure" && exit 70
fi fi
;; ;;
esac esac
if isCow "$FS"; then if isCow "$fs"; then
FA=$(lsattr "$DISK_FILE") attributes=$(lsattr "$diskFile")
if [[ "$FA" != *"C"* ]]; then if [[ "$attributes" != *"C"* ]]; then
error "Failed to disable COW for $DISK_DESC image $DISK_FILE on ${FS^^} filesystem (returned $FA)" error "Failed to disable COW for $diskDesc image $diskFile on ${fs^^} filesystem (returned $attributes)"
fi fi
fi fi
@@ -271,54 +304,53 @@ createDisk() {
resizeDisk() { resizeDisk() {
local DISK_FILE="$1" local diskFile="$1"
local DISK_SPACE="$2" local diskSpace="$2"
local DISK_DESC="$3" local diskDesc="$3"
local DISK_FMT="$4" local diskFmt="$4"
local FS="$5" local fs="$5"
local CUR_SIZE DATA_SIZE DIR BASE_DIR SPACE GB
CUR_SIZE=$(getSize "$DISK_FILE") || exit 71 local gb dir base dataSize
DATA_SIZE=$(numfmt --from=iec "$DISK_SPACE") local available currentSize
local REQ=$(( DATA_SIZE - CUR_SIZE ))
(( REQ < 1 )) && error "Shrinking disks is not supported yet, please increase ${DISK_DESC^^}_SIZE." && exit 71 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 if ! disabled "$ALLOCATE"; then
# Check free diskspace # Check free diskspace
DIR=$(dirname "$DISK_FILE") dir=$(dirname "$diskFile")
BASE_DIR=$(baseDir "$DIR") base=$(baseDir "$dir")
if ! SPACE=$(df --output=avail -B 1 "$DIR" | tail -n 1); then freeSpace "$dir"
error "Failed to check free space in $BASE_DIR."
exit 76
fi
if (( REQ > SPACE )); then if (( required > available )); then
GB=$(formatBytes "$SPACE") gb=$(formatBytes "$available")
error "Not enough free space to resize $DISK_DESC to ${DISK_SPACE/G/ GB} in $BASE_DIR, it has only $GB 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 ${DISK_DESC^^}_SIZE or disable preallocation by setting ALLOCATE=N." && exit 74 error "Please specify a smaller ${diskDesc^^}_SIZE or disable preallocation by setting ALLOCATE=N." && exit 74
fi fi
fi fi
GB=$(formatBytes "$CUR_SIZE") gb=$(formatBytes "$currentSize")
MSG="Resizing $DISK_DESC from $GB to ${DISK_SPACE/G/ GB}..." MSG="Resizing $diskDesc from $gb to ${diskSpace/G/ GB}..."
info "$MSG" && html "$MSG" info "$MSG" && html "$MSG"
local FAIL="Could not resize the $DISK_STYLE $DISK_FMT $DISK_DESC image from ${GB} to ${DISK_SPACE/G/ GB} ($DISK_FILE)" local failure="Could not resize the $DISK_STYLE $diskFmt $diskDesc image from ${gb} to ${diskSpace/G/ GB} ($diskFile)"
case "${DISK_FMT,,}" in case "${diskFmt,,}" in
raw) raw)
if ! allocateRaw "$DISK_FILE" "$DATA_SIZE"; then if ! allocateRaw "$diskFile" "$dataSize"; then
error "$FAIL" && exit 75 error "$failure" && exit 75
fi fi
;; ;;
qcow2) qcow2)
if ! qemu-img resize -f "$DISK_FMT" "--$DISK_ALLOC" "$DISK_FILE" "$DATA_SIZE" ; then if ! qemu-img resize -f "$diskFmt" "--$DISK_ALLOC" "$diskFile" "$dataSize" ; then
error "$FAIL" && exit 72 error "$failure" && exit 72
fi fi
;; ;;
@@ -329,135 +361,131 @@ resizeDisk() {
convertDisk() { convertDisk() {
local SOURCE_FILE="$1" local sourceFile="$1"
local SOURCE_FMT="$2" local sourceFmt="$2"
local DST_FILE="$3" local destinationFile="$3"
local DST_FMT="$4" local destinationFmt="$4"
local DISK_BASE="$5" local diskBase="$5"
local DISK_DESC="$6" local diskDesc="$6"
local FS="$7" local fs="$7"
local tmpFile="$diskBase.tmp"
[ -f "$DST_FILE" ] && error "Conversion failed, destination file $DST_FILE already exists?" && exit 79 local gb dir base attributes
[ ! -f "$SOURCE_FILE" ] && error "Conversion failed, source file $SOURCE_FILE does not exist?" && exit 79 local available currentSize
local TMP_FILE="$DISK_BASE.tmp" [ -f "$destinationFile" ] && error "Conversion failed, destination file $destinationFile already exists?" && exit 79
rm -f "$TMP_FILE" [ ! -f "$sourceFile" ] && error "Conversion failed, source file $sourceFile does not exist?" && exit 79
local DIR BASE_DIR FA rm -f "$tmpFile"
DIR=$(dirname "$TMP_FILE")
BASE_DIR=$(baseDir "$DIR") dir=$(dirname "$tmpFile")
base=$(baseDir "$dir")
if ! disabled "$ALLOCATE"; then if ! disabled "$ALLOCATE"; then
local CUR_SIZE SPACE GB
# Check free diskspace # Check free diskspace
CUR_SIZE=$(getSize "$SOURCE_FILE") || exit 79 currentSize=$(getSize "$sourceFile") || exit 79
if ! SPACE=$(df --output=avail -B 1 "$DIR" | tail -n 1); then freeSpace "$dir"
error "Failed to check free space in $BASE_DIR."
exit 76
fi
if (( CUR_SIZE > SPACE )); then if (( currentSize > available )); then
GB=$(formatBytes "$SPACE") gb=$(formatBytes "$available")
error "Not enough free space to convert $DISK_DESC to $DST_FMT in $BASE_DIR, it has only $GB 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 error "Please free up some disk space or disable preallocation by setting ALLOCATE=N." && exit 76
fi fi
fi fi
local msg="Converting $DISK_DESC to $DST_FMT" local msg="Converting $diskDesc to $destinationFmt"
html "$msg..." html "$msg..."
info "$msg, please wait until completed..." info "$msg, please wait until completed..."
local CONV_FLAGS="-p" local convertFlags="-p"
local DISK_PARAM local diskParam
DISK_PARAM=$(getDiskOptions "$FS" "$DST_FMT") diskParam=$(getDiskOptions "$fs" "$destinationFmt")
if [[ "$DST_FMT" != "raw" ]]; then if [[ "$destinationFmt" != "raw" ]]; then
if disabled "$ALLOCATE"; then if disabled "$ALLOCATE"; then
CONV_FLAGS+=" -c" convertFlags+=" -c"
fi fi
fi fi
# shellcheck disable=SC2086 # shellcheck disable=SC2086
if ! qemu-img convert -f "$SOURCE_FMT" $CONV_FLAGS -o "$DISK_PARAM" -O "$DST_FMT" -- "$SOURCE_FILE" "$TMP_FILE"; then if ! qemu-img convert -f "$sourceFmt" $convertFlags -o "$diskParam" -O "$destinationFmt" -- "$sourceFile" "$tmpFile"; then
rm -f "$TMP_FILE" rm -f "$tmpFile"
error "Failed to convert $DISK_STYLE $DISK_DESC image to $DST_FMT format in $BASE_DIR, is there enough space available?" && exit 79 error "Failed to convert $DISK_STYLE $diskDesc image to $destinationFmt format in $base, is there enough space available?" && exit 79
fi fi
if [[ "$DST_FMT" == "raw" ]]; then if [[ "$destinationFmt" == "raw" ]]; then
if ! disabled "$ALLOCATE"; then if ! disabled "$ALLOCATE"; then
# Work around qemu-img bug # Work around qemu-img bug
if ! CUR_SIZE=$(stat -c%s "$TMP_FILE"); then if ! currentSize=$(stat -c%s "$tmpFile"); then
error "Failed to determine converted image size: $TMP_FILE" error "Failed to determine converted image size: $tmpFile"
exit 79 exit 79
fi fi
if ! fallocate -l "$CUR_SIZE" "$TMP_FILE" &>/dev/null; then if ! fallocate -l "$currentSize" "$tmpFile" &>/dev/null; then
if ! fallocate -l -x "$CUR_SIZE" "$TMP_FILE"; then if ! fallocate -l -x "$currentSize" "$tmpFile"; then
error "Failed to allocate $CUR_SIZE bytes for $DISK_DESC image $TMP_FILE" error "Failed to allocate $currentSize bytes for $diskDesc image $tmpFile"
fi fi
fi fi
fi fi
fi fi
if ! rm -f "$SOURCE_FILE"; then if ! mv "$tmpFile" "$destinationFile"; then
rm -f "$TMP_FILE" error "Failed to move converted $diskDesc image to $destinationFile."
error "Failed to remove old $DISK_DESC image $SOURCE_FILE."
exit 79 exit 79
fi fi
if ! mv "$TMP_FILE" "$DST_FILE"; then if ! rm -f "$sourceFile"; then
rm -f "$TMP_FILE" error "Failed to remove old $diskDesc image $sourceFile."
error "Failed to move converted $DISK_DESC image to $DST_FILE."
exit 79 exit 79
fi fi
if isCow "$FS"; then if isCow "$fs"; then
FA=$(lsattr "$DST_FILE") attributes=$(lsattr "$destinationFile")
if [[ "$FA" != *"C"* ]]; then if [[ "$attributes" != *"C"* ]]; then
error "Failed to disable COW for $DISK_DESC image $DST_FILE on ${FS^^} filesystem (returned $FA)" error "Failed to disable COW for $diskDesc image $destinationFile on ${fs^^} filesystem (returned $attributes)"
fi fi
fi fi
msg="Conversion of $DISK_DESC" msg="Conversion of $diskDesc"
html "$msg completed..." info "$msg to $destinationFmt completed successfully!"
info "$msg to $DST_FMT completed successfully!"
return 0 return 0
} }
checkFS () { checkFS () {
local FS="$1" local fs="$1"
local DISK_FILE="$2" local diskFile="$2"
local DISK_DESC="$3" local diskDesc="$3"
local DIR BASE_DIR FA
DIR=$(dirname "$DISK_FILE") local dir base
BASE_DIR=$(baseDir "$DIR") local attributes
[ ! -d "$DIR" ] && return 0
if [[ "${FS,,}" == "overlay"* && "${ENGINE,,}" == "docker" ]]; then dir=$(dirname "$diskFile")
warn "the filesystem of $BASE_DIR is OverlayFS, this usually means it was binded to an invalid path!" 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 fi
if [[ "${FS,,}" == "fuse"* ]]; then if [[ "${fs,,}" == "fuse"* ]]; then
warn "the filesystem of $BASE_DIR is FUSE, this extra layer will negatively affect performance!" warn "the filesystem of $base is FUSE, this extra layer will negatively affect performance!"
fi fi
if ! supportsDirect "$FS"; then if ! supportsDirect "$fs"; then
warn "the filesystem of $BASE_DIR is $FS, which does not support O_DIRECT mode, adjusting settings..." warn "the filesystem of $base is $fs, which does not support O_DIRECT mode, adjusting settings..."
fi fi
if isCow "$FS"; then if isCow "$fs"; then
if [ -f "$DISK_FILE" ]; then if [ -f "$diskFile" ]; then
FA=$(lsattr "$DISK_FILE") attributes=$(lsattr "$diskFile")
if [[ "$FA" != *"C"* ]]; then if [[ "$attributes" != *"C"* ]]; then
warn "COW (copy on write) is not disabled for $DISK_DESC image file $DISK_FILE, this is recommended on ${FS^^} filesystems!" warn "COW (copy on write) is not disabled for $diskDesc image file $diskFile, this is recommended on ${fs^^} filesystems!"
fi fi
fi fi
fi fi
@@ -467,51 +495,54 @@ checkFS () {
createDevice () { createDevice () {
local DISK_FILE="$1" local diskFile="$1"
local DISK_TYPE="$2" local diskType="$2"
local DISK_INDEX="$3" local diskIndex="$3"
local DISK_ADDRESS="$4" local diskAddress="$4"
local DISK_FMT="$5" local diskFmt="$5"
local DISK_IO="$6" local diskIo="$6"
local DISK_CACHE="$7" local diskCache="$7"
local DISK_SERIAL="$8" local diskSerial="$8"
local DISK_SECTORS="$9" local diskSectors="$9"
local DISK_ID="data$DISK_INDEX" local bus="${PCI_BUS:-pcie.0}"
local index="" [[ -z "${PCI_BUS:-}" && ( "${MACHINE,,}" == pc || "${MACHINE,,}" == pc-i440fx* ) ]] && bus="pci.0"
[ -n "$DISK_INDEX" ] && index=",bootindex=$DISK_INDEX"
local result=" -drive file=$DISK_FILE,id=$DISK_ID,format=$DISK_FMT,cache=$DISK_CACHE,aio=$DISK_IO,discard=$DISK_DISCARD,detect-zeroes=on"
case "${DISK_TYPE,,}" in 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" ) ;; "none" ) ;;
"auto" ) "auto" )
echo "$result" echo "$result"
;; ;;
"usb" ) "usb" )
result+=",if=none \ result+=",if=none \
-device usb-storage,drive=${DISK_ID}${index}${DISK_SERIAL}${DISK_SECTORS}" -device usb-storage,drive=${diskId}${bootIndex}${diskSerial}${diskSectors}"
echo "$result" echo "$result"
;; ;;
"nvme" ) "nvme" )
result+=",if=none \ result+=",if=none \
-device nvme,drive=${DISK_ID}${index},serial=deadbeaf${DISK_INDEX}${DISK_SERIAL}${DISK_SECTORS}" -device nvme,drive=${diskId}${bootIndex},serial=deadbeaf${diskIndex}${diskSerial}${diskSectors}"
echo "$result" echo "$result"
;; ;;
"ide" | "sata" ) "ide" | "sata" )
result+=",if=none \ result+=",if=none \
-device ich9-ahci,id=ahci${DISK_INDEX},addr=$DISK_ADDRESS \ -device ich9-ahci,id=ahci${diskIndex},addr=$diskAddress \
-device ide-hd,drive=${DISK_ID},bus=ahci$DISK_INDEX.0,rotation_rate=$DISK_ROTATION${index}${DISK_SERIAL}${DISK_SECTORS}" -device ide-hd,drive=${diskId},bus=ahci$diskIndex.0,rotation_rate=$DISK_ROTATION${bootIndex}${diskSerial}${diskSectors}"
echo "$result" echo "$result"
;; ;;
"blk" | "virtio-blk" ) "blk" | "virtio-blk" )
result+=",if=none \ result+=",if=none \
-device virtio-blk-pci,drive=${DISK_ID},bus=pcie.0,addr=$DISK_ADDRESS,iothread=io2${index}${DISK_SERIAL}${DISK_SECTORS}" -device virtio-blk-pci,drive=${diskId},bus=$bus,addr=$diskAddress,iothread=io2${bootIndex}${diskSerial}${diskSectors}"
echo "$result" echo "$result"
;; ;;
"scsi" | "virtio-scsi" ) "scsi" | "virtio-scsi" )
result+=",if=none \ result+=",if=none \
-device virtio-scsi-pci,id=${DISK_ID}b,bus=pcie.0,addr=$DISK_ADDRESS,iothread=io2 \ -device virtio-scsi-pci,id=${diskId}b,bus=$bus,addr=$diskAddress,iothread=io2,hotplug=off \
-device scsi-hd,drive=${DISK_ID},bus=${DISK_ID}b.0,channel=0,scsi-id=0,lun=0,rotation_rate=$DISK_ROTATION${index}${DISK_SERIAL}${DISK_SECTORS}" -device scsi-hd,drive=${diskId},bus=${diskId}b.0,channel=0,scsi-id=0,lun=0,rotation_rate=$DISK_ROTATION${bootIndex}${diskSerial}${diskSectors}"
echo "$result" echo "$result"
;; ;;
esac esac
@@ -526,73 +557,74 @@ finishDisks () {
[[ "$DISK_OPTS" != *" -object iothread,id=io2"* ]] && DISK_OPTS+=" -object iothread,id=io2" ;; [[ "$DISK_OPTS" != *" -object iothread,id=io2"* ]] && DISK_OPTS+=" -object iothread,id=io2" ;;
esac esac
if ! enabled "$DISK_DISABLE"; then
html "Initialized disks successfully..."
fi
return 0 return 0
} }
addDisk () { addDisk () {
local DISK_BASE="$1" local diskBase="$1"
local DISK_TYPE="$2" local diskType="$2"
local DISK_DESC="$3" local diskDesc="$3"
local DISK_SPACE="$4" local diskSpace="$4"
local DISK_INDEX="$5" local diskIndex="$5"
local DISK_ADDRESS="$6" local diskAddress="$6"
local DISK_FMT="$7" local diskFmt="$7"
local DISK_IO="$8" local diskIo="$8"
local DISK_CACHE="$9" local diskCache="$9"
local DISK_EXT DIR SPACE DATA_SIZE FS PREV_FMT PREV_EXT CUR_SIZE LEFT FREE USED
DISK_EXT=$(fmt2ext "$DISK_FMT") local fs dir used space
local DISK_FILE="$DISK_BASE.$DISK_EXT" local diskExt diskFile
local dataSize missing
local available currentSize
local previousExt previousFmt
DIR=$(dirname "$DISK_FILE") diskExt=$(fmt2ext "$diskFmt")
[ ! -d "$DIR" ] && return 0 diskFile="$diskBase.$diskExt"
SPACE=$(normalizeSize "$DISK_SPACE" "$DISK_DESC" "$DIR") dir=$(dirname "$diskFile")
DATA_SIZE=$(numfmt --from=iec "$SPACE") [ ! -d "$dir" ] && return 0
FS=$(stat -f -c %T "$DIR") space=$(normalizeSize "$diskSpace" "$diskDesc" "$dir")
checkFS "$FS" "$DISK_FILE" "$DISK_DESC" || exit $? dataSize=$(numfmt --from=iec "$space")
if ! supportsDirect "$FS"; then fs=$(stat -f -c %T "$dir")
DISK_IO="threads" checkFS "$fs" "$diskFile" "$diskDesc" || exit $?
DISK_CACHE="writeback"
if ! supportsDirect "$fs"; then
diskIo="threads"
diskCache="writeback"
fi fi
if [ ! -s "$DISK_FILE" ] ; then if [ ! -s "$diskFile" ] ; then
if [[ "${DISK_FMT,,}" != "raw" ]]; then if [[ "${diskFmt,,}" != "raw" ]]; then
PREV_FMT="raw" previousFmt="raw"
else else
PREV_FMT="qcow2" previousFmt="qcow2"
fi fi
PREV_EXT=$(fmt2ext "$PREV_FMT") previousExt=$(fmt2ext "$previousFmt")
if [ -s "$DISK_BASE.$PREV_EXT" ] ; then if [ -s "$diskBase.$previousExt" ] ; then
convertDisk "$DISK_BASE.$PREV_EXT" "$PREV_FMT" "$DISK_FILE" "$DISK_FMT" "$DISK_BASE" "$DISK_DESC" "$FS" || exit $? convertDisk "$diskBase.$previousExt" "$previousFmt" "$diskFile" "$diskFmt" "$diskBase" "$diskDesc" "$fs" || exit $?
fi fi
fi fi
if [ -s "$DISK_FILE" ]; then if [ -s "$diskFile" ]; then
CUR_SIZE=$(getSize "$DISK_FILE") || exit 71 currentSize=$(getSize "$diskFile") || exit 71
if (( DATA_SIZE > CUR_SIZE )); then if (( dataSize > currentSize )); then
resizeDisk "$DISK_FILE" "$SPACE" "$DISK_DESC" "$DISK_FMT" "$FS" || exit $? resizeDisk "$diskFile" "$space" "$diskDesc" "$diskFmt" "$fs" || exit $?
else else
if (( DATA_SIZE < CUR_SIZE )); then if (( dataSize < currentSize )); then
if [[ "${DISK_SPACE,,}" != "max" && "${DISK_SPACE,,}" != "half" ]]; then if [[ "${diskSpace,,}" != "max" && "${diskSpace,,}" != "half" ]]; then
info "You decreased the ${DISK_DESC^^}_SIZE variable to ${DISK_SPACE/G/ GB} but shrinking disks is not supported, will be ignored..." info "You decreased the ${diskDesc^^}_SIZE variable to ${diskSpace/G/ GB} but shrinking disks is not supported, will be ignored..."
fi fi
fi fi
@@ -600,73 +632,72 @@ addDisk () {
else else
createDisk "$DISK_FILE" "$SPACE" "$DISK_DESC" "$DISK_FMT" "$FS" || exit $? createDisk "$diskFile" "$space" "$diskDesc" "$diskFmt" "$fs" || exit $?
fi fi
if [ -f "$DISK_FILE" ] && disabled "$ALLOCATE"; then if [ -f "$diskFile" ] && disabled "$ALLOCATE"; then
CUR_SIZE=$(getSize "$DISK_FILE") || exit 73 currentSize=$(getSize "$diskFile") || exit 73
USED=$(du -sB 1 "$DISK_FILE" | cut -f1) used=$(du -sB 1 "$diskFile" | cut -f1)
FREE=$(df --output=avail -B 1 "$DIR" | tail -n 1) available=$(df --output=avail -B 1 "$dir" | tail -n 1)
LEFT=$(( CUR_SIZE - USED - FREE )) missing=$(( currentSize - used - available ))
(( LEFT < 0 )) && LEFT=0 (( missing < 0 )) && missing=0
if (( LEFT > 0 )); then if (( missing > 0 )); then
local GB BASE_DIR local gb base msg
GB=$(formatBytes "$FREE")
BASE_DIR=$(baseDir "$DIR")
LEFT=$(formatBytes "$LEFT")
CUR_SIZE=$(formatBytes "$CUR_SIZE")
msg="The virtual size of the ${DISK_DESC,,} is $CUR_SIZE"
if [ -n "$USED" ] && [[ "$USED" != "0" ]]; then gb=$(formatBytes "$available")
USED=$(formatBytes "$USED") base=$(baseDir "$dir")
msg+=" (of which $USED is used)" missing=$(formatBytes "$missing")
currentSize=$(formatBytes "$currentSize")
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 fi
info "$msg, but there is only $GB of free space remaining in $BASE_DIR now." info "$msg, but there is only $gb of free space remaining in $base now."
info "Please consider making at least $LEFT more space available in $BASE_DIR for future expansions." info "Please consider making at least $missing more space available in $base for future expansions."
fi fi
fi
if [ -f "$DISK_FILE" ]; then
if ! setOwner "$DISK_FILE"; then
error "Failed to set the owner for \"$DISK_FILE\" !"
exit 77
fi
fi fi
DISK_OPTS+=$(createDevice "$DISK_FILE" "$DISK_TYPE" "$DISK_INDEX" "$DISK_ADDRESS" "$DISK_FMT" "$DISK_IO" "$DISK_CACHE" "" "") 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 return 0
} }
addDevice () { addDevice () {
local DISK_DEV="$1" local diskDev="$1"
local DISK_TYPE="$2" local diskType="$2"
local DISK_INDEX="$3" local diskIndex="$3"
local DISK_ADDRESS="$4" local diskAddress="$4"
[ -z "$DISK_DEV" ] && return 0
[ ! -b "$DISK_DEV" ] && error "Device $DISK_DEV cannot be found! Please add it to the 'devices' section of your compose file." && exit 55
local sectors="" local sectors=""
local dev_type="" local devType
dev_type=$(lsblk -no TYPE "$DISK_DEV" 2>/dev/null | head -n1) devType=$(lsblk -no TYPE "$diskDev" 2>/dev/null | head -n1)
# Only detect and apply sector sizes for partitions, not whole disks [ -z "$diskDev" ] && return 0
# Whole disk passthrough with explicit sector sizes causes DSM not to recognize the disk [ ! -b "$diskDev" ] && error "Device $diskDev cannot be found! Please add it to the 'devices' section of your compose file." && exit 55
if [[ "$dev_type" == "part" ]]; then
local result=""
local logical=""
local physical=""
result=$(fdisk -l "$DISK_DEV" 2>/dev/null | grep -m 1 -o "(logical/physical): .*" | cut -c 21- || true) # 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 if [ -n "$result" ]; then
logical="${result%% *}" logical="${result%% *}"
@@ -675,13 +706,14 @@ addDevice () {
fi fi
if [ -z "$logical" ] || [ -z "$physical" ]; then if [ -z "$logical" ] || [ -z "$physical" ]; then
warn "Failed to determine the sector size for $DISK_DEV" warn "Failed to determine the sector size for $diskDev"
elif [[ "$physical" != "512" ]]; then elif [[ "$physical" != "512" ]]; then
sectors=",logical_block_size=$logical,physical_block_size=$physical" sectors=",logical_block_size=$logical,physical_block_size=$physical"
fi fi
fi fi
DISK_OPTS+=$(createDevice "$DISK_DEV" "$DISK_TYPE" "$DISK_INDEX" "$DISK_ADDRESS" "raw" "$DISK_IO" "$DISK_CACHE" "" "$sectors") DISK_OPTS+=$(createDevice "$diskDev" "$diskType" "$diskIndex" "$diskAddress" "raw" "$DISK_IO" "$DISK_CACHE" "" "$sectors")
return 0 return 0
} }
@@ -693,7 +725,6 @@ addDevice () {
if ! enabled "$DISK_DISABLE"; then if ! enabled "$DISK_DISABLE"; then
msg="Initializing disks..." msg="Initializing disks..."
html "$msg"
enabled "$DEBUG" && echo "$msg" enabled "$DEBUG" && echo "$msg"
fi fi
@@ -726,10 +757,10 @@ case "$DISK_FMT" in
* ) error "Invalid DISK_FMT specified, value \"$DISK_FMT\" is not recognized!" && exit 78 ;; * ) error "Invalid DISK_FMT specified, value \"$DISK_FMT\" is not recognized!" && exit 78 ;;
esac esac
case "${DISK_TYPE,,}" in if ! validDiskType "$DISK_TYPE"; then
"ide" | "sata" | "nvme" | "usb" | "scsi" | "blk" | "virtio-blk" | "virtio-scsi" | "auto" | "none" ) ;; error "Invalid DISK_TYPE specified, value \"$DISK_TYPE\" is not recognized!"
* ) error "Invalid DISK_TYPE specified, value \"$DISK_TYPE\" is not recognized!" && exit 80 ;; exit 80
esac fi
if [[ "$DISK_FLAGS" =~ [[:space:]] ]]; then if [[ "$DISK_FLAGS" =~ [[:space:]] ]]; then
error "Invalid DISK_FLAGS value '$DISK_FLAGS', spaces are not allowed." error "Invalid DISK_FLAGS value '$DISK_FLAGS', spaces are not allowed."
@@ -752,8 +783,7 @@ DISK_OPTS+=$(createDevice "$BOOT" "$DISK_TYPE" "1" "0xa" "raw" "$DISK_IO" "$DISK
DISK_OPTS+=$(createDevice "$SYSTEM" "$DISK_TYPE" "2" "0xb" "raw" "$DISK_IO" "$DISK_CACHE" "" "") DISK_OPTS+=$(createDevice "$SYSTEM" "$DISK_TYPE" "2" "0xb" "raw" "$DISK_IO" "$DISK_CACHE" "" "")
if enabled "$DISK_DISABLE"; then if enabled "$DISK_DISABLE"; then
finishDisks finishDisks && return 0
return 0
fi fi
DISK1_FILE="$STORAGE/${DISK_NAME}" DISK1_FILE="$STORAGE/${DISK_NAME}"
+14 -3
View File
@@ -6,19 +6,26 @@ set -Eeuo pipefail
: "${GPU:="N"}" # GPU passthrough : "${GPU:="N"}" # GPU passthrough
: "${VGA:="virtio"}" # VGA adaptor : "${VGA:="virtio"}" # VGA adaptor
: "${DISPLAY:="none"}" # Display type : "${DISPLAY:="none"}" # Display type
: "${LOSSY:="N"}" # Lossy VNC compression
: "${RENDERNODE:="/dev/dri/renderD128"}" # Render node : "${RENDERNODE:="/dev/dri/renderD128"}" # Render node
# Sanitize variables # Sanitize variables
VGA=$(strip "$VGA") VGA=$(strip "$VGA")
LOSSY=$(strip "$LOSSY")
DISPLAY=$(strip "$DISPLAY") DISPLAY=$(strip "$DISPLAY")
RENDERNODE=$(strip "$RENDERNODE") RENDERNODE=$(strip "$RENDERNODE")
CPU_VENDOR=$(lscpu | awk '/Vendor ID/{print $3}') CPU_VENDOR=$(lscpu | awk '/Vendor ID/{print $3}')
if ! enabled "$GPU" || [[ "$CPU_VENDOR" != "GenuineIntel" || "$ARCH" != "amd64" ]]; then if ! enabled "$GPU" || isAmdCpu || [[ "$ARCH" != "amd64" ]]; then
[[ "${DISPLAY,,}" == "none" ]] && VGA="none" [[ "${DISPLAY,,}" == "none" ]] && VGA="none"
DISPLAY_OPTS="-display $DISPLAY -vga $VGA"
if enabled "$LOSSY" && [[ "${DISPLAY,,}" == vnc=* ]]; then
DISPLAY+=",lossy=on"
fi
DISPLAY_OPTS="-display ${DISPLAY} -vga ${VGA}"
return 0 return 0
fi fi
@@ -27,7 +34,7 @@ msg="Configuring display drivers..."
html "$msg" html "$msg"
enabled "$DEBUG" && echo "$msg" enabled "$DEBUG" && echo "$msg"
DISPLAY_OPTS="-display egl-headless,rendernode=$RENDERNODE" DISPLAY_OPTS="-display egl-headless,rendernode=${RENDERNODE}"
DISPLAY_OPTS+=" -vga $VGA" DISPLAY_OPTS+=" -vga $VGA"
[ ! -d /dev/dri ] && mkdir -m 755 /dev/dri [ ! -d /dev/dri ] && mkdir -m 755 /dev/dri
@@ -48,6 +55,10 @@ if [ ! -c "$RENDERNODE" ]; then
fi fi
fi fi
if [ ! -c "$RENDERNODE" ] || [ ! -r "$RENDERNODE" ] || [ ! -w "$RENDERNODE" ]; then
warn "render device '${RENDERNODE}' is unavailable or inaccessible."
fi
addPackage "xserver-xorg-video-intel" "Intel GPU drivers" addPackage "xserver-xorg-video-intel" "Intel GPU drivers"
addPackage "qemu-system-modules-opengl" "OpenGL module" addPackage "qemu-system-modules-opengl" "OpenGL module"
+6 -3
View File
@@ -32,14 +32,17 @@ if ! enabled "$SHUTDOWN"; then
exec "${cmd[@]}" ${ARGS:+ $ARGS} exec "${cmd[@]}" ${ARGS:+ $ARGS}
fi fi
if [ ! -t 1 ] || [ ! -c /dev/tty ]; then if ! interactive; then
"${cmd[@]}" ${ARGS:+ $ARGS} & "${cmd[@]}" ${ARGS:+ $ARGS} &
else else
"${cmd[@]}" ${ARGS:+ $ARGS} </dev/tty >/dev/tty & startConsole
startQemu "${cmd[@]}" ${ARGS:+ $ARGS}
fi fi
pid=$!
rc=0 rc=0
wait $! || rc=$?
wait "$pid" || rc=$?
[ -f "$QEMU_END" ] && exit "$rc" [ -f "$QEMU_END" ] && exit "$rc"
sleep 1 & wait $! sleep 1 & wait $!
+55 -28
View File
@@ -17,7 +17,7 @@ DIR=$(find / -maxdepth 1 -type d -iname "$FN" -print -quit)
[ ! -d "$DIR" ] && DIR=$(find "$STORAGE" -maxdepth 1 -type d -iname "$FN" -print -quit) [ ! -d "$DIR" ] && DIR=$(find "$STORAGE" -maxdepth 1 -type d -iname "$FN" -print -quit)
if [ -d "$DIR" ]; then if [ -d "$DIR" ]; then
BASE="DSM_VirtualDSM" && URL="file://$DIR" BASE="DSM_VirtualDSM" && URL="file://$DIR"
if [[ ! -s "$STORAGE/$BASE.boot.img" || ! -s "$STORAGE/$BASE.system.img" ]]; then if [[ ! -s "$STORAGE/$BASE.boot.img" || ! -s "$STORAGE/$BASE.system.img" ]]; then
error "The bind $DIR maps to a file that does not exist!" && exit 65 error "The bind $DIR maps to a file that does not exist!" && exit 65
fi fi
@@ -25,7 +25,7 @@ fi
FILE=$(find / -maxdepth 1 -type f -iname "$FN" -print -quit) 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" ] && FILE=$(find "$STORAGE" -maxdepth 1 -type f -iname "$FN" -print -quit)
[ -s "$FILE" ] && BASE="DSM_VirtualDSM" && URL="file://$FILE" [ -s "$FILE" ] && BASE="DSM_VirtualDSM" && URL="file://$FILE"
URL=$(strip "$URL") URL=$(strip "$URL")
@@ -36,7 +36,7 @@ if [ -n "$URL" ] && [ ! -s "$FILE" ] && [ ! -d "$DIR" ]; then
printf -v BASE '%b' "${BASE//%/\\x}" printf -v BASE '%b' "${BASE//%/\\x}"
BASE="${BASE//[!A-Za-z0-9._-]/_}" BASE="${BASE//[!A-Za-z0-9._-]/_}"
fi fi
if [[ "${URL,,}" != "http"* && "${URL,,}" != "file:"* ]] ; then if [[ "${URL,,}" != "http"* && "${URL,,}" != "file:"* ]]; then
[ ! -s "$STORAGE/$BASE.pat" ] && error "Invalid URL: $URL" && exit 65 [ ! -s "$STORAGE/$BASE.pat" ] && error "Invalid URL: $URL" && exit 65
URL="file://$STORAGE/$BASE.pat" URL="file://$STORAGE/$BASE.pat"
fi fi
@@ -126,13 +126,6 @@ SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
SPACE_GB=$(formatBytes "$SPACE") 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 (( MIN_SPACE > SPACE )) && error "Not enough free space for installation in $STORAGE, have $SPACE_GB available but need at least 14 GB." && exit 94
# Check if output is to interactive TTY
if [ -t 1 ]; then
PROGRESS="--progress=bar:noscroll"
else
PROGRESS="--progress=dot:giga"
fi
if [[ "$URL" == "file://"* ]]; then if [[ "$URL" == "file://"* ]]; then
MSG="Copying DSM" MSG="Copying DSM"
ERR="Failed to copy ${URL:7}" ERR="Failed to copy ${URL:7}"
@@ -159,20 +152,53 @@ if [[ "$URL" == "file://"* ]]; then
else else
SIZE=0 SIZE=0
REASON=""
PROGRESS=()
OUTPUT=""
LOG=$(mktemp)
[[ "${URL,,}" == *"_72806.pat" ]] && SIZE=361010261 [[ "${URL,,}" == *"_72806.pat" ]] && SIZE=361010261
[[ "${URL,,}" == *"_69057.pat" ]] && SIZE=363837333 [[ "${URL,,}" == *"_69057.pat" ]] && SIZE=363837333
[[ "${URL,,}" == *"_42218.pat" ]] && SIZE=379637760 [[ "${URL,,}" == *"_42218.pat" ]] && SIZE=379637760
/run/progress.sh "$PAT" "$SIZE" "$MSG ([P])..." & # 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"
fi
{ wget "$URL" -O "$PAT" -q --no-check-certificate --timeout=10 --no-http-keep-alive --show-progress "$PROGRESS"; rc=$?; } || : /run/progress.sh "$PAT" "$SIZE" "$MSG ([P])..." "$OUTPUT" 52428800 &
{
LC_ALL=C wget "$URL" -O "$PAT" --no-verbose --no-check-certificate \
--timeout=30 --no-http-keep-alive "${PROGRESS[@]}" \
--output-file="$LOG"
rc=$?
} || :
fKill "progress.sh" fKill "progress.sh"
(( rc == 3 )) && error "$ERR , cannot write file (disk full?)" && exit 69 if (( rc != 0 )); then
(( rc == 4 )) && error "$ERR , network failure!" && exit 69 REASON=$(sed -n \
(( rc == 8 )) && error "$ERR , server issued an error response!" && exit 69 -e 's/^wget: //p' \
(( rc != 0 )) && error "$ERR , reason: $rc" && exit 69 -e 's/^[0-9-]\{10\} [0-9:]\{8\} ERROR //p' \
"$LOG" | tail -n 1)
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
fi
fi fi
@@ -228,7 +254,7 @@ if ! touch "$SYSTEM"; then
error "Could not create file $SYSTEM for the system disk." && exit 98 error "Could not create file $SYSTEM for the system disk." && exit 98
fi fi
! setOwner "$SYSTEM" && error "Failed to set the owner for \"$SYSTEM\" !" ! setOwner "$SYSTEM" && warn "failed to set the owner for \"$SYSTEM\" !"
if [[ "${FS,,}" == "btrfs" ]]; then if [[ "${FS,,}" == "btrfs" ]]; then
{ chattr +C "$SYSTEM"; } || : { chattr +C "$SYSTEM"; } || :
@@ -249,14 +275,15 @@ fi
PART="$TMP/partition.fdisk" PART="$TMP/partition.fdisk"
{ echo "label: dos" {
echo "label-id: 0x6f9ee2e9" echo "label: dos"
echo "device: $SYSTEM" echo "label-id: 0x6f9ee2e9"
echo "unit: sectors" echo "device: $SYSTEM"
echo "sector-size: 512" echo "unit: sectors"
echo "" echo "sector-size: 512"
echo "${SYSTEM}1 : start= 2048, size= 16777216, type=83" echo ""
echo "${SYSTEM}2 : start= 16779264, size= 4194304, type=82" echo "${SYSTEM}1 : start= 2048, size= 16777216, type=83"
echo "${SYSTEM}2 : start= 16779264, size= 4194304, type=82"
} > "$PART" } > "$PART"
sfdisk -q "$SYSTEM" < "$PART" sfdisk -q "$SYSTEM" < "$PART"
@@ -302,7 +329,7 @@ fakeroot -- bash -c "set -Eeu;\
rm -rf "$MOUNT" rm -rf "$MOUNT"
echo "$BASE" > "$STORAGE/dsm.ver" echo "$BASE" > "$STORAGE/dsm.ver"
! setOwner "$STORAGE/dsm.ver" && error "Failed to set the owner for \"$STORAGE/dsm.ver\" !" ! setOwner "$STORAGE/dsm.ver" && warn "failed to set the owner for \"$STORAGE/dsm.ver\" !"
if [[ "$URL" == "file://$STORAGE/$BASE.pat" ]]; then if [[ "$URL" == "file://$STORAGE/$BASE.pat" ]]; then
rm -f "$PAT" rm -f "$PAT"
@@ -311,11 +338,11 @@ else
fi fi
if [ -f "$STORAGE/$BASE.pat" ]; then if [ -f "$STORAGE/$BASE.pat" ]; then
! setOwner "$STORAGE/$BASE.pat" && error "Failed to set the owner for \"$STORAGE/$BASE.pat\" !" ! setOwner "$STORAGE/$BASE.pat" && warn "failed to set the owner for \"$STORAGE/$BASE.pat\" !"
fi fi
mv -f "$BOOT" "$STORAGE/$BASE.boot.img" mv -f "$BOOT" "$STORAGE/$BASE.boot.img"
! setOwner "$STORAGE/$BASE.boot.img" && error "Failed to set the owner for \"$STORAGE/$BASE.boot.img\" !" ! setOwner "$STORAGE/$BASE.boot.img" && warn "failed to set the owner for \"$STORAGE/$BASE.boot.img\" !"
rm -rf "$TMP" rm -rf "$TMP"
+12 -11
View File
@@ -4,21 +4,23 @@ set -Eeuo pipefail
msg="Checking memory..." msg="Checking memory..."
enabled "$DEBUG" && echo "$msg" enabled "$DEBUG" && echo "$msg"
RAM_AVAIL=$(free -b | grep -m 1 Mem: | awk '{print $7}') app() {
AVAIL_MEM=$(formatBytes "$RAM_AVAIL") echo "Virtual DSM"
return 0
}
checkConfiguredMemory() { checkConfiguredMemory() {
local wanted msg
if disabled "$RAM_CHECK" || [[ "${RAM_SIZE,,}" == "max" || "${RAM_SIZE,,}" == "half" ]]; then if disabled "$RAM_CHECK" || [[ "${RAM_SIZE,,}" == "max" || "${RAM_SIZE,,}" == "half" ]]; then
return 0 return 0
fi fi
local wanted msg avail_mem
wanted=$(numfmt --from=iec "$RAM_SIZE") wanted=$(numfmt --from=iec "$RAM_SIZE")
avail_mem=$(formatBytes "$RAM_AVAIL")
if (( (wanted + RAM_SPARE) > RAM_AVAIL )); then if (( (wanted + RAM_SPARE) > RAM_AVAIL )); then
msg="Your configured RAM_SIZE of ${RAM_SIZE/G/ GB} is too high for the $AVAIL_MEM of free memory available," msg="Your configured RAM_SIZE of ${RAM_SIZE/G/ GB} is too high for the $avail_mem of free memory available,"
if [[ "${FS,,}" == "zfs" ]]; then if [[ "${FS,,}" == "zfs" ]]; then
info "$msg but since ZFS is active this will be ignored." info "$msg but since ZFS is active this will be ignored."
else else
@@ -27,7 +29,7 @@ checkConfiguredMemory() {
fi fi
else else
if (( (wanted + (RAM_SPARE * 3)) > RAM_AVAIL )); then if (( (wanted + (RAM_SPARE * 3)) > RAM_AVAIL )); then
msg="your configured RAM_SIZE of ${RAM_SIZE/G/ GB} is very close to the $AVAIL_MEM of free memory available," msg="your configured RAM_SIZE of ${RAM_SIZE/G/ GB} is very close to the $avail_mem of free memory available,"
if [[ "${FS,,}" == "zfs" ]]; then if [[ "${FS,,}" == "zfs" ]]; then
info "$msg but since ZFS is active this will be ignored." info "$msg but since ZFS is active this will be ignored."
else else
@@ -40,7 +42,6 @@ checkConfiguredMemory() {
} }
configureHalfMemory() { configureHalfMemory() {
local wanted local wanted
if [[ "${RAM_SIZE,,}" != "half" ]]; then if [[ "${RAM_SIZE,,}" != "half" ]]; then
@@ -50,7 +51,7 @@ configureHalfMemory() {
if (( (RAM_AVAIL / 2) > RAM_SPARE )); then if (( (RAM_AVAIL / 2) > RAM_SPARE )); then
wanted=$(( (RAM_AVAIL / 2) / 1048577 )) wanted=$(( (RAM_AVAIL / 2) / 1048577 ))
RAM_SIZE="${wanted}M" RAM_SIZE="${wanted}M"
info "Allocated $wanted MB of RAM for the virtual machine." info "Allocated $wanted MB of RAM for $(app)."
else else
RAM_SIZE="max" RAM_SIZE="max"
fi fi
@@ -59,7 +60,6 @@ configureHalfMemory() {
} }
configureMaxMemory() { configureMaxMemory() {
local wanted local wanted
if [[ "${RAM_SIZE,,}" != "max" ]]; then if [[ "${RAM_SIZE,,}" != "max" ]]; then
@@ -83,13 +83,12 @@ configureMaxMemory() {
wanted=$(( wanted / 1048577 )) wanted=$(( wanted / 1048577 ))
RAM_SIZE="${wanted}M" RAM_SIZE="${wanted}M"
info "Allocated $wanted MB of RAM for the virtual machine." info "Allocated $wanted MB of RAM for $(app)."
return 0 return 0
} }
checkMinimumMemory() { checkMinimumMemory() {
local wanted local wanted
wanted=$(numfmt --from=iec "$RAM_SIZE") wanted=$(numfmt --from=iec "$RAM_SIZE")
@@ -103,6 +102,8 @@ checkMinimumMemory() {
return 0 return 0
} }
getMemoryInfo
checkConfiguredMemory checkConfiguredMemory
configureHalfMemory configureHalfMemory
configureMaxMemory configureMaxMemory
+892 -346
View File
File diff suppressed because it is too large Load Diff
+172 -35
View File
@@ -2,25 +2,31 @@
set -Eeuo pipefail set -Eeuo pipefail
: "${SHUTDOWN:="Y"}" # Graceful ACPI shutdown : "${SHUTDOWN:="Y"}" # Graceful ACPI shutdown
: "${TIMEOUT:="115"}" # QEMU termination timeout : "${TIMEOUT:="105"}" # QEMU termination timeout
: "${API_TIMEOUT:="90"}" # External API call timeout : "${API_TIMEOUT:="90"}" # External API call timeout
# Configure QEMU for graceful shutdown # Configure QEMU for graceful shutdown
API_CMD=6 API_CMD=6
API_HOST="127.0.0.1:$COM_PORT"
# Configure QEMU for graceful shutdown SHUTDOWN_SKIP=0
SHUTDOWN_SIGNAL=0
QEMU_END="$QEMU_DIR/qemu.end" QEMU_END="$QEMU_DIR/qemu.end"
CONSOLE_PID="$QEMU_DIR/console.pid"
CONSOLE_SOCKET="$QEMU_DIR/console.sock"
QEMU_START_PID="$QEMU_DIR/qemu.start.pid"
_trap() { _trap() {
local func="$1"; shift local func="$1"; shift
local sig local sig
TRAP_PID=$BASHPID TRAP_PID=$BASHPID
for sig; do for sig; do
# Capture the local callback and signal while registering the trap.
# shellcheck disable=SC2064
trap "$func $sig" "$sig" trap "$func $sig" "$sig"
done done
@@ -62,19 +68,55 @@ displayReason() {
readQemuPid() { readQemuPid() {
local -n _pid="$1" local -n _pid="$1"
local file
if [ ! -s "$QEMU_PID" ] || ! read -r _pid <"$QEMU_PID"; then for file in "$QEMU_START_PID" "$QEMU_PID"; do
return 1 if [ -s "$file" ] && read -r _pid < "$file"; then
fi return 0
fi
done
return 1
}
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 -n _pid="$1"
local cnt=0 value=""
while ! readQemuPid value; do
sleep 0.02
cnt=$((cnt + 1))
(( cnt >= 50 )) && return 1
done
_pid="$value"
return 0 return 0
} }
forceKillQemu() { forceKillQemu() {
local reason="$1" local reason="$1"
local pid="" local pid="" display
local display
! readQemuPid pid && return 0 ! readQemuPid pid && return 0
! isAlive "$pid" && return 0 ! isAlive "$pid" && return 0
@@ -88,29 +130,115 @@ forceKillQemu() {
cleanupHelpers() { cleanupHelpers() {
local pids=( "${HOST_PID:-}" "${WSD_PID:-}" \ local pids=( "${HOST_PID:-}" "${WSD_PID:-}" "${CONSOLE_PID:-}" \
"${WEB_PID:-}" "${PASST_PID:-}" "${DNSMASQ_PID:-}" ) "${WEB_PID:-}" "${PASST_PID:-}" "${DNSMASQ_PID:-}" )
mKill "${pids[@]}" mKill "${pids[@]}"
fKill "print.sh" fKill "print.sh"
rm -f -- "$HOST_API_SOCKET" "$HOST_AGENT_SOCKET"
closeNetwork closeNetwork
return 0 return 0
} }
startConsole() {
local output="${1:-/dev/tty}"
local cnt=0 pid=""
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"
) &
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"
(
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() { finish() {
local reason=$1 local reason=$1 failed=0
if [ ! -f "$QEMU_END" ] && (( reason != 0 )); then
failed=1
fi
touch "$QEMU_END" touch "$QEMU_END"
forceKillQemu "$reason" forceKillQemu "$reason"
cleanupHelpers cleanupHelpers
if ! waitPidFile "$QEMU_PID" 10; then if ! waitQemuExit 10; then
warn "Timed out while waiting for $(app) to exit!" warn "Timed out while waiting for $(app) to exit!"
fi fi
(( reason != 1 )) && echo && echo " Shutdown completed!" echo
if (( failed == 0 )); then
echo " Shutdown completed!"
else
error "QEMU exited unexpectedly!"
fi
exit "$reason" exit "$reason"
} }
@@ -118,16 +246,15 @@ finish() {
sendGuestShutdown() { sendGuestShutdown() {
local pid="$1" local pid="$1"
local response local response url
local url
# Don't send the powerdown signal because vDSM ignores ACPI signals # Don't send the powerdown signal because vDSM ignores ACPI signals
# nc -q 1 -w 1 -U "$QEMU_DIR/monitor.sock" &> /dev/null <<<'system_powerdown' || : # nc -q 1 -w 1 -U "$QEMU_DIR/monitor.sock" &> /dev/null <<<'system_powerdown' || :
# Send shutdown command to guest agent via serial port # Send shutdown command to guest agent via serial port
API_TIMEOUT=$(strip "$API_TIMEOUT") API_TIMEOUT=$(strip "$API_TIMEOUT")
url="http://$API_HOST/read?command=$API_CMD&timeout=$API_TIMEOUT" url="http://localhost/read?command=$API_CMD&timeout=$API_TIMEOUT"
response=$(curl -sk -m "$(( API_TIMEOUT+2 ))" -S "$url" 2>&1) response=$(curl --unix-socket "$HOST_API_SOCKET" -sk -m "$(( API_TIMEOUT+2 ))" -S "$url" 2>&1)
if [[ "$response" =~ "\"success\"" ]]; then if [[ "$response" =~ "\"success\"" ]]; then
@@ -150,13 +277,10 @@ normalizeTimeout() {
local term_grace=3 # seconds before loop ends to send SIGTERM local term_grace=3 # seconds before loop ends to send SIGTERM
local cleanup_grace=3 # seconds reserved after the loop for cleanup local cleanup_grace=3 # seconds reserved after the loop for cleanup
local elapsed
local timeout_left
local min
TIMEOUT=$(strip "$TIMEOUT") TIMEOUT=$(strip "$TIMEOUT")
if [[ ! "$TIMEOUT" =~ ^[0-9]+$ ]]; then if [[ ! "$TIMEOUT" =~ ^[0-9]+$ ]]; then
TIMEOUT=115 TIMEOUT=105
fi fi
if (( TIMEOUT >= 30 )); then if (( TIMEOUT >= 30 )); then
@@ -167,10 +291,10 @@ normalizeTimeout() {
cleanup_grace=4 cleanup_grace=4
fi fi
elapsed=$((SECONDS - start)) local elapsed=$((SECONDS - start))
timeout_left=$((TIMEOUT - elapsed)) local timeout_left=$((TIMEOUT - elapsed))
min=$((term_grace + cleanup_grace + 1)) local min=$((term_grace + cleanup_grace + 1))
(( timeout_left < min )) && timeout_left=$min (( timeout_left < min )) && timeout_left=$min
wait_until=$((timeout_left - cleanup_grace)) wait_until=$((timeout_left - cleanup_grace))
@@ -184,18 +308,17 @@ waitForShutdown() {
local cnt=0 local cnt=0
local pid="$1" local pid="$1"
local name="$APP" local name="$APP"
local slp
while (( cnt <= wait_until )); do while (( cnt <= wait_until && SHUTDOWN_SKIP == 0 )); do
sleep 1 & sleep 1 &
slp=$! local slp=$!
# Stop waiting if the process has exited # Stop waiting if the process has exited
! isAlive "$pid" && break ! isAlive "$pid" && break
# Workaround for stale/zombie QEMU pid file # Workaround for stale/zombie QEMU pid file
[ ! -s "$QEMU_PID" ] && break [ ! -s "$QEMU_START_PID" ] && [ ! -s "$QEMU_PID" ] && break
if (( cnt == sigterm_at )); then if (( cnt == sigterm_at )); then
info "${name^} is still running, sending SIGTERM... ($cnt/$wait_until)" info "${name^} is still running, sending SIGTERM... ($cnt/$wait_until)"
@@ -204,7 +327,7 @@ waitForShutdown() {
info "Waiting for $name to shut down... ($cnt/$wait_until)" info "Waiting for $name to shut down... ($cnt/$wait_until)"
fi fi
wait "$slp" wait "$slp" || :
(( cnt++ )) (( cnt++ ))
done done
@@ -215,29 +338,39 @@ waitForShutdown() {
graceful_shutdown() { graceful_shutdown() {
local sig="$1" local sig="$1"
local pid="" local pid="" code=0
local code=0
[[ $BASHPID != "$TRAP_PID" ]] && return [[ $BASHPID != "$TRAP_PID" ]] && return
code=$(signalCode "$sig") code=$(signalCode "$sig")
if [ -f "$QEMU_END" ]; then if [ -f "$QEMU_END" ]; then
echo && info "Received $1 signal while already shutting down..."
if (( code == 130 && SHUTDOWN_SIGNAL == code )); then
SHUTDOWN_SKIP=1
echo && info "Received SIGINT again, forcing shutdown..."
return
fi
echo && info "Received $sig signal while already shutting down..."
return return
fi fi
set +e set +e
start=$SECONDS start=$SECONDS
SHUTDOWN_SIGNAL=$code
touch "$QEMU_END" touch "$QEMU_END"
echo && info "Received $1 signal, sending shutdown command..." echo && info "Received $sig signal, sending shutdown command..."
if ! readQemuPid pid; then if ! readQemuPid pid; then
warn "QEMU PID file ($QEMU_PID) does not exist?" if ! interactive || ! waitQemuPid pid; then
finish "$code" warn "QEMU PID file does not exist?"
finish "$code"
fi
fi fi
if ! isAlive "$pid"; then if [ -z "$pid" ] || ! isAlive "$pid"; then
warn "QEMU process with PID $pid does not exist?" warn "QEMU process with PID $pid does not exist?"
finish "$code" finish "$code"
fi fi
@@ -252,6 +385,10 @@ graceful_shutdown() {
! enabled "$SHUTDOWN" && return 0 ! enabled "$SHUTDOWN" && return 0
[ -n "${QEMU_TIMEOUT:-}" ] && TIMEOUT="$QEMU_TIMEOUT" [ -n "${QEMU_TIMEOUT:-}" ] && TIMEOUT="$QEMU_TIMEOUT"
if interactive; then
_trap graceful_shutdown SIGINT
fi
_trap graceful_shutdown SIGTERM SIGHUP SIGABRT SIGQUIT _trap graceful_shutdown SIGTERM SIGHUP SIGABRT SIGQUIT
return 0 return 0
+3 -2
View File
@@ -19,8 +19,9 @@ driver="/run/shm/qemu.nic"
page="/run/shm/index.html" page="/run/shm/index.html"
address="/run/shm/qemu.ip" address="/run/shm/qemu.ip"
shutdown="/run/shm/qemu.end" shutdown="/run/shm/qemu.end"
socket="/run/shm/qemu-host-api.sock"
template="/var/www/index.html" template="/var/www/index.html"
url="http://127.0.0.1:2210/read?command=10" url="http://localhost/read?command=10"
resp_err="Guest returned an invalid response:" resp_err="Guest returned an invalid response:"
curl_err="Failed to connect to guest: curl error" curl_err="Failed to connect to guest: curl error"
@@ -37,7 +38,7 @@ queryGuest() {
local rc local rc
{ json=$(curl -m 20 -sk "$url"); rc=$?; } || : { json=$(curl --unix-socket "$socket" -m 20 -sk "$url"); rc=$?; } || :
exitIfShuttingDown exitIfShuttingDown
+3 -5
View File
@@ -45,7 +45,7 @@ checkClocksource() {
checkSse42() { checkSse42() {
if ! grep -qw "sse4_2" <<< "$flags"; then if ! hasFlag "sse4_2"; then
error "Your CPU does not have the SSE4 instruction set that Virtual DSM requires!" error "Your CPU does not have the SSE4 instruction set that Virtual DSM requires!"
! enabled "$DEBUG" && exit 88 ! enabled "$DEBUG" && exit 88
fi fi
@@ -94,10 +94,10 @@ configureKvmCpuModel() {
appendKvmInvtscFeature() { appendKvmInvtscFeature() {
if grep -qw "svm" <<< "$flags"; then if hasFlag "svm"; then
# AMD processor # AMD processor
if grep -qw "tsc_scale" <<< "$flags"; then if hasFlag "tsc_scale"; then
CPU_FEATURES+=",+invtsc" CPU_FEATURES+=",+invtsc"
fi fi
@@ -186,8 +186,6 @@ configureHostCpuName() {
selectClocksource selectClocksource
checkClocksource checkClocksource
flags=$(sed -ne '/^flags/s/^.*: //p' /proc/cpuinfo)
if ! disabled "$KVM"; then if ! disabled "$KVM"; then
configureKvm configureKvm
else else
+261 -20
View File
@@ -2,44 +2,285 @@
set -Eeuo pipefail set -Eeuo pipefail
info="/run/shm/msg.html" info="/run/shm/msg.html"
info_tmp="${info}.${BASHPID}.tmp"
escape () { escape() {
local s
s=${1//&/\&amp;} local s
s=${s//</\&lt;}
s=${s//>/\&gt;} s=${1//&/\&amp;}
s=${s//'"'/\&quot;} s=${s//</\&lt;}
printf -- %s "$s" s=${s//>/\&gt;}
s=${s//'"'/\&quot;}
s=${s//"'"/\&#39;}
printf '%s' "$s"
return 0
}
writeInfo() {
local content="$1"
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 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
}
finishProgress() {
rm -f -- "$info_tmp"
if [[ "$output" == "log" && "$printed" == "Y" ]]; then
printf '\n'
fi
return 0
} }
path="$1" path="$1"
total="$2" total="$2"
body=$(escape "$3") 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"
if [ -z "$total" ] || [[ "$total" == "0" ]]; then
log_mode="size"
fi
trap finishProgress EXIT
trap 'exit 0' HUP INT QUIT TERM
if [[ "$body" == *"..." ]]; then if [[ "$body" == *"..." ]]; then
body="<p class=\"loading\">${body::-3}</p>" body="<p class=\"loading\">${body::-3}</p>"
fi fi
while true while true; do
do
if [ ! -s "$path" ] && [ ! -d "$path" ]; then bytes=$(getBytes "$path" "$mode")
bytes="0" effective_total="$total"
else
bytes=$(du -sb "$path" 2>/dev/null | cut -f1) || bytes="0" 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 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 if (( bytes > 4096 )); then
if [ -z "$total" ] || [[ "$total" == "0" ]] || [ "$bytes" -gt "$total" ]; then
size=$(numfmt --to=iec --suffix=B "$bytes" | sed -r 's/([A-Z])/ \1/') || size="${bytes} bytes" 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 else
size="$(echo "$bytes" "$total" | awk '{printf "%.1f", $1 * 100 / $2}')" # Truncate to one decimal so progress is never reported early.
size="$size%" 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
[[ "$size" != "0.0%" ]] && echo "${body//(\[P\])/($size)}"> "$info"
fi fi
sleep 1 & wait $! sleep 1 & wait $!
done done
+82 -9
View File
@@ -13,6 +13,7 @@ enabled "${TRACE:-}" && set -o functrace && trap 'echo "# $BASH_COMMAND" >&2' DE
: "${KVM:="Y"}" # KVM acceleration : "${KVM:="Y"}" # KVM acceleration
: "${DEBUG:="N"}" # Disable debugging mode : "${DEBUG:="N"}" # Disable debugging mode
: "${COUNTRY:=""}" # Country code for mirror : "${COUNTRY:=""}" # Country code for mirror
: "${MACHINE:="q35"}" # Machine type selection
: "${ALLOCATE:=""}" # Preallocate diskspace : "${ALLOCATE:=""}" # Preallocate diskspace
: "${ARGUMENTS:=""}" # Extra QEMU parameters : "${ARGUMENTS:=""}" # Extra QEMU parameters
: "${CPU_CORES:="2"}" # Amount of CPU cores : "${CPU_CORES:="2"}" # Amount of CPU cores
@@ -25,12 +26,29 @@ detectEngine() {
if [ -f "/run/.containerenv" ]; then if [ -f "/run/.containerenv" ]; then
ENGINE="${container:-}" ENGINE="${container:-}"
if [[ "${ENGINE,,}" == *"podman"* ]]; then if [[ "${ENGINE,,}" == *"podman"* ]]; then
ROOTLESS="Y"
ENGINE="Podman" ENGINE="Podman"
else else
[ -z "$ENGINE" ] && ENGINE="Kubernetes" [ -z "$ENGINE" ] && ENGINE="Kubernetes"
fi fi
elif [ -f "/.dockerenv" ]; then
ENGINE="Docker"
fi
return 0
}
detectRootless() {
local uid_map=""
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 fi
return 0 return 0
@@ -53,7 +71,6 @@ checkPrivileged() {
max_cap=$(((1 << (last_cap + 1)) - 1)) max_cap=$(((1 << (last_cap + 1)) - 1))
if [ "$cap_bnd" -eq "$max_cap" ]; then if [ "$cap_bnd" -eq "$max_cap" ]; then
ROOTLESS="N"
PRIVILEGED="Y" PRIVILEGED="Y"
fi fi
@@ -123,11 +140,67 @@ checkFilesystem() {
return 0 return 0
} }
finiteMemoryLimit() {
local limit="$1"
local sentinel="4611686018427387904"
local i=0
local left=""
local right=""
[[ "$limit" =~ ^[0-9]+$ ]] || return 1
(( ${#limit} < ${#sentinel} )) && return 0
(( ${#limit} > ${#sentinel} )) && return 1
for (( i=0; i<${#sentinel}; i++ )); do
left="${limit:i:1}"
right="${sentinel:i:1}"
(( left < right )) && return 0
(( left > right )) && return 1
done
return 1
}
getMemoryInfo() {
local host_total=""
local host_avail=""
local limit=""
local current=""
local available=""
host_total=$(free -b | awk '/^Mem:/ {print $2; exit}')
host_avail=$(free -b | awk '/^Mem:/ {print $7; exit}')
RAM_TOTAL="$host_total"
RAM_AVAIL="$host_avail"
if [ -r /sys/fs/cgroup/memory.max ] && [ -r /sys/fs/cgroup/memory.current ]; then
limit=$(< /sys/fs/cgroup/memory.max)
current=$(< /sys/fs/cgroup/memory.current)
elif [ -r /sys/fs/cgroup/memory/memory.limit_in_bytes ] && [ -r /sys/fs/cgroup/memory/memory.usage_in_bytes ]; then
limit=$(< /sys/fs/cgroup/memory/memory.limit_in_bytes)
current=$(< /sys/fs/cgroup/memory/memory.usage_in_bytes)
fi
if finiteMemoryLimit "$limit" && [[ "$current" =~ ^[0-9]+$ ]]; then
(( limit < RAM_TOTAL )) && RAM_TOTAL="$limit"
available=$(( limit - current ))
(( available < 0 )) && available=0
(( available < RAM_AVAIL )) && RAM_AVAIL="$available"
fi
return 0
}
normalizeRamSize() { normalizeRamSize() {
# Read memory # Read host and container memory limits.
RAM_AVAIL=$(free -b | grep -m 1 Mem: | awk '{print $7}') getMemoryInfo
RAM_TOTAL=$(free -b | grep -m 1 Mem: | awk '{print $2}')
RAM_SPARE=500000000 RAM_SPARE=500000000
RAM_MINIMUM=136314880 RAM_MINIMUM=136314880
@@ -232,6 +305,7 @@ PROCESS="${APP,,}"
PROCESS="${PROCESS// /-}" PROCESS="${PROCESS// /-}"
detectEngine detectEngine
detectRootless
echo " Starting $APP for $ENGINE v$(</etc/version)..." echo " Starting $APP for $ENGINE v$(</etc/version)..."
echo " For support visit $SUPPORT" echo " For support visit $SUPPORT"
@@ -247,9 +321,8 @@ FOOTER2="<a href='$SUPPORT'>$SUPPORT</a>"
SOCKETS=1 SOCKETS=1
CPU=$(cpu) CPU=$(cpu)
SYS=$(uname -r) SYS=$(uname -r)
KERNEL=$(echo "$SYS" | cut -b 1)
MINOR=$(echo "$SYS" | cut -d '.' -f2)
ARCH=$(dpkg --print-architecture) ARCH=$(dpkg --print-architecture)
IFS=. read -r KERNEL MINOR _ <<< "$SYS"
CORES=$(grep -c '^processor' /proc/cpuinfo) CORES=$(grep -c '^processor' /proc/cpuinfo)
if grep -qi "socket(s)" <<< "$(lscpu)"; then if grep -qi "socket(s)" <<< "$(lscpu)"; then
@@ -273,14 +346,14 @@ SPACE_GB=$(formatBytes "$SPACE" "down")
AVAIL_MEM=$(formatBytes "$RAM_AVAIL" "down") AVAIL_MEM=$(formatBytes "$RAM_AVAIL" "down")
TOTAL_MEM=$(formatBytes "$RAM_TOTAL" "up") TOTAL_MEM=$(formatBytes "$RAM_TOTAL" "up")
echo " CPU: ${CPU} | RAM: ${AVAIL_MEM/ GB/}/$TOTAL_MEM | DISK: $SPACE_GB (${FS}) | KERNEL: ${SYS}..." echo " CPU: ${CPU} | RAM: ${AVAIL_MEM/ GB/}/$TOTAL_MEM | DISK: $SPACE_GB (${FS}) | KERNEL: ${SYS}"
echo echo
checkKvm checkKvm
# Cleanup files # Cleanup files
rm -f "$QEMU_DIR"/dsm.url rm -f "$QEMU_DIR"/dsm.url
rm -f "$QEMU_DIR"/qemu.* "$QEMU_DIR"/*.pid "$QEMU_DIR"/*.sock rm -f "$QEMU_DIR"/{qemu.*,*.{pid,sock,pipe}}
# Cleanup dirs # Cleanup dirs
rm -rf /tmp/dsm rm -rf /tmp/dsm
+50 -16
View File
@@ -15,6 +15,10 @@ HOST_MODEL=$(strip "$HOST_MODEL")
HOST_SERIAL=$(strip "$HOST_SERIAL") HOST_SERIAL=$(strip "$HOST_SERIAL")
GUEST_SERIAL=$(strip "$GUEST_SERIAL") GUEST_SERIAL=$(strip "$GUEST_SERIAL")
HOST_PID="$QEMU_DIR/host.pid"
HOST_API_SOCKET="$QEMU_DIR/qemu-host-api.sock"
HOST_AGENT_SOCKET="$QEMU_DIR/qemu-host-agent.sock"
validateHostMac() { validateHostMac() {
local m local m
@@ -41,6 +45,8 @@ buildHostArguments() {
HOST_ARGS=() HOST_ARGS=()
HOST_ARGS+=("-cpu=$CPU_CORES") HOST_ARGS+=("-cpu=$CPU_CORES")
HOST_ARGS+=("-cpu_arch=$HOST_CPU") HOST_ARGS+=("-cpu_arch=$HOST_CPU")
HOST_ARGS+=("-api=$HOST_API_SOCKET")
HOST_ARGS+=("-addr=$HOST_AGENT_SOCKET")
[ -n "$HOST_MAC" ] && HOST_ARGS+=("-mac=$HOST_MAC") [ -n "$HOST_MAC" ] && HOST_ARGS+=("-mac=$HOST_MAC")
[ -n "$HOST_MODEL" ] && HOST_ARGS+=("-model=$HOST_MODEL") [ -n "$HOST_MODEL" ] && HOST_ARGS+=("-model=$HOST_MODEL")
@@ -52,30 +58,47 @@ buildHostArguments() {
startHostBinary() { startHostBinary() {
local pid
rm -f -- "$HOST_PID" "$HOST_API_SOCKET" "$HOST_AGENT_SOCKET" || return 1
if enabled "$HOST_DEBUG"; then if enabled "$HOST_DEBUG"; then
set -x set -x
./host.bin "${HOST_ARGS[@]}" & ./host.bin "${HOST_ARGS[@]}" &
{ set +x; } 2>/dev/null { set +x; } 2>/dev/null
echo "$!" > "$HOST_PID" pid=$!
echo echo
else else
./host.bin "${HOST_ARGS[@]}" >/dev/null & ./host.bin "${HOST_ARGS[@]}" >/dev/null &
echo "$!" > "$HOST_PID" pid=$!
fi fi
echo "$pid" > "$HOST_PID"
return 0 return 0
} }
waitForPort() { waitForSocket() {
local port="$1" local socket="$1"
local exit_code="$2" local exit_code="$2"
local cnt=0 local pid cnt=0
while [ ! -S "$socket" ]; do
if ! read -r pid < "$HOST_PID" || ! isAlive "$pid"; then
error "qemu-host exited unexpectedly!"
exit "$exit_code"
fi
while ! nc -z -w2 127.0.0.1 "$port" > /dev/null 2>&1; do
sleep 0.1 sleep 0.1
cnt=$((cnt + 1)) cnt=$((cnt + 1))
(( cnt > 50 )) && error "Failed to connect to qemu-host.." && exit "$exit_code"
if (( cnt > 50 )); then
error "Failed to create qemu-host socket: $socket"
exit "$exit_code"
fi
done done
return 0 return 0
@@ -83,10 +106,25 @@ waitForPort() {
configureSerialPorts() { configureSerialPorts() {
# Configure serial ports if enabled "${SHUTDOWN:-Y}" && interactive; then
SERIAL_OPTS="-serial mon:stdio \
CONSOLE_SOCKET="$QEMU_DIR/console.sock"
MONITOR_SOCKET="$QEMU_DIR/monitor.sock"
SERIAL_OPTS="-chardev socket,id=console0,path=$CONSOLE_SOCKET,reconnect-ms=1000 \
-serial chardev:console0 \
-chardev socket,id=monitor0,path=$MONITOR_SOCKET,server=on,wait=off \
-mon chardev=monitor0,mode=readline"
else
SERIAL_OPTS="-serial mon:stdio"
fi
SERIAL_OPTS+=" \
-device virtio-serial-pci,id=virtio-serial0,bus=pcie.0,addr=0x3 \ -device virtio-serial-pci,id=virtio-serial0,bus=pcie.0,addr=0x3 \
-chardev socket,id=charchannel0,host=127.0.0.1,port=$CHR_PORT,reconnect=10 \ -chardev socket,id=charchannel0,path=$HOST_AGENT_SOCKET,reconnect-ms=1000 \
-device virtserialport,bus=virtio-serial0.0,nr=1,chardev=charchannel0,id=channel0,name=vchannel" -device virtserialport,bus=virtio-serial0.0,nr=1,chardev=charchannel0,id=channel0,name=vchannel"
return 0 return 0
@@ -94,15 +132,11 @@ configureSerialPorts() {
validateHostMac validateHostMac
HOST_PID="$QEMU_DIR/host.pid"
buildHostArguments buildHostArguments
startHostBinary startHostBinary
sleep 0.2 waitForSocket "$HOST_API_SOCKET" 58
waitForSocket "$HOST_AGENT_SOCKET" 59
waitForPort "$COM_PORT" 58
waitForPort "$CHR_PORT" 59
configureSerialPorts configureSerialPorts
+55 -21
View File
@@ -1,39 +1,49 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -Eeuo pipefail set -Eeuo pipefail
: "${COM_PORT:="2210"}" # Comm port
: "${WEB_PORT:="5000"}" # Webserver port : "${WEB_PORT:="5000"}" # Webserver port
: "${CHR_PORT:="12345"}" # Character port
: "${WSD_PORT:="8004"}" # Websockets port : "${WSD_PORT:="8004"}" # Websockets port
# Sanitize port variables # Sanitize port variables
COM_PORT=$(strip "$COM_PORT")
WEB_PORT=$(strip "$WEB_PORT") WEB_PORT=$(strip "$WEB_PORT")
CHR_PORT=$(strip "$CHR_PORT")
WSD_PORT=$(strip "$WSD_PORT") WSD_PORT=$(strip "$WSD_PORT")
WEB_PID="/run/nginx.pid" WEB_PID="/run/nginx.pid"
WSD_LOG="/var/log/websocketd.log"
WSD_PID="$QEMU_DIR/websocketd.pid" WSD_PID="$QEMU_DIR/websocketd.pid"
prepareWebFiles() { prepareWebFiles() {
cp -r /var/www/* "$QEMU_DIR"
rm -f "$WSD_PID" "$WEB_PID" cp -r /var/www/* "$QEMU_DIR" || return 1
rm -f "$WSD_PID" "$WEB_PID" "$WSD_LOG" || return 1
return 0 return 0
} }
configureWebPorts() { configureWebPorts() {
sed -i "s/listen 5000 default_server;/listen $WEB_PORT default_server;/g" /etc/nginx/sites-enabled/web.conf
sed -i "s/proxy_pass http:\/\/127.0.0.1:8004\/;/proxy_pass http:\/\/127.0.0.1:$WSD_PORT\/;/g" /etc/nginx/sites-enabled/web.conf if ! sed -i \
-e "s|listen 5000 default_server;|listen $WEB_PORT default_server;|g" \
-e "s|proxy_pass http://127.0.0.1:8004/;|proxy_pass http://127.0.0.1:$WSD_PORT/;|g" \
/etc/nginx/sites-enabled/web.conf; then
error "Failed to configure webserver ports!"
return 1
fi
return 0 return 0
} }
configureIpv6Listen() { configureIpv6Listen() {
# shellcheck disable=SC2143 if [ -f /proc/net/if_inet6 ] && [[ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null)" != "1" ]]; then
if [ -f /proc/net/if_inet6 ] && [[ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null)" != "1" ]] && [ -n "$(ifconfig -a | grep inet6)" ]; then
sed -i "s/listen $WEB_PORT default_server;/listen [::]:$WEB_PORT default_server ipv6only=off;/g" /etc/nginx/sites-enabled/web.conf 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 fi
return 0 return 0
@@ -44,8 +54,8 @@ configureWebServer() {
mkdir -p /etc/nginx/sites-enabled mkdir -p /etc/nginx/sites-enabled
cp /etc/nginx/default.conf /etc/nginx/sites-enabled/web.conf cp /etc/nginx/default.conf /etc/nginx/sites-enabled/web.conf
configureWebPorts configureWebPorts || return 1
configureIpv6Listen configureIpv6Listen || return 1
return 0 return 0
} }
@@ -53,24 +63,48 @@ configureWebServer() {
startWebServer() { startWebServer() {
# Start webserver # Start webserver
nginx -e stderr nginx -e stderr || return 1
return 0
} }
startWebsocketServer() { startWebsocketServer() {
# Start websocket server # Start websocket server
websocketd --address 127.0.0.1 --port="$WSD_PORT" /run/socket.sh >/var/log/websocketd.log & websocketd \
echo "$!" > "$WSD_PID" --address 127.0.0.1 \
--port="$WSD_PORT" \
/run/socket.sh \
>"$WSD_LOG" 2>&1 &
local pid=$!
if ! echo "$pid" > "$WSD_PID"; then
kill "$pid" 2>/dev/null || :
return 1
fi
sleep 0.1
if ! isAlive "$pid"; then
rm -f "$WSD_PID"
[ -s "$WSD_LOG" ] && cat "$WSD_LOG" >&2
error "Failed to start websocket server!"
return 1
fi
return 0
} }
prepareWebFiles prepareWebFiles
html "Starting $APP for $ENGINE..." html "Starting $APP for $ENGINE..."
if ! disabled "${WEB:-}"; then disabled "${WEB:-}" && return 0
configureWebServer
startWebServer configureWebServer
startWebsocketServer
fi startWebServer
startWebsocketServer
return 0 return 0
+18 -5
View File
@@ -3,6 +3,8 @@ set -Eeuo pipefail
lastmsg="" lastmsg=""
path="/run/shm/msg.html" path="/run/shm/msg.html"
dir=$(dirname -- "$path")
name=$(basename -- "$path")
refresh() { refresh() {
@@ -17,15 +19,26 @@ refresh() {
lastmsg="$msg" lastmsg="$msg"
echo "s: $msg" echo "s: $msg"
return 0 return 0
} }
refresh refresh
inotifywait -m "$path" | inotifywait \
while read -r fp event fn; do -m -q \
-e close_write,moved_to,delete \
--format '%e %f' \
"$dir" |
while read -r event file; do
[[ "$file" == "$name" ]] || continue
case "${event,,}" in case "${event,,}" in
"modify"* ) refresh ;; "delete"* )
"delete_self" ) echo "c: vnc" ;; echo "c: vnc" ;;
esac "close_write"* | "moved_to"* )
refresh ;;
esac
done done
+198 -28
View File
@@ -7,7 +7,36 @@ info () { printf "%b%s%b" "\E[1;34m \E[1;36m" "${1:-}" "\E[0m\n"; }
error () { printf "%b%s%b" "\E[1;31m " "ERROR: ${1:-}" "\E[0m\n" >&2; } error () { printf "%b%s%b" "\E[1;31m " "ERROR: ${1:-}" "\E[0m\n" >&2; }
warn () { printf "%b%s%b" "\E[1;31m " "Warning: ${1:-}" "\E[0m\n" >&2; } warn () { printf "%b%s%b" "\E[1;31m " "Warning: ${1:-}" "\E[0m\n" >&2; }
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" ]]
}
interactive() {
[ -t 0 ] && : 2>/dev/null </dev/tty >/dev/tty
}
strip() { strip() {
local value="${1:-}" local value="${1:-}"
# Remove surrounding whitespace # Remove surrounding whitespace
@@ -28,24 +57,38 @@ strip() {
} }
enabled() { enabled() {
case "$(strip "${1:-}")" in
Y|y|YES|Yes|yes|TRUE|True|true|1|ON|On|on) return 0 ;; local value
value=$(strip "${1:-}")
case "${value,,}" in
y|yes|true|1|on|enable|enabled) return 0 ;;
*) return 1 ;; *) return 1 ;;
esac esac
} }
disabled() { disabled() {
case "$(strip "${1:-}")" in
N|n|NO|No|no|FALSE|False|false|0|OFF|Off|off) return 0 ;; local value
value=$(strip "${1:-}")
case "${value,,}" in
n|no|none|false|0|off|disable|disabled) return 0 ;;
*) return 1 ;; *) return 1 ;;
esac esac
} }
formatBytes() { formatBytes() {
local result local result
result=$(numfmt --to=iec --suffix=B "$1" | sed -r 's/([A-Z])/ \1/' | sed 's/ B/ bytes/g;')
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. ]}" local unit="${result//[0-9. ]}"
result="${result//[a-zA-Z ]/}" result="${result//[a-zA-Z ]/}"
if [[ "${2:-}" == "up" ]]; then if [[ "${2:-}" == "up" ]]; then
if [[ "$result" == *"."* ]]; then if [[ "$result" == *"."* ]]; then
result="${result%%.*}" result="${result%%.*}"
@@ -56,11 +99,13 @@ formatBytes() {
result="${result%%.*}" result="${result%%.*}"
fi fi
fi fi
echo "$result $unit" echo "$result $unit"
return 0 return 0
} }
isAlive() { isAlive() {
local pid="$1" local pid="$1"
[ -z "$pid" ] && return 1 [ -z "$pid" ] && return 1
@@ -72,6 +117,7 @@ isAlive() {
} }
waitPid() { waitPid() {
local i=0 local i=0
local pid="$1" local pid="$1"
local timeout="${2:-10}" local timeout="${2:-10}"
@@ -86,6 +132,7 @@ waitPid() {
} }
waitPidFile() { waitPidFile() {
local i=0 local i=0
local pid="" local pid=""
local file="$1" local file="$1"
@@ -106,6 +153,7 @@ waitPidFile() {
} }
pKill() { pKill() {
local pid="$1" local pid="$1"
local timeout="${2:-10}" local timeout="${2:-10}"
@@ -119,6 +167,7 @@ pKill() {
} }
fWait() { fWait() {
local i=0 local i=0
local name="$1" local name="$1"
local timeout="${2:-10}" local timeout="${2:-10}"
@@ -138,6 +187,7 @@ fWait() {
} }
fKill() { fKill() {
local name="$1" local name="$1"
local timeout="${2:-10}" local timeout="${2:-10}"
@@ -150,6 +200,7 @@ fKill() {
} }
sKill() { sKill() {
local pid="" local pid=""
local file="$1" local file="$1"
@@ -165,6 +216,7 @@ sKill() {
} }
mKill() { mKill() {
local timeout=10 local timeout=10
local files=("$@") local files=("$@")
@@ -182,6 +234,7 @@ mKill() {
} }
setOwner() { setOwner() {
local file="$1" local file="$1"
local dir uid gid local dir uid gid
@@ -197,6 +250,7 @@ setOwner() {
} }
makeDir() { makeDir() {
local path="$1" local path="$1"
local dir uid gid local dir uid gid
@@ -204,25 +258,154 @@ makeDir() {
! mkdir -p "$path" && return 1 ! mkdir -p "$path" && return 1
dir=$(dirname -- "$path") dir=$(dirname -- "$path")
uid=$(stat -c '%u' "$dir") || return 1
gid=$(stat -c '%g' "$dir") || return 1
! chown "$uid:$gid" "$path" && return 1 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 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"
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
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 () { escape () {
local s local s
s=${1//&/\&amp;} s=${1//&/\&amp;}
s=${s//</\&lt;} s=${s//</\&lt;}
s=${s//>/\&gt;} s=${s//>/\&gt;}
s=${s//'"'/\&quot;} s=${s//'"'/\&quot;}
printf -- %s "$s" printf -- %s "$s"
return 0
}
escapeXML() {
printf '%s' "$1" | sed \
-e 's/&/\&amp;/g' \
-e 's/</\&lt;/g' \
-e 's/>/\&gt;/g' \
-e 's/"/\&quot;/g' \
-e "s/'/\&apos;/g"
return 0 return 0
} }
html() { html() {
local title local title
local body local body
local script local script
@@ -247,13 +430,14 @@ html() {
HTML="${HTML/\[4\]/$footer}" HTML="${HTML/\[4\]/$footer}"
HTML="${HTML/\[5\]/$FOOTER2}" HTML="${HTML/\[5\]/$FOOTER2}"
echo "$HTML" > "$PAGE" || return 1 writeAtomic "$PAGE" "$HTML" || return 1
echo "$body" > "$INFO" || return 1 writeAtomic "$INFO" "$body" || return 1
return 0 return 0
} }
cpu() { cpu() {
local ret local ret
local cpu="" local cpu=""
@@ -290,23 +474,8 @@ cpu() {
return 0 return 0
} }
hasDisk() {
enabled "${DISK_DISABLE:-}" && return 1
[ -b "/disk" ] && return 0
[ -b "/disk1" ] && return 0
[ -b "/dev/disk1" ] && return 0
[ -b "${DEVICE:-}" ] && return 0
[ -z "${DISK_NAME:-}" ] && DISK_NAME="data"
[ -s "$STORAGE/$DISK_NAME.img" ] && return 0
[ -s "$STORAGE/$DISK_NAME.qcow2" ] && return 0
return 1
}
getCountry() { getCountry() {
local url=$1 local url=$1
local query=$2 local query=$2
local rc json result local rc json result
@@ -345,6 +514,7 @@ setCountry() {
} }
addPackage() { addPackage() {
local pkg=$1 local pkg=$1
local desc=$2 local desc=$2
@@ -361,8 +531,8 @@ addPackage() {
sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources
fi fi
DEBIAN_FRONTEND=noninteractive apt-get -qq update DEBIAN_FRONTEND=noninteractive apt-get -qq update || return 1
DEBIAN_FRONTEND=noninteractive apt-get -qq --no-install-recommends -y install "$pkg" > /dev/null DEBIAN_FRONTEND=noninteractive apt-get -qq --no-install-recommends -y install "$pkg" > /dev/null || return 1
return 0 return 0
} }
+41 -8
View File
@@ -1,15 +1,27 @@
var timer;
var request; var request;
var booting = false; var booting = false;
var interval = 1000; var interval = 1000;
function abortRequest() {
if (!request) {
return false;
}
request.onreadystatechange = null;
request.abort();
request = null;
return true;
}
function getInfo() { function getInfo() {
var url = "msg.html"; var url = "msg.html";
try { try {
if (request) { abortRequest();
request.abort();
}
if (window.XMLHttpRequest) { if (window.XMLHttpRequest) {
request = new XMLHttpRequest(); request = new XMLHttpRequest();
@@ -56,7 +68,10 @@ function processInfo() {
return true; return true;
} }
var msg = request.responseText; var response = request;
request = null;
var msg = response.responseText;
if (msg == null || msg.length == 0) { if (msg == null || msg.length == 0) {
if (booting) { if (booting) {
@@ -68,9 +83,9 @@ function processInfo() {
return false; return false;
} }
var notFound = (request.status == 404); var notFound = (response.status == 404);
if (request.status == 200) { if (response.status == 200) {
if (msg.toLowerCase().indexOf("<html>") !== -1) { if (msg.toLowerCase().indexOf("<html>") !== -1) {
notFound = true; notFound = true;
} else { } else {
@@ -88,7 +103,7 @@ function processInfo() {
return true; return true;
} }
setError("Error: Received statuscode " + request.status); setError("Error: Received statuscode " + response.status);
return false; return false;
} catch (e) { } catch (e) {
@@ -157,7 +172,9 @@ function setError(text) {
} }
function schedule() { function schedule() {
setTimeout(getInfo, interval);
clearTimeout(timer);
timer = setTimeout(getInfo, interval);
} }
function reload() { function reload() {
@@ -179,11 +196,27 @@ function connect() {
switch (cmd) { switch (cmd) {
case "s": case "s":
var aborted = abortRequest();
processMsg(msg); processMsg(msg);
if (aborted &&
msg.toLowerCase().indexOf("href=") == -1) {
schedule();
}
break; break;
case "e": case "e":
if (abortRequest()) {
schedule();
}
setError(msg); setError(msg);
break; break;
default: default:
console.warn("Unknown event: " + cmd); console.warn("Unknown event: " + cmd);
break; break;