├── .devcontainer.json
├── .dockerignore
├── .github
├── ISSUE_TEMPLATE
│ ├── 1-issue.yml
│ ├── 2-feature.yml
│ ├── 3-bug.yml
│ ├── 4-question.yml
│ └── config.yml
├── dependabot.yml
├── renovate.json
├── screen.jpg
└── workflows
│ ├── build.yml
│ ├── check.yml
│ ├── hub.yml
│ └── test.yml
├── .gitignore
├── Dockerfile
├── compose.yml
├── kubernetes.yml
├── license.md
├── readme.md
├── src
├── check.sh
├── config.sh
├── disk.sh
├── display.sh
├── entry.sh
├── install.sh
├── network.sh
├── power.sh
├── print.sh
├── proc.sh
├── progress.sh
├── reset.sh
├── serial.sh
└── utils.sh
└── web
├── conf
└── nginx.conf
├── css
└── style.css
├── img
└── favicon.svg
├── index.html
└── js
└── script.js
/.devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dsm",
3 | "service": "dsm",
4 | "forwardPorts": [5000],
5 | "dockerComposeFile": "compose.yml"
6 | }
7 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .dockerignore
2 | .git
3 | .github
4 | .gitignore
5 | .gitlab-ci.yml
6 | .gitmodules
7 | Dockerfile
8 | Dockerfile.archive
9 | compose.yml
10 | compose.yaml
11 | docker-compose.yml
12 | docker-compose.yaml
13 |
14 | *.md
15 |
16 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/1-issue.yml:
--------------------------------------------------------------------------------
1 | name: "\U0001F6A8 Technical issue"
2 | description: When you're experiencing problems using the container
3 | body:
4 | - type: input
5 | id: os
6 | attributes:
7 | label: Operating system
8 | description: Your Linux distribution (can be shown by `lsb_release -a`).
9 | placeholder: e.g. Ubuntu 24.04
10 | validations:
11 | required: true
12 | - type: textarea
13 | id: summary
14 | attributes:
15 | label: Description
16 | description: A clear and concise description of your issue.
17 | validations:
18 | required: true
19 | - type: textarea
20 | id: compose
21 | attributes:
22 | label: Docker compose
23 | description: The compose file (or otherwise the `docker run` command used).
24 | render: yaml
25 | validations:
26 | required: true
27 | - type: textarea
28 | id: log
29 | attributes:
30 | label: Docker log
31 | description: The logfile of the container (as shown by `docker logs dsm`).
32 | render: shell
33 | validations:
34 | required: true
35 | - type: textarea
36 | id: screenshot
37 | attributes:
38 | label: Screenshots (optional)
39 | description: Screenshots that might help to make the problem more clear.
40 | validations:
41 | required: false
42 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/2-feature.yml:
--------------------------------------------------------------------------------
1 | name: "\U0001F680 Feature request"
2 | description: Suggest an idea for improving the container
3 | title: "[Feature]: "
4 | labels: ["enhancement"]
5 | body:
6 | - type: textarea
7 | id: problem
8 | attributes:
9 | label: Is your proposal related to a problem?
10 | description: |
11 | Provide a clear and concise description of what the problem is.
12 | For example, "I'm always frustrated when..."
13 | validations:
14 | required: true
15 | - type: textarea
16 | id: solution
17 | attributes:
18 | label: Describe the solution you'd like.
19 | description: |
20 | Provide a clear and concise description of what you want to happen.
21 | validations:
22 | required: true
23 | - type: textarea
24 | id: alternatives
25 | attributes:
26 | label: Describe alternatives you've considered.
27 | description: |
28 | Let us know about other solutions you've tried or researched.
29 | validations:
30 | required: true
31 | - type: textarea
32 | id: context
33 | attributes:
34 | label: Additional context
35 | description: |
36 | Is there anything else you can add about the proposal?
37 | You might want to link to related issues here, if you haven't already.
38 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/3-bug.yml:
--------------------------------------------------------------------------------
1 | name: "\U0001F41E Bug report"
2 | description: Create a report to help us improve the container
3 | title: "[Bug]: "
4 | labels: ["bug"]
5 | body:
6 | - type: input
7 | id: os
8 | attributes:
9 | label: Operating system
10 | description: Your Linux distribution (can be shown by `lsb_release -a`).
11 | placeholder: e.g. Ubuntu 24.04
12 | validations:
13 | required: true
14 | - type: textarea
15 | id: summary
16 | attributes:
17 | label: Description
18 | description: Describe the expected behaviour, the actual behaviour, and the steps to reproduce.
19 | validations:
20 | required: true
21 | - type: textarea
22 | id: compose
23 | attributes:
24 | label: Docker compose
25 | description: The compose file (or otherwise the `docker run` command used).
26 | render: yaml
27 | validations:
28 | required: true
29 | - type: textarea
30 | id: log
31 | attributes:
32 | label: Docker log
33 | description: The logfile of the container (as shown by `docker logs dsm`).
34 | render: shell
35 | validations:
36 | required: true
37 | - type: textarea
38 | id: screenshot
39 | attributes:
40 | label: Screenshots (optional)
41 | description: Screenshots that might help to make the problem more clear.
42 | validations:
43 | required: false
44 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/4-question.yml:
--------------------------------------------------------------------------------
1 | name: "\U00002753 General question"
2 | description: Questions about the container not related to an issue
3 | title: "[Question]: "
4 | labels: ["question"]
5 | body:
6 | - type: checkboxes
7 | attributes:
8 | label: Is your question not already answered in the FAQ?
9 | description: Please read the [FAQ](https://github.com/vdsm/virtual-dsm/blob/master/readme.md) carefully to avoid asking duplicate questions.
10 | options:
11 | - label: I made sure the question is not listed in the [FAQ](https://github.com/vdsm/virtual-dsm/blob/master/readme.md).
12 | required: true
13 | - type: checkboxes
14 | attributes:
15 | label: Is this a general question and not a technical issue?
16 | description: For questions related to issues you must use the [technical issue](https://github.com/vdsm/virtual-dsm/issues/new?assignees=&labels=&projects=&template=1-issue.yml) form instead. It contains all the right fields (system info, logfiles, etc.) we need in order to be able to help you.
17 | options:
18 | - label: I am sure my question is not about a technical issue.
19 | required: true
20 | - type: textarea
21 | id: question
22 | attributes:
23 | label: Question
24 | description: What's the question you have about the container?
25 | validations:
26 | required: true
27 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: docker
4 | directory: /
5 | schedule:
6 | interval: weekly
7 | - package-ecosystem: github-actions
8 | directory: /
9 | schedule:
10 | interval: weekly
11 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": ["config:recommended", ":disableDependencyDashboard"]
4 | }
5 |
--------------------------------------------------------------------------------
/.github/screen.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vdsm/virtual-dsm/1b8054e8472b3af4fdc311af328eb01ef1658418/.github/screen.jpg
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches:
7 | - master
8 | paths-ignore:
9 | - '**/*.md'
10 | - '**/*.yml'
11 | - '**/*.js'
12 | - '**/*.css'
13 | - '**/*.html'
14 | - 'web/**'
15 | - '.gitignore'
16 | - '.dockerignore'
17 | - '.github/**'
18 | - '.github/workflows/**'
19 |
20 | concurrency:
21 | group: build
22 | cancel-in-progress: false
23 |
24 | jobs:
25 | shellcheck:
26 | name: Check
27 | uses: ./.github/workflows/check.yml
28 | build:
29 | name: Build
30 | needs: shellcheck
31 | runs-on: ubuntu-latest
32 | permissions:
33 | actions: write
34 | packages: write
35 | contents: read
36 | steps:
37 | -
38 | name: Checkout
39 | uses: actions/checkout@v4
40 | with:
41 | fetch-depth: 0
42 | -
43 | name: Docker metadata
44 | id: meta
45 | uses: docker/metadata-action@v5
46 | with:
47 | context: git
48 | images: |
49 | ${{ secrets.DOCKERHUB_REPO }}
50 | ghcr.io/${{ github.repository }}
51 | tags: |
52 | type=raw,value=latest,priority=100
53 | type=raw,value=${{ vars.MAJOR }}.${{ vars.MINOR }}
54 | labels: |
55 | org.opencontainers.image.title=${{ vars.NAME }}
56 | env:
57 | DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
58 | -
59 | name: Set up Docker Buildx
60 | uses: docker/setup-buildx-action@v3
61 | -
62 | name: Login into Docker Hub
63 | uses: docker/login-action@v3
64 | with:
65 | username: ${{ secrets.DOCKERHUB_USERNAME }}
66 | password: ${{ secrets.DOCKERHUB_TOKEN }}
67 | -
68 | name: Login to GitHub Container Registry
69 | uses: docker/login-action@v3
70 | with:
71 | registry: ghcr.io
72 | username: ${{ github.actor }}
73 | password: ${{ secrets.GITHUB_TOKEN }}
74 | -
75 | name: Build Docker image
76 | uses: docker/build-push-action@v6
77 | with:
78 | context: .
79 | push: true
80 | provenance: false
81 | platforms: linux/amd64,linux/arm64
82 | tags: ${{ steps.meta.outputs.tags }}
83 | labels: ${{ steps.meta.outputs.labels }}
84 | annotations: ${{ steps.meta.outputs.annotations }}
85 | build-args: |
86 | VERSION_ARG=${{ steps.meta.outputs.version }}
87 | -
88 | name: Create a release
89 | uses: action-pack/github-release@v2
90 | with:
91 | tag: "v${{ steps.meta.outputs.version }}"
92 | title: "v${{ steps.meta.outputs.version }}"
93 | token: ${{ secrets.REPO_ACCESS_TOKEN }}
94 | -
95 | name: Increment version variable
96 | uses: action-pack/bump@v2
97 | with:
98 | token: ${{ secrets.REPO_ACCESS_TOKEN }}
99 | -
100 | name: Push to Gitlab mirror
101 | uses: action-pack/gitlab-sync@v3
102 | with:
103 | url: ${{ secrets.GITLAB_URL }}
104 | token: ${{ secrets.GITLAB_TOKEN }}
105 | username: ${{ secrets.GITLAB_USERNAME }}
106 | -
107 | name: Send mail
108 | uses: action-pack/send-mail@v1
109 | with:
110 | to: ${{secrets.MAILTO}}
111 | from: Github Actions <${{secrets.MAILTO}}>
112 | connection_url: ${{secrets.MAIL_CONNECTION}}
113 | subject: Build of ${{ github.event.repository.name }} v${{ steps.meta.outputs.version }} completed
114 | body: |
115 | The build job of ${{ github.event.repository.name }} v${{ steps.meta.outputs.version }} was completed successfully!
116 |
117 | See https://github.com/${{ github.repository }}/actions for more information.
118 |
--------------------------------------------------------------------------------
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | on: [workflow_call]
2 | name: "Check"
3 | permissions: {}
4 |
5 | jobs:
6 | shellcheck:
7 | name: shellcheck
8 | runs-on: ubuntu-latest
9 | steps:
10 | -
11 | name: Checkout
12 | uses: actions/checkout@v4
13 | -
14 | name: Run ShellCheck
15 | uses: ludeeus/action-shellcheck@master
16 | env:
17 | SHELLCHECK_OPTS: -x --source-path=src -e SC2001 -e SC2034 -e SC2064 -e SC2317 -e SC2153 -e SC2028
18 | -
19 | name: Lint Dockerfile
20 | uses: hadolint/hadolint-action@v3.1.0
21 | with:
22 | dockerfile: Dockerfile
23 | ignore: DL3008,DL3003,DL3006,DL3013
24 | failure-threshold: warning
25 |
--------------------------------------------------------------------------------
/.github/workflows/hub.yml:
--------------------------------------------------------------------------------
1 | name: Update
2 | on:
3 | push:
4 | branches:
5 | - master
6 | paths:
7 | - readme.md
8 | - README.md
9 | - .github/workflows/hub.yml
10 |
11 | jobs:
12 | dockerHubDescription:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v4
16 | -
17 | name: Docker Hub Description
18 | uses: peter-evans/dockerhub-description@v4
19 | with:
20 | username: ${{ secrets.DOCKERHUB_USERNAME }}
21 | password: ${{ secrets.DOCKERHUB_TOKEN }}
22 | repository: ${{ secrets.DOCKERHUB_REPO }}
23 | short-description: ${{ github.event.repository.description }}
24 | readme-filepath: ./readme.md
25 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | on:
2 | workflow_dispatch:
3 | pull_request:
4 | paths:
5 | - '**/*.sh'
6 | - 'Dockerfile'
7 | - '.github/workflows/test.yml'
8 | - '.github/workflows/check.yml'
9 |
10 | name: "Test"
11 | permissions: {}
12 |
13 | jobs:
14 | shellcheck:
15 | name: Test
16 | uses: ./.github/workflows/check.yml
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build.sh
2 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM qemux/qemu-host:2.05 AS builder
2 |
3 | # FROM golang as builder
4 | # WORKDIR /
5 | # RUN git clone https://github.com/qemus/qemu-host.git
6 | # WORKDIR /qemu-host/src
7 | # RUN go mod download
8 | # RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o /qemu-host.bin .
9 |
10 | FROM debian:trixie-slim
11 |
12 | ARG TARGETPLATFORM
13 | ARG VERSION_ARG="0.0"
14 | ARG DEBCONF_NOWARNINGS="yes"
15 | ARG DEBIAN_FRONTEND="noninteractive"
16 | ARG DEBCONF_NONINTERACTIVE_SEEN="true"
17 |
18 | RUN set -eu && \
19 | apt-get update && \
20 | apt-get --no-install-recommends -y install \
21 | jq \
22 | tini \
23 | curl \
24 | wget \
25 | fdisk \
26 | unzip \
27 | nginx \
28 | procps \
29 | python3 \
30 | python3-pip \
31 | python3-msgpack \
32 | python3-pysodium \
33 | xz-utils \
34 | iptables \
35 | iproute2 \
36 | apt-utils \
37 | dnsmasq \
38 | fakeroot \
39 | net-tools \
40 | e2fsprogs \
41 | qemu-utils \
42 | iputils-ping \
43 | ca-certificates \
44 | netcat-openbsd \
45 | qemu-system-x86 && \
46 | apt-get clean && \
47 | pip3 install --no-cache-dir --break-system-packages --root-user-action=ignore dissect.cstruct && \
48 | mkdir -p /etc/qemu && \
49 | echo "allow br0" > /etc/qemu/bridge.conf && \
50 | unlink /etc/nginx/sites-enabled/default && \
51 | sed -i 's/^worker_processes.*/worker_processes 1;/' /etc/nginx/nginx.conf && \
52 | echo "$VERSION_ARG" > /run/version && \
53 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
54 |
55 | COPY --chmod=755 ./src /run/
56 | COPY --chmod=755 ./web /var/www/
57 | COPY --chmod=755 --from=builder /qemu-host.bin /run/host.bin
58 | COPY --chmod=744 ./web/conf/nginx.conf /etc/nginx/sites-enabled/web.conf
59 | ADD --chmod=775 https://raw.githubusercontent.com/sud0woodo/patology/refs/heads/main/patology.py /run/extract.py
60 |
61 | VOLUME /storage
62 | EXPOSE 22 139 445 5000
63 |
64 | ENV RAM_SIZE="2G"
65 | ENV CPU_CORES="2"
66 | ENV DISK_SIZE="16G"
67 |
68 | HEALTHCHECK --interval=60s --start-period=45s --retries=2 CMD /run/check.sh
69 |
70 | ENTRYPOINT ["/usr/bin/tini", "-s", "/run/entry.sh"]
71 |
--------------------------------------------------------------------------------
/compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | dsm:
3 | container_name: dsm
4 | image: vdsm/virtual-dsm
5 | environment:
6 | DISK_SIZE: "16G"
7 | devices:
8 | - /dev/kvm
9 | - /dev/net/tun
10 | cap_add:
11 | - NET_ADMIN
12 | ports:
13 | - 5000:5000
14 | volumes:
15 | - ./dsm:/storage
16 | restart: always
17 | stop_grace_period: 2m
18 |
--------------------------------------------------------------------------------
/kubernetes.yml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: v1
3 | kind: PersistentVolumeClaim
4 | metadata:
5 | name: dsm-pvc
6 | spec:
7 | accessModes:
8 | - ReadWriteOnce
9 | resources:
10 | requests:
11 | storage: 16Gi
12 | ---
13 | apiVersion: apps/v1
14 | kind: Deployment
15 | metadata:
16 | name: dsm
17 | labels:
18 | name: dsm
19 | spec:
20 | replicas: 1
21 | selector:
22 | matchLabels:
23 | app: dsm
24 | template:
25 | metadata:
26 | labels:
27 | app: dsm
28 | spec:
29 | containers:
30 | - name: dsm
31 | image: vdsm/virtual-dsm
32 | env:
33 | - name: DISK_SIZE
34 | value: "16G"
35 | ports:
36 | - containerPort: 5000
37 | name: http
38 | protocol: TCP
39 | securityContext:
40 | capabilities:
41 | add:
42 | - NET_ADMIN
43 | privileged: true
44 | volumeMounts:
45 | - mountPath: /storage
46 | name: storage
47 | - mountPath: /dev/kvm
48 | name: dev-kvm
49 | - mountPath: /dev/net/tun
50 | name: dev-tun
51 | terminationGracePeriodSeconds: 120
52 | volumes:
53 | - name: storage
54 | persistentVolumeClaim:
55 | claimName: dsm-pvc
56 | - hostPath:
57 | path: /dev/kvm
58 | name: dev-kvm
59 | - hostPath:
60 | path: /dev/net/tun
61 | type: CharDevice
62 | name: dev-tun
63 | ---
64 | apiVersion: v1
65 | kind: Service
66 | metadata:
67 | name: dsm
68 | spec:
69 | internalTrafficPolicy: Cluster
70 | ports:
71 | - name: http
72 | port: 5000
73 | protocol: TCP
74 | targetPort: 5000
75 | selector:
76 | app: dsm
77 | type: ClusterIP
78 |
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |
Virtual DSM
2 |
3 |

4 |
5 |
6 |
7 | [![Build]][build_url]
8 | [![Version]][tag_url]
9 | [![Size]][tag_url]
10 | [![Package]][pkg_url]
11 | [![Pulls]][hub_url]
12 |
13 |
14 |
15 | Virtual DSM in a Docker container.
16 |
17 | ## Features ✨
18 |
19 | - Multiple disks
20 | - KVM acceleration
21 | - Upgrades supported
22 |
23 | ## Usage 🐳
24 |
25 | ##### Via Docker Compose:
26 |
27 | ```yaml
28 | services:
29 | dsm:
30 | container_name: dsm
31 | image: vdsm/virtual-dsm
32 | environment:
33 | DISK_SIZE: "16G"
34 | devices:
35 | - /dev/kvm
36 | - /dev/net/tun
37 | cap_add:
38 | - NET_ADMIN
39 | ports:
40 | - 5000:5000
41 | volumes:
42 | - ./dsm:/storage
43 | restart: always
44 | stop_grace_period: 2m
45 | ```
46 |
47 | ##### Via Docker CLI:
48 |
49 | ```bash
50 | docker run -it --rm --name dsm -p 5000:5000 --device=/dev/kvm --device=/dev/net/tun --cap-add NET_ADMIN -v "${PWD:-.}/dsm:/storage" --stop-timeout 120 vdsm/virtual-dsm
51 | ```
52 |
53 | ##### Via Kubernetes:
54 |
55 | ```shell
56 | kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/master/kubernetes.yml
57 | ```
58 |
59 | ##### Via Github Codespaces:
60 |
61 | [](https://codespaces.new/vdsm/virtual-dsm)
62 |
63 | ## FAQ 💬
64 |
65 | ### How do I use it?
66 |
67 | Very simple! These are the steps:
68 |
69 | - Start the container and connect to [port 5000](http://127.0.0.1:5000/) using your web browser.
70 |
71 | - Wait until DSM finishes its installation
72 |
73 | - Choose an username and password, and you will be taken to the desktop.
74 |
75 | Enjoy your brand new NAS, and don't forget to star this repo!
76 |
77 | ### How do I change the storage location?
78 |
79 | To change the storage location, include the following bind mount in your compose file:
80 |
81 | ```yaml
82 | volumes:
83 | - ./dsm:/storage
84 | ```
85 |
86 | Replace the example path `./dsm` with the desired storage folder or named volume.
87 |
88 | ### How do I change the size of the disk?
89 |
90 | To expand the default size of 16 GB, locate the `DISK_SIZE` setting in your compose file and modify it to your preferred capacity:
91 |
92 | ```yaml
93 | environment:
94 | DISK_SIZE: "128G"
95 | ```
96 |
97 | > [!TIP]
98 | > This can also be used to resize the existing disk to a larger capacity without any data loss.
99 |
100 | ### How do I create a growable disk?
101 |
102 | By default, the entire capacity of the disk will be reserved in advance.
103 |
104 | To create a growable disk that only allocates space that is actually used, add the following environment variable:
105 |
106 | ```yaml
107 | environment:
108 | DISK_FMT: "qcow2"
109 | ```
110 |
111 | ### How do I add multiple disks?
112 |
113 | To create additional disks, modify your compose file like this:
114 |
115 | ```yaml
116 | environment:
117 | DISK2_SIZE: "32G"
118 | DISK3_SIZE: "64G"
119 | volumes:
120 | - ./example2:/storage2
121 | - ./example3:/storage3
122 | ```
123 |
124 | ### How do I pass-through a disk?
125 |
126 | It is possible to pass-through disk devices or partitions directly by adding them to your compose file in this way:
127 |
128 | ```yaml
129 | devices:
130 | - /dev/sdb:/disk1
131 | - /dev/sdc1:/disk2
132 | ```
133 |
134 | Make sure it is totally empty (without any filesystem), otherwise DSM may not format it as a volume.
135 |
136 | ### How do I change the amount of CPU or RAM?
137 |
138 | By default, the container will be allowed to use a maximum of 2 CPU cores and 2 GB of RAM.
139 |
140 | If you want to adjust this, you can specify the desired amount using the following environment variables:
141 |
142 | ```yaml
143 | environment:
144 | RAM_SIZE: "4G"
145 | CPU_CORES: "4"
146 | ```
147 |
148 | ### How do I verify if my system supports KVM?
149 |
150 | First check if your software is compatible using this chart:
151 |
152 | | **Product** | **Linux** | **Win11** | **Win10** | **macOS** |
153 | |---|---|---|---|---|
154 | | Docker CLI | ✅ | ✅ | ❌ | ❌ |
155 | | Docker Desktop | ❌ | ✅ | ❌ | ❌ |
156 | | Podman CLI | ✅ | ✅ | ❌ | ❌ |
157 | | Podman Desktop | ✅ | ✅ | ❌ | ❌ |
158 |
159 | After that you can run the following commands in Linux to check your system:
160 |
161 | ```bash
162 | sudo apt install cpu-checker
163 | sudo kvm-ok
164 | ```
165 |
166 | If you receive an error from `kvm-ok` indicating that KVM cannot be used, please check whether:
167 |
168 | - the virtualization extensions (`Intel VT-x` or `AMD SVM`) are enabled in your BIOS.
169 |
170 | - you enabled "nested virtualization" if you are running the container inside a virtual machine.
171 |
172 | - you are not using a cloud provider, as most of them do not allow nested virtualization for their VPS's.
173 |
174 | 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.
175 |
176 | ### How do I assign an individual IP address to the container?
177 |
178 | By default, the container uses bridge networking, which shares the IP address with the host.
179 |
180 | If you want to assign an individual IP address to the container, you can create a macvlan network as follows:
181 |
182 | ```bash
183 | docker network create -d macvlan \
184 | --subnet=192.168.0.0/24 \
185 | --gateway=192.168.0.1 \
186 | --ip-range=192.168.0.100/28 \
187 | -o parent=eth0 vdsm
188 | ```
189 |
190 | Be sure to modify these values to match your local subnet.
191 |
192 | Once you have created the network, change your compose file to look as follows:
193 |
194 | ```yaml
195 | services:
196 | dsm:
197 | container_name: dsm
198 | ....
199 | networks:
200 | vdsm:
201 | ipv4_address: 192.168.0.100
202 |
203 | networks:
204 | vdsm:
205 | external: true
206 | ```
207 |
208 | An added benefit of this approach is that you won't have to perform any port mapping anymore, since all ports will be exposed by default.
209 |
210 | > [!IMPORTANT]
211 | > This IP address won't be accessible from the Docker host due to the design of macvlan, which doesn't permit communication between the two. If this is a concern, you need to create a [second macvlan](https://blog.oddbit.com/post/2018-03-12-using-docker-macvlan-networks/#host-access) as a workaround.
212 |
213 | ### How can DSM acquire an IP address from my router?
214 |
215 | After configuring the container for [macvlan](#how-do-i-assign-an-individual-ip-address-to-the-container), it is possible for DSM to become part of your home network by requesting an IP from your router, just like your other devices.
216 |
217 | To enable this mode, in which the container and DSM will have separate IP addresses, add the following lines to your compose file:
218 |
219 | ```yaml
220 | environment:
221 | DHCP: "Y"
222 | devices:
223 | - /dev/vhost-net
224 | device_cgroup_rules:
225 | - 'c *:* rwm'
226 | ```
227 |
228 | ### How do I pass-through the GPU?
229 |
230 | To pass-through your Intel GPU, add the following lines to your compose file:
231 |
232 | ```yaml
233 | environment:
234 | GPU: "Y"
235 | devices:
236 | - /dev/dri
237 | ```
238 |
239 | > [!NOTE]
240 | > This can be used to enable the facial recognition function in Synology Photos, but does not provide hardware transcoding for video.
241 |
242 | ### How do I install a specific version of vDSM?
243 |
244 | By default, version 7.2 will be installed, but if you prefer an older version, you can add the download URL of the `.pat` file to your compose file as follows:
245 |
246 | ```yaml
247 | environment:
248 | URL: "https://global.synologydownload.com/download/DSM/release/7.0.1/42218/DSM_VirtualDSM_42218.pat"
249 | ```
250 |
251 | With this method, it is even possible to switch back and forth between versions while keeping your file data intact.
252 |
253 | Alternatively, you can also skip the download and use a local file instead, by binding it in your compose file in this way:
254 |
255 | ```yaml
256 | volumes:
257 | - ./DSM_VirtualDSM_42218.pat:/boot.pat
258 | ```
259 |
260 | Replace the example path `./DSM_VirtualDSM_42218.pat` with the filename of your desired `.pat` file. The value of `URL` will be ignored in this case.
261 |
262 | ### What are the differences compared to the standard DSM?
263 |
264 | There are only two minor differences: the Virtual Machine Manager package is not available, and Surveillance Station will not include any free licenses.
265 |
266 | ### How do I run Windows in a container?
267 |
268 | You can use [dockur/windows](https://github.com/dockur/windows) for that. It shares many of the same features, and even has completely automatic installation.
269 |
270 | ### How do I run a Linux desktop in a container?
271 |
272 | You can use [qemus/qemu](https://github.com/qemus/qemu) in that case.
273 |
274 | ### Is this project legal?
275 |
276 | Yes, this project contains only open-source code and does not distribute any copyrighted material. Neither does it try to circumvent any copyright protection measures. So under all applicable laws, this project will be considered legal.
277 |
278 | However, by installing Synology's Virtual DSM, you must accept their end-user license agreement, which does not permit installation on non-Synology hardware. So only run this container on an official Synology NAS, as any other use will be a violation of their terms and conditions.
279 |
280 | ## Stars 🌟
281 | [](https://starchart.cc/vdsm/virtual-dsm)
282 |
283 | ## Disclaimer ⚖️
284 |
285 | *Only run this container on Synology hardware, any other use is not permitted by their EULA. The product names, logos, brands, and other trademarks referred to within this project are the property of their respective trademark holders. This project is not affiliated, sponsored, or endorsed by Synology, Inc.*
286 |
287 | [build_url]: https://github.com/vdsm/virtual-dsm/
288 | [hub_url]: https://hub.docker.com/r/vdsm/virtual-dsm
289 | [tag_url]: https://hub.docker.com/r/vdsm/virtual-dsm/tags
290 | [pkg_url]: https://github.com/vdsm/virtual-dsm/pkgs/container/virtual-dsm
291 |
292 | [Build]: https://github.com/vdsm/virtual-dsm/actions/workflows/build.yml/badge.svg
293 | [Size]: https://img.shields.io/docker/image-size/vdsm/virtual-dsm/latest?color=066da5&label=size
294 | [Pulls]: https://img.shields.io/docker/pulls/vdsm/virtual-dsm.svg?style=flat&label=pulls&logo=docker
295 | [Version]: https://img.shields.io/docker/v/vdsm/virtual-dsm/latest?arch=amd64&sort=semver&color=066da5
296 | [Package]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fipitio.github.io%2Fbackage%2Fvdsm%2Fvirtual-dsm%2Fvirtual-dsm.json&query=%24.downloads&logo=github&style=flat&color=066da5&label=pulls
297 |
--------------------------------------------------------------------------------
/src/check.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -Eeuo pipefail
3 |
4 | : "${NETWORK:="Y"}"
5 |
6 | [ -f "/run/shm/qemu.end" ] && echo "QEMU is shutting down.." && exit 1
7 | [ ! -s "/run/shm/qemu.pid" ] && echo "QEMU is not running yet.." && exit 0
8 | [[ "$NETWORK" == [Nn]* ]] && echo "Networking is disabled.." && exit 0
9 |
10 | file="/run/shm/dsm.url"
11 | address="/run/shm/qemu.ip"
12 |
13 | [ ! -s "$file" ] && echo "DSM has not enabled networking yet.." && exit 1
14 |
15 | location=$(<"$file")
16 |
17 | if ! curl -m 20 -ILfSs "http://$location/" > /dev/null; then
18 |
19 | if [[ "$location" == "20.20"* ]]; then
20 | ip="20.20.20.1"
21 | port="${location##*:}"
22 | echo "Failed to reach DSM at port $port"
23 | else
24 | echo "Failed to reach DSM at http://$location"
25 | ip=$(<"$address")
26 | fi
27 |
28 | echo "You might need to whitelist IP $ip in the DSM firewall." && exit 1
29 |
30 | fi
31 |
32 | echo "Healthcheck OK"
33 | exit 0
34 |
--------------------------------------------------------------------------------
/src/config.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -Eeuo pipefail
3 |
4 | DEF_OPTS="-nodefaults -boot strict=on"
5 | RAM_OPTS=$(echo "-m ${RAM_SIZE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
6 | CPU_OPTS="-cpu $CPU_FLAGS -smp $CPU_CORES,sockets=1,dies=1,cores=$CPU_CORES,threads=1"
7 | MAC_OPTS="-machine type=q35,smm=off,usb=off,vmport=off,dump-guest-core=off,hpet=off${KVM_OPTS}"
8 | DEV_OPTS="-device virtio-balloon-pci,id=balloon0,bus=pcie.0,addr=0x4"
9 | DEV_OPTS+=" -object rng-random,id=objrng0,filename=/dev/urandom"
10 | DEV_OPTS+=" -device virtio-rng-pci,rng=objrng0,id=rng0,bus=pcie.0,addr=0x1c"
11 |
12 | ARGS="$DEF_OPTS $CPU_OPTS $RAM_OPTS $MAC_OPTS $DISPLAY_OPTS $MON_OPTS $SERIAL_OPTS $NET_OPTS $DISK_OPTS $DEV_OPTS $ARGUMENTS"
13 | ARGS=$(echo "$ARGS" | sed 's/\t/ /g' | tr -s ' ')
14 |
15 | # Check available memory as the very last step
16 |
17 | if [[ "$RAM_CHECK" != [Nn]* ]]; then
18 |
19 | RAM_AVAIL=$(free -b | grep -m 1 Mem: | awk '{print $7}')
20 | AVAIL_MEM=$(formatBytes "$RAM_AVAIL")
21 |
22 | if (( (RAM_WANTED + RAM_SPARE) > RAM_AVAIL )); then
23 | 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."
24 | [[ "${FS,,}" != "zfs" ]] && error "$msg" && exit 17
25 | info "$msg"
26 | else
27 | if (( (RAM_WANTED + (RAM_SPARE * 3)) > RAM_AVAIL )); then
28 | 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."
29 | if [[ "${FS,,}" != "zfs" ]]; then
30 | warn "$msg"
31 | else
32 | info "$msg"
33 | fi
34 | fi
35 | fi
36 |
37 | fi
38 |
39 | if [[ "$DEBUG" == [Yy1]* ]]; then
40 | printf "Arguments:\n\n%s\n\n" "${ARGS// -/$'\n-'}"
41 | fi
42 |
43 | return 0
44 |
--------------------------------------------------------------------------------
/src/disk.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -Eeuo pipefail
3 |
4 | # Docker environment variables
5 |
6 | : "${DISK_IO:="native"}" # I/O Mode, can be set to 'native', 'threads' or 'io_uring'
7 | : "${DISK_FMT:="raw"}" # Disk file format, 'raw' by default for best performance
8 | : "${DISK_TYPE:=""}" # Device type to be used, "sata", "nvme", "blk" or "scsi"
9 | : "${DISK_FLAGS:=""}" # Specifies the options for use with the qcow2 disk format
10 | : "${DISK_CACHE:="none"}" # Caching mode, can be set to 'writeback' for better performance
11 | : "${DISK_DISCARD:="on"}" # Controls whether unmap (TRIM) commands are passed to the host.
12 | : "${DISK_ROTATION:="1"}" # Rotation rate, set to 1 for SSD storage and increase for HDD
13 |
14 | BOOT="$STORAGE/$BASE.boot.img"
15 | SYSTEM="$STORAGE/$BASE.system.img"
16 |
17 | [ ! -s "$BOOT" ] && error "Virtual DSM boot-image does not exist ($BOOT)" && exit 81
18 | [ ! -s "$SYSTEM" ] && error "Virtual DSM system-image does not exist ($SYSTEM)" && exit 82
19 |
20 | fmt2ext() {
21 | local DISK_FMT=$1
22 |
23 | case "${DISK_FMT,,}" in
24 | qcow2)
25 | echo "qcow2"
26 | ;;
27 | raw)
28 | echo "img"
29 | ;;
30 | *)
31 | error "Unrecognized disk format: $DISK_FMT" && exit 78
32 | ;;
33 | esac
34 | }
35 |
36 | ext2fmt() {
37 | local DISK_EXT=$1
38 |
39 | case "${DISK_EXT,,}" in
40 | qcow2)
41 | echo "qcow2"
42 | ;;
43 | img)
44 | echo "raw"
45 | ;;
46 | *)
47 | error "Unrecognized file extension: .$DISK_EXT" && exit 78
48 | ;;
49 | esac
50 | }
51 |
52 | getSize() {
53 | local DISK_FILE=$1
54 | local DISK_EXT DISK_FMT
55 |
56 | DISK_EXT=$(echo "${DISK_FILE//*./}" | sed 's/^.*\.//')
57 | DISK_FMT=$(ext2fmt "$DISK_EXT")
58 |
59 | case "${DISK_FMT,,}" in
60 | raw)
61 | stat -c%s "$DISK_FILE"
62 | ;;
63 | qcow2)
64 | qemu-img info "$DISK_FILE" -f "$DISK_FMT" | grep '^virtual size: ' | sed 's/.*(\(.*\) bytes)/\1/'
65 | ;;
66 | *)
67 | error "Unrecognized disk format: $DISK_FMT" && exit 78
68 | ;;
69 | esac
70 | }
71 |
72 | isCow() {
73 | local FS=$1
74 |
75 | if [[ "${FS,,}" == "btrfs" ]]; then
76 | return 0
77 | fi
78 |
79 | return 1
80 | }
81 |
82 | supportsDirect() {
83 | local FS=$1
84 |
85 | if [[ "${FS,,}" == "ecryptfs" ]] || [[ "${FS,,}" == "tmpfs" ]]; then
86 | return 1
87 | fi
88 |
89 | return 0
90 | }
91 |
92 | createDisk() {
93 |
94 | local DISK_FILE=$1
95 | local DISK_SPACE=$2
96 | local DISK_DESC=$3
97 | local DISK_FMT=$4
98 | local FS=$5
99 | local DATA_SIZE DIR SPACE GB FA
100 |
101 | DATA_SIZE=$(numfmt --from=iec "$DISK_SPACE")
102 |
103 | rm -f "$DISK_FILE"
104 |
105 | if [[ "$ALLOCATE" != [Nn]* ]]; then
106 |
107 | # Check free diskspace
108 | DIR=$(dirname "$DISK_FILE")
109 | SPACE=$(df --output=avail -B 1 "$DIR" | tail -n 1)
110 |
111 | if (( DATA_SIZE > SPACE )); then
112 | GB=$(formatBytes "$SPACE")
113 | error "Not enough free space to create a $DISK_DESC of ${DISK_SPACE/G/ GB} in $DIR, it has only $GB available..."
114 | error "Please specify a smaller ${DISK_DESC^^}_SIZE or disable preallocation by setting ALLOCATE=N." && exit 76
115 | fi
116 | fi
117 |
118 | html "Creating a $DISK_DESC image..."
119 | info "Creating a ${DISK_SPACE/G/ GB} $DISK_STYLE $DISK_DESC image in $DISK_FMT format..."
120 |
121 | local FAIL="Could not create a $DISK_STYLE $DISK_FMT $DISK_DESC image of ${DISK_SPACE/G/ GB} ($DISK_FILE)"
122 |
123 | case "${DISK_FMT,,}" in
124 | raw)
125 |
126 | if isCow "$FS"; then
127 | if ! touch "$DISK_FILE"; then
128 | error "$FAIL" && exit 77
129 | fi
130 | { chattr +C "$DISK_FILE"; } || :
131 | fi
132 |
133 | if [[ "$ALLOCATE" == [Nn]* ]]; then
134 |
135 | # Create an empty file
136 | if ! truncate -s "$DATA_SIZE" "$DISK_FILE"; then
137 | rm -f "$DISK_FILE"
138 | error "$FAIL" && exit 77
139 | fi
140 |
141 | else
142 |
143 | # Create an empty file
144 | if ! fallocate -l "$DATA_SIZE" "$DISK_FILE" &>/dev/null; then
145 | if ! fallocate -l -x "$DATA_SIZE" "$DISK_FILE"; then
146 | if ! truncate -s "$DATA_SIZE" "$DISK_FILE"; then
147 | rm -f "$DISK_FILE"
148 | error "$FAIL" && exit 77
149 | fi
150 | fi
151 | fi
152 |
153 | fi
154 | ;;
155 | qcow2)
156 |
157 | local DISK_PARAM="$DISK_ALLOC"
158 | isCow "$FS" && DISK_PARAM+=",nocow=on"
159 | [ -n "$DISK_FLAGS" ] && DISK_PARAM+=",$DISK_FLAGS"
160 |
161 | if ! qemu-img create -f "$DISK_FMT" -o "$DISK_PARAM" -- "$DISK_FILE" "$DATA_SIZE" ; then
162 | rm -f "$DISK_FILE"
163 | error "$FAIL" && exit 70
164 | fi
165 | ;;
166 | esac
167 |
168 | if isCow "$FS"; then
169 | FA=$(lsattr "$DISK_FILE")
170 | if [[ "$FA" != *"C"* ]]; then
171 | error "Failed to disable COW for $DISK_DESC image $DISK_FILE on ${FS^^} filesystem (returned $FA)"
172 | fi
173 | fi
174 |
175 | return 0
176 | }
177 |
178 | resizeDisk() {
179 |
180 | local DISK_FILE=$1
181 | local DISK_SPACE=$2
182 | local DISK_DESC=$3
183 | local DISK_FMT=$4
184 | local FS=$5
185 | local CUR_SIZE DATA_SIZE DIR SPACE GB
186 |
187 | CUR_SIZE=$(getSize "$DISK_FILE")
188 | DATA_SIZE=$(numfmt --from=iec "$DISK_SPACE")
189 | local REQ=$((DATA_SIZE-CUR_SIZE))
190 | (( REQ < 1 )) && error "Shrinking disks is not supported yet, please increase ${DISK_DESC^^}_SIZE." && exit 71
191 |
192 | if [[ "$ALLOCATE" != [Nn]* ]]; then
193 |
194 | # Check free diskspace
195 | DIR=$(dirname "$DISK_FILE")
196 | SPACE=$(df --output=avail -B 1 "$DIR" | tail -n 1)
197 |
198 | if (( REQ > SPACE )); then
199 | GB=$(formatBytes "$SPACE")
200 | error "Not enough free space to resize $DISK_DESC to ${DISK_SPACE/G/ GB} in $DIR, it has only $GB available.."
201 | error "Please specify a smaller ${DISK_DESC^^}_SIZE or disable preallocation by setting ALLOCATE=N." && exit 74
202 | fi
203 | fi
204 |
205 | GB=$(formatBytes "$CUR_SIZE")
206 | MSG="Resizing $DISK_DESC from $GB to ${DISK_SPACE/G/ GB}..."
207 | info "$MSG" && html "$MSG"
208 |
209 | local FAIL="Could not resize the $DISK_STYLE $DISK_FMT $DISK_DESC image from ${GB} to ${DISK_SPACE/G/ GB} ($DISK_FILE)"
210 |
211 | case "${DISK_FMT,,}" in
212 | raw)
213 |
214 | if [[ "$ALLOCATE" == [Nn]* ]]; then
215 |
216 | # Resize file by changing its length
217 | if ! truncate -s "$DATA_SIZE" "$DISK_FILE"; then
218 | error "$FAIL" && exit 75
219 | fi
220 |
221 | else
222 |
223 | # Resize file by allocating more space
224 | if ! fallocate -l "$DATA_SIZE" "$DISK_FILE" &>/dev/null; then
225 | if ! fallocate -l -x "$DATA_SIZE" "$DISK_FILE"; then
226 | if ! truncate -s "$DATA_SIZE" "$DISK_FILE"; then
227 | error "$FAIL" && exit 75
228 | fi
229 | fi
230 | fi
231 |
232 | fi
233 | ;;
234 | qcow2)
235 |
236 | if ! qemu-img resize -f "$DISK_FMT" "--$DISK_ALLOC" "$DISK_FILE" "$DATA_SIZE" ; then
237 | error "$FAIL" && exit 72
238 | fi
239 |
240 | ;;
241 | esac
242 |
243 | return 0
244 | }
245 |
246 | convertDisk() {
247 |
248 | local SOURCE_FILE=$1
249 | local SOURCE_FMT=$2
250 | local DST_FILE=$3
251 | local DST_FMT=$4
252 | local DISK_BASE=$5
253 | local DISK_DESC=$6
254 | local FS=$7
255 |
256 | [ -f "$DST_FILE" ] && error "Conversion failed, destination file $DST_FILE already exists?" && exit 79
257 | [ ! -f "$SOURCE_FILE" ] && error "Conversion failed, source file $SOURCE_FILE does not exists?" && exit 79
258 |
259 | local TMP_FILE="$DISK_BASE.tmp"
260 | rm -f "$TMP_FILE"
261 |
262 | if [[ "$ALLOCATE" != [Nn]* ]]; then
263 |
264 | local DIR CUR_SIZE SPACE GB
265 |
266 | # Check free diskspace
267 | DIR=$(dirname "$TMP_FILE")
268 | CUR_SIZE=$(getSize "$SOURCE_FILE")
269 | SPACE=$(df --output=avail -B 1 "$DIR" | tail -n 1)
270 |
271 | if (( CUR_SIZE > SPACE )); then
272 | GB=$(formatBytes "$SPACE")
273 | error "Not enough free space to convert $DISK_DESC to $DST_FMT in $DIR, it has only $GB available..."
274 | error "Please free up some disk space or disable preallocation by setting ALLOCATE=N." && exit 76
275 | fi
276 | fi
277 |
278 | local msg="Converting $DISK_DESC to $DST_FMT"
279 | html "$msg..."
280 | info "$msg, please wait until completed..."
281 |
282 | local CONV_FLAGS="-p"
283 | local DISK_PARAM="$DISK_ALLOC"
284 | isCow "$FS" && DISK_PARAM+=",nocow=on"
285 |
286 | if [[ "$DST_FMT" != "raw" ]]; then
287 | if [[ "$ALLOCATE" == [Nn]* ]]; then
288 | CONV_FLAGS+=" -c"
289 | fi
290 | [ -n "$DISK_FLAGS" ] && DISK_PARAM+=",$DISK_FLAGS"
291 | fi
292 |
293 | # shellcheck disable=SC2086
294 | if ! qemu-img convert -f "$SOURCE_FMT" $CONV_FLAGS -o "$DISK_PARAM" -O "$DST_FMT" -- "$SOURCE_FILE" "$TMP_FILE"; then
295 | rm -f "$TMP_FILE"
296 | error "Failed to convert $DISK_STYLE $DISK_DESC image to $DST_FMT format in $DIR, is there enough space available?" && exit 79
297 | fi
298 |
299 | if [[ "$DST_FMT" == "raw" ]]; then
300 | if [[ "$ALLOCATE" != [Nn]* ]]; then
301 | # Work around qemu-img bug
302 | CUR_SIZE=$(stat -c%s "$TMP_FILE")
303 | if ! fallocate -l "$CUR_SIZE" "$TMP_FILE" &>/dev/null; then
304 | if ! fallocate -l -x "$CUR_SIZE" "$TMP_FILE"; then
305 | error "Failed to allocate $CUR_SIZE bytes for $DISK_DESC image $TMP_FILE"
306 | fi
307 | fi
308 | fi
309 | fi
310 |
311 | rm -f "$SOURCE_FILE"
312 | mv "$TMP_FILE" "$DST_FILE"
313 |
314 | if isCow "$FS"; then
315 | FA=$(lsattr "$DST_FILE")
316 | if [[ "$FA" != *"C"* ]]; then
317 | error "Failed to disable COW for $DISK_DESC image $DST_FILE on ${FS^^} filesystem (returned $FA)"
318 | fi
319 | fi
320 |
321 | msg="Conversion of $DISK_DESC"
322 | html "$msg completed..."
323 | info "$msg to $DST_FMT completed successfully!"
324 |
325 | return 0
326 | }
327 |
328 | checkFS () {
329 |
330 | local FS=$1
331 | local DISK_FILE=$2
332 | local DISK_DESC=$3
333 | local DIR FA
334 |
335 | DIR=$(dirname "$DISK_FILE")
336 | [ ! -d "$DIR" ] && return 0
337 |
338 | if [[ "${FS,,}" == "overlay"* ]]; then
339 | info "Warning: the filesystem of $DIR is OverlayFS, this usually means it was binded to an invalid path!"
340 | fi
341 |
342 | if [[ "${FS,,}" == "fuse"* ]]; then
343 | info "Warning: the filesystem of $DIR is FUSE, this extra layer will negatively affect performance!"
344 | fi
345 |
346 | if ! supportsDirect "$FS"; then
347 | info "Warning: the filesystem of $DIR is $FS, which does not support O_DIRECT mode, adjusting settings..."
348 | fi
349 |
350 | if isCow "$FS"; then
351 | if [ -f "$DISK_FILE" ]; then
352 | FA=$(lsattr "$DISK_FILE")
353 | if [[ "$FA" != *"C"* ]]; then
354 | info "Warning: COW (copy on write) is not disabled for $DISK_DESC image file $DISK_FILE, this is recommended on ${FS^^} filesystems!"
355 | fi
356 | fi
357 | fi
358 |
359 | return 0
360 | }
361 |
362 | createDevice () {
363 |
364 | local DISK_FILE=$1
365 | local DISK_TYPE=$2
366 | local DISK_INDEX=$3
367 | local DISK_ADDRESS=$4
368 | local DISK_FMT=$5
369 | local DISK_IO=$6
370 | local DISK_CACHE=$7
371 | local DISK_SERIAL=$8
372 | local DISK_SECTORS=$9
373 | local DISK_ID="data$DISK_INDEX"
374 |
375 | local index=""
376 | [ -n "$DISK_INDEX" ] && index=",bootindex=$DISK_INDEX"
377 | local result=" -drive file=$DISK_FILE,id=$DISK_ID,format=$DISK_FMT,cache=$DISK_CACHE,aio=$DISK_IO,discard=$DISK_DISCARD,detect-zeroes=on"
378 |
379 | case "${DISK_TYPE,,}" in
380 | "none" ) ;;
381 | "auto" )
382 | echo "$result"
383 | ;;
384 | "usb" )
385 | result+=",if=none \
386 | -device usb-storage,drive=${DISK_ID}${index}${DISK_SERIAL}${DISK_SECTORS}"
387 | echo "$result"
388 | ;;
389 | "nvme" )
390 | result+=",if=none \
391 | -device nvme,drive=${DISK_ID}${index},serial=deadbeaf${DISK_INDEX}${DISK_SERIAL}${DISK_SECTORS}"
392 | echo "$result"
393 | ;;
394 | "ide" | "sata" )
395 | result+=",if=none \
396 | -device ich9-ahci,id=ahci${DISK_INDEX},addr=$DISK_ADDRESS \
397 | -device ide-hd,drive=${DISK_ID},bus=ahci$DISK_INDEX.0,rotation_rate=$DISK_ROTATION${index}${DISK_SERIAL}${DISK_SECTORS}"
398 | echo "$result"
399 | ;;
400 | "blk" | "virtio-blk" )
401 | result+=",if=none \
402 | -device virtio-blk-pci,drive=${DISK_ID},bus=pcie.0,addr=$DISK_ADDRESS,iothread=io2${index}${DISK_SERIAL}${DISK_SECTORS}"
403 | echo "$result"
404 | ;;
405 | "scsi" | "virtio-scsi" )
406 | result+=",if=none \
407 | -device virtio-scsi-pci,id=${DISK_ID}b,bus=pcie.0,addr=$DISK_ADDRESS,iothread=io2 \
408 | -device scsi-hd,drive=${DISK_ID},bus=${DISK_ID}b.0,channel=0,scsi-id=0,lun=0,rotation_rate=$DISK_ROTATION${index}${DISK_SERIAL}${DISK_SECTORS}"
409 | echo "$result"
410 | ;;
411 | esac
412 |
413 | return 0
414 | }
415 |
416 | addDisk () {
417 |
418 | local DISK_BASE=$1
419 | local DISK_TYPE=$2
420 | local DISK_DESC=$3
421 | local DISK_SPACE=$4
422 | local DISK_INDEX=$5
423 | local DISK_ADDRESS=$6
424 | local DISK_FMT=$7
425 | local DISK_IO=$8
426 | local DISK_CACHE=$9
427 | local DISK_EXT DIR SPACE DATA_SIZE FS PREV_FMT PREV_EXT CUR_SIZE
428 |
429 | DISK_EXT=$(fmt2ext "$DISK_FMT")
430 | local DISK_FILE="$DISK_BASE.$DISK_EXT"
431 |
432 | DIR=$(dirname "$DISK_FILE")
433 | [ ! -d "$DIR" ] && return 0
434 |
435 | SPACE="${DISK_SPACE// /}"
436 | [ -z "$SPACE" ] && SPACE="16G"
437 | [ -z "${SPACE//[0-9. ]}" ] && SPACE="${SPACE}G"
438 | SPACE=$(echo "${SPACE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
439 |
440 | if ! numfmt --from=iec "$SPACE" &>/dev/null; then
441 | error "Invalid value for ${DISK_DESC^^}_SIZE: $DISK_SPACE" && exit 73
442 | fi
443 |
444 | DATA_SIZE=$(numfmt --from=iec "$SPACE")
445 |
446 | if (( DATA_SIZE < 6442450944 )); then
447 | error "Please increase ${DISK_DESC^^}_SIZE to at least 6 GB." && exit 73
448 | fi
449 |
450 | FS=$(stat -f -c %T "$DIR")
451 | checkFS "$FS" "$DISK_FILE" "$DISK_DESC" || exit $?
452 |
453 | if ! supportsDirect "$FS"; then
454 | DISK_IO="threads"
455 | DISK_CACHE="writeback"
456 | fi
457 |
458 | if ! [ -s "$DISK_FILE" ] ; then
459 |
460 | if [[ "${DISK_FMT,,}" != "raw" ]]; then
461 | PREV_FMT="raw"
462 | else
463 | PREV_FMT="qcow2"
464 | fi
465 |
466 | PREV_EXT=$(fmt2ext "$PREV_FMT")
467 |
468 | if [ -s "$DISK_BASE.$PREV_EXT" ] ; then
469 | convertDisk "$DISK_BASE.$PREV_EXT" "$PREV_FMT" "$DISK_FILE" "$DISK_FMT" "$DISK_BASE" "$DISK_DESC" "$FS" || exit $?
470 | fi
471 | fi
472 |
473 | if [ -s "$DISK_FILE" ]; then
474 |
475 | CUR_SIZE=$(getSize "$DISK_FILE")
476 |
477 | if (( DATA_SIZE > CUR_SIZE )); then
478 | resizeDisk "$DISK_FILE" "$SPACE" "$DISK_DESC" "$DISK_FMT" "$FS" || exit $?
479 | fi
480 |
481 | else
482 |
483 | createDisk "$DISK_FILE" "$SPACE" "$DISK_DESC" "$DISK_FMT" "$FS" || exit $?
484 |
485 | fi
486 |
487 | DISK_OPTS+=$(createDevice "$DISK_FILE" "$DISK_TYPE" "$DISK_INDEX" "$DISK_ADDRESS" "$DISK_FMT" "$DISK_IO" "$DISK_CACHE" "" "")
488 |
489 | return 0
490 | }
491 |
492 | addDevice () {
493 |
494 | local DISK_DEV=$1
495 | local DISK_TYPE=$2
496 | local DISK_INDEX=$3
497 | local DISK_ADDRESS=$4
498 |
499 | [ -z "$DISK_DEV" ] && return 0
500 | [ ! -b "$DISK_DEV" ] && error "Device $DISK_DEV cannot be found! Please add it to the 'devices' section of your compose file." && exit 55
501 |
502 | local sectors=""
503 | local result logical physical
504 | result=$(fdisk -l "$DISK_DEV" | grep -m 1 -o "(logical/physical): .*" | cut -c 21-)
505 | logical="${result%% *}"
506 | physical=$(echo "$result" | grep -m 1 -o "/ .*" | cut -c 3-)
507 | physical="${physical%% *}"
508 |
509 | if [ -n "$physical" ]; then
510 | if [[ "$physical" == "512" ]] || [[ "$physical" == "4096" ]]; then
511 | if [[ "$physical" == "4096" ]]; then
512 | sectors=",logical_block_size=$logical,physical_block_size=$physical"
513 | fi
514 | else
515 | warn "Unknown physical sector size: $physical for $DISK_DEV"
516 | fi
517 | else
518 | warn "Failed to determine the sector size for $DISK_DEV"
519 | fi
520 |
521 | DISK_OPTS+=$(createDevice "$DISK_DEV" "$DISK_TYPE" "$DISK_INDEX" "$DISK_ADDRESS" "raw" "$DISK_IO" "$DISK_CACHE" "" "$sectors")
522 |
523 | return 0
524 | }
525 |
526 | html "Initializing disks..."
527 |
528 | [ -z "${DISK_OPTS:-}" ] && DISK_OPTS=""
529 | [ -z "${DISK_TYPE:-}" ] && DISK_TYPE="scsi"
530 | [ -z "${DISK_NAME:-}" ] && DISK_NAME="data"
531 |
532 | case "${DISK_TYPE,,}" in
533 | "ide" | "sata" | "nvme" | "usb" | "scsi" | "blk" | "auto" | "none" ) ;;
534 | * ) error "Invalid DISK_TYPE specified, value \"$DISK_TYPE\" is not recognized!" && exit 80 ;;
535 | esac
536 |
537 | if [ -z "$ALLOCATE" ]; then
538 | if [[ "${DISK_FMT,,}" == "raw" ]]; then
539 | ALLOCATE="Y"
540 | else
541 | ALLOCATE="N"
542 | fi
543 | fi
544 |
545 | if [[ "$ALLOCATE" == [Nn]* ]]; then
546 | DISK_STYLE="growable"
547 | DISK_ALLOC="preallocation=off"
548 | else
549 | DISK_STYLE="preallocated"
550 | DISK_ALLOC="preallocation=falloc"
551 | fi
552 |
553 | DISK_OPTS+=$(createDevice "$BOOT" "$DISK_TYPE" "1" "0xa" "raw" "$DISK_IO" "$DISK_CACHE" "" "")
554 | DISK_OPTS+=$(createDevice "$SYSTEM" "$DISK_TYPE" "2" "0xb" "raw" "$DISK_IO" "$DISK_CACHE" "" "")
555 |
556 | DISK1_FILE="$STORAGE/${DISK_NAME}"
557 | if [[ ! -f "$DISK1_FILE.img" ]] && [[ -f "$STORAGE/data${DISK_SIZE}.img" ]]; then
558 | # Fallback for legacy installs
559 | mv "$STORAGE/data${DISK_SIZE}.img" "$DISK1_FILE.img"
560 | fi
561 |
562 | DISK2_FILE="/storage2/${DISK_NAME}2"
563 | if [ ! -f "$DISK2_FILE.img" ]; then
564 | # Fallback for legacy installs
565 | FALLBACK="/storage2/data.img"
566 | if [[ -f "$DISK1_FILE.img" ]] && [[ -f "$FALLBACK" ]]; then
567 | SIZE1=$(stat -c%s "$FALLBACK")
568 | SIZE2=$(stat -c%s "$DISK1_FILE.img")
569 | if [[ SIZE1 -ne SIZE2 ]]; then
570 | mv "$FALLBACK" "$DISK2_FILE.img"
571 | fi
572 | fi
573 | fi
574 |
575 | DISK3_FILE="/storage3/${DISK_NAME}3"
576 | if [ ! -f "$DISK3_FILE.img" ]; then
577 | # Fallback for legacy installs
578 | FALLBACK="/storage3/data.img"
579 | if [[ -f "$DISK1_FILE.img" ]] && [[ -f "$FALLBACK" ]]; then
580 | SIZE1=$(stat -c%s "$FALLBACK")
581 | SIZE2=$(stat -c%s "$DISK1_FILE.img")
582 | if [[ SIZE1 -ne SIZE2 ]]; then
583 | mv "$FALLBACK" "$DISK3_FILE.img"
584 | fi
585 | fi
586 | fi
587 |
588 | DISK4_FILE="/storage4/${DISK_NAME}4"
589 |
590 | : "${DISK2_SIZE:=""}"
591 | : "${DISK3_SIZE:=""}"
592 | : "${DISK4_SIZE:=""}"
593 |
594 | : "${DEVICE:=""}" # Docker variables to passthrough a block device, like /dev/vdc1.
595 | : "${DEVICE2:=""}"
596 | : "${DEVICE3:=""}"
597 | : "${DEVICE4:=""}"
598 |
599 | [ -z "$DEVICE" ] && [ -b "/disk" ] && DEVICE="/disk"
600 | [ -z "$DEVICE" ] && [ -b "/disk1" ] && DEVICE="/disk1"
601 | [ -z "$DEVICE2" ] && [ -b "/disk2" ] && DEVICE2="/disk2"
602 | [ -z "$DEVICE3" ] && [ -b "/disk3" ] && DEVICE3="/disk3"
603 | [ -z "$DEVICE4" ] && [ -b "/disk4" ] && DEVICE4="/disk4"
604 |
605 | [ -z "$DEVICE" ] && [ -b "/dev/disk1" ] && DEVICE="/dev/disk1"
606 | [ -z "$DEVICE2" ] && [ -b "/dev/disk2" ] && DEVICE2="/dev/disk2"
607 | [ -z "$DEVICE3" ] && [ -b "/dev/disk3" ] && DEVICE3="/dev/disk3"
608 | [ -z "$DEVICE4" ] && [ -b "/dev/disk4" ] && DEVICE4="/dev/disk4"
609 |
610 | if [ -n "$DEVICE" ]; then
611 | addDevice "$DEVICE" "$DISK_TYPE" "3" "0xc" || exit $?
612 | else
613 | addDisk "$DISK1_FILE" "$DISK_TYPE" "disk" "$DISK_SIZE" "3" "0xc" "$DISK_FMT" "$DISK_IO" "$DISK_CACHE" || exit $?
614 | fi
615 |
616 | if [ -n "$DEVICE2" ]; then
617 | addDevice "$DEVICE2" "$DISK_TYPE" "4" "0xd" || exit $?
618 | else
619 | addDisk "$DISK2_FILE" "$DISK_TYPE" "disk2" "$DISK2_SIZE" "4" "0xd" "$DISK_FMT" "$DISK_IO" "$DISK_CACHE" || exit $?
620 | fi
621 |
622 | if [ -n "$DEVICE3" ]; then
623 | addDevice "$DEVICE3" "$DISK_TYPE" "5" "0xe" || exit $?
624 | else
625 | addDisk "$DISK3_FILE" "$DISK_TYPE" "disk3" "$DISK3_SIZE" "5" "0xe" "$DISK_FMT" "$DISK_IO" "$DISK_CACHE" || exit $?
626 | fi
627 |
628 | if [ -n "$DEVICE4" ]; then
629 | addDevice "$DEVICE4" "$DISK_TYPE" "6" "0xf" || exit $?
630 | else
631 | addDisk "$DISK4_FILE" "$DISK_TYPE" "disk4" "$DISK4_SIZE" "6" "0xf" "$DISK_FMT" "$DISK_IO" "$DISK_CACHE" || exit $?
632 | fi
633 |
634 | DISK_OPTS+=" -object iothread,id=io2"
635 |
636 | html "Initialized disks successfully..."
637 | return 0
638 |
--------------------------------------------------------------------------------
/src/display.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -Eeuo pipefail
3 |
4 | # Docker environment variables
5 |
6 | : "${GPU:="N"}" # GPU passthrough
7 | : "${VGA:="virtio"}" # VGA adaptor
8 | : "${DISPLAY:="none"}" # Display type
9 | : "${RENDERNODE:="/dev/dri/renderD128"}" # Render node
10 |
11 | CPU_VENDOR=$(lscpu | awk '/Vendor ID/{print $3}')
12 |
13 | if [[ "$GPU" != [Yy1]* ]] || [[ "$CPU_VENDOR" != "GenuineIntel" ]] || [[ "$ARCH" != "amd64" ]]; then
14 |
15 | [[ "${DISPLAY,,}" == "none" ]] && VGA="none"
16 | DISPLAY_OPTS="-display $DISPLAY -vga $VGA"
17 | return 0
18 |
19 | fi
20 |
21 | DISPLAY_OPTS="-display egl-headless,rendernode=$RENDERNODE"
22 | DISPLAY_OPTS+=" -vga $VGA"
23 |
24 | [ ! -d /dev/dri ] && mkdir -m 755 /dev/dri
25 |
26 | # Extract the card number from the render node
27 | CARD_NUMBER=$(echo "$RENDERNODE" | grep -oP '(?<=renderD)\d+')
28 | CARD_DEVICE="/dev/dri/card$((CARD_NUMBER - 128))"
29 |
30 | if [ ! -c "$CARD_DEVICE" ]; then
31 | if mknod "$CARD_DEVICE" c 226 $((CARD_NUMBER - 128)); then
32 | chmod 666 "$CARD_DEVICE"
33 | fi
34 | fi
35 |
36 | if [ ! -c "$RENDERNODE" ]; then
37 | if mknod "$RENDERNODE" c 226 "$CARD_NUMBER"; then
38 | chmod 666 "$RENDERNODE"
39 | fi
40 | fi
41 |
42 | addPackage "xserver-xorg-video-intel" "Intel GPU drivers"
43 | addPackage "qemu-system-modules-opengl" "OpenGL module"
44 |
45 | return 0
46 |
--------------------------------------------------------------------------------
/src/entry.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -Eeuo pipefail
3 |
4 | : "${APP:="Virtual DSM"}"
5 | : "${SUPPORT:="https://github.com/vdsm/virtual-dsm"}"
6 |
7 | cd /run
8 |
9 | . utils.sh # Load functions
10 | . reset.sh # Initialize system
11 | . install.sh # Run installation
12 | . disk.sh # Initialize disks
13 | . display.sh # Initialize graphics
14 | . network.sh # Initialize network
15 | . proc.sh # Initialize processor
16 | . serial.sh # Initialize serialport
17 | . power.sh # Configure shutdown
18 | . config.sh # Configure arguments
19 |
20 | trap - ERR
21 |
22 | version=$(qemu-system-x86_64 --version | head -n 1 | cut -d '(' -f 1 | awk '{ print $NF }')
23 | info "Booting $APP using QEMU v$version..."
24 |
25 | if [[ "$CONSOLE" == [Yy]* ]]; then
26 | exec qemu-system-x86_64 ${ARGS:+ $ARGS}
27 | fi
28 |
29 | { qemu-system-x86_64 ${ARGS:+ $ARGS} >"$QEMU_OUT" 2>"$QEMU_LOG"; rc=$?; } || :
30 | (( rc != 0 )) && error "$(<"$QEMU_LOG")" && exit 15
31 |
32 | terminal
33 | tail -fn +0 "$QEMU_LOG" 2>/dev/null &
34 | cat "$QEMU_TERM" 2>/dev/null & wait $! || :
35 |
36 | sleep 1 & wait $!
37 | [ ! -f "$QEMU_END" ] && finish 0
38 |
--------------------------------------------------------------------------------
/src/install.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -Eeuo pipefail
3 |
4 | : "${URL:=""}" # URL of the PAT file to be downloaded.
5 |
6 | if [ -f "$STORAGE/dsm.ver" ]; then
7 | BASE=$(<"$STORAGE/dsm.ver")
8 | BASE="${BASE//[![:print:]]/}"
9 | [ -z "$BASE" ] && BASE="DSM_VirtualDSM_69057"
10 | else
11 | # Fallback for old installs
12 | BASE="DSM_VirtualDSM_42962"
13 | fi
14 |
15 | FN="boot.pat"
16 | DIR=$(find / -maxdepth 1 -type d -iname "$FN" -print -quit)
17 | [ ! -d "$DIR" ] && DIR=$(find "$STORAGE" -maxdepth 1 -type d -iname "$FN" -print -quit)
18 |
19 | if [ -d "$DIR" ]; then
20 | BASE="DSM_VirtualDSM" && URL="file://$DIR"
21 | if [[ ! -s "$STORAGE/$BASE.boot.img" ]] || [[ ! -s "$STORAGE/$BASE.system.img" ]]; then
22 | error "The bind $DIR maps to a file that does not exist!" && exit 65
23 | fi
24 | fi
25 |
26 | FILE=$(find / -maxdepth 1 -type f -iname "$FN" -print -quit)
27 | [ ! -s "$FILE" ] && FILE=$(find "$STORAGE" -maxdepth 1 -type f -iname "$FN" -print -quit)
28 | [ -s "$FILE" ] && BASE="DSM_VirtualDSM" && URL="file://$FILE"
29 |
30 | if [ -n "$URL" ] && [ ! -s "$FILE" ] && [ ! -d "$DIR" ]; then
31 | BASE=$(basename "$URL" .pat)
32 | if [ ! -s "$STORAGE/$BASE.system.img" ]; then
33 | BASE=$(basename "${URL%%\?*}" .pat)
34 | : "${BASE//+/ }"; printf -v BASE '%b' "${_//%/\\x}"
35 | BASE=$(echo "$BASE" | sed -e 's/[^A-Za-z0-9._-]/_/g')
36 | fi
37 | if [[ "${URL,,}" != "http"* ]] && [[ "${URL,,}" != "file:"* ]] ; then
38 | [ ! -s "$STORAGE/$BASE.pat" ] && error "Invalid URL: $URL" && exit 65
39 | URL="file://$STORAGE/$BASE.pat"
40 | fi
41 | fi
42 |
43 | if [[ -s "$STORAGE/$BASE.boot.img" ]] && [[ -s "$STORAGE/$BASE.system.img" ]]; then
44 | return 0 # Previous installation found
45 | fi
46 |
47 | html "Please wait while Virtual DSM is being installed..."
48 |
49 | DL=""
50 | DL_CHINA="https://cndl.synology.cn/download/DSM"
51 | DL_GLOBAL="https://global.synologydownload.com/download/DSM"
52 |
53 | [[ "${URL,,}" == *"cndl.synology"* ]] && DL="$DL_CHINA"
54 | [[ "${URL,,}" == *"global.synology"* ]] && DL="$DL_GLOBAL"
55 |
56 | if [ -z "$DL" ]; then
57 | [ -z "$COUNTRY" ] && setCountry
58 | [ -z "$COUNTRY" ] && info "Warning: could not detect country to select mirror!"
59 | [[ "${COUNTRY^^}" == "CN" ]] && DL="$DL_CHINA" || DL="$DL_GLOBAL"
60 | fi
61 |
62 | if [ -z "$URL" ]; then
63 | URL="$DL/release/7.2.2/72806/DSM_VirtualDSM_72806.pat"
64 | fi
65 |
66 | if [ ! -s "$FILE" ]; then
67 | BASE=$(basename "${URL%%\?*}" .pat)
68 | : "${BASE//+/ }"; printf -v BASE '%b' "${_//%/\\x}"
69 | BASE=$(echo "$BASE" | sed -e 's/[^A-Za-z0-9._-]/_/g')
70 | fi
71 |
72 | if [[ "$URL" != "file://$STORAGE/$BASE.pat" ]]; then
73 | rm -f "$STORAGE/$BASE.pat"
74 | fi
75 |
76 | rm -f "$STORAGE/$BASE.agent"
77 | rm -f "$STORAGE/$BASE.boot.img"
78 | rm -f "$STORAGE/$BASE.system.img"
79 |
80 | # Check filesystem
81 | FS=$(stat -f -c %T "$STORAGE")
82 |
83 | if [[ "${FS,,}" == "overlay"* ]]; then
84 | info "Warning: the filesystem of $STORAGE is OverlayFS, this usually means it was binded to an invalid path!"
85 | fi
86 |
87 | if [[ "${FS,,}" == "fuse"* ]]; then
88 | info "Warning: the filesystem of $STORAGE is FUSE, this extra layer will negatively affect performance!"
89 | fi
90 |
91 | if [[ "${FS,,}" == "ecryptfs" ]] || [[ "${FS,,}" == "tmpfs" ]]; then
92 | info "Warning: the filesystem of $STORAGE is $FS, which does not support O_DIRECT mode, adjusting settings..."
93 | fi
94 |
95 | if [[ "${FS,,}" == "fat"* || "${FS,,}" == "vfat"* || "${FS,,}" == "msdos"* ]]; then
96 | error "Unable to install on $FS filesystems, please use a different filesystem for /storage." && exit 61
97 | fi
98 |
99 | if [[ "${FS,,}" != "exfat"* && "${FS,,}" != "ntfs"* && "${FS,,}" != "unknown"* ]]; then
100 | TMP="$STORAGE/tmp"
101 | else
102 | TMP="/tmp/dsm"
103 | TMP_SPACE=2147483648
104 | SPACE=$(df --output=avail -B 1 /tmp | tail -n 1)
105 | SPACE_MB=$(formatBytes "$SPACE")
106 | if (( TMP_SPACE > SPACE )); then
107 | error "Not enough free space inside the container, have $SPACE_MB available but need at least 2 GB." && exit 93
108 | fi
109 | fi
110 |
111 | rm -rf "$TMP" && mkdir -p "$TMP"
112 |
113 | # Check free diskspace
114 | ROOT_SPACE=536870912
115 | SPACE=$(df --output=avail -B 1 / | tail -n 1)
116 | SPACE_MB=$(formatBytes "$SPACE" "down")
117 | (( ROOT_SPACE > SPACE )) && error "Not enough free space inside the container, have $SPACE_MB available but need at least 500 MB." && exit 96
118 |
119 | MIN_SPACE=15032385536
120 | SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
121 | SPACE_GB=$(formatBytes "$SPACE")
122 | (( MIN_SPACE > SPACE )) && error "Not enough free space for installation in $STORAGE, have $SPACE_GB available but need at least 14 GB." && exit 94
123 |
124 | # Check if output is to interactive TTY
125 | if [ -t 1 ]; then
126 | PROGRESS="--progress=bar:noscroll"
127 | else
128 | PROGRESS="--progress=dot:giga"
129 | fi
130 |
131 | if [[ "$URL" == "file://"* ]]; then
132 | MSG="Copying DSM"
133 | ERR="Failed to copy ${URL:7}"
134 | info "Install: Copying installation image..."
135 | else
136 | MSG="Downloading DSM"
137 | ERR="Failed to download $URL"
138 | info "Install: Downloading $BASE.pat..."
139 | fi
140 |
141 | html "$MSG..."
142 |
143 | PAT="/$BASE.pat"
144 | rm -f "$PAT"
145 |
146 | if [[ "$URL" == "file://"* ]]; then
147 |
148 | if [ ! -f "${URL:7}" ]; then
149 | error "File '${URL:7}' does not exist!" && exit 65
150 | fi
151 |
152 | cp "${URL:7}" "$PAT"
153 |
154 | else
155 |
156 | SIZE=0
157 | [[ "${URL,,}" == *"_72806.pat" ]] && SIZE=361010261
158 | [[ "${URL,,}" == *"_69057.pat" ]] && SIZE=363837333
159 | [[ "${URL,,}" == *"_42218.pat" ]] && SIZE=379637760
160 |
161 | /run/progress.sh "$PAT" "$SIZE" "$MSG ([P])..." &
162 |
163 | { wget "$URL" -O "$PAT" -q --no-check-certificate --timeout=10 --no-http-keep-alive --show-progress "$PROGRESS"; rc=$?; } || :
164 |
165 | fKill "progress.sh"
166 |
167 | (( rc == 3 )) && error "$ERR , cannot write file (disk full?)" && exit 69
168 | (( rc == 4 )) && error "$ERR , network failure!" && exit 69
169 | (( rc == 8 )) && error "$ERR , server issued an error response!" && exit 69
170 | (( rc != 0 )) && error "$ERR , reason: $rc" && exit 69
171 |
172 | fi
173 |
174 | [ ! -s "$PAT" ] && error "$ERR" && exit 69
175 |
176 | SIZE=$(stat -c%s "$PAT")
177 |
178 | if ((SIZE<250000000)); then
179 | error "The specified PAT file is probably an update pack as it's too small." && exit 62
180 | fi
181 |
182 | MSG="Extracting installation image..."
183 | info "Install: $MSG" && html "$MSG"
184 |
185 | if { tar tf "$PAT"; } >/dev/null 2>&1; then
186 |
187 | tar xpf "$PAT" -C "$TMP/."
188 |
189 | else
190 |
191 | { (cd "$TMP" && python3 /run/extract.py -i "$PAT" -d 2>/run/extract.log); rc=$?; } || :
192 |
193 | if (( rc != 0 )); then
194 | cat /run/extract.log
195 | error "Failed to extract PAT file, reason $rc" && exit 63
196 | fi
197 |
198 | fi
199 |
200 | MSG="Preparing system partition..."
201 | info "Install: $MSG" && html "$MSG"
202 |
203 | BOOT=$(find "$TMP" -name "*.bin.zip")
204 | [ ! -s "$BOOT" ] && error "The PAT file contains no boot image." && exit 67
205 |
206 | BOOT=$(echo "$BOOT" | head -c -5)
207 | unzip -q -o "$BOOT".zip -d "$TMP"
208 |
209 | SYSTEM="$STORAGE/$BASE.system.img"
210 | rm -f "$SYSTEM"
211 |
212 | # Check free diskspace
213 | SYSTEM_SIZE=10738466816
214 | SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
215 | SPACE_MB=$(formatBytes "$SPACE")
216 |
217 | if (( SYSTEM_SIZE > SPACE )); then
218 | error "Not enough free space in $STORAGE to create a 10 GB system disk, have only $SPACE_MB available." && exit 97
219 | fi
220 |
221 | if ! touch "$SYSTEM"; then
222 | error "Could not create file $SYSTEM for the system disk." && exit 98
223 | fi
224 |
225 | if [[ "${FS,,}" == "btrfs" ]]; then
226 | { chattr +C "$SYSTEM"; } || :
227 | FA=$(lsattr "$SYSTEM")
228 | if [[ "$FA" != *"C"* ]]; then
229 | error "Failed to disable COW for system image $SYSTEM on ${FS^^} filesystem."
230 | fi
231 | fi
232 |
233 | if ! fallocate -l "$SYSTEM_SIZE" "$SYSTEM" &>/dev/null; then
234 | if ! fallocate -l -x "$SYSTEM_SIZE" "$SYSTEM"; then
235 | if ! truncate -s "$SYSTEM_SIZE" "$SYSTEM"; then
236 | rm -f "$SYSTEM"
237 | error "Could not allocate file $SYSTEM for the system disk." && exit 98
238 | fi
239 | fi
240 | fi
241 |
242 | PART="$TMP/partition.fdisk"
243 |
244 | { echo "label: dos"
245 | echo "label-id: 0x6f9ee2e9"
246 | echo "device: $SYSTEM"
247 | echo "unit: sectors"
248 | echo "sector-size: 512"
249 | echo ""
250 | echo "${SYSTEM}1 : start= 2048, size= 16777216, type=83"
251 | echo "${SYSTEM}2 : start= 16779264, size= 4194304, type=82"
252 | } > "$PART"
253 |
254 | sfdisk -q "$SYSTEM" < "$PART"
255 |
256 | MOUNT="$TMP/system"
257 | rm -rf "$MOUNT" && mkdir -p "$MOUNT"
258 |
259 | MSG="Extracting system partition..."
260 | info "Install: $MSG" && html "$MSG"
261 |
262 | HDA="$TMP/hda1"
263 | IDB="$TMP/indexdb"
264 | PKG="$TMP/packages"
265 | HDP="$TMP/synohdpack_img"
266 |
267 | [ ! -s "$HDA.tgz" ] && error "The PAT file contains no OS image." && exit 64
268 | mv "$HDA.tgz" "$HDA.txz"
269 |
270 | [ -d "$PKG" ] && mv "$PKG/" "$MOUNT/.SynoUpgradePackages/"
271 | rm -f "$MOUNT/.SynoUpgradePackages/ActiveInsight-"*
272 |
273 | if [ -s "$IDB.txz" ]; then
274 | INDEX_DB="$MOUNT/usr/syno/synoman/indexdb"
275 | mkdir -p "$INDEX_DB"
276 | fi
277 |
278 | LABEL="1.44.1-42218"
279 | OFFSET="1048576" # 2048 * 512
280 | NUMBLOCKS="2097152" # (16777216 * 512) / 4096
281 | MSG="Installing system partition..."
282 |
283 | fakeroot -- bash -c "set -Eeu;\
284 | [ -s $HDP.txz ] && tar xpfJ $HDP.txz --absolute-names -C $MOUNT/;\
285 | [ -s $IDB.txz ] && tar xpfJ $IDB.txz --absolute-names -C $INDEX_DB/;\
286 | tar xpfJ $HDA.txz --absolute-names --skip-old-files -C $MOUNT/;\
287 | printf '%b%s%b' '\E[1;34m❯ \E[1;36m' 'Install: $MSG' '\E[0m\n';\
288 | mke2fs -q -t ext4 -b 4096 -d $MOUNT/ -L $LABEL -F -E offset=$OFFSET $SYSTEM $NUMBLOCKS"
289 |
290 | rm -rf "$MOUNT"
291 | echo "$BASE" > "$STORAGE/dsm.ver"
292 |
293 | if [[ "$URL" == "file://$STORAGE/$BASE.pat" ]]; then
294 | rm -f "$PAT"
295 | else
296 | mv -f "$PAT" "$STORAGE/$BASE.pat"
297 | fi
298 |
299 | mv -f "$BOOT" "$STORAGE/$BASE.boot.img"
300 | rm -rf "$TMP"
301 |
302 | html "Booting DSM instance..."
303 | sleep 1.2
304 |
305 | return 0
306 |
--------------------------------------------------------------------------------
/src/network.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -Eeuo pipefail
3 |
4 | # Docker environment variables
5 |
6 | : "${MAC:=""}"
7 | : "${MTU:=""}"
8 | : "${DHCP:="N"}"
9 | : "${NETWORK:="Y"}"
10 | : "${USER_PORTS:=""}"
11 | : "${HOST_PORTS:=""}"
12 | : "${ADAPTER:="virtio-net-pci"}"
13 |
14 | : "${VM_NET_DEV:=""}"
15 | : "${VM_NET_TAP:="dsm"}"
16 | : "${VM_NET_MAC:="$MAC"}"
17 | : "${VM_NET_IP:="20.20.20.21"}"
18 | : "${VM_NET_HOST:="VirtualDSM"}"
19 |
20 | : "${DNSMASQ_OPTS:=""}"
21 | : "${DNSMASQ:="/usr/sbin/dnsmasq"}"
22 | : "${DNSMASQ_CONF_DIR:="/etc/dnsmasq.d"}"
23 |
24 | ADD_ERR="Please add the following setting to your container:"
25 |
26 | # ######################################
27 | # Functions
28 | # ######################################
29 |
30 | configureDHCP() {
31 |
32 | # Create the necessary file structure for /dev/vhost-net
33 | if [ ! -c /dev/vhost-net ]; then
34 | if mknod /dev/vhost-net c 10 238; then
35 | chmod 660 /dev/vhost-net
36 | fi
37 | fi
38 |
39 | # Create a macvtap network for the VM guest
40 | { msg=$(ip link add link "$VM_NET_DEV" name "$VM_NET_TAP" address "$VM_NET_MAC" type macvtap mode bridge 2>&1); rc=$?; } || :
41 |
42 | case "$msg" in
43 | "RTNETLINK answers: File exists"* )
44 | while ! ip link add link "$VM_NET_DEV" name "$VM_NET_TAP" address "$VM_NET_MAC" type macvtap mode bridge; do
45 | info "Waiting for macvtap interface to become available.."
46 | sleep 5
47 | done ;;
48 | "RTNETLINK answers: Invalid argument"* )
49 | error "Cannot create macvtap interface. Please make sure that the network type of the container is 'macvlan' and not 'ipvlan'."
50 | return 1 ;;
51 | "RTNETLINK answers: Operation not permitted"* )
52 | error "No permission to create macvtap interface. Please make sure that your host kernel supports it and that the NET_ADMIN capability is set."
53 | return 1 ;;
54 | *)
55 | [ -n "$msg" ] && echo "$msg" >&2
56 | if (( rc != 0 )); then
57 | error "Cannot create macvtap interface."
58 | return 1
59 | fi ;;
60 | esac
61 |
62 | if [[ "$MTU" != "0" ]] && [[ "$MTU" != "1500" ]]; then
63 | if ! ip link set dev "$VM_NET_TAP" mtu "$MTU"; then
64 | warn "Failed to set MTU size.."
65 | fi
66 | fi
67 |
68 | while ! ip link set "$VM_NET_TAP" up; do
69 | info "Waiting for MAC address $VM_NET_MAC to become available..."
70 | sleep 2
71 | done
72 |
73 | local TAP_NR TAP_PATH MAJOR MINOR
74 | TAP_NR=$(>"$TAP_PATH"; rc=$?; } 2>/dev/null || :
89 |
90 | if (( rc != 0 )); then
91 | error "Cannot create TAP interface ($rc). $ADD_ERR --device-cgroup-rule='c *:* rwm'" && return 1
92 | fi
93 |
94 | { exec 40>>/dev/vhost-net; rc=$?; } 2>/dev/null || :
95 |
96 | if (( rc != 0 )); then
97 | error "VHOST can not be found ($rc). $ADD_ERR --device=/dev/vhost-net" && return 1
98 | fi
99 |
100 | NET_OPTS="-netdev tap,id=hostnet0,vhost=on,vhostfd=40,fd=30"
101 |
102 | return 0
103 | }
104 |
105 | configureDNS() {
106 |
107 | # Create lease file for faster resolve
108 | echo "0 $VM_NET_MAC $VM_NET_IP $VM_NET_HOST 01:$VM_NET_MAC" > /var/lib/misc/dnsmasq.leases
109 | chmod 644 /var/lib/misc/dnsmasq.leases
110 |
111 | # dnsmasq configuration:
112 | DNSMASQ_OPTS+=" --dhcp-authoritative"
113 |
114 | # Set DHCP range and host
115 | DNSMASQ_OPTS+=" --dhcp-range=$VM_NET_IP,$VM_NET_IP"
116 | DNSMASQ_OPTS+=" --dhcp-host=$VM_NET_MAC,,$VM_NET_IP,$VM_NET_HOST,infinite"
117 |
118 | # Set DNS server and gateway
119 | DNSMASQ_OPTS+=" --dhcp-option=option:netmask,255.255.255.0"
120 | DNSMASQ_OPTS+=" --dhcp-option=option:router,${VM_NET_IP%.*}.1"
121 | DNSMASQ_OPTS+=" --dhcp-option=option:dns-server,${VM_NET_IP%.*}.1"
122 |
123 | # Add DNS entry for container
124 | DNSMASQ_OPTS+=" --address=/host.lan/${VM_NET_IP%.*}.1"
125 |
126 | DNSMASQ_OPTS=$(echo "$DNSMASQ_OPTS" | sed 's/\t/ /g' | tr -s ' ' | sed 's/^ *//')
127 |
128 | if [[ "${DEBUG_DNS:-}" == [Yy1]* ]]; then
129 | DNSMASQ_OPTS+=" -d"
130 | $DNSMASQ ${DNSMASQ_OPTS:+ $DNSMASQ_OPTS} &
131 | return 0
132 | fi
133 |
134 | if ! $DNSMASQ ${DNSMASQ_OPTS:+ $DNSMASQ_OPTS}; then
135 | error "Failed to start dnsmasq, reason: $?" && return 1
136 | fi
137 |
138 | return 0
139 | }
140 |
141 | getUserPorts() {
142 |
143 | local args=""
144 | local list=$1
145 | local ssh="22"
146 | local dsm="5000"
147 |
148 | [ -z "$list" ] && list="$ssh,$dsm" || list+=",$ssh,$dsm"
149 |
150 | list="${list//,/ }"
151 | list="${list## }"
152 | list="${list%% }"
153 |
154 | for port in $list; do
155 | args+="hostfwd=tcp::$port-$VM_NET_IP:$port,"
156 | done
157 |
158 | echo "${args%?}"
159 | return 0
160 | }
161 |
162 | getHostPorts() {
163 |
164 | local list=$1
165 |
166 | [ -z "$list" ] && echo "" && return 0
167 |
168 | if [[ "$list" != *","* ]]; then
169 | echo " ! --dport $list"
170 | else
171 | echo " -m multiport ! --dports $list"
172 | fi
173 |
174 | return 0
175 | }
176 |
177 | configureUser() {
178 |
179 | if [ -z "$IP6" ]; then
180 | 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"
181 | else
182 | NET_OPTS="-netdev user,id=hostnet0,ipv4=on,host=${VM_NET_IP%.*}.1,net=${VM_NET_IP%.*}.0/24,dhcpstart=$VM_NET_IP,ipv6=on,hostname=$VM_NET_HOST"
183 | fi
184 |
185 | local forward
186 | forward=$(getUserPorts "$USER_PORTS")
187 | [ -n "$forward" ] && NET_OPTS+=",$forward"
188 |
189 | return 0
190 | }
191 |
192 | configureNAT() {
193 |
194 | local tuntap="TUN device is missing. $ADD_ERR --device /dev/net/tun"
195 | local tables="The 'ip_tables' kernel module is not loaded. Try this command: sudo modprobe ip_tables iptable_nat"
196 |
197 | # Create the necessary file structure for /dev/net/tun
198 | if [ ! -c /dev/net/tun ]; then
199 | [[ "$PODMAN" == [Yy1]* ]] && return 1
200 | [ ! -d /dev/net ] && mkdir -m 755 /dev/net
201 | if mknod /dev/net/tun c 10 200; then
202 | chmod 666 /dev/net/tun
203 | fi
204 | fi
205 |
206 | if [ ! -c /dev/net/tun ]; then
207 | error "$tuntap" && return 1
208 | fi
209 |
210 | # Check port forwarding flag
211 | if [[ $(< /proc/sys/net/ipv4/ip_forward) -eq 0 ]]; then
212 | { sysctl -w net.ipv4.ip_forward=1 > /dev/null 2>&1; rc=$?; } || :
213 | if (( rc != 0 )) || [[ $(< /proc/sys/net/ipv4/ip_forward) -eq 0 ]]; then
214 | [[ "$PODMAN" == [Yy1]* ]] && return 1
215 | error "IP forwarding is disabled. $ADD_ERR --sysctl net.ipv4.ip_forward=1"
216 | return 1
217 | fi
218 | fi
219 |
220 | # Create a bridge with a static IP for the VM guest
221 | { ip link add dev dockerbridge type bridge ; rc=$?; } || :
222 |
223 | if (( rc != 0 )); then
224 | error "Failed to create bridge. $ADD_ERR --cap-add NET_ADMIN" && return 1
225 | fi
226 |
227 | if ! ip address add "${VM_NET_IP%.*}.1/24" broadcast "${VM_NET_IP%.*}.255" dev dockerbridge; then
228 | error "Failed to add IP address pool!" && return 1
229 | fi
230 |
231 | while ! ip link set dockerbridge up; do
232 | info "Waiting for IP address to become available..."
233 | sleep 2
234 | done
235 |
236 | # QEMU Works with taps, set tap to the bridge created
237 | if ! ip tuntap add dev "$VM_NET_TAP" mode tap; then
238 | error "$tuntap" && return 1
239 | fi
240 |
241 | if [[ "$MTU" != "0" ]] && [[ "$MTU" != "1500" ]]; then
242 | if ! ip link set dev "$VM_NET_TAP" mtu "$MTU"; then
243 | warn "Failed to set MTU size.."
244 | fi
245 | fi
246 |
247 | GATEWAY_MAC=$(echo "$VM_NET_MAC" | md5sum | sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\).*$/02:\1:\2:\3:\4:\5/')
248 |
249 | if ! ip link set dev "$VM_NET_TAP" address "$GATEWAY_MAC"; then
250 | warn "Failed to set gateway MAC address.."
251 | fi
252 |
253 | while ! ip link set "$VM_NET_TAP" up promisc on; do
254 | info "Waiting for TAP to become available..."
255 | sleep 2
256 | done
257 |
258 | if ! ip link set dev "$VM_NET_TAP" master dockerbridge; then
259 | error "Failed to set IP link!" && return 1
260 | fi
261 |
262 | # Add internet connection to the VM
263 | update-alternatives --set iptables /usr/sbin/iptables-legacy > /dev/null
264 | update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy > /dev/null
265 |
266 | exclude=$(getHostPorts "$HOST_PORTS")
267 |
268 | if ! iptables -t nat -A POSTROUTING -o "$VM_NET_DEV" -j MASQUERADE; then
269 | error "$tables" && return 1
270 | fi
271 |
272 | # shellcheck disable=SC2086
273 | if ! iptables -t nat -A PREROUTING -i "$VM_NET_DEV" -d "$IP" -p tcp${exclude} -j DNAT --to "$VM_NET_IP"; then
274 | error "Failed to configure IP tables!" && return 1
275 | fi
276 |
277 | if ! iptables -t nat -A PREROUTING -i "$VM_NET_DEV" -d "$IP" -p udp -j DNAT --to "$VM_NET_IP"; then
278 | error "Failed to configure IP tables!" && return 1
279 | fi
280 |
281 | if (( KERNEL > 4 )); then
282 | # Hack for guest VMs complaining about "bad udp checksums in 5 packets"
283 | iptables -A POSTROUTING -t mangle -p udp --dport bootpc -j CHECKSUM --checksum-fill > /dev/null 2>&1 || true
284 | fi
285 |
286 | NET_OPTS="-netdev tap,id=hostnet0,ifname=$VM_NET_TAP"
287 |
288 | if [ -c /dev/vhost-net ]; then
289 | { exec 40>>/dev/vhost-net; rc=$?; } 2>/dev/null || :
290 | (( rc == 0 )) && NET_OPTS+=",vhost=on,vhostfd=40"
291 | fi
292 |
293 | NET_OPTS+=",script=no,downscript=no"
294 |
295 | configureDNS || return 1
296 |
297 | return 0
298 | }
299 |
300 | closeBridge() {
301 |
302 | local pid="/var/run/dnsmasq.pid"
303 | [ -s "$pid" ] && pKill "$(<"$pid")"
304 |
305 | [[ "${NETWORK,,}" == "user"* ]] && return 0
306 |
307 | ip link set "$VM_NET_TAP" down promisc off &> null || true
308 | ip link delete "$VM_NET_TAP" &> null || true
309 |
310 | ip link set dockerbridge down &> null || true
311 | ip link delete dockerbridge &> null || true
312 |
313 | return 0
314 | }
315 |
316 | closeNetwork() {
317 |
318 | if [[ "$DHCP" == [Yy1]* ]]; then
319 |
320 | # Shutdown nginx
321 | nginx -s stop 2> /dev/null
322 | fWait "nginx"
323 |
324 | fi
325 |
326 | [[ "$NETWORK" == [Nn]* ]] && return 0
327 |
328 | exec 30<&- || true
329 | exec 40<&- || true
330 |
331 | if [[ "$DHCP" != [Yy1]* ]]; then
332 |
333 | closeBridge
334 | return 0
335 |
336 | fi
337 |
338 | ip link set "$VM_NET_TAP" down || true
339 | ip link delete "$VM_NET_TAP" || true
340 |
341 | return 0
342 | }
343 |
344 | checkOS() {
345 |
346 | local kernel
347 | local os=""
348 | local if="macvlan"
349 | kernel=$(uname -a)
350 |
351 | [[ "${kernel,,}" == *"darwin"* ]] && os="Docker Desktop for macOS"
352 | [[ "${kernel,,}" == *"microsoft"* ]] && os="Docker Desktop for Windows"
353 |
354 | if [[ "$DHCP" == [Yy1]* ]]; then
355 | if="macvtap"
356 | [[ "${kernel,,}" == *"synology"* ]] && os="Synology Container Manager"
357 | fi
358 |
359 | if [ -n "$os" ]; then
360 | warn "you are using $os which does not support $if, please revert to bridge networking!"
361 | fi
362 |
363 | return 0
364 | }
365 |
366 | getInfo() {
367 |
368 | if [ -z "$VM_NET_DEV" ]; then
369 | # Give Kubernetes priority over the default interface
370 | [ -d "/sys/class/net/net0" ] && VM_NET_DEV="net0"
371 | [ -d "/sys/class/net/net1" ] && VM_NET_DEV="net1"
372 | [ -d "/sys/class/net/net2" ] && VM_NET_DEV="net2"
373 | [ -d "/sys/class/net/net3" ] && VM_NET_DEV="net3"
374 | # Automaticly detect the default network interface
375 | [ -z "$VM_NET_DEV" ] && VM_NET_DEV=$(awk '$2 == 00000000 { print $1 }' /proc/net/route)
376 | [ -z "$VM_NET_DEV" ] && VM_NET_DEV="eth0"
377 | fi
378 |
379 | if [ ! -d "/sys/class/net/$VM_NET_DEV" ]; then
380 | error "Network interface '$VM_NET_DEV' does not exist inside the container!"
381 | error "$ADD_ERR -e \"VM_NET_DEV=NAME\" to specify another interface name." && exit 26
382 | fi
383 |
384 | BASE_IP="${VM_NET_IP%.*}."
385 |
386 | if [ "${VM_NET_IP/$BASE_IP/}" -lt "3" ]; then
387 | error "Invalid VM_NET_IP, must end in a higher number than .3" && exit 27
388 | fi
389 |
390 | if [ -z "$MTU" ]; then
391 | MTU=$(cat "/sys/class/net/$VM_NET_DEV/mtu")
392 | fi
393 |
394 | if [ "$MTU" -gt "1500" ]; then
395 | info "MTU size is too large: $MTU, ignoring..." && MTU="0"
396 | fi
397 |
398 | if [[ "${ADAPTER,,}" != "virtio-net-pci" ]]; then
399 | if [[ "$MTU" != "0" ]] && [[ "$MTU" != "1500" ]]; then
400 | warn "MTU size is $MTU, but cannot be set for $ADAPTER adapters!" && MTU="0"
401 | fi
402 | fi
403 |
404 | if [ -z "$VM_NET_MAC" ]; then
405 | local file="$STORAGE/dsm.mac"
406 | [ -s "$file" ] && VM_NET_MAC=$(<"$file")
407 | VM_NET_MAC="${VM_NET_MAC//[![:print:]]/}"
408 | if [ -z "$VM_NET_MAC" ]; then
409 | # Generate MAC address based on Docker container ID in hostname
410 | VM_NET_MAC=$(echo "$HOST" | md5sum | sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\).*$/02:11:32:\3:\4:\5/')
411 | echo "${VM_NET_MAC^^}" > "$file"
412 | fi
413 | fi
414 |
415 | VM_NET_MAC="${VM_NET_MAC^^}"
416 | VM_NET_MAC="${VM_NET_MAC//-/:}"
417 |
418 | if [[ ${#VM_NET_MAC} == 12 ]]; then
419 | m="$VM_NET_MAC"
420 | VM_NET_MAC="${m:0:2}:${m:2:2}:${m:4:2}:${m:6:2}:${m:8:2}:${m:10:2}"
421 | fi
422 |
423 | if [[ ${#VM_NET_MAC} != 17 ]]; then
424 | error "Invalid MAC address: '$VM_NET_MAC', should be 12 or 17 digits long!" && exit 28
425 | fi
426 |
427 | GATEWAY=$(ip route list dev "$VM_NET_DEV" | awk ' /^default/ {print $3}' | head -n 1)
428 | IP=$(ip address show dev "$VM_NET_DEV" | grep inet | awk '/inet / { print $2 }' | cut -f1 -d/ | head -n 1)
429 |
430 | IP6=""
431 | # shellcheck disable=SC2143
432 | if [ -f /proc/net/if_inet6 ] && [ -n "$(ifconfig -a | grep inet6)" ]; then
433 | IP6=$(ip -6 addr show dev "$VM_NET_DEV" scope global up)
434 | [ -n "$IP6" ] && IP6=$(echo "$IP6" | sed -e's/^.*inet6 \([^ ]*\)\/.*$/\1/;t;d' | head -n 1)
435 | fi
436 |
437 | [ -f "/run/.containerenv" ] && PODMAN="Y" || PODMAN="N"
438 | echo "$IP" > /run/shm/qemu.ip
439 |
440 | return 0
441 | }
442 |
443 | # ######################################
444 | # Configure Network
445 | # ######################################
446 |
447 | if [[ "$NETWORK" == [Nn]* ]]; then
448 | NET_OPTS=""
449 | return 0
450 | fi
451 |
452 | getInfo
453 | html "Initializing network..."
454 |
455 | if [[ "$DEBUG" == [Yy1]* ]]; then
456 | mtu=$(cat "/sys/class/net/$VM_NET_DEV/mtu")
457 | line="Host: $HOST IP: $IP Gateway: $GATEWAY Interface: $VM_NET_DEV MAC: $VM_NET_MAC MTU: $mtu"
458 | [[ "$MTU" != "0" ]] && [[ "$MTU" != "$mtu" ]] && line+=" ($MTU)"
459 | info "$line"
460 | if [ -f /etc/resolv.conf ]; then
461 | nameservers=$(grep '^nameserver*' /etc/resolv.conf | head -c -1 | sed 's/nameserver //g;' | sed -z 's/\n/, /g')
462 | [ -n "$nameservers" ] && info "Nameservers: $nameservers"
463 | fi
464 | echo
465 | fi
466 |
467 | if [[ "$IP" == "172.17."* ]]; then
468 | warn "your container IP starts with 172.17.* which will cause conflicts when you install the Container Manager package inside DSM!"
469 | fi
470 |
471 | if [[ -d "/sys/class/net/$VM_NET_TAP" ]]; then
472 | info "Lingering interface will be removed..."
473 | ip link delete "$VM_NET_TAP" || true
474 | fi
475 |
476 | if [[ "$DHCP" == [Yy1]* ]]; then
477 |
478 | checkOS
479 |
480 | if [[ "$IP" == "172."* ]]; then
481 | warn "container IP starts with 172.* which is often a sign that you are not on a macvlan network (required for DHCP)!"
482 | fi
483 |
484 | # Configure for macvtap interface
485 | configureDHCP || exit 20
486 |
487 | MSG="Booting DSM instance..."
488 | html "$MSG"
489 |
490 | else
491 |
492 | if [[ "$IP" != "172."* ]] && [[ "$IP" != "10.8"* ]] && [[ "$IP" != "10.9"* ]]; then
493 | checkOS
494 | fi
495 |
496 | # Shutdown nginx
497 | nginx -s stop 2> /dev/null
498 | fWait "nginx"
499 |
500 | if [[ "${NETWORK,,}" != "user"* ]]; then
501 |
502 | # Configure for tap interface
503 | if ! configureNAT; then
504 |
505 | closeBridge
506 | NETWORK="user"
507 | msg="falling back to user-mode networking!"
508 | if [[ "$PODMAN" != [Yy1]* ]]; then
509 | msg="an error occured, $msg"
510 | else
511 | msg="podman detected, $msg"
512 | fi
513 | warn "$msg"
514 | [ -z "$USER_PORTS" ] && info "Notice: port mapping will not work without \"USER_PORTS\" now."
515 |
516 | fi
517 |
518 | fi
519 |
520 | if [[ "${NETWORK,,}" == "user"* ]]; then
521 |
522 | # Configure for user-mode networking (slirp)
523 | configureUser || exit 24
524 |
525 | fi
526 |
527 | fi
528 |
529 | NET_OPTS+=" -device $ADAPTER,id=net0,netdev=hostnet0,romfile=,mac=$VM_NET_MAC"
530 | [[ "$MTU" != "0" ]] && [[ "$MTU" != "1500" ]] && NET_OPTS+=",host_mtu=$MTU"
531 |
532 | return 0
533 |
--------------------------------------------------------------------------------
/src/power.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -Eeuo pipefail
3 |
4 | # Configure QEMU for graceful shutdown
5 |
6 | API_CMD=6
7 | API_TIMEOUT=50
8 | API_HOST="127.0.0.1:2210"
9 |
10 | QEMU_TERM=""
11 | QEMU_PORT=7100
12 | QEMU_TIMEOUT=50
13 | QEMU_DIR="/run/shm"
14 | QEMU_PID="$QEMU_DIR/qemu.pid"
15 | QEMU_LOG="$QEMU_DIR/qemu.log"
16 | QEMU_OUT="$QEMU_DIR/qemu.out"
17 | QEMU_END="$QEMU_DIR/qemu.end"
18 |
19 | if [[ "$KVM" == [Nn]* ]]; then
20 | API_TIMEOUT=$(( API_TIMEOUT*2 ))
21 | QEMU_TIMEOUT=$(( QEMU_TIMEOUT*2 ))
22 | fi
23 |
24 | touch "$QEMU_LOG"
25 |
26 | _trap() {
27 | func="$1" ; shift
28 | for sig ; do
29 | trap "$func $sig" "$sig"
30 | done
31 | }
32 |
33 | finish() {
34 |
35 | local pid
36 | local reason=$1
37 |
38 | touch "$QEMU_END"
39 |
40 | if [ -s "$QEMU_PID" ]; then
41 |
42 | pid=$(<"$QEMU_PID")
43 | echo && error "Forcefully terminating QEMU process, reason: $reason..."
44 | { kill -15 "$pid" || true; } 2>/dev/null
45 |
46 | while isAlive "$pid"; do
47 | sleep 1
48 | # Workaround for zombie pid
49 | [ ! -s "$QEMU_PID" ] && break
50 | done
51 | fi
52 |
53 | fKill "print.sh"
54 | fKill "host.bin"
55 |
56 | closeNetwork
57 |
58 | sleep 1
59 | echo && echo "❯ Shutdown completed!"
60 |
61 | exit "$reason"
62 | }
63 |
64 | terminal() {
65 |
66 | local dev=""
67 |
68 | if [ -s "$QEMU_OUT" ]; then
69 |
70 | local msg
71 | msg=$(<"$QEMU_OUT")
72 |
73 | if [ -n "$msg" ]; then
74 |
75 | if [[ "${msg,,}" != "char"* || "$msg" != *"serial0)" ]]; then
76 | echo "$msg"
77 | fi
78 |
79 | dev="${msg#*/dev/p}"
80 | dev="/dev/p${dev%% *}"
81 |
82 | fi
83 | fi
84 |
85 | if [ ! -c "$dev" ]; then
86 | dev=$(echo 'info chardev' | nc -q 1 -w 1 localhost "$QEMU_PORT" | tr -d '\000')
87 | dev="${dev#*serial0}"
88 | dev="${dev#*pty:}"
89 | dev="${dev%%$'\n'*}"
90 | dev="${dev%%$'\r'*}"
91 | fi
92 |
93 | if [ ! -c "$dev" ]; then
94 | error "Device '$dev' not found!"
95 | finish 34 && return 34
96 | fi
97 |
98 | QEMU_TERM="$dev"
99 | return 0
100 | }
101 |
102 | _graceful_shutdown() {
103 |
104 | local code=$?
105 | local pid url response
106 |
107 | set +e
108 |
109 | if [ -f "$QEMU_END" ]; then
110 | echo && info "Received $1 signal while already shutting down..."
111 | return
112 | fi
113 |
114 | touch "$QEMU_END"
115 | echo && info "Received $1 signal, sending shutdown command..."
116 |
117 | if [ ! -s "$QEMU_PID" ]; then
118 | echo && error "QEMU PID file does not exist?"
119 | finish "$code" && return "$code"
120 | fi
121 |
122 | pid=$(<"$QEMU_PID")
123 |
124 | if ! isAlive "$pid"; then
125 | echo && error "QEMU process does not exist?"
126 | finish "$code" && return "$code"
127 | fi
128 |
129 | # Don't send the powerdown signal because vDSM ignores ACPI signals
130 | # echo 'system_powerdown' | nc -q 1 -w 1 localhost "${QEMU_PORT}" > /dev/null
131 |
132 | # Send shutdown command to guest agent via serial port
133 | url="http://$API_HOST/read?command=$API_CMD&timeout=$API_TIMEOUT"
134 | response=$(curl -sk -m "$(( API_TIMEOUT+2 ))" -S "$url" 2>&1)
135 |
136 | if [[ "$response" =~ "\"success\"" ]]; then
137 |
138 | echo && info "Virtual DSM is now ready to shutdown..."
139 |
140 | else
141 |
142 | response="${response#*message\"\: \"}"
143 | [ -z "$response" ] && response="second signal"
144 | echo && error "Forcefully terminating because of: ${response%%\"*}"
145 | { kill -15 "$pid" || true; } 2>/dev/null
146 |
147 | fi
148 |
149 | local cnt=0
150 |
151 | while [ "$cnt" -lt "$QEMU_TIMEOUT" ]; do
152 |
153 | ! isAlive "$pid" && break
154 |
155 | sleep 1
156 | cnt=$((cnt+1))
157 |
158 | [[ "$DEBUG" == [Yy1]* ]] && info "Shutting down, waiting... ($cnt/$QEMU_TIMEOUT)"
159 |
160 | # Workaround for zombie pid
161 | [ ! -s "$QEMU_PID" ] && break
162 |
163 | done
164 |
165 | if [ "$cnt" -ge "$QEMU_TIMEOUT" ]; then
166 | echo && error "Shutdown timeout reached, aborting..."
167 | fi
168 |
169 | finish "$code" && return "$code"
170 | }
171 |
172 | MON_OPTS="\
173 | -pidfile $QEMU_PID \
174 | -name $PROCESS,process=$PROCESS,debug-threads=on \
175 | -monitor telnet:localhost:$QEMU_PORT,server,nowait,nodelay"
176 |
177 | if [[ "$CONSOLE" != [Yy]* ]]; then
178 |
179 | MON_OPTS+=" -daemonize -D $QEMU_LOG"
180 |
181 | _trap _graceful_shutdown SIGTERM SIGHUP SIGINT SIGABRT SIGQUIT
182 |
183 | fi
184 |
185 | return 0
186 |
--------------------------------------------------------------------------------
/src/print.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -Eeuo pipefail
3 |
4 | : "${DHCP:="N"}"
5 | : "${NETWORK:="Y"}"
6 |
7 | [[ "$NETWORK" == [Nn]* ]] && exit 0
8 |
9 | info () { printf "%b%s%b" "\E[1;34m❯ \E[1;36m" "$1" "\E[0m\n" >&2; }
10 | error () { printf "%b%s%b" "\E[1;31m❯ " "ERROR: $1" "\E[0m\n" >&2; }
11 |
12 | file="/run/shm/dsm.url"
13 | info="/run/shm/msg.html"
14 | page="/run/shm/index.html"
15 | address="/run/shm/qemu.ip"
16 | shutdown="/run/shm/qemu.end"
17 | template="/var/www/index.html"
18 | url="http://127.0.0.1:2210/read?command=10"
19 |
20 | resp_err="Guest returned an invalid response:"
21 | curl_err="Failed to connect to guest: curl error"
22 | jq_err="Failed to parse response from guest: jq error"
23 |
24 | while [ ! -s "$file" ]
25 | do
26 |
27 | # Check if not shutting down
28 | [ -f "$shutdown" ] && exit 1
29 |
30 | sleep 3
31 |
32 | [ -f "$shutdown" ] && exit 1
33 | [ -s "$file" ] && break
34 |
35 | # Retrieve network info from guest VM
36 | { json=$(curl -m 20 -sk "$url"); rc=$?; } || :
37 |
38 | [ -f "$shutdown" ] && exit 1
39 | (( rc != 0 )) && error "$curl_err $rc" && continue
40 |
41 | { result=$(echo "$json" | jq -r '.status'); rc=$?; } || :
42 | (( rc != 0 )) && error "$jq_err $rc ( $json )" && continue
43 | [[ "$result" == "null" ]] && error "$resp_err $json" && continue
44 |
45 | if [[ "$result" != "success" ]] ; then
46 | { msg=$(echo "$json" | jq -r '.message'); rc=$?; } || :
47 | error "Guest replied $result: $msg" && continue
48 | fi
49 |
50 | { port=$(echo "$json" | jq -r '.data.data.dsm_setting.data.http_port'); rc=$?; } || :
51 | (( rc != 0 )) && error "$jq_err $rc ( $json )" && continue
52 | [[ "$port" == "null" ]] && error "$resp_err $json" && continue
53 | [ -z "$port" ] && continue
54 |
55 | { ip=$(echo "$json" | jq -r '.data.data.ip.data[] | select((.name=="eth0") and has("ip")).ip'); rc=$?; } || :
56 | (( rc != 0 )) && error "$jq_err $rc ( $json )" && continue
57 | [[ "$ip" == "null" ]] && error "$resp_err $json" && continue
58 |
59 | if [ -z "$ip" ]; then
60 | [[ "$DHCP" == [Yy1]* ]] && continue
61 | ip="20.20.20.21"
62 | fi
63 |
64 | echo "$ip:$port" > $file
65 |
66 | done
67 |
68 | [ -f "$shutdown" ] && exit 1
69 |
70 | location=$(<"$file")
71 |
72 | if [[ "$location" != "20.20"* ]]; then
73 |
74 | msg="http://$location"
75 | title="Virtual DSM"
76 | body="The location of DSM is http://$location"
77 | script=""
78 |
79 | HTML=$(<"$template")
80 | HTML="${HTML/\[1\]/$title}"
81 | HTML="${HTML/\[2\]/$script}"
82 | HTML="${HTML/\[3\]/$body}"
83 | HTML="${HTML/\[4\]/}"
84 | HTML="${HTML/\[5\]/}"
85 |
86 | echo "$HTML" > "$page"
87 | echo "$body" > "$info"
88 |
89 | else
90 |
91 | ip=$(<"$address")
92 | port="${location##*:}"
93 |
94 | if [[ "$ip" == "172."* ]]; then
95 | msg="port $port"
96 | else
97 | msg="http://$ip:$port"
98 | fi
99 |
100 | fi
101 |
102 | echo "" >&2
103 | info "-----------------------------------------------------------"
104 | info " You can now login to DSM at $msg"
105 | info "-----------------------------------------------------------"
106 | echo "" >&2
107 |
108 | exit 0
109 |
--------------------------------------------------------------------------------
/src/proc.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -Eeuo pipefail
3 |
4 | # Docker environment variables
5 |
6 | : "${KVM:="Y"}"
7 | : "${HOST_CPU:=""}"
8 | : "${CPU_FLAGS:=""}"
9 | : "${CPU_MODEL:=""}"
10 | : "${DEF_MODEL:="qemu64"}"
11 |
12 | CLOCKSOURCE="tsc"
13 | [[ "${ARCH,,}" == "arm64" ]] && CLOCKSOURCE="arch_sys_counter"
14 | CLOCK="/sys/devices/system/clocksource/clocksource0/current_clocksource"
15 |
16 | if [ ! -f "$CLOCK" ]; then
17 | warn "file \"$CLOCK\" cannot not found?"
18 | else
19 | result=$(<"$CLOCK")
20 | result="${result//[![:print:]]/}"
21 | case "${result,,}" in
22 | "${CLOCKSOURCE,,}" ) ;;
23 | "kvm-clock" ) info "Nested KVM virtualization detected.." ;;
24 | "hyperv_clocksource_tsc_page" ) info "Nested Hyper-V virtualization detected.." ;;
25 | "hpet" ) warn "unsupported clock source detected: '$result'. Please set host clock source to '$CLOCKSOURCE'." ;;
26 | *) warn "unexpected clock source detected: '$result'. Please set host clock source to '$CLOCKSOURCE'." ;;
27 | esac
28 | fi
29 |
30 | if [[ "${ARCH,,}" != "amd64" ]]; then
31 | KVM="N"
32 | warn "your CPU architecture is ${ARCH^^} and cannot provide KVM acceleration for x64 instructions, this will cause a major loss of performance."
33 | fi
34 |
35 | if [[ "$KVM" != [Nn]* ]]; then
36 |
37 | KVM_ERR=""
38 |
39 | if [ ! -e /dev/kvm ]; then
40 | KVM_ERR="(/dev/kvm is missing)"
41 | else
42 | if ! sh -c 'echo -n > /dev/kvm' &> /dev/null; then
43 | KVM_ERR="(/dev/kvm is unwriteable)"
44 | else
45 | flags=$(sed -ne '/^flags/s/^.*: //p' /proc/cpuinfo)
46 | if ! grep -qw "vmx\|svm" <<< "$flags"; then
47 | KVM_ERR="(not enabled in BIOS)"
48 | fi
49 | fi
50 | fi
51 |
52 | if [ -n "$KVM_ERR" ]; then
53 | KVM="N"
54 | if [[ "$OSTYPE" =~ ^darwin ]]; then
55 | warn "you are using macOS which has no KVM support, this will cause a major loss of performance."
56 | else
57 | kernel=$(uname -a)
58 | case "${kernel,,}" in
59 | *"microsoft"* )
60 | error "Please bind '/dev/kvm' as a volume in the optional container settings when using Docker Desktop." ;;
61 | *"synology"* )
62 | error "Please make sure that Synology VMM (Virtual Machine Manager) is installed and that '/dev/kvm' is binded to this container." ;;
63 | *)
64 | error "KVM acceleration is not available $KVM_ERR, this will cause a major loss of performance."
65 | error "See the FAQ for possible causes, or continue without it by adding KVM: \"N\" (not recommended)." ;;
66 | esac
67 | [[ "$DEBUG" != [Yy1]* ]] && exit 88
68 | fi
69 | fi
70 |
71 | fi
72 |
73 | if [[ "$KVM" != [Nn]* ]]; then
74 |
75 | CPU_FEATURES="kvm=on,l3-cache=on,+hypervisor"
76 | KVM_OPTS=",accel=kvm -enable-kvm -global kvm-pit.lost_tick_policy=discard"
77 |
78 | if ! grep -qw "sse4_2" <<< "$flags"; then
79 | info "Your CPU does not have the SSE4 instruction set that Virtual DSM requires, it will be emulated..."
80 | [ -z "$CPU_MODEL" ] && CPU_MODEL="$DEF_MODEL"
81 | CPU_FEATURES+=",+ssse3,+sse4.1,+sse4.2"
82 | fi
83 |
84 | if [ -z "$CPU_MODEL" ]; then
85 | CPU_MODEL="host"
86 | CPU_FEATURES+=",migratable=no"
87 | fi
88 |
89 | if grep -qw "svm" <<< "$flags"; then
90 |
91 | # AMD processor
92 |
93 | if grep -qw "tsc_scale" <<< "$flags"; then
94 | CPU_FEATURES+=",+invtsc"
95 | fi
96 |
97 | else
98 |
99 | # Intel processor
100 |
101 | vmx=$(sed -ne '/^vmx flags/s/^.*: //p' /proc/cpuinfo)
102 |
103 | if grep -qw "tsc_scaling" <<< "$vmx"; then
104 | CPU_FEATURES+=",+invtsc"
105 | fi
106 |
107 | fi
108 |
109 | else
110 |
111 | KVM_OPTS=""
112 | CPU_FEATURES="l3-cache=on,+hypervisor"
113 |
114 | if [[ "$ARCH" == "amd64" ]]; then
115 | KVM_OPTS=" -accel tcg,thread=multi"
116 | fi
117 |
118 | if [ -z "$CPU_MODEL" ]; then
119 | if [[ "$ARCH" == "amd64" ]]; then
120 | CPU_MODEL="max"
121 | CPU_FEATURES+=",migratable=no"
122 | else
123 | CPU_MODEL="$DEF_MODEL"
124 | fi
125 | fi
126 |
127 | CPU_FEATURES+=",+ssse3,+sse4.1,+sse4.2"
128 |
129 | fi
130 |
131 | if [ -z "$CPU_FLAGS" ]; then
132 | if [ -z "$CPU_FEATURES" ]; then
133 | CPU_FLAGS="$CPU_MODEL"
134 | else
135 | CPU_FLAGS="$CPU_MODEL,$CPU_FEATURES"
136 | fi
137 | else
138 | if [ -z "$CPU_FEATURES" ]; then
139 | CPU_FLAGS="$CPU_MODEL,$CPU_FLAGS"
140 | else
141 | CPU_FLAGS="$CPU_MODEL,$CPU_FEATURES,$CPU_FLAGS"
142 | fi
143 | fi
144 |
145 | if [ -z "$HOST_CPU" ]; then
146 | [[ "${CPU,,}" != "unknown" ]] && HOST_CPU="$CPU"
147 | fi
148 |
149 | if [ -n "$HOST_CPU" ]; then
150 | HOST_CPU="${HOST_CPU%%,*},,"
151 | else
152 | HOST_CPU="QEMU, Virtual CPU,"
153 | if [ "$ARCH" == "amd64" ]; then
154 | HOST_CPU+=" X86_64"
155 | else
156 | HOST_CPU+=" $ARCH"
157 | fi
158 | fi
159 |
160 | return 0
161 |
--------------------------------------------------------------------------------
/src/progress.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -Eeuo pipefail
3 |
4 | escape () {
5 | local s
6 | s=${1//&/\&}
7 | s=${s//\<}
8 | s=${s//>/\>}
9 | s=${s//'"'/\"}
10 | printf -- %s "$s"
11 | return 0
12 | }
13 |
14 | file="$1"
15 | total="$2"
16 | body=$(escape "$3")
17 | info="/run/shm/msg.html"
18 |
19 | if [[ "$body" == *"..." ]]; then
20 | body="${body/.../}
"
21 | fi
22 |
23 | while true
24 | do
25 | if [ -s "$file" ]; then
26 | bytes=$(du -sb "$file" | cut -f1)
27 | if (( bytes > 1000 )); then
28 | if [ -z "$total" ] || [[ "$total" == "0" ]]; then
29 | size=$(numfmt --to=iec --suffix=B "$bytes" | sed -r 's/([A-Z])/ \1/')
30 | else
31 | size="$(echo "$bytes" "$total" | awk '{printf "%.1f", $1 * 100 / $2}')"
32 | size="$size%"
33 | fi
34 | echo "${body//(\[P\])/($size)}"> "$info"
35 | fi
36 | fi
37 | sleep 1 & wait $!
38 | done
39 |
--------------------------------------------------------------------------------
/src/reset.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -Eeuo pipefail
3 |
4 | trap 'error "Status $? while: $BASH_COMMAND (line $LINENO/$BASH_LINENO)"' ERR
5 |
6 | [ ! -f "/run/entry.sh" ] && error "Script must run inside Docker container!" && exit 11
7 | [ "$(id -u)" -ne "0" ] && error "Script must be executed with root privileges." && exit 12
8 |
9 | echo "❯ Starting $APP for Docker v$($SUPPORT"
36 |
37 | CPU=$(cpu)
38 | SYS=$(uname -r)
39 | HOST=$(hostname -s)
40 | KERNEL=$(echo "$SYS" | cut -b 1)
41 | MINOR=$(echo "$SYS" | cut -d '.' -f2)
42 | ARCH=$(dpkg --print-architecture)
43 | CORES=$(grep -c '^processor' /proc/cpuinfo)
44 |
45 | if ! grep -qi "socket(s)" <<< "$(lscpu)"; then
46 | SOCKETS=1
47 | else
48 | SOCKETS=$(lscpu | grep -m 1 -i 'socket(s)' | awk '{print $(2)}')
49 | fi
50 |
51 | [ -n "${CPU_CORES//[0-9 ]}" ] && error "Invalid amount of CPU_CORES: $CPU_CORES" && exit 15
52 |
53 | # Check system
54 |
55 | if [ ! -d "/dev/shm" ]; then
56 | error "Directory /dev/shm not found!" && exit 14
57 | else
58 | [ ! -d "/run/shm" ] && ln -s /dev/shm /run/shm
59 | fi
60 |
61 | # Check folder
62 |
63 | if [[ "${COMMIT:-}" == [Yy1]* ]]; then
64 | STORAGE="/local"
65 | mkdir -p "$STORAGE"
66 | fi
67 |
68 | if [ ! -d "$STORAGE" ]; then
69 | error "Storage folder ($STORAGE) not found!" && exit 13
70 | fi
71 |
72 | # Check filesystem
73 | FS=$(stat -f -c %T "$STORAGE")
74 |
75 | if [[ "${FS,,}" == "ecryptfs" ]] || [[ "${FS,,}" == "tmpfs" ]]; then
76 | DISK_IO="threads"
77 | DISK_CACHE="writeback"
78 | fi
79 |
80 | # Read memory
81 | RAM_SPARE=500000000
82 | RAM_AVAIL=$(free -b | grep -m 1 Mem: | awk '{print $7}')
83 | RAM_TOTAL=$(free -b | grep -m 1 Mem: | awk '{print $2}')
84 |
85 | RAM_SIZE="${RAM_SIZE// /}"
86 | [ -z "$RAM_SIZE" ] && error "RAM_SIZE not specified!" && exit 16
87 |
88 | if [ -z "${RAM_SIZE//[0-9. ]}" ]; then
89 | [ "${RAM_SIZE%%.*}" -lt "130" ] && RAM_SIZE="${RAM_SIZE}G" || RAM_SIZE="${RAM_SIZE}M"
90 | fi
91 |
92 | RAM_SIZE=$(echo "${RAM_SIZE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
93 | ! numfmt --from=iec "$RAM_SIZE" &>/dev/null && error "Invalid RAM_SIZE: $RAM_SIZE" && exit 16
94 | RAM_WANTED=$(numfmt --from=iec "$RAM_SIZE")
95 | [ "$RAM_WANTED" -lt "136314880 " ] && error "RAM_SIZE is too low: $RAM_SIZE" && exit 16
96 |
97 | # Print system info
98 | SYS="${SYS/-generic/}"
99 | FS="${FS/UNKNOWN //}"
100 | FS="${FS/ext2\/ext3/ext4}"
101 | FS=$(echo "$FS" | sed 's/[)(]//g')
102 | SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
103 | SPACE_GB=$(formatBytes "$SPACE" "down")
104 | AVAIL_MEM=$(formatBytes "$RAM_AVAIL" "down")
105 | TOTAL_MEM=$(formatBytes "$RAM_TOTAL" "up")
106 |
107 | echo "❯ CPU: ${CPU} | RAM: ${AVAIL_MEM/ GB/}/$TOTAL_MEM | DISK: $SPACE_GB (${FS}) | KERNEL: ${SYS}..."
108 | echo
109 |
110 | # Check available memory
111 |
112 | if [[ "$RAM_CHECK" != [Nn]* ]] && (( (RAM_WANTED + RAM_SPARE) > RAM_AVAIL )); then
113 | AVAIL_MEM=$(formatBytes "$RAM_AVAIL")
114 | 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."
115 | [[ "${FS,,}" != "zfs" ]] && error "$msg" && exit 17
116 | info "$msg"
117 | fi
118 |
119 | # Cleanup files
120 | rm -f /run/shm/qemu.*
121 | rm -f /run/shm/dsm.url
122 |
123 | # Cleanup dirs
124 | rm -rf /tmp/dsm
125 | rm -rf "$STORAGE/tmp"
126 |
127 | getCountry() {
128 | local url=$1
129 | local query=$2
130 | local rc json result
131 |
132 | { json=$(curl -m 5 -H "Accept: application/json" -sfk "$url"); rc=$?; } || :
133 | (( rc != 0 )) && return 0
134 |
135 | { result=$(echo "$json" | jq -r "$query" 2> /dev/null); rc=$?; } || :
136 | (( rc != 0 )) && return 0
137 |
138 | [[ ${#result} -ne 2 ]] && return 0
139 | [[ "${result^^}" == "XX" ]] && return 0
140 |
141 | COUNTRY="${result^^}"
142 |
143 | return 0
144 | }
145 |
146 | setCountry() {
147 |
148 | [[ "${TZ,,}" == "asia/harbin" ]] && COUNTRY="CN"
149 | [[ "${TZ,,}" == "asia/beijing" ]] && COUNTRY="CN"
150 | [[ "${TZ,,}" == "asia/urumqi" ]] && COUNTRY="CN"
151 | [[ "${TZ,,}" == "asia/kashgar" ]] && COUNTRY="CN"
152 | [[ "${TZ,,}" == "asia/shanghai" ]] && COUNTRY="CN"
153 | [[ "${TZ,,}" == "asia/chongqing" ]] && COUNTRY="CN"
154 |
155 | [ -z "$COUNTRY" ] && getCountry "https://api.ipapi.is" ".location.country_code"
156 | [ -z "$COUNTRY" ] && getCountry "https://ifconfig.co/json" ".country_iso"
157 | [ -z "$COUNTRY" ] && getCountry "https://api.ip2location.io" ".country_code"
158 | [ -z "$COUNTRY" ] && getCountry "https://ipinfo.io/json" ".country"
159 | [ -z "$COUNTRY" ] && getCountry "https://api.myip.com" ".cc"
160 |
161 | return 0
162 | }
163 |
164 | addPackage() {
165 | local pkg=$1
166 | local desc=$2
167 |
168 | if apt-mark showinstall | grep -qx "$pkg"; then
169 | return 0
170 | fi
171 |
172 | MSG="Installing $desc..."
173 | info "$MSG" && html "$MSG"
174 |
175 | [ -z "$COUNTRY" ] && setCountry
176 |
177 | if [[ "${COUNTRY^^}" == "CN" ]]; then
178 | sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources
179 | fi
180 |
181 | DEBIAN_FRONTEND=noninteractive apt-get -qq update
182 | DEBIAN_FRONTEND=noninteractive apt-get -qq --no-install-recommends -y install "$pkg" > /dev/null
183 |
184 | return 0
185 | }
186 |
187 | # shellcheck disable=SC2143
188 | if [ -f /proc/net/if_inet6 ] && [ -n "$(ifconfig -a | grep inet6)" ]; then
189 |
190 | sed -i "s/listen 5000 default_server;/listen [::]:5000 default_server ipv6only=off;/g" /etc/nginx/sites-enabled/web.conf
191 |
192 | else
193 |
194 | sed -i "s/listen [::]:5000 default_server ipv6only=off;/listen 5000 default_server;/g" /etc/nginx/sites-enabled/web.conf
195 |
196 | fi
197 |
198 | # Start webserver
199 | cp -r /var/www/* /run/shm
200 | html "Starting $APP for Docker..."
201 | nginx -e stderr
202 |
203 | return 0
204 |
--------------------------------------------------------------------------------
/src/serial.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -Eeuo pipefail
3 |
4 | # Docker environment variables
5 |
6 | : "${HOST_MAC:=""}"
7 | : "${HOST_DEBUG:=""}"
8 | : "${HOST_SERIAL:=""}"
9 | : "${HOST_MODEL:=""}"
10 | : "${GUEST_SERIAL:=""}"
11 |
12 | if [ -n "$HOST_MAC" ]; then
13 |
14 | HOST_MAC="${HOST_MAC//-/:}"
15 |
16 | if [[ ${#HOST_MAC} == 12 ]]; then
17 | m="$HOST_MAC"
18 | HOST_MAC="${m:0:2}:${m:2:2}:${m:4:2}:${m:6:2}:${m:8:2}:${m:10:2}"
19 | fi
20 |
21 | if [[ ${#HOST_MAC} != 17 ]]; then
22 | error "Invalid HOST_MAC address: '$HOST_MAC', should be 12 or 17 digits long!" && exit 28
23 | fi
24 |
25 | fi
26 |
27 | HOST_ARGS=()
28 | HOST_ARGS+=("-cpu=$CPU_CORES")
29 | HOST_ARGS+=("-cpu_arch=$HOST_CPU")
30 |
31 | [ -n "$HOST_MAC" ] && HOST_ARGS+=("-mac=$HOST_MAC")
32 | [ -n "$HOST_MODEL" ] && HOST_ARGS+=("-model=$HOST_MODEL")
33 | [ -n "$HOST_SERIAL" ] && HOST_ARGS+=("-hostsn=$HOST_SERIAL")
34 | [ -n "$GUEST_SERIAL" ] && HOST_ARGS+=("-guestsn=$GUEST_SERIAL")
35 |
36 | if [[ "$HOST_DEBUG" == [Yy1]* ]]; then
37 | set -x
38 | ./host.bin "${HOST_ARGS[@]}" &
39 | { set +x; } 2>/dev/null
40 | echo
41 | else
42 | ./host.bin "${HOST_ARGS[@]}" >/dev/null &
43 | fi
44 |
45 | cnt=0
46 | sleep 0.2
47 |
48 | while ! nc -z -w2 127.0.0.1 2210 > /dev/null 2>&1; do
49 | sleep 0.1
50 | cnt=$((cnt + 1))
51 | (( cnt > 50 )) && error "Failed to connect to qemu-host.." && exit 58
52 | done
53 |
54 | cnt=0
55 |
56 | while ! nc -z -w2 127.0.0.1 12345 > /dev/null 2>&1; do
57 | sleep 0.1
58 | cnt=$((cnt + 1))
59 | (( cnt > 50 )) && error "Failed to connect to qemu-host.." && exit 59
60 | done
61 |
62 | # Configure serial ports
63 |
64 | if [[ "$CONSOLE" != [Yy]* ]]; then
65 | SERIAL_OPTS="-serial pty"
66 | else
67 | SERIAL_OPTS="-serial mon:stdio"
68 | fi
69 |
70 | SERIAL_OPTS+=" \
71 | -device virtio-serial-pci,id=virtio-serial0,bus=pcie.0,addr=0x3 \
72 | -chardev socket,id=charchannel0,host=127.0.0.1,port=12345,reconnect=10 \
73 | -device virtserialport,bus=virtio-serial0.0,nr=1,chardev=charchannel0,id=channel0,name=vchannel"
74 |
75 | return 0
76 |
--------------------------------------------------------------------------------
/src/utils.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -Eeuo pipefail
3 |
4 | # Helper functions
5 |
6 | info () { printf "%b%s%b" "\E[1;34m❯ \E[1;36m" "${1:-}" "\E[0m\n"; }
7 | error () { printf "%b%s%b" "\E[1;31m❯ " "ERROR: ${1:-}" "\E[0m\n" >&2; }
8 | warn () { printf "%b%s%b" "\E[1;31m❯ " "Warning: ${1:-}" "\E[0m\n" >&2; }
9 |
10 | formatBytes() {
11 | local result
12 | result=$(numfmt --to=iec --suffix=B "$1" | sed -r 's/([A-Z])/ \1/' | sed 's/ B/ bytes/g;')
13 | local unit="${result//[0-9. ]}"
14 | result="${result//[a-zA-Z ]/}"
15 | if [[ "${2:-}" == "up" ]]; then
16 | if [[ "$result" == *"."* ]]; then
17 | result="${result%%.*}"
18 | result=$((result+1))
19 | fi
20 | else
21 | if [[ "${2:-}" == "down" ]]; then
22 | result="${result%%.*}"
23 | fi
24 | fi
25 | echo "$result $unit"
26 | return 0
27 | }
28 |
29 | isAlive() {
30 | local pid="$1"
31 |
32 | if kill -0 "$pid" 2>/dev/null; then
33 | return 0
34 | fi
35 |
36 | return 1
37 | }
38 |
39 | pKill() {
40 | local pid="$1"
41 |
42 | { kill -15 "$pid" || true; } 2>/dev/null
43 |
44 | while isAlive "$pid"; do
45 | sleep 0.2
46 | done
47 |
48 | return 0
49 | }
50 |
51 | fWait() {
52 | local name="$1"
53 |
54 | while pgrep -f -l "$name" >/dev/null; do
55 | sleep 0.2
56 | done
57 |
58 | return 0
59 | }
60 |
61 | fKill() {
62 | local name="$1"
63 |
64 | { pkill -f "$name" || true; } 2>/dev/null
65 | fWait "$name"
66 |
67 | return 0
68 | }
69 |
70 | escape () {
71 | local s
72 | s=${1//&/\&}
73 | s=${s//\<}
74 | s=${s//>/\>}
75 | s=${s//'"'/\"}
76 | printf -- %s "$s"
77 | return 0
78 | }
79 |
80 | html() {
81 | local title
82 | local body
83 | local script
84 | local footer
85 |
86 | title=$(escape "$APP")
87 | title="$title"
88 | footer=$(escape "$FOOTER1")
89 |
90 | body=$(escape "$1")
91 | if [[ "$body" == *"..." ]]; then
92 | body="${body/.../}
"
93 | fi
94 |
95 | [ -n "${2:-}" ] && script="$2" || script=""
96 |
97 | local HTML
98 | HTML=$(<"$TEMPLATE")
99 | HTML="${HTML/\[1\]/$title}"
100 | HTML="${HTML/\[2\]/$script}"
101 | HTML="${HTML/\[3\]/$body}"
102 | HTML="${HTML/\[4\]/$footer}"
103 | HTML="${HTML/\[5\]/$FOOTER2}"
104 |
105 | echo "$HTML" > "$PAGE"
106 | echo "$body" > "$INFO"
107 |
108 | return 0
109 | }
110 |
111 | cpu() {
112 | local ret
113 | local cpu=""
114 |
115 | ret=$(lscpu)
116 |
117 | if grep -qi "model name" <<< "$ret"; then
118 | cpu=$(echo "$ret" | grep -m 1 -i 'model name' | cut -f 2 -d ":" | awk '{$1=$1}1' | sed 's# @.*##g' | sed s/"(R)"//g | sed 's/[^[:alnum:] ]\+/ /g' | sed 's/ */ /g')
119 | fi
120 |
121 | if [ -z "${cpu// /}" ] && grep -qi "model:" <<< "$ret"; then
122 | cpu=$(echo "$ret" | grep -m 1 -i 'model:' | cut -f 2 -d ":" | awk '{$1=$1}1' | sed 's# @.*##g' | sed s/"(R)"//g | sed 's/[^[:alnum:] ]\+/ /g' | sed 's/ */ /g')
123 | fi
124 |
125 | cpu="${cpu// CPU/}"
126 | cpu="${cpu// 4 Core/}"
127 | cpu="${cpu// 6 Core/}"
128 | cpu="${cpu// 8 Core/}"
129 | cpu="${cpu// 10 Core/}"
130 | cpu="${cpu// 12 Core/}"
131 | cpu="${cpu// 16 Core/}"
132 | cpu="${cpu// 32 Core/}"
133 | cpu="${cpu// 48 Core/}"
134 | cpu="${cpu// 64 Core/}"
135 | cpu="${cpu// 96 Core/}"
136 | cpu="${cpu// 128 Core/}"
137 | cpu="${cpu//7th Gen /}"
138 | cpu="${cpu//8th Gen /}"
139 | cpu="${cpu//9th Gen /}"
140 | cpu="${cpu//10th Gen /}"
141 | cpu="${cpu//11th Gen /}"
142 | cpu="${cpu//12th Gen /}"
143 | cpu="${cpu//13th Gen /}"
144 | cpu="${cpu//14th Gen /}"
145 | cpu="${cpu//15th Gen /}"
146 | cpu="${cpu// Processor/}"
147 | cpu="${cpu// Quad core/}"
148 | cpu="${cpu// Dual core/}"
149 | cpu="${cpu// Octa core/}"
150 | cpu="${cpu// Core TM/ Core}"
151 | cpu="${cpu// with Radeon Graphics/}"
152 | cpu="${cpu// with Radeon Vega Graphics/}"
153 | cpu="${cpu// with Radeon Vega Mobile Gfx/}"
154 |
155 | [ -z "${cpu// /}" ] && cpu="Unknown"
156 |
157 | echo "$cpu"
158 | return 0
159 | }
160 |
161 | hasDisk() {
162 |
163 | [ -b "/disk" ] && return 0
164 | [ -b "/disk1" ] && return 0
165 | [ -b "/dev/disk1" ] && return 0
166 | [ -b "${DEVICE:-}" ] && return 0
167 |
168 | [ -z "${DISK_NAME:-}" ] && DISK_NAME="data"
169 | [ -s "$STORAGE/$DISK_NAME.img" ] && return 0
170 | [ -s "$STORAGE/$DISK_NAME.qcow2" ] && return 0
171 |
172 | return 1
173 | }
174 |
175 | return 0
176 |
--------------------------------------------------------------------------------
/web/conf/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 5000 default_server;
3 |
4 | autoindex on;
5 | tcp_nodelay on;
6 | server_tokens off;
7 | absolute_redirect off;
8 |
9 | error_log /dev/null;
10 | access_log /dev/null;
11 |
12 | include /etc/nginx/mime.types;
13 |
14 | gzip on;
15 | gzip_vary on;
16 | gzip_proxied any;
17 | gzip_comp_level 5;
18 | gzip_min_length 500;
19 | gzip_disable "msie6";
20 | gzip_types text/css text/javascript text/xml text/plain text/x-component application/javascript application/json application/xml application/rss+xml font/truetype font/opentype application/vnd.ms-fontobject image/svg+xml;
21 |
22 | add_header Cache-Control "no-cache";
23 |
24 | location / {
25 |
26 | root /run/shm;
27 | index index.html;
28 |
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/web/css/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | color: white;
3 | background-color: #125bdb;
4 | font-smoothing: antialiased;
5 | -webkit-font-smoothing: antialiased;
6 | -moz-osx-font-smoothing: grayscale;
7 | font-family: Verdana, Geneva, sans-serif;
8 | }
9 |
10 | #info {
11 | text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.25);
12 | }
13 |
14 | #content {
15 | text-align: center;
16 | padding: 20px;
17 | margin-top: 50px;
18 | }
19 |
20 | footer {
21 | width: 98%;
22 | position: fixed;
23 | bottom: 0px;
24 | height: 40px;
25 | text-align: center;
26 | color: #0c8aeb;
27 | text-shadow: 0 0 1px #0c8aeb;
28 | }
29 |
30 | #empty {
31 | height: 40px;
32 | /* Same height as footer */
33 | }
34 |
35 | a,
36 | a:hover,
37 | a:active,
38 | a:visited {
39 | color: white;
40 | }
41 |
42 | footer a:link,
43 | footer a:visited,
44 | footer a:active {
45 | color: #0c8aeb;
46 | }
47 |
48 | footer a:hover {
49 | color: #73e6ff;
50 | }
51 |
52 | .loading:after {
53 | content: " .";
54 | animation: dots 1s steps(5, end) infinite;
55 | }
56 |
57 | @keyframes dots {
58 |
59 | 0%,
60 | 20% {
61 | color: rgba(0, 0, 0, 0);
62 | text-shadow: 0.25em 0 0 rgba(0, 0, 0, 0), 0.5em 0 0 rgba(0, 0, 0, 0);
63 | }
64 |
65 | 40% {
66 | color: white;
67 | text-shadow: 0.25em 0 0 rgba(0, 0, 0, 0), 0.5em 0 0 rgba(0, 0, 0, 0);
68 | }
69 |
70 | 60% {
71 | text-shadow: 0.25em 0 0 white, 0.5em 0 0 rgba(0, 0, 0, 0);
72 | }
73 |
74 | 80%,
75 | 100% {
76 | text-shadow: 0.25em 0 0 white, 0.5em 0 0 white;
77 | }
78 | }
79 |
80 | .spinner_LWk7 {
81 | animation: spinner_GWy6 1.2s linear infinite, spinner_BNNO 1.2s linear infinite
82 | }
83 |
84 | .spinner_yOMU {
85 | animation: spinner_GWy6 1.2s linear infinite, spinner_pVqn 1.2s linear infinite;
86 | animation-delay: .15s
87 | }
88 |
89 | .spinner_KS4S {
90 | animation: spinner_GWy6 1.2s linear infinite, spinner_6uKB 1.2s linear infinite;
91 | animation-delay: .3s
92 | }
93 |
94 | .spinner_zVee {
95 | animation: spinner_GWy6 1.2s linear infinite, spinner_Qw4x 1.2s linear infinite;
96 | animation-delay: .45s
97 | }
98 |
99 | @keyframes spinner_GWy6 {
100 |
101 | 0%,
102 | 50% {
103 | width: 9px;
104 | height: 9px
105 | }
106 |
107 | 10% {
108 | width: 11px;
109 | height: 11px
110 | }
111 | }
112 |
113 | @keyframes spinner_BNNO {
114 |
115 | 0%,
116 | 50% {
117 | x: 1.5px;
118 | y: 1.5px
119 | }
120 |
121 | 10% {
122 | x: .5px;
123 | y: .5px
124 | }
125 | }
126 |
127 | @keyframes spinner_pVqn {
128 |
129 | 0%,
130 | 50% {
131 | x: 13.5px;
132 | y: 1.5px
133 | }
134 |
135 | 10% {
136 | x: 12.5px;
137 | y: .5px
138 | }
139 | }
140 |
141 | @keyframes spinner_6uKB {
142 |
143 | 0%,
144 | 50% {
145 | x: 13.5px;
146 | y: 13.5px
147 | }
148 |
149 | 10% {
150 | x: 12.5px;
151 | y: 12.5px
152 | }
153 | }
154 |
155 | @keyframes spinner_Qw4x {
156 |
157 | 0%,
158 | 50% {
159 | x: 1.5px;
160 | y: 13.5px
161 | }
162 |
163 | 10% {
164 | x: .5px;
165 | y: 12.5px
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/web/img/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [1]
6 |
7 |
8 |
9 |
10 | [2]
11 |
12 |
13 |
14 |
15 |
16 |
22 |
[3]
23 |
24 |
25 |
26 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/web/js/script.js:
--------------------------------------------------------------------------------
1 | var request;
2 | var interval = 1000;
3 |
4 | function getInfo() {
5 |
6 | var url = "msg.html";
7 |
8 | try {
9 |
10 | if (window.XMLHttpRequest) {
11 | request = new XMLHttpRequest();
12 | } else {
13 | throw "XMLHttpRequest not available!";
14 | }
15 |
16 | request.onreadystatechange = processInfo;
17 | request.open("GET", url, true);
18 | request.send();
19 |
20 | } catch (e) {
21 | var err = "Error: " + e.message;
22 | console.log(err);
23 | setError(err);
24 | }
25 | }
26 |
27 | function processInfo() {
28 | try {
29 | if (request.readyState != 4) {
30 | return true;
31 | }
32 |
33 | var msg = request.responseText;
34 | if (msg == null || msg.length == 0) {
35 | setInfo("Booting DSM instance", true);
36 | schedule();
37 | return false;
38 | }
39 |
40 | var notFound = (request.status == 404);
41 |
42 | if (request.status == 200) {
43 | if (msg.toLowerCase().indexOf("") !== -1) {
44 | notFound = true;
45 | } else {
46 | if (msg.toLowerCase().indexOf("href=") !== -1) {
47 | var div = document.createElement("div");
48 | div.innerHTML = msg;
49 | var url = div.querySelector("a").href;
50 | setTimeout(() => {
51 | window.location.assign(url);
52 | }, 3000);
53 | setInfo(msg);
54 | return true;
55 | } else {
56 | setInfo(msg);
57 | schedule();
58 | return true;
59 | }
60 | }
61 | }
62 |
63 | if (notFound) {
64 | setInfo("Connecting to web portal", true);
65 | reload();
66 | return true;
67 | }
68 |
69 | setError("Error: Received statuscode " + request.status);
70 | schedule();
71 | return false;
72 |
73 | } catch (e) {
74 | var err = "Error: " + e.message;
75 | console.log(err);
76 | setError(err);
77 | return false;
78 | }
79 | }
80 |
81 | function setInfo(msg, loading, error) {
82 |
83 | try {
84 | if (msg == null || msg.length == 0) {
85 | return false;
86 | }
87 |
88 | var el = document.getElementById("spinner");
89 |
90 | error = !!error;
91 | if (!error) {
92 | el.style.visibility = 'visible';
93 | } else {
94 | el.style.visibility = 'hidden';
95 | }
96 |
97 | loading = !!loading;
98 | if (loading) {
99 | msg = "" + msg + "
";
100 | }
101 |
102 | el = document.getElementById("info");
103 |
104 | if (el.innerHTML != msg) {
105 | el.innerHTML = msg;
106 | }
107 |
108 | return true;
109 |
110 | } catch (e) {
111 | console.log("Error: " + e.message);
112 | return false;
113 | }
114 | }
115 |
116 | function setError(text) {
117 | return setInfo(text, false, true);
118 | }
119 |
120 | function schedule() {
121 | setTimeout(getInfo, interval);
122 | }
123 |
124 | function reload() {
125 | setTimeout(() => {
126 | document.location.reload();
127 | }, 3000);
128 | }
129 |
130 | schedule();
131 |
--------------------------------------------------------------------------------