├── .dockerignore
├── .github
├── dependabot.yml
├── renovate.json
├── screen.jpg
└── workflows
│ ├── build.yml
│ ├── check.yml
│ ├── hub.yml
│ └── test.yml
├── .gitignore
├── Dockerfile
├── docker-compose.yml
├── license.md
├── luci-app-dsm
├── Makefile
├── luasrc
│ ├── controller
│ │ └── dsm.lua
│ ├── model
│ │ ├── cbi
│ │ │ └── dsm.lua
│ │ └── dsm.lua
│ └── view
│ │ └── dsm
│ │ └── status.htm
├── po
│ └── zh-cn
│ │ └── dsm.po
├── root
│ ├── etc
│ │ └── config
│ │ │ └── dsm
│ └── usr
│ │ └── libexec
│ │ └── istorec
│ │ ├── dsm.sh
│ │ └── dsmentry
│ │ └── dsmentry.sh
└── simple-install.sh
├── 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
└── web
├── css
└── style.css
├── img
└── favicon.svg
├── index.html
├── js
└── script.js
└── nginx.conf
/.dockerignore:
--------------------------------------------------------------------------------
1 | .dockerignore
2 | .git
3 | .github
4 | .gitignore
5 | .gitlab-ci.yml
6 | .gitmodules
7 | Dockerfile
8 | Dockerfile.archive
9 | docker-compose.yml
10 |
11 | *.md
12 |
13 |
--------------------------------------------------------------------------------
/.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/jannson/virtual-dsm/3cfe5b19575d99c7466db421b9a651cde49c7b19/.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@v5
77 | with:
78 | context: .
79 | push: true
80 | provenance: false
81 | platforms: linux/amd64,linux/arm64,linux/arm
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
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 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 DEBCONF_NOWARNINGS "yes"
14 | ARG DEBIAN_FRONTEND "noninteractive"
15 | ARG DEBCONF_NONINTERACTIVE_SEEN "true"
16 |
17 | RUN if [ "$TARGETPLATFORM" != "linux/amd64" ]; then extra="qemu-user"; fi \
18 | && apt-get update \
19 | && apt-get --no-install-recommends -y install \
20 | jq \
21 | tini \
22 | curl \
23 | cpio \
24 | wget \
25 | fdisk \
26 | unzip \
27 | nginx \
28 | procps \
29 | xz-utils \
30 | iptables \
31 | iproute2 \
32 | apt-utils \
33 | dnsmasq \
34 | fakeroot \
35 | net-tools \
36 | qemu-utils \
37 | ca-certificates \
38 | netcat-openbsd \
39 | qemu-system-x86 \
40 | "$extra" \
41 | && apt-get clean \
42 | && unlink /etc/nginx/sites-enabled/default \
43 | && sed -i 's/^worker_processes.*/worker_processes 1;/' /etc/nginx/nginx.conf \
44 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
45 |
46 | COPY ./src /run/
47 | COPY ./web /var/www/
48 | COPY --from=builder /qemu-host.bin /run/host.bin
49 |
50 | RUN chmod +x /run/*.sh && chmod +x /run/*.bin
51 | RUN mv /var/www/nginx.conf /etc/nginx/sites-enabled/web.conf
52 |
53 | VOLUME /storage
54 | EXPOSE 22 139 445 5000
55 |
56 | ENV RAM_SIZE "1G"
57 | ENV DISK_SIZE "16G"
58 | ENV CPU_CORES "1"
59 |
60 | ARG VERSION_ARG "0.0"
61 | RUN echo "$VERSION_ARG" > /run/version
62 |
63 | HEALTHCHECK --interval=60s --start-period=45s --retries=2 CMD /run/check.sh
64 |
65 | ENTRYPOINT ["/usr/bin/tini", "-s", "/run/entry.sh"]
66 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | dsm:
4 | container_name: dsm
5 | image: vdsm/virtual-dsm:latest
6 | environment:
7 | DISK_SIZE: "16G"
8 | RAM_SIZE: "1G"
9 | CPU_CORES: "1"
10 | devices:
11 | - /dev/kvm
12 | device_cgroup_rules:
13 | - 'c *:* rwm'
14 | cap_add:
15 | - NET_ADMIN
16 | ports:
17 | - 5000:5000
18 | volumes:
19 | - /var/dsm:/storage
20 | restart: on-failure
21 | stop_grace_period: 2m
22 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/luci-app-dsm/Makefile:
--------------------------------------------------------------------------------
1 |
2 |
3 | include $(TOPDIR)/rules.mk
4 |
5 | PKG_VERSION:=1.0.2-20240307
6 | PKG_RELEASE:=
7 |
8 | LUCI_TITLE:=LuCI support for Synology DSM
9 | LUCI_PKGARCH:=all
10 | LUCI_DEPENDS:=+lsblk +jsonfilter +docker +luci-lib-taskd +luci-lib-docker
11 |
12 | define Package/luci-app-dsm/conffiles
13 | /etc/config/dsm
14 | endef
15 |
16 | include $(TOPDIR)/feeds/luci/luci.mk
17 |
18 | # call BuildPackage - OpenWrt buildroot signature
19 |
--------------------------------------------------------------------------------
/luci-app-dsm/luasrc/controller/dsm.lua:
--------------------------------------------------------------------------------
1 |
2 | module("luci.controller.dsm", package.seeall)
3 |
4 | function index()
5 | entry({"admin", "services", "dsm"}, alias("admin", "services", "dsm", "config"), _("SynologyDSM"), 30).dependent = true
6 | entry({"admin", "services", "dsm", "config"}, cbi("dsm"))
7 | end
8 |
--------------------------------------------------------------------------------
/luci-app-dsm/luasrc/model/cbi/dsm.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | LuCI - Lua Configuration Interface
3 | ]]--
4 |
5 | local taskd = require "luci.model.tasks"
6 | local docker = require "luci.docker"
7 | local dsm_model = require "luci.model.dsm"
8 | local m, s, o
9 |
10 | m = taskd.docker_map("dsm", "main", "/usr/libexec/istorec/dsm.sh",
11 | translate("SynologyDSM"),
12 | translate("SynologyDSM is Virtual DSM in a Docker container. You can only run in Synology Device.")
13 | .. translate("OpenSource link:") .. ' https://github.com/vdsm/virtual-dsm')
14 |
15 | local dk = docker.new({socket_path="/var/run/docker.sock"})
16 | local dockerd_running = dk:_ping().code == 200
17 | local docker_info = dockerd_running and dk:info().body or {}
18 | local docker_aspace = 0
19 | if docker_info.DockerRootDir then
20 | local statvfs = nixio.fs.statvfs(docker_info.DockerRootDir)
21 | docker_aspace = statvfs and (statvfs.bavail * statvfs.bsize) or 0
22 | end
23 |
24 | s = m:section(SimpleSection, translate("Service Status"), translate("SynologyDSM status:"))
25 | s:append(Template("dsm/status"))
26 |
27 | s = m:section(TypedSection, "main", translate("Setup"),
28 | (docker_aspace < 2147483648 and
29 | (translate("The free space of Docker is less than 2GB, which may cause the installation to fail.")
30 | .. "
") or "") .. translate("The following parameters will only take effect during installation or upgrade:"))
31 | s.addremove=false
32 | s.anonymous=true
33 |
34 | o = s:option(Value, "port", translate("Port").."*")
35 | o.default = "5000"
36 | o.datatype = "port"
37 |
38 | local defaultNet = dsm_model.defaultNet()
39 |
40 | o = s:option(Value, "ip", translate("IP").."*")
41 | o.default = defaultNet.ip
42 | o.datatype = "string"
43 |
44 | o = s:option(Value, "gateway", translate("Gateway").."*")
45 | o.default = defaultNet.gateway
46 | o.datatype = "string"
47 |
48 | o = s:option(Value, "cpucore", translate("CPU core number").."*")
49 | o.rmempty = false
50 | o.datatype = "string"
51 | o:value("2", "2")
52 | o:value("4", "4")
53 | o:value("8", "8")
54 | o:value("16", "16")
55 | o.default = "2"
56 |
57 | o = s:option(Value, "ramsize", translate("RAM size").."*")
58 | o.rmempty = false
59 | o.datatype = "string"
60 | o:value("2G", "2G")
61 | o:value("4G", "4G")
62 | o:value("8G", "8G")
63 | o:value("16G", "16G")
64 | o.default = "2G"
65 |
66 | o = s:option(Value, "disksize", translate("Disk size").."*")
67 | o.rmempty = false
68 | o.datatype = "string"
69 | o:value("20G", "20G")
70 | o:value("40G", "40G")
71 | o:value("800G", "800G")
72 | o:value("160G", "160G")
73 | o.default = "40G"
74 |
75 | if dsm_model.hasGpu() then
76 | o = s:option(Flag, "gpu", translate("GPU"), translate("GPU accelerate"))
77 | o.default = 1
78 | o.rmempty = false
79 | end
80 |
81 | o = s:option(Value, "image_name", translate("Image").."*")
82 | o.rmempty = false
83 | o.datatype = "string"
84 | o.default = "vdsm/virtual-dsm"
85 |
86 | local blocks = dsm_model.blocks()
87 | local home = dsm_model.home()
88 |
89 | o = s:option(Value, "storage_path", translate("Storage path").."*")
90 | o.rmempty = false
91 | o.datatype = "string"
92 |
93 | local paths, default_path = dsm_model.find_paths(blocks, home, "Configs")
94 | for _, val in pairs(paths) do
95 | o:value(val, val)
96 | end
97 | o.default = default_path
98 |
99 | return m
100 |
101 |
--------------------------------------------------------------------------------
/luci-app-dsm/luasrc/model/dsm.lua:
--------------------------------------------------------------------------------
1 | local util = require "luci.util"
2 | local fs = require "nixio.fs"
3 | local jsonc = require "luci.jsonc"
4 |
5 | local dsm = {}
6 |
7 | dsm.blocks = function()
8 | local f = io.popen("lsblk -s -f -b -o NAME,FSSIZE,MOUNTPOINT --json", "r")
9 | local vals = {}
10 | if f then
11 | local ret = f:read("*all")
12 | f:close()
13 | local obj = jsonc.parse(ret)
14 | for _, val in pairs(obj["blockdevices"]) do
15 | local fsize = val["fssize"]
16 | if fsize ~= nil and string.len(fsize) > 10 and val["mountpoint"] then
17 | -- fsize > 1G
18 | vals[#vals+1] = val["mountpoint"]
19 | end
20 | end
21 | end
22 | return vals
23 | end
24 |
25 | dsm.home = function()
26 | local uci = require "luci.model.uci".cursor()
27 | local home_dirs = {}
28 | home_dirs["main_dir"] = uci:get_first("quickstart", "main", "main_dir", "/root")
29 | home_dirs["Configs"] = uci:get_first("quickstart", "main", "conf_dir", home_dirs["main_dir"].."/Configs")
30 | home_dirs["Public"] = uci:get_first("quickstart", "main", "pub_dir", home_dirs["main_dir"].."/Public")
31 | home_dirs["Downloads"] = uci:get_first("quickstart", "main", "dl_dir", home_dirs["Public"].."/Downloads")
32 | home_dirs["Caches"] = uci:get_first("quickstart", "main", "tmp_dir", home_dirs["main_dir"].."/Caches")
33 | return home_dirs
34 | end
35 |
36 | dsm.find_paths = function(blocks, home_dirs, path_name)
37 | local default_path = ''
38 | local configs = {}
39 |
40 | default_path = home_dirs[path_name] .. "/Dsm"
41 | if #blocks == 0 then
42 | table.insert(configs, default_path)
43 | else
44 | for _, val in pairs(blocks) do
45 | table.insert(configs, val .. "/" .. path_name .. "/Dsm")
46 | end
47 | local without_conf_dir = "/root/" .. path_name .. "/Dsm"
48 | if default_path == without_conf_dir then
49 | default_path = configs[1]
50 | end
51 | end
52 |
53 | return configs, default_path
54 | end
55 |
56 | dsm.hasGpu = function()
57 | return fs.stat("/dev/dri", "type") == "dir"
58 | end
59 |
60 | local function findLast(haystack, needle)
61 | local i=haystack:match(".*"..needle.."()")
62 | if i==nil then return nil else return i-1 end
63 | end
64 |
65 | dsm.defaultNet = function()
66 | local defNet = {}
67 | local ip = util.trim(util.exec("ubus call network.interface.lan status | jsonfilter -e '@[\"ipv4-address\"][0].address'"))
68 | local mask = util.trim(util.exec("ubus call network.interface.lan status | jsonfilter -e '@[\"ipv4-address\"][0].mask'"))
69 | if ip ~= nil and ip ~= "" then
70 | local p = findLast(ip, "%.")
71 | if p ~= nil then
72 | defNet["ip"] = string.sub(ip,1,p) .. "66"
73 | defNet["ipmask"] = string.sub(ip,1,p) .. "0/" .. mask
74 | end
75 | defNet["gateway"] = ip
76 | return defNet
77 | end
78 |
79 | defNet["ip"] = "192.168.100.77"
80 | defNet["ipmask"] = "192.168.100.0/24"
81 | defNet["gateway"] = "192.168.100.1"
82 | return defNet
83 | end
84 |
85 | return dsm
86 |
87 |
--------------------------------------------------------------------------------
/luci-app-dsm/luasrc/view/dsm/status.htm:
--------------------------------------------------------------------------------
1 | <%
2 | local util = require "luci.util"
3 | local status_port_ip = util.trim(util.exec("/usr/libexec/istorec/dsm.sh status_port_ip"))
4 | local params = {}
5 | for param in status_port_ip:gmatch("%S+") do
6 | table.insert(params, param)
7 | end
8 | local container_install = params[1] ~= "not_install"
9 | local container_running = params[1] == "running"
10 | local port = params[2]
11 | local ip = params[3]
12 | local dockerid=""
13 | if table.getn(params) > 3 then
14 | dockerid = params[4]
15 | end
16 | -%>
17 |
18 |
19 |
20 | <% if container_running then %>
21 |
22 | <% else %>
23 |
24 | <% end %>
25 |
26 |
27 | <%
28 | if container_running then
29 | -%>
30 |
36 | <% end %>
37 |
--------------------------------------------------------------------------------
/luci-app-dsm/po/zh-cn/dsm.po:
--------------------------------------------------------------------------------
1 | msgid ""
2 | msgstr "Content-Type: text/plain; charset=UTF-8"
3 |
4 | msgid "SynologyDSM"
5 | msgstr "群晖DSM"
6 |
7 | msgid "OpenSource link:"
8 | msgstr "开源地址:"
9 |
10 | msgid "SynologyDSM is Virtual DSM in a Docker container. You can only run in Synology Device."
11 | msgstr "群晖DSM是运行在 Docker 的群晖系统。你只能运行在群晖的硬件上。"
12 |
13 | msgid "Storage path"
14 | msgstr "存储路径"
15 |
16 | msgid "Port"
17 | msgstr "端口"
18 |
19 | msgid "Service Status"
20 | msgstr "服务状态"
21 |
22 | msgid "SynologyDSM status:"
23 | msgstr "SynologyDSM 的状态信息如下:"
24 |
25 | msgid "Setup"
26 | msgstr "安装配置"
27 |
28 | msgid "The following parameters will only take effect during installation or upgrade:"
29 | msgstr "以下参数只在安装或者升级时才会生效:"
30 |
31 | msgid "Status"
32 | msgstr "状态"
33 |
34 | msgid "Running, click to show log"
35 | msgstr "运行中,点击看日志"
36 |
37 | msgid "DSM is not running"
38 | msgstr "DSM 未运行"
39 |
40 | msgid "Open DSM"
41 | msgstr "打开 DSM"
42 |
43 | msgid "The free space of Docker is less than 2GB, which may cause the installation to fail."
44 | msgstr "Docker 可用空间已不足2GB,可能导致安装失败。"
45 |
46 | msgid "IP"
47 | msgstr "独立IP"
48 |
49 | msgid "CPU core number"
50 | msgstr "核心数量"
51 |
52 | msgid "RAM size"
53 | msgstr "内存大小"
54 |
55 | msgid "Disk size"
56 | msgstr "硬盘大小"
57 |
58 |
--------------------------------------------------------------------------------
/luci-app-dsm/root/etc/config/dsm:
--------------------------------------------------------------------------------
1 | config main
2 | option 'port' '5000'
3 |
4 | # option 'ramsize' '2G'
5 | # option 'disksize' '40G'
6 | # option 'cpucore' '2'
7 | # option 'gpu' '1'
8 | # option 'storage_path' ''
9 | # option 'image_name' ''
10 | # option 'dhcp' '0'
11 | # option 'ip' ''
12 | # option 'gateway' ''
13 |
14 |
--------------------------------------------------------------------------------
/luci-app-dsm/root/usr/libexec/istorec/dsm.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # Author Xiaobao(xiaobao@linkease.com)
3 |
4 | ACTION=${1}
5 | shift 1
6 |
7 | do_install() {
8 | local dhcp=`uci get dsm.@main[0].dhcp 2>/dev/null`
9 | local port=`uci get dsm.@main[0].port 2>/dev/null`
10 | local ip=`uci get dsm.@main[0].ip 2>/dev/null`
11 | local gateway=`uci get dsm.@main[0].gateway 2>/dev/null`
12 | local ramsize=`uci get dsm.@main[0].ramsize 2>/dev/null`
13 | local disksize=`uci get dsm.@main[0].disksize 2>/dev/null`
14 | local cpucore=`uci get dsm.@main[0].cpucore 2>/dev/null`
15 | local gpu=`uci get dsm.@main[0].gpu 2>/dev/null`
16 | local image_name=`uci get dsm.@main[0].image_name 2>/dev/null`
17 | local storage_path=`uci get dsm.@main[0].storage_path 2>/dev/null`
18 |
19 | if [ -z "$storage_path" ]; then
20 | echo "storage path is empty!"
21 | exit 1
22 | fi
23 | if [ -z "$ip" ]; then
24 | echo "ip is empty!"
25 | exit 1
26 | fi
27 | if [ ! -e "/dev/kvm" ]; then
28 | echo "/dev/kvm not found"
29 | exit 1
30 | fi
31 | [ -z "$dhcp" ] && dhcp="0"
32 | [ -z "$port" ] && port="5000"
33 | [ -z "$ramsize" ] && ramsize="2G"
34 | [ -z "$disksize" ] && disksize="40G"
35 | [ -z "$cpucore" ] && cpucore="2"
36 | [ -z "$image_name" ] && image_name="vdsm/virtual-dsm"
37 | echo "docker pull ${image_name}"
38 | docker pull ${image_name}
39 | docker rm -f dsm
40 |
41 | local hasvlan=`docker network inspect dsm-net -f '{{.Name}}' 2>/dev/null`
42 | if [ ! "$hasvlan" = "dsm-net" ]; then
43 | docker network create -o com.docker.network.bridge.name=dsm-br --driver=bridge dsm-net
44 | fi
45 | local mask=`ubus call network.interface.lan status | jsonfilter -e '@["ipv4-address"][0].mask'`
46 |
47 | local cmd="docker run --entrypoint /usr/bin/tini --restart=unless-stopped -d -h SynologyDSMServer \
48 | -p $port:5000 \
49 | -v \"$storage_path:/storage\" \
50 | -v /usr/libexec/istorec/dsmentry:/dsmentry \
51 | -v /var/run/vmease:/var/run/vmease \
52 | -e DISK_SIZE=$disksize \
53 | -e RAM_SIZE=$ramsize \
54 | -e CPU_CORES=$cpucore \
55 | -e DHCP=$dhcp \
56 | -e DSMIP=$ip \
57 | -e DSMMASK=$mask \
58 | -e DSMGATEWAY=$gateway \
59 | --dns=223.5.5.5 \
60 | --net=dsm-net \
61 | --device /dev/kvm \
62 | --cap-add NET_ADMIN "
63 |
64 | if [ "$gpu" = "1" ]; then
65 | if [ -d /dev/dri ]; then
66 | cmd="$cmd\
67 | -e GPU=Y --device /dev/dri:/dev/dri "
68 | fi
69 | fi
70 |
71 | if [ "$macvlan" = "1" ]; then
72 | cmd="$cmd\
73 | --net=dsm-net "
74 | fi
75 |
76 | local tz="`uci get system.@system[0].zonename | sed 's/ /_/g'`"
77 | [ -z "$tz" ] || cmd="$cmd -e TZ=$tz"
78 |
79 | cmd="$cmd -v /mnt:/mnt"
80 | mountpoint -q /mnt && cmd="$cmd:rslave"
81 | cmd="$cmd --stop-timeout 120 --name dsm \"$image_name\" -s /dsmentry/dsmentry.sh "
82 |
83 | echo "$cmd"
84 | eval "$cmd"
85 | }
86 |
87 | usage() {
88 | echo "usage: $0 sub-command"
89 | echo "where sub-command is one of:"
90 | echo " install Install the dsm"
91 | echo " upgrade Upgrade the dsm"
92 | echo " rm/start/stop/restart Remove/Start/Stop/Restart the dsm"
93 | echo " status_port_ip SynologyDSM status"
94 | }
95 |
96 | case ${ACTION} in
97 | "install")
98 | do_install
99 | ;;
100 | "upgrade")
101 | do_install
102 | ;;
103 | "rm")
104 | docker rm -f dsm
105 | ;;
106 | "start" | "stop" | "restart")
107 | docker ${ACTION} dsm
108 | ;;
109 | "status")
110 | docker ps --all -f 'name=dsm' --format '{{.State}}'
111 | ;;
112 | "port")
113 | docker ps --all -f 'name=dsm' --format '{{.Ports}}' | grep -om1 '0.0.0.0:[0-9]*->5000/tcp' | sed 's/0.0.0.0:\([0-9]*\)->.*/\1/'
114 | ;;
115 | "status_port_ip")
116 | running=`docker ps --all -f 'name=dsm' --format '{{.State}}'`
117 | if [ -z "$running" ]; then
118 | running="not_install"
119 | fi
120 | port=`docker ps --all -f 'name=dsm' --format '{{.Ports}}' | grep -om1 '0.0.0.0:[0-9]*->5000/tcp' | sed 's/0.0.0.0:\([0-9]*\)->.*/\1/'`
121 | if [ -z "$port" ]; then
122 | port="5000"
123 | fi
124 | ip=`uci get dsm.@main[0].ip 2>/dev/null`
125 | if [ -z "$ip" ]; then
126 | ip="127.0.0.1"
127 | fi
128 | dockerid=`docker inspect --format="{{.Id}}" dsm`
129 | echo "$running $port $ip $dockerid"
130 | ;;
131 | *)
132 | usage
133 | exit 1
134 | ;;
135 | esac
136 |
--------------------------------------------------------------------------------
/luci-app-dsm/root/usr/libexec/istorec/dsmentry/dsmentry.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | br=dsm-br
4 | vnet1=dsm-int
5 | vnet2=dsm-ext
6 | curl -H "Content-Type: application/json" -X POST \
7 | -d '{"br":"'$br'","vnet1":"'$vnet1'","vnet2":"'$vnet2'"}' \
8 | --fail --max-time 15 --unix-socket /var/run/vmease/daemon.sock \
9 | "http://localhost/api/vmease/create-br/"
10 |
11 | ip addr flush dev eth0
12 | ip addr add $DSMIP/$DSMMASK dev eth0
13 | ip route add default via $DSMGATEWAY dev eth0
14 |
15 | bash /run/entry.sh
16 |
17 |
--------------------------------------------------------------------------------
/luci-app-dsm/simple-install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # run in router
4 | APPNAME=$1
5 |
6 | CURR=`pwd`
7 | if [ -z "${APPNAME}" ]; then
8 | APPNAME=`basename "$CURR"|cut -d '-' -f 3`
9 | fi
10 |
11 | if [ -z "${APPNAME}" ]; then
12 | echo "please run in luci-app-xxx paths"
13 | exit 1
14 | fi
15 |
16 | if [ ! -d luasrc ]; then
17 | echo "luasrc not found, please run in luci-app-xxx paths"
18 | exit 1
19 | fi
20 |
21 | mkdir -p /usr/lib/lua/luci/view/${APPNAME}
22 | cp ./luasrc/controller/${APPNAME}.lua /usr/lib/lua/luci/controller/
23 | cp ./luasrc/view/${APPNAME}/* /usr/lib/lua/luci/view/${APPNAME}/
24 | cp -rf ./luasrc/model/* /usr/lib/lua/luci/model/
25 | cp -rf ./root/* /
26 | rm -rf /tmp/luci-*
27 |
28 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | Virtual DSM
2 |
3 |

4 |
5 |
6 |
7 | [![Build]][build_url]
8 | [![Version]][tag_url]
9 | [![Size]][tag_url]
10 | [![Pulls]][hub_url]
11 |
12 |
13 |
14 | Virtual DSM in a Docker container.
15 |
16 | ## Features
17 |
18 | - Multiple disks
19 | - KVM acceleration
20 | - Upgrades supported
21 |
22 | ## Usage
23 |
24 | Via `docker-compose.yml`
25 |
26 | ```yaml
27 | version: "3"
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 | cap_add:
37 | - NET_ADMIN
38 | ports:
39 | - 5000:5000
40 | volumes:
41 | - /var/dsm:/storage
42 | restart: on-failure
43 | stop_grace_period: 2m
44 | ```
45 |
46 | Via `docker run`
47 |
48 | ```bash
49 | docker run -it --rm --name dsm -p 5000:5000 --device=/dev/kvm --cap-add NET_ADMIN --stop-timeout 120 vdsm/virtual-dsm
50 | ```
51 |
52 | ## FAQ
53 |
54 | * ### How do I use it?
55 |
56 | Very simple! These are the steps:
57 |
58 | - Start the container and connect to [port 5000](http://localhost:5000) using your web browser.
59 |
60 | - Wait until DSM is ready, choose an username and password, and you will be taken to the desktop.
61 |
62 | Enjoy your brand new machine, and don't forget to star this repo!
63 |
64 | * ### How do I change the size of the disk?
65 |
66 | To expand the default size of 16 GB, locate the `DISK_SIZE` setting in your compose file and modify it to your preferred capacity:
67 |
68 | ```yaml
69 | environment:
70 | DISK_SIZE: "128G"
71 | ```
72 |
73 | This can also be used to resize the existing disk to a larger capacity without any data loss.
74 |
75 | * ### How do I change the storage location?
76 |
77 | To change the storage location, include the following bind mount in your compose file:
78 |
79 | ```yaml
80 | volumes:
81 | - /var/dsm:/storage
82 | ```
83 |
84 | Replace the example path `/var/dsm` with the desired storage folder.
85 |
86 | * ### How do I create a growable disk?
87 |
88 | By default, the entire capacity of the disk is reserved in advance.
89 |
90 | To create a growable disk that only allocates space that is actually used, add the following environment variable:
91 |
92 | ```yaml
93 | environment:
94 | DISK_FMT: "qcow2"
95 | ```
96 |
97 | Please note that this may reduce the write performance of the disk.
98 |
99 | * ### How do I add multiple disks?
100 |
101 | To create additional disks, modify your compose file like this:
102 |
103 | ```yaml
104 | environment:
105 | DISK2_SIZE: "32G"
106 | DISK3_SIZE: "64G"
107 | volumes:
108 | - /home/example:/storage2
109 | - /mnt/data/example:/storage3
110 | ```
111 |
112 | * ### How do I pass-through a disk?
113 |
114 | It is possible to pass-through disk devices directly by adding them to your compose file in this way:
115 |
116 | ```yaml
117 | environment:
118 | DEVICE2: "/dev/sda"
119 | DEVICE3: "/dev/sdb"
120 | devices:
121 | - /dev/sda
122 | - /dev/sdb
123 | ```
124 |
125 | Please note that the device needs to be totally empty (without any partition table) otherwise DSM does not always format it into a volume.
126 |
127 | Do NOT use this feature with the goal of sharing files from the host, they will all be lost without warning when DSM creates the volume.
128 |
129 | * ### How do I increase the amount of CPU or RAM?
130 |
131 | By default, a single CPU core and 1 GB of RAM are allocated to the container.
132 |
133 | To increase this, add the following environment variables:
134 |
135 | ```yaml
136 | environment:
137 | RAM_SIZE: "4G"
138 | CPU_CORES: "4"
139 | ```
140 |
141 | * ### How do I verify if my system supports KVM?
142 |
143 | To verify if your system supports KVM, run the following commands:
144 |
145 | ```bash
146 | sudo apt install cpu-checker
147 | sudo kvm-ok
148 | ```
149 |
150 | If you receive an error from `kvm-ok` indicating that KVM acceleration can't be used, check the virtualization settings in the BIOS.
151 |
152 | * ### How do I assign an individual IP address to the container?
153 |
154 | By default, the container uses bridge networking, which shares the IP address with the host.
155 |
156 | If you want to assign an individual IP address to the container, you can create a macvlan network as follows:
157 |
158 | ```bash
159 | docker network create -d macvlan \
160 | --subnet=192.168.0.0/24 \
161 | --gateway=192.168.0.1 \
162 | --ip-range=192.168.0.100/28 \
163 | -o parent=eth0 vdsm
164 | ```
165 |
166 | Be sure to modify these values to match your local subnet.
167 |
168 | Once you have created the network, change your compose file to look as follows:
169 |
170 | ```yaml
171 | services:
172 | dsm:
173 | container_name: dsm
174 | ....
175 | networks:
176 | vdsm:
177 | ipv4_address: 192.168.0.100
178 |
179 | networks:
180 | vdsm:
181 | external: true
182 | ```
183 |
184 | 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.
185 |
186 | Please note that 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.
187 |
188 | * ### How can DSM acquire an IP address from my router?
189 |
190 | After configuring the container for macvlan (see above), it is possible for DSM to become part of your home network by requesting an IP from your router, just like your other devices.
191 |
192 | To enable this mode, add the following lines to your compose file:
193 |
194 | ```yaml
195 | environment:
196 | DHCP: "Y"
197 | device_cgroup_rules:
198 | - 'c *:* rwm'
199 | ```
200 |
201 | Please note that even if you don't need DHCP, it's still recommended to enable this mode, as it prevents NAT issues and increases performance by using a `macvtap` interface. You can just set a static IP from the DSM control panel afterwards.
202 |
203 | * ### How do I pass-through the GPU?
204 |
205 | To pass-through your Intel GPU, add the following lines to your compose file:
206 |
207 | ```yaml
208 | environment:
209 | GPU: "Y"
210 | devices:
211 | - /dev/dri
212 | ```
213 |
214 | This can be used to enable the facial recognition function in Synology Photos for example.
215 |
216 | * ### How do I install a specific version of vDSM?
217 |
218 | By default, version 7.2 will be installed, but if you prefer an older version, you can add its download URL to your compose file as follows:
219 |
220 | ```yaml
221 | environment:
222 | URL: "https://global.synologydownload.com/download/DSM/release/7.0.1/42218/DSM_VirtualDSM_42218.pat"
223 | ```
224 |
225 | With this method, it is even possible to switch between different versions while keeping all your file data intact.
226 |
227 | * ### What are the differences compared to the standard DSM?
228 |
229 | There are only two minor differences: the Virtual Machine Manager package is not available, and Surveillance Station will not include any free licenses.
230 |
231 | * ### Is this project legal?
232 |
233 | 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 would be considered legal.
234 |
235 | 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.
236 |
237 | ## Stars
238 | [](https://starchart.cc/vdsm/virtual-dsm)
239 |
240 | ## Disclaimer
241 |
242 | 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.
243 |
244 | [build_url]: https://github.com/vdsm/virtual-dsm/
245 | [hub_url]: https://hub.docker.com/r/vdsm/virtual-dsm
246 | [tag_url]: https://hub.docker.com/r/vdsm/virtual-dsm/tags
247 |
248 | [Build]: https://github.com/vdsm/virtual-dsm/actions/workflows/build.yml/badge.svg
249 | [Size]: https://img.shields.io/docker/image-size/vdsm/virtual-dsm/latest?color=066da5&label=size
250 | [Pulls]: https://img.shields.io/docker/pulls/kroese/virtual-dsm.svg?style=flat&label=pulls&logo=docker
251 | [Version]: https://img.shields.io/docker/v/vdsm/virtual-dsm/latest?arch=amd64&sort=semver&color=066da5
252 |
--------------------------------------------------------------------------------
/src/check.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -Eeuo pipefail
3 |
4 | [ -f "/run/shm/qemu.end" ] && echo "QEMU is shutting down.." && exit 1
5 | [ ! -f "/run/shm/qemu.pid" ] && echo "QEMU is not running yet.." && exit 0
6 |
7 | file="/run/shm/dsm.url"
8 | address="/run/shm/qemu.ip"
9 |
10 | [ ! -f "$file" ] && echo "DSM has not enabled networking yet.." && exit 1
11 |
12 | location=$(<"$file")
13 |
14 | if ! curl -m 20 -ILfSs "http://$location/" > /dev/null; then
15 |
16 | if [[ "$location" == "20.20"* ]]; then
17 | ip="20.20.20.1"
18 | port="${location##*:}"
19 | echo "Failed to reach DSM at port $port"
20 | else
21 | echo "Failed to reach DSM at http://$location"
22 | ip=$(<"$address")
23 | fi
24 |
25 | echo "You might need to whitelist IP $ip in the DSM firewall." && exit 1
26 |
27 | fi
28 |
29 | echo "Healthcheck OK"
30 | exit 0
31 |
--------------------------------------------------------------------------------
/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,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="$DEV_OPTS -object rng-random,id=objrng0,filename=/dev/urandom"
10 | DEV_OPTS="$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 | return 0
16 |
--------------------------------------------------------------------------------
/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_turing'
7 | : "${DISK_FMT:="raw"}" # Disk file format, 'raw' by default for best performance
8 | : "${DISK_FLAGS:=""}" # Specifies the options for use with the qcow2 disk format
9 | : "${DISK_CACHE:="none"}" # Caching mode, can be set to 'writeback' for better performance
10 | : "${DISK_DISCARD:="on"}" # Controls whether unmap (TRIM) commands are passed to the host.
11 | : "${DISK_ROTATION:="1"}" # Rotation rate, set to 1 for SSD storage and increase for HDD
12 |
13 | BOOT="$STORAGE/$BASE.boot.img"
14 | SYSTEM="$STORAGE/$BASE.system.img"
15 |
16 | [ ! -f "$BOOT" ] && error "Virtual DSM boot-image does not exist ($BOOT)" && exit 81
17 | [ ! -f "$SYSTEM" ] && error "Virtual DSM system-image does not exist ($SYSTEM)" && exit 82
18 |
19 | DISK_OPTS="\
20 | -object iothread,id=io2 \
21 | -device virtio-scsi-pci,id=hw-synoboot,iothread=io2,bus=pcie.0,addr=0xa \
22 | -drive file=$BOOT,if=none,id=drive-synoboot,format=raw,cache=$DISK_CACHE,aio=$DISK_IO,discard=$DISK_DISCARD,detect-zeroes=on \
23 | -device scsi-hd,bus=hw-synoboot.0,channel=0,scsi-id=0,lun=0,drive=drive-synoboot,id=synoboot0,rotation_rate=$DISK_ROTATION,bootindex=1 \
24 | -device virtio-scsi-pci,id=hw-synosys,iothread=io2,bus=pcie.0,addr=0xb \
25 | -drive file=$SYSTEM,if=none,id=drive-synosys,format=raw,cache=$DISK_CACHE,aio=$DISK_IO,discard=$DISK_DISCARD,detect-zeroes=on \
26 | -device scsi-hd,bus=hw-synosys.0,channel=0,scsi-id=0,lun=0,drive=drive-synosys,id=synosys0,rotation_rate=$DISK_ROTATION,bootindex=2"
27 |
28 | fmt2ext() {
29 | local DISK_FMT=$1
30 |
31 | case "${DISK_FMT,,}" in
32 | qcow2)
33 | echo "qcow2"
34 | ;;
35 | raw)
36 | echo "img"
37 | ;;
38 | *)
39 | error "Unrecognized disk format: $DISK_FMT" && exit 78
40 | ;;
41 | esac
42 | }
43 |
44 | ext2fmt() {
45 | local DISK_EXT=$1
46 |
47 | case "${DISK_EXT,,}" in
48 | qcow2)
49 | echo "qcow2"
50 | ;;
51 | img)
52 | echo "raw"
53 | ;;
54 | *)
55 | error "Unrecognized file extension: .$DISK_EXT" && exit 78
56 | ;;
57 | esac
58 | }
59 |
60 | getSize() {
61 | local DISK_FILE=$1
62 | local DISK_EXT DISK_FMT
63 |
64 | DISK_EXT=$(echo "${DISK_FILE//*./}" | sed 's/^.*\.//')
65 | DISK_FMT=$(ext2fmt "$DISK_EXT")
66 |
67 | case "${DISK_FMT,,}" in
68 | raw)
69 | stat -c%s "$DISK_FILE"
70 | ;;
71 | qcow2)
72 | qemu-img info "$DISK_FILE" -f "$DISK_FMT" | grep '^virtual size: ' | sed 's/.*(\(.*\) bytes)/\1/'
73 | ;;
74 | *)
75 | error "Unrecognized disk format: $DISK_FMT" && exit 78
76 | ;;
77 | esac
78 | }
79 |
80 | isCow() {
81 | local FS=$1
82 |
83 | if [[ "${FS,,}" == "btrfs" ]]; then
84 | return 0
85 | fi
86 |
87 | return 1
88 | }
89 |
90 | createDisk() {
91 | local DISK_FILE=$1
92 | local DISK_SPACE=$2
93 | local DISK_DESC=$3
94 | local DISK_FMT=$4
95 | local FS=$5
96 | local DATA_SIZE DIR SPACE FA
97 |
98 | DATA_SIZE=$(numfmt --from=iec "$DISK_SPACE")
99 |
100 | rm -f "$DISK_FILE"
101 |
102 | if [[ "$ALLOCATE" != [Nn]* ]]; then
103 |
104 | # Check free diskspace
105 | DIR=$(dirname "$DISK_FILE")
106 | SPACE=$(df --output=avail -B 1 "$DIR" | tail -n 1)
107 |
108 | if (( DATA_SIZE > SPACE )); then
109 | local SPACE_GB=$(( (SPACE + 1073741823)/1073741824 ))
110 | error "Not enough free space to create a $DISK_DESC of $DISK_SPACE in $DIR, it has only $SPACE_GB GB available..."
111 | error "Please specify a smaller ${DISK_DESC^^}_SIZE or disable preallocation by setting ALLOCATE=N." && exit 76
112 | fi
113 | fi
114 |
115 | html "Creating a $DISK_DESC image..."
116 | info "Creating a $DISK_SPACE $DISK_TYPE $DISK_DESC image in $DISK_FMT format..."
117 |
118 | local FAIL="Could not create a $DISK_TYPE $DISK_FMT $DISK_DESC image of $DISK_SPACE ($DISK_FILE)"
119 |
120 | case "${DISK_FMT,,}" in
121 | raw)
122 |
123 | if isCow "$FS"; then
124 | if ! touch "$DISK_FILE"; then
125 | error "$FAIL" && exit 77
126 | fi
127 | { chattr +C "$DISK_FILE"; } || :
128 | fi
129 |
130 | if [[ "$ALLOCATE" == [Nn]* ]]; then
131 |
132 | # Create an empty file
133 | if ! truncate -s "$DATA_SIZE" "$DISK_FILE"; then
134 | rm -f "$DISK_FILE"
135 | error "$FAIL" && exit 77
136 | fi
137 |
138 | else
139 |
140 | # Create an empty file
141 | if ! fallocate -l "$DATA_SIZE" "$DISK_FILE"; then
142 | if ! truncate -s "$DATA_SIZE" "$DISK_FILE"; then
143 | rm -f "$DISK_FILE"
144 | error "$FAIL" && exit 77
145 | fi
146 | fi
147 |
148 | fi
149 | ;;
150 | qcow2)
151 |
152 | local DISK_PARAM="$DISK_ALLOC"
153 | isCow "$FS" && DISK_PARAM="$DISK_PARAM,nocow=on"
154 | [ -n "$DISK_FLAGS" ] && DISK_PARAM="$DISK_PARAM,$DISK_FLAGS"
155 |
156 | if ! qemu-img create -f "$DISK_FMT" -o "$DISK_PARAM" -- "$DISK_FILE" "$DATA_SIZE" ; then
157 | rm -f "$DISK_FILE"
158 | error "$FAIL" && exit 70
159 | fi
160 | ;;
161 | esac
162 |
163 | if isCow "$FS"; then
164 | FA=$(lsattr "$DISK_FILE")
165 | if [[ "$FA" != *"C"* ]]; then
166 | error "Failed to disable COW for $DISK_DESC image $DISK_FILE on ${FS^^} filesystem (returned $FA)"
167 | fi
168 | fi
169 |
170 | return 0
171 | }
172 |
173 | resizeDisk() {
174 | local DISK_FILE=$1
175 | local DISK_SPACE=$2
176 | local DISK_DESC=$3
177 | local DISK_FMT=$4
178 | local FS=$5
179 | local CUR_SIZE DATA_SIZE DIR SPACE
180 |
181 | CUR_SIZE=$(getSize "$DISK_FILE")
182 | DATA_SIZE=$(numfmt --from=iec "$DISK_SPACE")
183 | local REQ=$((DATA_SIZE-CUR_SIZE))
184 | (( REQ < 1 )) && error "Shrinking disks is not supported yet, please increase ${DISK_DESC^^}_SIZE." && exit 71
185 |
186 | if [[ "$ALLOCATE" != [Nn]* ]]; then
187 |
188 | # Check free diskspace
189 | DIR=$(dirname "$DISK_FILE")
190 | SPACE=$(df --output=avail -B 1 "$DIR" | tail -n 1)
191 |
192 | if (( REQ > SPACE )); then
193 | local SPACE_GB=$(( (SPACE + 1073741823)/1073741824 ))
194 | error "Not enough free space to resize $DISK_DESC to $DISK_SPACE in $DIR, it has only $SPACE_GB GB available.."
195 | error "Please specify a smaller ${DISK_DESC^^}_SIZE or disable preallocation by setting ALLOCATE=N." && exit 74
196 | fi
197 | fi
198 |
199 | local GB=$(( (CUR_SIZE + 1073741823)/1073741824 ))
200 | MSG="Resizing $DISK_DESC from ${GB}G to $DISK_SPACE..."
201 | info "$MSG" && html "$MSG"
202 |
203 | local FAIL="Could not resize the $DISK_TYPE $DISK_FMT $DISK_DESC image from ${GB}G to $DISK_SPACE ($DISK_FILE)"
204 |
205 | case "${DISK_FMT,,}" in
206 | raw)
207 |
208 | if [[ "$ALLOCATE" == [Nn]* ]]; then
209 |
210 | # Resize file by changing its length
211 | if ! truncate -s "$DATA_SIZE" "$DISK_FILE"; then
212 | error "$FAIL" && exit 75
213 | fi
214 |
215 | else
216 |
217 | # Resize file by allocating more space
218 | if ! fallocate -l "$DATA_SIZE" "$DISK_FILE"; then
219 | if ! truncate -s "$DATA_SIZE" "$DISK_FILE"; then
220 | error "$FAIL" && exit 75
221 | fi
222 | fi
223 |
224 | fi
225 | ;;
226 | qcow2)
227 |
228 | if ! qemu-img resize -f "$DISK_FMT" "--$DISK_ALLOC" "$DISK_FILE" "$DATA_SIZE" ; then
229 | error "$FAIL" && exit 72
230 | fi
231 |
232 | ;;
233 | esac
234 |
235 | return 0
236 | }
237 |
238 | convertDisk() {
239 | local SOURCE_FILE=$1
240 | local SOURCE_FMT=$2
241 | local DST_FILE=$3
242 | local DST_FMT=$4
243 | local DISK_BASE=$5
244 | local DISK_DESC=$6
245 | local FS=$7
246 |
247 | [ -f "$DST_FILE" ] && error "Conversion failed, destination file $DST_FILE already exists?" && exit 79
248 | [ ! -f "$SOURCE_FILE" ] && error "Conversion failed, source file $SOURCE_FILE does not exists?" && exit 79
249 |
250 | local TMP_FILE="$DISK_BASE.tmp"
251 | rm -f "$TMP_FILE"
252 |
253 | if [[ "$ALLOCATE" != [Nn]* ]]; then
254 |
255 | local DIR CUR_SIZE SPACE
256 |
257 | # Check free diskspace
258 | DIR=$(dirname "$TMP_FILE")
259 | CUR_SIZE=$(getSize "$SOURCE_FILE")
260 | SPACE=$(df --output=avail -B 1 "$DIR" | tail -n 1)
261 |
262 | if (( CUR_SIZE > SPACE )); then
263 | local SPACE_GB=$(( (SPACE + 1073741823)/1073741824 ))
264 | error "Not enough free space to convert $DISK_DESC to $DST_FMT in $DIR, it has only $SPACE_GB GB available..."
265 | error "Please free up some disk space or disable preallocation by setting ALLOCATE=N." && exit 76
266 | fi
267 | fi
268 |
269 | html "Converting $DISK_DESC to $DST_FMT..."
270 | info "Converting $DISK_DESC to $DST_FMT, please wait until completed..."
271 |
272 | local CONV_FLAGS="-p"
273 | local DISK_PARAM="$DISK_ALLOC"
274 | isCow "$FS" && DISK_PARAM="$DISK_PARAM,nocow=on"
275 |
276 | if [[ "$DST_FMT" != "raw" ]]; then
277 | if [[ "$ALLOCATE" == [Nn]* ]]; then
278 | CONV_FLAGS="$CONV_FLAGS -c"
279 | fi
280 | [ -n "$DISK_FLAGS" ] && DISK_PARAM="$DISK_PARAM,$DISK_FLAGS"
281 | fi
282 |
283 | # shellcheck disable=SC2086
284 | if ! qemu-img convert -f "$SOURCE_FMT" $CONV_FLAGS -o "$DISK_PARAM" -O "$DST_FMT" -- "$SOURCE_FILE" "$TMP_FILE"; then
285 | rm -f "$TMP_FILE"
286 | error "Failed to convert $DISK_TYPE $DISK_DESC image to $DST_FMT format in $DIR, is there enough space available?" && exit 79
287 | fi
288 |
289 | if [[ "$DST_FMT" == "raw" ]]; then
290 | if [[ "$ALLOCATE" != [Nn]* ]]; then
291 | # Work around qemu-img bug
292 | CUR_SIZE=$(stat -c%s "$TMP_FILE")
293 | if ! fallocate -l "$CUR_SIZE" "$TMP_FILE"; then
294 | error "Failed to allocate $CUR_SIZE bytes for $DISK_DESC image $TMP_FILE"
295 | fi
296 | fi
297 | fi
298 |
299 | rm -f "$SOURCE_FILE"
300 | mv "$TMP_FILE" "$DST_FILE"
301 |
302 | if isCow "$FS"; then
303 | FA=$(lsattr "$DST_FILE")
304 | if [[ "$FA" != *"C"* ]]; then
305 | error "Failed to disable COW for $DISK_DESC image $DST_FILE on ${FS^^} filesystem (returned $FA)"
306 | fi
307 | fi
308 |
309 | html "Conversion of $DISK_DESC completed..."
310 | info "Conversion of $DISK_DESC to $DST_FMT completed succesfully!"
311 |
312 | return 0
313 | }
314 |
315 | checkFS () {
316 | local FS=$1
317 | local DISK_FILE=$2
318 | local DISK_DESC=$3
319 | local DIR FA
320 |
321 | DIR=$(dirname "$DISK_FILE")
322 | [ ! -d "$DIR" ] && return 0
323 |
324 | if [[ "${FS,,}" == "overlay"* ]]; then
325 | info "Warning: the filesystem of $DIR is OverlayFS, this usually means it was binded to an invalid path!"
326 | fi
327 |
328 | if [[ "${FS,,}" == "fuse"* ]]; then
329 | info "Warning: the filesystem of $DIR is FUSE, this extra layer will negatively affect performance!"
330 | fi
331 |
332 | if isCow "$FS"; then
333 | if [ -f "$DISK_FILE" ]; then
334 | FA=$(lsattr "$DISK_FILE")
335 | if [[ "$FA" != *"C"* ]]; then
336 | info "Warning: COW (copy on write) is not disabled for $DISK_DESC image file $DISK_FILE, this is recommended on ${FS^^} filesystems!"
337 | fi
338 | fi
339 | fi
340 |
341 | return 0
342 | }
343 |
344 | createDevice () {
345 |
346 | local DISK_ID=$1
347 | local DISK_FILE=$2
348 | local DISK_INDEX=$3
349 | local DISK_ADDRESS=$4
350 | local DISK_FMT=$5
351 |
352 | echo "-drive file=$DISK_FILE,if=none,id=drive-$DISK_ID,format=$DISK_FMT,cache=$DISK_CACHE,aio=$DISK_IO,discard=$DISK_DISCARD,detect-zeroes=on \
353 | -device virtio-scsi-pci,id=hw-$DISK_ID,iothread=io2,bus=pcie.0,addr=$DISK_ADDRESS \
354 | -device scsi-hd,bus=hw-$DISK_ID.0,channel=0,scsi-id=0,lun=0,drive=drive-$DISK_ID,id=$DISK_ID,rotation_rate=$DISK_ROTATION,bootindex=$DISK_INDEX"
355 |
356 | return 0
357 | }
358 |
359 | addDisk () {
360 | local DISK_ID=$1
361 | local DISK_BASE=$2
362 | local DISK_EXT=$3
363 | local DISK_DESC=$4
364 | local DISK_SPACE=$5
365 | local DISK_INDEX=$6
366 | local DISK_ADDRESS=$7
367 | local DISK_FMT=$8
368 | local DISK_FILE="$DISK_BASE.$DISK_EXT"
369 | local DIR DATA_SIZE FS PREV_FMT PREV_EXT CUR_SIZE OPTS
370 |
371 | DIR=$(dirname "$DISK_FILE")
372 | [ ! -d "$DIR" ] && return 0
373 |
374 | [ -z "$DISK_SPACE" ] && DISK_SPACE="16G"
375 | DISK_SPACE=$(echo "${DISK_SPACE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
376 | DATA_SIZE=$(numfmt --from=iec "$DISK_SPACE")
377 |
378 | if (( DATA_SIZE < 6442450944 )); then
379 | if (( DATA_SIZE < 1 )); then
380 | error "Invalid value for ${DISK_DESC^^}_SIZE: $DISK_SPACE" && exit 73
381 | else
382 | error "Please increase ${DISK_DESC^^}_SIZE to at least 6 GB." && exit 73
383 | fi
384 | fi
385 |
386 | FS=$(stat -f -c %T "$DIR")
387 | checkFS "$FS" "$DISK_FILE" "$DISK_DESC" || exit $?
388 |
389 | if ! [ -f "$DISK_FILE" ] ; then
390 |
391 | if [[ "${DISK_FMT,,}" != "raw" ]]; then
392 | PREV_FMT="raw"
393 | else
394 | PREV_FMT="qcow2"
395 | fi
396 | PREV_EXT=$(fmt2ext "$PREV_FMT")
397 |
398 | if [ -f "$DISK_BASE.$PREV_EXT" ] ; then
399 | convertDisk "$DISK_BASE.$PREV_EXT" "$PREV_FMT" "$DISK_FILE" "$DISK_FMT" "$DISK_BASE" "$DISK_DESC" "$FS" || exit $?
400 | fi
401 | fi
402 |
403 | if [ -f "$DISK_FILE" ]; then
404 |
405 | CUR_SIZE=$(getSize "$DISK_FILE")
406 |
407 | if (( DATA_SIZE > CUR_SIZE )); then
408 | resizeDisk "$DISK_FILE" "$DISK_SPACE" "$DISK_DESC" "$DISK_FMT" "$FS" || exit $?
409 | fi
410 |
411 | else
412 |
413 | createDisk "$DISK_FILE" "$DISK_SPACE" "$DISK_DESC" "$DISK_FMT" "$FS" || exit $?
414 |
415 | fi
416 |
417 | OPTS=$(createDevice "$DISK_ID" "$DISK_FILE" "$DISK_INDEX" "$DISK_ADDRESS" "$DISK_FMT")
418 | DISK_OPTS="$DISK_OPTS $OPTS"
419 |
420 | return 0
421 | }
422 |
423 | addDevice () {
424 |
425 | local DISK_ID=$1
426 | local DISK_DEV=$2
427 | local DISK_DESC=$3
428 | local DISK_INDEX=$4
429 | local DISK_ADDRESS=$5
430 |
431 | [ -z "$DISK_DEV" ] && return 0
432 | [ ! -b "$DISK_DEV" ] && error "Device $DISK_DEV cannot be found! Please add it to the 'devices' section of your compose file." && exit 55
433 |
434 | local OPTS
435 | OPTS=$(createDevice "$DISK_ID" "$DISK_DEV" "$DISK_INDEX" "$DISK_ADDRESS" "raw")
436 | DISK_OPTS="$DISK_OPTS $OPTS"
437 |
438 | return 0
439 | }
440 |
441 | html "Initializing disks..."
442 |
443 | DISK_EXT=$(fmt2ext "$DISK_FMT")
444 |
445 | if [ -z "$ALLOCATE" ]; then
446 | if [[ "${DISK_FMT,,}" == "raw" ]]; then
447 | ALLOCATE="Y"
448 | else
449 | ALLOCATE="N"
450 | fi
451 | fi
452 |
453 | if [[ "$ALLOCATE" == [Nn]* ]]; then
454 | DISK_TYPE="growable"
455 | DISK_ALLOC="preallocation=off"
456 | else
457 | DISK_TYPE="preallocated"
458 | DISK_ALLOC="preallocation=falloc"
459 | fi
460 |
461 | DISK1_FILE="$STORAGE/data"
462 | if [[ ! -f "$DISK1_FILE.img" ]] && [[ -f "$STORAGE/data${DISK_SIZE}.img" ]]; then
463 | # Fallback for legacy installs
464 | mv "$STORAGE/data${DISK_SIZE}.img" "$DISK1_FILE.img"
465 | fi
466 |
467 | DISK2_FILE="/storage2/data2"
468 | if [ ! -f "$DISK2_FILE.img" ]; then
469 | # Fallback for legacy installs
470 | FALLBACK="/storage2/data.img"
471 | if [[ -f "$DISK1_FILE.img" ]] && [[ -f "$FALLBACK" ]]; then
472 | SIZE1=$(stat -c%s "$FALLBACK")
473 | SIZE2=$(stat -c%s "$DISK1_FILE.img")
474 | if [[ SIZE1 -ne SIZE2 ]]; then
475 | mv "$FALLBACK" "$DISK2_FILE.img"
476 | fi
477 | fi
478 | fi
479 |
480 | DISK3_FILE="/storage3/data3"
481 | if [ ! -f "$DISK3_FILE.img" ]; then
482 | # Fallback for legacy installs
483 | FALLBACK="/storage3/data.img"
484 | if [[ -f "$DISK1_FILE.img" ]] && [[ -f "$FALLBACK" ]]; then
485 | SIZE1=$(stat -c%s "$FALLBACK")
486 | SIZE2=$(stat -c%s "$DISK1_FILE.img")
487 | if [[ SIZE1 -ne SIZE2 ]]; then
488 | mv "$FALLBACK" "$DISK3_FILE.img"
489 | fi
490 | fi
491 | fi
492 |
493 | DISK4_FILE="/storage4/data4"
494 |
495 | : "${DISK2_SIZE:=""}"
496 | : "${DISK3_SIZE:=""}"
497 | : "${DISK4_SIZE:=""}"
498 |
499 | : "${DEVICE:=""}" # Docker variables to passthrough a block device, like /dev/vdc1.
500 | : "${DEVICE2:=""}"
501 | : "${DEVICE3:=""}"
502 | : "${DEVICE4:=""}"
503 |
504 | if [ -n "$DEVICE" ]; then
505 | addDevice "userdata" "$DEVICE" "device" "3" "0xc" || exit $?
506 | else
507 | addDisk "userdata" "$DISK1_FILE" "$DISK_EXT" "disk" "$DISK_SIZE" "3" "0xc" "$DISK_FMT" || exit $?
508 | fi
509 |
510 | if [ -n "$DEVICE2" ]; then
511 | addDevice "userdata2" "$DEVICE2" "device2" "4" "0xd" || exit $?
512 | else
513 | addDisk "userdata2" "$DISK2_FILE" "$DISK_EXT" "disk2" "$DISK2_SIZE" "4" "0xd" "$DISK_FMT" || exit $?
514 | fi
515 |
516 | if [ -n "$DEVICE3" ]; then
517 | addDevice "userdata3" "$DEVICE3" "device3" "5" "0xe" || exit $?
518 | else
519 | addDisk "userdata3" "$DISK3_FILE" "$DISK_EXT" "disk3" "$DISK3_SIZE" "5" "0xe" "$DISK_FMT" || exit $?
520 | fi
521 |
522 | if [ -n "$DEVICE4" ]; then
523 | addDevice "userdata4" "$DEVICE4" "device4" "6" "0xf" || exit $?
524 | else
525 | addDisk "userdata4" "$DISK4_FILE" "$DISK_EXT" "disk4" "$DISK4_SIZE" "6" "0xf" "$DISK_FMT" || exit $?
526 | fi
527 |
528 | html "Initialized disks successfully..."
529 | return 0
530 |
--------------------------------------------------------------------------------
/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 |
10 | if [[ "$GPU" != [Yy1]* ]] || [[ "$ARCH" != "amd64" ]]; then
11 |
12 | [[ "${DISPLAY,,}" == "none" ]] && VGA="none"
13 | DISPLAY_OPTS="-display $DISPLAY -vga $VGA"
14 | return 0
15 |
16 | fi
17 |
18 | DISPLAY_OPTS="-display egl-headless,rendernode=/dev/dri/renderD128"
19 | DISPLAY_OPTS="$DISPLAY_OPTS -vga $VGA"
20 |
21 | [ ! -d /dev/dri ] && mkdir -m 755 /dev/dri
22 |
23 | if [ ! -c /dev/dri/card0 ]; then
24 | if mknod /dev/dri/card0 c 226 0; then
25 | chmod 666 /dev/dri/card0
26 | fi
27 | fi
28 |
29 | if [ ! -c /dev/dri/renderD128 ]; then
30 | if mknod /dev/dri/renderD128 c 226 128; then
31 | chmod 666 /dev/dri/renderD128
32 | fi
33 | fi
34 |
35 | addPackage "xserver-xorg-video-intel" "Intel GPU drivers"
36 | addPackage "qemu-system-modules-opengl" "OpenGL module"
37 |
38 | return 0
39 |
--------------------------------------------------------------------------------
/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 | . reset.sh # Initialize system
10 | . install.sh # Run installation
11 | . disk.sh # Initialize disks
12 | . display.sh # Initialize graphics
13 | . network.sh # Initialize network
14 | . proc.sh # Initialize processor
15 | . serial.sh # Initialize serialport
16 | . power.sh # Configure shutdown
17 | . config.sh # Configure arguments
18 |
19 | trap - ERR
20 |
21 | info "Booting $APP using $VERS..."
22 | [[ "$DEBUG" == [Yy1]* ]] && echo "Arguments: $ARGS" && echo
23 |
24 | if [[ "$CONSOLE" == [Yy]* ]]; then
25 | exec qemu-system-x86_64 ${ARGS:+ $ARGS}
26 | fi
27 |
28 | { qemu-system-x86_64 ${ARGS:+ $ARGS} >"$QEMU_OUT" 2>"$QEMU_LOG"; rc=$?; } || :
29 | (( rc != 0 )) && error "$(<"$QEMU_LOG")" && exit 15
30 |
31 | terminal
32 | tail -fn +0 "$QEMU_LOG" 2>/dev/null &
33 | cat "$QEMU_TERM" 2>/dev/null & wait $! || :
34 |
35 | sleep 1 && finish 0
36 |
--------------------------------------------------------------------------------
/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 | else
9 | # Fallback for old installs
10 | BASE="DSM_VirtualDSM_42962"
11 | fi
12 |
13 | if [ -n "$URL" ]; then
14 | BASE=$(basename "$URL" .pat)
15 | if [ ! -f "$STORAGE/$BASE.system.img" ]; then
16 | BASE=$(basename "${URL%%\?*}" .pat)
17 | : "${BASE//+/ }"; printf -v BASE '%b' "${_//%/\\x}"
18 | BASE=$(echo "$BASE" | sed -e 's/[^A-Za-z0-9._-]/_/g')
19 | fi
20 | fi
21 |
22 | if [[ -f "$STORAGE/$BASE.boot.img" ]] && [[ -f "$STORAGE/$BASE.system.img" ]]; then
23 | return 0 # Previous installation found
24 | fi
25 |
26 | html "Please wait while Virtual DSM is being installed..."
27 |
28 | DL=""
29 | DL_CHINA="https://cndl.synology.cn/download/DSM"
30 | DL_GLOBAL="https://global.synologydownload.com/download/DSM"
31 |
32 | [[ "${URL,,}" == *"cndl.synology"* ]] && DL="$DL_CHINA"
33 | [[ "${URL,,}" == *"global.synology"* ]] && DL="$DL_GLOBAL"
34 |
35 | if [ -z "$DL" ]; then
36 | [ -z "$COUNTRY" ] && setCountry
37 | [ -z "$COUNTRY" ] && info "Warning: could not detect country to select mirror!"
38 | [[ "${COUNTRY^^}" == "CN" ]] && DL="$DL_CHINA" || DL="$DL_GLOBAL"
39 | fi
40 |
41 | [ -z "$URL" ] && URL="$DL/release/7.2.1/69057-1/DSM_VirtualDSM_69057.pat"
42 |
43 | BASE=$(basename "${URL%%\?*}" .pat)
44 | : "${BASE//+/ }"; printf -v BASE '%b' "${_//%/\\x}"
45 | BASE=$(echo "$BASE" | sed -e 's/[^A-Za-z0-9._-]/_/g')
46 |
47 | if [[ "$URL" != "file://$STORAGE/$BASE.pat" ]]; then
48 | rm -f "$STORAGE/$BASE.pat"
49 | fi
50 |
51 | rm -f "$STORAGE/$BASE.agent"
52 | rm -f "$STORAGE/$BASE.boot.img"
53 | rm -f "$STORAGE/$BASE.system.img"
54 |
55 | [[ "$DEBUG" == [Yy1]* ]] && set -x
56 |
57 | # Check filesystem
58 | FS=$(stat -f -c %T "$STORAGE")
59 |
60 | if [[ "${FS,,}" == "overlay"* ]]; then
61 | info "Warning: the filesystem of $STORAGE is OverlayFS, this usually means it was binded to an invalid path!"
62 | fi
63 |
64 | if [[ "${FS,,}" == "fuse"* ]]; then
65 | info "Warning: the filesystem of $STORAGE is FUSE, this extra layer will negatively affect performance!"
66 | fi
67 |
68 | if [[ "${FS,,}" == "fat"* || "${FS,,}" == "vfat"* || "${FS,,}" == "msdos"* ]]; then
69 | error "Unable to install on $FS filesystems, please use a different filesystem for /storage." && exit 61
70 | fi
71 |
72 | if [[ "${FS,,}" != "exfat"* && "${FS,,}" != "ntfs"* && "${FS,,}" != "unknown"* ]]; then
73 | TMP="$STORAGE/tmp"
74 | else
75 | TMP="/tmp/dsm"
76 | TMP_SPACE=2147483648
77 | SPACE=$(df --output=avail -B 1 /tmp | tail -n 1)
78 | SPACE_MB=$(( (SPACE + 1048575)/1048576 ))
79 | if (( TMP_SPACE > SPACE )); then
80 | error "Not enough free space inside the container, have $SPACE_MB MB available but need at least 2 GB." && exit 93
81 | fi
82 | fi
83 |
84 | rm -rf "$TMP" && mkdir -p "$TMP"
85 |
86 | # Check free diskspace
87 | ROOT_SPACE=536870912
88 | SPACE=$(df --output=avail -B 1 / | tail -n 1)
89 | SPACE_MB=$(( (SPACE + 1048575)/1048576 ))
90 | (( ROOT_SPACE > SPACE )) && error "Not enough free space inside the container, have $SPACE_MB MB available but need at least 500 MB." && exit 96
91 |
92 | MIN_SPACE=8589934592
93 | SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
94 | SPACE_GB=$(( (SPACE + 1073741823)/1073741824 ))
95 | (( MIN_SPACE > SPACE )) && error "Not enough free space for installation in $STORAGE, have $SPACE_GB GB available but need at least 8 GB." && exit 94
96 |
97 | # Check if output is to interactive TTY
98 | if [ -t 1 ]; then
99 | PROGRESS="--progress=bar:noscroll"
100 | else
101 | PROGRESS="--progress=dot:giga"
102 | fi
103 |
104 | # Download the required files from the Synology website
105 |
106 | ROOT="Y"
107 | RDC="$STORAGE/dsm.rd"
108 |
109 | if [ ! -f "$RDC" ]; then
110 |
111 | MSG="Downloading installer..."
112 | PRG="Downloading installer ([P])..."
113 | info "Install: $MSG" && html "$MSG"
114 |
115 | RD="$TMP/rd.gz"
116 | POS="65627648-71021835"
117 | VERIFY="b4215a4b213ff5154db0488f92c87864"
118 | LOC="$DL/release/7.0.1/42218/DSM_VirtualDSM_42218.pat"
119 |
120 | rm -f "$RD"
121 | /run/progress.sh "$RD" "$PRG" &
122 | { curl -r "$POS" -sfk -S -o "$RD" "$LOC"; rc=$?; } || :
123 |
124 | fKill "progress.sh"
125 | (( rc != 0 )) && error "Failed to download $LOC, reason: $rc" && exit 60
126 |
127 | SUM=$(md5sum "$RD" | cut -f 1 -d " ")
128 |
129 | if [ "$SUM" != "$VERIFY" ]; then
130 |
131 | PAT="/install.pat"
132 | rm "$RD"
133 | rm -f "$PAT"
134 |
135 | html "$MSG"
136 | /run/progress.sh "$PAT" "$PRG" &
137 | { wget "$LOC" -O "$PAT" -q --no-check-certificate --show-progress "$PROGRESS"; rc=$?; } || :
138 |
139 | fKill "progress.sh"
140 | (( rc != 0 )) && error "Failed to download $LOC , reason: $rc" && exit 60
141 |
142 | tar --extract --file="$PAT" --directory="$(dirname "$RD")"/. "$(basename "$RD")"
143 | rm "$PAT"
144 |
145 | fi
146 |
147 | cp "$RD" "$RDC"
148 |
149 | fi
150 |
151 | if [ -f "$RDC" ]; then
152 |
153 | { xz -dc <"$RDC" >"$TMP/rd" 2>/dev/null; rc=$?; } || :
154 | (( rc != 1 )) && error "Failed to unxz $RDC on $FS, reason $rc" && exit 91
155 |
156 | { (cd "$TMP" && cpio -idm <"$TMP/rd" 2>/dev/null); rc=$?; } || :
157 |
158 | if (( rc != 0 )); then
159 | ROOT="N"
160 | { (cd "$TMP" && fakeroot cpio -idmu <"$TMP/rd" 2>/dev/null); rc=$?; } || :
161 | (( rc != 0 )) && error "Failed to extract $RDC on $FS, reason $rc" && exit 92
162 | fi
163 |
164 | rm -rf /run/extract && mkdir -p /run/extract
165 | for file in $TMP/usr/lib/libcurl.so.4 \
166 | $TMP/usr/lib/libmbedcrypto.so.5 \
167 | $TMP/usr/lib/libmbedtls.so.13 \
168 | $TMP/usr/lib/libmbedx509.so.1 \
169 | $TMP/usr/lib/libmsgpackc.so.2 \
170 | $TMP/usr/lib/libsodium.so \
171 | $TMP/usr/lib/libsynocodesign-ng-virtual-junior-wins.so.7 \
172 | $TMP/usr/syno/bin/scemd; do
173 | cp "$file" /run/extract/
174 | done
175 |
176 | if [ "$ARCH" != "amd64" ]; then
177 | mkdir -p /lib64/
178 | cp "$TMP/usr/lib/libc.so.6" /lib64/
179 | cp "$TMP/usr/lib/libpthread.so.0" /lib64/
180 | cp "$TMP/usr/lib/ld-linux-x86-64.so.2" /lib64/
181 | fi
182 |
183 | mv /run/extract/scemd /run/extract/syno_extract_system_patch
184 | chmod +x /run/extract/syno_extract_system_patch
185 |
186 | fi
187 |
188 | rm -rf "$TMP" && mkdir -p "$TMP"
189 |
190 | info "Install: Downloading $BASE.pat..."
191 |
192 | MSG="Downloading DSM..."
193 | PRG="Downloading DSM ([P])..."
194 | html "$MSG"
195 |
196 | PAT="/$BASE.pat"
197 | rm -f "$PAT"
198 |
199 | if [[ "$URL" == "file://"* ]]; then
200 |
201 | cp "${URL:7}" "$PAT"
202 |
203 | else
204 |
205 | /run/progress.sh "$PAT" "$PRG" &
206 |
207 | { wget "$URL" -O "$PAT" -q --no-check-certificate --show-progress "$PROGRESS"; rc=$?; } || :
208 |
209 | fKill "progress.sh"
210 | (( rc != 0 )) && error "Failed to download $URL , reason: $rc" && exit 69
211 |
212 | fi
213 |
214 | [ ! -f "$PAT" ] && error "Failed to download $URL" && exit 69
215 |
216 | SIZE=$(stat -c%s "$PAT")
217 |
218 | if ((SIZE<250000000)); then
219 | error "The specified PAT file is probably an update pack as it's too small." && exit 62
220 | fi
221 |
222 | MSG="Extracting downloaded image..."
223 | info "Install: $MSG" && html "$MSG"
224 |
225 | if { tar tf "$PAT"; } >/dev/null 2>&1; then
226 |
227 | tar xpf "$PAT" -C "$TMP/."
228 |
229 | else
230 |
231 | export LD_LIBRARY_PATH="/run/extract"
232 |
233 | if [ "$ARCH" == "amd64" ]; then
234 | { /run/extract/syno_extract_system_patch "$PAT" "$TMP/."; rc=$?; } || :
235 | else
236 | { qemu-x86_64 /run/extract/syno_extract_system_patch "$PAT" "$TMP/."; rc=$?; } || :
237 | fi
238 |
239 | export LD_LIBRARY_PATH=""
240 |
241 | (( rc != 0 )) && error "Failed to extract PAT file, reason $rc" && exit 63
242 |
243 | fi
244 |
245 | rm -rf /run/extract
246 |
247 | MSG="Preparing system partition..."
248 | info "Install: $MSG" && html "$MSG"
249 |
250 | BOOT=$(find "$TMP" -name "*.bin.zip")
251 | [ ! -f "$BOOT" ] && error "The PAT file contains no boot image." && exit 67
252 |
253 | BOOT=$(echo "$BOOT" | head -c -5)
254 | unzip -q -o "$BOOT".zip -d "$TMP"
255 |
256 | SYSTEM="$STORAGE/$BASE.system.img"
257 | rm -f "$SYSTEM"
258 |
259 | # Check free diskspace
260 | SYSTEM_SIZE=4954537983
261 | SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
262 | SPACE_MB=$(( (SPACE + 1048575)/1048576 ))
263 |
264 | if (( SYSTEM_SIZE > SPACE )); then
265 | error "Not enough free space in $STORAGE to create a 5 GB system disk, have only $SPACE_MB MB available." && exit 97
266 | fi
267 |
268 | if ! touch "$SYSTEM"; then
269 | error "Could not create file $SYSTEM for the system disk." && exit 98
270 | fi
271 |
272 | if [[ "${FS,,}" == "btrfs" ]]; then
273 | { chattr +C "$SYSTEM"; } || :
274 | FA=$(lsattr "$SYSTEM")
275 | if [[ "$FA" != *"C"* ]]; then
276 | error "Failed to disable COW for system image $SYSTEM on ${FS^^} filesystem."
277 | fi
278 | fi
279 |
280 | if ! fallocate -l "$SYSTEM_SIZE" "$SYSTEM"; then
281 | if ! truncate -s "$SYSTEM_SIZE" "$SYSTEM"; then
282 | rm -f "$SYSTEM"
283 | error "Could not allocate file $SYSTEM for the system disk." && exit 98
284 | fi
285 | fi
286 |
287 | PART="$TMP/partition.fdisk"
288 |
289 | { echo "label: dos"
290 | echo "label-id: 0x6f9ee2e9"
291 | echo "device: $SYSTEM"
292 | echo "unit: sectors"
293 | echo "sector-size: 512"
294 | echo ""
295 | echo "${SYSTEM}1 : start= 2048, size= 4980480, type=83"
296 | echo "${SYSTEM}2 : start= 4982528, size= 4194304, type=82"
297 | } > "$PART"
298 |
299 | sfdisk -q "$SYSTEM" < "$PART"
300 |
301 | MOUNT="$TMP/system"
302 | rm -rf "$MOUNT" && mkdir -p "$MOUNT"
303 |
304 | MSG="Extracting system partition..."
305 | info "Install: $MSG" && html "$MSG"
306 |
307 | HDA="$TMP/hda1"
308 | IDB="$TMP/indexdb"
309 | PKG="$TMP/packages"
310 | HDP="$TMP/synohdpack_img"
311 |
312 | [ ! -f "$HDA.tgz" ] && error "The PAT file contains no OS image." && exit 64
313 | mv "$HDA.tgz" "$HDA.txz"
314 |
315 | [ -d "$PKG" ] && mv "$PKG/" "$MOUNT/.SynoUpgradePackages/"
316 | rm -f "$MOUNT/.SynoUpgradePackages/ActiveInsight-"*
317 |
318 | [ -f "$HDP.txz" ] && tar xpfJ "$HDP.txz" --absolute-names -C "$MOUNT/"
319 |
320 | if [ -f "$IDB.txz" ]; then
321 | INDEX_DB="$MOUNT/usr/syno/synoman/indexdb/"
322 | mkdir -p "$INDEX_DB"
323 | tar xpfJ "$IDB.txz" --absolute-names -C "$INDEX_DB"
324 | fi
325 |
326 | LABEL="1.44.1-42218"
327 | OFFSET="1048576" # 2048 * 512
328 | NUMBLOCKS="622560" # (4980480 * 512) / 4096
329 | MSG="Installing system partition..."
330 |
331 | if [[ "$ROOT" != [Nn]* ]]; then
332 |
333 | tar xpfJ "$HDA.txz" --absolute-names --skip-old-files -C "$MOUNT/"
334 |
335 | info "Install: $MSG" && html "$MSG"
336 |
337 | mke2fs -q -t ext4 -b 4096 -d "$MOUNT/" -L "$LABEL" -F -E "offset=$OFFSET" "$SYSTEM" "$NUMBLOCKS"
338 |
339 | else
340 |
341 | fakeroot -- bash -c "set -Eeu;\
342 | tar xpfJ $HDA.txz --absolute-names --skip-old-files -C $MOUNT/;\
343 | printf '%b%s%b' '\E[1;34m❯ \E[1;36m' 'Install: $MSG' '\E[0m\n';\
344 | mke2fs -q -t ext4 -b 4096 -d $MOUNT/ -L $LABEL -F -E offset=$OFFSET $SYSTEM $NUMBLOCKS"
345 |
346 | fi
347 |
348 | rm -rf "$MOUNT"
349 | echo "$BASE" > "$STORAGE/dsm.ver"
350 |
351 | if [[ "$URL" == "file://$STORAGE/$BASE.pat" ]]; then
352 | rm -f "$PAT"
353 | else
354 | mv -f "$PAT" "$STORAGE/$BASE.pat"
355 | fi
356 |
357 | mv -f "$BOOT" "$STORAGE/$BASE.boot.img"
358 | rm -rf "$TMP"
359 |
360 | { set +x; } 2>/dev/null
361 | [[ "$DEBUG" == [Yy1]* ]] && echo
362 |
363 | html "Installation finished successfully..."
364 | return 0
365 |
--------------------------------------------------------------------------------
/src/network.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -Eeuo pipefail
3 |
4 | # Docker environment variables
5 |
6 | : "${MAC:=""}"
7 | : "${DHCP:="N"}"
8 |
9 | : "${VM_NET_DEV:=""}"
10 | : "${VM_NET_TAP:="dsm"}"
11 | : "${VM_NET_MAC:="$MAC"}"
12 | : "${VM_NET_HOST:="VirtualDSM"}"
13 |
14 | : "${DNSMASQ_OPTS:=""}"
15 | : "${DNSMASQ:="/usr/sbin/dnsmasq"}"
16 | : "${DNSMASQ_CONF_DIR:="/etc/dnsmasq.d"}"
17 |
18 | ADD_ERR="Please add the following setting to your container:"
19 |
20 | # ######################################
21 | # Functions
22 | # ######################################
23 |
24 | configureDHCP() {
25 |
26 | # Create a macvtap network for the VM guest
27 |
28 | { ip link add link "$VM_NET_DEV" name "$VM_NET_TAP" address "$VM_NET_MAC" type macvtap mode bridge ; rc=$?; } || :
29 |
30 | if (( rc != 0 )); then
31 | error "Cannot create macvtap interface. Please make sure the network type is 'macvlan' and not 'ipvlan',"
32 | error "and that the NET_ADMIN capability has been added to the container: --cap-add NET_ADMIN" && exit 16
33 | fi
34 |
35 | while ! ip link set "$VM_NET_TAP" up; do
36 | info "Waiting for MAC address $VM_NET_MAC to become available..."
37 | sleep 2
38 | done
39 |
40 | local TAP_NR TAP_PATH MAJOR MINOR
41 | TAP_NR=$(>"$TAP_PATH"; rc=$?; } 2>/dev/null || :
56 |
57 | if (( rc != 0 )); then
58 | error "Cannot create TAP interface ($rc). $ADD_ERR --device-cgroup-rule='c *:* rwm'" && exit 21
59 | fi
60 |
61 | { exec 40>>/dev/vhost-net; rc=$?; } 2>/dev/null || :
62 |
63 | if (( rc != 0 )); then
64 | error "VHOST can not be found ($rc). $ADD_ERR --device=/dev/vhost-net" && exit 22
65 | fi
66 |
67 | NET_OPTS="-netdev tap,id=hostnet0,vhost=on,vhostfd=40,fd=30"
68 |
69 | return 0
70 | }
71 |
72 | configureDNS() {
73 |
74 | # dnsmasq configuration:
75 | DNSMASQ_OPTS="$DNSMASQ_OPTS --dhcp-range=$VM_NET_IP,$VM_NET_IP --dhcp-host=$VM_NET_MAC,,$VM_NET_IP,$VM_NET_HOST,infinite --dhcp-option=option:netmask,255.255.255.0"
76 |
77 | # Create lease file for faster resolve
78 | echo "0 $VM_NET_MAC $VM_NET_IP $VM_NET_HOST 01:$VM_NET_MAC" > /var/lib/misc/dnsmasq.leases
79 | chmod 644 /var/lib/misc/dnsmasq.leases
80 |
81 | # Set DNS server and gateway
82 | DNSMASQ_OPTS="$DNSMASQ_OPTS --dhcp-option=option:dns-server,${VM_NET_IP%.*}.1 --dhcp-option=option:router,${VM_NET_IP%.*}.1"
83 |
84 | # Add DNS entry for container
85 | DNSMASQ_OPTS="$DNSMASQ_OPTS --address=/host.lan/${VM_NET_IP%.*}.1"
86 |
87 | DNSMASQ_OPTS=$(echo "$DNSMASQ_OPTS" | sed 's/\t/ /g' | tr -s ' ' | sed 's/^ *//')
88 | [[ "$DEBUG" == [Yy1]* ]] && set -x
89 |
90 | if ! $DNSMASQ ${DNSMASQ_OPTS:+ $DNSMASQ_OPTS}; then
91 | error "Failed to start dnsmasq, reason: $?" && exit 29
92 | fi
93 | { set +x; } 2>/dev/null
94 | [[ "$DEBUG" == [Yy1]* ]] && echo
95 |
96 | return 0
97 | }
98 |
99 | configureNAT() {
100 |
101 | # Create the necessary file structure for /dev/net/tun
102 | if [ ! -c /dev/net/tun ]; then
103 | [ ! -d /dev/net ] && mkdir -m 755 /dev/net
104 | if mknod /dev/net/tun c 10 200; then
105 | chmod 666 /dev/net/tun
106 | fi
107 | fi
108 |
109 | if [ ! -c /dev/net/tun ]; then
110 | error "TUN device missing. $ADD_ERR --cap-add NET_ADMIN" && exit 25
111 | fi
112 |
113 | # Check port forwarding flag
114 | if [[ $(< /proc/sys/net/ipv4/ip_forward) -eq 0 ]]; then
115 | { sysctl -w net.ipv4.ip_forward=1 ; rc=$?; } || :
116 | if (( rc != 0 )); then
117 | error "IP forwarding is disabled. $ADD_ERR --sysctl net.ipv4.ip_forward=1" && exit 24
118 | fi
119 | fi
120 |
121 | # Create a bridge with a static IP for the VM guest
122 |
123 | VM_NET_IP='20.20.20.21'
124 |
125 | { ip link add dev dockerbridge type bridge ; rc=$?; } || :
126 |
127 | if (( rc != 0 )); then
128 | error "Failed to create bridge. $ADD_ERR --cap-add NET_ADMIN" && exit 23
129 | fi
130 |
131 | ip address add ${VM_NET_IP%.*}.1/24 broadcast ${VM_NET_IP%.*}.255 dev dockerbridge
132 |
133 | while ! ip link set dockerbridge up; do
134 | info "Waiting for IP address to become available..."
135 | sleep 2
136 | done
137 |
138 | # QEMU Works with taps, set tap to the bridge created
139 | ip tuntap add dev "$VM_NET_TAP" mode tap
140 |
141 | while ! ip link set "$VM_NET_TAP" up promisc on; do
142 | info "Waiting for TAP to become available..."
143 | sleep 2
144 | done
145 |
146 | ip link set dev "$VM_NET_TAP" master dockerbridge
147 |
148 | # Add internet connection to the VM
149 | update-alternatives --set iptables /usr/sbin/iptables-legacy > /dev/null
150 | update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy > /dev/null
151 |
152 | iptables -t nat -A POSTROUTING -o "$VM_NET_DEV" -j MASQUERADE
153 | iptables -t nat -A PREROUTING -i "$VM_NET_DEV" -d "$IP" -p tcp -j DNAT --to "$VM_NET_IP"
154 | iptables -t nat -A PREROUTING -i "$VM_NET_DEV" -d "$IP" -p udp -j DNAT --to "$VM_NET_IP"
155 |
156 | if (( KERNEL > 4 )); then
157 | # Hack for guest VMs complaining about "bad udp checksums in 5 packets"
158 | iptables -A POSTROUTING -t mangle -p udp --dport bootpc -j CHECKSUM --checksum-fill || true
159 | fi
160 |
161 | NET_OPTS="-netdev tap,ifname=$VM_NET_TAP,script=no,downscript=no,id=hostnet0"
162 |
163 | { exec 40>>/dev/vhost-net; rc=$?; } 2>/dev/null || :
164 | (( rc == 0 )) && NET_OPTS="$NET_OPTS,vhost=on,vhostfd=40"
165 |
166 | configureDNS
167 |
168 | return 0
169 | }
170 |
171 | closeNetwork() {
172 |
173 | exec 30<&- || true
174 | exec 40<&- || true
175 |
176 | if [[ "$DHCP" == [Yy1]* ]]; then
177 |
178 | # Shutdown nginx
179 | nginx -s stop 2> /dev/null
180 | fWait "nginx"
181 |
182 | ip link set "$VM_NET_TAP" down || true
183 | ip link delete "$VM_NET_TAP" || true
184 |
185 | else
186 |
187 | local pid="/var/run/dnsmasq.pid"
188 | [ -f "$pid" ] && pKill "$(<"$pid")"
189 |
190 | ip link set "$VM_NET_TAP" down promisc off || true
191 | ip link delete "$VM_NET_TAP" || true
192 |
193 | ip link set dockerbridge down || true
194 | ip link delete dockerbridge || true
195 |
196 | fi
197 |
198 | return 0
199 | }
200 |
201 | getInfo() {
202 |
203 | if [ -z "$VM_NET_DEV" ]; then
204 | # Automaticly detect the default network interface
205 | VM_NET_DEV=$(awk '$2 == 00000000 { print $1 }' /proc/net/route)
206 | [ -z "$VM_NET_DEV" ] && VM_NET_DEV="eth0"
207 | fi
208 |
209 | if [ ! -d "/sys/class/net/$VM_NET_DEV" ]; then
210 | error "Network interface '$VM_NET_DEV' does not exist inside the container!"
211 | error "$ADD_ERR -e \"VM_NET_DEV=NAME\" to specify another interface name." && exit 27
212 | fi
213 |
214 | if [ -z "$VM_NET_MAC" ]; then
215 | local file="$STORAGE/dsm.mac"
216 | if [ -f "$file" ]; then
217 | VM_NET_MAC=$(<"$file")
218 | else
219 | # Generate MAC address based on Docker container ID in hostname
220 | VM_NET_MAC=$(echo "$HOST" | md5sum | sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\).*$/02:11:32:\3:\4:\5/')
221 | echo "${VM_NET_MAC^^}" > "$file"
222 | fi
223 | fi
224 |
225 | VM_NET_MAC="${VM_NET_MAC^^}"
226 | VM_NET_MAC="${VM_NET_MAC//-/:}"
227 |
228 | if [[ ${#VM_NET_MAC} == 12 ]]; then
229 | m="$VM_NET_MAC"
230 | VM_NET_MAC="${m:0:2}:${m:2:2}:${m:4:2}:${m:6:2}:${m:8:2}:${m:10:2}"
231 | fi
232 |
233 | if [[ ${#VM_NET_MAC} != 17 ]]; then
234 | error "Invalid MAC address: '$VM_NET_MAC', should be 12 or 17 digits long!" && exit 28
235 | fi
236 |
237 | GATEWAY=$(ip r | grep default | awk '{print $3}')
238 | IP=$(ip address show dev "$VM_NET_DEV" | grep inet | awk '/inet / { print $2 }' | cut -f1 -d/)
239 | echo "$IP" > /run/shm/qemu.ip
240 |
241 | return 0
242 | }
243 |
244 | # ######################################
245 | # Configure Network
246 | # ######################################
247 |
248 | if [ ! -c /dev/vhost-net ]; then
249 | if mknod /dev/vhost-net c 10 238; then
250 | chmod 660 /dev/vhost-net
251 | fi
252 | fi
253 |
254 | getInfo
255 | html "Initializing network..."
256 |
257 | if [[ "$DEBUG" == [Yy1]* ]]; then
258 | info "Host: $HOST IP: $IP Gateway: $GATEWAY Interface: $VM_NET_DEV MAC: $VM_NET_MAC"
259 | [ -f /etc/resolv.conf ] && grep '^nameserver*' /etc/resolv.conf
260 | echo
261 | fi
262 |
263 | if [[ "$DHCP" == [Yy1]* ]]; then
264 |
265 | if [[ "$GATEWAY" == "172."* ]] && [[ "$DEBUG" != [Yy1]* ]]; then
266 | error "You can only enable DHCP while the container is on a macvlan network!" && exit 26
267 | fi
268 |
269 | # Configuration for DHCP IP
270 | configureDHCP
271 |
272 | MSG="Booting DSM instance..."
273 | html "$MSG"
274 |
275 | else
276 |
277 | # Shutdown nginx
278 | nginx -s stop 2> /dev/null
279 | fWait "nginx"
280 |
281 | # Configuration for static IP
282 | configureNAT
283 |
284 | fi
285 |
286 | NET_OPTS="$NET_OPTS -device virtio-net-pci,romfile=,netdev=hostnet0,mac=$VM_NET_MAC,id=net0"
287 |
288 | return 0
289 |
--------------------------------------------------------------------------------
/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_PID="/run/shm/qemu.pid"
14 | QEMU_LOG="/run/shm/qemu.log"
15 | QEMU_OUT="/run/shm/qemu.out"
16 | QEMU_END="/run/shm/qemu.end"
17 |
18 | if [[ "$KVM" == [Nn]* ]]; then
19 | API_TIMEOUT=$(( API_TIMEOUT*2 ))
20 | QEMU_TIMEOUT=$(( QEMU_TIMEOUT*2 ))
21 | fi
22 |
23 | touch "$QEMU_LOG"
24 |
25 | _trap() {
26 | func="$1" ; shift
27 | for sig ; do
28 | trap "$func $sig" "$sig"
29 | done
30 | }
31 |
32 | finish() {
33 |
34 | local pid
35 | local reason=$1
36 |
37 | touch "$QEMU_END"
38 |
39 | if [ -f "$QEMU_PID" ]; then
40 |
41 | pid=$(<"$QEMU_PID")
42 | echo && error "Forcefully terminating QEMU process, reason: $reason..."
43 | { kill -15 "$pid" || true; } 2>/dev/null
44 |
45 | while isAlive "$pid"; do
46 | sleep 1
47 | # Workaround for zombie pid
48 | [ ! -f "$QEMU_PID" ] && break
49 | done
50 | fi
51 |
52 | fKill "print.sh"
53 | fKill "host.bin"
54 |
55 | closeNetwork
56 |
57 | sleep 1
58 | echo && echo "❯ Shutdown completed!"
59 |
60 | exit "$reason"
61 | }
62 |
63 | terminal() {
64 |
65 | local dev=""
66 |
67 | if [ -f "$QEMU_OUT" ]; then
68 |
69 | local msg
70 | msg=$(<"$QEMU_OUT")
71 |
72 | if [ -n "$msg" ]; then
73 |
74 | if [[ "${msg,,}" != "char"* || "$msg" != *"serial0)" ]]; then
75 | echo "$msg"
76 | fi
77 |
78 | dev="${msg#*/dev/p}"
79 | dev="/dev/p${dev%% *}"
80 |
81 | fi
82 | fi
83 |
84 | if [ ! -c "$dev" ]; then
85 | dev=$(echo 'info chardev' | nc -q 1 -w 1 localhost "$QEMU_PORT" | tr -d '\000')
86 | dev="${dev#*serial0}"
87 | dev="${dev#*pty:}"
88 | dev="${dev%%$'\n'*}"
89 | dev="${dev%%$'\r'*}"
90 | fi
91 |
92 | if [ ! -c "$dev" ]; then
93 | error "Device '$dev' not found!"
94 | finish 34 && return 34
95 | fi
96 |
97 | QEMU_TERM="$dev"
98 | return 0
99 | }
100 |
101 | _graceful_shutdown() {
102 |
103 | local code=$?
104 | local pid url response
105 |
106 | set +e
107 |
108 | if [ -f "$QEMU_END" ]; then
109 | echo && info "Received $1 signal while already shutting down..."
110 | return
111 | fi
112 |
113 | touch "$QEMU_END"
114 | echo && info "Received $1 signal, sending shutdown command..."
115 |
116 | if [ ! -f "$QEMU_PID" ]; then
117 | echo && error "QEMU PID file does not exist?"
118 | finish "$code" && return "$code"
119 | fi
120 |
121 | pid=$(<"$QEMU_PID")
122 |
123 | if ! isAlive "$pid"; then
124 | echo && error "QEMU process does not exist?"
125 | finish "$code" && return "$code"
126 | fi
127 |
128 | # Don't send the powerdown signal because vDSM ignores ACPI signals
129 | # echo 'system_powerdown' | nc -q 1 -w 1 localhost "${QEMU_PORT}" > /dev/null
130 |
131 | # Send shutdown command to guest agent via serial port
132 | url="http://$API_HOST/read?command=$API_CMD&timeout=$API_TIMEOUT"
133 | response=$(curl -sk -m "$(( API_TIMEOUT+2 ))" -S "$url" 2>&1)
134 |
135 | if [[ "$response" =~ "\"success\"" ]]; then
136 |
137 | echo && info "Virtual DSM is now ready to shutdown..."
138 |
139 | else
140 |
141 | response="${response#*message\"\: \"}"
142 | [ -z "$response" ] && response="second signal"
143 | echo && error "Forcefully terminating because of: ${response%%\"*}"
144 | { kill -15 "$pid" || true; } 2>/dev/null
145 |
146 | fi
147 |
148 | local cnt=0
149 |
150 | while [ "$cnt" -lt "$QEMU_TIMEOUT" ]; do
151 |
152 | ! isAlive "$pid" && break
153 |
154 | sleep 1
155 | cnt=$((cnt+1))
156 |
157 | [[ "$DEBUG" == [Yy1]* ]] && info "Shutting down, waiting... ($cnt/$QEMU_TIMEOUT)"
158 |
159 | # Workaround for zombie pid
160 | [ ! -f "$QEMU_PID" ] && break
161 |
162 | done
163 |
164 | if [ "$cnt" -ge "$QEMU_TIMEOUT" ]; then
165 | echo && error "Shutdown timeout reached, aborting..."
166 | fi
167 |
168 | finish "$code" && return "$code"
169 | }
170 |
171 | MON_OPTS="\
172 | -pidfile $QEMU_PID \
173 | -name $PROCESS,process=$PROCESS,debug-threads=on \
174 | -monitor telnet:localhost:$QEMU_PORT,server,nowait,nodelay"
175 |
176 | if [[ "$CONSOLE" != [Yy]* ]]; then
177 |
178 | MON_OPTS="$MON_OPTS -daemonize -D $QEMU_LOG"
179 |
180 | _trap _graceful_shutdown SIGTERM SIGHUP SIGINT SIGABRT SIGQUIT
181 |
182 | fi
183 |
184 | return 0
185 |
--------------------------------------------------------------------------------
/src/print.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -Eeuo pipefail
3 |
4 | : "${DHCP:="N"}"
5 |
6 | info () { printf "%b%s%b" "\E[1;34m❯ \E[1;36m" "$1" "\E[0m\n" >&2; }
7 | error () { printf "%b%s%b" "\E[1;31m❯ " "ERROR: $1" "\E[0m\n" >&2; }
8 |
9 | file="/run/shm/dsm.url"
10 | info="/run/shm/msg.html"
11 | page="/run/shm/index.html"
12 | address="/run/shm/qemu.ip"
13 | shutdown="/run/shm/qemu.end"
14 | template="/var/www/index.html"
15 | url="http://127.0.0.1:2210/read?command=10"
16 |
17 | resp_err="Guest returned an invalid response:"
18 | curl_err="Failed to connect to guest: curl error"
19 | jq_err="Failed to parse response from guest: jq error"
20 |
21 | while [ ! -f "$file" ]
22 | do
23 |
24 | # Check if not shutting down
25 | [ -f "$shutdown" ] && exit 1
26 |
27 | sleep 3
28 |
29 | [ -f "$shutdown" ] && exit 1
30 | [ -f "$file" ] && break
31 |
32 | # Retrieve network info from guest VM
33 | { json=$(curl -m 20 -sk "$url"); rc=$?; } || :
34 |
35 | [ -f "$shutdown" ] && exit 1
36 | (( rc != 0 )) && error "$curl_err $rc" && continue
37 |
38 | { result=$(echo "$json" | jq -r '.status'); rc=$?; } || :
39 | (( rc != 0 )) && error "$jq_err $rc ( $json )" && continue
40 | [[ "$result" == "null" ]] && error "$resp_err $json" && continue
41 |
42 | if [[ "$result" != "success" ]] ; then
43 | { msg=$(echo "$json" | jq -r '.message'); rc=$?; } || :
44 | error "Guest replied $result: $msg" && continue
45 | fi
46 |
47 | { port=$(echo "$json" | jq -r '.data.data.dsm_setting.data.http_port'); rc=$?; } || :
48 | (( rc != 0 )) && error "$jq_err $rc ( $json )" && continue
49 | [[ "$port" == "null" ]] && error "$resp_err $json" && continue
50 | [ -z "$port" ] && continue
51 |
52 | { ip=$(echo "$json" | jq -r '.data.data.ip.data[] | select((.name=="eth0") and has("ip")).ip'); rc=$?; } || :
53 | (( rc != 0 )) && error "$jq_err $rc ( $json )" && continue
54 | [[ "$ip" == "null" ]] && error "$resp_err $json" && continue
55 |
56 | if [ -z "$ip" ]; then
57 | [[ "$DHCP" == [Yy1]* ]] && continue
58 | ip="20.20.20.21"
59 | fi
60 |
61 | echo "$ip:$port" > $file
62 |
63 | done
64 |
65 | [ -f "$shutdown" ] && exit 1
66 |
67 | location=$(<"$file")
68 |
69 | if [[ "$location" != "20.20"* ]]; then
70 |
71 | msg="http://$location"
72 | title="Virtual DSM"
73 | body="The location of DSM is http://$location"
74 | script=""
75 |
76 | HTML=$(<"$template")
77 | HTML="${HTML/\[1\]/$title}"
78 | HTML="${HTML/\[2\]/$script}"
79 | HTML="${HTML/\[3\]/$body}"
80 | HTML="${HTML/\[4\]/}"
81 | HTML="${HTML/\[5\]/}"
82 |
83 | echo "$HTML" > "$page"
84 | echo "$body" > "$info"
85 |
86 | else
87 |
88 | ip=$(<"$address")
89 | port="${location##*:}"
90 |
91 | if [[ "$ip" == "172."* ]]; then
92 | msg="port $port"
93 | else
94 | msg="http://$ip:$port"
95 | fi
96 |
97 | fi
98 |
99 | echo "" >&2
100 | info "-----------------------------------------------------------"
101 | info " You can now login to DSM at $msg"
102 | info "-----------------------------------------------------------"
103 | echo "" >&2
104 |
--------------------------------------------------------------------------------
/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 | [ "$ARCH" != "amd64" ] && KVM="N"
13 |
14 | if [[ "$KVM" != [Nn]* ]]; then
15 |
16 | KVM_ERR=""
17 |
18 | if [ ! -e /dev/kvm ]; then
19 | KVM_ERR="(device file missing)"
20 | else
21 | if ! sh -c 'echo -n > /dev/kvm' &> /dev/null; then
22 | KVM_ERR="(no write access)"
23 | else
24 | if ! grep -q -e vmx -e svm /proc/cpuinfo; then
25 | KVM_ERR="(vmx/svm disabled)"
26 | fi
27 | fi
28 | fi
29 |
30 | if [ -n "$KVM_ERR" ]; then
31 | KVM="N"
32 | error "KVM acceleration not available $KVM_ERR, this will cause a major loss of performance."
33 | error "See the FAQ on how to enable it, or continue without KVM by setting KVM=N (not recommended)."
34 | [[ "$DEBUG" != [Yy1]* ]] && exit 88
35 | fi
36 |
37 | fi
38 |
39 | if [[ "$KVM" != [Nn]* ]]; then
40 |
41 | CPU_FEATURES="kvm=on,l3-cache=on"
42 | KVM_OPTS=",accel=kvm -enable-kvm -global kvm-pit.lost_tick_policy=discard"
43 |
44 | if ! grep -qE '^flags.* (sse4_2)' /proc/cpuinfo; then
45 | info "Your CPU does not have the SSE4 instruction set that Virtual DSM requires, it will be emulated..."
46 | [ -z "$CPU_MODEL" ] && CPU_MODEL="$DEF_MODEL"
47 | CPU_FEATURES="$CPU_FEATURES,+ssse3,+sse4.1,+sse4.2"
48 | fi
49 |
50 | if [ -z "$CPU_MODEL" ]; then
51 | CPU_MODEL="host"
52 | CPU_FEATURES="$CPU_FEATURES,migratable=no"
53 | fi
54 |
55 | else
56 |
57 | KVM_OPTS=""
58 | CPU_FEATURES="l3-cache=on"
59 |
60 | if [[ "$ARCH" == "amd64" ]]; then
61 | KVM_OPTS=" -accel tcg,thread=multi"
62 | fi
63 |
64 | if [ -z "$CPU_MODEL" ]; then
65 | if [[ "$ARCH" == "amd64" ]]; then
66 | CPU_MODEL="max"
67 | CPU_FEATURES="$CPU_FEATURES,migratable=no"
68 | else
69 | CPU_MODEL="$DEF_MODEL"
70 | fi
71 | fi
72 |
73 | CPU_FEATURES="$CPU_FEATURES,+ssse3,+sse4.1,+sse4.2"
74 |
75 | fi
76 |
77 | if [ -z "$CPU_FLAGS" ]; then
78 | if [ -z "$CPU_FEATURES" ]; then
79 | CPU_FLAGS="$CPU_MODEL"
80 | else
81 | CPU_FLAGS="$CPU_MODEL,$CPU_FEATURES"
82 | fi
83 | else
84 | if [ -z "$CPU_FEATURES" ]; then
85 | CPU_FLAGS="$CPU_MODEL,$CPU_FLAGS"
86 | else
87 | CPU_FLAGS="$CPU_MODEL,$CPU_FEATURES,$CPU_FLAGS"
88 | fi
89 | fi
90 |
91 | if [ -z "$HOST_CPU" ]; then
92 | HOST_CPU=$(lscpu | grep 'Model name' | cut -f 2 -d ":" | awk '{$1=$1}1' | sed 's# @.*##g' | sed s/"(R)"//g | sed 's/[^[:alnum:] ]\+/ /g' | sed 's/ */ /g')
93 | fi
94 |
95 | if [ -n "$HOST_CPU" ]; then
96 | HOST_CPU="${HOST_CPU%%,*},,"
97 | else
98 | HOST_CPU="QEMU, Virtual CPU,"
99 | if [ "$ARCH" == "amd64" ]; then
100 | HOST_CPU="$HOST_CPU X86_64"
101 | else
102 | HOST_CPU="$HOST_CPU $ARCH"
103 | fi
104 | fi
105 |
106 | return 0
107 |
--------------------------------------------------------------------------------
/src/progress.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -Eeuo pipefail
3 |
4 | escape () {
5 | local s
6 | s=${1//&/\&}
7 | s=${s//\<}
8 | s=${s//>/\>}
9 | s=${s//'"'/\"}
10 | printf -- %s "$s"
11 | return 0
12 | }
13 |
14 | file="$1"
15 | body=$(escape "$2")
16 | info="/run/shm/msg.html"
17 |
18 | if [[ "$body" == *"..." ]]; then
19 | body="${body/.../}
"
20 | fi
21 |
22 | while true
23 | do
24 | if [ -f "$file" ]; then
25 | bytes=$(du -sb "$file" | cut -f1)
26 | if (( bytes > 1000 )); then
27 | size=$(echo "$bytes" | numfmt --to=iec --suffix=B | sed -r 's/([A-Z])/ \1/')
28 | echo "${body//(\[P\])/($size)}"> "$info"
29 | fi
30 | fi
31 | sleep 1 & wait $!
32 | done
33 |
--------------------------------------------------------------------------------
/src/reset.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -Eeuo pipefail
3 |
4 | info () { printf "%b%s%b" "\E[1;34m❯ \E[1;36m" "$1" "\E[0m\n"; }
5 | error () { printf "%b%s%b" "\E[1;31m❯ " "ERROR: $1" "\E[0m\n" >&2; }
6 | warn () { printf "%b%s%b" "\E[1;31m❯ " "Warning: $1" "\E[0m\n" >&2; }
7 |
8 | trap 'error "Status $? while: $BASH_COMMAND (line $LINENO/$BASH_LINENO)"' ERR
9 |
10 | [ ! -f "/run/entry.sh" ] && error "Script must run inside Docker container!" && exit 11
11 | [ "$(id -u)" -ne "0" ] && error "Script must be executed with root privileges." && exit 12
12 |
13 | echo "❯ Starting $APP for Docker v$($SUPPORT"
40 |
41 | HOST=$(hostname -s)
42 | KERNEL=$(uname -r | cut -b 1)
43 | MINOR=$(uname -r | cut -d '.' -f2)
44 | ARCH=$(dpkg --print-architecture)
45 | VERS=$(qemu-system-x86_64 --version | head -n 1 | cut -d '(' -f 1)
46 |
47 | # Check system
48 |
49 | if [ ! -d "/dev/shm" ]; then
50 | error "Directory /dev/shm not found!" && exit 14
51 | else
52 | [ ! -d "/run/shm" ] && ln -s /dev/shm /run/shm
53 | fi
54 |
55 | # Check folder
56 |
57 | if [ ! -d "$STORAGE" ]; then
58 | error "Storage folder ($STORAGE) not found!" && exit 13
59 | fi
60 |
61 | # Cleanup files
62 | rm -f /run/shm/qemu.*
63 | rm -f /run/shm/dsm.url
64 |
65 | # Cleanup dirs
66 | rm -rf /tmp/dsm
67 | rm -rf "$STORAGE/tmp"
68 |
69 | # Helper functions
70 |
71 | isAlive() {
72 | local pid=$1
73 |
74 | if kill -0 "$pid" 2>/dev/null; then
75 | return 0
76 | fi
77 |
78 | return 1
79 | }
80 |
81 | pKill() {
82 | local pid=$1
83 |
84 | { kill -15 "$pid" || true; } 2>/dev/null
85 |
86 | while isAlive "$pid"; do
87 | sleep 0.2
88 | done
89 |
90 | return 0
91 | }
92 |
93 | fWait() {
94 | local name=$1
95 |
96 | while pgrep -f -l "$name" >/dev/null; do
97 | sleep 0.2
98 | done
99 |
100 | return 0
101 | }
102 |
103 | fKill() {
104 | local name=$1
105 |
106 | { pkill -f "$name" || true; } 2>/dev/null
107 | fWait "$name"
108 |
109 | return 0
110 | }
111 |
112 | escape () {
113 | local s
114 | s=${1//&/\&}
115 | s=${s//\<}
116 | s=${s//>/\>}
117 | s=${s//'"'/\"}
118 | printf -- %s "$s"
119 | return 0
120 | }
121 |
122 | html()
123 | {
124 | local title
125 | local body
126 | local script
127 | local footer
128 |
129 | title=$(escape "$APP")
130 | title="$title"
131 | footer=$(escape "$FOOTER1")
132 |
133 | body=$(escape "$1")
134 | if [[ "$body" == *"..." ]]; then
135 | body="${body/.../}
"
136 | fi
137 |
138 | [ -n "${2:-}" ] && script="$2" || script=""
139 |
140 | local HTML
141 | HTML=$(<"$TEMPLATE")
142 | HTML="${HTML/\[1\]/$title}"
143 | HTML="${HTML/\[2\]/$script}"
144 | HTML="${HTML/\[3\]/$body}"
145 | HTML="${HTML/\[4\]/$footer}"
146 | HTML="${HTML/\[5\]/$FOOTER2}"
147 |
148 | echo "$HTML" > "$PAGE"
149 | echo "$body" > "$INFO"
150 |
151 | return 0
152 | }
153 |
154 | getCountry() {
155 | local url=$1
156 | local query=$2
157 | local rc json result
158 |
159 | { json=$(curl -m 5 -H "Accept: application/json" -sfk "$url"); rc=$?; } || :
160 | (( rc != 0 )) && return 0
161 |
162 | { result=$(echo "$json" | jq -r "$query" 2> /dev/null); rc=$?; } || :
163 | (( rc != 0 )) && return 0
164 |
165 | [[ ${#result} -ne 2 ]] && return 0
166 | [[ "${result^^}" == "XX" ]] && return 0
167 |
168 | COUNTRY="${result^^}"
169 |
170 | return 0
171 | }
172 |
173 | setCountry() {
174 |
175 | [[ "${TZ,,}" == "asia/harbin" ]] && COUNTRY="CN"
176 | [[ "${TZ,,}" == "asia/beijing" ]] && COUNTRY="CN"
177 | [[ "${TZ,,}" == "asia/urumqi" ]] && COUNTRY="CN"
178 | [[ "${TZ,,}" == "asia/kashgar" ]] && COUNTRY="CN"
179 | [[ "${TZ,,}" == "asia/shanghai" ]] && COUNTRY="CN"
180 | [[ "${TZ,,}" == "asia/chongqing" ]] && COUNTRY="CN"
181 |
182 | [ -z "$COUNTRY" ] && getCountry "https://api.ipapi.is" ".location.country_code"
183 | [ -z "$COUNTRY" ] && getCountry "https://ifconfig.co/json" ".country_iso"
184 | [ -z "$COUNTRY" ] && getCountry "https://api.ip2location.io" ".country_code"
185 | [ -z "$COUNTRY" ] && getCountry "https://ipinfo.io/json" ".country"
186 | [ -z "$COUNTRY" ] && getCountry "https://api.myip.com" ".cc"
187 |
188 | return 0
189 | }
190 |
191 | addPackage() {
192 | local pkg=$1
193 | local desc=$2
194 |
195 | if apt-mark showinstall | grep -qx "$pkg"; then
196 | return 0
197 | fi
198 |
199 | MSG="Installing $desc..."
200 | info "$MSG" && html "$MSG"
201 |
202 | [ -z "$COUNTRY" ] && setCountry
203 |
204 | if [[ "${COUNTRY^^}" == "CN" ]]; then
205 | sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources
206 | fi
207 |
208 | DEBIAN_FRONTEND=noninteractive apt-get -qq update
209 | DEBIAN_FRONTEND=noninteractive apt-get -qq --no-install-recommends -y install "$pkg" > /dev/null
210 |
211 | return 0
212 | }
213 |
214 | # Start webserver
215 | cp -r /var/www/* /run/shm
216 | html "Starting $APP for Docker..."
217 | nginx -e stderr
218 |
219 | return 0
220 |
--------------------------------------------------------------------------------
/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="$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 |
--------------------------------------------------------------------------------
/web/css/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | color: white;
3 | background-color: #125bdb;
4 | font-smoothing: antialiased;
5 | -webkit-font-smoothing: antialiased;
6 | -moz-osx-font-smoothing: grayscale;
7 | font-family: Verdana, Geneva, sans-serif;
8 | }
9 |
10 | #info {
11 | text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.25);
12 | }
13 |
14 | #content {
15 | text-align: center;
16 | padding: 20px;
17 | margin-top: 50px;
18 | }
19 |
20 | footer {
21 | width: 98%;
22 | position: fixed;
23 | bottom: 0px;
24 | height: 40px;
25 | text-align: center;
26 | color: #0c8aeb;
27 | text-shadow: 0 0 1px #0c8aeb;
28 | }
29 |
30 | #empty {
31 | height: 40px;
32 | /* Same height as footer */
33 | }
34 |
35 | a,
36 | a:hover,
37 | a:active,
38 | a:visited {
39 | color: white;
40 | }
41 |
42 | footer a:link,
43 | footer a:visited,
44 | footer a:active {
45 | color: #0c8aeb;
46 | }
47 |
48 | footer a:hover {
49 | color: #73e6ff;
50 | }
51 |
52 | .loading:after {
53 | content: " .";
54 | animation: dots 1s steps(5, end) infinite;
55 | }
56 |
57 | @keyframes dots {
58 |
59 | 0%,
60 | 20% {
61 | color: rgba(0, 0, 0, 0);
62 | text-shadow: 0.25em 0 0 rgba(0, 0, 0, 0), 0.5em 0 0 rgba(0, 0, 0, 0);
63 | }
64 |
65 | 40% {
66 | color: white;
67 | text-shadow: 0.25em 0 0 rgba(0, 0, 0, 0), 0.5em 0 0 rgba(0, 0, 0, 0);
68 | }
69 |
70 | 60% {
71 | text-shadow: 0.25em 0 0 white, 0.5em 0 0 rgba(0, 0, 0, 0);
72 | }
73 |
74 | 80%,
75 | 100% {
76 | text-shadow: 0.25em 0 0 white, 0.5em 0 0 white;
77 | }
78 | }
79 |
80 | .spinner_LWk7 {
81 | animation: spinner_GWy6 1.2s linear infinite, spinner_BNNO 1.2s linear infinite
82 | }
83 |
84 | .spinner_yOMU {
85 | animation: spinner_GWy6 1.2s linear infinite, spinner_pVqn 1.2s linear infinite;
86 | animation-delay: .15s
87 | }
88 |
89 | .spinner_KS4S {
90 | animation: spinner_GWy6 1.2s linear infinite, spinner_6uKB 1.2s linear infinite;
91 | animation-delay: .3s
92 | }
93 |
94 | .spinner_zVee {
95 | animation: spinner_GWy6 1.2s linear infinite, spinner_Qw4x 1.2s linear infinite;
96 | animation-delay: .45s
97 | }
98 |
99 | @keyframes spinner_GWy6 {
100 |
101 | 0%,
102 | 50% {
103 | width: 9px;
104 | height: 9px
105 | }
106 |
107 | 10% {
108 | width: 11px;
109 | height: 11px
110 | }
111 | }
112 |
113 | @keyframes spinner_BNNO {
114 |
115 | 0%,
116 | 50% {
117 | x: 1.5px;
118 | y: 1.5px
119 | }
120 |
121 | 10% {
122 | x: .5px;
123 | y: .5px
124 | }
125 | }
126 |
127 | @keyframes spinner_pVqn {
128 |
129 | 0%,
130 | 50% {
131 | x: 13.5px;
132 | y: 1.5px
133 | }
134 |
135 | 10% {
136 | x: 12.5px;
137 | y: .5px
138 | }
139 | }
140 |
141 | @keyframes spinner_6uKB {
142 |
143 | 0%,
144 | 50% {
145 | x: 13.5px;
146 | y: 13.5px
147 | }
148 |
149 | 10% {
150 | x: 12.5px;
151 | y: 12.5px
152 | }
153 | }
154 |
155 | @keyframes spinner_Qw4x {
156 |
157 | 0%,
158 | 50% {
159 | x: 1.5px;
160 | y: 13.5px
161 | }
162 |
163 | 10% {
164 | x: .5px;
165 | y: 12.5px
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/web/img/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [1]
6 |
7 |
8 |
9 |
10 | [2]
11 |
12 |
13 |
14 |
15 |
16 |
22 |
[3]
23 |
24 |
25 |
26 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/web/js/script.js:
--------------------------------------------------------------------------------
1 | var request;
2 | var interval = 1000;
3 |
4 | function getInfo() {
5 |
6 | var url = "/msg.html";
7 |
8 | try {
9 |
10 | if (window.XMLHttpRequest) {
11 | request = new XMLHttpRequest();
12 | } else {
13 | throw "XMLHttpRequest not available!";
14 | }
15 |
16 | request.onreadystatechange = processInfo;
17 | request.open("GET", url, true);
18 | request.send();
19 |
20 | } catch (e) {
21 | var err = "Error: " + e.message;
22 | console.log(err);
23 | setError(err);
24 | }
25 | }
26 |
27 | function processInfo() {
28 | try {
29 | if (request.readyState != 4) {
30 | return true;
31 | }
32 |
33 | var msg = request.responseText;
34 | if (msg == null || msg.length == 0) {
35 | setInfo("Booting DSM instance", true);
36 | schedule();
37 | return false;
38 | }
39 |
40 | var notFound = (request.status == 404);
41 |
42 | if (request.status == 200) {
43 | if (msg.toLowerCase().indexOf("") !== -1) {
44 | notFound = true;
45 | } else {
46 | if (msg.toLowerCase().indexOf("href=") !== -1) {
47 | var div = document.createElement("div");
48 | div.innerHTML = msg;
49 | var url = div.querySelector("a").href;
50 | setTimeout(() => {
51 | window.location.assign(url);
52 | }, 3000);
53 | setInfo(msg);
54 | return true;
55 | } else {
56 | setInfo(msg);
57 | schedule();
58 | return true;
59 | }
60 | }
61 | }
62 |
63 | if (notFound) {
64 | setInfo("Connecting to web portal", true);
65 | reload();
66 | return true;
67 | }
68 |
69 | setError("Error: Received statuscode " + request.status);
70 | schedule();
71 | return false;
72 |
73 | } catch (e) {
74 | var err = "Error: " + e.message;
75 | console.log(err);
76 | setError(err);
77 | return false;
78 | }
79 | }
80 |
81 | function setInfo(msg, loading, error) {
82 |
83 | try {
84 | if (msg == null || msg.length == 0) {
85 | return false;
86 | }
87 |
88 | var el = document.getElementById("spinner");
89 |
90 | error = !!error;
91 | if (!error) {
92 | el.style.visibility = 'visible';
93 | } else {
94 | el.style.visibility = 'hidden';
95 | }
96 |
97 | loading = !!loading;
98 | if (loading) {
99 | msg = "" + msg + "
";
100 | }
101 |
102 | el = document.getElementById("info");
103 |
104 | if (el.innerHTML != msg) {
105 | el.innerHTML = msg;
106 | }
107 |
108 | return true;
109 |
110 | } catch (e) {
111 | console.log("Error: " + e.message);
112 | return false;
113 | }
114 | }
115 |
116 | function setError(text) {
117 | return setInfo(text, false, true);
118 | }
119 |
120 | function schedule() {
121 | setTimeout(getInfo, interval);
122 | }
123 |
124 | function reload() {
125 | setTimeout(() => {
126 | document.location.reload();
127 | }, 3000);
128 | }
129 |
130 | schedule();
131 |
--------------------------------------------------------------------------------
/web/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | listen [::]:80;
4 | listen 5000 default_server;
5 | listen [::]:5000 default_server;
6 |
7 | autoindex on;
8 | tcp_nodelay on;
9 | server_tokens off;
10 | absolute_redirect off;
11 |
12 | error_log /dev/null;
13 | access_log /dev/null;
14 |
15 | include /etc/nginx/mime.types;
16 |
17 | gzip on;
18 | gzip_vary on;
19 | gzip_proxied any;
20 | gzip_comp_level 5;
21 | gzip_min_length 500;
22 | gzip_disable "msie6";
23 | 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;
24 |
25 | add_header Cache-Control "no-cache";
26 |
27 | location / {
28 |
29 | root /run/shm;
30 | index index.html;
31 |
32 | }
33 | }
34 |
--------------------------------------------------------------------------------