├── .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 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](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 | [![Stars](https://starchart.cc/vdsm/virtual-dsm.svg?variant=adaptive)](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///\>} 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$(/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///\>} 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 | 17 | 18 | 19 | 20 | 21 | 22 |

[3]

23 |
24 |
25 |
26 |
27 | [4]
28 | [5] 29 |
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 | --------------------------------------------------------------------------------