mirror of
https://github.com/vdsm/virtual-dsm.git
synced 2025-11-07 02:23:42 +08:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41db7c1035 | ||
|
|
d71834a777 | ||
|
|
d078af0397 | ||
|
|
2827d1d375 | ||
|
|
898499a4e3 | ||
|
|
eb010cc215 | ||
|
|
84440d5159 | ||
|
|
ff3744ead9 | ||
|
|
d00fe4b3eb | ||
|
|
72a86a5d7f | ||
|
|
811ab622df | ||
|
|
e76b72cddf | ||
|
|
e9edacc9c3 | ||
|
|
a6694a6b29 | ||
|
|
59323cd375 | ||
|
|
6dc714e449 | ||
|
|
5d75a9b039 | ||
|
|
92b4cf5997 | ||
|
|
906e61b1b2 | ||
|
|
dab230f9d5 | ||
|
|
5e8bcda9fc | ||
|
|
d4bf83ae86 | ||
|
|
8244a48511 | ||
|
|
43ffa18a5f | ||
|
|
b131a32d0c | ||
|
|
c81787b837 | ||
|
|
f9df3c6db6 | ||
|
|
e383ec30e3 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -54,7 +54,7 @@ jobs:
|
|||||||
labels: |
|
labels: |
|
||||||
org.opencontainers.image.title=${{ vars.NAME }}
|
org.opencontainers.image.title=${{ vars.NAME }}
|
||||||
env:
|
env:
|
||||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||||
-
|
-
|
||||||
name: Set up Docker Buildx
|
name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM qemux/qemu-host:2.05 as builder
|
FROM qemux/qemu-host:2.05 AS builder
|
||||||
|
|
||||||
# FROM golang as builder
|
# FROM golang as builder
|
||||||
# WORKDIR /
|
# WORKDIR /
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ services:
|
|||||||
DISK_SIZE: "16G"
|
DISK_SIZE: "16G"
|
||||||
devices:
|
devices:
|
||||||
- /dev/kvm
|
- /dev/kvm
|
||||||
|
- /dev/net/tun
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: PersistentVolumeClaim
|
kind: PersistentVolumeClaim
|
||||||
metadata:
|
metadata:
|
||||||
name: dsm-pvc
|
name: dsm-pvc
|
||||||
spec:
|
spec:
|
||||||
accessModes:
|
accessModes:
|
||||||
- ReadWriteOnce
|
- ReadWriteOnce
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
storage: 16Gi
|
storage: 16Gi
|
||||||
@@ -16,44 +17,51 @@ metadata:
|
|||||||
labels:
|
labels:
|
||||||
name: dsm
|
name: dsm
|
||||||
spec:
|
spec:
|
||||||
terminationGracePeriodSeconds: 120 # the Kubernetes default is 30 seconds and it may be not enough
|
|
||||||
containers:
|
containers:
|
||||||
- name: dsm
|
- name: dsm
|
||||||
image: vdsm/virtual-dsm
|
image: vdsm/virtual-dsm
|
||||||
ports:
|
env:
|
||||||
- containerPort: 5000
|
- name: RAM_SIZE
|
||||||
protocol: TCP
|
value: "1G"
|
||||||
securityContext:
|
- name: CPU_CORES
|
||||||
privileged: true
|
value: "1"
|
||||||
env:
|
- name: DISK_SIZE
|
||||||
- name: RAM_SIZE
|
value: "16G"
|
||||||
value: 1G
|
ports:
|
||||||
- name: CPU_CORES
|
- containerPort: 5000
|
||||||
value: "1"
|
securityContext:
|
||||||
- name: DISK_SIZE
|
capabilities:
|
||||||
value: "16G" # Kubernetes uses Gi, but DSM uses GB
|
add:
|
||||||
volumeMounts:
|
- NET_ADMIN
|
||||||
- mountPath: /storage
|
privileged: true
|
||||||
name: storage
|
volumeMounts:
|
||||||
- mountPath: /dev/kvm
|
- mountPath: /storage
|
||||||
name: dev-kvm
|
name: storage
|
||||||
|
- mountPath: /dev/kvm
|
||||||
|
name: dev-kvm
|
||||||
|
- mountPath: /dev/net/tun
|
||||||
|
name: dev-tun
|
||||||
|
terminationGracePeriodSeconds: 120
|
||||||
volumes:
|
volumes:
|
||||||
- name: storage
|
- name: storage
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: dsm-pvc
|
claimName: dsm-pvc
|
||||||
- name: dev-kvm
|
- hostPath:
|
||||||
hostPath:
|
path: /dev/kvm
|
||||||
path: /dev/kvm
|
name: dev-kvm
|
||||||
|
- hostPath:
|
||||||
|
path: /dev/net/tun
|
||||||
|
type: CharDevice
|
||||||
|
name: dev-tun
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
metadata:
|
metadata:
|
||||||
name: dsm
|
name: dsm
|
||||||
spec:
|
spec:
|
||||||
type: NodePort
|
ports:
|
||||||
|
- name: tcp-5000
|
||||||
|
port: 5000
|
||||||
selector:
|
selector:
|
||||||
name: dsm
|
name: dsm
|
||||||
ports:
|
type: NodePort
|
||||||
- name: tcp-5000
|
|
||||||
port: 5000
|
|
||||||
targetPort: 5000
|
|
||||||
|
|||||||
31
readme.md
31
readme.md
@@ -33,6 +33,7 @@ services:
|
|||||||
DISK_SIZE: "16G"
|
DISK_SIZE: "16G"
|
||||||
devices:
|
devices:
|
||||||
- /dev/kvm
|
- /dev/kvm
|
||||||
|
- /dev/net/tun
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
ports:
|
ports:
|
||||||
@@ -45,15 +46,25 @@ services:
|
|||||||
Via Docker CLI:
|
Via Docker CLI:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -it --rm -p 5000:5000 --device=/dev/kvm --cap-add NET_ADMIN --stop-timeout 120 vdsm/virtual-dsm
|
docker run -it --rm -p 5000:5000 --device=/dev/kvm --device=/dev/net/tun --cap-add NET_ADMIN --stop-timeout 120 vdsm/virtual-dsm
|
||||||
```
|
```
|
||||||
|
|
||||||
Via Kubernetes:
|
Via Kubernetes:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
kubectl apply -f kubernetes.yml
|
kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/master/kubernetes.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Compatibility ⚙️
|
||||||
|
|
||||||
|
| **Product** | **Platform** | |
|
||||||
|
|---|---|---|
|
||||||
|
| Docker Engine | Linux| ✅ |
|
||||||
|
| Docker Desktop | Linux | ❌ |
|
||||||
|
| Docker Desktop | macOS | ❌ |
|
||||||
|
| Docker Desktop | Windows 11 | ✅ |
|
||||||
|
| Docker Desktop | Windows 10 | ❌ |
|
||||||
|
|
||||||
## FAQ 💬
|
## FAQ 💬
|
||||||
|
|
||||||
### How do I use it?
|
### How do I use it?
|
||||||
@@ -64,7 +75,7 @@ kubectl apply -f kubernetes.yml
|
|||||||
|
|
||||||
- Wait until DSM is ready, choose an username and password, and you will be taken to the desktop.
|
- Wait until DSM is ready, choose an username and password, and you will be taken to the desktop.
|
||||||
|
|
||||||
Enjoy your brand new machine, and don't forget to star this repo!
|
Enjoy your brand new NAS, and don't forget to star this repo!
|
||||||
|
|
||||||
### How do I change the storage location?
|
### How do I change the storage location?
|
||||||
|
|
||||||
@@ -147,24 +158,28 @@ kubectl apply -f kubernetes.yml
|
|||||||
|
|
||||||
### How do I verify if my system supports KVM?
|
### How do I verify if my system supports KVM?
|
||||||
|
|
||||||
To verify that your system supports KVM, run the following commands:
|
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
|
```bash
|
||||||
sudo apt install cpu-checker
|
sudo apt install cpu-checker
|
||||||
sudo kvm-ok
|
sudo kvm-ok
|
||||||
```
|
```
|
||||||
|
|
||||||
If you receive an error from `kvm-ok` indicating that KVM acceleration can't be used, please check whether:
|
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.
|
- the virtualization extensions (`Intel VT-x` or `AMD SVM`) are enabled in your BIOS.
|
||||||
|
|
||||||
- you are running an operating system that supports them, like Linux or Windows 11 (macOS and Windows 10 do not unfortunately).
|
|
||||||
|
|
||||||
- you enabled "nested virtualization" if you are running the container inside a virtual machine.
|
- 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 VPS's.
|
- you are not using a cloud provider, as most of them do not allow nested virtualization for their VPS's.
|
||||||
|
|
||||||
If you didn't receive any error from `kvm-ok` at all, but the container still complains that `/dev/kvm` is missing, it might help to add `privileged: true` to your compose file (or `--privileged` to your `run` 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:
|
||||||
|
|
||||||
|
- you are not using "Docker Desktop for Linux" as it does not support KVM, instead make use of Docker Engine directly.
|
||||||
|
|
||||||
|
- 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?
|
### How do I assign an individual IP address to the container?
|
||||||
|
|
||||||
|
|||||||
22
src/disk.sh
22
src/disk.sh
@@ -3,9 +3,9 @@ set -Eeuo pipefail
|
|||||||
|
|
||||||
# Docker environment variables
|
# Docker environment variables
|
||||||
|
|
||||||
: "${DISK_IO:="native"}" # I/O Mode, can be set to 'native', 'threads' or 'io_turing'
|
: "${DISK_IO:="native"}" # I/O Mode, can be set to 'native', 'threads' or 'io_uring'
|
||||||
: "${DISK_FMT:="raw"}" # Disk file format, 'raw' by default for best performance
|
: "${DISK_FMT:="raw"}" # Disk file format, 'raw' by default for best performance
|
||||||
: "${DISK_TYPE:=""}" # Device type to be used, choose "ide", "usb", "blk" or "scsi"
|
: "${DISK_TYPE:=""}" # Device type to be used, "sata", "nvme", "blk" or "scsi"
|
||||||
: "${DISK_FLAGS:=""}" # Specifies the options for use with the qcow2 disk format
|
: "${DISK_FLAGS:=""}" # Specifies the options for use with the qcow2 disk format
|
||||||
: "${DISK_CACHE:="none"}" # Caching mode, can be set to 'writeback' for better performance
|
: "${DISK_CACHE:="none"}" # Caching mode, can be set to 'writeback' for better performance
|
||||||
: "${DISK_DISCARD:="on"}" # Controls whether unmap (TRIM) commands are passed to the host.
|
: "${DISK_DISCARD:="on"}" # Controls whether unmap (TRIM) commands are passed to the host.
|
||||||
@@ -369,6 +369,7 @@ createDevice () {
|
|||||||
local result=" -drive file=$DISK_FILE,id=$DISK_ID,format=$DISK_FMT,cache=$DISK_CACHE,aio=$DISK_IO,discard=$DISK_DISCARD,detect-zeroes=on"
|
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
|
case "${DISK_TYPE,,}" in
|
||||||
|
"none" ) ;;
|
||||||
"auto" )
|
"auto" )
|
||||||
echo "$result"
|
echo "$result"
|
||||||
;;
|
;;
|
||||||
@@ -377,7 +378,12 @@ createDevice () {
|
|||||||
-device usb-storage,drive=${DISK_ID}${index}"
|
-device usb-storage,drive=${DISK_ID}${index}"
|
||||||
echo "$result"
|
echo "$result"
|
||||||
;;
|
;;
|
||||||
"ide" )
|
"nvme" )
|
||||||
|
result+=",if=none \
|
||||||
|
-device nvme,drive=${DISK_ID}${index},serial=deadbeaf${DISK_INDEX}"
|
||||||
|
echo "$result"
|
||||||
|
;;
|
||||||
|
"ide" | "sata" )
|
||||||
result+=",if=none \
|
result+=",if=none \
|
||||||
-device ich9-ahci,id=ahci${DISK_INDEX},addr=$DISK_ADDRESS \
|
-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}"
|
-device ide-hd,drive=${DISK_ID},bus=ahci$DISK_INDEX.0,rotation_rate=$DISK_ROTATION${index}"
|
||||||
@@ -385,7 +391,7 @@ createDevice () {
|
|||||||
;;
|
;;
|
||||||
"blk" | "virtio-blk" )
|
"blk" | "virtio-blk" )
|
||||||
result+=",if=none \
|
result+=",if=none \
|
||||||
-device virtio-blk-pci,drive=${DISK_ID},scsi=off,bus=pcie.0,addr=$DISK_ADDRESS,iothread=io2${index}"
|
-device virtio-blk-pci,drive=${DISK_ID},bus=pcie.0,addr=$DISK_ADDRESS,iothread=io2${index}"
|
||||||
echo "$result"
|
echo "$result"
|
||||||
;;
|
;;
|
||||||
"scsi" | "virtio-scsi" )
|
"scsi" | "virtio-scsi" )
|
||||||
@@ -410,7 +416,7 @@ addDisk () {
|
|||||||
local DISK_FMT=$7
|
local DISK_FMT=$7
|
||||||
local DISK_IO=$8
|
local DISK_IO=$8
|
||||||
local DISK_CACHE=$9
|
local DISK_CACHE=$9
|
||||||
local DISK_EXT DIR DATA_SIZE FS PREV_FMT PREV_EXT CUR_SIZE OPTS
|
local DISK_EXT DIR DATA_SIZE FS PREV_FMT PREV_EXT CUR_SIZE
|
||||||
|
|
||||||
DISK_EXT=$(fmt2ext "$DISK_FMT")
|
DISK_EXT=$(fmt2ext "$DISK_FMT")
|
||||||
local DISK_FILE="$DISK_BASE.$DISK_EXT"
|
local DISK_FILE="$DISK_BASE.$DISK_EXT"
|
||||||
@@ -424,11 +430,11 @@ addDisk () {
|
|||||||
DATA_SIZE=$(numfmt --from=iec "$DISK_SPACE")
|
DATA_SIZE=$(numfmt --from=iec "$DISK_SPACE")
|
||||||
|
|
||||||
if (( DATA_SIZE < 1 )); then
|
if (( DATA_SIZE < 1 )); then
|
||||||
error "Invalid value for ${DISK_DESC^^}_SIZE: $DISK_SPACE" && exit 73
|
error "Invalid value for ${DISK_DESC^^}_SIZE: $DISK_SPACE" && exit 73
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if (( DATA_SIZE < 6442450944 )); then
|
if (( DATA_SIZE < 6442450944 )); then
|
||||||
error "Please increase ${DISK_DESC^^}_SIZE to at least 6 GB." && exit 73
|
error "Please increase ${DISK_DESC^^}_SIZE to at least 6 GB." && exit 73
|
||||||
fi
|
fi
|
||||||
|
|
||||||
FS=$(stat -f -c %T "$DIR")
|
FS=$(stat -f -c %T "$DIR")
|
||||||
@@ -495,7 +501,7 @@ html "Initializing disks..."
|
|||||||
[ -z "${DISK_NAME:-}" ] && DISK_NAME="data"
|
[ -z "${DISK_NAME:-}" ] && DISK_NAME="data"
|
||||||
|
|
||||||
case "${DISK_TYPE,,}" in
|
case "${DISK_TYPE,,}" in
|
||||||
"ide" | "usb" | "scsi" | "blk" | "auto" ) ;;
|
"ide" | "sata" | "nvme" | "usb" | "scsi" | "blk" | "auto" | "none" ) ;;
|
||||||
* ) error "Invalid DISK_TYPE specified, value \"$DISK_TYPE\" is not recognized!" && exit 80 ;;
|
* ) error "Invalid DISK_TYPE specified, value \"$DISK_TYPE\" is not recognized!" && exit 80 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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
|
||||||
|
: "${RENDERNODE:="/dev/dri/renderD128"}" # Render node
|
||||||
|
|
||||||
if [[ "$GPU" != [Yy1]* ]] || [[ "$ARCH" != "amd64" ]]; then
|
if [[ "$GPU" != [Yy1]* ]] || [[ "$ARCH" != "amd64" ]]; then
|
||||||
|
|
||||||
@@ -15,20 +16,24 @@ if [[ "$GPU" != [Yy1]* ]] || [[ "$ARCH" != "amd64" ]]; then
|
|||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
DISPLAY_OPTS="-display egl-headless,rendernode=/dev/dri/renderD128"
|
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
|
||||||
|
|
||||||
if [ ! -c /dev/dri/card0 ]; then
|
# Extract the card number from the render node
|
||||||
if mknod /dev/dri/card0 c 226 0; then
|
CARD_NUMBER=$(echo "$RENDERNODE" | grep -oP '(?<=renderD)\d+')
|
||||||
chmod 666 /dev/dri/card0
|
CARD_DEVICE="/dev/dri/card$((CARD_NUMBER - 128))"
|
||||||
|
|
||||||
|
if [ ! -c "$CARD_DEVICE" ]; then
|
||||||
|
if mknod "$CARD_DEVICE" c 226 $((CARD_NUMBER - 128)); then
|
||||||
|
chmod 666 "$CARD_DEVICE"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -c /dev/dri/renderD128 ]; then
|
if [ ! -c "$RENDERNODE" ]; then
|
||||||
if mknod /dev/dri/renderD128 c 226 128; then
|
if mknod "$RENDERNODE" c 226 "$CARD_NUMBER"; then
|
||||||
chmod 666 /dev/dri/renderD128
|
chmod 666 "$RENDERNODE"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ if [ -z "$DL" ]; then
|
|||||||
[[ "${COUNTRY^^}" == "CN" ]] && DL="$DL_CHINA" || DL="$DL_GLOBAL"
|
[[ "${COUNTRY^^}" == "CN" ]] && DL="$DL_CHINA" || DL="$DL_GLOBAL"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[ -z "$URL" ] && URL="$DL/release/7.2.1/69057-1/DSM_VirtualDSM_69057.pat"
|
[ -z "$URL" ] && URL="$DL/release/7.2.2/72806/DSM_VirtualDSM_72806.pat"
|
||||||
|
|
||||||
BASE=$(basename "${URL%%\?*}" .pat)
|
BASE=$(basename "${URL%%\?*}" .pat)
|
||||||
: "${BASE//+/ }"; printf -v BASE '%b' "${_//%/\\x}"
|
: "${BASE//+/ }"; printf -v BASE '%b' "${_//%/\\x}"
|
||||||
@@ -99,10 +99,10 @@ SPACE=$(df --output=avail -B 1 / | tail -n 1)
|
|||||||
SPACE_MB=$(( (SPACE + 1048575)/1048576 ))
|
SPACE_MB=$(( (SPACE + 1048575)/1048576 ))
|
||||||
(( ROOT_SPACE > SPACE )) && error "Not enough free space inside the container, have $SPACE_MB MB available but need at least 500 MB." && exit 96
|
(( ROOT_SPACE > SPACE )) && error "Not enough free space inside the container, have $SPACE_MB MB available but need at least 500 MB." && exit 96
|
||||||
|
|
||||||
MIN_SPACE=8589934592
|
MIN_SPACE=15032385536
|
||||||
SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
|
SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
|
||||||
SPACE_GB=$(( (SPACE + 1073741823)/1073741824 ))
|
SPACE_GB=$(( (SPACE + 1073741823)/1073741824 ))
|
||||||
(( MIN_SPACE > SPACE )) && error "Not enough free space for installation in $STORAGE, have $SPACE_GB GB available but need at least 8 GB." && exit 94
|
(( MIN_SPACE > SPACE )) && error "Not enough free space for installation in $STORAGE, have $SPACE_GB GB available but need at least 14 GB." && exit 94
|
||||||
|
|
||||||
# Check if output is to interactive TTY
|
# Check if output is to interactive TTY
|
||||||
if [ -t 1 ]; then
|
if [ -t 1 ]; then
|
||||||
@@ -170,7 +170,7 @@ if [ ! -s "$RDC" ]; then
|
|||||||
|
|
||||||
html "$MSG..."
|
html "$MSG..."
|
||||||
/run/progress.sh "$PAT" "$SIZE" "$MSG ([P])..." &
|
/run/progress.sh "$PAT" "$SIZE" "$MSG ([P])..." &
|
||||||
{ wget "$LOC" -O "$PAT" -q --no-check-certificate --timeout=10 --show-progress "$PROGRESS"; rc=$?; } || :
|
{ wget "$LOC" -O "$PAT" -q --no-check-certificate --timeout=10 --no-http-keep-alive --show-progress "$PROGRESS"; rc=$?; } || :
|
||||||
|
|
||||||
fKill "progress.sh"
|
fKill "progress.sh"
|
||||||
|
|
||||||
@@ -245,12 +245,13 @@ if [[ "$URL" == "file://"* ]]; then
|
|||||||
else
|
else
|
||||||
|
|
||||||
SIZE=0
|
SIZE=0
|
||||||
|
[[ "${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])..." &
|
/run/progress.sh "$PAT" "$SIZE" "$MSG ([P])..." &
|
||||||
|
|
||||||
{ wget "$URL" -O "$PAT" -q --no-check-certificate --timeout=10 --show-progress "$PROGRESS"; rc=$?; } || :
|
{ wget "$URL" -O "$PAT" -q --no-check-certificate --timeout=10 --no-http-keep-alive --show-progress "$PROGRESS"; rc=$?; } || :
|
||||||
|
|
||||||
fKill "progress.sh"
|
fKill "progress.sh"
|
||||||
|
|
||||||
@@ -307,12 +308,12 @@ SYSTEM="$STORAGE/$BASE.system.img"
|
|||||||
rm -f "$SYSTEM"
|
rm -f "$SYSTEM"
|
||||||
|
|
||||||
# Check free diskspace
|
# Check free diskspace
|
||||||
SYSTEM_SIZE=4954537983
|
SYSTEM_SIZE=10738466816
|
||||||
SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
|
SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
|
||||||
SPACE_MB=$(( (SPACE + 1048575)/1048576 ))
|
SPACE_MB=$(( (SPACE + 1048575)/1048576 ))
|
||||||
|
|
||||||
if (( SYSTEM_SIZE > SPACE )); then
|
if (( SYSTEM_SIZE > SPACE )); then
|
||||||
error "Not enough free space in $STORAGE to create a 5 GB system disk, have only $SPACE_MB MB available." && exit 97
|
error "Not enough free space in $STORAGE to create a 10 GB system disk, have only $SPACE_MB MB available." && exit 97
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! touch "$SYSTEM"; then
|
if ! touch "$SYSTEM"; then
|
||||||
@@ -342,8 +343,8 @@ PART="$TMP/partition.fdisk"
|
|||||||
echo "unit: sectors"
|
echo "unit: sectors"
|
||||||
echo "sector-size: 512"
|
echo "sector-size: 512"
|
||||||
echo ""
|
echo ""
|
||||||
echo "${SYSTEM}1 : start= 2048, size= 4980480, type=83"
|
echo "${SYSTEM}1 : start= 2048, size= 16777216, type=83"
|
||||||
echo "${SYSTEM}2 : start= 4982528, size= 4194304, type=82"
|
echo "${SYSTEM}2 : start= 16779264, size= 4194304, type=82"
|
||||||
} > "$PART"
|
} > "$PART"
|
||||||
|
|
||||||
sfdisk -q "$SYSTEM" < "$PART"
|
sfdisk -q "$SYSTEM" < "$PART"
|
||||||
@@ -374,8 +375,8 @@ if [ -s "$IDB.txz" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
LABEL="1.44.1-42218"
|
LABEL="1.44.1-42218"
|
||||||
OFFSET="1048576" # 2048 * 512
|
OFFSET="1048576" # 2048 * 512
|
||||||
NUMBLOCKS="622560" # (4980480 * 512) / 4096
|
NUMBLOCKS="2097152" # (16777216 * 512) / 4096
|
||||||
MSG="Installing system partition..."
|
MSG="Installing system partition..."
|
||||||
|
|
||||||
if [[ "$ROOT" != [Nn]* ]]; then
|
if [[ "$ROOT" != [Nn]* ]]; then
|
||||||
@@ -407,5 +408,7 @@ fi
|
|||||||
mv -f "$BOOT" "$STORAGE/$BASE.boot.img"
|
mv -f "$BOOT" "$STORAGE/$BASE.boot.img"
|
||||||
rm -rf "$TMP"
|
rm -rf "$TMP"
|
||||||
|
|
||||||
html "Installation finished successfully..."
|
html "Booting DSM instance..."
|
||||||
|
sleep 1.2
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ set -Eeuo pipefail
|
|||||||
: "${MAC:=""}"
|
: "${MAC:=""}"
|
||||||
: "${DHCP:="N"}"
|
: "${DHCP:="N"}"
|
||||||
: "${NETWORK:="Y"}"
|
: "${NETWORK:="Y"}"
|
||||||
: "${HOST_PORTS:=""}"
|
|
||||||
: "${USER_PORTS:=""}"
|
: "${USER_PORTS:=""}"
|
||||||
|
: "${HOST_PORTS:=""}"
|
||||||
|
: "${ADAPTER:="virtio-net-pci"}"
|
||||||
|
|
||||||
: "${VM_NET_DEV:=""}"
|
: "${VM_NET_DEV:=""}"
|
||||||
: "${VM_NET_TAP:="dsm"}"
|
: "${VM_NET_TAP:="dsm"}"
|
||||||
@@ -35,12 +36,27 @@ configureDHCP() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Create a macvtap network for the VM guest
|
# 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=$?; } || :
|
{ msg=$(ip link add link "$VM_NET_DEV" name "$VM_NET_TAP" address "$VM_NET_MAC" type macvtap mode bridge 2>&1); rc=$?; } || :
|
||||||
|
|
||||||
if (( rc != 0 )); then
|
case "$msg" in
|
||||||
error "Cannot create macvtap interface. Please make sure that the network type is 'macvlan' and not 'ipvlan',"
|
"RTNETLINK answers: File exists"* )
|
||||||
error "that your kernel is recent (>4) and supports it, and that the container has the NET_ADMIN capability set." && return 1
|
while ! ip link add link "$VM_NET_DEV" name "$VM_NET_TAP" address "$VM_NET_MAC" type macvtap mode bridge; do
|
||||||
fi
|
info "Waiting for macvtap interface to become available.."
|
||||||
|
sleep 5
|
||||||
|
done ;;
|
||||||
|
"RTNETLINK answers: Invalid argument"* )
|
||||||
|
error "Cannot create macvtap interface. Please make sure that the network type of the container is 'macvlan' and not 'ipvlan'."
|
||||||
|
return 1 ;;
|
||||||
|
"RTNETLINK answers: Operation not permitted"* )
|
||||||
|
error "No permission to create macvtap interface. Please make sure that your host kernel supports it and that the NET_ADMIN capability is set."
|
||||||
|
return 1 ;;
|
||||||
|
*)
|
||||||
|
[ -n "$msg" ] && echo "$msg" >&2
|
||||||
|
if (( rc != 0 )); then
|
||||||
|
error "Cannot create macvtap interface."
|
||||||
|
return 1
|
||||||
|
fi ;;
|
||||||
|
esac
|
||||||
|
|
||||||
while ! ip link set "$VM_NET_TAP" up; do
|
while ! ip link set "$VM_NET_TAP" up; do
|
||||||
info "Waiting for MAC address $VM_NET_MAC to become available..."
|
info "Waiting for MAC address $VM_NET_MAC to become available..."
|
||||||
@@ -112,7 +128,7 @@ getUserPorts() {
|
|||||||
|
|
||||||
[ -z "$list" ] && list="$ssh,$dsm" || list+=",$ssh,$dsm"
|
[ -z "$list" ] && list="$ssh,$dsm" || list+=",$ssh,$dsm"
|
||||||
|
|
||||||
list="${list/,/ }"
|
list="${list//,/ }"
|
||||||
list="${list## }"
|
list="${list## }"
|
||||||
list="${list%% }"
|
list="${list%% }"
|
||||||
|
|
||||||
@@ -152,6 +168,9 @@ configureUser() {
|
|||||||
|
|
||||||
configureNAT() {
|
configureNAT() {
|
||||||
|
|
||||||
|
local tuntap="TUN device is missing. $ADD_ERR --device /dev/net/tun"
|
||||||
|
local tables="The 'ip_tables' kernel module is not loaded. Try this command: sudo modprobe ip_tables iptable_nat"
|
||||||
|
|
||||||
# Create the necessary file structure for /dev/net/tun
|
# Create the necessary file structure for /dev/net/tun
|
||||||
if [ ! -c /dev/net/tun ]; then
|
if [ ! -c /dev/net/tun ]; then
|
||||||
[ ! -d /dev/net ] && mkdir -m 755 /dev/net
|
[ ! -d /dev/net ] && mkdir -m 755 /dev/net
|
||||||
@@ -161,7 +180,7 @@ configureNAT() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -c /dev/net/tun ]; then
|
if [ ! -c /dev/net/tun ]; then
|
||||||
error "TUN device missing. $ADD_ERR --device /dev/net/tun --cap-add NET_ADMIN" && return 1
|
error "$tuntap" && return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check port forwarding flag
|
# Check port forwarding flag
|
||||||
@@ -172,9 +191,6 @@ configureNAT() {
|
|||||||
fi
|
fi
|
||||||
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
|
# Create a bridge with a static IP for the VM guest
|
||||||
|
|
||||||
{ ip link add dev dockerbridge type bridge ; rc=$?; } || :
|
{ ip link add dev dockerbridge type bridge ; rc=$?; } || :
|
||||||
@@ -239,7 +255,7 @@ configureNAT() {
|
|||||||
|
|
||||||
NET_OPTS+=",script=no,downscript=no"
|
NET_OPTS+=",script=no,downscript=no"
|
||||||
|
|
||||||
! configureDNS && return 1
|
configureDNS || return 1
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -286,13 +302,19 @@ checkOS() {
|
|||||||
|
|
||||||
local name
|
local name
|
||||||
local os=""
|
local os=""
|
||||||
|
local if="macvlan"
|
||||||
name=$(uname -a)
|
name=$(uname -a)
|
||||||
|
|
||||||
[[ "${name,,}" == *"darwin"* ]] && os="MacOS"
|
[[ "${name,,}" == *"darwin"* ]] && os="Docker Desktop for macOS"
|
||||||
[[ "${name,,}" == *"microsoft"* ]] && os="Windows"
|
[[ "${name,,}" == *"microsoft"* ]] && os="Docker Desktop for Windows"
|
||||||
|
|
||||||
|
if [[ "$DHCP" == [Yy1]* ]]; then
|
||||||
|
if="macvtap"
|
||||||
|
[[ "${name,,}" == *"synology"* ]] && os="Synology Container Manager"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -n "$os" ]; then
|
if [ -n "$os" ]; then
|
||||||
warn "you are using Docker Desktop for $os which does not support macvlan, please revert to bridge networking!"
|
warn "you are using $os which does not support $if, please revert to bridge networking!"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
@@ -376,7 +398,7 @@ if [[ "$DHCP" == [Yy1]* ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Configure for macvtap interface
|
# Configure for macvtap interface
|
||||||
! configureDHCP && exit 20
|
configureDHCP || exit 20
|
||||||
|
|
||||||
MSG="Booting DSM instance..."
|
MSG="Booting DSM instance..."
|
||||||
html "$MSG"
|
html "$MSG"
|
||||||
@@ -397,7 +419,7 @@ else
|
|||||||
if ! configureNAT; then
|
if ! configureNAT; then
|
||||||
|
|
||||||
NETWORK="user"
|
NETWORK="user"
|
||||||
warn "falling back to usermode networking! Performance will be bad and port forwarding will not work."
|
warn "falling back to usermode networking! Performance will be bad and port mapping will not work."
|
||||||
|
|
||||||
ip link set "$VM_NET_TAP" down promisc off &> null || true
|
ip link set "$VM_NET_TAP" down promisc off &> null || true
|
||||||
ip link delete "$VM_NET_TAP" &> null || true
|
ip link delete "$VM_NET_TAP" &> null || true
|
||||||
@@ -412,12 +434,12 @@ else
|
|||||||
if [[ "${NETWORK,,}" == "user"* ]]; then
|
if [[ "${NETWORK,,}" == "user"* ]]; then
|
||||||
|
|
||||||
# Configure for usermode networking (slirp)
|
# Configure for usermode networking (slirp)
|
||||||
! configureUser && exit 24
|
configureUser || exit 24
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
NET_OPTS+=" -device virtio-net-pci,romfile=,netdev=hostnet0,mac=$VM_NET_MAC,id=net0"
|
NET_OPTS+=" -device $ADAPTER,romfile=,netdev=hostnet0,mac=$VM_NET_MAC,id=net0"
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
12
src/proc.sh
12
src/proc.sh
@@ -34,15 +34,11 @@ if [[ "$KVM" != [Nn]* ]]; then
|
|||||||
if [ -n "$KVM_ERR" ]; then
|
if [ -n "$KVM_ERR" ]; then
|
||||||
KVM="N"
|
KVM="N"
|
||||||
if [[ "$OSTYPE" =~ ^darwin ]]; then
|
if [[ "$OSTYPE" =~ ^darwin ]]; then
|
||||||
warn "you are using MacOS which has no KVM support, this will cause a major loss of performance."
|
warn "you are using macOS which has no KVM support, this will cause a major loss of performance."
|
||||||
else
|
else
|
||||||
if grep -qi Microsoft /proc/version; then
|
error "KVM acceleration not available $KVM_ERR, this will cause a major loss of performance."
|
||||||
warn "you are using Windows 10 which has no KVM support, 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)."
|
||||||
else
|
[[ "$DEBUG" != [Yy1]* ]] && exit 88
|
||||||
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
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
53
src/reset.sh
53
src/reset.sh
@@ -15,23 +15,24 @@ echo "❯ For support visit $SUPPORT"
|
|||||||
|
|
||||||
# Docker environment variables
|
# Docker environment variables
|
||||||
|
|
||||||
: "${TZ:=""}" # System local timezone
|
: "${TZ:=""}" # System local timezone
|
||||||
: "${DEBUG:="N"}" # Disable debugging mode
|
: "${DEBUG:="N"}" # Disable debugging mode
|
||||||
: "${COUNTRY:=""}" # Country code for mirror
|
: "${COMMIT:="N"}" # Commit to local image
|
||||||
: "${CONSOLE:="N"}" # Disable console mode
|
: "${COUNTRY:=""}" # Country code for mirror
|
||||||
: "${ALLOCATE:=""}" # Preallocate diskspace
|
: "${CONSOLE:="N"}" # Disable console mode
|
||||||
: "${ARGUMENTS:=""}" # Extra QEMU parameters
|
: "${ALLOCATE:=""}" # Preallocate diskspace
|
||||||
: "${CPU_CORES:="1"}" # Amount of CPU cores
|
: "${ARGUMENTS:=""}" # Extra QEMU parameters
|
||||||
: "${RAM_SIZE:="1G"}" # Maximum RAM amount
|
: "${CPU_CORES:="1"}" # Amount of CPU cores
|
||||||
: "${RAM_CHECK:="Y"}" # Check available RAM
|
: "${RAM_SIZE:="1G"}" # Maximum RAM amount
|
||||||
: "${DISK_SIZE:="16G"}" # Initial data disk size
|
: "${RAM_CHECK:="Y"}" # Check available RAM
|
||||||
|
: "${DISK_SIZE:="16G"}" # Initial data disk size
|
||||||
|
: "${STORAGE:="/storage"}" # Storage folder location
|
||||||
|
|
||||||
# Helper variables
|
# Helper variables
|
||||||
|
|
||||||
PROCESS="${APP,,}"
|
PROCESS="${APP,,}"
|
||||||
PROCESS="${PROCESS// /-}"
|
PROCESS="${PROCESS// /-}"
|
||||||
|
|
||||||
STORAGE="/storage"
|
|
||||||
INFO="/run/shm/msg.html"
|
INFO="/run/shm/msg.html"
|
||||||
PAGE="/run/shm/index.html"
|
PAGE="/run/shm/index.html"
|
||||||
TEMPLATE="/var/www/index.html"
|
TEMPLATE="/var/www/index.html"
|
||||||
@@ -53,12 +54,25 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if ! grep -qi "model name" <<< "$CPI"; then
|
if ! grep -qi "model name" <<< "$CPI"; then
|
||||||
CPU="Unknown"
|
CPU=""
|
||||||
else
|
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')
|
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')
|
||||||
CPU="${CPU// with Radeon Graphics/}"
|
|
||||||
fi
|
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// 32 Core/}"
|
||||||
|
CPU="${CPU// 64 Core/}"
|
||||||
|
CPU="${CPU// Processor/}"
|
||||||
|
CPU="${CPU// Quad core/}"
|
||||||
|
CPU="${CPU// Core TM/ Core}"
|
||||||
|
CPU="${CPU// with Radeon Graphics/}"
|
||||||
|
[ -z "${CPU// /}" ] && CPU="Unknown"
|
||||||
|
|
||||||
# Check system
|
# Check system
|
||||||
|
|
||||||
if [ ! -d "/dev/shm" ]; then
|
if [ ! -d "/dev/shm" ]; then
|
||||||
@@ -69,8 +83,13 @@ fi
|
|||||||
|
|
||||||
# Check folder
|
# Check folder
|
||||||
|
|
||||||
if [ ! -d "$STORAGE" ]; then
|
if [[ "$COMMIT" != [Nn]* ]]; then
|
||||||
error "Storage folder ($STORAGE) not found!" && exit 13
|
STORAGE="/local"
|
||||||
|
mkdir -p "$STORAGE"
|
||||||
|
else
|
||||||
|
if [ ! -d "$STORAGE" ]; then
|
||||||
|
error "Storage folder ($STORAGE) not found!" && exit 13
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check filesystem
|
# Check filesystem
|
||||||
@@ -93,11 +112,13 @@ WANTED_GB=$(( (RAM_WANTED + 1073741823)/1073741824 ))
|
|||||||
|
|
||||||
# Print system info
|
# Print system info
|
||||||
SYS="${SYS/-generic/}"
|
SYS="${SYS/-generic/}"
|
||||||
|
FS="${FS/UNKNOWN //}"
|
||||||
FS="${FS/ext2\/ext3/ext4}"
|
FS="${FS/ext2\/ext3/ext4}"
|
||||||
|
FS=$(echo "$FS" | sed 's/[)(]//g')
|
||||||
SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
|
SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
|
||||||
SPACE_GB=$(( (SPACE + 1073741823)/1073741824 ))
|
SPACE_GB=$(( (SPACE + 1073741823)/1073741824 ))
|
||||||
|
|
||||||
echo "❯ CPU: ${CPU} | RAM: $AVAIL_GB/$TOTAL_GB GB | DISK: $SPACE_GB GB (${FS}) | HOST: ${SYS}..."
|
echo "❯ CPU: ${CPU} | RAM: $AVAIL_GB/$TOTAL_GB GB | DISK: $SPACE_GB GB (${FS}) | KERNEL: ${SYS}..."
|
||||||
echo
|
echo
|
||||||
|
|
||||||
# Check memory
|
# Check memory
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
[1]
|
[1]
|
||||||
<meta http-equiv="Cache-Control" content="no-cache" />
|
<meta http-equiv="Cache-Control" content="no-cache" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="stylesheet" type="text/css" href="/css/style.css" />
|
<link rel="stylesheet" type="text/css" href="css/style.css" />
|
||||||
<link rel="icon" href="/img/favicon.svg" type="image/x-icon">
|
<link rel="icon" href="img/favicon.svg" type="image/x-icon">
|
||||||
[2]
|
[2]
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
[5]
|
[5]
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript" src="/js/script.js"></script>
|
<script type="text/javascript" src="js/script.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ var interval = 1000;
|
|||||||
|
|
||||||
function getInfo() {
|
function getInfo() {
|
||||||
|
|
||||||
var url = "/msg.html";
|
var url = "msg.html";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user