Compare commits

..

No commits in common. "master" and "v2.21" have entirely different histories.

19 changed files with 1855 additions and 349 deletions

View file

@ -1,6 +0,0 @@
{
"name": "qemu",
"service": "qemu",
"forwardPorts": [8006],
"dockerComposeFile": "compose.yml"
}

View file

@ -21,7 +21,6 @@ body:
attributes:
label: Docker compose
description: The compose file (or otherwise the `docker run` command used).
render: yaml
validations:
required: true
- type: textarea
@ -29,7 +28,6 @@ body:
attributes:
label: Docker log
description: The logfile of the container (as shown by `docker logs qemu`).
render: shell
validations:
required: true
- type: textarea

View file

@ -23,7 +23,6 @@ body:
attributes:
label: Docker compose
description: The compose file (or otherwise the `docker run` command used).
render: yaml
validations:
required: true
- type: textarea
@ -31,7 +30,6 @@ body:
attributes:
label: Docker log
description: The logfile of the container (as shown by `docker logs qemu`).
render: shell
validations:
required: true
- type: textarea

View file

@ -46,16 +46,16 @@ jobs:
with:
context: git
images: |
ghcr.io/${{ github.repository }}
${{ secrets.DOCKERHUB_REPO }}
ghcr.io/${{ github.repository }}
tags: |
type=raw,value=latest,priority=100
type=raw,value=${{ vars.MAJOR }}.${{ vars.MINOR }}
labels: |
org.opencontainers.image.title=${{ vars.NAME }}
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
-
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
@ -112,6 +112,6 @@ jobs:
connection_url: ${{secrets.MAIL_CONNECTION}}
subject: Build of ${{ github.event.repository.name }} v${{ steps.meta.outputs.version }} completed
body: |
The build job of ${{ github.event.repository.name }} v${{ steps.meta.outputs.version }} was completed successfully!
The build job of ${{ github.event.repository.name }} v${{ steps.meta.outputs.version }} was completed successfully!
See https://github.com/${{ github.repository }}/actions for more information.

View file

@ -14,7 +14,7 @@ jobs:
name: Run ShellCheck
uses: ludeeus/action-shellcheck@master
env:
SHELLCHECK_OPTS: -x --source-path=src -e SC1091 -e SC2001 -e SC2034 -e SC2064 -e SC2317 -e SC2153
SHELLCHECK_OPTS: -x --source-path=src -e SC2001 -e SC2034 -e SC2064 -e SC2317 -e SC2153
-
name: Lint Dockerfile
uses: hadolint/hadolint-action@v3.1.0

View file

@ -1,10 +1,7 @@
ARG VERSION_ARG="latest"
FROM qemux/qemu:${VERSION_ARG} AS src
FROM debian:trixie-slim
ARG VERSION_ARG="0.0"
ARG VERSION_VNC="1.6.0"
ARG VERSION_VNC="1.5.0"
ARG DEBCONF_NOWARNINGS="yes"
ARG DEBIAN_FRONTEND="noninteractive"
@ -13,13 +10,9 @@ ARG DEBCONF_NONINTERACTIVE_SEEN="true"
RUN set -eu && \
apt-get update && \
apt-get --no-install-recommends -y install \
bc \
jq \
tini \
wget \
7zip \
curl \
fdisk \
nginx \
procps \
seabios \
@ -29,40 +22,38 @@ RUN set -eu && \
dnsmasq \
xz-utils \
net-tools \
e2fsprogs \
qemu-utils \
iputils-ping \
genisoimage \
ca-certificates \
qemu-system-arm && \
netcat-openbsd \
qemu-system-arm \
qemu-efi-aarch64 && \
apt-get clean && \
mkdir -p /etc/qemu && \
echo "allow br0" > /etc/qemu/bridge.conf && \
wget "https://snapshot.debian.org/archive/debian/20250128T092032Z/pool/main/e/edk2/qemu-efi-aarch64_2024.11-5_all.deb" -O /tmp/aavmf.deb -q --timeout=10 && \
dpkg -i /tmp/aavmf.deb && \
mkdir -p /usr/share/novnc && \
wget "https://github.com/novnc/noVNC/archive/refs/tags/v${VERSION_VNC}.tar.gz" -O /tmp/novnc.tar.gz -q --timeout=10 && \
tar -xf /tmp/novnc.tar.gz -C /tmp/ && \
cd "/tmp/noVNC-${VERSION_VNC}" && \
mv app core vendor package.json *.html /usr/share/novnc && \
sed -i "s|UI\.initSetting('path', 'websockify')|UI.initSetting('path', window.location.pathname.replace(/[^/]*$/, '').substring(1) + 'websockify')|" /usr/share/novnc/app/ui.js && \
unlink /etc/nginx/sites-enabled/default && \
sed -i 's/^worker_processes.*/worker_processes 1;/' /etc/nginx/nginx.conf && \
echo "$VERSION_ARG" > /run/version && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
COPY --from=src /run/*.sh /run
COPY --from=src /var/www /var/www
COPY --from=src /usr/share/novnc /usr/share/novnc
COPY --from=src /etc/nginx/sites-enabled /etc/nginx/sites-enabled
COPY --chmod=755 ./src /run/
ADD --chmod=664 https://raw.githubusercontent.com/qemus/qemu-docker/master/web/index.html /var/www/index.html
ADD --chmod=664 https://raw.githubusercontent.com/qemus/qemu-docker/master/web/js/script.js /var/www/js/script.js
ADD --chmod=664 https://raw.githubusercontent.com/qemus/qemu-docker/master/web/css/style.css /var/www/css/style.css
ADD --chmod=664 https://raw.githubusercontent.com/qemus/qemu-docker/master/web/img/favicon.svg /var/www/img/favicon.svg
ADD --chmod=744 https://raw.githubusercontent.com/qemus/qemu-docker/master/web/nginx.conf /etc/nginx/sites-enabled/web.conf
VOLUME /storage
EXPOSE 22 5900 8006
ENV BOOT="alpine"
ENV CPU_CORES="2"
ENV RAM_SIZE="2G"
ENV CPU_CORES="1"
ENV RAM_SIZE="1G"
ENV DISK_SIZE="16G"
ENV BOOT="http://example.com/image.iso"
ENTRYPOINT ["/usr/bin/tini", "-s", "/run/entry.sh"]

View file

@ -3,15 +3,11 @@ services:
container_name: qemu
image: qemux/qemu-arm
environment:
BOOT: "alpine"
BOOT: "https://dl-cdn.alpinelinux.org/alpine/v3.19/releases/aarch64/alpine-virt-3.19.1-aarch64.iso"
devices:
- /dev/kvm
- /dev/net/tun
cap_add:
- NET_ADMIN
ports:
- 8006:8006
volumes:
- ./qemu:/storage
restart: always
stop_grace_period: 2m

View file

@ -1,86 +1,62 @@
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: qemu-pvc
spec:
accessModes:
- ReadWriteOnce
- ReadWriteOnce
resources:
requests:
storage: 16Gi
---
apiVersion: apps/v1
kind: Deployment
apiVersion: v1
kind: Pod
metadata:
name: qemu
labels:
name: qemu
spec:
replicas: 1
selector:
matchLabels:
app: qemu
template:
metadata:
labels:
app: qemu
spec:
containers:
- name: qemu
image: qemux/qemu-arm
env:
terminationGracePeriodSeconds: 120 # the Kubernetes default is 30 seconds and it may be not enough
containers:
- name: qemu
image: qemux/qemu-arm
ports:
- containerPort: 8006
protocol: TCP
securityContext:
privileged: true
env:
- name: BOOT
value: "alpine"
value: "https://dl-cdn.alpinelinux.org/alpine/v3.19/releases/aarch64/alpine-virt-3.19.1-aarch64.iso"
- name: RAM_SIZE
value: 1G
- name: CPU_CORES
value: "1"
- name: DISK_SIZE
value: "16G"
ports:
- containerPort: 8006
name: http
protocol: TCP
- containerPort: 5900
name: vnc
protocol: TCP
securityContext:
capabilities:
add:
- NET_ADMIN
privileged: true
volumeMounts:
volumeMounts:
- mountPath: /storage
name: storage
- mountPath: /dev/kvm
name: dev-kvm
- mountPath: /dev/net/tun
name: dev-tun
terminationGracePeriodSeconds: 120
volumes:
- name: storage
persistentVolumeClaim:
claimName: qemu-pvc
- hostPath:
path: /dev/kvm
name: dev-kvm
- hostPath:
path: /dev/net/tun
type: CharDevice
name: dev-tun
volumes:
- name: storage
persistentVolumeClaim:
claimName: qemu-pvc
- name: dev-kvm
hostPath:
path: /dev/kvm
---
apiVersion: v1
kind: Service
metadata:
name: qemu
spec:
internalTrafficPolicy: Cluster
ports:
- name: http
port: 8006
protocol: TCP
targetPort: 8006
- name: vnc
port: 5900
protocol: TCP
targetPort: 5900
type: NodePort
selector:
app: qemu
type: ClusterIP
name: qemu
ports:
- name: tcp-8006
protocol: TCP
port: 8006
targetPort: 8006

225
readme.md
View file

@ -24,7 +24,7 @@ Docker container for running ARM-based virtual machines using QEMU, for devices
## Usage 🐳
##### Via Docker Compose:
Via Docker Compose:
```yaml
services:
@ -32,35 +32,35 @@ services:
container_name: qemu
image: qemux/qemu-arm
environment:
BOOT: "alpine"
BOOT: "https://dl-cdn.alpinelinux.org/alpine/v3.19/releases/aarch64/alpine-virt-3.19.1-aarch64.iso"
devices:
- /dev/kvm
- /dev/net/tun
cap_add:
- NET_ADMIN
ports:
- 8006:8006
volumes:
- ./qemu:/storage
restart: always
stop_grace_period: 2m
```
##### Via Docker CLI:
Via Docker CLI:
```bash
docker run -it --rm --name qemu -e "BOOT=alpine" -p 8006:8006 --device=/dev/kvm --device=/dev/net/tun --cap-add NET_ADMIN -v "${PWD:-.}/qemu:/storage" --stop-timeout 120 qemux/qemu-arm
docker run -it --rm -e "BOOT=http://example.com/image.iso" -p 8006:8006 --device=/dev/kvm --cap-add NET_ADMIN qemux/qemu-arm
```
##### Via Kubernetes:
Via Kubernetes:
```shell
kubectl apply -f https://raw.githubusercontent.com/qemus/qemu-arm/refs/heads/master/kubernetes.yml
```
##### Via Github Codespaces:
## Compatibility ⚙️
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/qemus/qemu)
| **Product** | **Platform** | |
|---|---|---|
| Docker Engine | Linux| ✅ |
| Docker Desktop | Linux | ❌ |
| Docker Desktop | macOS | ❌ |
## FAQ 💬
@ -68,85 +68,25 @@ kubectl apply -f https://raw.githubusercontent.com/qemus/qemu-arm/refs/heads/mas
Very simple! These are the steps:
- Set the `BOOT` variable to the [operating system](#how-do-i-select-the-operating-system) you want to install.
- Set the `BOOT` environment variable to the URL of any [disk image](#what-image-formats-are-supported) you want to install.
- Start the container and connect to [port 8006](http://127.0.0.1:8006/) using your web browser.
- Start the container and connect to [port 8006](http://localhost:8006) using your web browser.
- You will see the screen and can now install the OS of your choice using your keyboard and mouse.
Enjoy your brand new machine, and don't forget to star this repo!
### How do I select the operating system?
You can use the `BOOT` environment variable in order to specify the operating system that will be downloaded:
```yaml
environment:
BOOT: "alpine"
```
Select from the values below:
| **Value** | **Operating System** | **Size** |
|---|---|---|
| `alma` | Alma Linux | 1.7 GB |
| `alpine` | Alpine Linux | 60 MB |
| `cachy` | CachyOS | 2.6 GB |
| `centos` | CentOS | 6.4 GB |
| `debian` | Debian | 3.7 GB |
| `fedora` | Fedora | 2.9 GB |
| `gentoo` | Gentoo | 1.3 GB |
| `kali` | Kali Linux | 3.4 GB |
| `nixos` | NixOS | 2.4 GB |
| `suse` | OpenSUSE | 1.0 GB |
| `oracle` | Oracle Linux | 1.0 GB |
| `rocky` | Rocky Linux | 1.9 GB |
| `ubuntu` | Ubuntu Desktop | 3.3 GB |
| `ubuntus` | Ubuntu Server | 2.7 GB |
### How can I use my own image?
If you want to download an operating system that is not in the list above, you can set the `BOOT` variable to the URL of the image:
```yaml
environment:
BOOT: "https://dl-cdn.alpinelinux.org/alpine/v3.19/releases/aarch64/alpine-virt-3.19.1-aarch64.iso"
```
The `BOOT` URL accepts files in any of the following formats:
| **Extension** | **Format** |
|---|---|
| `.img` | Raw |
| `.raw` | Raw |
| `.iso` | Optical |
| `.qcow2` | QEMU |
| `.vmdk` | VMware |
| `.vhd` | VirtualPC |
| `.vhdx` | Hyper-V |
| `.vdi` | VirtualBox |
It will also accept files such as `.img.gz`, `.qcow2.xz`, `.iso.zip` and many more, because it will automaticly extract compressed files.
Alternatively you can use a local image file directly, by binding it in your compose file:
```yaml
volumes:
- ./example.iso:/boot.iso
```
This way you can supply either a `/boot.iso`, `/boot.img` or a `/boot.qcow2` file. The value of `BOOT` will be ignored in this case.
### How do I change the storage location?
To change the storage location, include the following bind mount in your compose file:
```yaml
volumes:
- ./qemu:/storage
- /var/qemu:/storage
```
Replace the example path `./qemu` with the desired storage folder or named volume.
Replace the example path `/var/qemu` with the desired storage folder.
### How do I change the size of the disk?
To expand the default size of 16 GB, add the `DISK_SIZE` setting to your compose file and set it to your preferred capacity:
@ -159,18 +99,6 @@ kubectl apply -f https://raw.githubusercontent.com/qemus/qemu-arm/refs/heads/mas
> [!TIP]
> This can also be used to resize the existing disk to a larger capacity without any data loss.
### How do I change the amount of CPU or RAM?
By default, the container will be allowed to use a maximum of 2 CPU cores and 2 GB of RAM.
If you want to adjust this, you can specify the desired amount using the following environment variables:
```yaml
environment:
RAM_SIZE: "8G"
CPU_CORES: "4"
```
### How do I increase the display resolution?
For maximum compatibility, the display output will be a simple framebuffer by default. While this isn't the most optimal, it doesn't require any drivers.
@ -187,26 +115,59 @@ kubectl apply -f https://raw.githubusercontent.com/qemus/qemu-arm/refs/heads/mas
> [!NOTE]
> Using this method your screen will stay black during the boot process, until the point where the driver is actually loaded.
### How do I boot a local image?
You can use a local image file directly, and skip the download altogether, by binding it in your compose file:
```yaml
volumes:
- /home/user/example.iso:/boot.iso
```
This way you can supply a `boot.iso`, `boot.img` or `boot.qcow2` file.
> [!NOTE]
> The URL of the `BOOT` variable will be ignored in this case.
### How do I boot Windows?
Use [dockur/windows-arm](https://github.com/dockur/windows-arm) instead, as it includes all the drivers required during installation, amongst many other features.
### How do I boot x86/x64 images?
### How do I boot a x86 image?
You can use the [qemu](https://github.com/qemus/qemu/) container to run x86 and x64 images on ARM.
You can use [qemu-docker](https://github.com/qemus/qemu-docker/) to run x86 and x64 images on ARM.
### How do I boot without SCSI drivers?
By default, the machine makes use of `virtio-scsi` drives for performance reasons, and even though most Linux kernels bundle the necessary driver for this device, that may not always be the case for other operating systems.
If your machine fails to detect the hard drive, you can modify your compose file to use `virtio-blk` instead:
```yaml
environment:
DISK_TYPE: "blk"
```
> [!TIP]
> If it still fails to boot, you can set the value to `usb` to emulate a USB drive, which is slower but requires no drivers and is compatible with almost every system.
### How do I change the amount of CPU or RAM?
By default, the container will be allowed to use a maximum of 1 CPU core and 1 GB of RAM.
If you want to adjust this, you can specify the desired amount using the following environment variables:
```yaml
environment:
RAM_SIZE: "4G"
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:
Only Linux and Windows 11 support KVM virtualization, macOS and Windows 10 do not unfortunately.
You can run the following commands in Linux to check your system:
```bash
sudo apt install cpu-checker
@ -221,18 +182,11 @@ kubectl apply -f https://raw.githubusercontent.com/qemus/qemu-arm/refs/heads/mas
- you are not using a cloud provider, as most of them do not allow nested virtualization for their VPS's.
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.
If you do not receive any error from `kvm-ok` but the container still complains about KVM, please check whether:
### How do I expose network ports?
- you are not using "Docker Desktop for Linux" as it does not support KVM, instead make use of Docker Engine directly.
You can expose ports just by adding them to your compose file. If you want to be able to connect to the SSH service of the machine for example, you would add it like this:
```yaml
ports:
- 2222:22
```
This will make port 2222 on your host redirect to port 22 of the virtual machine.
- it could help to add `privileged: true` to your compose file (or `sudo` to your `docker run` command), to rule out any permission issue.
### How do I assign an individual IP address to the container?
@ -275,7 +229,7 @@ kubectl apply -f https://raw.githubusercontent.com/qemus/qemu-arm/refs/heads/mas
After configuring the container for [macvlan](#how-do-i-assign-an-individual-ip-address-to-the-container), it is possible for the VM to become part of your home network by requesting an IP from your router, just like a real PC.
To enable this mode, in which the container and the VM will have separate IP addresses, add the following lines to your compose file:
To enable this mode, add the following lines to your compose file:
```yaml
environment:
@ -286,6 +240,9 @@ kubectl apply -f https://raw.githubusercontent.com/qemus/qemu-arm/refs/heads/mas
- 'c *:* rwm'
```
> [!NOTE]
> In this mode, the container and the VM will each have their own separate IPs.
### How do I add multiple disks?
To create additional disks, modify your compose file like this:
@ -295,18 +252,18 @@ kubectl apply -f https://raw.githubusercontent.com/qemus/qemu-arm/refs/heads/mas
DISK2_SIZE: "32G"
DISK3_SIZE: "64G"
volumes:
- ./example2:/storage2
- ./example3:/storage3
- /home/example:/storage2
- /mnt/data/example:/storage3
```
### How do I pass-through a disk?
It is possible to pass-through disk devices or partitions directly by adding them to your compose file in this way:
It is possible to pass-through disk devices directly by adding them to your compose file in this way:
```yaml
devices:
- /dev/sdb:/disk1
- /dev/sdc1:/disk2
- /dev/sdc:/disk2
```
Use `/disk1` if you want it to become your main drive, and use `/disk2` and higher to add them as secondary drives.
@ -322,23 +279,6 @@ kubectl apply -f https://raw.githubusercontent.com/qemus/qemu-arm/refs/heads/mas
- /dev/bus/usb
```
### How do I share files with the host?
To share files with the host, first ensure that your guest OS has `9pfs` support compiled in or available as a kernel module. If so, add the following volume to your compose file:
```yaml
volumes:
- ./example:/shared
```
Then start the container and execute the following command in the guest:
```shell
mount -t 9p -o trans=virtio shared /mnt/example
```
Now the `./example` directory on the host will be available as `/mnt/example` in the guest.
### How can I provide custom arguments to QEMU?
You can create the `ARGUMENTS` environment variable to provide additional arguments to QEMU at runtime:
@ -348,12 +288,23 @@ kubectl apply -f https://raw.githubusercontent.com/qemus/qemu-arm/refs/heads/mas
ARGUMENTS: "-device usb-tablet"
```
If you want to see the full command-line arguments used, you can set:
### What image formats are supported?
```yaml
environment:
DEBUG: "Y"
```
The `BOOT` URL accepts files in any of the following formats:
| **Extension** | **Format** |
|---|---|
| `.img` | Raw |
| `.raw` | Raw |
| `.iso` | Optical |
| `.qcow2` | QEMU |
| `.vmdk` | VMware |
| `.vhd` | VirtualPC |
| `.vhdx` | Hyper-V |
| `.vdi` | VirtualBox |
> [!TIP]
> It will also accept `.img.gz`, `.qcow2.xz`, `.iso.zip` and many more, as it automaticly extracts compressed files.
## Stars 🌟
[![Stars](https://starchart.cc/qemus/qemu-arm.svg?variant=adaptive)](https://starchart.cc/qemus/qemu-arm)

View file

@ -2,14 +2,23 @@
set -Eeuo pipefail
# Docker environment variables
: "${BIOS:=""}" # BIOS file
: "${SECURE:="off"}" # Secure boot
: "${BIOS:=""}" # BIOS file
BOOT_DESC=""
SECURE="off"
BOOT_OPTS=""
[ -n "$BIOS" ] && BOOT_MODE="custom"
BOOT_DESC=""
if [ -n "$BIOS" ]; then
BOOT_MODE="custom"
BOOT_OPTS="-bios $BIOS"
BOOT_DESC=" with custom BIOS file"
return 0
fi
case "${BOOT_MODE,,}" in
"legacy" )
BOOT_DESC=" with SeaBIOS"
;;
"uefi" | "" )
BOOT_MODE="uefi"
ROM="AAVMF_CODE.no-secboot.fd"
@ -33,13 +42,6 @@ case "${BOOT_MODE,,}" in
VARS="AAVMF_VARS.ms.fd"
BOOT_OPTS="-rtc base=localtime"
;;
"legacy" )
BOOT_DESC=" with SeaBIOS"
;;
"custom" )
BOOT_OPTS="-bios $BIOS"
BOOT_DESC=" with custom BIOS file"
;;
*)
error "Unknown BOOT_MODE, value \"${BOOT_MODE}\" is not recognized!"
exit 33
@ -72,45 +74,4 @@ case "${BOOT_MODE,,}" in
;;
esac
MSRS="/sys/module/kvm/parameters/ignore_msrs"
if [ -e "$MSRS" ]; then
result=$(<"$MSRS")
result="${result//[![:print:]]/}"
if [[ "$result" == "0" ]] || [[ "${result^^}" == "N" ]]; then
echo 1 | tee "$MSRS" > /dev/null 2>&1 || true
fi
fi
CLOCKSOURCE="tsc"
[[ "${ARCH,,}" == "arm64" ]] && CLOCKSOURCE="arch_sys_counter"
CLOCK="/sys/devices/system/clocksource/clocksource0/current_clocksource"
if [ ! -f "$CLOCK" ]; then
warn "file \"$CLOCK\" cannot not found?"
else
result=$(<"$CLOCK")
result="${result//[![:print:]]/}"
case "${result,,}" in
"${CLOCKSOURCE,,}" ) ;;
"kvm-clock" ) info "Nested KVM virtualization detected.." ;;
"hyperv_clocksource_tsc_page" ) info "Nested Hyper-V virtualization detected.." ;;
"hpet" ) warn "unsupported clock source detected: '$result'. Please set host clock source to '$CLOCKSOURCE'." ;;
*) warn "unexpected clock source detected: '$result'. Please set host clock source to '$CLOCKSOURCE'." ;;
esac
fi
SM_BIOS=""
PS="/sys/class/dmi/id/product_serial"
if [ -s "$PS" ] && [ -r "$PS" ]; then
BIOS_SERIAL=$(<"$PS")
BIOS_SERIAL="${BIOS_SERIAL//[![:alnum:]]/}"
if [ -n "$BIOS_SERIAL" ]; then
SM_BIOS="-smbios type=1,serial=$BIOS_SERIAL"
fi
fi
return 0

View file

@ -3,7 +3,7 @@ set -Eeuo pipefail
: "${UUID:=""}"
: "${SERIAL:="mon:stdio"}"
: "${USB:="qemu-xhci,id=xhci,p2=7,p3=7"}"
: "${USB:="qemu-xhci,id=xhci"}"
: "${MONITOR:="telnet:localhost:7100,server,nowait,nodelay"}"
: "${SMP:="$CPU_CORES,sockets=1,dies=1,cores=$CPU_CORES,threads=1"}"
@ -12,24 +12,12 @@ SERIAL_OPTS="-serial $SERIAL"
CPU_OPTS="-cpu $CPU_FLAGS -smp $SMP"
RAM_OPTS=$(echo "-m ${RAM_SIZE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
MON_OPTS="-monitor $MONITOR -name $PROCESS,process=$PROCESS,debug-threads=on"
MAC_OPTS="-machine type=${MACHINE},secure=${SECURE},dump-guest-core=off${KVM_OPTS}"
[ -n "$UUID" ] && MAC_OPTS+=" -uuid $UUID"
[ -n "$SM_BIOS" ] && MAC_OPTS+=" $SM_BIOS"
DEV_OPTS="-object rng-random,id=objrng0,filename=/dev/urandom"
DEV_OPTS+=" -device virtio-rng-pci,rng=objrng0,id=rng0,bus=pcie.0"
if [[ "${BOOT_MODE,,}" != "windows"* ]]; then
DEV_OPTS+=" -device virtio-balloon-pci,id=balloon0,bus=pcie.0"
fi
if [ -d "/shared" ] && [[ "${BOOT_MODE,,}" != "windows"* ]]; then
DEV_OPTS+=" -fsdev local,id=fsdev0,path=/shared,security_model=none"
DEV_OPTS+=" -device virtio-9p-pci,id=fs0,fsdev=fsdev0,mount_tag=shared"
fi
[ -n "$USB" ] && [[ "${USB,,}" != "no"* ]] && USB_OPTS="-device $USB -device usb-kbd -device usb-tablet"
MAC_OPTS="-machine type=${MACHINE},secure=${SECURE},dump-guest-core=off${KVM_OPTS}"
[ -n "$UUID" ] && MAC_OPTS="$MAC_OPTS -uuid $UUID"
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"
[[ "${BOOT_MODE,,}" != "windows"* ]] && DEV_OPTS+=" -device virtio-balloon-pci,id=balloon0,bus=pcie.0,addr=0x4"
ARGS="$DEF_OPTS $CPU_OPTS $RAM_OPTS $MAC_OPTS $DISPLAY_OPTS $MON_OPTS $SERIAL_OPTS ${USB_OPTS:-} $NET_OPTS $DISK_OPTS $BOOT_OPTS $DEV_OPTS $ARGUMENTS"
ARGS=$(echo "$ARGS" | sed 's/\t/ /g' | tr -s ' ')
@ -52,27 +40,21 @@ fi
if [[ "$RAM_CHECK" != [Nn]* ]]; then
RAM_AVAIL=$(free -b | grep -m 1 Mem: | awk '{print $7}')
AVAIL_MEM=$(formatBytes "$RAM_AVAIL")
AVAIL_GB=$(( RAM_AVAIL/1073741824 ))
if (( (RAM_WANTED + RAM_SPARE) > RAM_AVAIL )); then
msg="Your configured RAM_SIZE of ${RAM_SIZE/G/ GB} is too high for the $AVAIL_MEM of memory available, please set a lower value."
[[ "${FS,,}" != "zfs" ]] && error "$msg" && exit 17
info "$msg"
else
if (( (RAM_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 memory available, please consider a lower value."
if [[ "${FS,,}" != "zfs" ]]; then
warn "$msg"
else
info "$msg"
fi
fi
error "Your configured RAM_SIZE of $WANTED_GB GB is too high for the $AVAIL_GB GB of memory available, please set a lower value."
exit 17
fi
if (( (RAM_WANTED + (RAM_SPARE * 3)) > RAM_AVAIL )); then
warn "your configured RAM_SIZE of $WANTED_GB GB is very close to the $AVAIL_GB GB of memory available, please consider a lower value."
fi
fi
if [[ "$DEBUG" == [Yy1]* ]]; then
printf "Arguments:\n\n%s\n\n" "${ARGS// -/$'\n-'}"
printf "Arguments:\n\n%s" "${ARGS// -/$'\n-'}" && echo
fi
return 0

675
src/disk.sh Normal file
View file

@ -0,0 +1,675 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Docker environment variables
: "${DISK_IO:="native"}" # I/O Mode, can be set to 'native', 'threads' or 'io_uring'
: "${DISK_FMT:=""}" # Disk file format, can be set to "raw" (default) or "qcow2"
: "${DISK_TYPE:=""}" # Device type to be used, "sata", "nvme", "blk" or "scsi"
: "${DISK_FLAGS:=""}" # Specifies the options for use with the qcow2 disk format
: "${DISK_CACHE:="none"}" # Caching mode, can be set to 'writeback' for better performance
: "${DISK_DISCARD:="on"}" # Controls whether unmap (TRIM) commands are passed to the host.
: "${DISK_ROTATION:="1"}" # Rotation rate, set to 1 for SSD storage and increase for HDD
fmt2ext() {
local DISK_FMT=$1
case "${DISK_FMT,,}" in
qcow2)
echo "qcow2"
;;
raw)
echo "img"
;;
*)
error "Unrecognized disk format: $DISK_FMT" && exit 78
;;
esac
}
ext2fmt() {
local DISK_EXT=$1
case "${DISK_EXT,,}" in
qcow2)
echo "qcow2"
;;
img)
echo "raw"
;;
*)
error "Unrecognized file extension: .$DISK_EXT" && exit 78
;;
esac
}
getSize() {
local DISK_FILE=$1
local DISK_EXT DISK_FMT
DISK_EXT=$(echo "${DISK_FILE//*./}" | sed 's/^.*\.//')
DISK_FMT=$(ext2fmt "$DISK_EXT")
case "${DISK_FMT,,}" in
raw)
stat -c%s "$DISK_FILE"
;;
qcow2)
qemu-img info "$DISK_FILE" -f "$DISK_FMT" | grep '^virtual size: ' | sed 's/.*(\(.*\) bytes)/\1/'
;;
*)
error "Unrecognized disk format: $DISK_FMT" && exit 78
;;
esac
}
isCow() {
local FS=$1
if [[ "${FS,,}" == "btrfs" ]]; then
return 0
fi
return 1
}
supportsDirect() {
local FS=$1
if [[ "${FS,,}" == "ecryptfs" ]] || [[ "${FS,,}" == "tmpfs" ]]; then
return 1
fi
return 0
}
createDisk() {
local DISK_FILE=$1
local DISK_SPACE=$2
local DISK_DESC=$3
local DISK_FMT=$4
local FS=$5
local DATA_SIZE DIR SPACE FA
DATA_SIZE=$(numfmt --from=iec "$DISK_SPACE")
rm -f "$DISK_FILE"
if [[ "$ALLOCATE" != [Nn]* ]]; then
# Check free diskspace
DIR=$(dirname "$DISK_FILE")
SPACE=$(df --output=avail -B 1 "$DIR" | tail -n 1)
if (( DATA_SIZE > SPACE )); then
local SPACE_GB=$(( (SPACE + 1073741823)/1073741824 ))
error "Not enough free space to create a $DISK_DESC of $DISK_SPACE in $DIR, it has only $SPACE_GB GB available..."
error "Please specify a smaller ${DISK_DESC^^}_SIZE or disable preallocation by setting ALLOCATE=N." && exit 76
fi
fi
html "Creating a $DISK_DESC image..."
info "Creating a $DISK_SPACE $DISK_STYLE $DISK_DESC image in $DISK_FMT format..."
local FAIL="Could not create a $DISK_STYLE $DISK_FMT $DISK_DESC image of $DISK_SPACE ($DISK_FILE)"
case "${DISK_FMT,,}" in
raw)
if isCow "$FS"; then
if ! touch "$DISK_FILE"; then
error "$FAIL" && exit 77
fi
{ chattr +C "$DISK_FILE"; } || :
fi
if [[ "$ALLOCATE" == [Nn]* ]]; then
# Create an empty file
if ! truncate -s "$DATA_SIZE" "$DISK_FILE"; then
rm -f "$DISK_FILE"
error "$FAIL" && exit 77
fi
else
# Create an empty file
if ! fallocate -l "$DATA_SIZE" "$DISK_FILE"; then
if ! truncate -s "$DATA_SIZE" "$DISK_FILE"; then
rm -f "$DISK_FILE"
error "$FAIL" && exit 77
fi
fi
fi
;;
qcow2)
local DISK_PARAM="$DISK_ALLOC"
isCow "$FS" && DISK_PARAM+=",nocow=on"
[ -n "$DISK_FLAGS" ] && DISK_PARAM+=",$DISK_FLAGS"
if ! qemu-img create -f "$DISK_FMT" -o "$DISK_PARAM" -- "$DISK_FILE" "$DATA_SIZE" ; then
rm -f "$DISK_FILE"
error "$FAIL" && exit 70
fi
;;
esac
if isCow "$FS"; then
FA=$(lsattr "$DISK_FILE")
if [[ "$FA" != *"C"* ]]; then
error "Failed to disable COW for $DISK_DESC image $DISK_FILE on ${FS^^} filesystem (returned $FA)"
fi
fi
return 0
}
resizeDisk() {
local DISK_FILE=$1
local DISK_SPACE=$2
local DISK_DESC=$3
local DISK_FMT=$4
local FS=$5
local CUR_SIZE DATA_SIZE DIR SPACE
CUR_SIZE=$(getSize "$DISK_FILE")
DATA_SIZE=$(numfmt --from=iec "$DISK_SPACE")
local REQ=$((DATA_SIZE-CUR_SIZE))
(( REQ < 1 )) && error "Shrinking disks is not supported yet, please increase ${DISK_DESC^^}_SIZE." && exit 71
if [[ "$ALLOCATE" != [Nn]* ]]; then
# Check free diskspace
DIR=$(dirname "$DISK_FILE")
SPACE=$(df --output=avail -B 1 "$DIR" | tail -n 1)
if (( REQ > SPACE )); then
local SPACE_GB=$(( (SPACE + 1073741823)/1073741824 ))
error "Not enough free space to resize $DISK_DESC to $DISK_SPACE in $DIR, it has only $SPACE_GB GB available.."
error "Please specify a smaller ${DISK_DESC^^}_SIZE or disable preallocation by setting ALLOCATE=N." && exit 74
fi
fi
local GB=$(( (CUR_SIZE + 1073741823)/1073741824 ))
MSG="Resizing $DISK_DESC from ${GB}G to $DISK_SPACE..."
info "$MSG" && html "$MSG"
local FAIL="Could not resize the $DISK_STYLE $DISK_FMT $DISK_DESC image from ${GB}G to $DISK_SPACE ($DISK_FILE)"
case "${DISK_FMT,,}" in
raw)
if [[ "$ALLOCATE" == [Nn]* ]]; then
# Resize file by changing its length
if ! truncate -s "$DATA_SIZE" "$DISK_FILE"; then
error "$FAIL" && exit 75
fi
else
# Resize file by allocating more space
if ! fallocate -l "$DATA_SIZE" "$DISK_FILE"; then
if ! truncate -s "$DATA_SIZE" "$DISK_FILE"; then
error "$FAIL" && exit 75
fi
fi
fi
;;
qcow2)
if ! qemu-img resize -f "$DISK_FMT" "--$DISK_ALLOC" "$DISK_FILE" "$DATA_SIZE" ; then
error "$FAIL" && exit 72
fi
;;
esac
return 0
}
convertDisk() {
local SOURCE_FILE=$1
local SOURCE_FMT=$2
local DST_FILE=$3
local DST_FMT=$4
local DISK_BASE=$5
local DISK_DESC=$6
local FS=$7
[ -f "$DST_FILE" ] && error "Conversion failed, destination file $DST_FILE already exists?" && exit 79
[ ! -f "$SOURCE_FILE" ] && error "Conversion failed, source file $SOURCE_FILE does not exists?" && exit 79
local TMP_FILE="$DISK_BASE.tmp"
rm -f "$TMP_FILE"
if [[ "$ALLOCATE" != [Nn]* ]]; then
local DIR CUR_SIZE SPACE
# Check free diskspace
DIR=$(dirname "$TMP_FILE")
CUR_SIZE=$(getSize "$SOURCE_FILE")
SPACE=$(df --output=avail -B 1 "$DIR" | tail -n 1)
if (( CUR_SIZE > SPACE )); then
local SPACE_GB=$(( (SPACE + 1073741823)/1073741824 ))
error "Not enough free space to convert $DISK_DESC to $DST_FMT in $DIR, it has only $SPACE_GB GB available..."
error "Please free up some disk space or disable preallocation by setting ALLOCATE=N." && exit 76
fi
fi
local msg="Converting $DISK_DESC to $DST_FMT"
html "$msg..."
info "$msg, please wait until completed..."
local CONV_FLAGS="-p"
local DISK_PARAM="$DISK_ALLOC"
isCow "$FS" && DISK_PARAM+=",nocow=on"
if [[ "$DST_FMT" != "raw" ]]; then
if [[ "$ALLOCATE" == [Nn]* ]]; then
CONV_FLAGS+=" -c"
fi
[ -n "$DISK_FLAGS" ] && DISK_PARAM+=",$DISK_FLAGS"
fi
# shellcheck disable=SC2086
if ! qemu-img convert -f "$SOURCE_FMT" $CONV_FLAGS -o "$DISK_PARAM" -O "$DST_FMT" -- "$SOURCE_FILE" "$TMP_FILE"; then
rm -f "$TMP_FILE"
error "Failed to convert $DISK_STYLE $DISK_DESC image to $DST_FMT format in $DIR, is there enough space available?" && exit 79
fi
if [[ "$DST_FMT" == "raw" ]]; then
if [[ "$ALLOCATE" != [Nn]* ]]; then
# Work around qemu-img bug
CUR_SIZE=$(stat -c%s "$TMP_FILE")
if ! fallocate -l "$CUR_SIZE" "$TMP_FILE"; then
error "Failed to allocate $CUR_SIZE bytes for $DISK_DESC image $TMP_FILE"
fi
fi
fi
rm -f "$SOURCE_FILE"
mv "$TMP_FILE" "$DST_FILE"
if isCow "$FS"; then
FA=$(lsattr "$DST_FILE")
if [[ "$FA" != *"C"* ]]; then
error "Failed to disable COW for $DISK_DESC image $DST_FILE on ${FS^^} filesystem (returned $FA)"
fi
fi
msg="Conversion of $DISK_DESC"
html "$msg completed..."
info "$msg to $DST_FMT completed succesfully!"
return 0
}
checkFS () {
local FS=$1
local DISK_FILE=$2
local DISK_DESC=$3
local DIR FA
DIR=$(dirname "$DISK_FILE")
[ ! -d "$DIR" ] && return 0
if [[ "${FS,,}" == "overlay"* ]]; then
info "Warning: the filesystem of $DIR is OverlayFS, this usually means it was binded to an invalid path!"
fi
if [[ "${FS,,}" == "fuse"* ]]; then
info "Warning: the filesystem of $DIR is FUSE, this extra layer will negatively affect performance!"
fi
if ! supportsDirect "$FS"; then
info "Warning: the filesystem of $DIR is $FS, which does not support O_DIRECT mode, adjusting settings..."
fi
if isCow "$FS"; then
if [ -f "$DISK_FILE" ]; then
FA=$(lsattr "$DISK_FILE")
if [[ "$FA" != *"C"* ]]; then
info "Warning: COW (copy on write) is not disabled for $DISK_DESC image file $DISK_FILE, this is recommended on ${FS^^} filesystems!"
fi
fi
fi
return 0
}
createDevice () {
local DISK_FILE=$1
local DISK_TYPE=$2
local DISK_INDEX=$3
local DISK_ADDRESS=$4
local DISK_FMT=$5
local DISK_IO=$6
local DISK_CACHE=$7
local DISK_ID="data$DISK_INDEX"
local index=""
[ -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
"none" ) ;;
"auto" )
echo "$result"
;;
"usb" )
result+=",if=none \
-device usb-storage,drive=${DISK_ID}${index}"
echo "$result"
;;
"nvme" )
result+=",if=none \
-device nvme,drive=${DISK_ID}${index},serial=deadbeaf${DISK_INDEX}"
echo "$result"
;;
"ide" | "sata" )
result+=",if=none \
-device ich9-ahci,id=ahci${DISK_INDEX},addr=$DISK_ADDRESS \
-device ide-hd,drive=${DISK_ID},bus=ahci$DISK_INDEX.0,rotation_rate=$DISK_ROTATION${index}"
echo "$result"
;;
"blk" | "virtio-blk" )
result+=",if=none \
-device virtio-blk-pci,drive=${DISK_ID},bus=pcie.0,addr=$DISK_ADDRESS,iothread=io2${index}"
echo "$result"
;;
"scsi" | "virtio-scsi" )
result+=",if=none \
-device virtio-scsi-pci,id=${DISK_ID}b,bus=pcie.0,addr=$DISK_ADDRESS,iothread=io2 \
-device scsi-hd,drive=${DISK_ID},bus=${DISK_ID}b.0,channel=0,scsi-id=0,lun=0,rotation_rate=$DISK_ROTATION${index}"
echo "$result"
;;
esac
return 0
}
addMedia () {
local DISK_FILE=$1
local DISK_TYPE=$2
local DISK_INDEX=$3
local DISK_ADDRESS=$4
local index=""
local DISK_ID="cdrom$DISK_INDEX"
[ -n "$DISK_INDEX" ] && index=",bootindex=$DISK_INDEX"
local result=" -drive file=$DISK_FILE,id=$DISK_ID,format=raw,cache=unsafe,readonly=on,media=cdrom"
case "${DISK_TYPE,,}" in
"none" ) ;;
"auto" )
echo "$result"
;;
"usb" )
result+=",if=none \
-device usb-storage,drive=${DISK_ID}${index},removable=on"
echo "$result"
;;
"nvme" )
result+=",if=none \
-device nvme,drive=${DISK_ID}${index},serial=deadbeaf${DISK_INDEX}"
echo "$result"
;;
"ide" | "sata" )
result+=",if=none \
-device ich9-ahci,id=ahci${DISK_INDEX},addr=$DISK_ADDRESS \
-device ide-cd,drive=${DISK_ID},bus=ahci${DISK_INDEX}.0${index}"
echo "$result"
;;
"blk" | "virtio-blk" )
result+=",if=none \
-device virtio-blk-pci,drive=${DISK_ID},bus=pcie.0,addr=$DISK_ADDRESS,iothread=io2${index}"
echo "$result"
;;
"scsi" | "virtio-scsi" )
result+=",if=none \
-device virtio-scsi-pci,id=${DISK_ID}b,bus=pcie.0,addr=$DISK_ADDRESS,iothread=io2 \
-device scsi-cd,drive=${DISK_ID},bus=${DISK_ID}b.0${index}"
echo "$result"
;;
esac
return 0
}
addDisk () {
local DISK_BASE=$1
local DISK_TYPE=$2
local DISK_DESC=$3
local DISK_SPACE=$4
local DISK_INDEX=$5
local DISK_ADDRESS=$6
local DISK_FMT=$7
local DISK_IO=$8
local DISK_CACHE=$9
local DISK_EXT DIR DATA_SIZE FS PREV_FMT PREV_EXT CUR_SIZE
DISK_EXT=$(fmt2ext "$DISK_FMT")
local DISK_FILE="$DISK_BASE.$DISK_EXT"
DIR=$(dirname "$DISK_FILE")
[ ! -d "$DIR" ] && return 0
[ -z "$DISK_SPACE" ] && DISK_SPACE="16G"
DISK_SPACE=$(echo "${DISK_SPACE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
[[ -z "${DISK_SPACE//[0-9]}" ]] && DISK_SPACE="${DISK_SPACE}G"
DATA_SIZE=$(numfmt --from=iec "$DISK_SPACE")
if (( DATA_SIZE < 1 )); then
error "Invalid value for ${DISK_DESC^^}_SIZE: $DISK_SPACE" && exit 73
fi
if (( DATA_SIZE < 104857600 )); then
error "Please increase ${DISK_DESC^^}_SIZE to at least 100 MB." && exit 73
fi
FS=$(stat -f -c %T "$DIR")
checkFS "$FS" "$DISK_FILE" "$DISK_DESC" || exit $?
if ! supportsDirect "$FS"; then
DISK_IO="threads"
DISK_CACHE="writeback"
fi
if ! [ -s "$DISK_FILE" ] ; then
if [[ "${DISK_FMT,,}" != "raw" ]]; then
PREV_FMT="raw"
else
PREV_FMT="qcow2"
fi
PREV_EXT=$(fmt2ext "$PREV_FMT")
if [ -s "$DISK_BASE.$PREV_EXT" ] ; then
convertDisk "$DISK_BASE.$PREV_EXT" "$PREV_FMT" "$DISK_FILE" "$DISK_FMT" "$DISK_BASE" "$DISK_DESC" "$FS" || exit $?
fi
fi
if [ -s "$DISK_FILE" ]; then
CUR_SIZE=$(getSize "$DISK_FILE")
if (( DATA_SIZE > CUR_SIZE )); then
resizeDisk "$DISK_FILE" "$DISK_SPACE" "$DISK_DESC" "$DISK_FMT" "$FS" || exit $?
fi
else
createDisk "$DISK_FILE" "$DISK_SPACE" "$DISK_DESC" "$DISK_FMT" "$FS" || exit $?
fi
DISK_OPTS+=$(createDevice "$DISK_FILE" "$DISK_TYPE" "$DISK_INDEX" "$DISK_ADDRESS" "$DISK_FMT" "$DISK_IO" "$DISK_CACHE")
return 0
}
addDevice () {
local DISK_DEV=$1
local DISK_TYPE=$2
local DISK_INDEX=$3
local DISK_ADDRESS=$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
DISK_OPTS+=$(createDevice "$DISK_DEV" "$DISK_TYPE" "$DISK_INDEX" "$DISK_ADDRESS" "raw" "$DISK_IO" "$DISK_CACHE")
return 0
}
html "Initializing disks..."
[ -z "${DISK_OPTS:-}" ] && DISK_OPTS=""
[ -z "${DISK_TYPE:-}" ] && DISK_TYPE="scsi"
[ -z "${DISK_NAME:-}" ] && DISK_NAME="data"
case "${DISK_TYPE,,}" in
"ide" | "sata" | "nvme" | "usb" | "scsi" | "blk" | "auto" | "none" ) ;;
* ) error "Invalid DISK_TYPE specified, value \"$DISK_TYPE\" is not recognized!" && exit 80 ;;
esac
if [[ "${MACHINE,,}" != "virt" ]]; then
FALLBACK="ide"
else
FALLBACK="usb"
fi
[[ "${BOOT_MODE:-}" == "windows_legacy" ]] && FALLBACK="auto"
if [ -z "${MEDIA_TYPE:-}" ]; then
if [[ "${BOOT_MODE:-}" != "windows"* ]]; then
if [[ "${DISK_TYPE,,}" == "blk" ]]; then
MEDIA_TYPE="$FALLBACK"
else
MEDIA_TYPE="$DISK_TYPE"
fi
else
MEDIA_TYPE="$FALLBACK"
fi
fi
case "${MEDIA_TYPE,,}" in
"ide" | "sata" | "nvme" | "usb" | "scsi" | "blk" | "auto" | "none" ) ;;
* ) error "Invalid MEDIA_TYPE specified, value \"$MEDIA_TYPE\" is not recognized!" && exit 80 ;;
esac
if [ -f "$BOOT" ] && [ -s "$BOOT" ]; then
case "${BOOT,,}" in
*".iso" )
DISK_OPTS+=$(addMedia "$BOOT" "$MEDIA_TYPE" "$BOOT_INDEX" "0x5") ;;
*".img" | *".raw" )
DISK_OPTS+=$(createDevice "$BOOT" "$DISK_TYPE" "$BOOT_INDEX" "0x5" "raw" "$DISK_IO" "$DISK_CACHE") ;;
*".qcow2" )
DISK_OPTS+=$(createDevice "$BOOT" "$DISK_TYPE" "$BOOT_INDEX" "0x5" "qcow2" "$DISK_IO" "$DISK_CACHE") ;;
* )
error "Invalid BOOT image specified, extension \".${BOOT/*./}\" is not recognized!" && exit 80 ;;
esac
fi
DRIVERS="/drivers.iso"
[ ! -f "$DRIVERS" ] || [ ! -s "$DRIVERS" ] && DRIVERS="$STORAGE/drivers.iso"
if [ -f "$DRIVERS" ] && [ -s "$DRIVERS" ]; then
DISK_OPTS+=$(addMedia "$DRIVERS" "$FALLBACK" "" "0x6")
fi
RESCUE="/start.iso"
[ ! -f "$RESCUE" ] || [ ! -s "$RESCUE" ] && RESCUE="$STORAGE/start.iso"
if [ -f "$RESCUE" ] && [ -s "$RESCUE" ]; then
DISK_OPTS+=$(addMedia "$RESCUE" "$FALLBACK" "1" "0x6")
fi
DISK1_FILE="$STORAGE/${DISK_NAME}"
DISK2_FILE="/storage2/${DISK_NAME}2"
DISK3_FILE="/storage3/${DISK_NAME}3"
DISK4_FILE="/storage4/${DISK_NAME}4"
if [ -z "$DISK_FMT" ]; then
if [ -f "$DISK1_FILE.qcow2" ]; then
DISK_FMT="qcow2"
else
DISK_FMT="raw"
fi
fi
if [ -z "$ALLOCATE" ]; then
ALLOCATE="N"
fi
if [[ "$ALLOCATE" == [Nn]* ]]; then
DISK_STYLE="growable"
DISK_ALLOC="preallocation=off"
else
DISK_STYLE="preallocated"
DISK_ALLOC="preallocation=falloc"
fi
: "${DISK2_SIZE:=""}"
: "${DISK3_SIZE:=""}"
: "${DISK4_SIZE:=""}"
: "${DEVICE:=""}" # Docker variables to passthrough a block device, like /dev/vdc1.
: "${DEVICE2:=""}"
: "${DEVICE3:=""}"
: "${DEVICE4:=""}"
[ -z "$DEVICE" ] && [ -b "/disk" ] && DEVICE="/disk"
[ -z "$DEVICE" ] && [ -b "/disk1" ] && DEVICE="/disk1"
[ -z "$DEVICE2" ] && [ -b "/disk2" ] && DEVICE2="/disk2"
[ -z "$DEVICE3" ] && [ -b "/disk3" ] && DEVICE3="/disk3"
[ -z "$DEVICE4" ] && [ -b "/disk4" ] && DEVICE4="/disk4"
[ -z "$DEVICE" ] && [ -b "/dev/disk1" ] && DEVICE="/dev/disk1"
[ -z "$DEVICE2" ] && [ -b "/dev/disk2" ] && DEVICE2="/dev/disk2"
[ -z "$DEVICE3" ] && [ -b "/dev/disk3" ] && DEVICE3="/dev/disk3"
[ -z "$DEVICE4" ] && [ -b "/dev/disk4" ] && DEVICE4="/dev/disk4"
if [ -n "$DEVICE" ]; then
addDevice "$DEVICE" "$DISK_TYPE" "3" "0xa" || exit $?
else
addDisk "$DISK1_FILE" "$DISK_TYPE" "disk" "$DISK_SIZE" "3" "0xa" "$DISK_FMT" "$DISK_IO" "$DISK_CACHE" || exit $?
fi
if [ -n "$DEVICE2" ]; then
addDevice "$DEVICE2" "$DISK_TYPE" "4" "0xb" || exit $?
else
addDisk "$DISK2_FILE" "$DISK_TYPE" "disk2" "$DISK2_SIZE" "4" "0xb" "$DISK_FMT" "$DISK_IO" "$DISK_CACHE" || exit $?
fi
if [ -n "$DEVICE3" ]; then
addDevice "$DEVICE3" "$DISK_TYPE" "5" "0xc" || exit $?
else
addDisk "$DISK3_FILE" "$DISK_TYPE" "disk3" "$DISK3_SIZE" "5" "0xc" "$DISK_FMT" "$DISK_IO" "$DISK_CACHE" || exit $?
fi
if [ -n "$DEVICE4" ]; then
addDevice "$DEVICE4" "$DISK_TYPE" "6" "0xd" || exit $?
else
addDisk "$DISK4_FILE" "$DISK_TYPE" "disk4" "$DISK4_SIZE" "6" "0xd" "$DISK_FMT" "$DISK_IO" "$DISK_CACHE" || exit $?
fi
DISK_OPTS+=" -object iothread,id=io2"
html "Initialized disks successfully..."
return 0

View file

@ -6,22 +6,20 @@ set -Eeuo pipefail
: "${VGA:="ramfb"}" # VGA adaptor
: "${DISPLAY:="web"}" # Display type
[[ "$DISPLAY" == ":0" ]] && DISPLAY="web"
case "${DISPLAY,,}" in
"vnc" )
vnc)
DISPLAY_OPTS="-display vnc=:0 -device $VGA"
;;
"web" )
web)
DISPLAY_OPTS="-display vnc=:0,websocket=5700 -device $VGA"
;;
"ramfb" )
ramfb)
DISPLAY_OPTS="-display vnc=:0,websocket=5700 -device ramfb"
;;
"disabled" )
disabled)
DISPLAY_OPTS="-display none -device $VGA"
;;
"none" )
none)
DISPLAY_OPTS="-display none"
;;
*)

View file

@ -1,17 +1,13 @@
#!/usr/bin/env bash
set -Eeuo pipefail
: "${APP:="QEMU"}"
: "${MACHINE:="virt"}"
: "${PLATFORM:="arm64"}"
: "${SUPPORT:="https://github.com/qemus/qemu-arm"}"
APP="QEMU"
SUPPORT="https://github.com/qemus/qemu-arm"
cd /run
. utils.sh # Load functions
. reset.sh # Initialize system
. define.sh # Define images
. install.sh # Download image
. install.sh # Get bootdisk
. disk.sh # Initialize disks
. display.sh # Initialize graphics
. network.sh # Initialize network

294
src/install.sh Normal file
View file

@ -0,0 +1,294 @@
#!/usr/bin/env bash
set -Eeuo pipefail
detectType() {
local dir=""
local file="$1"
[ ! -f "$file" ] && return 1
[ ! -s "$file" ] && return 1
case "${file,,}" in
*".iso" | *".img" | *".raw" | *".qcow2" )
BOOT="$file" ;;
* ) return 1 ;;
esac
[ -n "$BOOT_MODE" ] && return 0
[[ "${file,,}" != *".iso" ]] && return 0
# Automaticly detect UEFI-compatible ISO's
dir=$(isoinfo -f -i "$file")
if [ -z "$dir" ]; then
BOOT=""
error "Failed to read ISO file, invalid format!" && return 1
fi
dir=$(echo "${dir^^}" | grep "^/EFI")
[ -z "$dir" ] && BOOT_MODE="legacy"
return 0
}
downloadFile() {
local url="$1"
local base="$2"
local msg rc total progress
local dest="$STORAGE/$base.tmp"
rm -f "$dest"
# Check if running with interactive TTY or redirected to docker log
if [ -t 1 ]; then
progress="--progress=bar:noscroll"
else
progress="--progress=dot:giga"
fi
msg="Downloading image"
info "Downloading $base..."
html "$msg..."
/run/progress.sh "$dest" "0" "$msg ([P])..." &
{ wget "$url" -O "$dest" -q --timeout=30 --no-http-keep-alive --show-progress "$progress"; rc=$?; } || :
fKill "progress.sh"
if (( rc == 0 )) && [ -f "$dest" ]; then
total=$(stat -c%s "$dest")
if [ "$total" -lt 100000 ]; then
error "Invalid image file: is only $total bytes?" && return 1
fi
html "Download finished successfully..."
mv -f "$dest" "$STORAGE/$base"
return 0
fi
msg="Failed to download $url"
(( rc == 3 )) && error "$msg , cannot write file (disk full?)" && return 1
(( rc == 4 )) && error "$msg , network failure!" && return 1
(( rc == 8 )) && error "$msg , server issued an error response!" && return 1
error "$msg , reason: $rc"
return 1
}
convertImage() {
local source_file=$1
local source_fmt=$2
local dst_file=$3
local dst_fmt=$4
local dir base fs fa space
local cur_size src_size disk_param
[ -f "$dst_file" ] && error "Conversion failed, destination file $dst_file already exists?" && return 1
[ ! -f "$source_file" ] && error "Conversion failed, source file $source_file does not exists?" && return 1
if [[ "${source_fmt,,}" == "${dst_fmt,,}" ]]; then
mv -f "$source_file" "$dst_file"
return 0
fi
local tmp_file="$dst_file.tmp"
dir=$(dirname "$tmp_file")
rm -f "$tmp_file"
if [ -n "$ALLOCATE" ] && [[ "$ALLOCATE" != [Nn]* ]]; then
# Check free diskspace
src_size=$(qemu-img info "$source_file" -f "$source_fmt" | grep '^virtual size: ' | sed 's/.*(\(.*\) bytes)/\1/')
space=$(df --output=avail -B 1 "$dir" | tail -n 1)
if (( src_size > space )); then
local space_gb=$(( (space + 1073741823)/1073741824 ))
error "Not enough free space to convert image in $dir, it has only $space_gb GB available..." && return 1
fi
fi
base=$(basename "$source_file")
info "Converting $base..."
html "Converting image..."
local conv_flags="-p"
if [ -z "$ALLOCATE" ] || [[ "$ALLOCATE" == [Nn]* ]]; then
disk_param="preallocation=off"
else
disk_param="preallocation=falloc"
fi
fs=$(stat -f -c %T "$dir")
[[ "${fs,,}" == "btrfs" ]] && disk_param+=",nocow=on"
if [[ "$dst_fmt" != "raw" ]]; then
if [ -z "$ALLOCATE" ] || [[ "$ALLOCATE" == [Nn]* ]]; then
conv_flags+=" -c"
fi
[ -n "${DISK_FLAGS:-}" ] && disk_param+=",$DISK_FLAGS"
fi
# shellcheck disable=SC2086
if ! qemu-img convert -f "$source_fmt" $conv_flags -o "$disk_param" -O "$dst_fmt" -- "$source_file" "$tmp_file"; then
rm -f "$tmp_file"
error "Failed to convert image in $dir, is there enough space available?" && return 1
fi
if [[ "$dst_fmt" == "raw" ]]; then
if [ -n "$ALLOCATE" ] && [[ "$ALLOCATE" != [Nn]* ]]; then
# Work around qemu-img bug
cur_size=$(stat -c%s "$tmp_file")
if ! fallocate -l "$cur_size" "$tmp_file"; then
error "Failed to allocate $cur_size bytes for image!"
fi
fi
fi
rm -f "$source_file"
mv "$tmp_file" "$dst_file"
if [[ "${fs,,}" == "btrfs" ]]; then
fa=$(lsattr "$dst_file")
if [[ "$fa" != *"C"* ]]; then
error "Failed to disable COW for image on ${fs^^} filesystem!"
fi
fi
html "Conversion completed..."
return 0
}
findFile() {
local ext="$1"
local file
file=$(find / -maxdepth 1 -type f -iname "boot.$ext" | head -n 1)
[ ! -s "$file" ] && file=$(find "$STORAGE" -maxdepth 1 -type f -iname "boot.$ext" | head -n 1)
detectType "$file" && return 0
return 1
}
findFile "iso" && return 0
findFile "img" && return 0
findFile "raw" && return 0
findFile "qcow2" && return 0
if [ -z "$BOOT" ] || [[ "$BOOT" == *"example.com/image.iso" ]]; then
hasDisk && return 0
error "No boot disk specified, set BOOT= to the URL of a disk image file." && exit 64
fi
base=$(basename "${BOOT%%\?*}")
: "${base//+/ }"; printf -v base '%b' "${_//%/\\x}"
base=$(echo "$base" | sed -e 's/[^A-Za-z0-9._-]/_/g')
case "${base,,}" in
*".iso" | *".img" | *".raw" | *".qcow2" )
detectType "$STORAGE/$base" && return 0 ;;
*".vdi" | *".vmdk" | *".vhd" | *".vhdx" )
detectType "$STORAGE/${base%.*}.img" && return 0
detectType "$STORAGE/${base%.*}.qcow2" && return 0 ;;
*".gz" | *".gzip" | *".xz" | *".7z" | *".zip" | *".rar" | *".lzma" | *".bz" | *".bz2" )
case "${base%.*}" in
*".iso" | *".img" | *".raw" | *".qcow2" )
detectType "$STORAGE/${base%.*}" && return 0 ;;
*".vdi" | *".vmdk" | *".vhd" | *".vhdx" )
find="${base%.*}"
detectType "$STORAGE/${find%.*}.img" && return 0
detectType "$STORAGE/${find%.*}.qcow2" && return 0 ;;
esac ;;
* ) error "Unknown file extension, type \".${base/*./}\" is not recognized!" && exit 33 ;;
esac
if ! downloadFile "$BOOT" "$base"; then
rm -f "$STORAGE/$base.tmp" && exit 60
fi
case "${base,,}" in
*".gz" | *".gzip" | *".xz" | *".7z" | *".zip" | *".rar" | *".lzma" | *".bz" | *".bz2" )
info "Extracting $base..."
html "Extracting image..." ;;
esac
case "${base,,}" in
*".gz" | *".gzip" )
gzip -dc "$STORAGE/$base" > "$STORAGE/${base%.*}"
rm -f "$STORAGE/$base"
base="${base%.*}"
;;
*".xz" )
xz -dc "$STORAGE/$base" > "$STORAGE/${base%.*}"
rm -f "$STORAGE/$base"
base="${base%.*}"
;;
*".7z" | *".zip" | *".rar" | *".lzma" | *".bz" | *".bz2" )
tmp="$STORAGE/extract"
rm -rf "$tmp"
mkdir -p "$tmp"
7z x "$STORAGE/$base" -o"$tmp" > /dev/null
rm -f "$STORAGE/$base"
base="${base%.*}"
if [ ! -s "$tmp/$base" ]; then
rm -rf "$tmp"
error "Cannot find file \"${base}\" in .${BOOT/*./} archive!" && exit 32
fi
mv "$tmp/$base" "$STORAGE/$base"
rm -rf "$tmp"
;;
esac
case "${base,,}" in
*".iso" | *".img" | *".raw" | *".qcow2" )
detectType "$STORAGE/$base" && return 0
error "Cannot read file \"${base}\"" && exit 63 ;;
esac
target_ext="img"
target_fmt="${DISK_FMT:-}"
[ -z "$target_fmt" ] && target_fmt="raw"
[[ "$target_fmt" != "raw" ]] && target_ext="qcow2"
case "${base,,}" in
*".vdi" ) source_fmt="vdi" ;;
*".vhd" ) source_fmt="vpc" ;;
*".vhdx" ) source_fmt="vpc" ;;
*".vmdk" ) source_fmt="vmdk" ;;
* ) error "Unknown file extension, type \".${base/*./}\" is not recognized!" && exit 33 ;;
esac
dst="$STORAGE/${base%.*}.$target_ext"
! convertImage "$STORAGE/$base" "$source_fmt" "$dst" "$target_fmt" && exit 35
base=$(basename "$dst")
detectType "$STORAGE/$base" && return 0
error "Cannot read file \"${base}\"" && exit 36

416
src/network.sh Normal file
View file

@ -0,0 +1,416 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Docker environment variables
: "${MAC:=""}"
: "${DHCP:="N"}"
: "${NETWORK:="Y"}"
: "${HOST_PORTS:=""}"
: "${USER_PORTS:=""}"
: "${VM_NET_DEV:=""}"
: "${VM_NET_TAP:="qemu"}"
: "${VM_NET_MAC:="$MAC"}"
: "${VM_NET_HOST:="QEMU"}"
: "${VM_NET_IP:="20.20.20.21"}"
: "${DNSMASQ_OPTS:=""}"
: "${DNSMASQ:="/usr/sbin/dnsmasq"}"
: "${DNSMASQ_CONF_DIR:="/etc/dnsmasq.d"}"
ADD_ERR="Please add the following setting to your container:"
# ######################################
# Functions
# ######################################
configureDHCP() {
# Create the necessary file structure for /dev/vhost-net
if [ ! -c /dev/vhost-net ]; then
if mknod /dev/vhost-net c 10 238; then
chmod 660 /dev/vhost-net
fi
fi
# Create a macvtap network for the VM guest
{ ip link add link "$VM_NET_DEV" name "$VM_NET_TAP" address "$VM_NET_MAC" type macvtap mode bridge ; rc=$?; } || :
if (( rc != 0 )); then
error "Cannot create macvtap interface. Please make sure that the network type is 'macvlan' and not 'ipvlan',"
error "that your kernel is recent (>4) and supports it, and that the container has the NET_ADMIN capability set." && return 1
fi
while ! ip link set "$VM_NET_TAP" up; do
info "Waiting for MAC address $VM_NET_MAC to become available..."
sleep 2
done
local TAP_NR TAP_PATH MAJOR MINOR
TAP_NR=$(</sys/class/net/"$VM_NET_TAP"/ifindex)
TAP_PATH="/dev/tap${TAP_NR}"
# Create dev file (there is no udev in container: need to be done manually)
IFS=: read -r MAJOR MINOR < <(cat /sys/devices/virtual/net/"$VM_NET_TAP"/tap*/dev)
(( MAJOR < 1)) && error "Cannot find: sys/devices/virtual/net/$VM_NET_TAP" && return 1
[[ ! -e "$TAP_PATH" ]] && [[ -e "/dev0/${TAP_PATH##*/}" ]] && ln -s "/dev0/${TAP_PATH##*/}" "$TAP_PATH"
if [[ ! -e "$TAP_PATH" ]]; then
{ mknod "$TAP_PATH" c "$MAJOR" "$MINOR" ; rc=$?; } || :
(( rc != 0 )) && error "Cannot mknod: $TAP_PATH ($rc)" && return 1
fi
{ exec 30>>"$TAP_PATH"; rc=$?; } 2>/dev/null || :
if (( rc != 0 )); then
error "Cannot create TAP interface ($rc). $ADD_ERR --device-cgroup-rule='c *:* rwm'" && return 1
fi
{ exec 40>>/dev/vhost-net; rc=$?; } 2>/dev/null || :
if (( rc != 0 )); then
error "VHOST can not be found ($rc). $ADD_ERR --device=/dev/vhost-net" && return 1
fi
NET_OPTS="-netdev tap,id=hostnet0,vhost=on,vhostfd=40,fd=30"
return 0
}
configureDNS() {
# dnsmasq configuration:
DNSMASQ_OPTS+=" --dhcp-range=$VM_NET_IP,$VM_NET_IP --dhcp-host=$VM_NET_MAC,,$VM_NET_IP,$VM_NET_HOST,infinite --dhcp-option=option:netmask,255.255.255.0"
# Create lease file for faster resolve
echo "0 $VM_NET_MAC $VM_NET_IP $VM_NET_HOST 01:$VM_NET_MAC" > /var/lib/misc/dnsmasq.leases
chmod 644 /var/lib/misc/dnsmasq.leases
# Set DNS server and gateway
DNSMASQ_OPTS+=" --dhcp-option=option:dns-server,${VM_NET_IP%.*}.1 --dhcp-option=option:router,${VM_NET_IP%.*}.1"
# Add DNS entry for container
DNSMASQ_OPTS+=" --address=/host.lan/${VM_NET_IP%.*}.1"
DNSMASQ_OPTS=$(echo "$DNSMASQ_OPTS" | sed 's/\t/ /g' | tr -s ' ' | sed 's/^ *//')
if ! $DNSMASQ ${DNSMASQ_OPTS:+ $DNSMASQ_OPTS}; then
error "Failed to start dnsmasq, reason: $?" && return 1
fi
return 0
}
getUserPorts() {
local args=""
local list=$1
local ssh="22"
local rdp="3389"
[ -z "$list" ] && list="$ssh,$rdp" || list+=",$ssh,$rdp"
list="${list//,/ }"
list="${list## }"
list="${list%% }"
for port in $list; do
args+="hostfwd=tcp::$port-$VM_NET_IP:$port,"
done
echo "${args%?}"
return 0
}
getHostPorts() {
local list=$1
local vnc="5900"
local web="8006"
[ -z "$list" ] && list="$web" || list+=",$web"
if [[ "${DISPLAY,,}" == "vnc" ]] || [[ "${DISPLAY,,}" == "web" ]]; then
[ -z "$list" ] && list="$vnc" || list+=",$vnc"
fi
[ -z "$list" ] && echo "" && return 0
if [[ "$list" != *","* ]]; then
echo " ! --dport $list"
else
echo " -m multiport ! --dports $list"
fi
return 0
}
configureUser() {
NET_OPTS="-netdev user,id=hostnet0,host=${VM_NET_IP%.*}.1,net=${VM_NET_IP%.*}.0/24,dhcpstart=$VM_NET_IP,hostname=$VM_NET_HOST"
local forward
forward=$(getUserPorts "$USER_PORTS")
[ -n "$forward" ] && NET_OPTS+=",$forward"
return 0
}
configureNAT() {
# Create the necessary file structure for /dev/net/tun
if [ ! -c /dev/net/tun ]; then
[ ! -d /dev/net ] && mkdir -m 755 /dev/net
if mknod /dev/net/tun c 10 200; then
chmod 666 /dev/net/tun
fi
fi
if [ ! -c /dev/net/tun ]; then
error "TUN device missing. $ADD_ERR --device /dev/net/tun --cap-add NET_ADMIN" && return 1
fi
# Check port forwarding flag
if [[ $(< /proc/sys/net/ipv4/ip_forward) -eq 0 ]]; then
{ sysctl -w net.ipv4.ip_forward=1 > /dev/null; rc=$?; } || :
if (( rc != 0 )) || [[ $(< /proc/sys/net/ipv4/ip_forward) -eq 0 ]]; then
error "IP forwarding is disabled. $ADD_ERR --sysctl net.ipv4.ip_forward=1" && return 1
fi
fi
local tables="The 'ip_tables' kernel module is not loaded. Try this command: sudo modprobe ip_tables iptable_nat"
local tuntap="The 'tun' kernel module is not available. Try this command: 'sudo modprobe tun' or run the container with 'privileged: true'."
# Create a bridge with a static IP for the VM guest
{ ip link add dev dockerbridge type bridge ; rc=$?; } || :
if (( rc != 0 )); then
error "Failed to create bridge. $ADD_ERR --cap-add NET_ADMIN" && return 1
fi
if ! ip address add "${VM_NET_IP%.*}.1/24" broadcast "${VM_NET_IP%.*}.255" dev dockerbridge; then
error "Failed to add IP address!" && return 1
fi
while ! ip link set dockerbridge up; do
info "Waiting for IP address to become available..."
sleep 2
done
# QEMU Works with taps, set tap to the bridge created
if ! ip tuntap add dev "$VM_NET_TAP" mode tap; then
error "$tuntap" && return 1
fi
while ! ip link set "$VM_NET_TAP" up promisc on; do
info "Waiting for TAP to become available..."
sleep 2
done
if ! ip link set dev "$VM_NET_TAP" master dockerbridge; then
error "Failed to set IP link!" && return 1
fi
# Add internet connection to the VM
update-alternatives --set iptables /usr/sbin/iptables-legacy > /dev/null
update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy > /dev/null
exclude=$(getHostPorts "$HOST_PORTS")
if ! iptables -t nat -A POSTROUTING -o "$VM_NET_DEV" -j MASQUERADE; then
error "$tables" && return 1
fi
# shellcheck disable=SC2086
if ! iptables -t nat -A PREROUTING -i "$VM_NET_DEV" -d "$IP" -p tcp${exclude} -j DNAT --to "$VM_NET_IP"; then
error "Failed to configure IP tables!" && return 1
fi
if ! iptables -t nat -A PREROUTING -i "$VM_NET_DEV" -d "$IP" -p udp -j DNAT --to "$VM_NET_IP"; then
error "Failed to configure IP tables!" && return 1
fi
if (( KERNEL > 4 )); then
# Hack for guest VMs complaining about "bad udp checksums in 5 packets"
iptables -A POSTROUTING -t mangle -p udp --dport bootpc -j CHECKSUM --checksum-fill > /dev/null 2>&1 || true
fi
NET_OPTS="-netdev tap,id=hostnet0,ifname=$VM_NET_TAP"
if [ -c /dev/vhost-net ]; then
{ exec 40>>/dev/vhost-net; rc=$?; } 2>/dev/null || :
(( rc == 0 )) && NET_OPTS+=",vhost=on,vhostfd=40"
fi
NET_OPTS+=",script=no,downscript=no"
configureDNS || return 1
return 0
}
closeNetwork() {
# Shutdown nginx
nginx -s stop 2> /dev/null
fWait "nginx"
[[ "$NETWORK" == [Nn]* ]] && return 0
exec 30<&- || true
exec 40<&- || true
if [[ "$DHCP" == [Yy1]* ]]; then
ip link set "$VM_NET_TAP" down || true
ip link delete "$VM_NET_TAP" || true
else
local pid="/var/run/dnsmasq.pid"
[ -s "$pid" ] && pKill "$(<"$pid")"
[[ "${NETWORK,,}" == "user"* ]] && return 0
ip link set "$VM_NET_TAP" down promisc off || true
ip link delete "$VM_NET_TAP" || true
ip link set dockerbridge down || true
ip link delete dockerbridge || true
fi
return 0
}
checkOS() {
local name
local os=""
name=$(uname -a)
[[ "${name,,}" == *"darwin"* ]] && os="MacOS"
[[ "${name,,}" == *"microsoft"* ]] && os="Windows"
if [ -n "$os" ]; then
warn "you are using Docker Desktop for $os which does not support macvlan, please revert to bridge networking!"
fi
return 0
}
getInfo() {
if [ -z "$VM_NET_DEV" ]; then
# Give Kubernetes priority over the default interface
[ -d "/sys/class/net/net0" ] && VM_NET_DEV="net0"
[ -d "/sys/class/net/net1" ] && VM_NET_DEV="net1"
[ -d "/sys/class/net/net2" ] && VM_NET_DEV="net2"
[ -d "/sys/class/net/net3" ] && VM_NET_DEV="net3"
# Automaticly detect the default network interface
[ -z "$VM_NET_DEV" ] && VM_NET_DEV=$(awk '$2 == 00000000 { print $1 }' /proc/net/route)
[ -z "$VM_NET_DEV" ] && VM_NET_DEV="eth0"
fi
if [ ! -d "/sys/class/net/$VM_NET_DEV" ]; then
error "Network interface '$VM_NET_DEV' does not exist inside the container!"
error "$ADD_ERR -e \"VM_NET_DEV=NAME\" to specify another interface name." && exit 27
fi
if [ -z "$MAC" ]; then
local file="$STORAGE/$PROCESS.mac"
[ -s "$file" ] && MAC=$(<"$file")
if [ -z "$MAC" ]; then
# Generate MAC address based on Docker container ID in hostname
MAC=$(echo "$HOST" | md5sum | sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\).*$/02:\1:\2:\3:\4:\5/')
echo "${MAC^^}" > "$file"
fi
fi
VM_NET_MAC="${MAC^^}"
VM_NET_MAC="${VM_NET_MAC//-/:}"
if [[ ${#VM_NET_MAC} == 12 ]]; then
m="$VM_NET_MAC"
VM_NET_MAC="${m:0:2}:${m:2:2}:${m:4:2}:${m:6:2}:${m:8:2}:${m:10:2}"
fi
if [[ ${#VM_NET_MAC} != 17 ]]; then
error "Invalid MAC address: '$VM_NET_MAC', should be 12 or 17 digits long!" && exit 28
fi
GATEWAY=$(ip route list dev "$VM_NET_DEV" | awk ' /^default/ {print $3}')
IP=$(ip address show dev "$VM_NET_DEV" | grep inet | awk '/inet / { print $2 }' | cut -f1 -d/)
return 0
}
# ######################################
# Configure Network
# ######################################
if [[ "$NETWORK" == [Nn]* ]]; then
NET_OPTS=""
return 0
fi
getInfo
html "Initializing network..."
if [[ "$DEBUG" == [Yy1]* ]]; then
info "Host: $HOST IP: $IP Gateway: $GATEWAY Interface: $VM_NET_DEV MAC: $VM_NET_MAC"
[ -f /etc/resolv.conf ] && grep '^nameserver*' /etc/resolv.conf
echo
fi
if [[ "$DHCP" == [Yy1]* ]]; then
checkOS
if [[ "$IP" == "172."* ]]; then
warn "container IP starts with 172.* which is often a sign that you are not on a macvlan network (required for DHCP)!"
fi
# Configure for macvtap interface
configureDHCP || exit 20
else
if [[ "$IP" != "172."* ]] && [[ "$IP" != "10.8"* ]] && [[ "$IP" != "10.9"* ]]; then
checkOS
fi
if [[ "${NETWORK,,}" != "user"* ]]; then
# Configure for tap interface
if ! configureNAT; then
NETWORK="user"
warn "falling back to usermode networking! Performance will be bad and port forwarding will not work."
ip link set "$VM_NET_TAP" down promisc off &> null || true
ip link delete "$VM_NET_TAP" &> null || true
ip link set dockerbridge down &> null || true
ip link delete dockerbridge &> null || true
fi
fi
if [[ "${NETWORK,,}" == "user"* ]]; then
# Configure for usermode networking (slirp)
configureUser || exit 24
fi
fi
NET_OPTS+=" -device virtio-net-pci,romfile=,netdev=hostnet0,mac=$VM_NET_MAC,id=net0"
html "Initialized network successfully..."
return 0

View file

@ -24,7 +24,7 @@ if [[ "$CPU" == "Rockchip RK3588"* ]] && [[ "$CORES" == "8" ]]; then
[ -z "$CPU_PIN" ] && CPU_PIN="4,5,6,7"
fi
if [[ "${ARCH,,}" != "arm64" ]]; then
if [[ "${ARCH,,}" != "arm64" ]]; then
KVM="N"
warn "your CPU architecture is ${ARCH^^} and cannot provide KVM acceleration for ARM64 instructions, this will cause a major loss of performance."
fi
@ -34,10 +34,10 @@ if [[ "$KVM" != [Nn]* ]]; then
KVM_ERR=""
if [ ! -e /dev/kvm ]; then
KVM_ERR="(/dev/kvm is missing)"
KVM_ERR="(device file missing)"
else
if ! sh -c 'echo -n > /dev/kvm' &> /dev/null; then
KVM_ERR="(/dev/kvm is unwriteable)"
KVM_ERR="(no write access)"
fi
fi
@ -45,17 +45,9 @@ if [[ "$KVM" != [Nn]* ]]; then
KVM="N"
if [[ "$OSTYPE" =~ ^darwin ]]; then
warn "you are using macOS which has no KVM support, this will cause a major loss of performance."
else
kernel=$(uname -a)
case "${kernel,,}" in
*"microsoft"* )
error "Please bind '/dev/kvm' as a volume in the optional container settings when using Docker Desktop." ;;
*"synology"* )
error "Please make sure that Synology VMM (Virtual Machine Manager) is installed and that '/dev/kvm' is binded to this container." ;;
*)
error "KVM acceleration is not available $KVM_ERR, this will cause a major loss of performance."
error "See the FAQ for possible causes, or continue without it by adding KVM: \"N\" (not recommended)." ;;
esac
else
error "KVM acceleration not available $KVM_ERR, this will cause a major loss of performance."
error "See the FAQ on how to diagnose the cause, or continue without KVM by setting KVM=N (not recommended)."
[[ "$DEBUG" != [Yy1]* ]] && exit 88
fi
fi

38
src/progress.sh Normal file
View file

@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -Eeuo pipefail
escape () {
local s
s=${1//&/\&amp;}
s=${s//</\&lt;}
s=${s//>/\&gt;}
s=${s//'"'/\&quot;}
printf -- %s "$s"
return 0
}
file="$1"
total="$2"
body=$(escape "$3")
info="/run/shm/msg.html"
if [[ "$body" == *"..." ]]; then
body="<p class=\"loading\">${body/.../}</p>"
fi
while true
do
if [ -s "$file" ]; then
bytes=$(du -sb "$file" | cut -f1)
if (( bytes > 1000 )); then
if [ -z "$total" ] || [[ "$total" == "0" ]]; then
size=$(numfmt --to=iec --suffix=B "$bytes" | sed -r 's/([A-Z])/ \1/')
else
size="$(echo "$bytes" "$total" | awk '{printf "%.1f", $1 * 100 / $2}')"
size="$size%"
fi
echo "${body//(\[P\])/($size)}"> "$info"
fi
fi
sleep 1 & wait $!
done

250
src/reset.sh Normal file
View file

@ -0,0 +1,250 @@
#!/usr/bin/env bash
set -Eeuo pipefail
info () { printf "%b%s%b" "\E[1;34m \E[1;36m" "${1:-}" "\E[0m\n"; }
error () { printf "%b%s%b" "\E[1;31m " "ERROR: ${1:-}" "\E[0m\n" >&2; }
warn () { printf "%b%s%b" "\E[1;31m " "Warning: ${1:-}" "\E[0m\n" >&2; }
trap 'error "Status $? while: $BASH_COMMAND (line $LINENO/$BASH_LINENO)"' ERR
[ ! -f "/run/entry.sh" ] && error "Script must run inside Docker container!" && exit 11
[ "$(id -u)" -ne "0" ] && error "Script must be executed with root privileges." && exit 12
echo " Starting $APP for Docker v$(</run/version)..."
echo " For support visit $SUPPORT"
# Docker environment variables
: "${BOOT:=""}" # URL of the ISO file
: "${DEBUG:="N"}" # Disable debugging
: "${COMMIT:="N"}" # Commit to image
: "${MACHINE:="virt"}" # Machine selection
: "${ALLOCATE:=""}" # Preallocate diskspace
: "${ARGUMENTS:=""}" # Extra QEMU parameters
: "${CPU_CORES:="1"}" # Amount of CPU cores
: "${RAM_SIZE:="1G"}" # Maximum RAM amount
: "${RAM_CHECK:="Y"}" # Check available RAM
: "${DISK_SIZE:="16G"}" # Initial data disk size
: "${BOOT_MODE:=""}" # Boot system with UEFI
: "${BOOT_INDEX:="9"}" # Boot index of CD drive
: "${STORAGE:="/storage"}" # Storage folder location
# Helper variables
PROCESS="${APP,,}"
PROCESS="${PROCESS// /-}"
INFO="/run/shm/msg.html"
PAGE="/run/shm/index.html"
TEMPLATE="/var/www/index.html"
FOOTER1="$APP for Docker v$(</run/version)"
FOOTER2="<a href='$SUPPORT'>$SUPPORT</a>"
CPI=$(lscpu)
SYS=$(uname -r)
HOST=$(hostname -s)
KERNEL=$(echo "$SYS" | cut -b 1)
MINOR=$(echo "$SYS" | cut -d '.' -f2)
ARCH=$(dpkg --print-architecture)
CORES=$(grep -c '^processor' /proc/cpuinfo)
if ! grep -qi "socket(s)" <<< "$CPI"; then
SOCKETS=1
else
SOCKETS=$(echo "$CPI" | grep -m 1 -i 'socket(s)' | awk '{print $(2)}')
fi
if ! grep -qi "model name" <<< "$CPI"; then
CPU=""
else
CPU=$(echo "$CPI" | grep -m 1 -i 'model name' | cut -f 2 -d ":" | awk '{$1=$1}1' | sed 's# @.*##g' | sed s/"(R)"//g | sed 's/[^[:alnum:] ]\+/ /g' | sed 's/ */ /g')
fi
if [ -z "${CPU// /}" ] && grep -qi "model:" <<< "$CPI"; then
CPU=$(echo "$CPI" | grep -m 1 -i 'model:' | cut -f 2 -d ":" | awk '{$1=$1}1' | sed 's# @.*##g' | sed s/"(R)"//g | sed 's/[^[:alnum:] ]\+/ /g' | sed 's/ */ /g')
fi
CPU="${CPU// CPU/}"
CPU="${CPU// 16 Core/}"
CPU="${CPU// Processor/}"
CPU="${CPU// Quad core/}"
CPU="${CPU// with Radeon Graphics/}"
[ -z "${CPU// /}" ] && CPU="Unknown"
# Check system
if [ ! -d "/dev/shm" ]; then
error "Directory /dev/shm not found!" && exit 14
else
[ ! -d "/run/shm" ] && ln -s /dev/shm /run/shm
fi
# Check folder
if [[ "$COMMIT" != [Nn]* ]]; then
STORAGE="/local"
mkdir -p "$STORAGE"
else
if [ ! -d "$STORAGE" ]; then
error "Storage folder ($STORAGE) not found!" && exit 13
fi
fi
# Read memory
RAM_SPARE=500000000
RAM_AVAIL=$(free -b | grep -m 1 Mem: | awk '{print $7}')
RAM_TOTAL=$(free -b | grep -m 1 Mem: | awk '{print $2}')
RAM_SIZE=$(echo "${RAM_SIZE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
RAM_WANTED=$(numfmt --from=iec "$RAM_SIZE")
AVAIL_GB=$(( RAM_AVAIL/1073741824 ))
TOTAL_GB=$(( (RAM_TOTAL + 1073741823)/1073741824 ))
WANTED_GB=$(( (RAM_WANTED + 1073741823)/1073741824 ))
# Print system info
SYS="${SYS/-generic/}"
FS=$(stat -f -c %T "$STORAGE")
FS="${FS/UNKNOWN //}"
FS="${FS/ext2\/ext3/ext4}"
FS=$(echo "$FS" | sed 's/[)(]//g')
SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
SPACE_GB=$(( (SPACE + 1073741823)/1073741824 ))
echo " CPU: ${CPU} | RAM: $AVAIL_GB/$TOTAL_GB GB | DISK: $SPACE_GB GB (${FS}) | KERNEL: ${SYS}..."
echo
# Check compatibilty
if [[ "${FS,,}" == "ecryptfs" ]] || [[ "${FS,,}" == "tmpfs" ]]; then
DISK_IO="threads"
DISK_CACHE="writeback"
fi
# Check memory
if [[ "$RAM_CHECK" != [Nn]* ]] && (( (RAM_WANTED + RAM_SPARE) > RAM_AVAIL )); then
error "Your configured RAM_SIZE of $WANTED_GB GB is too high for the $AVAIL_GB GB of memory available, please set a lower value."
exit 17
fi
# Helper functions
isAlive() {
local pid=$1
if kill -0 "$pid" 2>/dev/null; then
return 0
fi
return 1
}
pKill() {
local pid=$1
{ kill -15 "$pid" || true; } 2>/dev/null
while isAlive "$pid"; do
sleep 0.2
done
return 0
}
fWait() {
local name=$1
while pgrep -f -l "$name" >/dev/null; do
sleep 0.2
done
return 0
}
fKill() {
local name=$1
{ pkill -f "$name" || true; } 2>/dev/null
fWait "$name"
return 0
}
escape () {
local s
s=${1//&/\&amp;}
s=${s//</\&lt;}
s=${s//>/\&gt;}
s=${s//'"'/\&quot;}
printf -- %s "$s"
return 0
}
html()
{
local title
local body
local script
local footer
title=$(escape "$APP")
title="<title>$title</title>"
footer=$(escape "$FOOTER1")
body=$(escape "$1")
if [[ "$body" == *"..." ]]; then
body="<p class=\"loading\">${body/.../}</p>"
fi
[ -n "${2:-}" ] && script="$2" || script=""
local HTML
HTML=$(<"$TEMPLATE")
HTML="${HTML/\[1\]/$title}"
HTML="${HTML/\[2\]/$script}"
HTML="${HTML/\[3\]/$body}"
HTML="${HTML/\[4\]/$footer}"
HTML="${HTML/\[5\]/$FOOTER2}"
echo "$HTML" > "$PAGE"
echo "$body" > "$INFO"
return 0
}
addPackage() {
local pkg=$1
local desc=$2
if apt-mark showinstall | grep -qx "$pkg"; then
return 0
fi
MSG="Installing $desc..."
info "$MSG" && html "$MSG"
DEBIAN_FRONTEND=noninteractive apt-get -qq update
DEBIAN_FRONTEND=noninteractive apt-get -qq --no-install-recommends -y install "$pkg" > /dev/null
return 0
}
hasDisk() {
[ -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
}
# Start webserver
cp -r /var/www/* /run/shm
html "Starting $APP for Docker..."
nginx -e stderr
return 0