├── .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 |
31 | 32 |
33 | 34 |
35 |
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 | [![Stars](https://starchart.cc/vdsm/virtual-dsm.svg?variant=adaptive)](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///\>} 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$(/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///\>} 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 | 17 | 18 | 19 | 20 | 21 | 22 |

[3]

23 |
24 |
25 |
26 |
27 | [4]
28 | [5] 29 |
30 |
31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /web/js/script.js: -------------------------------------------------------------------------------- 1 | var request; 2 | var interval = 1000; 3 | 4 | function getInfo() { 5 | 6 | var url = "/msg.html"; 7 | 8 | try { 9 | 10 | if (window.XMLHttpRequest) { 11 | request = new XMLHttpRequest(); 12 | } else { 13 | throw "XMLHttpRequest not available!"; 14 | } 15 | 16 | request.onreadystatechange = processInfo; 17 | request.open("GET", url, true); 18 | request.send(); 19 | 20 | } catch (e) { 21 | var err = "Error: " + e.message; 22 | console.log(err); 23 | setError(err); 24 | } 25 | } 26 | 27 | function processInfo() { 28 | try { 29 | if (request.readyState != 4) { 30 | return true; 31 | } 32 | 33 | var msg = request.responseText; 34 | if (msg == null || msg.length == 0) { 35 | setInfo("Booting DSM instance", true); 36 | schedule(); 37 | return false; 38 | } 39 | 40 | var notFound = (request.status == 404); 41 | 42 | if (request.status == 200) { 43 | if (msg.toLowerCase().indexOf("") !== -1) { 44 | notFound = true; 45 | } else { 46 | if (msg.toLowerCase().indexOf("href=") !== -1) { 47 | var div = document.createElement("div"); 48 | div.innerHTML = msg; 49 | var url = div.querySelector("a").href; 50 | setTimeout(() => { 51 | window.location.assign(url); 52 | }, 3000); 53 | setInfo(msg); 54 | return true; 55 | } else { 56 | setInfo(msg); 57 | schedule(); 58 | return true; 59 | } 60 | } 61 | } 62 | 63 | if (notFound) { 64 | setInfo("Connecting to web portal", true); 65 | reload(); 66 | return true; 67 | } 68 | 69 | setError("Error: Received statuscode " + request.status); 70 | schedule(); 71 | return false; 72 | 73 | } catch (e) { 74 | var err = "Error: " + e.message; 75 | console.log(err); 76 | setError(err); 77 | return false; 78 | } 79 | } 80 | 81 | function setInfo(msg, loading, error) { 82 | 83 | try { 84 | if (msg == null || msg.length == 0) { 85 | return false; 86 | } 87 | 88 | var el = document.getElementById("spinner"); 89 | 90 | error = !!error; 91 | if (!error) { 92 | el.style.visibility = 'visible'; 93 | } else { 94 | el.style.visibility = 'hidden'; 95 | } 96 | 97 | loading = !!loading; 98 | if (loading) { 99 | msg = "

" + msg + "

"; 100 | } 101 | 102 | el = document.getElementById("info"); 103 | 104 | if (el.innerHTML != msg) { 105 | el.innerHTML = msg; 106 | } 107 | 108 | return true; 109 | 110 | } catch (e) { 111 | console.log("Error: " + e.message); 112 | return false; 113 | } 114 | } 115 | 116 | function setError(text) { 117 | return setInfo(text, false, true); 118 | } 119 | 120 | function schedule() { 121 | setTimeout(getInfo, interval); 122 | } 123 | 124 | function reload() { 125 | setTimeout(() => { 126 | document.location.reload(); 127 | }, 3000); 128 | } 129 | 130 | schedule(); 131 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------