Compare commits

...
203 Commits
Author SHA1 Message Date
KroeseandGitHub 489e3eda6f feat: Support simpler QMP configuration (#1403) 2026-08-28 04:09:16 +02:00
KroeseandGitHub b00f1d5d78 fix: Update createDevice parameters for base image (#1402) 2026-08-27 14:31:52 +02:00
renovate[bot]andGitHub 8fea46853e chore(deps): update hadolint/hadolint-action action to v3.5.0 (#1401) 2026-08-25 16:11:10 +02:00
KroeseandGitHub a41692add1 docs: Environment variables (#1400) 2026-08-25 15:32:26 +02:00
KroeseandGitHub b012806b86 build: Update Dockerfile (#1399) 2026-08-24 04:30:37 +02:00
KroeseandGitHub b9b464d2cc docs: GPU acceleration (#1398) 2026-08-23 22:03:58 +02:00
renovate[bot]andGitHub 20fa7018bc build: Update QEMU base image to v7.49 (#1397) 2026-08-23 21:01:47 +02:00
KroeseandGitHub 401d990381 docs: Environment variables (#1396) 2026-08-23 20:56:33 +02:00
KroeseandGitHub 47af1d07f4 build: Update QEMU base image to v7.48 (#1394) 2026-08-19 20:11:14 +02:00
KroeseandGitHub 4660512f12 docs: GPU acceleration (#1393) 2026-08-18 05:15:43 +02:00
KroeseandGitHub 6d37a0426b build: Update QEMU base image to v7.47 (#1392) 2026-08-18 05:14:00 +02:00
KroeseandGitHub 604cad0207 feat: Support GPU acceleration for AMD GPU's (#1391) 2026-08-18 03:23:38 +02:00
KroeseandGitHub 3ad7e78787 fix: Sanitize config variables (#1390) 2026-08-18 03:02:33 +02:00
KroeseandGitHub 5f908488ef docs: GPU acceleration (#1389) 2026-08-18 02:30:58 +02:00
KroeseandGitHub dcdc796e59 docs: GPU acceleration (#1388) 2026-08-18 02:16:21 +02:00
KroeseandGitHub 087d876eaf docs: Add AMD GPU support (#1387) 2026-08-18 02:04:17 +02:00
KroeseandGitHub 25a7d55cdd fix: Improve argument ordering (#1386) 2026-08-17 15:01:24 +02:00
KroeseandGitHub 9683b195f3 docs: Environment variables (#1385) 2026-08-16 16:12:54 +02:00
KroeseandGitHub f810576651 docs: Environment variables (#1384) 2026-08-16 15:54:51 +02:00
KroeseandGitHub 5fd954bea4 fix: Set machine name (#1383) 2026-08-16 08:45:26 +02:00
KroeseandGitHub 768b8fa51a feat: Don't remove casing from machine name (#1382) 2026-08-16 07:27:05 +02:00
KroeseandGitHub b909555ef3 fix: Make RNG device configurable (#1381) 2026-08-16 06:18:04 +02:00
KroeseandGitHub a77026eafb build: Update QEMU base image to v7.46 (#1380) 2026-08-16 03:00:51 +02:00
KroeseandGitHub e8ef357ed2 fix: Restore HTTP portal redirect fallback (#1379) 2026-08-15 11:50:16 +02:00
KroeseandGitHub a16c27ecce fix: Use replacement navigation for web transitions (#1378) 2026-08-14 19:40:47 +02:00
KroeseandGitHub 1917eae0ae fix: Do not set no-store on index page (#1377) 2026-08-14 14:02:07 +02:00
KroeseandGitHub d5bb90f012 fix: Disable COW for Virtual DSM boot image on Btrfs (#1376) 2026-08-13 16:03:19 +02:00
KroeseandGitHub d854ae811c fix: Prevent caching of web root (#1375) 2026-08-13 07:00:48 +02:00
KroeseandGitHub 03993aa773 fix: Prevent estimated progress from reaching 100% (#1374) 2026-08-11 20:08:08 +02:00
KroeseandGitHub b39ff267f2 fix: Update entrypoint (#1373) 2026-08-11 17:48:21 +02:00
KroeseandGitHub f7870d1693 fix: Only disable no-store for msg.html (#1372) 2026-08-11 15:47:14 +02:00
KroeseandGitHub c648338a18 feat: Use Intel Mesa package from base image (#1371) 2026-08-11 15:42:50 +02:00
KroeseandGitHub 9fe52c874e fix: Prevent caching of web status (#1370) 2026-08-10 23:47:43 +02:00
KroeseandGitHub c5eae88dfc fix: Improve error reporting (#1369) 2026-08-10 14:33:35 +02:00
KroeseandGitHub 1d0ac30ae1 fix: Add comment (#1368) 2026-08-10 07:25:00 +02:00
KroeseandGitHub c355cbe5dc fix: Separate stopped container status lines (#1367) 2026-08-10 07:23:35 +02:00
KroeseandGitHub 6f69eb5c68 fix: Stop console after QEMU exits (#1366) 2026-08-10 03:36:49 +02:00
KroeseandGitHub 07595976b2 feat: Use stopAllServers from base image (#1365) 2026-08-10 03:01:45 +02:00
KroeseandGitHub ec8440db03 build: Switched to QEMU base image (#1363) 2026-08-10 02:06:17 +02:00
KroeseandGitHub 06ddd19d1a fix: Add missing HTML element (#1364) 2026-08-09 14:36:02 +02:00
KroeseandGitHub 04cd929c9c feat: Show progress while extracting DSM image (#1362) 2026-08-09 13:33:33 +02:00
KroeseandGitHub c64046411b fix: Preserve web status after connection loss (#1361) 2026-08-09 13:15:44 +02:00
KroeseandGitHub cb8f440564 feat: Add QMP configuration variables (#1360) 2026-08-09 11:24:49 +02:00
KroeseandGitHub 8b6624cf2f fix: Do not fail if marker cannot be created (#1359) 2026-08-08 15:51:56 +02:00
KroeseandGitHub c70b93c2b6 docs: Readme (#1358) 2026-08-07 22:21:14 +02:00
KroeseandGitHub 4c700e87fe fix: Improve nginx configuration handling (#1357) 2026-08-07 22:20:28 +02:00
KroeseandGitHub 030255004f fix: Handle privileged user-mode ports (#1356) 2026-08-07 22:19:18 +02:00
KroeseandGitHub e64f9f2c39 fix: Wait for websocket sockets during startup (#1355) 2026-08-07 12:59:56 +02:00
KroeseandGitHub 537fc97d53 feat: Use Unix sockets for internal services (#1354) 2026-08-07 03:30:03 +02:00
KroeseandGitHub 438e992653 feat: Check available memory before installation starts (#1353) 2026-08-07 02:52:21 +02:00
KroeseandGitHub 615fad2a0d feat: Use dynamic PCI bus configuration (#1352) 2026-08-06 14:44:03 +02:00
KroeseandGitHub aac543e5c7 fix: Restrict minimal configured RAM amount to 1 GB (#1351) 2026-08-06 14:38:10 +02:00
KroeseandGitHub fec4f876e7 fix: Race between signal handler and cleanup (#1350) 2026-08-05 21:09:01 +02:00
KroeseandGitHub dced815c06 docs: Improve commenting (#1349) 2026-08-03 16:40:10 +02:00
KroeseandGitHub 51d6f3aeca fix: Reserve internal ports for user-mode forwarding (#1348) 2026-08-02 23:01:52 +02:00
KroeseandGitHub 4544620d48 feat: Increase indentation of printed QEMU arguments (#1347) 2026-08-01 05:49:50 +02:00
KroeseandGitHub de8468161f feat: Refactor negated command conditions (#1346) 2026-07-30 21:22:21 +02:00
renovate[bot]andGitHub 18d6be8210 chore(deps): update hadolint/hadolint-action action to v3.4.0 (#1345) 2026-07-30 21:06:27 +02:00
KroeseandGitHub e5b8cf3bf8 build: Update Passt to v2026_07_28 (#1344) 2026-07-30 10:10:41 +02:00
KroeseandGitHub ec12039f43 feat: Use deadline-based process timeouts (#1343) 2026-07-28 15:22:47 +02:00
KroeseandGitHub 3c7c3ca1b1 fix: Prevent race when reading PID files (#1342) 2026-07-28 14:22:08 +02:00
KroeseandGitHub 2b27f32cd4 fix: Disk options were applied to the controller (#1341) 2026-07-28 03:30:55 +02:00
KroeseandGitHub 55d1d50284 fix: Retry transient gateway errors in web status (#1340) 2026-07-27 23:49:22 +02:00
KroeseandGitHub 784b73b5b5 feat: Add DISK_OPTIONS support to disk devices (#1339) 2026-07-27 23:48:21 +02:00
KroeseandGitHub 85f00bb9cc docs: Added new DISK_OPTIONS variable (#1338) 2026-07-27 12:04:25 +02:00
KroeseandGitHub efe8732e47 fix: Avoid shadowing PID output variables (#1337) 2026-07-27 12:03:24 +02:00
KroeseandGitHub f6871dd61b fix: Clear invalid PID values in shared reader (#1336) 2026-07-26 17:52:14 +02:00
KroeseandGitHub a713b728ff fix: Use shared PID reader for helper processes (#1335) 2026-07-26 17:41:46 +02:00
KroeseandGitHub 0d74c6ac80 build: Update dependabot config (#1334) 2026-07-26 00:58:36 +02:00
KroeseandGitHub 63e4529eff fix: Add cleanup for failed web server startup (#1333) 2026-07-25 13:47:20 +02:00
KroeseandGitHub afd848cebb fix: Make healthcheck succeed during download (#1332) 2026-07-25 12:16:41 +02:00
KroeseandGitHub 695b075130 feat: Provide host access through system.lan (#1331) 2026-07-25 03:38:36 +02:00
KroeseandGitHub 7eff53e722 feat: Improve disk error handling (#1328) 2026-07-24 13:51:59 +02:00
KroeseandGitHub bcaf0e5980 fix: Improve network error handling (#1327) 2026-07-24 13:27:09 +02:00
KroeseandGitHub faa820c2cc feat: Inline local variable declarations (#1326) 2026-07-24 13:11:40 +02:00
KroeseandGitHub 81e477fcc4 feat: Warn when DSM and container share an IP address (#1325) 2026-07-24 12:45:54 +02:00
KroeseandGitHub abd3a1c3df fix: Network mode shown for DHCP mode (#1324) 2026-07-24 12:03:57 +02:00
KroeseandGitHub 3b59bcd284 fix: Ensure progress reaches 100% (#1322) 2026-07-23 23:03:54 +02:00
KroeseandGitHub f1b56f394a build: Create a detailed .gitignore file (#1321) 2026-07-23 15:42:59 +02:00
KroeseandGitHub 1a983ebcd1 feat: Improve error handling for webserver config (#1320) 2026-07-23 04:17:26 +02:00
KroeseandGitHub 41c76198fa feat: Use atomic writes for progress updates (#1319) 2026-07-23 04:10:09 +02:00
KroeseandGitHub 222b5649b0 fix: Prevent stale polling responses for web status (#1318) 2026-07-23 04:00:12 +02:00
KroeseandGitHub e14fd4b711 docs: Environment variables (#1317) 2026-07-21 13:14:32 +02:00
KroeseandGitHub 6fe19a8af4 feat: Improve download progress reporting (#1316) 2026-07-21 06:00:58 +02:00
KroeseandGitHub 072b5c3070 build: Update workflow (#1315) 2026-07-21 01:31:29 +02:00
KroeseandGitHub 89dbaeb2a4 feat: Improve download progress reporting (#1314) 2026-07-21 00:13:53 +02:00
KroeseandGitHub 02f76b44cd fix: Resolve shellcheck error (#1313) 2026-07-20 17:00:32 +02:00
KroeseandGitHub 99d6b8f830 feat: Preserve graceful shutdown for interactive console (#1312) 2026-07-20 16:31:21 +02:00
KroeseandGitHub e6b68b2f08 fix: Avoid shellcheck warning (#1311) 2026-07-20 14:32:03 +02:00
KroeseandGitHub 7a8f0041e7 build: Update workflow (#1310) 2026-07-20 12:31:13 +02:00
KroeseandGitHub 64809bb4d4 feat: Support progress tracking for segmented downloads (#1309) 2026-07-20 05:48:16 +02:00
KroeseandGitHub f08b2cbd99 fix: Decrease default timeout (#1307) 2026-07-19 09:17:51 +02:00
KroeseandGitHub 18d9ecd434 fix: Decrease default timeout (#1308) 2026-07-19 08:57:09 +02:00
KroeseandGitHub 6944293a32 fix: Add Recreate strategy to Kubernetes deployment (#1306) 2026-07-19 03:15:10 +02:00
KroeseandGitHub d807a365ff feat: Added optional lossy VNC compression (#1305) 2026-07-19 03:09:35 +02:00
KroeseandGitHub 066af83d07 fix: Decrease default timeout (#1304) 2026-07-19 01:03:21 +02:00
KroeseandGitHub 81536818df feat: Add escapeHTML function (#1303) 2026-07-18 22:00:45 +02:00
KroeseandGitHub 949c288565 fix: Set correct MTU option for dnsmasq (#1302) 2026-07-18 20:40:52 +02:00
KroeseandGitHub 5d5bbbcf4d feat: Improve NAT networking (#1300) 2026-07-18 15:34:46 +02:00
KroeseandGitHub 82a62c0240 build: Update workflow (#1301) 2026-07-18 15:32:07 +02:00
KroeseandGitHub 5399e1d463 feat: Use Unix sockets for qemu-host communication (#1299) 2026-07-18 11:31:20 +02:00
KroeseandGitHub c7e7b60f01 fix: Improve IP tables cleanup verification (#1298) 2026-07-18 10:54:20 +02:00
KroeseandGitHub f1defa6890 docs: Environment variables (#1297) 2026-07-18 00:28:24 +02:00
KroeseandGitHub a7fb161974 feat: Improve download progress readability (#1296) 2026-07-17 22:59:07 +02:00
KroeseandGitHub dcdf5e5293 fix: Improve interactive terminal detection (#1295) 2026-07-17 22:45:07 +02:00
KroeseandGitHub 1eb0db66ba feat: Improve download progress in container logs (#1294) 2026-07-17 22:29:10 +02:00
KroeseandGitHub 3eec8e03ed feat: Improve AMD detection (#1293) 2026-07-17 20:54:41 +02:00
KroeseandGitHub 5483981ed4 feat: Refactor config code (#1292) 2026-07-17 20:37:54 +02:00
KroeseandGitHub 62330a5bf4 fix: Cleanup stale pipe files (#1291) 2026-07-17 20:01:53 +02:00
KroeseandGitHub 2b0b59dfba feat: Preserve graceful shutdown for interactive console (#1290) 2026-07-17 19:56:05 +02:00
KroeseandGitHub b680ff9dd8 feat: Preserve graceful shutdown for interactive console (#1289) 2026-07-17 19:16:30 +02:00
KroeseandGitHub 5c889b272d fix: Store pid for serial debugging (#1288) 2026-07-17 14:44:49 +02:00
KroeseandGitHub 585ebb3f53 build: Update Passt to v2026_07_16 (#1287) 2026-07-17 13:49:26 +02:00
KroeseandGitHub 401307b981 build: Update QEMU host to v2.06 (#1286) 2026-07-17 13:24:34 +02:00
KroeseandGitHub 46820fe6eb build: Set dependabot cooldown period (#1284) 2026-07-17 13:20:19 +02:00
KroeseandGitHub 9452d48694 feat: Make machine type configurable (#1283) 2026-07-17 11:33:05 +02:00
KroeseandGitHub 5c3fed1387 fix: Clear service logs on startup (#1281) 2026-07-17 06:26:25 +02:00
KroeseandGitHub 797411ae7f fix: Show QEMU errors on unexpected exit (#1280) 2026-07-17 05:54:04 +02:00
KroeseandGitHub e81c509208 feat: Check RENDERNODE permissions (#1279) 2026-07-17 02:11:38 +02:00
KroeseandGitHub ace8614a50 feat: Refactor disk code (#1278) 2026-07-17 01:52:47 +02:00
KroeseandGitHub 8263b3a737 feat: Improve iptable backend selection (#1277) 2026-07-17 01:51:39 +02:00
KroeseandGitHub 61c6142988 feat: Remove unused functions (#1276) 2026-07-17 00:10:59 +02:00
KroeseandGitHub 5d26338bb3 feat: Improve detection of valid data disk (#1275) 2026-07-16 21:41:29 +02:00
KroeseandGitHub 084d475cde build: Add diffutils package (#1274) 2026-07-16 20:55:47 +02:00
KroeseandGitHub c36b0dcb00 docs: Environment variables (#1273) 2026-07-16 20:50:17 +02:00
KroeseandGitHub 6e9993742b feat: Limit wget progress output (#1272) 2026-07-15 23:01:39 +02:00
KroeseandGitHub 6946af0212 build: Update workflow (#1271) 2026-07-15 21:55:07 +02:00
KroeseandGitHub 0a9361d62f fix: Localize available memory formatting (#1270) 2026-07-15 21:13:51 +02:00
KroeseandGitHub 7cbfcd44a6 fix: Remove unnecessary guards (#1269) 2026-07-15 21:00:50 +02:00
KroeseandGitHub 6bcacf5e59 feat: Improve boolean helpers (#1268) 2026-07-15 19:35:16 +02:00
KroeseandGitHub 3bea87f087 feat: Suppress empty iptables debug sections (#1267) 2026-07-15 19:21:43 +02:00
KroeseandGitHub f01e1564f7 fix: Make setting owner non-fatal (#1266) 2026-07-15 18:00:24 +02:00
KroeseandGitHub cf8cdbf0c1 fix: Make failed chown non-fatal (#1265) 2026-07-15 17:39:05 +02:00
KroeseandGitHub 0813494ee0 feat: Use cgroup when calculating RAM (#1264) 2026-07-15 15:40:51 +02:00
KroeseandGitHub 0d71e30f50 feat: Add state helpers to utils (#1263) 2026-07-15 15:28:42 +02:00
KroeseandGitHub c203deab9a feat: Improve webserver configuration (#1262) 2026-07-15 15:27:24 +02:00
KroeseandGitHub 9cf31df266 fix: Validate subnet mask (#1261) 2026-07-15 14:17:01 +02:00
KroeseandGitHub 0cd08f2ee1 fix: Improve NAT port forwarding (#1260) 2026-07-15 12:24:25 +02:00
KroeseandGitHub a32d215e90 feat: Improve wget error reporting (#1259) 2026-07-15 11:34:55 +02:00
KroeseandGitHub c1f9a690f9 fix: Remove dots from info line (#1258) 2026-07-15 09:00:14 +02:00
KroeseandGitHub 5de504deb3 feat: Improve rootless container detection (#1256) 2026-07-15 06:02:12 +02:00
KroeseandGitHub 250eb18238 docs: Readme (#1255) 2026-07-14 12:50:07 +02:00
KroeseandGitHub ca80da59bd docs: Environment variables (#1254) 2026-07-14 11:29:10 +02:00
KroeseandGitHub 9cd86dfe82 fix: Use exec form for health check (#1253) 2026-07-13 19:40:30 +02:00
KroeseandGitHub 5bb93651f7 docs: Readme (#1252) 2026-07-13 00:38:58 +02:00
KroeseandGitHub 0c268308a4 feat: Preserve source image until conversion completes (#1251) 2026-07-12 23:33:10 +02:00
KroeseandGitHub 7b741d3e4b feat: Warn about UDP host ports (#1250) 2026-07-12 22:14:11 +02:00
KroeseandGitHub f1d29212ae feat: Improve iptables rule cleanup (#1249) 2026-07-12 21:58:49 +02:00
KroeseandGitHub fdee9f89a3 feat: Improve iptables backend selection (#1248) 2026-07-12 21:36:52 +02:00
KroeseandGitHub 210a360797 fix: Restore compatible NAT forwarding rules (#1247) 2026-07-12 13:18:52 +03:00
KroeseandGitHub 991bb2627a fix: Prefer legacy iptables backend for NAT (#1246) 2026-07-12 12:44:00 +03:00
KroeseandGitHub 764a186836 feat: Allow all forwarded NAT traffic (#1245) 2026-07-11 11:58:35 +03:00
KroeseandGitHub 726786a98a feat: Default host and user ports to TCP only (#1244) 2026-07-09 11:06:01 +02:00
KroeseandGitHub 2caf97d407 docs: Network modes (#1243) 2026-07-09 00:39:52 +02:00
KroeseandGitHub 0e67286599 fix: Improve file handling (#1242) 2026-07-08 23:55:02 +02:00
KroeseandGitHub 2b9fc80275 feat: Validate MAC file writes (#1241) 2026-07-08 23:28:56 +02:00
KroeseandGitHub 0d88774bd3 feat: Display base directory in warnings (#1240) 2026-07-08 23:16:20 +02:00
KroeseandGitHub fa23052ed4 fix: Set default DHCP lease time to 1 hour (#1239) 2026-07-08 22:46:47 +02:00
KroeseandGitHub cc3267f258 docs: Correct hostname (#1238) 2026-07-08 18:57:55 +02:00
KroeseandGitHub fd002564a6 feat: Validate custom QEMU CPU arguments (#1237) 2026-07-08 18:50:31 +02:00
KroeseandGitHub 2703a97cfa docs: Refer to environment documentation (#1236) 2026-07-08 18:44:04 +02:00
KroeseandGitHub 29e2d034bf docs: Document all environment variables (#1235) 2026-07-08 17:11:10 +02:00
KroeseandGitHub cbe3644335 fix: Add extra line after boot (#1234) 2026-07-08 16:50:21 +02:00
KroeseandGitHub c9a2a6966b feat: Always show network info (#1233) 2026-07-07 19:37:59 +02:00
KroeseandGitHub 498248f43e feat: Display guest info before mode warning (#1232) 2026-07-07 12:21:25 +02:00
KroeseandGitHub 8963228b1c fix: Set returnvalue for waitForPort (#1231) 2026-07-07 12:13:13 +02:00
KroeseandGitHub 827d6d9e10 fix: Status monitor exited early (#1230) 2026-07-07 12:03:58 +02:00
KroeseandGitHub 1c577abef4 fix: Guest IP was not retrieved correctly (#1229) 2026-07-07 11:16:22 +02:00
KroeseandGitHub 20fd1a975c fix: Read QEMU pid correctly (#1228) 2026-07-07 10:53:53 +02:00
KroeseandGitHub 770774680c feat: Improve shutdown logic (#1226) 2026-07-07 03:16:35 +02:00
KroeseandGitHub 0c75aa81fe fix: Detect guest IP without assuming eth0 (#1225) 2026-07-06 23:54:24 +02:00
KroeseandGitHub 36ec57ac15 fix: Initialize indexdb path before extraction (#1224) 2026-07-06 23:50:26 +02:00
KroeseandGitHub 9c488c52b9 feat: Normalize disk image format (#1223)
*
2026-07-06 23:44:14 +02:00
KroeseandGitHub 2ba756260f fix: Sanitize guest hostname (#1222) 2026-07-06 23:33:34 +02:00
KroeseandGitHub b306012743 feat: Normalize discard mode for qemu drives (#1221) 2026-07-06 23:21:25 +02:00
KroeseandGitHub 61b3ee7e01 feat: Read qcow2 virtual size from qemu-img json (#1220) 2026-07-06 23:15:27 +02:00
KroeseandGitHub 245036f844 fix: Preserve port protocols in passt mode (#1219) 2026-07-06 23:11:34 +02:00
KroeseandGitHub 68cadfe035 fix: Tolerate missing sector-size output (#1218) 2026-07-06 22:56:00 +02:00
KroeseandGitHub 96102832b1 fix: Tolerate missing sector-size output (#1217) 2026-07-06 22:38:58 +02:00
KroeseandGitHub 211a56bf73 feat: Show additional guest info in debug mode (#1216) 2026-07-06 22:11:03 +02:00
KroeseandGitHub 506fb7b114 fix: Parse Debian ipcalc output correctly (#1215) 2026-07-06 21:58:11 +02:00
KroeseandGitHub bcc3393b7f fix: Shutdown Passt after DNS failure (#1214) 2026-07-06 19:21:48 +02:00
KroeseandGitHub 45c0f91d6e feat: Refactor networking code (#1213) 2026-07-06 17:47:29 +02:00
KroeseandGitHub a1910546a6 feat: Check file operation results (#1212) 2026-07-06 07:44:41 +02:00
KroeseandGitHub 637ed0e3a6 fix: Do not match multiple files (#1211) 2026-07-06 05:09:29 +02:00
KroeseandGitHub 954a6076ad feat: Add returnvalues to functions (#1210) 2026-07-06 03:48:39 +02:00
KroeseandGitHub 415aaa3038 feat: Refactor server code (#1209) 2026-07-05 23:35:56 +02:00
KroeseandGitHub c87e45719b feat: Refactor serial code (#1208) 2026-07-05 23:24:43 +02:00
KroeseandGitHub 102945ab48 feat: Refactor memory check (#1207) 2026-07-05 23:17:53 +02:00
KroeseandGitHub 1c3d490303 feat: Refactor status monitor (#1206) 2026-07-05 23:02:51 +02:00
KroeseandGitHub 73fa70f52f feat: Refactor CPU logic (#1205) 2026-07-05 22:46:39 +02:00
KroeseandGitHub 7b095702fc feat: Refactor shutdown logic (#1204) 2026-07-05 22:09:49 +02:00
KroeseandGitHub f82b2769d3 feat: Refactor entrypoint (#1203) 2026-07-05 21:43:10 +02:00
Peng LiuandGitHub cafe619028 fix: Parse quoted values consistently (#1202) 2026-07-05 21:25:33 +02:00
KroeseandGitHub e94a0c4a52 feat: Refactor disk code (#1201) 2026-07-05 20:09:47 +02:00
KroeseandGitHub d6a353df0e feat: Refactor network code (#1200) 2026-07-05 19:58:36 +02:00
KroeseandGitHub 050812e996 fix: Filter IPv6 DNS records when disabled (#1199) 2026-07-05 19:30:41 +02:00
KroeseandGitHub 75280e9aa9 feat: Improve bus detection (#1198) 2026-07-05 18:51:56 +02:00
KroeseandGitHub 8dcc8ccee1 fix: Move MTU check to finish (#1197) 2026-07-05 18:34:55 +02:00
KroeseandGitHub ba866e9e75 feat: Improve support for non-standard MTU sizes (#1196) 2026-07-05 17:10:59 +02:00
KroeseandGitHub 06e7a8edb9 docs: Readme (#1195) 2026-07-05 14:49:47 +02:00
KroeseandGitHub 495c67734a fix: Check adapter before setting MTU (#1194) 2026-07-05 03:13:14 +02:00
KroeseandGitHub 1557735331 build: Use heredoc for Dockerfile commands (#1193) 2026-07-04 20:34:32 +02:00
33 changed files with 2405 additions and 3568 deletions
+11
View File
@@ -1,10 +1,21 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: docker - package-ecosystem: docker
directory: / directory: /
schedule: schedule:
interval: weekly interval: weekly
cooldown:
default-days: 7
- package-ecosystem: github-actions - package-ecosystem: github-actions
directory: / directory: /
schedule: schedule:
interval: weekly interval: weekly
cooldown:
default-days: 7
ignore:
- dependency-name: "*"
update-types:
- version-update:semver-minor
- version-update:semver-patch
+13 -6
View File
@@ -1,5 +1,7 @@
on: [workflow_call] on: [workflow_call]
name: "Check" name: "Check"
permissions: {} permissions: {}
jobs: jobs:
@@ -15,16 +17,21 @@ jobs:
name: Run ShellCheck name: Run ShellCheck
uses: ludeeus/action-shellcheck@master uses: ludeeus/action-shellcheck@master
env: env:
SHELLCHECK_OPTS: -x --source-path=src -e SC2001 -e SC2034 -e SC2064 -e SC2317 -e SC2153 -e SC2028 SHELLCHECK_OPTS: >-
-x
--source-path=src
-e SC1091
-e SC2001
-e SC2034
-e SC2317
-e SC2153
- -
name: Lint Dockerfile name: Lint Dockerfile
uses: hadolint/hadolint-action@v3.3.0 uses: hadolint/hadolint-action@v3.5.0
with: with:
dockerfile: Dockerfile dockerfile: Dockerfile
ignore: DL3008,DL3003,DL3006,DL3013 ignore: DL3008,DL3067
failure-threshold: warning failure-threshold: warning
- -
name: Validate JSON and YML files name: Validate JSON and YML files
uses: GrantBirki/json-yaml-validate@v5.0.0 uses: GrantBirki/json-yaml-validate@v5
with:
yaml_exclude_regex: ".*\\kubernetes\\.yml$"
+1 -58
View File
@@ -11,61 +11,4 @@ permissions:
jobs: jobs:
review: review:
name: review name: review
runs-on: ubuntu-latest uses: action-pack/.github/.github/workflows/review.yml@master
steps:
-
name: Checkout
uses: actions/checkout@v7
- parallel:
-
name: Spelling
uses: reviewdog/action-misspell@v1
with:
locale: "US"
level: warning
pattern: |
*.md
*.sh
reporter: github-pr-review
github_token: ${{ secrets.GITHUB_TOKEN }}
-
name: Hadolint
uses: reviewdog/action-hadolint@v1
with:
level: warning
fail_level: error
reporter: github-pr-review
hadolint_ignore: DL3008 DL3003 DL3006 DL3013
github_token: ${{ secrets.GITHUB_TOKEN }}
-
name: YamlLint
uses: reviewdog/action-yamllint@v1
with:
level: warning
reporter: github-pr-review
github_token: ${{ secrets.GITHUB_TOKEN }}
-
name: ActionLint
uses: reviewdog/action-actionlint@v1
with:
level: warning
reporter: github-pr-review
github_token: ${{ secrets.GITHUB_TOKEN }}
-
name: Shellformat
uses: reviewdog/action-shfmt@v1
if: false
with:
level: warning
fail_on_error: "true"
shfmt_flags: "-i 2 -ci -bn"
github_token: ${{ secrets.GITHUB_TOKEN }}
-
name: Shellcheck
uses: reviewdog/action-shellcheck@v1
with:
level: warning
fail_level: error
reporter: github-pr-review
shellcheck_flags: -x -e SC1091 -e SC2001 -e SC2034 -e SC2064 -e SC2317 -e SC2153 -e SC2028
github_token: ${{ secrets.GITHUB_TOKEN }}
+268 -1
View File
@@ -1 +1,268 @@
build.sh ##############################
# Operating System Files
##############################
.DS_Store
.AppleDouble
.LSOverride
Thumbs.db
ehthumbs.db
Desktop.ini
Icon?
$RECYCLE.BIN/
.Spotlight-V100/
.Trashes/
.fseventsd
##############################
# IDEs
##############################
.vscode/
.idea/
*.iml
*.ipr
*.iws
##############################
# VS Code
##############################
.history/
*.code-workspace
##############################
# Vim
##############################
*.swp
*.swo
Session.vim
##############################
# Sublime
##############################
*.sublime-workspace
*.sublime-project
##############################
# Temporary Files
##############################
*.tmp
*.temp
*.bak
*.old
*.orig
*.rej
*.save
##############################
# Logs
##############################
*.log
logs/
log/
*.out
*.err
*.trace
##############################
# Runtime
##############################
*.pid
*.seed
*.pid.lock
##############################
# Secrets
##############################
.env
.env.*
!.env.example
*.pem
*.key
*.crt
*.cer
*.p12
*.pfx
*.kdbx
*.secret
*.token
##############################
# SSH
##############################
.ssh/
##############################
# Docker
##############################
docker-compose.override.yml
docker-compose.local.yml
##############################
# VM Storage
##############################
storage/
windows/
downloads/
##############################
# Disk Images
##############################
*.qcow2
*.qcow
*.vhd
*.vhdx
*.vdi
*.raw
*.img
*.iso
*.bin
##############################
# QEMU
##############################
*.nvram
*.fd
*.efi
*.sock
*.monitor
*.serial
##############################
# Samba
##############################
shared/
share/
##############################
# Backups
##############################
backup/
backups/
*.backup
##############################
# Cache
##############################
.cache/
.cache-loader/
.tmp/
temp/
tmp/
##############################
# Python
##############################
__pycache__/
*.py[cod]
.pytest_cache/
.mypy_cache/
.venv/
venv/
##############################
# Node
##############################
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
##############################
# Build
##############################
dist/
build/
out/
release/
##############################
# Coverage
##############################
coverage/
.coverage
coverage.xml
##############################
# Archives
##############################
*.zip
*.tar
*.tar.gz
*.tgz
*.7z
*.rar
##############################
# Generated Config
##############################
config.local.*
settings.local.*
local.env
##############################
# Test Files
##############################
test-output/
playwright-report/
##############################
# macOS
##############################
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
##############################
# Linux
##############################
*~
.nfs*
##############################
# Windows
##############################
*.stackdump
##############################
# Misc
##############################
*.cache
*.lock
*.lock.json
*.bak.*
##############################
# Keep Examples
##############################
!.gitkeep
!.env.example
+23 -46
View File
@@ -1,72 +1,49 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
FROM qemux/qemu-host:2.05 AS builder FROM scratch
FROM debian:trixie-slim
COPY --from=qemux/qemu:7.49 / /
ARG TARGETARCH
ARG TARGETPLATFORM
ARG VERSION_ARG="0.0" ARG VERSION_ARG="0.0"
ARG VERSION_PASST="2026_06_11" ARG VERSION_CSTRUCT="4.7"
ARG DEBCONF_NOWARNINGS="yes" ARG DEBCONF_NOWARNINGS="yes"
ARG DEBIAN_FRONTEND="noninteractive" ARG DEBIAN_FRONTEND="noninteractive"
ARG DEBCONF_NONINTERACTIVE_SEEN="true" ARG DEBCONF_NONINTERACTIVE_SEEN="true"
RUN set -eu && \ RUN <<EOF
apt-get update && \ set -eu
apt-get update
apt-get --no-install-recommends -y install \ apt-get --no-install-recommends -y install \
jq \
tini \
curl \
wget \
fdisk \
unzip \
nginx \
procps \
ethtool \
python3 \
python3-pip \
python3-msgpack \
python3-pysodium \
xz-utils \
iptables \
iproute2 \
dnsmasq \
fakeroot \ fakeroot \
apt-utils \ python3-msgpack \
net-tools \ python3-pysodium
e2fsprogs \
qemu-utils \ apt-get clean
websocketd \
iputils-ping \ # Install Python dependencies
inotify-tools \ pip3 install --no-cache-dir --break-system-packages --root-user-action=ignore "dissect.cstruct==$VERSION_CSTRUCT"
ca-certificates \
netcat-openbsd \ # Set version file
qemu-system-x86 && \ echo "$VERSION_ARG" > /etc/version
wget "https://github.com/qemus/passt/releases/download/v${VERSION_PASST}/passt_${VERSION_PASST}_${TARGETARCH}.deb" -O /tmp/passt.deb -q --timeout=10 && \
dpkg -i /tmp/passt.deb && \
apt-get clean && \
pip3 install --no-cache-dir --break-system-packages --root-user-action=ignore dissect.cstruct && \
mkdir -p /etc/qemu && \
echo "allow br0" > /etc/qemu/bridge.conf && \
unlink /etc/nginx/sites-enabled/default && \
sed -i 's/^worker_processes.*/worker_processes 1;/' /etc/nginx/nginx.conf && \
echo "$VERSION_ARG" > /etc/version && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
EOF
COPY --chmod=755 ./src /run/ COPY --chmod=755 ./src /run/
COPY --chmod=755 ./web /var/www/ COPY --chmod=755 ./web /var/www/
COPY --chmod=755 --from=builder /qemu-host.bin /run/host.bin
COPY --chmod=744 ./web/conf/nginx.conf /etc/nginx/default.conf COPY --chmod=744 ./web/conf/nginx.conf /etc/nginx/default.conf
COPY --chmod=755 --from=qemux/qemu-host:2.06 /qemu-host.bin /run/host.bin
ADD --chmod=775 https://raw.githubusercontent.com/sud0woodo/patology/refs/heads/main/patology.py /run/extract.py ADD --chmod=775 https://raw.githubusercontent.com/sud0woodo/patology/refs/heads/main/patology.py /run/extract.py
VOLUME /storage VOLUME /storage
EXPOSE 22 139 445 5000 EXPOSE 445 5000
ENV RAM_SIZE="2G" ENV RAM_SIZE="2G"
ENV CPU_CORES="2" ENV CPU_CORES="2"
ENV DISK_SIZE="256G" ENV DISK_SIZE="256G"
HEALTHCHECK --interval=60s --start-period=45s --retries=2 CMD /run/check.sh HEALTHCHECK --interval=60s --start-period=45s --retries=2 CMD ["/run/check.sh"]
ENTRYPOINT ["/usr/bin/tini", "-s", "/run/entry.sh"] ENTRYPOINT ["/usr/bin/tini", "-s", "/run/entry.sh"]
+115
View File
@@ -0,0 +1,115 @@
# Environment Variables
This page lists all the environment variables that can be used to configure the container.
An empty default means the variable is unset and its value is determined automatically when applicable.
## 💽 Virtual DSM
| Variable | Default | Description |
|---|---|---|
| `URL` | | URL or local path to a custom `.pat` installation file. |
| `COUNTRY` | | Country code used to select the Synology download mirror. |
| `HOST_MAC` | | MAC address reported to DSM. |
| `HOST_MODEL` | | Synology host model reported to DSM. |
| `HOST_SERIAL` | | Synology host serial number reported to DSM. |
| `GUEST_SERIAL` | | Synology guest serial number reported to DSM. |
## 🧠 CPU and Memory
| Variable | Default | Description |
|---|---|---|
| `CPU_CORES` | `2` | Number of virtual CPU cores, such as `4`, `half`, or `max`. |
| `CPU_MODEL` | `host` | QEMU CPU model. |
| `CPU_FLAGS` | | Additional QEMU CPU flags. |
| `HOST_CPU` | | CPU name reported to DSM. Selected automatically when unset. |
| `KVM` | `Y` | Enables KVM hardware acceleration. |
| `RAM_SIZE` | `2G` | Amount of RAM assigned to DSM, such as `2G`, `4G`, `half`, or `max`. |
| `RAM_BACKEND` | | Guest RAM backing mechanism, set `memfd` for memfd-backed memory. |
| `RAM_CHECK` | `Y` | Checks whether enough host memory is available before starting DSM. |
## 💾 Storage
| Variable | Default | Description |
|---|---|---|
| `DISK_SIZE` | `256G` | Size of the main data disk. |
| `DISK_FMT` | `raw` | Disk image format: `raw` or `qcow2`. |
| `DISK_TYPE` | `scsi` | Disk device type, such as `ide`, `scsi`, or `blk`. |
| `DISK_CACHE` | `none` | Disk cache mode, such as `none` or `writeback`. |
| `DISK_IO` | `native` | Disk I/O mode, such as `native`, `threads`, or `io_uring`. |
| `DISK_DISCARD` | `unmap` | Discard/TRIM mode for the primary disk. |
| `DISK_ROTATION` | `1` | Rotation rate reported to the guest. Use `1` to identify the disk as an SSD. |
| `DISK_FLAGS` | | Additional options used when creating `qcow2` disks. |
| `DISK_OPTIONS` | | Additional options appended to QEMU disk devices. |
| `ALLOCATE` | `N` | Preallocates space for the data disks. |
| `STORAGE` | `/storage` | Storage directory used for disks, settings, and downloads. |
## 🌐 Networking
| Variable | Default | Description |
|---|---|---|
| `NETWORK` | | Network mode, such as `nat`, `user`, or `N` to disable networking. |
| `DHCP` | `N` | Enables macvtap networking so DSM receives a DHCP address. |
| `HOST` | | Hostname assigned to the machine on the network. |
| `IP` | | Overrides the automatically selected guest IPv4 address. |
| `MAC` | | Guest network adapter MAC address. |
| `ADAPTER` | `virtio-net-pci` | QEMU network adapter model. |
| `DEV` | `eth0` | Container network interface used as the uplink. |
| `MTU` | | MTU assigned to the guest network interface. |
| `MASK` | `255.255.255.0` | IPv4 netmask for guest network. |
| `TAP` | `qemu` | TAP or macvtap interface name. |
| `BRIDGE` | `docker` | Bridge name used for NAT networking. |
| `HOST_PORTS` | | Ports excluded from guest forwarding. |
| `USER_PORTS` | | Additional ports to forward to DSM when using user-mode networking. |
| `DNSMASQ_OPTS` | | Additional options passed to dnsmasq. |
| `DNSMASQ_DEBUG` | `N` | Enables dnsmasq debug output. |
| `DNSMASQ_DISABLE` | `N` | Disables the internal dnsmasq resolver. |
| `PASST_OPTS` | | Additional options passed to passt. |
| `PASST_DEBUG` | `N` | Enables passt debug output. |
## 🖥️ Display
| Variable | Default | Description |
|---|---|---|
| `DISPLAY` | `none` | Display backend, such as `vnc`, `disabled`, or `none`. |
| `VGA` | `none` | QEMU video adapter model. |
| `GPU` | `N` | Enables GPU acceleration. |
| `VRAM_SIZE` | `4G` | Virtual GPU memory budget. |
| `RENDERNODE` | `/dev/dri/renderD128` | Render node used for GPU acceleration. |
## 🎈 Memory Ballooning
Also see [Dynamic memory allocation](https://github.com/qemus/qemu/blob/master/docs/ballooning.md) for usage instructions and important caveats.
| Variable | Default | Description |
|---|---|---|
| `BALLOONING` | `N` | Enables dynamic memory ballooning. |
| `BALLOONING_MIN_MEM` | `33%` | Minimum amount of memory retained by the VM. |
| `BALLOONING_RAM_THRESHOLD` | `80.0` | Host RAM usage percentage at which ballooning begins adjusting memory. |
| `BALLOONING_RAM_THRESHOLD_HARD` | `90.0` | Host RAM usage percentage at which ballooning becomes more aggressive. |
| `BALLOONING_PSI_PRESSURE` | `10.0` | PSI memory pressure level at which ballooning becomes more aggressive. |
| `BALLOONING_PSI_PRESSURE_MAX` | `50.0` | PSI memory pressure level at which ballooning reaches its strongest response. |
| `BALLOONING_HYSTERESIS` | `128M` | Minimum memory change before the balloon target is updated. |
| `BALLOONING_KP` | `0.5` | Proportional gain used by the ballooning controller. |
| `BALLOONING_KI` | `0.05` | Integral gain used by the ballooning controller. |
| `BALLOONING_INTERVAL` | `5` | Polling interval in seconds. |
| `BALLOONING_DEBUG` | `N` | Enables debug output for the ballooning monitor. |
## 🔌 Shutdown
| Variable | Default | Description |
|---|---|---|
| `SHUTDOWN` | `Y` | Enables graceful shutdown. |
| `TIMEOUT` | `105` | Maximum time, in seconds, to wait before forcing DSM to stop. |
| `API_TIMEOUT` | `90` | Maximum time, in seconds, to wait for the shutdown API call. |
## 🐞 Debugging
| Variable | Default | Description |
|---|---|---|
| `DEBUG` | `N` | Enables verbose debug output. |
| `TRACE` | `N` | Enables shell command tracing. |
| `HOST_DEBUG` | `N` | Enables debug output for the DSM host helper. |
| `MONITOR` | | QEMU monitor, such as `/storage/monitor.sock` or `4444`. |
| `QMP` | | QEMU Machine Protocol, such as `/storage/qmp.sock` or `4444`. |
| `ARGUMENTS` | | Additional raw arguments appended to the QEMU command line. |
+2
View File
@@ -18,6 +18,8 @@ metadata:
name: dsm name: dsm
spec: spec:
replicas: 1 replicas: 1
strategy:
type: Recreate
selector: selector:
matchLabels: matchLabels:
app: dsm app: dsm
+75 -40
View File
@@ -16,9 +16,14 @@ Virtual DSM in a Docker container.
## Features ✨ ## Features ✨
- Multiple disks - Runs Virtual DSM inside a Docker container
- KVM acceleration - Automatic download of the installation files
- Automatic download - Web-based access to the DSM desktop
- Near-native performance with KVM acceleration
- Customizable CPU, memory, and storage allocation
- Supports multiple disks and physical disk passthrough
- Dynamic memory allocation with memory ballooning
- Supports NAT, user-mode, macvlan, and macvtap networking
## Usage 🐳 ## Usage 🐳
@@ -62,12 +67,13 @@ kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/m
## Requirements ⚙️ ## Requirements ⚙️
- A Linux host with KVM support, or Docker Desktop / Podman on Windows 11 with nested virtualization enabled. - Docker or Podman on a Linux host with KVM support.
- At least 2 GB of RAM available. - Docker Desktop or Podman (Desktop) on Windows 11 with nested virtualization enabled.
- At least 32 GB of free disk space. - At least 1 GB of available RAM.
- At least 16 GB of free disk space.
> [!NOTE] > [!NOTE]
> Docker Desktop on macOS and Windows 10 do not currently provide the required KVM support for this image. > Docker Desktop on Linux, macOS, and Windows 10 does not currently provide KVM access to containers and is therefore not supported.
## FAQ 💬 ## FAQ 💬
@@ -79,7 +85,7 @@ kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/m
- Wait until DSM finishes its installation - Wait until DSM finishes its installation
- Choose an username and password, and you will be taken to the desktop. - Choose a username and password, and you will be taken to the desktop.
Enjoy your brand new NAS, and don't forget to star this repo! Enjoy your brand new NAS, and don't forget to star this repo!
@@ -143,34 +149,6 @@ kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/m
CPU_CORES: "4" CPU_CORES: "4"
``` ```
### How do I verify if my system supports KVM?
First check if your software is compatible using this chart:
| **Product** | **Linux** | **Win11** | **Win10** | **macOS** |
|---|---|---|---|---|
| Docker CLI | ✅ | ✅ | ❌ | ❌ |
| Docker Desktop | ❌ | ✅ | ❌ | ❌ |
| Podman CLI | ✅ | ✅ | ❌ | ❌ |
| Podman Desktop | ✅ | ✅ | ❌ | ❌ |
After that you can run the following commands in Linux to check your system:
```bash
sudo apt install cpu-checker
sudo kvm-ok
```
If you receive an error from `kvm-ok` indicating that KVM cannot be used, please check whether:
- the virtualization extensions (`Intel VT-x` or `AMD SVM`) are enabled in your BIOS.
- you enabled "nested virtualization" if you are running the container inside a virtual machine.
- you are not using a cloud provider, as most of them do not allow nested virtualization for their VPSs.
If you did not receive any error from `kvm-ok` but the container still complains about a missing KVM device, it could help to add `privileged: true` to your compose file (or `sudo` to your `docker` command) to rule out any permission issue.
### How do I assign an individual IP address to the container? ### How do I assign an individual IP address to the container?
By default, the container uses bridge networking, which shares the IP address with the host. By default, the container uses bridge networking, which shares the IP address with the host.
@@ -223,9 +201,9 @@ kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/m
- 'c *:* rwm' - 'c *:* rwm'
``` ```
### How do I pass through the GPU? ### How do I enable GPU acceleration?
To pass through your Intel GPU, add the following lines to your compose file: To enable GPU acceleration using an Intel or AMD GPU, add the following lines to your compose file:
```yaml ```yaml
environment: environment:
@@ -234,8 +212,30 @@ kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/m
- /dev/dri - /dev/dri
``` ```
> [!NOTE] For NVIDIA GPUs, the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) must be installed on the host and the GPU must be exposed to the container:
> This can be used to enable the facial recognition function in Synology Photos, but does not provide hardware transcoding for video.
```yaml
environment:
GPU: "Y"
NVIDIA_DRIVER_CAPABILITIES: "all"
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities:
- gpu
```
GPU acceleration can be used to enable the facial recognition function in Synology Photos, but currently does not provide hardware transcoding for video yet.
### How do I enable dynamic memory allocation?
By default, the DSM is allocated the full amount of RAM configured via `RAM_SIZE` for its entire lifetime.
However, you can enable [memory ballooning](https://github.com/qemus/qemu/blob/master/docs/ballooning.md) if you want the container to dynamically reclaim unused guest RAM based on host memory pressure.
### How do I install a specific version of vDSM? ### How do I install a specific version of vDSM?
@@ -257,6 +257,41 @@ kubectl apply -f https://raw.githubusercontent.com/vdsm/virtual-dsm/refs/heads/m
Replace the example path `./DSM_VirtualDSM_42218.pat` with the filename of your desired `.pat` file. The value of `URL` will be ignored in this case. Replace the example path `./DSM_VirtualDSM_42218.pat` with the filename of your desired `.pat` file. The value of `URL` will be ignored in this case.
### Are these all available options?
No. For a complete overview of all supported settings, see the [environment variables](docs/environment.md) page.
### How do I verify that KVM is available?
First, make sure your platform and container runtime meet the [requirements](#requirements-) listed above.
On a Linux host, install `cpu-checker` and run:
```bash
sudo apt install cpu-checker
sudo kvm-ok
```
A working configuration should report:
```text
KVM acceleration can be used
```
You can also verify that the KVM device exists:
```bash
ls -l /dev/kvm
```
If KVM is unavailable, check whether:
- Hardware virtualization (`Intel VT-x` or `AMD-V`) is enabled in your BIOS or UEFI.
- Nested virtualization is enabled when the host itself is a virtual machine.
- Your VPS or cloud provider supports nested virtualization.
If `kvm-ok` succeeds but the container still reports that KVM is unavailable, you can temporarily add `privileged: true` to your Compose file to rule out a permission or device-access issue.
### What are the differences compared to the standard DSM? ### What are the differences compared to the standard DSM?
There are only two minor differences: the Virtual Machine Manager package is not available, and Surveillance Station will not include any free licenses. There are only two minor differences: the Virtual Machine Manager package is not available, and Surveillance Station will not include any free licenses.
+16 -5
View File
@@ -1,24 +1,35 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -Eeuo pipefail set -Eeuo pipefail
cd /run
. utils.sh # Load functions
: "${DHCP:="N"}" : "${DHCP:="N"}"
: "${NETWORK:="Y"}" : "${NETWORK:="Y"}"
[ -f "/run/shm/qemu.end" ] && echo "QEMU is shutting down..." && exit 1 [ -f "/run/shm/qemu.end" ] && echo "QEMU is shutting down..." && exit 1
# Treat the startup window as healthy so container health checks do not restart
# the service before QEMU has had time to publish its PID.
[ ! -s "/run/shm/qemu.pid" ] && echo "QEMU is not running yet..." && exit 0 [ ! -s "/run/shm/qemu.pid" ] && echo "QEMU is not running yet..." && exit 0
[[ "$NETWORK" == [Nn]* ]] && echo "Networking is disabled." && exit 0
disabled "$NETWORK" && echo "Networking is disabled." && exit 0
file="/run/shm/dsm.url" file="/run/shm/dsm.url"
address="/run/shm/qemu.ip" address="/run/shm/qemu.host"
gateway="/run/shm/qemu.gw" gateway="/run/shm/qemu.gw"
[ ! -s "$file" ] && echo "DSM has not enabled networking yet..." && exit 1 # dsm.url is written only after the guest agent reports both the DSM
# address and its configured HTTP port.
[ ! -s "$file" ] && echo "DSM has not enabled networking yet..." && exit 0
location=$(<"$file") location=$(<"$file")
if ! curl -m 20 -ILfSs "http://$location/" > /dev/null; then if ! curl -m 20 -LfSs -o /dev/null "http://$location/"; then
if [[ "$DHCP" == [Yy1]* ]]; then # In DHCP mode the firewall must allow the container address; with port
# forwarding it must allow the internal gateway used to reach the guest.
if enabled "$DHCP"; then
ip=$(<"$address") ip=$(<"$address")
echo "Failed to reach DSM at http://$location" echo "Failed to reach DSM at http://$location"
else else
+145 -10
View File
@@ -1,16 +1,151 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -Eeuo pipefail set -Eeuo pipefail
DEF_OPTS="-nodefaults -boot strict=on" : "${RNG:=""}"
RAM_OPTS=$(echo "-m ${RAM_SIZE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g') : "${QMP:=""}"
MON_OPTS="-name $PROCESS,process=$PROCESS,debug-threads=on -pidfile $QEMU_PID" : "${UUID:=""}"
CPU_OPTS="-cpu $CPU_FLAGS -smp $CPU_CORES,sockets=1,dies=1,cores=$CPU_CORES,threads=1" : "${MONITOR:=""}"
MAC_OPTS="-machine type=q35,smm=off,usb=off,vmport=off,dump-guest-core=off,hpet=off${KVM_OPTS}"
DEV_OPTS="-device virtio-balloon-pci,id=balloon0,bus=pcie.0,addr=0x4"
DEV_OPTS+=" -object rng-random,id=objrng0,filename=/dev/urandom"
DEV_OPTS+=" -device virtio-rng-pci,rng=objrng0,id=rng0,bus=pcie.0,addr=0x1c"
ARGS="$DEF_OPTS $CPU_OPTS $RAM_OPTS $MAC_OPTS $DISPLAY_OPTS $MON_OPTS $SERIAL_OPTS $NET_OPTS $DISK_OPTS $DEV_OPTS $ARGUMENTS" msg="Configuring QEMU..."
ARGS=$(echo "$ARGS" | sed 's/\t/ /g' | tr -s ' ') enabled "$DEBUG" && echo "$msg"
# Sanitize variables
QMP=$(strip "$QMP")
UUID=$(strip "$UUID")
MONITOR=$(strip "$MONITOR")
configureProcessor() {
# Expose one thread per core in a single socket; DSM licensing and topology
# reporting are more predictable with this fixed layout.
CPU_OPTS="-cpu $CPU_FLAGS"
CPU_OPTS+=" -smp $CPU_CORES,sockets=1,dies=1,cores=$CPU_CORES,threads=1"
return 0
}
configureMemory() {
RAM_OPTS=$(echo "-m ${RAM_SIZE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
return 0
}
normalizeSocket() {
local value="$1"
local backend="${value%%,*}"
if [[ "$backend" == *.sock && "$backend" != *:* ]]; then
value="unix:$value"
[[ ",$value," == *,server=* ]] || value+=",server=on"
if [[ ",$value," != *,wait=* ]] && [[ ",$value," != *,server=off,* ]]; then
value+=",wait=off"
fi
fi
echo "$value"
}
normalizePort() {
local value="$1"
local protocol="$2"
if [[ "$value" =~ ^[0-9]+$ ]]; then
value="$protocol:0.0.0.0:$value,server=on,wait=off"
fi
echo "$value"
}
configureMonitor() {
MON_OPTS=""
if [ -n "$MONITOR" ]; then
MONITOR=$(normalizePort "$MONITOR" "telnet")
MONITOR=$(normalizeSocket "$MONITOR")
MON_OPTS+=" -monitor $MONITOR"
fi
if [ -n "$QMP" ]; then
QMP=$(normalizePort "$QMP" "tcp")
QMP=$(normalizeSocket "$QMP")
MON_OPTS+=" -qmp $QMP"
fi
ID_OPTS="-name ${APP// /-},process=$PROCESS"
PID_OPTS="-pidfile $QEMU_PID"
MON_OPTS="${MON_OPTS# }"
return 0
}
configureMachine() {
local smm="off"
enabled "${SMM:-}" && smm="on"
# Disable firmware and chipset features that Virtual DSM does not use and
# that can introduce extra devices or timing differences.
MAC_OPTS="-machine type=$MACHINE,smm=$smm,usb=off"
MAC_OPTS+=",vmport=off,dump-guest-core=off,hpet=off${KVM_OPTS}"
[ -n "$UUID" ] && ID_OPTS+=" -uuid $UUID"
[ -n "${SM_BIOS:-}" ] && ID_OPTS+=" $SM_BIOS"
return 0
}
configureDevices() {
local bus
bus=$(getPciBus)
DEV_OPTS=""
if ! disabled "$RNG"; then
DEV_OPTS+=" -object rng-random,id=objrng0,filename=/dev/urandom"
DEV_OPTS+=" -device virtio-rng-pci,rng=objrng0,id=rng0,bus=$bus,addr=0x1c"
fi
# Keep the existing balloon device by default. When dynamic ballooning is
# enabled, expose guest statistics and a dedicated QMP control socket.
if ! enabled "${BALLOONING:-}"; then
DEV_OPTS+=" -device virtio-balloon-pci,id=balloon0,bus=$bus,addr=0x4"
else
MON_OPTS+=" -qmp unix:${BALLOONING_SOCKET},server=on,wait=off"
DEV_OPTS+=" -device virtio-balloon-pci,free-page-reporting=on,guest-stats-polling-interval=1,id=balloon0,bus=$bus,addr=0x4"
fi
DEV_OPTS="${DEV_OPTS# }"
return 0
}
buildArguments() {
local default="-nodefaults"
local boot="-boot strict=on"
ARGS="$default $MAC_OPTS $CPU_OPTS $RAM_OPTS $ID_OPTS $PID_OPTS $DISPLAY_OPTS $MON_OPTS $SERIAL_OPTS $NET_OPTS $DISK_OPTS $boot $BOOT_OPTS $DEV_OPTS $ARGUMENTS"
# Collapse whitespace after optional argument groups are assembled so empty
# features cannot leave malformed spacing in the final QEMU command.
ARGS=$(echo "$ARGS" | sed 's/\t/ /g' | tr -s ' ')
return 0
}
finalizeMemory
configureMemory
configureMonitor
configureMachine
configureProcessor
configureDevices
buildArguments
return 0 return 0
-713
View File
@@ -1,713 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Docker environment variables
: "${DISK_IO:="native"}" # I/O Mode, can be set to 'native', 'threads' or 'io_uring'
: "${DISK_FMT:="raw"}" # Disk file format, 'raw' by default for best performance
: "${DISK_TYPE:=""}" # Device type to be used, "sata", "nvme", "blk" or "scsi"
: "${DISK_FLAGS:=""}" # Specifies the options for use with the qcow2 disk format
: "${DISK_CACHE:="none"}" # Caching mode, can be set to 'writeback' for better performance
: "${DISK_DISCARD:="on"}" # Controls whether unmap (TRIM) commands are passed to the host.
: "${DISK_ROTATION:="1"}" # Rotation rate, set to 1 for SSD storage and increase for HDD
# Sanitize all variables
DISK_IO=$(strip "$DISK_IO")
DISK_FMT=$(strip "$DISK_FMT")
DISK_TYPE=$(strip "$DISK_TYPE")
DISK_FLAGS=$(strip "$DISK_FLAGS")
DISK_CACHE=$(strip "$DISK_CACHE")
DISK_DISCARD=$(strip "$DISK_DISCARD")
DISK_ROTATION=$(strip "$DISK_ROTATION")
BOOT="$STORAGE/$BASE.boot.img"
SYSTEM="$STORAGE/$BASE.system.img"
[ ! -s "$BOOT" ] && error "Virtual DSM boot-image does not exist ($BOOT)" && exit 81
[ ! -s "$SYSTEM" ] && error "Virtual DSM system-image does not exist ($SYSTEM)" && exit 82
if ! setOwner "$BOOT"; then
error "Failed to set the owner for \"$BOOT\" !"
fi
if ! setOwner "$SYSTEM"; then
error "Failed to set the owner for \"$SYSTEM\" !"
fi
fmt2ext() {
local DISK_FMT="$1"
case "${DISK_FMT,,}" in
qcow2)
echo "qcow2"
;;
raw)
echo "img"
;;
*)
error "Unrecognized disk format: $DISK_FMT" && exit 78
;;
esac
}
ext2fmt() {
local DISK_EXT="$1"
case "${DISK_EXT,,}" in
qcow2)
echo "qcow2"
;;
img)
echo "raw"
;;
*)
error "Unrecognized file extension: .$DISK_EXT" && exit 78
;;
esac
}
getSize() {
local DISK_FILE="$1"
local DISK_EXT DISK_FMT
DISK_EXT=$(echo "${DISK_FILE//*./}" | sed 's/^.*\.//')
DISK_FMT=$(ext2fmt "$DISK_EXT")
case "${DISK_FMT,,}" in
raw)
stat -c%s "$DISK_FILE"
;;
qcow2)
qemu-img info "$DISK_FILE" -f "$DISK_FMT" | grep '^virtual size: ' | sed 's/.*(\(.*\) bytes)/\1/'
;;
*)
error "Unrecognized disk format: $DISK_FMT" && exit 78
;;
esac
}
isCow() {
local FS="$1"
if [[ "${FS,,}" == "btrfs" ]]; then
return 0
fi
return 1
}
supportsDirect() {
local FS="$1"
if [[ "${FS,,}" == "ecryptfs" || "${FS,,}" == "tmpfs" ]]; then
return 1
fi
return 0
}
createDisk() {
local DISK_FILE="$1"
local DISK_SPACE="$2"
local DISK_DESC="$3"
local DISK_FMT="$4"
local FS="$5"
local DATA_SIZE DIR SPACE GB FA
rm -f "$DISK_FILE"
DATA_SIZE=$(numfmt --from=iec "$DISK_SPACE")
if ! disabled "$ALLOCATE"; then
# Check free diskspace
DIR=$(dirname "$DISK_FILE")
SPACE=$(df --output=avail -B 1 "$DIR" | tail -n 1)
if (( DATA_SIZE > SPACE )); then
GB=$(formatBytes "$SPACE")
error "Not enough free space to create a $DISK_DESC of ${DISK_SPACE/G/ GB} in $DIR, it has only $GB available..."
error "Please specify a smaller ${DISK_DESC^^}_SIZE or disable preallocation by setting ALLOCATE=N." && exit 76
fi
fi
html "Creating a $DISK_DESC image..."
info "Creating a ${DISK_SPACE/G/ GB} $DISK_STYLE $DISK_DESC image in $DISK_FMT format..."
local FAIL="Could not create a $DISK_STYLE $DISK_FMT $DISK_DESC image of ${DISK_SPACE/G/ GB} ($DISK_FILE)"
case "${DISK_FMT,,}" in
raw)
if isCow "$FS"; then
if ! touch "$DISK_FILE"; then
error "$FAIL" && exit 77
fi
{ chattr +C "$DISK_FILE"; } || :
fi
if disabled "$ALLOCATE"; then
# Create an empty file
if ! truncate -s "$DATA_SIZE" "$DISK_FILE"; then
rm -f "$DISK_FILE"
error "$FAIL" && exit 77
fi
else
# Create an empty file
if ! fallocate -l "$DATA_SIZE" "$DISK_FILE" &>/dev/null; then
if ! fallocate -l -x "$DATA_SIZE" "$DISK_FILE"; then
if ! truncate -s "$DATA_SIZE" "$DISK_FILE"; then
rm -f "$DISK_FILE"
error "$FAIL" && exit 77
fi
fi
fi
fi
;;
qcow2)
local DISK_PARAM="$DISK_ALLOC"
isCow "$FS" && DISK_PARAM+=",nocow=on"
[ -n "$DISK_FLAGS" ] && DISK_PARAM+=",$DISK_FLAGS"
if ! qemu-img create -f "$DISK_FMT" -o "$DISK_PARAM" -- "$DISK_FILE" "$DATA_SIZE" ; then
rm -f "$DISK_FILE"
error "$FAIL" && exit 70
fi
;;
esac
if isCow "$FS"; then
FA=$(lsattr "$DISK_FILE")
if [[ "$FA" != *"C"* ]]; then
error "Failed to disable COW for $DISK_DESC image $DISK_FILE on ${FS^^} filesystem (returned $FA)"
fi
fi
return 0
}
resizeDisk() {
local DISK_FILE="$1"
local DISK_SPACE="$2"
local DISK_DESC="$3"
local DISK_FMT="$4"
local FS="$5"
local CUR_SIZE DATA_SIZE DIR SPACE GB
CUR_SIZE=$(getSize "$DISK_FILE")
DATA_SIZE=$(numfmt --from=iec "$DISK_SPACE")
local REQ=$(( DATA_SIZE - CUR_SIZE ))
(( REQ < 1 )) && error "Shrinking disks is not supported yet, please increase ${DISK_DESC^^}_SIZE." && exit 71
if ! disabled "$ALLOCATE"; then
# Check free diskspace
DIR=$(dirname "$DISK_FILE")
SPACE=$(df --output=avail -B 1 "$DIR" | tail -n 1)
if (( REQ > SPACE )); then
GB=$(formatBytes "$SPACE")
error "Not enough free space to resize $DISK_DESC to ${DISK_SPACE/G/ GB} in $DIR, it has only $GB available.."
error "Please specify a smaller ${DISK_DESC^^}_SIZE or disable preallocation by setting ALLOCATE=N." && exit 74
fi
fi
GB=$(formatBytes "$CUR_SIZE")
MSG="Resizing $DISK_DESC from $GB to ${DISK_SPACE/G/ GB}..."
info "$MSG" && html "$MSG"
local FAIL="Could not resize the $DISK_STYLE $DISK_FMT $DISK_DESC image from ${GB} to ${DISK_SPACE/G/ GB} ($DISK_FILE)"
case "${DISK_FMT,,}" in
raw)
if disabled "$ALLOCATE"; then
# Resize file by changing its length
if ! truncate -s "$DATA_SIZE" "$DISK_FILE"; then
error "$FAIL" && exit 75
fi
else
# Resize file by allocating more space
if ! fallocate -l "$DATA_SIZE" "$DISK_FILE" &>/dev/null; then
if ! fallocate -l -x "$DATA_SIZE" "$DISK_FILE"; then
if ! truncate -s "$DATA_SIZE" "$DISK_FILE"; then
error "$FAIL" && exit 75
fi
fi
fi
fi
;;
qcow2)
if ! qemu-img resize -f "$DISK_FMT" "--$DISK_ALLOC" "$DISK_FILE" "$DATA_SIZE" ; then
error "$FAIL" && exit 72
fi
;;
esac
return 0
}
convertDisk() {
local SOURCE_FILE="$1"
local SOURCE_FMT="$2"
local DST_FILE="$3"
local DST_FMT="$4"
local DISK_BASE="$5"
local DISK_DESC="$6"
local FS="$7"
[ -f "$DST_FILE" ] && error "Conversion failed, destination file $DST_FILE already exists?" && exit 79
[ ! -f "$SOURCE_FILE" ] && error "Conversion failed, source file $SOURCE_FILE does not exist?" && exit 79
local TMP_FILE="$DISK_BASE.tmp"
rm -f "$TMP_FILE"
local DIR FA
DIR=$(dirname "$TMP_FILE")
if ! disabled "$ALLOCATE"; then
local CUR_SIZE SPACE GB
# Check free diskspace
CUR_SIZE=$(getSize "$SOURCE_FILE")
SPACE=$(df --output=avail -B 1 "$DIR" | tail -n 1)
if (( CUR_SIZE > SPACE )); then
GB=$(formatBytes "$SPACE")
error "Not enough free space to convert $DISK_DESC to $DST_FMT in $DIR, it has only $GB available..."
error "Please free up some disk space or disable preallocation by setting ALLOCATE=N." && exit 76
fi
fi
local msg="Converting $DISK_DESC to $DST_FMT"
html "$msg..."
info "$msg, please wait until completed..."
local CONV_FLAGS="-p"
local DISK_PARAM="$DISK_ALLOC"
isCow "$FS" && DISK_PARAM+=",nocow=on"
if [[ "$DST_FMT" != "raw" ]]; then
if disabled "$ALLOCATE"; then
CONV_FLAGS+=" -c"
fi
[ -n "$DISK_FLAGS" ] && DISK_PARAM+=",$DISK_FLAGS"
fi
# shellcheck disable=SC2086
if ! qemu-img convert -f "$SOURCE_FMT" $CONV_FLAGS -o "$DISK_PARAM" -O "$DST_FMT" -- "$SOURCE_FILE" "$TMP_FILE"; then
rm -f "$TMP_FILE"
error "Failed to convert $DISK_STYLE $DISK_DESC image to $DST_FMT format in $DIR, is there enough space available?" && exit 79
fi
if [[ "$DST_FMT" == "raw" ]]; then
if ! disabled "$ALLOCATE"; then
# Work around qemu-img bug
CUR_SIZE=$(stat -c%s "$TMP_FILE")
if ! fallocate -l "$CUR_SIZE" "$TMP_FILE" &>/dev/null; then
if ! fallocate -l -x "$CUR_SIZE" "$TMP_FILE"; then
error "Failed to allocate $CUR_SIZE bytes for $DISK_DESC image $TMP_FILE"
fi
fi
fi
fi
rm -f "$SOURCE_FILE"
mv "$TMP_FILE" "$DST_FILE"
if isCow "$FS"; then
FA=$(lsattr "$DST_FILE")
if [[ "$FA" != *"C"* ]]; then
error "Failed to disable COW for $DISK_DESC image $DST_FILE on ${FS^^} filesystem (returned $FA)"
fi
fi
msg="Conversion of $DISK_DESC"
html "$msg completed..."
info "$msg to $DST_FMT completed successfully!"
return 0
}
checkFS () {
local FS="$1"
local DISK_FILE="$2"
local DISK_DESC="$3"
local DIR FA
DIR=$(dirname "$DISK_FILE")
[ ! -d "$DIR" ] && return 0
if [[ "${FS,,}" == "overlay"* && "${ENGINE,,}" == "docker" ]]; then
warn "the filesystem of $DIR is OverlayFS, this usually means it was binded to an invalid path!"
fi
if [[ "${FS,,}" == "fuse"* ]]; then
warn "the filesystem of $DIR is FUSE, this extra layer will negatively affect performance!"
fi
if ! supportsDirect "$FS"; then
warn "the filesystem of $DIR is $FS, which does not support O_DIRECT mode, adjusting settings..."
fi
if isCow "$FS"; then
if [ -f "$DISK_FILE" ]; then
FA=$(lsattr "$DISK_FILE")
if [[ "$FA" != *"C"* ]]; then
warn "COW (copy on write) is not disabled for $DISK_DESC image file $DISK_FILE, this is recommended on ${FS^^} filesystems!"
fi
fi
fi
return 0
}
createDevice () {
local DISK_FILE="$1"
local DISK_TYPE="$2"
local DISK_INDEX="$3"
local DISK_ADDRESS="$4"
local DISK_FMT="$5"
local DISK_IO="$6"
local DISK_CACHE="$7"
local DISK_SERIAL="$8"
local DISK_SECTORS="$9"
local DISK_ID="data$DISK_INDEX"
local index=""
[ -n "$DISK_INDEX" ] && index=",bootindex=$DISK_INDEX"
local result=" -drive file=$DISK_FILE,id=$DISK_ID,format=$DISK_FMT,cache=$DISK_CACHE,aio=$DISK_IO,discard=$DISK_DISCARD,detect-zeroes=on"
case "${DISK_TYPE,,}" in
"none" ) ;;
"auto" )
echo "$result"
;;
"usb" )
result+=",if=none \
-device usb-storage,drive=${DISK_ID}${index}${DISK_SERIAL}${DISK_SECTORS}"
echo "$result"
;;
"nvme" )
result+=",if=none \
-device nvme,drive=${DISK_ID}${index},serial=deadbeaf${DISK_INDEX}${DISK_SERIAL}${DISK_SECTORS}"
echo "$result"
;;
"ide" | "sata" )
result+=",if=none \
-device ich9-ahci,id=ahci${DISK_INDEX},addr=$DISK_ADDRESS \
-device ide-hd,drive=${DISK_ID},bus=ahci$DISK_INDEX.0,rotation_rate=$DISK_ROTATION${index}${DISK_SERIAL}${DISK_SECTORS}"
echo "$result"
;;
"blk" | "virtio-blk" )
result+=",if=none \
-device virtio-blk-pci,drive=${DISK_ID},bus=pcie.0,addr=$DISK_ADDRESS,iothread=io2${index}${DISK_SERIAL}${DISK_SECTORS}"
echo "$result"
;;
"scsi" | "virtio-scsi" )
result+=",if=none \
-device virtio-scsi-pci,id=${DISK_ID}b,bus=pcie.0,addr=$DISK_ADDRESS,iothread=io2 \
-device scsi-hd,drive=${DISK_ID},bus=${DISK_ID}b.0,channel=0,scsi-id=0,lun=0,rotation_rate=$DISK_ROTATION${index}${DISK_SERIAL}${DISK_SECTORS}"
echo "$result"
;;
esac
return 0
}
finishDisks () {
case "${DISK_TYPE,,}" in
"blk" | "scsi" | "virtio-blk" | "virtio-scsi" )
DISK_OPTS+=" -object iothread,id=io2" ;;
esac
if ! enabled "$DISK_DISABLE"; then
html "Initialized disks successfully..."
fi
return 0
}
addDisk () {
local DISK_BASE="$1"
local DISK_TYPE="$2"
local DISK_DESC="$3"
local DISK_SPACE="$4"
local DISK_INDEX="$5"
local DISK_ADDRESS="$6"
local DISK_FMT="$7"
local DISK_IO="$8"
local DISK_CACHE="$9"
local DISK_EXT DIR SPACE GB DATA_SIZE FS PREV_FMT PREV_EXT CUR_SIZE LEFT FREE USED
DISK_EXT=$(fmt2ext "$DISK_FMT")
local DISK_FILE="$DISK_BASE.$DISK_EXT"
DIR=$(dirname "$DISK_FILE")
[ ! -d "$DIR" ] && return 0
if [[ "${DISK_SPACE,,}" == "max" || "${DISK_SPACE,,}" == "half" ]]; then
local SPARE=1073741824
FREE=$(df --output=avail -B 1 "$DIR" | tail -n 1)
if [[ "${DISK_SPACE,,}" == "max" ]]; then
FREE=$(( FREE - SPARE ))
else
FREE=$(( FREE / 2 ))
fi
(( FREE < SPARE )) && FREE="$SPARE"
GB=$(( FREE / 1073741825 ))
DISK_SPACE="${GB}G"
fi
SPACE="${DISK_SPACE// /}"
[ -z "$SPACE" ] && SPACE="256G"
[ -z "${SPACE//[0-9. ]}" ] && SPACE="${SPACE}G"
SPACE=$(echo "${SPACE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
if ! numfmt --from=iec "$SPACE" &>/dev/null; then
error "Invalid value for ${DISK_DESC^^}_SIZE: $DISK_SPACE" && exit 73
fi
DATA_SIZE=$(numfmt --from=iec "$SPACE")
if (( DATA_SIZE < 6442450944 )); then
error "Please increase the ${DISK_DESC^^}_SIZE variable to at least 6 GB." && exit 73
fi
FS=$(stat -f -c %T "$DIR")
checkFS "$FS" "$DISK_FILE" "$DISK_DESC" || exit $?
if ! supportsDirect "$FS"; then
DISK_IO="threads"
DISK_CACHE="writeback"
fi
if [ ! -s "$DISK_FILE" ] ; then
if [[ "${DISK_FMT,,}" != "raw" ]]; then
PREV_FMT="raw"
else
PREV_FMT="qcow2"
fi
PREV_EXT=$(fmt2ext "$PREV_FMT")
if [ -s "$DISK_BASE.$PREV_EXT" ] ; then
convertDisk "$DISK_BASE.$PREV_EXT" "$PREV_FMT" "$DISK_FILE" "$DISK_FMT" "$DISK_BASE" "$DISK_DESC" "$FS" || exit $?
fi
fi
if [ -s "$DISK_FILE" ]; then
CUR_SIZE=$(getSize "$DISK_FILE")
if (( DATA_SIZE > CUR_SIZE )); then
resizeDisk "$DISK_FILE" "$SPACE" "$DISK_DESC" "$DISK_FMT" "$FS" || exit $?
else
if (( DATA_SIZE < CUR_SIZE )); then
if [[ "${DISK_SPACE,,}" != "max" && "${DISK_SPACE,,}" != "half" ]]; then
info "You decreased the ${DISK_DESC^^}_SIZE variable to ${DISK_SPACE/G/ GB} but shrinking disks is not supported, will be ignored..."
fi
fi
fi
else
createDisk "$DISK_FILE" "$SPACE" "$DISK_DESC" "$DISK_FMT" "$FS" || exit $?
fi
if [ -f "$DISK_FILE" ] && disabled "$ALLOCATE"; then
CUR_SIZE=$(getSize "$DISK_FILE")
USED=$(du -sB 1 "$DISK_FILE" | cut -f1)
FREE=$(df --output=avail -B 1 "$DIR" | tail -n 1)
LEFT=$(( CUR_SIZE - USED - FREE ))
if (( LEFT > 0 )); then
GB=$(formatBytes "$FREE")
LEFT=$(formatBytes "$LEFT")
CUR_SIZE=$(formatBytes "$CUR_SIZE")
msg="The virtual size of the ${DISK_DESC,,} is $CUR_SIZE"
if [ -n "$USED" ] && [[ "$USED" != "0" ]]; then
USED=$(formatBytes "$USED")
msg+=" (of which $USED is used)"
fi
info "$msg, but there is only $GB of free space remaining in $DIR now."
info "Please consider making at least $LEFT more space available in $DIR for future expansions."
fi
fi
if [ -f "$DISK_FILE" ]; then
if ! setOwner "$DISK_FILE"; then
error "Failed to set the owner for \"$DISK_FILE\" !"
fi
fi
DISK_OPTS+=$(createDevice "$DISK_FILE" "$DISK_TYPE" "$DISK_INDEX" "$DISK_ADDRESS" "$DISK_FMT" "$DISK_IO" "$DISK_CACHE" "" "")
return 0
}
addDevice () {
local DISK_DEV="$1"
local DISK_TYPE="$2"
local DISK_INDEX="$3"
local DISK_ADDRESS="$4"
[ -z "$DISK_DEV" ] && return 0
[ ! -b "$DISK_DEV" ] && error "Device $DISK_DEV cannot be found! Please add it to the 'devices' section of your compose file." && exit 55
local sectors=""
local dev_type=""
dev_type=$(lsblk -no TYPE "$DISK_DEV" 2>/dev/null | head -n1)
# Only detect and apply sector sizes for partitions, not whole disks
# Whole disk passthrough with explicit sector sizes causes DSM not to recognize the disk
if [[ "$dev_type" == "part" ]]; then
local result logical physical
result=$(fdisk -l "$DISK_DEV" | grep -m 1 -o "(logical/physical): .*" | cut -c 21-)
logical="${result%% *}"
physical=$(echo "$result" | grep -m 1 -o "/ .*" | cut -c 3-)
physical="${physical%% *}"
if [ -n "$physical" ]; then
if [[ "$physical" != "512" ]]; then
sectors=",logical_block_size=$logical,physical_block_size=$physical"
fi
else
warn "Failed to determine the sector size for $DISK_DEV"
fi
fi
DISK_OPTS+=$(createDevice "$DISK_DEV" "$DISK_TYPE" "$DISK_INDEX" "$DISK_ADDRESS" "raw" "$DISK_IO" "$DISK_CACHE" "" "$sectors")
return 0
}
[ -z "${DISK_OPTS:-}" ] && DISK_OPTS=""
[ -z "${DISK_TYPE:-}" ] && DISK_TYPE="scsi"
[ -z "${DISK_NAME:-}" ] && DISK_NAME="data"
[ -z "${DISK_DISABLE:-}" ] && DISK_DISABLE=""
if ! enabled "$DISK_DISABLE"; then
msg="Initializing disks..."
html "$msg"
enabled "$DEBUG" && echo "$msg"
fi
case "${DISK_TYPE,,}" in
"ide" | "sata" | "nvme" | "usb" | "scsi" | "blk" | "auto" | "none" ) ;;
* ) error "Invalid DISK_TYPE specified, value \"$DISK_TYPE\" is not recognized!" && exit 80 ;;
esac
if [ -z "$ALLOCATE" ]; then
ALLOCATE="N"
fi
if disabled "$ALLOCATE"; then
DISK_STYLE="growable"
DISK_ALLOC="preallocation=off"
else
DISK_STYLE="preallocated"
DISK_ALLOC="preallocation=falloc"
fi
DISK_OPTS+=$(createDevice "$BOOT" "$DISK_TYPE" "1" "0xa" "raw" "$DISK_IO" "$DISK_CACHE" "" "")
DISK_OPTS+=$(createDevice "$SYSTEM" "$DISK_TYPE" "2" "0xb" "raw" "$DISK_IO" "$DISK_CACHE" "" "")
if enabled "$DISK_DISABLE"; then
finishDisks
return 0
fi
DISK1_FILE="$STORAGE/${DISK_NAME}"
DISK2_FILE="/storage2/${DISK_NAME}2"
DISK3_FILE="/storage3/${DISK_NAME}3"
DISK4_FILE="/storage4/${DISK_NAME}4"
: "${DISK2_SIZE:=""}"
: "${DISK3_SIZE:=""}"
: "${DISK4_SIZE:=""}"
: "${DEVICE:=""}" # Docker variables to passthrough a block device, like /dev/vdc1.
: "${DEVICE2:=""}"
: "${DEVICE3:=""}"
: "${DEVICE4:=""}"
[ -z "$DEVICE" ] && [ -b "/disk" ] && DEVICE="/disk"
[ -z "$DEVICE" ] && [ -b "/disk1" ] && DEVICE="/disk1"
[ -z "$DEVICE2" ] && [ -b "/disk2" ] && DEVICE2="/disk2"
[ -z "$DEVICE3" ] && [ -b "/disk3" ] && DEVICE3="/disk3"
[ -z "$DEVICE4" ] && [ -b "/disk4" ] && DEVICE4="/disk4"
[ -z "$DEVICE" ] && [ -b "/dev/disk1" ] && DEVICE="/dev/disk1"
[ -z "$DEVICE2" ] && [ -b "/dev/disk2" ] && DEVICE2="/dev/disk2"
[ -z "$DEVICE3" ] && [ -b "/dev/disk3" ] && DEVICE3="/dev/disk3"
[ -z "$DEVICE4" ] && [ -b "/dev/disk4" ] && DEVICE4="/dev/disk4"
if [ -n "$DEVICE" ]; then
addDevice "$DEVICE" "$DISK_TYPE" "3" "0xc" || exit $?
else
addDisk "$DISK1_FILE" "$DISK_TYPE" "disk" "$DISK_SIZE" "3" "0xc" "$DISK_FMT" "$DISK_IO" "$DISK_CACHE" || exit $?
fi
if [ -n "$DEVICE2" ]; then
addDevice "$DEVICE2" "$DISK_TYPE" "4" "0xd" || exit $?
else
addDisk "$DISK2_FILE" "$DISK_TYPE" "disk2" "$DISK2_SIZE" "4" "0xd" "$DISK_FMT" "$DISK_IO" "$DISK_CACHE" || exit $?
fi
if [ -n "$DEVICE3" ]; then
addDevice "$DEVICE3" "$DISK_TYPE" "5" "0xe" || exit $?
else
addDisk "$DISK3_FILE" "$DISK_TYPE" "disk3" "$DISK3_SIZE" "5" "0xe" "$DISK_FMT" "$DISK_IO" "$DISK_CACHE" || exit $?
fi
if [ -n "$DEVICE4" ]; then
addDevice "$DEVICE4" "$DISK_TYPE" "6" "0xf" || exit $?
else
addDisk "$DISK4_FILE" "$DISK_TYPE" "disk4" "$DISK4_SIZE" "6" "0xf" "$DISK_FMT" "$DISK_IO" "$DISK_CACHE" || exit $?
fi
finishDisks
return 0
-54
View File
@@ -1,54 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Docker environment variables
: "${GPU:="N"}" # GPU passthrough
: "${VGA:="virtio"}" # VGA adaptor
: "${DISPLAY:="none"}" # Display type
: "${RENDERNODE:="/dev/dri/renderD128"}" # Render node
# Sanitize variables
VGA=$(strip "$VGA")
DISPLAY=$(strip "$DISPLAY")
RENDERNODE=$(strip "$RENDERNODE")
CPU_VENDOR=$(lscpu | awk '/Vendor ID/{print $3}')
if ! enabled "$GPU" || [[ "$CPU_VENDOR" != "GenuineIntel" || "$ARCH" != "amd64" ]]; then
[[ "${DISPLAY,,}" == "none" ]] && VGA="none"
DISPLAY_OPTS="-display $DISPLAY -vga $VGA"
return 0
fi
msg="Configuring display drivers..."
html "$msg"
enabled "$DEBUG" && echo "$msg"
DISPLAY_OPTS="-display egl-headless,rendernode=$RENDERNODE"
DISPLAY_OPTS+=" -vga $VGA"
[ ! -d /dev/dri ] && mkdir -m 755 /dev/dri
# Extract the card number from the render node
CARD_NUMBER=$(echo "$RENDERNODE" | grep -oP '(?<=renderD)\d+')
CARD_DEVICE="/dev/dri/card$((CARD_NUMBER - 128))"
if [ ! -c "$CARD_DEVICE" ]; then
if mknod "$CARD_DEVICE" c 226 $((CARD_NUMBER - 128)); then
chmod 666 "$CARD_DEVICE"
fi
fi
if [ ! -c "$RENDERNODE" ]; then
if mknod "$RENDERNODE" c 226 "$CARD_NUMBER"; then
chmod 666 "$RENDERNODE"
fi
fi
addPackage "xserver-xorg-video-intel" "Intel GPU drivers"
addPackage "qemu-system-modules-opengl" "OpenGL module"
return 0
+24 -6
View File
@@ -5,20 +5,35 @@ set -Eeuo pipefail
: "${APP:="Virtual DSM"}" : "${APP:="Virtual DSM"}"
: "${SUPPORT:="https://github.com/vdsm/virtual-dsm"}" : "${SUPPORT:="https://github.com/vdsm/virtual-dsm"}"
: "${USB:="N"}"
: "${AUDIO:="N"}"
: "${VGA:="none"}"
: "${DISPLAY:="none"}"
: "${WEB_PORT:="5000"}"
: "${DISK_OFFSET:="2"}"
: "${DISK_SIZE:="256G"}"
: "${RAM_MINIMUM:="1G"}"
: "${DISK_MINIMUM:="6G"}"
: "${BOOT_MODE:="legacy"}"
cd /run cd /run
. start.sh # Startup hook . start.sh # Startup hook
. utils.sh # Load functions . utils.sh # Load functions
. reset.sh # Initialize system . init.sh # Initialize system
. memory.sh # Check memory
. server.sh # Start webserver . server.sh # Start webserver
. download.sh # Load functions
. install.sh # Run installation . install.sh # Run installation
. disk.sh # Initialize disks . disk.sh # Initialize disks
. display.sh # Initialize graphics . display.sh # Initialize graphics
. prepare.sh # Prepare for launch
. network.sh # Initialize network . network.sh # Initialize network
. boot.sh # Configure boot
. proc.sh # Initialize processor . proc.sh # Initialize processor
. serial.sh # Initialize serialport . serial.sh # Initialize serialport
. power.sh # Configure shutdown . power.sh # Configure shutdown
. memory.sh # Check available memory . balloon.sh # Initialize ballooning
. config.sh # Configure arguments . config.sh # Configure arguments
. finish.sh # Finish initialization . finish.sh # Finish initialization
@@ -26,20 +41,23 @@ trap - ERR
cmd=(qemu-system-x86_64) cmd=(qemu-system-x86_64)
version=$("${cmd[@]}" --version | awk 'NR==1 { print $4 }') version=$("${cmd[@]}" --version | awk 'NR==1 { print $4 }')
info "Booting $APP using QEMU v$version..." info "Booting $APP using QEMU v$version..." && echo
if ! enabled "$SHUTDOWN"; then if ! enabled "$SHUTDOWN"; then
exec "${cmd[@]}" ${ARGS:+ $ARGS} exec "${cmd[@]}" ${ARGS:+ $ARGS}
fi fi
if [ ! -t 1 ] || [ ! -c /dev/tty ]; then if ! interactive; then
"${cmd[@]}" ${ARGS:+ $ARGS} & "${cmd[@]}" ${ARGS:+ $ARGS} &
else else
"${cmd[@]}" ${ARGS:+ $ARGS} </dev/tty >/dev/tty & startConsole
startQemu "${cmd[@]}" ${ARGS:+ $ARGS}
fi fi
pid=$!
rc=0 rc=0
wait $! || rc=$?
wait "$pid" || rc=$?
[ -f "$QEMU_END" ] && exit "$rc" [ -f "$QEMU_END" ] && exit "$rc"
sleep 1 & wait $! sleep 1 & wait $!
+12 -2
View File
@@ -1,8 +1,18 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -Eeuo pipefail set -Eeuo pipefail
if enabled "$DEBUG"; then fKill "progress.sh"
printf "QEMU arguments:\n\n%s\n\n" "${ARGS// -/$'\n-'}"
if [ -s "$QEMU_DIR/qemu.host" ] && [[ "$(<"$QEMU_DIR/qemu.host")" == "172.17."* ]]; then
warn "your container IP starts with 172.17.* which will cause conflicts when you install the Container Manager package inside DSM!"
fi fi
if enabled "$DEBUG"; then
echo
printf "QEMU arguments:\n\n %s\n\n" "${ARGS// -/$'\n -'}"
fi
# Must always remain the very last command
enableTrap
return 0 return 0
+585 -243
View File
@@ -1,254 +1,260 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -Eeuo pipefail set -Eeuo pipefail
: "${TZ:=""}" # System timezone
: "${COUNTRY:=""}" # Country code for mirror
: "${URL:=""}" # URL of the PAT file to be downloaded. : "${URL:=""}" # URL of the PAT file to be downloaded.
if [ -f "$STORAGE/dsm.ver" ]; then TZ=$(strip "$TZ")
BASE=$(<"$STORAGE/dsm.ver") COUNTRY=$(strip "$COUNTRY")
BASE="${BASE//[![:print:]]/}"
[ -z "$BASE" ] && BASE="DSM_VirtualDSM_69057"
else
# Fallback for old installs
BASE="DSM_VirtualDSM_42962"
fi
FN="boot.pat" checkDsmFilesystem() {
DIR=$(find / -maxdepth 1 -type d -iname "$FN" -print -quit)
[ ! -d "$DIR" ] && DIR=$(find "$STORAGE" -maxdepth 1 -type d -iname "$FN" -print -quit)
if [ -d "$DIR" ]; then local fs="$1"
BASE="DSM_VirtualDSM" && URL="file://$DIR"
if [[ ! -s "$STORAGE/$BASE.boot.img" || ! -s "$STORAGE/$BASE.system.img" ]]; then
error "The bind $DIR maps to a file that does not exist!" && exit 65
fi
fi
FILE=$(find / -maxdepth 1 -type f -iname "$FN" -print -quit) if [[ "${fs,,}" == "overlay"* && "${ENGINE,,}" == "docker" ]]; then
[ ! -s "$FILE" ] && FILE=$(find "$STORAGE" -maxdepth 1 -type f -iname "$FN" -print -quit)
[ -s "$FILE" ] && BASE="DSM_VirtualDSM" && URL="file://$FILE"
URL=$(strip "$URL")
if [ -n "$URL" ] && [ ! -s "$FILE" ] && [ ! -d "$DIR" ]; then
BASE=$(basename "$URL" .pat)
if [ ! -s "$STORAGE/$BASE.system.img" ]; then
BASE=$(basename "${URL%%\?*}" .pat)
printf -v BASE '%b' "${BASE//%/\\x}"
BASE="${BASE//[!A-Za-z0-9._-]/_}"
fi
if [[ "${URL,,}" != "http"* && "${URL,,}" != "file:"* ]] ; then
[ ! -s "$STORAGE/$BASE.pat" ] && error "Invalid URL: $URL" && exit 65
URL="file://$STORAGE/$BASE.pat"
fi
fi
if [[ -s "$STORAGE/$BASE.boot.img" && -s "$STORAGE/$BASE.system.img" ]]; then
return 0 # Previous installation found
fi
html "Please wait while Virtual DSM is being installed..."
DL=""
DL_CHINA="https://cndl.synology.cn/download/DSM"
DL_GLOBAL="https://global.synologydownload.com/download/DSM"
[[ "${URL,,}" == *"cndl.synology"* ]] && DL="$DL_CHINA"
[[ "${URL,,}" == *"global.synology"* ]] && DL="$DL_GLOBAL"
if [ -z "$DL" ]; then
[ -z "$COUNTRY" ] && setCountry
[ -z "$COUNTRY" ] && info "Warning: could not detect country to select mirror!"
[[ "${COUNTRY^^}" == "CN" ]] && DL="$DL_CHINA" || DL="$DL_GLOBAL"
fi
if [ -z "$URL" ]; then
URL="$DL/release/7.2.2/72806/DSM_VirtualDSM_72806.pat"
fi
if [ ! -s "$FILE" ]; then
BASE=$(basename "${URL%%\?*}" .pat)
printf -v BASE '%b' "${BASE//%/\\x}"
BASE="${BASE//[!A-Za-z0-9._-]/_}"
fi
if [[ "$URL" != "file://$STORAGE/$BASE.pat" ]]; then
rm -f "$STORAGE/$BASE.pat"
fi
rm -f "$STORAGE/$BASE.agent"
rm -f "$STORAGE/$BASE.boot.img"
rm -f "$STORAGE/$BASE.system.img"
# Check filesystem
FS=$(stat -f -c %T "$STORAGE")
if [[ "${FS,,}" == "overlay"* && "${ENGINE,,}" == "docker" ]]; then
warn "the filesystem of $STORAGE is OverlayFS, this usually means it was binded to an invalid path!" warn "the filesystem of $STORAGE is OverlayFS, this usually means it was binded to an invalid path!"
fi fi
if [[ "${FS,,}" == "fuse"* ]]; then if [[ "${fs,,}" == "fuse"* ]]; then
warn "the filesystem of $STORAGE is FUSE, this extra layer will negatively affect performance!" warn "the filesystem of $STORAGE is FUSE, this extra layer will negatively affect performance!"
fi
if [[ "${FS,,}" == "ecryptfs" || "${FS,,}" == "tmpfs" ]]; then
warn "the filesystem of $STORAGE is $FS, which does not support O_DIRECT mode, adjusting settings..."
fi
if [[ "${FS,,}" == "fat"* || "${FS,,}" == "vfat"* || "${FS,,}" == "msdos"* ]]; then
error "Unable to install on $FS filesystems, please use a different filesystem for /storage." && exit 61
fi
if [[ "${FS,,}" != "exfat"* && "${FS,,}" != "ntfs"* && "${FS,,}" != "unknown"* ]]; then
TMP="$STORAGE/tmp"
rm -rf "$TMP"
if ! makeDir "$TMP"; then
error "Failed to create directory \"$TMP\" !" && exit 93
fi fi
else
TMP="/tmp/dsm" if [[ "${fs,,}" == "ecryptfs" || "${fs,,}" == "tmpfs" ]]; then
TMP_SPACE=2147483648 warn "the filesystem of $STORAGE is $fs, which does not support O_DIRECT mode, adjusting settings..."
SPACE=$(df --output=avail -B 1 /tmp | tail -n 1)
SPACE_MB=$(formatBytes "$SPACE")
if (( TMP_SPACE > SPACE )); then
error "Not enough free space inside the container, have $SPACE_MB available but need at least 2 GB." && exit 93
fi fi
rm -rf "$TMP" && mkdir -p "$TMP"
fi
# Check free diskspace if [[ "${fs,,}" == "fat"* || "${fs,,}" == "vfat"* || "${fs,,}" == "msdos"* ]]; then
ROOT_SPACE=536870912 error "Unable to install on $fs filesystems, please use a different filesystem for /storage."
SPACE=$(df --output=avail -B 1 / | tail -n 1) return 61
SPACE_MB=$(formatBytes "$SPACE" "down") fi
(( ROOT_SPACE > SPACE )) && error "Not enough free space inside the container, have $SPACE_MB available but need at least 500 MB." && exit 96
MIN_SPACE=15032385536 return 0
SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1) }
SPACE_GB=$(formatBytes "$SPACE")
(( MIN_SPACE > SPACE )) && error "Not enough free space for installation in $STORAGE, have $SPACE_GB available but need at least 14 GB." && exit 94
# Check if output is to interactive TTY checkDsmSpace() {
if [ -t 1 ]; then
PROGRESS="--progress=bar:noscroll"
else
PROGRESS="--progress=dot:giga"
fi
if [[ "$URL" == "file://"* ]]; then local rootSpace=536870912
MSG="Copying DSM" local minSpace=15032385536
ERR="Failed to copy ${URL:7}" local space available
space=$(df --output=avail -B 1 / | tail -n 1) || return $?
available=$(formatBytes "$space" "down") || return $?
if (( rootSpace > space )); then
error "Not enough free space inside the container, have $available available but need at least 500 MB."
return 96
fi
space=$(df --output=avail -B 1 "$STORAGE" | tail -n 1) || return $?
available=$(formatBytes "$space") || return $?
if (( minSpace > space )); then
error "Not enough free space for installation in $STORAGE, have $available available but need at least 14 GB."
return 94
fi
return 0
}
downloadFile() {
local url="$1"
local pat="$2"
local msg="$3"
local connections="${4:-1}"
downloadToFile \
"$url" \
"$pat" \
"$msg" \
"0" \
"$connections" \
"Y"
}
downloadPat() {
local pat="$1"
local msg err
if [[ "$URL" == "file://"* ]]; then
msg="Copying DSM"
err="Failed to copy ${URL:7}"
info "Install: Copying installation image..." info "Install: Copying installation image..."
else html "$msg..." || return $?
MSG="Downloading DSM"
ERR="Failed to download $URL"
info "Install: Downloading $BASE.pat..."
fi
html "$MSG..." rm -f "$pat" || return $?
PAT="/$BASE.pat"
rm -f "$PAT"
if [[ "$URL" == "file://"* ]]; then
if [ ! -f "${URL:7}" ]; then if [ ! -f "${URL:7}" ]; then
error "File '${URL:7}' does not exist!" && exit 65 error "File '${URL:7}' does not exist!"
return 65
fi fi
cp "${URL:7}" "$PAT" cp "${URL:7}" "$pat" || return $?
else else
SIZE=0 msg="Downloading DSM"
[[ "${URL,,}" == *"_72806.pat" ]] && SIZE=361010261 err="Failed to download $URL"
[[ "${URL,,}" == *"_69057.pat" ]] && SIZE=363837333 info "Install: Downloading $BASE.pat..."
[[ "${URL,,}" == *"_42218.pat" ]] && SIZE=379637760
/run/progress.sh "$PAT" "$SIZE" "$MSG ([P])..." & downloadRetry \
"$pat" \
"$CONNECTIONS" \
"5" \
"$BASE.pat" \
"0" \
"$URL" \
"$pat" \
"$msg" || return 69
fi
{ wget "$URL" -O "$PAT" -q --no-check-certificate --timeout=10 --no-http-keep-alive --show-progress "$PROGRESS"; rc=$?; } || : if [ ! -s "$pat" ]; then
error "$err"
return 69
fi
return 0
}
extractPat() {
local pat="$1"
local tmp="$2"
local size="$3"
local msg="Extracting installation image..."
local rc
info "Install: $msg" && html "$msg" || return $?
/run/progress.sh "$tmp" "$size" "$msg ([P])..." &
# Newer PAT files are normal tar archives; older encrypted/proprietary forms
# require the bundled extractor as a compatibility fallback.
if { tar tf "$pat"; } >/dev/null 2>&1; then
tar xpf "$pat" -C "$tmp/." || {
rc=$?
fKill "progress.sh" fKill "progress.sh"
return "$rc"
}
(( rc == 3 )) && error "$ERR , cannot write file (disk full?)" && exit 69 else
(( rc == 4 )) && error "$ERR , network failure!" && exit 69
(( rc == 8 )) && error "$ERR , server issued an error response!" && exit 69
(( rc != 0 )) && error "$ERR , reason: $rc" && exit 69
fi { (cd "$tmp" && python3 /run/extract.py -i "$pat" -d 2>/run/extract.log); rc=$?; } || :
[ ! -s "$PAT" ] && error "$ERR" && exit 69
SIZE=$(stat -c%s "$PAT")
if ((SIZE<250000000)); then
error "The specified PAT file is probably an update pack as it's too small." && exit 62
fi
MSG="Extracting installation image..."
info "Install: $MSG" && html "$MSG"
if { tar tf "$PAT"; } >/dev/null 2>&1; then
tar xpf "$PAT" -C "$TMP/."
else
{ (cd "$TMP" && python3 /run/extract.py -i "$PAT" -d 2>/run/extract.log); rc=$?; } || :
if (( rc != 0 )); then if (( rc != 0 )); then
fKill "progress.sh"
cat /run/extract.log cat /run/extract.log
error "Failed to extract PAT file, reason $rc" && exit 63 error "Failed to extract PAT file, reason $rc"
return 63
fi fi
fi fi
MSG="Preparing system partition..." fKill "progress.sh"
info "Install: $MSG" && html "$MSG" return 0
}
BOOT=$(find "$TMP" -name "*.bin.zip") createSystemImage() {
[ ! -s "$BOOT" ] && error "The PAT file contains no boot image." && exit 67
BOOT=$(echo "$BOOT" | head -c -5) local tmp="$1"
unzip -q -o "$BOOT".zip -d "$TMP" local fs="$2"
local msg="Preparing system partition..."
SYSTEM="$STORAGE/$BASE.system.img" info "Install: $msg" && html "$msg" || return $?
rm -f "$SYSTEM"
# Check free diskspace # The PAT boot archive becomes the persistent QEMU boot disk after its
SYSTEM_SIZE=10738466816 # companion system partition has been assembled.
SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1) DSM_BOOT=$(find "$tmp" -name "*.bin.zip" -print -quit) || return $?
SPACE_MB=$(formatBytes "$SPACE")
if (( SYSTEM_SIZE > SPACE )); then if [ -z "$DSM_BOOT" ]; then
error "Not enough free space in $STORAGE to create a 10 GB system disk, have only $SPACE_MB available." && exit 97 error "The PAT file contains no boot image."
fi return 67
fi
if ! touch "$SYSTEM"; then if [ ! -s "$DSM_BOOT" ]; then
error "Could not create file $SYSTEM for the system disk." && exit 98 error "The PAT boot image archive is empty."
fi return 67
fi
! setOwner "$SYSTEM" && error "Failed to set the owner for \"$SYSTEM\" !" local bootTmp="$tmp/boot" bootBase
bootBase="$(basename "${DSM_BOOT%.zip}")"
if [[ "${FS,,}" == "btrfs" ]]; then rm -rf "$bootTmp" || return $?
if ! makeDir "$bootTmp"; then
error "Failed to create directory \"$bootTmp\" !"
return 93
fi
if [[ "${fs,,}" == "btrfs" ]]; then
{ chattr +C "$bootTmp"; } || :
fi
7z x -y -o"$bootTmp" "$DSM_BOOT" >/dev/null || return $?
DSM_BOOT="$bootTmp/$bootBase"
if [ ! -s "$DSM_BOOT" ]; then
error "The PAT boot image could not be extracted."
return 67
fi
if [[ "${fs,,}" == "btrfs" ]]; then
local attrs
attrs=$(lsattr "$DSM_BOOT") || return $?
if [[ "$attrs" != *"C"* ]]; then
error "Failed to disable COW for boot image $DSM_BOOT on ${fs^^} filesystem."
fi
fi
SYSTEM="$STORAGE/$BASE.system.img"
rm -f "$SYSTEM" || return $?
# Check free diskspace
local systemSize=10738466816
local space available
space=$(df --output=avail -B 1 "$STORAGE" | tail -n 1) || return $?
available=$(formatBytes "$space") || return $?
if (( systemSize > space )); then
error "Not enough free space in $STORAGE to create a 10 GB system disk, have only $available available."
return 97
fi
if ! touch "$SYSTEM"; then
error "Could not create file $SYSTEM for the system disk."
return 98
fi
setOwner "$SYSTEM" || warn "failed to set the owner for \"$SYSTEM\" !"
if [[ "${fs,,}" == "btrfs" ]]; then
{ chattr +C "$SYSTEM"; } || : { chattr +C "$SYSTEM"; } || :
FA=$(lsattr "$SYSTEM") local attrs
if [[ "$FA" != *"C"* ]]; then attrs=$(lsattr "$SYSTEM") || return $?
error "Failed to disable COW for system image $SYSTEM on ${FS^^} filesystem." if [[ "$attrs" != *"C"* ]]; then
error "Failed to disable COW for system image $SYSTEM on ${fs^^} filesystem."
fi
fi fi
fi
if ! fallocate -l "$SYSTEM_SIZE" "$SYSTEM" &>/dev/null; then if ! fallocate -l "$systemSize" "$SYSTEM" &>/dev/null; then
if ! fallocate -l -x "$SYSTEM_SIZE" "$SYSTEM"; then if ! fallocate -l -x "$systemSize" "$SYSTEM"; then
if ! truncate -s "$SYSTEM_SIZE" "$SYSTEM"; then if ! truncate -s "$systemSize" "$SYSTEM"; then
rm -f "$SYSTEM" rm -f "$SYSTEM"
error "Could not allocate file $SYSTEM for the system disk." && exit 98 error "Could not allocate file $SYSTEM for the system disk."
return 98
fi
fi fi
fi fi
fi
PART="$TMP/partition.fdisk" # Recreate Synology's expected DOS partition layout inside the fixed 10 GiB
# system image before populating the ext4 root partition.
local part="$tmp/partition.fdisk"
{ echo "label: dos" if ! {
echo "label: dos"
echo "label-id: 0x6f9ee2e9" echo "label-id: 0x6f9ee2e9"
echo "device: $SYSTEM" echo "device: $SYSTEM"
echo "unit: sectors" echo "unit: sectors"
@@ -256,65 +262,401 @@ PART="$TMP/partition.fdisk"
echo "" echo ""
echo "${SYSTEM}1 : start= 2048, size= 16777216, type=83" echo "${SYSTEM}1 : start= 2048, size= 16777216, type=83"
echo "${SYSTEM}2 : start= 16779264, size= 4194304, type=82" echo "${SYSTEM}2 : start= 16779264, size= 4194304, type=82"
} > "$PART" } > "$part"; then
return 1
fi
sfdisk -q "$SYSTEM" < "$PART" sfdisk -q "$SYSTEM" < "$part" || return $?
MOUNT="$TMP/system" local mount="$tmp/system"
rm -rf "$MOUNT" rm -rf "$mount" || return $?
if ! makeDir "$MOUNT"; then if ! makeDir "$mount"; then
error "Failed to create directory \"$MOUNT\" !" && exit 93 error "Failed to create directory \"$mount\" !"
fi return 93
fi
MSG="Extracting system partition..." return 0
info "Install: $MSG" && html "$MSG" }
HDA="$TMP/hda1" installSystemPartition() {
IDB="$TMP/indexdb"
PKG="$TMP/packages"
HDP="$TMP/synohdpack_img"
[ ! -s "$HDA.tgz" ] && error "The PAT file contains no OS image." && exit 64 local tmp="$1"
mv "$HDA.tgz" "$HDA.txz" local mount="$tmp/system"
local msg="Extracting system partition..."
[ -d "$PKG" ] && mv "$PKG/" "$MOUNT/.SynoUpgradePackages/" info "Install: $msg" && html "$msg" || return $?
rm -f "$MOUNT/.SynoUpgradePackages/ActiveInsight-"*
if [ -s "$IDB.txz" ]; then local hda="$tmp/hda1"
INDEX_DB="$MOUNT/usr/syno/synoman/indexdb" local idb="$tmp/indexdb"
mkdir -p "$INDEX_DB" local pkg="$tmp/packages"
fi local hdp="$tmp/synohdpack_img"
LABEL="1.44.1-42218" if [ ! -s "$hda.tgz" ]; then
OFFSET="1048576" # 2048 * 512 error "The PAT file contains no OS image."
NUMBLOCKS="2097152" # (16777216 * 512) / 4096 return 64
MSG="Installing system partition..." fi
fakeroot -- bash -c "set -Eeu;\ mv "$hda.tgz" "$hda.txz" || return $?
[ -s $HDP.txz ] && tar xpfJ $HDP.txz --absolute-names -C $MOUNT/;\
[ -s $IDB.txz ] && tar xpfJ $IDB.txz --absolute-names -C $INDEX_DB/;\
tar xpfJ $HDA.txz --absolute-names --skip-old-files -C $MOUNT/;\
printf '%b%s%b' '\E[1;34m \E[1;36m' 'Install: $MSG' '\E[0m\n';\
mke2fs -q -t ext4 -b 4096 -d $MOUNT/ -L $LABEL -F -E offset=$OFFSET $SYSTEM $NUMBLOCKS"
rm -rf "$MOUNT" if [ -d "$pkg" ]; then
echo "$BASE" > "$STORAGE/dsm.ver" mv "$pkg/" "$mount/.SynoUpgradePackages/" || return $?
! setOwner "$STORAGE/dsm.ver" && error "Failed to set the owner for \"$STORAGE/dsm.ver\" !" fi
if [[ "$URL" == "file://$STORAGE/$BASE.pat" ]]; then rm -f "$mount/.SynoUpgradePackages/ActiveInsight-"* || return $?
rm -f "$PAT"
else
mv -f "$PAT" "$STORAGE/$BASE.pat"
fi
if [ -f "$STORAGE/$BASE.pat" ]; then local indexDb="$mount/usr/syno/synoman/indexdb"
! setOwner "$STORAGE/$BASE.pat" && error "Failed to set the owner for \"$STORAGE/$BASE.pat\" !"
fi
mv -f "$BOOT" "$STORAGE/$BASE.boot.img" if [ -s "$idb.txz" ]; then
! setOwner "$STORAGE/$BASE.boot.img" && error "Failed to set the owner for \"$STORAGE/$BASE.boot.img\" !" mkdir -p "$indexDb" || return $?
fi
rm -rf "$TMP" local label="1.44.1-42218"
local offset="1048576" # 2048 * 512
local numBlocks="2097152" # (16777216 * 512) / 4096
local rc
msg="Installing system partition..."
# Build the ext4 filesystem directly from the extracted tree under fakeroot,
# preserving archive ownership without mounting a loop device.
if fakeroot -- bash -c "set -Eeu;\
[ -s $hdp.txz ] && tar xpfJ $hdp.txz --absolute-names -C $mount/;\
[ -s $idb.txz ] && tar xpfJ $idb.txz --absolute-names -C $indexDb/;\
tar xpfJ $hda.txz --absolute-names --skip-old-files -C $mount/;\
printf '%b%s%b' '\E[1;34m \E[1;36m' 'Install: $msg' '\E[0m\n';\
mke2fs -q -t ext4 -b 4096 -d $mount/ -L $label -F -E offset=$offset $SYSTEM $numBlocks"; then
:
else
rc=$?
return "$rc"
fi
rm -rf "$mount" || return $?
return 0
}
checkSse42() {
if disabled "$KVM" || [[ "${PLATFORM,,}" != "x64" ]]; then
return 0
fi
if ! hasFlag "sse4_2"; then
error "Your CPU does not have the SSE4.2 instruction set that Virtual DSM requires!"
enabled "$DEBUG" || return 88
fi
return 0
}
sanitizePatBase() {
local source="$1"
local base
base=$(basename "${source%%\?*}" .pat) || return 1
printf -v base '%b' "${base//%/\\x}" || return 1
base="${base//[!A-Za-z0-9._-]/_}"
printf '%s' "$base"
}
reservePorts() {
local port ports=""
local hostPorts="${HOST_PORTS:-}"
# Older configurations may reserve DSM's web ports for the container.
# They now need to be forwarded to the guest instead.
for port in ${hostPorts//,/ }; do
case "${port,,}" in
5000|5000/tcp|5001|5001/tcp)
continue ;;
esac
ports+="${ports:+,}$port"
done
HOST_PORTS="$ports"
# NAT can fall back to user-mode networking after this point,
# so always add the DSM web interface ports for non-DHCP networking.
USER_PORTS="${USER_PORTS:+$USER_PORTS,}5000/tcp,5001/tcp,80/tcp,443/tcp,445/tcp"
return 0
}
prepareMac() {
local file="$STORAGE/$PROCESS.mac"
local legacy="$STORAGE/dsm.mac"
local container mac=""
# An explicitly configured MAC remains network.sh's responsibility.
[ -n "${MAC:-}" ] && return 0
[ -s "$file" ] && return 0
# Preserve the address generated by older Virtual DSM releases.
if [ -s "$legacy" ]; then
mac=$(readFile "$legacy") || return $?
fi
if [ -z "$mac" ]; then
container=$(containerID) || return $?
mac=$(echo "$container" | md5sum |
sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\).*$/02:11:32:\3:\4:\5/') || return $?
fi
if ! writeState "mac" "${mac^^}"; then
error "Failed to write MAC address to \"$file\" !"
return 28
fi
return 0
}
getCountry() {
local url=$1
local query=$2
local json result
{ json=$(curl -m 5 -H "Accept: application/json" -sfk "$url"); local rc=$?; } || :
(( rc != 0 )) && return 0
{ result=$(echo "$json" | jq -r "$query" 2> /dev/null); rc=$?; } || :
(( rc != 0 )) && return 0
[[ ${#result} -ne 2 ]] && return 0
[[ "${result^^}" == "XX" ]] && return 0
COUNTRY="${result^^}"
return 0
}
setCountry() {
[[ "${TZ,,}" == "asia/harbin" ]] && COUNTRY="CN"
[[ "${TZ,,}" == "asia/beijing" ]] && COUNTRY="CN"
[[ "${TZ,,}" == "asia/urumqi" ]] && COUNTRY="CN"
[[ "${TZ,,}" == "asia/kashgar" ]] && COUNTRY="CN"
[[ "${TZ,,}" == "asia/shanghai" ]] && COUNTRY="CN"
[[ "${TZ,,}" == "asia/chongqing" ]] && COUNTRY="CN"
# Country detection is best-effort and tries independent services in order;
# failure leaves mirror selection at its global default.
[ -z "$COUNTRY" ] && getCountry "https://api.ipapi.is" ".location.country_code"
[ -z "$COUNTRY" ] && getCountry "https://ifconfig.co/json" ".country_iso"
[ -z "$COUNTRY" ] && getCountry "https://api.ip2location.io" ".country_code"
[ -z "$COUNTRY" ] && getCountry "https://ipinfo.io/json" ".country"
[ -z "$COUNTRY" ] && getCountry "https://api.ipquery.io/?format=json" ".location.country_code"
[ -z "$COUNTRY" ] && getCountry "https://api.myip.com" ".cc"
return 0
}
finishDsmInstall() {
local pat="$1"
local tmp="$2"
echo "$BASE" > "$STORAGE/dsm.ver" || return 1
setOwner "$STORAGE/dsm.ver" || warn "failed to set the owner for \"$STORAGE/dsm.ver\" !"
# Do not keep a second copy when the source PAT already lives in storage;
# downloaded or externally mounted sources are cached for later reuse.
if [[ "$URL" == "file://$STORAGE/$BASE.pat" ]]; then
rm -f "$pat" || return $?
else
mv -f "$pat" "$STORAGE/$BASE.pat" || return $?
fi
if [ -f "$STORAGE/$BASE.pat" ]; then
setOwner "$STORAGE/$BASE.pat" || warn "failed to set the owner for \"$STORAGE/$BASE.pat\" !"
fi
mv -f "$DSM_BOOT" "$STORAGE/$BASE.boot.img" || return $?
setOwner "$STORAGE/$BASE.boot.img" || warn "failed to set the owner for \"$STORAGE/$BASE.boot.img\" !"
rm -rf "$tmp" || return $?
return 0
}
installDSM() {
rm -rf /tmp/dsm || return $?
rm -f "$QEMU_DIR/dsm.url" || return $?
checkSse42 || return $?
reservePorts || return $?
prepareMac || return $?
local patName="boot.pat"
local patDir patFile
# Persist the exact PAT base name so future starts reopen the matching boot,
# system, and cached installation files.
if [ -f "$STORAGE/dsm.ver" ]; then
BASE=$(<"$STORAGE/dsm.ver") || return $?
BASE="${BASE//[![:print:]]/}"
[ -z "$BASE" ] && BASE="DSM_VirtualDSM_69057"
else
# Fallback for old installs
BASE="DSM_VirtualDSM_42962"
fi
patDir=$(find / -maxdepth 1 -type d -iname "$patName" -print -quit) || return $?
if [ ! -d "$patDir" ]; then
patDir=$(find "$STORAGE" -maxdepth 1 -type d -iname "$patName" -print -quit) || return $?
fi
# A boot.pat directory bind represents already extracted boot and system
# images and therefore takes precedence over PAT file or URL discovery.
if [ -d "$patDir" ]; then
BASE="DSM_VirtualDSM"
URL="file://$patDir"
if [[ ! -s "$STORAGE/$BASE.boot.img" || ! -s "$STORAGE/$BASE.system.img" ]]; then
error "The bind $patDir maps to a file that does not exist!"
return 65
fi
fi
patFile=$(find / -maxdepth 1 -type f -iname "$patName" -print -quit) || return $?
if [ ! -s "$patFile" ]; then
patFile=$(find "$STORAGE" -maxdepth 1 -type f -iname "$patName" -print -quit) || return $?
fi
if [ -s "$patFile" ]; then
BASE="DSM_VirtualDSM"
URL="file://$patFile"
fi
URL=$(strip "$URL") || return $?
# Derive a filesystem-safe identity from the URL only when no local boot.pat
# source was supplied; preserve an existing system image identity if present.
if [ -n "$URL" ] && [ ! -s "$patFile" ] && [ ! -d "$patDir" ]; then
BASE=$(basename "$URL" .pat) || return $?
if [ ! -s "$STORAGE/$BASE.system.img" ]; then
BASE=$(sanitizePatBase "$URL") || return $?
fi
if [[ "${URL,,}" != "http"* && "${URL,,}" != "file:"* ]]; then
if [ ! -s "$STORAGE/$BASE.pat" ]; then
error "Invalid URL: $URL"
return 65
fi
URL="file://$STORAGE/$BASE.pat"
fi
fi
# A complete matching image pair is the installation marker; the cached PAT
# itself is optional after installation.
if [[ -s "$STORAGE/$BASE.boot.img" && -s "$STORAGE/$BASE.system.img" ]]; then
return 0 # Previous installation found
fi
html "Please wait while Virtual DSM is being installed..." || return $?
local mirror=""
local chinaMirror="https://cndl.synology.cn/download/DSM"
local globalMirror="https://global.synologydownload.com/download/DSM"
[[ "${URL,,}" == *"cndl.synology"* ]] && mirror="$chinaMirror"
[[ "${URL,,}" == *"global.synology"* ]] && mirror="$globalMirror"
# Honor an explicitly selected Synology mirror first, otherwise choose the
# China or global endpoint from the detected country.
if [ -z "$mirror" ]; then
if [ -z "$COUNTRY" ]; then
setCountry || return $?
fi
[ -z "$COUNTRY" ] && info "Warning: could not detect country to select mirror!"
[[ "${COUNTRY^^}" == "CN" ]] && mirror="$chinaMirror" || mirror="$globalMirror"
fi
if [ -z "$URL" ]; then
URL="$mirror/release/7.2.2/72806/DSM_VirtualDSM_72806.pat"
fi
if [ ! -s "$patFile" ]; then
BASE=$(sanitizePatBase "$URL") || return $?
fi
if [[ "$URL" != "file://$STORAGE/$BASE.pat" ]]; then
rm -f "$STORAGE/$BASE.pat" || return $?
fi
rm -f "$STORAGE/$BASE.agent" || return $?
rm -f "$STORAGE/$BASE.boot.img" || return $?
rm -f "$STORAGE/$BASE.system.img" || return $?
# Check filesystem
local fs
fs=$(stat -f -c %T "$STORAGE") || return $?
checkDsmFilesystem "$fs" || return $?
local tmp
# Extract beside storage on Unix filesystems to avoid container-space limits;
# use /tmp for filesystems that cannot safely host the installer workspace.
if [[ "${fs,,}" != "exfat"* && "${fs,,}" != "ntfs"* && "${fs,,}" != "unknown"* ]]; then
tmp="$STORAGE/tmp"
rm -rf "$tmp" || return $?
if ! makeDir "$tmp"; then
error "Failed to create directory \"$tmp\" !"
return 93
fi
else
tmp="/tmp/dsm"
local tmpSpace=2147483648
local space available
space=$(df --output=avail -B 1 /tmp | tail -n 1) || return $?
available=$(formatBytes "$space") || return $?
if (( tmpSpace > space )); then
error "Not enough free space inside the container, have $available available but need at least 2 GB."
return 93
fi
rm -rf "$tmp" || return $?
mkdir -p "$tmp" || return $?
fi
# Check free diskspace
checkDsmSpace || return $?
local pat="/$BASE.pat"
downloadPat "$pat" || return $?
local size
size=$(stat -c%s "$pat") || return $?
# Full Virtual DSM PAT files are substantially larger than update packs;
# reject undersized inputs before attempting destructive image preparation.
if (( size < 250000000 )); then
error "The specified PAT file is probably an update pack as it's too small."
return 62
fi
extractPat "$pat" "$tmp" "$size" || return $?
createSystemImage "$tmp" "$fs" || return $?
installSystemPartition "$tmp" || return $?
finishDsmInstall "$pat" "$tmp" || return $?
return 0
}
installDSM || exit $?
return 0 return 0
-78
View File
@@ -1,78 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
msg="Checking memory..."
enabled "$DEBUG" && echo "$msg"
RAM_AVAIL=$(free -b | grep -m 1 Mem: | awk '{print $7}')
AVAIL_MEM=$(formatBytes "$RAM_AVAIL")
if ! disabled "$RAM_CHECK" && [[ "${RAM_SIZE,,}" != "max" && "${RAM_SIZE,,}" != "half" ]]; then
wanted=$(numfmt --from=iec "$RAM_SIZE")
if (( (wanted + RAM_SPARE) > RAM_AVAIL )); then
msg="Your configured RAM_SIZE of ${RAM_SIZE/G/ GB} is too high for the $AVAIL_MEM of free memory available,"
if [[ "${FS,,}" == "zfs" ]]; then
info "$msg but since ZFS is active this will be ignored."
else
RAM_SIZE="max"
warn "$msg it will automatically be adjusted to a lower amount."
fi
else
if (( (wanted + (RAM_SPARE * 3)) > RAM_AVAIL )); then
msg="your configured RAM_SIZE of ${RAM_SIZE/G/ GB} is very close to the $AVAIL_MEM of free memory available,"
if [[ "${FS,,}" == "zfs" ]]; then
info "$msg but since ZFS is active this will be ignored."
else
warn "$msg please consider a lower amount."
fi
fi
fi
fi
if [[ "${RAM_SIZE,,}" == "half" ]]; then
if (( (RAM_AVAIL / 2) > RAM_SPARE )); then
wanted=$(( (RAM_AVAIL / 2) / 1048577 ))
RAM_SIZE="${wanted}M"
info "Allocated $wanted MB of RAM for the virtual machine."
else
RAM_SIZE="max"
fi
fi
if [[ "${RAM_SIZE,,}" == "max" ]]; then
if (( RAM_AVAIL < (RAM_SPARE * 2) )); then
wanted=$(( RAM_AVAIL / 2 ))
else
wanted=$(( RAM_AVAIL - (RAM_SPARE * 3) ))
if (( wanted < (RAM_SPARE * 6) )); then
wanted=$(( RAM_AVAIL - RAM_SPARE ))
fi
fi
wanted=$(( wanted / 1048577 ))
RAM_SIZE="${wanted}M"
info "Allocated $wanted MB of RAM for the virtual machine."
fi
wanted=$(numfmt --from=iec "$RAM_SIZE")
if [ "$wanted" -lt "$RAM_MINIMUM" ]; then
wanted=$(( wanted / 1048577 ))
error "Not enough memory available, there is only $wanted MB left!"
exit 16
fi
return 0
-906
View File
@@ -1,906 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Docker environment variables
: "${MAC:=""}"
: "${MTU:=""}"
: "${DHCP:="N"}"
: "${NETWORK:="Y"}"
: "${HOST_PORTS:=""}"
: "${USER_PORTS:=""}"
: "${ADAPTER:="virtio-net-pci"}"
: "${VM_NET_IP:=""}"
: "${VM_NET_DEV:=""}"
: "${VM_NET_TAP:="dsm"}"
: "${VM_NET_MAC:="$MAC"}"
: "${VM_NET_BRIDGE:="docker"}"
: "${VM_NET_HOST:="VirtualDSM"}"
: "${VM_NET_MASK:="255.255.255.0"}"
: "${PASST:="/run/passt"}"
: "${PASST_MTU:=""}"
: "${PASST_OPTS:=""}"
: "${PASST_DEBUG:=""}"
: "${PASST_PID:="/var/run/passt.pid"}"
: "${PASST_SOCKET:="/tmp/passt.socket"}"
: "${DNSMASQ_OPTS:=""}"
: "${DNSMASQ_DEBUG:=""}"
: "${DNSMASQ:="/usr/sbin/dnsmasq"}"
: "${DNSMASQ_PID:="/var/run/dnsmasq.pid"}"
: "${DNSMASQ_CONF_DIR:="/etc/dnsmasq.d"}"
# Sanitize variables
MAC=$(strip "$MAC")
MTU=$(strip "$MTU")
ADAPTER=$(strip "$ADAPTER")
NETWORK=$(strip "$NETWORK")
PASST_MTU=$(strip "$PASST_MTU")
HOST_PORTS=$(strip "$HOST_PORTS")
USER_PORTS=$(strip "$USER_PORTS")
ADD_ERR="Please add the following setting to your container:"
# ######################################
# Functions
# ######################################
configureDHCP() {
enabled "$DEBUG" && echo "Configuring MACVTAP networking..."
# Create the necessary file structure for /dev/vhost-net
if [ ! -c /dev/vhost-net ]; then
if mknod /dev/vhost-net c 10 238; then
chmod 660 /dev/vhost-net
fi
fi
# Create a macvtap network for the VM guest
{ msg=$(ip link add link "$VM_NET_DEV" name "$VM_NET_TAP" address "$VM_NET_MAC" type macvtap mode bridge 2>&1); rc=$?; } || :
case "$msg" in
"RTNETLINK answers: File exists"* )
while ! ip link add link "$VM_NET_DEV" name "$VM_NET_TAP" address "$VM_NET_MAC" type macvtap mode bridge; do
info "Waiting for macvtap interface to become available.."
sleep 5
done ;;
"RTNETLINK answers: Invalid argument"* )
error "Cannot create macvtap interface. Please make sure that the network type of the container is 'macvlan' and not 'ipvlan'."
return 1 ;;
"RTNETLINK answers: Operation not permitted"* )
error "No permission to create macvtap interface. Please make sure that your host kernel supports it and that the NET_ADMIN capability is set."
return 1 ;;
*)
[ -n "$msg" ] && echo "$msg" >&2
if (( rc != 0 )); then
error "Cannot create macvtap interface."
return 1
fi ;;
esac
if [[ "$MTU" != "0" && "$MTU" != "1500" ]]; then
if ! ip link set dev "$VM_NET_TAP" mtu "$MTU"; then
warn "Failed to set MTU size to $MTU."
fi
fi
while ! ip link set "$VM_NET_TAP" up; do
info "Waiting for MAC address $VM_NET_MAC to become available..."
info "If you cloned this machine, please delete the 'dsm.mac' file to generate a different MAC address."
sleep 2
done
local TAP_NR TAP_PATH MAJOR MINOR
TAP_NR=$(</sys/class/net/"$VM_NET_TAP"/ifindex)
TAP_PATH="/dev/tap${TAP_NR}"
# Create dev file (there is no udev in container: need to be done manually)
IFS=: read -r MAJOR MINOR < <(cat /sys/devices/virtual/net/"$VM_NET_TAP"/tap*/dev)
(( MAJOR < 1)) && error "Cannot find: sys/devices/virtual/net/$VM_NET_TAP" && return 1
[[ ! -e "$TAP_PATH" && -e "/dev0/${TAP_PATH##*/}" ]] && ln -s "/dev0/${TAP_PATH##*/}" "$TAP_PATH"
if [[ ! -e "$TAP_PATH" ]]; then
{ mknod "$TAP_PATH" c "$MAJOR" "$MINOR" ; rc=$?; } || :
(( rc != 0 )) && error "Cannot mknod: $TAP_PATH ($rc)" && return 1
fi
{ exec 30>>"$TAP_PATH"; rc=$?; } 2>/dev/null || :
if (( rc != 0 )); then
error "Cannot create TAP interface ($rc). $ADD_ERR --device-cgroup-rule='c *:* rwm'" && return 1
fi
{ exec 40>>/dev/vhost-net; rc=$?; } 2>/dev/null || :
if (( rc != 0 )); then
error "VHOST can not be found ($rc). $ADD_ERR --device=/dev/vhost-net" && return 1
fi
NET_OPTS="-netdev tap,id=hostnet0,vhost=on,vhostfd=40,fd=30"
return 0
}
configureDNS() {
local fa="$1"
local ip="$2"
local mac="$3"
local host="$4"
local mask="$5"
local gateway="$6"
local arguments="$DNSMASQ_OPTS"
echo "$gateway" > /run/shm/qemu.gw
enabled "${DNSMASQ_DISABLE:-}" && return 0
enabled "$DEBUG" && echo "Starting dnsmasq daemon..."
[ -s "$DNSMASQ_PID" ] && pKill "$(<"$DNSMASQ_PID")"
rm -f "$DNSMASQ_PID"
case "${NETWORK,,}" in
"tap" | "tun" | "tuntap" | "y" )
# Create lease file for faster resolve
echo "0 $mac $ip $host 01:$mac" > /var/lib/misc/dnsmasq.leases
chmod 644 /var/lib/misc/dnsmasq.leases
# dnsmasq configuration:
arguments+=" --dhcp-authoritative"
# Set DHCP range and host
arguments+=" --dhcp-range=$ip,$ip"
arguments+=" --dhcp-host=$mac,,$ip,$host,infinite"
# Set DNS server and gateway
arguments+=" --dhcp-option=option:netmask,$mask"
arguments+=" --dhcp-option=option:router,$gateway"
arguments+=" --dhcp-option=option:dns-server,$gateway"
esac
# Set interfaces
arguments+=" --interface=$fa"
arguments+=" --bind-interfaces"
# Workaround NET_RAW capability
arguments+=" --no-ping"
# Add DNS entry for container
arguments+=" --address=/host.lan/$gateway"
# Set local dns resolver to dnsmasq when needed
[ -f /etc/resolv.dnsmasq ] && arguments+=" --resolv-file=/etc/resolv.dnsmasq"
# Enable logging to file
local log="/var/log/dnsmasq.log"
rm -f "$log"
arguments+=" --log-facility=$log"
arguments=$(echo "$arguments" | sed 's/\t/ /g' | tr -s ' ' | sed 's/^ *//')
enabled "$DEBUG" && printf "Dnsmasq arguments:\n\n%s\n\n" "${arguments// -/$'\n-'}"
{ $DNSMASQ ${arguments:+ $arguments}; rc=$?; } || :
if (( rc != 0 )); then
local msg="Failed to start Dnsmasq, reason: $rc"
if [[ "${NETWORK,,}" == "slirp" || "${NETWORK,,}" == "passt" ]] || ! enabled "$ROOTLESS" || enabled "$DEBUG"; then
[ -f "$log" ] && [ -s "$log" ] && cat "$log"
error "$msg"
fi
return 1
fi
if enabled "$DNSMASQ_DEBUG"; then
tail -fn +0 "$log" --pid=$$ &
fi
return 0
}
getHostPorts() {
local list="${HOST_PORTS// /},"
# Remove duplicates
list=$(echo "${list//,,/,}," | awk 'BEGIN{RS=ORS=","} !seen[$0]++' | sed 's/,*$//g')
echo "$list"
return 0
}
getUserPorts() {
local ssh="22"
local dsm="5000,5001"
local list="$ssh,$dsm,"
list+="${USER_PORTS// /},"
local exclude
exclude=$(getHostPorts)
local ports=""
local userport=""
local hostport=""
for userport in ${list//,/ }; do
local num="${userport///tcp}"
num="${num///udp}"
for hostport in ${exclude//,/ }; do
local port="${hostport///tcp}"
port="${port///udp}"
if [[ "$num" == "$port" ]]; then
num=""
if [[ "$port" != "$WEB_PORT" ]]; then
warn "Could not assign port $port to \"USER_PORTS\" because it is already in \"HOST_PORTS\"!"
fi
fi
done
[ -n "$num" ] && ports+="$userport,"
done
# Remove duplicates
echo "${ports//,,/,}," | awk 'BEGIN{RS=ORS=","} !seen[$0]++' | sed 's/,*$//g'
return 0
}
getSlirp() {
local ip="$1"
local args=""
local list=""
list=$(getUserPorts)
for port in ${list//,/ }; do
local proto="tcp"
local num="${port%/tcp}"
[ -z "$num" ] && continue
if [[ "$port" == *"/udp" ]]; then
proto="udp"
num="${port%/udp}"
elif [[ "$port" != *"/tcp" ]]; then
args+="hostfwd=$proto::$num-$ip:$num,"
proto="udp"
num="${port%/udp}"
fi
args+="hostfwd=$proto::$num-$ip:$num,"
done
echo "$args" | sed 's/,*$//g'
return 0
}
configureSlirp() {
NETWORK="slirp"
enabled "$DEBUG" && echo "Configuring slirp networking..."
local ip="$IP"
[ -n "$VM_NET_IP" ] && ip="$VM_NET_IP"
local base="${ip%.*}."
[ "${ip/$base/}" -lt "4" ] && ip="${ip%.*}.4"
local gateway="${ip%.*}.1"
local ipv6=""
[ -n "$IP6" ] && ipv6="ipv6=on,"
NET_OPTS="-netdev user,id=hostnet0,ipv4=on,host=$gateway,net=${gateway%.*}.0/24,dhcpstart=$ip,${ipv6}hostname=$VM_NET_HOST"
local forward=""
forward=$(getSlirp "$ip")
[ -n "$forward" ] && NET_OPTS+=",$forward"
if enabled "${DNSMASQ_DISABLE:-}"; then
echo "$gateway" > /run/shm/qemu.gw
else
[ ! -f /etc/resolv.dnsmasq ] && cp /etc/resolv.conf /etc/resolv.dnsmasq
configureDNS "lo" "$ip" "$VM_NET_MAC" "$VM_NET_HOST" "$VM_NET_MASK" "$gateway" || return 1
printf '%s\n' \
'nameserver 127.0.0.1' \
'search .' \
'options ndots:0' > /etc/resolv.conf
fi
VM_NET_IP="$ip"
return 0
}
configurePasst() {
NETWORK="passt"
enabled "$DEBUG" && echo "Configuring user-mode networking..."
local log="/tmp/passt.log"
rm -f "$log"
local ip="$IP"
[ -n "$VM_NET_IP" ] && ip="$VM_NET_IP"
local gateway=""
if [[ "$ip" != *".1" ]]; then
gateway="${ip%.*}.1"
else
gateway="${ip%.*}.2"
fi
# passt configuration:
[ -z "$IP6" ] && PASST_OPTS+=" -4"
PASST_OPTS+=" -a $ip"
PASST_OPTS+=" -g $gateway"
PASST_OPTS+=" -n $VM_NET_MASK"
[ -n "$PASST_MTU" ] && PASST_OPTS+=" -m $PASST_MTU"
local forward=""
forward=$(getUserPorts)
forward="${forward///tcp}"
forward="${forward///udp}"
if [ -n "$forward" ]; then
forward="%${VM_NET_DEV}/$forward"
PASST_OPTS+=" -t $forward"
PASST_OPTS+=" -u $forward"
fi
PASST_OPTS+=" -H $VM_NET_HOST"
PASST_OPTS+=" -M $GATEWAY_MAC"
PASST_OPTS+=" -P $PASST_PID"
PASST_OPTS+=" -s $PASST_SOCKET"
PASST_OPTS+=" -l $log"
PASST_OPTS+=" -q"
if ! enabled "${DNSMASQ_DISABLE:-}"; then
[ ! -f /etc/resolv.dnsmasq ] && cp /etc/resolv.conf /etc/resolv.dnsmasq
printf '%s\n' \
'nameserver 127.0.0.1' \
'search .' \
'options ndots:0' > /etc/resolv.conf
fi
PASST_OPTS=$(echo "$PASST_OPTS" | sed 's/\t/ /g' | tr -s ' ' | sed 's/^ *//')
if enabled "$DEBUG" || enabled "$PASST_DEBUG"; then
printf "Passt arguments:\n\n%s\n\n" "${PASST_OPTS// -/$'\n-'}"
fi
[ ! -f "$PASST" ] && cp /usr/bin/passt* /run
if ! "$PASST" ${PASST_OPTS:+$PASST_OPTS} >/dev/null 2>&1; then
rm -f "$log"
PASST_OPTS="${PASST_OPTS/ -q/}"
{ "$PASST" ${PASST_OPTS:+$PASST_OPTS}; rc=$?; } || :
if (( rc != 0 )); then
[ -f "$log" ] && [ -s "$log" ] && cat "$log"
warn "failed to start passt ($rc), falling back to slirp networking!"
configureSlirp && return 0 || return 1
fi
fi
if enabled "$PASST_DEBUG"; then
tail -fn +0 "$log" --pid=$$ &
else
if enabled "$DEBUG"; then
[ -f "$log" ] && [ -s "$log" ] && cat "$log" && echo ""
fi
fi
NET_OPTS="-netdev stream,id=hostnet0,server=off,addr.type=unix,addr.path=$PASST_SOCKET"
configureDNS "lo" "$ip" "$VM_NET_MAC" "$VM_NET_HOST" "$VM_NET_MASK" "$gateway" || return 1
VM_NET_IP="$ip"
return 0
}
clearTables() {
local table="" line rules
# Choose between iptables or nftables
if command -v iptables-nft >/dev/null 2>&1 && iptables-nft -V >/dev/null 2>&1; then
update-alternatives --set iptables /usr/sbin/iptables-nft > /dev/null
update-alternatives --set ip6tables /usr/sbin/ip6tables-nft > /dev/null
else
update-alternatives --set iptables /usr/sbin/iptables-legacy > /dev/null
update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy > /dev/null
fi
# Store the current iptables ruleset
! rules=$(iptables-save 2> /dev/null) && return 0
[ -z "$rules" ] && return 0
# Delete every rule tagged with our unique identifier, leaving all other rules intact.
while IFS= read -r line; do
case "$line" in
\*nat) table="nat" ;;
\*filter) table="filter" ;;
\*mangle) table="mangle" ;;
\*raw) table="raw" ;;
esac
if [[ "$line" == -A* ]]; then
local re="--comment[[:space:]]+\"?remove\"?([[:space:]]|\$)"
if [[ "$line" =~ $re ]]; then
read -ra args <<< "${line/-A /-D }"
iptables -t "$table" "${args[@]}" &> /dev/null || :
fi
fi
done <<< "$rules"
return 0
}
configureNAT() {
local tuntap="TUN device is missing. $ADD_ERR --device /dev/net/tun"
local tables="the 'ip_tables' kernel module is not loaded. Try this command: sudo modprobe ip_tables iptable_nat"
enabled "$DEBUG" && echo "Configuring NAT networking..."
# Create the necessary file structure for /dev/net/tun
if [ ! -c /dev/net/tun ]; then
[ ! -d /dev/net ] && mkdir -m 755 /dev/net
if mknod /dev/net/tun c 10 200; then
chmod 666 /dev/net/tun
fi
fi
if [ ! -c /dev/net/tun ]; then
enabled "$ROOTLESS" && ! enabled "$DEBUG" && return 1
warn "$tuntap" && return 1
fi
# Check port forwarding flag
if [[ $(< /proc/sys/net/ipv4/ip_forward) -eq 0 ]]; then
{ sysctl -w net.ipv4.ip_forward=1 > /dev/null 2>&1; rc=$?; } || :
if (( rc != 0 )) || [[ $(< /proc/sys/net/ipv4/ip_forward) -eq 0 ]]; then
enabled "$ROOTLESS" && ! enabled "$DEBUG" && return 1
warn "IP forwarding is disabled. $ADD_ERR --sysctl net.ipv4.ip_forward=1"
return 1
fi
fi
local ip base gateway exclude
base=$(cut -d. -f3,4 <<< "$IP")
if [[ "$IP" != "172.30."* ]]; then
ip="172.30.$base"
else
ip="172.31.$base"
fi
[ -n "$VM_NET_IP" ] && ip="$VM_NET_IP"
if [[ "$ip" != *".1" ]]; then
gateway="${ip%.*}.1"
else
gateway="${ip%.*}.2"
fi
local subnet="${ip%.*}.0/24"
local broadcast="${ip%.*}.255"
# Create a bridge with a static IP for the VM guest
{ ip link add dev "$VM_NET_BRIDGE" type bridge ; rc=$?; } || :
if (( rc != 0 )); then
enabled "$ROOTLESS" && ! enabled "$DEBUG" && return 1
warn "failed to create bridge. $ADD_ERR --cap-add NET_ADMIN" && return 1
fi
if ! ip address add "$gateway/24" broadcast "$broadcast" dev "$VM_NET_BRIDGE"; then
warn "failed to add IP address pool!" && return 1
fi
while ! ip link set "$VM_NET_BRIDGE" up; do
info "Waiting for IP address to become available..."
sleep 2
done
# Set tap to the bridge created
if ! ip tuntap add dev "$VM_NET_TAP" mode tap; then
enabled "$ROOTLESS" && ! enabled "$DEBUG" && return 1
warn "$tuntap" && return 1
fi
if [[ "$MTU" != "0" && "$MTU" != "1500" ]]; then
if ! ip link set dev "$VM_NET_TAP" mtu "$MTU"; then
warn "failed to set MTU size to $MTU."
fi
fi
if ! ip link set dev "$VM_NET_TAP" address "$GATEWAY_MAC"; then
warn "failed to set gateway MAC address."
fi
while ! ip link set "$VM_NET_TAP" up promisc on; do
info "Waiting for TAP to become available..."
sleep 2
done
if ! ip link set dev "$VM_NET_TAP" master "$VM_NET_BRIDGE"; then
warn "failed to set master bridge!" && return 1
fi
# Flush existing tables
clearTables
exclude=$(getHostPorts)
if [ -n "$exclude" ]; then
if [[ "$exclude" != *","* ]]; then
exclude=" ! --dport $exclude"
else
exclude=" -m multiport ! --dports $exclude"
fi
fi
# NAT traffic from bridge subnet to Docker uplink
if ! iptables -t nat -A POSTROUTING -o "$VM_NET_DEV" -s "$subnet" ! -d "$subnet" -m comment --comment "remove" -j MASQUERADE > /dev/null 2>&1; then
enabled "$ROOTLESS" && ! enabled "$DEBUG" && return 1
if ! iptables -t nat -A POSTROUTING -o "$VM_NET_DEV" -s "$subnet" ! -d "$subnet" -m comment --comment "remove" -j MASQUERADE; then
warn "$tables" && return 1
fi
fi
# shellcheck disable=SC2086
if ! iptables -t nat -A PREROUTING -i "$VM_NET_DEV" -d "$IP" -p tcp${exclude} -m comment --comment "remove" -j DNAT --to "$ip"; then
warn "failed to configure IP tables!" && return 1
fi
if ! iptables -t nat -A PREROUTING -i "$VM_NET_DEV" -d "$IP" -p udp -m comment --comment "remove" -j DNAT --to "$ip"; then
warn "failed to configure IP tables!" && return 1
fi
if (( KERNEL > 4 )); then
# Hack for guest VMs complaining about "bad udp checksums in 5 packets"
iptables -A POSTROUTING -t mangle -p udp --dport bootpc -m comment --comment "remove" -j CHECKSUM --checksum-fill > /dev/null 2>&1 || true
fi
# Allow forwarding from bridge -> dev
if ! iptables -A FORWARD -i "$VM_NET_BRIDGE" -o "$VM_NET_DEV" -m comment --comment "remove" -j ACCEPT; then
warn "failed to configure IP tables!" && return 1
fi
# Allow return traffic
if ! iptables -A FORWARD -i "$VM_NET_DEV" -o "$VM_NET_BRIDGE" -m conntrack --ctstate RELATED,ESTABLISHED -m comment --comment "remove" -j ACCEPT; then
warn "failed to configure IP tables!" && return 1
fi
NET_OPTS="-netdev tap,id=hostnet0,ifname=$VM_NET_TAP"
if [ -c /dev/vhost-net ]; then
{ exec 40>>/dev/vhost-net; rc=$?; } 2>/dev/null || :
(( rc == 0 )) && NET_OPTS+=",vhost=on,vhostfd=40"
fi
NET_OPTS+=",script=no,downscript=no"
configureDNS "$VM_NET_BRIDGE" "$ip" "$VM_NET_MAC" "$VM_NET_HOST" "$VM_NET_MASK" "$gateway" || return 1
VM_NET_IP="$ip"
return 0
}
closeBridge() {
local pids=( "$PASST_PID" "$DNSMASQ_PID" )
mKill "${pids[@]}"
ip link set "$VM_NET_TAP" down promisc off &> /dev/null || :
ip link delete "$VM_NET_TAP" &> /dev/null || :
ip link set "$VM_NET_BRIDGE" down &> /dev/null || :
ip link delete "$VM_NET_BRIDGE" &> /dev/null || :
clearTables
return 0
}
closeWeb() {
local pids=( "$WEB_PID" "$WSD_PID" )
mKill "${pids[@]}"
return 0
}
closeNetwork() {
if ! disabled "${WEB:-}" && enabled "$DHCP"; then
closeWeb
fi
disabled "$NETWORK" && return 0
exec 30<&- || true
exec 40<&- || true
closeBridge
return 0
}
cleanUp() {
closeBridge
# Clean up old files
rm -f "$PASST_PID" "$PASST_SOCKET"
rm -f "$DNSMASQ_PID" /etc/resolv.dnsmasq
return 0
}
checkOS() {
local os=""
local kernel=""
local iface="macvlan"
kernel=$(uname -a)
[[ "${kernel,,}" == *"darwin"* ]] && os="$ENGINE Desktop for macOS"
[[ "${kernel,,}" == *"microsoft"* ]] && os="$ENGINE Desktop for Windows"
if enabled "$DHCP"; then
iface="macvtap"
[[ "${kernel,,}" == *"synology"* ]] && os="Synology Container Manager"
fi
if [ -n "$os" ]; then
warn "you are using $os which does not support $iface, please revert to bridge networking!"
fi
return 0
}
getInfo() {
if [ -z "$VM_NET_DEV" ]; then
# Give Kubernetes priority over the default interface
[ -d "/sys/class/net/net0" ] && VM_NET_DEV="net0"
[ -d "/sys/class/net/net1" ] && VM_NET_DEV="net1"
[ -d "/sys/class/net/net2" ] && VM_NET_DEV="net2"
[ -d "/sys/class/net/net3" ] && VM_NET_DEV="net3"
# Automatically detect the default network interface
[ -z "$VM_NET_DEV" ] && VM_NET_DEV=$(awk '$2 == 00000000 { print $1; exit }' /proc/net/route)
[ -z "$VM_NET_DEV" ] && VM_NET_DEV="eth0"
fi
if [ ! -d "/sys/class/net/$VM_NET_DEV" ]; then
error "Network interface '$VM_NET_DEV' does not exist inside the container!"
error "$ADD_ERR -e \"VM_NET_DEV=NAME\" to specify another interface name." && exit 26
fi
GATEWAY=$(ip route list dev "$VM_NET_DEV" | awk ' /^default/ {print $3}' | head -n 1)
{ IP=$(ip address show dev "$VM_NET_DEV" | grep inet | awk '/inet / { print $2 }' | cut -f1 -d/ | head -n 1); } 2>/dev/null || :
[ -z "$IP" ] && ! enabled "$DHCP" && error "Could not determine container IPv4 address!" && exit 26
IP6=""
# shellcheck disable=SC2143
if [ -f /proc/net/if_inet6 ] && [[ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null)" != "1" ]] && [ -n "$(ifconfig -a | grep inet6)" ]; then
{ IP6=$(ip -6 addr show dev "$VM_NET_DEV" scope global up); rc=$?; } 2>/dev/null || :
(( rc != 0 )) && IP6=""
[ -n "$IP6" ] && IP6=$(echo "$IP6" | sed -e's/^.*inet6 \([^ ]*\)\/.*$/\1/;t;d' | head -n 1)
fi
local result nic bus
result=$(ethtool -i "$VM_NET_DEV")
nic=$(grep -m 1 -i 'driver:' <<< "$result" | awk '{print $2}')
bus=$(grep -m 1 -i 'bus-info:' <<< "$result" | awk '{print $2}')
if [[ "${bus,,}" != "" && "${bus,,}" != "n/a" && "${bus,,}" != "tap" ]]; then
enabled "$DEBUG" && info "Detected BUS: $bus"
error "This container does not support host mode networking!"
exit 29
fi
if enabled "$DHCP"; then
checkOS
if [[ "${nic,,}" == "ipvlan" ]]; then
error "This container does not support IPVLAN networking when DHCP=Y."
exit 29
fi
if [[ "${nic,,}" != "macvlan" ]]; then
enabled "$DEBUG" && info "Detected NIC: $nic"
error "The container needs to be in a MACVLAN network when DHCP=Y."
exit 29
fi
if uname -a | grep -Eqi 'unraid|truenas'; then
# Check if host exposes the bridge-nf sysctl
# (only visible if br_netfilter is loaded and /proc/sys is accessible)
BNF="/proc/sys/net/bridge/bridge-nf-call-iptables"
if [[ -r "$BNF" ]] && [[ "$(cat "$BNF")" != "0" ]]; then
warn "external LAN clients may not be able to reach this container, because net.bridge.bridge-nf-call-iptables=1."
warn "you can fix this issue by running 'sysctl -w net.bridge.bridge-nf-call-iptables=0' on the host system."
fi
fi
else
if [[ "$IP" != "172."* && "$IP" != "10.8"* && "$IP" != "10.9"* ]]; then
checkOS
fi
fi
local mtu=""
if [ -f "/sys/class/net/$VM_NET_DEV/mtu" ]; then
mtu=$(< "/sys/class/net/$VM_NET_DEV/mtu")
fi
[ -z "$MTU" ] && MTU="$mtu"
[ -z "$MTU" ] && MTU="0"
if [[ "${ADAPTER,,}" != "virtio-net-pci" ]]; then
if [[ "$MTU" != "0" ]] && [ "$MTU" -lt "1500" ]; then
warn "MTU size is $MTU, but cannot be set for $ADAPTER adapters!" && MTU="0"
fi
fi
if [ -z "$VM_NET_MAC" ]; then
local file="$STORAGE/dsm.mac"
[ -s "$file" ] && VM_NET_MAC=$(<"$file")
VM_NET_MAC="${VM_NET_MAC//[![:print:]]/}"
if [ -z "$VM_NET_MAC" ]; then
# Generate MAC address based on Docker container ID in hostname
VM_NET_MAC=$(echo "$HOST" | md5sum | sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\).*$/02:11:32:\3:\4:\5/')
echo "${VM_NET_MAC^^}" > "$file"
! setOwner "$file" && error "Failed to set the owner for \"$file\" !"
fi
fi
VM_NET_MAC="${VM_NET_MAC^^}"
VM_NET_MAC="${VM_NET_MAC//-/:}"
if [[ ${#VM_NET_MAC} == 12 ]]; then
m="$VM_NET_MAC"
VM_NET_MAC="${m:0:2}:${m:2:2}:${m:4:2}:${m:6:2}:${m:8:2}:${m:10:2}"
fi
if [[ ${#VM_NET_MAC} != 17 ]]; then
error "Invalid MAC address: '$VM_NET_MAC', should be 12 or 17 digits long!" && exit 28
fi
GATEWAY_MAC=$(echo "$VM_NET_MAC" | md5sum | sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\).*$/02:\1:\2:\3:\4:\5/')
if enabled "$DEBUG"; then
line="Host: $HOST IP: $IP Gateway: $GATEWAY Interface: $VM_NET_DEV MAC: $VM_NET_MAC MTU: $mtu"
[[ "$MTU" != "0" && "$MTU" != "$mtu" ]] && line+=" ($MTU)"
info "$line"
if [ -f /etc/resolv.conf ]; then
nameservers=$(grep '^nameserver ' /etc/resolv.conf | sed 's/^nameserver //' | paste -sd ',' | sed 's/,/, /g')
[ -n "$nameservers" ] && info "Nameservers: $nameservers"
fi
echo
fi
echo "$IP" > "$QEMU_DIR"/qemu.ip
echo "$nic" > "$QEMU_DIR"//qemu.nic
return 0
}
# ######################################
# Configure Network
# ######################################
if disabled "$NETWORK"; then
NET_OPTS=""
return 0
fi
msg="Initializing network..."
html "$msg"
enabled "$DEBUG" && echo "$msg"
getInfo
cleanUp
if [[ "$IP" == "172.17."* ]]; then
warn "your container IP starts with 172.17.* which will cause conflicts when you install the Container Manager package inside DSM!"
fi
MSG="Booting DSM instance..."
html "$MSG"
if enabled "$DHCP"; then
# Configure for macvtap interface
configureDHCP || exit 20
else
if ! disabled "${WEB:-}"; then
sleep 1.2
closeWeb
fi
case "${NETWORK,,}" in
"passt" | "slirp" | "user"* ) ;;
"tap" | "tun" | "tuntap" | "y" | "" )
# Configure tap interface
if ! configureNAT; then
closeBridge
NETWORK="user"
if ! enabled "$ROOTLESS" || enabled "$DEBUG"; then
msg="falling back to user-mode networking!"
msg="failed to setup NAT networking, $msg"
warn "$msg"
fi
fi ;;
esac
case "${NETWORK,,}" in
"tap" | "tun" | "tuntap" | "y" | "" ) ;;
"passt" | "user"* )
# Configure for user-mode networking (passt)
if ! configurePasst; then
error "Failed to configure user-mode networking!"
exit 24
fi ;;
"slirp" )
# Configure for user-mode networking (slirp)
if ! configureSlirp; then
error "Failed to configure user-mode networking!"
exit 24
fi ;;
*)
error "Unrecognized NETWORK value: \"$NETWORK\"" && exit 24 ;;
esac
case "${NETWORK,,}" in
"passt" | "slirp" )
if [ -z "$USER_PORTS" ]; then
info "Notice: because user-mode networking is active, when you need to forward custom ports to DSM, add them to the \"USER_PORTS\" variable."
fi ;;
esac
fi
NET_OPTS+=" -device $ADAPTER,id=net0,netdev=hostnet0,romfile=,mac=$VM_NET_MAC"
[[ "$MTU" != "0" && "$MTU" != "1500" ]] && NET_OPTS+=",host_mtu=$MTU"
return 0
+126 -96
View File
@@ -2,116 +2,68 @@
set -Eeuo pipefail set -Eeuo pipefail
: "${SHUTDOWN:="Y"}" # Graceful ACPI shutdown : "${SHUTDOWN:="Y"}" # Graceful ACPI shutdown
: "${TIMEOUT:="115"}" # QEMU termination timeout : "${TIMEOUT:="105"}" # QEMU termination timeout
: "${API_TIMEOUT:="90"}" # External API call timeout : "${API_TIMEOUT:="90"}" # External API call timeout
# Configure QEMU for graceful shutdown # Configure QEMU for graceful shutdown
API_CMD=6 API_CMD=6
API_HOST="127.0.0.1:$COM_PORT"
# Configure QEMU for graceful shutdown SHUTDOWN_SKIP=0
SHUTDOWN_SIGNAL=0
QEMU_END="$QEMU_DIR/qemu.end" QEMU_END="$QEMU_DIR/qemu.end"
CONSOLE_PID="$QEMU_DIR/console.pid"
_trap() { CONSOLE_SOCKET="$QEMU_DIR/console.sock"
local func="$1"; shift QEMU_START_PID="$QEMU_DIR/qemu.start.pid"
local sig
TRAP_PID=$BASHPID
for sig; do
trap "$func $sig" "$sig"
done
}
app() {
echo "$APP" && return 0
}
finish() { finish() {
local i=0 local reason=$1 failed=0
local pid=""
local reason=$1
local pids=( "${HOST_PID:-}" "${WSD_PID:-}" \
"${WEB_PID:-}" "${PASST_PID:-}" "${DNSMASQ_PID:-}" )
touch "$QEMU_END" # A nonzero exit is unexpected only when QEMU_END is missing.
if [ ! -f "$QEMU_END" ] && (( reason != 0 )); then
if [ -s "$QEMU_PID" ]; then failed=1
if read -r pid <"$QEMU_PID"; then
if [ -n "$pid" ] && isAlive "$pid"; then
local display="$reason"
case "$reason" in
129 ) display="SIGHUP" ;;
130 ) display="SIGINT" ;;
131 ) display="SIGQUIT" ;;
134 ) display="SIGABRT" ;;
143 ) display="SIGTERM" ;;
esac
error "Forcefully terminating $(app), reason: $display..."
{ disown "$pid" || :; kill -9 -- "$pid" || :; } 2>/dev/null
fi
fi
fi fi
mKill "${pids[@]}" touch "$QEMU_END" || :
forceKillQemu "$reason"
cleanupHelpers "$HOST_PID"
fKill "print.sh" fKill "print.sh"
rm -f -- "$HOST_API_SOCKET" "$HOST_AGENT_SOCKET"
closeNetwork if ! waitQemuExit 10; then
if ! waitPidFile "$QEMU_PID" 10; then
warn "Timed out while waiting for $(app) to exit!" warn "Timed out while waiting for $(app) to exit!"
fi fi
(( reason != 1 )) && echo && echo " Shutdown completed!" stopConsole
echo
if (( failed == 0 )); then
echo " Shutdown completed!"
else
error "QEMU exited unexpectedly!"
fi
exit "$reason" exit "$reason"
} }
graceful_shutdown() { sendGuestShutdown() {
local sig="$1" local pid="$1"
local pid="" local response
local code=0
local start url response elapsed
[[ $BASHPID != "$TRAP_PID" ]] && return
case "$sig" in
SIGHUP) code=129 ;;
SIGINT) code=130 ;;
SIGQUIT) code=131 ;;
SIGABRT) code=134 ;;
SIGTERM) code=143 ;;
esac
if [ -f "$QEMU_END" ]; then
echo && info "Received $1 signal while already shutting down..."
return
fi
set +e
start=$SECONDS
touch "$QEMU_END"
echo && info "Received $1 signal, sending shutdown command..."
if [ ! -s "$QEMU_PID" ] || ! read -r pid <"$QEMU_PID"; then
warn "QEMU PID file ($QEMU_PID) does not exist?"
finish "$code"
fi
if [ -z "$pid" ] || ! isAlive "$pid"; then
warn "QEMU process with PID $pid does not exist?"
finish "$code"
fi
# Virtual DSM ignores ACPI powerdown, so graceful shutdown must go through
# the qemu-host guest API exposed on the Unix socket.
# Don't send the powerdown signal because vDSM ignores ACPI signals # Don't send the powerdown signal because vDSM ignores ACPI signals
# nc -q 1 -w 1 -U "$QEMU_DIR/monitor.sock" &> /dev/null <<<'system_powerdown' || : # nc -q 1 -w 1 -U "$QEMU_DIR/monitor.sock" &> /dev/null <<<'system_powerdown' || :
# Send shutdown command to guest agent via serial port # Send shutdown command to guest agent via serial port
API_TIMEOUT=$(strip "$API_TIMEOUT") API_TIMEOUT=$(strip "$API_TIMEOUT")
url="http://$API_HOST/read?command=$API_CMD&timeout=$API_TIMEOUT" local url="http://localhost/read?command=$API_CMD&timeout=$API_TIMEOUT"
response=$(curl -sk -m "$(( API_TIMEOUT+2 ))" -S "$url" 2>&1) response=$(curl --unix-socket "$HOST_API_SOCKET" -sk -m "$(( API_TIMEOUT+2 ))" -S "$url" 2>&1)
if [[ "$response" =~ "\"success\"" ]]; then if [[ "$response" =~ "\"success\"" ]]; then
@@ -121,20 +73,25 @@ graceful_shutdown() {
response="${response#*message\"\: \"}" response="${response#*message\"\: \"}"
[ -z "$response" ] && response="second signal" [ -z "$response" ] && response="second signal"
echo && error "Forcefully terminating because of: ${response%%\"*}" echo && error "Forcefully terminating because of: ${response%%\"*}"
kill -15 -- "$pid" 2>/dev/null || : kill -15 -- "$pid" 2>/dev/null || :
fi fi
local name return 0
name="$(app)" }
normalizeTimeout() {
# Divide the remaining timeout into guest wait, SIGTERM grace, and final
# cleanup instead of allowing the API call to consume the entire budget.
local term_grace=3 # seconds before loop ends to send SIGTERM local term_grace=3 # seconds before loop ends to send SIGTERM
local cleanup_grace=3 # seconds reserved after the loop for cleanup local cleanup_grace=3 # seconds reserved after the loop for cleanup
TIMEOUT=$(strip "$TIMEOUT") TIMEOUT=$(strip "$TIMEOUT")
if [[ ! "$TIMEOUT" =~ ^[0-9]+$ ]]; then if [[ ! "$TIMEOUT" =~ ^[0-9]+$ ]]; then
TIMEOUT=115 TIMEOUT=105
fi fi
if (( TIMEOUT >= 30 )); then if (( TIMEOUT >= 30 )); then
@@ -145,27 +102,36 @@ graceful_shutdown() {
cleanup_grace=4 cleanup_grace=4
fi fi
local cnt=0 sigterm_at=0 min wait_until elapsed timeout_left local elapsed=$((SECONDS - start))
local timeout_left=$((TIMEOUT - elapsed))
elapsed=$((SECONDS - start)) local min=$((term_grace + cleanup_grace + 1))
timeout_left=$((TIMEOUT - elapsed))
min=$((term_grace + cleanup_grace + 1))
(( timeout_left < min )) && timeout_left=$min (( timeout_left < min )) && timeout_left=$min
wait_until=$((timeout_left - cleanup_grace)) wait_until=$((timeout_left - cleanup_grace))
sigterm_at=$((wait_until - term_grace)) sigterm_at=$((wait_until - term_grace))
while (( cnt <= wait_until )); do return 0
}
waitForShutdown() {
local cnt=0
local pid="$1"
local name="$APP"
while (( cnt <= wait_until && SHUTDOWN_SKIP == 0 )); do
sleep 1 & sleep 1 &
local slp=$! local slp=$!
# Stop waiting if the process has exited # Stop waiting if the process has exited
! isAlive "$pid" && break isAlive "$pid" || break
# The process state is authoritative, but disappearance of both pidfiles
# also ends the wait when a wrapper exits before process reaping completes.
# Workaround for stale/zombie QEMU pid file # Workaround for stale/zombie QEMU pid file
[ ! -s "$QEMU_PID" ] && break [ ! -s "$QEMU_START_PID" ] && [ ! -s "$QEMU_PID" ] && break
if (( cnt == sigterm_at )); then if (( cnt == sigterm_at )); then
info "${name^} is still running, sending SIGTERM... ($cnt/$wait_until)" info "${name^} is still running, sending SIGTERM... ($cnt/$wait_until)"
@@ -174,17 +140,81 @@ graceful_shutdown() {
info "Waiting for $name to shut down... ($cnt/$wait_until)" info "Waiting for $name to shut down... ($cnt/$wait_until)"
fi fi
wait "$slp" wait "$slp" || :
(( cnt++ )) (( cnt++ ))
done done
return 0
}
gracefulShutdown() {
local sig="$1"
local pid code
[[ $BASHPID != "$TRAP_PID" ]] && return
code=$(signalCode "$sig")
if (( SHUTDOWN_SIGNAL != 0 )); then
# A second Ctrl-C is the explicit user request to skip the remaining
# graceful-shutdown wait and proceed to forced cleanup.
if (( code == 130 && SHUTDOWN_SIGNAL == code )); then
SHUTDOWN_SKIP=1
echo && info "Received SIGINT again, forcing shutdown..."
return
fi
echo && info "Received $sig signal while already shutting down..."
return
fi
start=$SECONDS
SHUTDOWN_SIGNAL=$code
# Shutdown handlers must continue through missing processes and failed cleanup
# commands instead of being aborted by errexit.
set +e
touch "$QEMU_END"
echo && info "Received $sig signal, sending shutdown command..."
if ! readQemuPid pid; then
if ! interactive || ! waitQemuPid pid; then
warn "QEMU PID file does not exist?"
finish "$code"
fi
fi
if [ -z "$pid" ] || ! isAlive "$pid"; then
warn "QEMU process with PID $pid does not exist?"
finish "$code"
fi
sendGuestShutdown "$pid"
normalizeTimeout
waitForShutdown "$pid"
finish "$code" finish "$code"
} }
! enabled "$SHUTDOWN" && return 0 enableTrap() {
enabled "$SHUTDOWN" || return 0
# Keep Ctrl-C available to interactive users without installing an unnecessary
# SIGINT handler for background/container execution.
if interactive; then
_trap gracefulShutdown SIGINT
fi
_trap gracefulShutdown SIGTERM SIGHUP SIGABRT SIGQUIT
return 0
}
[ -n "${QEMU_TIMEOUT:-}" ] && TIMEOUT="$QEMU_TIMEOUT" [ -n "${QEMU_TIMEOUT:-}" ] && TIMEOUT="$QEMU_TIMEOUT"
_trap graceful_shutdown SIGTERM SIGHUP SIGABRT SIGQUIT
return 0 return 0
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -Eeuo pipefail
addSystemDisks() {
local boot="$STORAGE/$BASE.boot.img"
local system="$STORAGE/$BASE.system.img"
[ ! -s "$boot" ] && error "Virtual DSM boot-image does not exist ($boot)" && return 81
[ ! -s "$system" ] && error "Virtual DSM system-image does not exist ($system)" && return 82
setOwner "$boot" || warn "failed to set the owner for \"$boot\" !"
setOwner "$system" || warn "failed to set the owner for \"$system\" !"
DISK_OPTS+=$(createDevice "$boot" "$DISK_TYPE" "1" "0xa" "raw" "" "" "synoboot")
DISK_OPTS+=$(createDevice "$system" "$DISK_TYPE" "2" "0xb" "raw" "" "" "synosys")
return 0
}
closeWebserver() {
disabled "${NETWORK:-Y}" && return 0
MSG="Booting DSM instance..."
html "$MSG" || return $?
if enabled "${DHCP:-N}" || disabled "${WEB:-}"; then
return 0
fi
writeAtomic "$WSD_COMMAND" "portal" || return $?
sleep 1.2
stopAllServers
WEB="N"
return 0
}
addSystemDisks || return $?
closeWebserver || return $?
return 0
+172 -50
View File
@@ -1,106 +1,228 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# shellcheck disable=SC2329
set -Eeuo pipefail set -Eeuo pipefail
: "${DHCP:="N"}" : "${DHCP:="N"}"
: "${NETWORK:="Y"}" : "${NETWORK:="Y"}"
[[ "$NETWORK" == [Nn]* ]] && exit 0 cd /run
. utils.sh # Load functions
info () { printf "%b%s%b" "\E[1;34m \E[1;36m" "$1" "\E[0m\n" >&2; } info () { printf "%b%s%b" "\E[1;34m \E[1;36m" "$1" "\E[0m\n" >&2; }
warn () { printf "%b%s%b" "\E[1;33m " "WARNING: $1" "\E[0m\n" >&2; }
error () { printf "%b%s%b" "\E[1;31m " "ERROR: $1" "\E[0m\n" >&2; } error () { printf "%b%s%b" "\E[1;31m " "ERROR: $1" "\E[0m\n" >&2; }
disabled "$NETWORK" && exit 0
file="/run/shm/dsm.url" file="/run/shm/dsm.url"
info="/run/shm/msg.html" msgs="/run/shm/msg.html"
driver="/run/shm/qemu.nic" driver="/run/shm/qemu.nic"
page="/run/shm/index.html" page="/run/shm/index.html"
address="/run/shm/qemu.ip" address="/run/shm/qemu.host"
shutdown="/run/shm/qemu.end" shutdown="/run/shm/qemu.end"
command="/run/shm/status.cmd"
socket="/run/shm/qemu-host-api.sock"
template="/var/www/index.html" template="/var/www/index.html"
url="http://127.0.0.1:2210/read?command=10" url="http://localhost/read?command=10"
resp_err="Guest returned an invalid response:" resp_err="Guest returned an invalid response:"
curl_err="Failed to connect to guest: curl error" curl_err="Failed to connect to guest: curl error"
jq_err="Failed to parse response from guest: jq error" jq_err="Failed to parse response from guest: jq error"
while [ ! -s "$file" ] exitIfShuttingDown() {
do
[ -f "$shutdown" ] && exit 1
return 0
}
queryGuest() {
# Query DSM through the qemu-host sidecar rather than the guest network,
# which may not be configured yet.
{ json=$(curl --unix-socket "$socket" -m 20 -sk "$url"); local rc=$?; } || :
exitIfShuttingDown
if (( rc != 0 )); then
error "$curl_err $rc"
return 1
fi
return 0
}
readJsonField() {
local query="$1"
local result
{ result=$(jq -r "$query" <<< "$json"); local rc=$?; } || :
if (( rc != 0 )); then
error "$jq_err $rc ( $json )"
return 1
fi
if [[ "$result" == "null" ]]; then
error "$resp_err $json"
return 1
fi
printf '%s\n' "$result"
return 0
}
readGuestStatus() {
local result msg
result=$(readJsonField '.status') || return 1
if [[ "$result" != "success" ]]; then
{ msg=$(jq -r '.message // empty' <<< "$json"); local rc=$?; } || :
if (( rc != 0 )); then
error "$jq_err $rc ( $json )"
return 1
fi
error "Guest replied $result: $msg"
return 1
fi
return 0
}
readGuestPort() {
port=$(readJsonField '.data.data.dsm_setting.data.http_port') || return 1
[ -z "$port" ] && return 1
return 0
}
readGuestIp() {
ip=$(readJsonField '.data.data.ip.data[] | select(.name=="eth0" and has("ip")) | .ip | select(test("^[0-9]+\\."))') || return 1
[ -z "$ip" ] && return 1
return 0
}
writeDsmLocation() {
echo "$ip:$port" > "$file"
return 0
}
pollGuestLocation() {
# Keep polling until the guest reports a usable address, but stop promptly
# when container shutdown begins.
while [ ! -s "$file" ]; do
# Check if not shutting down # Check if not shutting down
[ -f "$shutdown" ] && exit 1 exitIfShuttingDown
sleep 3 sleep 3
[ -f "$shutdown" ] && exit 1 exitIfShuttingDown
[ -s "$file" ] && break [ -s "$file" ] && break
# Retrieve network info from guest VM # Retrieve network info from guest VM
{ json=$(curl -m 20 -sk "$url"); rc=$?; } || : queryGuest || continue
readGuestStatus || continue
readGuestPort || continue
readGuestIp || continue
writeDsmLocation
[ -f "$shutdown" ] && exit 1 done
(( rc != 0 )) && error "$curl_err $rc" && continue
{ result=$(echo "$json" | jq -r '.status'); rc=$?; } || : return 0
(( rc != 0 )) && error "$jq_err $rc ( $json )" && continue }
[[ "$result" == "null" ]] && error "$resp_err $json" && continue
if [[ "$result" != "success" ]] ; then checkAddressConflict() {
{ msg=$(echo "$json" | jq -r '.message'); rc=$?; } || :
error "Guest replied $result: $msg" && continue
fi
{ port=$(echo "$json" | jq -r '.data.data.dsm_setting.data.http_port'); rc=$?; } || : local guest_ip="${location%:*}"
(( rc != 0 )) && error "$jq_err $rc ( $json )" && continue local container_ip=""
[[ "$port" == "null" ]] && error "$resp_err $json" && continue
[ -z "$port" ] && continue
{ ip=$(echo "$json" | jq -r '.data.data.ip.data[] | select((.name=="eth0") and has("ip")).ip'); rc=$?; } || : [ -s "$address" ] && container_ip=$(<"$address")
(( rc != 0 )) && error "$jq_err $rc ( $json )" && continue [ -z "$container_ip" ] && return 0
[[ "$ip" == "null" ]] && error "$resp_err $json" && continue [[ "$guest_ip" != "$container_ip" ]] && return 0
[ -z "$ip" ] && continue warn "DSM is using the same IP as the container, this will cause connectivity issues."
echo "$ip:$port" > $file warn "change the container's macvlan IP or assign DSM a different address in your router."
done return 0
}
[ -f "$shutdown" ] && exit 1 writeDhcpPage() {
location=$(<"$file") local html
if [[ "$DHCP" == [Yy1]* ]]; then
msg="http://$location" msg="http://$location"
title="<title>Virtual DSM</title>" local title="<title>Virtual DSM</title>"
body="The location of DSM is <a href='http://$location'>http://$location</a>" local body="The location of DSM is <a href='http://$location'>http://$location</a>"
script="<script>setTimeout(function(){ window.location.assign('http://$location'); }, 3000);</script>" local script="<script>setTimeout(function(){ window.location.assign('http://$location'); }, 3000);</script>"
HTML=$(<"$template") html=$(<"$template")
HTML="${HTML/\[1\]/$title}" html="${html/\[1\]/$title}"
HTML="${HTML/\[2\]/$script}" html="${html/\[2\]/$script}"
HTML="${HTML/\[3\]/$body}" html="${html/\[3\]/$body}"
HTML="${HTML/\[4\]/}" html="${html/\[4\]/}"
HTML="${HTML/\[5\]/}" html="${html/\[5\]/}"
echo "$HTML" > "$page" echo "$html" > "$page"
echo "$body" > "$info" echo "$body" > "$msgs"
writeAtomic "$command" "portal http://$location"
else return 0
}
buildStaticMessage() {
local nic ip
nic=$(<"$driver") nic=$(<"$driver")
ip=$(<"$address") ip=$(<"$address")
port="${location##*:}" local port="${location##*:}"
# NAT and user-mode networking are reached through a forwarded host port;
# macvlan exposes DSM directly on the container-facing LAN address.
if [[ "${nic,,}" != "macvlan" ]]; then if [[ "${nic,,}" != "macvlan" ]]; then
msg="port $port" msg="port $port"
else else
msg="http://$ip:$port" msg="http://$ip:$port"
fi fi
return 0
}
printLoginMessage() {
echo "" >&2
info "-----------------------------------------------------------"
info " You can now login to DSM at $msg"
info "-----------------------------------------------------------"
echo "" >&2
return 0
}
pollGuestLocation
exitIfShuttingDown
location=$(<"$file")
if enabled "$DHCP"; then
checkAddressConflict
writeDhcpPage
else
buildStaticMessage
fi fi
echo "" >&2 printLoginMessage
info "-----------------------------------------------------------"
info " You can now login to DSM at $msg"
info "-----------------------------------------------------------"
echo "" >&2
exit 0 exit 0
-144
View File
@@ -1,144 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Docker environment variables
: "${HOST_CPU:=""}"
: "${CPU_FLAGS:=""}"
: "${CPU_MODEL:=""}"
HOST_CPU=$(strip "$HOST_CPU")
CPU_FLAGS=$(strip "$CPU_FLAGS")
CPU_MODEL=$(strip "$CPU_MODEL")
CLOCKSOURCE="tsc"
[[ "${ARCH,,}" == "arm64" ]] && CLOCKSOURCE="arch_sys_counter"
CLOCK="/sys/devices/system/clocksource/clocksource0/current_clocksource"
if [ ! -f "$CLOCK" ]; then
warn "file \"$CLOCK\" cannot be found?"
else
result=$(<"$CLOCK")
result="${result//[![:print:]]/}"
case "${result,,}" in
"${CLOCKSOURCE,,}" ) ;;
"kvm-clock" ) info "Nested KVM virtualization detected.." ;;
"hyperv_clocksource_tsc_page" ) info "Nested Hyper-V virtualization detected.." ;;
"hpet" ) warn "unsupported clock source detected: '$result'. Please set host clock source to '$CLOCKSOURCE'." ;;
*) warn "unexpected clock source detected: '$result'. Please set host clock source to '$CLOCKSOURCE'." ;;
esac
fi
flags=$(sed -ne '/^flags/s/^.*: //p' /proc/cpuinfo)
if ! disabled "$KVM"; then
CPU_FEATURES="kvm=on,l3-cache=on,+hypervisor"
KVM_OPTS=",accel=kvm -enable-kvm -global kvm-pit.lost_tick_policy=discard"
if ! grep -qw "sse4_2" <<< "$flags"; then
error "Your CPU does not have the SSE4 instruction set that Virtual DSM requires!"
! enabled "$DEBUG" && exit 88
fi
if [ -z "$CPU_MODEL" ]; then
CPU_MODEL="host"
CPU_FEATURES+=",migratable=no"
fi
if grep -qw "svm" <<< "$flags"; then
# AMD processor
if grep -qw "tsc_scale" <<< "$flags"; then
CPU_FEATURES+=",+invtsc"
fi
else
# Intel processor
vmx=$(sed -ne '/^vmx flags/s/^.*: //p' /proc/cpuinfo)
if grep -qw "tsc_scaling" <<< "$vmx"; then
CPU_FEATURES+=",+invtsc"
fi
fi
else
KVM_OPTS=""
CPU_FEATURES="l3-cache=on,+hypervisor"
if [[ "$ARCH" == "amd64" ]]; then
KVM_OPTS=" -accel tcg,thread=multi"
fi
if [ -z "$CPU_MODEL" ]; then
if [[ "$ARCH" == "amd64" ]]; then
CPU_MODEL="max"
CPU_FEATURES+=",migratable=no"
else
CPU_MODEL="qemu64"
fi
fi
CPU_FEATURES+=",+ssse3,+sse4.1,+sse4.2"
fi
if [[ "$ARGUMENTS" == *"-cpu host,"* ]]; then
args="${ARGUMENTS} "
prefix="${args/-cpu host,*/}"
suffix="${args/*-cpu host,/}"
param="${suffix%% *}"
suffix="${suffix#* }"
args="${prefix}${suffix}"
ARGUMENTS="${args::-1}"
if [ -z "$CPU_FLAGS" ]; then
CPU_FLAGS="$param"
else
CPU_FLAGS+=",$param"
fi
else
if [[ "$ARGUMENTS" == *"-cpu host"* ]]; then
ARGUMENTS="${ARGUMENTS//-cpu host/}"
fi
fi
if [ -z "$CPU_FLAGS" ]; then
if [ -z "$CPU_FEATURES" ]; then
CPU_FLAGS="$CPU_MODEL"
else
CPU_FLAGS="$CPU_MODEL,$CPU_FEATURES"
fi
else
if [ -z "$CPU_FEATURES" ]; then
CPU_FLAGS="$CPU_MODEL,$CPU_FLAGS"
else
CPU_FLAGS="$CPU_MODEL,$CPU_FEATURES,$CPU_FLAGS"
fi
fi
if [ -z "$HOST_CPU" ]; then
[[ "${CPU,,}" != "unknown" ]] && HOST_CPU="$CPU"
fi
if [ -n "$HOST_CPU" ]; then
HOST_CPU="${HOST_CPU%%,*},,"
else
HOST_CPU="QEMU, Virtual CPU,"
if [ "$ARCH" == "amd64" ]; then
HOST_CPU+=" X86_64"
else
HOST_CPU+=" $ARCH"
fi
fi
return 0
-45
View File
@@ -1,45 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
info="/run/shm/msg.html"
escape () {
local s
s=${1//&/\&amp;}
s=${s//</\&lt;}
s=${s//>/\&gt;}
s=${s//'"'/\&quot;}
printf -- %s "$s"
return 0
}
path="$1"
total="$2"
body=$(escape "$3")
if [[ "$body" == *"..." ]]; then
body="<p class=\"loading\">${body::-3}</p>"
fi
while true
do
if [ ! -s "$path" ] && [ ! -d "$path" ]; then
bytes="0"
else
bytes=$(du -sb "$path" | cut -f1)
fi
if (( bytes > 4096 )); then
if [ -z "$total" ] || [[ "$total" == "0" ]] || [ "$bytes" -gt "$total" ]; then
size=$(numfmt --to=iec --suffix=B "$bytes" | sed -r 's/([A-Z])/ \1/')
else
size="$(echo "$bytes" "$total" | awk '{printf "%.1f", $1 * 100 / $2}')"
size="$size%"
fi
[[ "$size" != "0.0%" ]] && echo "${body//(\[P\])/($size)}"> "$info"
fi
sleep 1 & wait $!
done
-240
View File
@@ -1,240 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
trap 'error "Status $? while: $BASH_COMMAND (line $LINENO/$BASH_LINENO)"' ERR
enabled "${TRACE:-}" && set -o functrace && trap 'echo "# $BASH_COMMAND" >&2' DEBUG
[ ! -f "/run/entry.sh" ] && error "Script must be run inside the container!" && exit 11
[ "$(id -u)" -ne "0" ] && error "Script must be executed with root privileges." && exit 12
# Docker environment variables
: "${TZ:=""}" # System timezone
: "${KVM:="Y"}" # KVM acceleration
: "${DEBUG:="N"}" # Disable debugging mode
: "${COUNTRY:=""}" # Country code for mirror
: "${ALLOCATE:=""}" # Preallocate diskspace
: "${ARGUMENTS:=""}" # Extra QEMU parameters
: "${CPU_CORES:="2"}" # Amount of CPU cores
: "${RAM_SIZE:="2G"}" # Maximum RAM amount
: "${RAM_CHECK:="Y"}" # Check available RAM
: "${DISK_SIZE:="16G"}" # Initial data disk size
: "${STORAGE:="/storage"}" # Storage folder location
# Sanitize variables
TZ=$(strip "$TZ")
STORAGE=$(strip "$STORAGE")
COUNTRY=$(strip "$COUNTRY")
DISK_SIZE=$(strip "$DISK_SIZE")
# Helper variables
ROOTLESS="N"
PRIVILEGED="N"
ENGINE="Docker"
PROCESS="${APP,,}"
PROCESS="${PROCESS// /-}"
if [ -f "/run/.containerenv" ]; then
ENGINE="${container:-}"
if [[ "${ENGINE,,}" == *"podman"* ]]; then
ROOTLESS="Y"
ENGINE="Podman"
else
[ -z "$ENGINE" ] && ENGINE="Kubernetes"
fi
fi
echo " Starting $APP for $ENGINE v$(</etc/version)..."
echo " For support visit $SUPPORT"
# Get the capability bounding set
CAP_BND=$(grep '^CapBnd:' /proc/$$/status | awk '{print $2}')
CAP_BND=$(printf "%d" "0x${CAP_BND}")
# Get the last capability number
LAST_CAP=$(cat /proc/sys/kernel/cap_last_cap)
# Calculate the maximum capability value
MAX_CAP=$(((1 << (LAST_CAP + 1)) - 1))
if [ "${CAP_BND}" -eq "${MAX_CAP}" ]; then
ROOTLESS="N"
PRIVILEGED="Y"
fi
INFO="/run/shm/msg.html"
PAGE="/run/shm/index.html"
TEMPLATE="/var/www/index.html"
FOOTER1="$APP for $ENGINE v$(</etc/version)"
FOOTER2="<a href='$SUPPORT'>$SUPPORT</a>"
SOCKETS=1
CPU=$(cpu)
SYS=$(uname -r)
HOST=$(hostname -s)
KERNEL=$(echo "$SYS" | cut -b 1)
MINOR=$(echo "$SYS" | cut -d '.' -f2)
ARCH=$(dpkg --print-architecture)
CORES=$(grep -c '^processor' /proc/cpuinfo)
if grep -qi "socket(s)" <<< "$(lscpu)"; then
SOCKETS=$(lscpu | grep -m 1 -i 'socket(s)' | awk '{print $2}')
[ -z "${SOCKETS##*[!0-9]*}" ] && SOCKETS=1
[ "$SOCKETS" -lt "1" ] && SOCKETS=1
fi
CPU_CORES=$(strip "$CPU_CORES")
[ -z "$CPU_CORES" ] && CPU_CORES=2
[[ "${CPU_CORES,,}" == "max" ]] && CPU_CORES="$CORES"
[[ "${CPU_CORES,,}" == "half" ]] && CPU_CORES=$(( CORES / 2 ))
[ -z "${CPU_CORES##*[!0-9]*}" ] && error "Invalid amount of CPU_CORES: $CPU_CORES" && exit 15
[ "$CPU_CORES" -lt "1" ] && CPU_CORES=1
if [ "$CPU_CORES" -gt "$CORES" ]; then
warn "The amount for CPU_CORES (${CPU_CORES}) exceeds the amount of logical cores available (${CORES}) and will be limited."
CPU_CORES="$CORES"
fi
# Check system
QEMU_DIR="/run/shm"
if [ ! -d "/dev/shm" ]; then
error "Directory /dev/shm not found!" && exit 14
else
[ ! -d "$QEMU_DIR" ] && ln -s /dev/shm "$QEMU_DIR"
fi
QEMU_PID="$QEMU_DIR/qemu.pid"
# Check folder
if [[ "${STORAGE,,}" != "/storage" ]]; then
mkdir -p "$STORAGE"
fi
if [ ! -d "$STORAGE" ]; then
error "Storage folder ($STORAGE) not found!" && exit 13
fi
if [ ! -w "$STORAGE" ]; then
msg="Storage folder ($STORAGE) is not writeable!"
msg+=" If SELinux is active, you need to add the \":Z\" flag to the bind mount."
error "$msg" && exit 13
fi
# Check filesystem
FS=$(stat -f -c %T "$STORAGE")
if [[ "${FS,,}" == "ecryptfs" || "${FS,,}" == "tmpfs" ]]; then
DISK_IO="threads"
DISK_CACHE="writeback"
fi
# Read memory
RAM_AVAIL=$(free -b | grep -m 1 Mem: | awk '{print $7}')
RAM_TOTAL=$(free -b | grep -m 1 Mem: | awk '{print $2}')
RAM_SPARE=500000000
RAM_MINIMUM=136314880
RAM_SIZE=$(strip "$RAM_SIZE")
RAM_SIZE="${RAM_SIZE// /}"
[ -z "$RAM_SIZE" ] && RAM_SIZE="2G"
if [[ "${RAM_SIZE,,}" != "max" && "${RAM_SIZE,,}" != "half" ]]; then
if [ -z "${RAM_SIZE//[0-9. ]}" ]; then
[ "${RAM_SIZE%%.*}" -lt "130" ] && RAM_SIZE="${RAM_SIZE}G" || RAM_SIZE="${RAM_SIZE}M"
fi
RAM_SIZE=$(echo "${RAM_SIZE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
! numfmt --from=iec "$RAM_SIZE" &>/dev/null && error "Invalid RAM_SIZE: $RAM_SIZE" && exit 16
wanted=$(numfmt --from=iec "$RAM_SIZE")
[ "$wanted" -lt "$RAM_MINIMUM" ] && error "RAM_SIZE is too low: $RAM_SIZE" && exit 16
fi
# Print system info
SYS="${SYS/-generic/}"
FS="${FS/UNKNOWN //}"
FS="${FS/ext2\/ext3/ext4}"
FS=$(echo "$FS" | sed 's/[)(]//g')
SPACE=$(df --output=avail -B 1 "$STORAGE" | tail -n 1)
SPACE_GB=$(formatBytes "$SPACE" "down")
AVAIL_MEM=$(formatBytes "$RAM_AVAIL" "down")
TOTAL_MEM=$(formatBytes "$RAM_TOTAL" "up")
echo " CPU: ${CPU} | RAM: ${AVAIL_MEM/ GB/}/$TOTAL_MEM | DISK: $SPACE_GB (${FS}) | KERNEL: ${SYS}..."
echo
# Check KVM support
if [[ "${PLATFORM,,}" == "x64" ]]; then
TARGET="amd64"
else
TARGET="arm64"
fi
if disabled "$KVM"; then
warn "KVM acceleration is disabled, this will cause the machine to run about 10 times slower!"
else
if [[ "${ARCH,,}" != "$TARGET" ]]; then
KVM="N"
warn "your CPU architecture is ${ARCH^^} and cannot provide KVM acceleration for ${PLATFORM^^} instructions, so the machine will run about 10 times slower."
fi
fi
if ! disabled "$KVM"; then
KVM_ERR=""
if [ ! -e /dev/kvm ]; then
KVM_ERR="(/dev/kvm is missing)"
else
if ! sh -c 'echo -n > /dev/kvm' &> /dev/null; then
KVM_ERR="(/dev/kvm is unwriteable)"
else
if [[ "${PLATFORM,,}" == "x64" ]]; then
flags=$(sed -ne '/^flags/s/^.*: //p' /proc/cpuinfo)
if ! grep -qw "vmx\|svm" <<< "$flags"; then
KVM_ERR="(not enabled in BIOS)"
fi
if ! grep -qw "sse4_2" <<< "$flags"; then
error "Your CPU does not have the SSE4 instruction set that Virtual DSM requires!"
! enabled "$DEBUG" && exit 88
fi
fi
fi
fi
if [ -n "$KVM_ERR" ]; then
KVM="N"
if [[ "$OSTYPE" =~ ^darwin ]]; then
warn "you are using macOS which has no KVM support, so the machine will run about 10 times slower."
else
kernel=$(uname -a)
case "${kernel,,}" in
*"microsoft"* )
error "Please bind '/dev/kvm' as a volume in the optional container settings when using Docker Desktop." ;;
*"synology"* )
error "Please make sure that Synology VMM (Virtual Machine Manager) is installed and that '/dev/kvm' is binded to this container." ;;
*)
error "KVM acceleration is not available $KVM_ERR, this will cause the machine to run about 10 times slower."
error "See the FAQ for possible causes, or disable acceleration by adding the \"KVM=N\" variable (not recommended)." ;;
esac
! enabled "$DEBUG" && exit 88
fi
fi
fi
# Cleanup files
rm -f "$QEMU_DIR"/dsm.url
rm -f "$QEMU_DIR"/qemu.* "$QEMU_DIR"/*.pid "$QEMU_DIR"/*.sock
# Cleanup dirs
rm -rf /tmp/dsm
rm -rf "$STORAGE/tmp"
return 0
+133 -33
View File
@@ -4,6 +4,7 @@ set -Eeuo pipefail
# Docker environment variables # Docker environment variables
: "${HOST_MAC:=""}" : "${HOST_MAC:=""}"
: "${HOST_CPU:=""}"
: "${HOST_DEBUG:=""}" : "${HOST_DEBUG:=""}"
: "${HOST_MODEL:=""}" : "${HOST_MODEL:=""}"
: "${HOST_SERIAL:=""}" : "${HOST_SERIAL:=""}"
@@ -11,68 +12,167 @@ set -Eeuo pipefail
# Sanitize variables # Sanitize variables
HOST_MAC=$(strip "$HOST_MAC") HOST_MAC=$(strip "$HOST_MAC")
HOST_CPU=$(strip "$HOST_CPU")
HOST_MODEL=$(strip "$HOST_MODEL") HOST_MODEL=$(strip "$HOST_MODEL")
HOST_SERIAL=$(strip "$HOST_SERIAL") HOST_SERIAL=$(strip "$HOST_SERIAL")
GUEST_SERIAL=$(strip "$GUEST_SERIAL") GUEST_SERIAL=$(strip "$GUEST_SERIAL")
if [ -n "$HOST_MAC" ]; then HOST_PID="$QEMU_DIR/host.pid"
HOST_API_SOCKET="$QEMU_DIR/qemu-host-api.sock"
HOST_AGENT_SOCKET="$QEMU_DIR/qemu-host-agent.sock"
validateHostMac() {
if [ -z "$HOST_MAC" ]; then
return 0
fi
HOST_MAC="${HOST_MAC//-/:}" HOST_MAC="${HOST_MAC//-/:}"
if [[ ${#HOST_MAC} == 12 ]]; then if [[ ${#HOST_MAC} == 12 ]]; then
m="$HOST_MAC" local m="$HOST_MAC"
HOST_MAC="${m:0:2}:${m:2:2}:${m:4:2}:${m:6:2}:${m:8:2}:${m:10:2}" HOST_MAC="${m:0:2}:${m:2:2}:${m:4:2}:${m:6:2}:${m:8:2}:${m:10:2}"
fi fi
if [[ ${#HOST_MAC} != 17 ]]; then if [[ ${#HOST_MAC} != 17 ]]; then
error "Invalid HOST_MAC address: '$HOST_MAC', should be 12 or 17 digits long!" && exit 28 error "Invalid HOST_MAC address: '$HOST_MAC', should be 12 or 17 digits long!"
exit 28
fi fi
fi return 0
}
HOST_PID="$QEMU_DIR/host.pid" configureHostCpuName() {
HOST_ARGS=() if [ -z "$HOST_CPU" ]; then
HOST_ARGS+=("-cpu=$CPU_CORES") [[ "${CPU,,}" != "unknown" ]] && HOST_CPU="$CPU"
HOST_ARGS+=("-cpu_arch=$HOST_CPU") fi
[ -n "$HOST_MAC" ] && HOST_ARGS+=("-mac=$HOST_MAC") if [ -n "$HOST_CPU" ]; then
[ -n "$HOST_MODEL" ] && HOST_ARGS+=("-model=$HOST_MODEL") # qemu-host expects a comma-separated CPU description with empty family
[ -n "$HOST_SERIAL" ] && HOST_ARGS+=("-hostsn=$HOST_SERIAL") # and suffix fields, not QEMU's -cpu syntax.
[ -n "$GUEST_SERIAL" ] && HOST_ARGS+=("-guestsn=$GUEST_SERIAL") HOST_CPU="${HOST_CPU%%,*},,"
else
HOST_CPU="QEMU, Virtual CPU,"
if [ "$ARCH" == "amd64" ]; then
HOST_CPU+=" X86_64"
else
HOST_CPU+=" $ARCH"
fi
fi
if enabled "$HOST_DEBUG"; then return 0
}
buildHostArguments() {
# qemu-host is a sidecar that bridges DSM's proprietary serial agent to
# Unix sockets used by shutdown and post-boot discovery helpers.
HOST_ARGS=()
HOST_ARGS+=("-cpu=$CPU_CORES")
HOST_ARGS+=("-cpu_arch=$HOST_CPU")
HOST_ARGS+=("-api=$HOST_API_SOCKET")
HOST_ARGS+=("-addr=$HOST_AGENT_SOCKET")
[ -n "$HOST_MAC" ] && HOST_ARGS+=("-mac=$HOST_MAC")
[ -n "$HOST_MODEL" ] && HOST_ARGS+=("-model=$HOST_MODEL")
[ -n "$HOST_SERIAL" ] && HOST_ARGS+=("-hostsn=$HOST_SERIAL")
[ -n "$GUEST_SERIAL" ] && HOST_ARGS+=("-guestsn=$GUEST_SERIAL")
return 0
}
startHostBinary() {
local pid
# Remove stale sockets and pid state before starting the sidecar; a Unix
# socket path cannot be rebound while an old filesystem entry remains.
rm -f -- "$HOST_PID" "$HOST_API_SOCKET" "$HOST_AGENT_SOCKET" || return 1
if enabled "$HOST_DEBUG"; then
set -x set -x
./host.bin "${HOST_ARGS[@]}" & ./host.bin "${HOST_ARGS[@]}" &
{ set +x; } 2>/dev/null { set +x; } 2>/dev/null
echo "$!" > "$HOST_PID" pid=$!
echo echo
else else
./host.bin "${HOST_ARGS[@]}" >/dev/null & ./host.bin "${HOST_ARGS[@]}" >/dev/null &
echo "$!" > "$HOST_PID" pid=$!
fi fi
cnt=0 printf '%s\n' "$pid" > "$HOST_PID"
sleep 0.2
return 0
}
waitForSocket() {
local socket="$1"
local exit_code="$2"
local timeout=5 pid
local deadline=$((SECONDS + timeout))
# Do not start QEMU until both sidecar sockets are ready; otherwise the
# VirtIO serial channel or API client may race initial creation.
while [ ! -S "$socket" ]; do
if ! readPidFile pid "$HOST_PID" || ! isAlive "$pid"; then
error "qemu-host exited unexpectedly!"
exit "$exit_code"
fi
if (( SECONDS >= deadline )); then
error "Failed to create qemu-host socket: $socket"
exit "$exit_code"
fi
while ! nc -z -w2 127.0.0.1 "$COM_PORT" > /dev/null 2>&1; do
sleep 0.1 sleep 0.1
cnt=$((cnt + 1)) done
(( cnt > 50 )) && error "Failed to connect to qemu-host.." && exit 58
done
cnt=0 return 0
}
while ! nc -z -w2 127.0.0.1 "$CHR_PORT" > /dev/null 2>&1; do configureSerialPorts() {
sleep 0.1
cnt=$((cnt + 1))
(( cnt > 50 )) && error "Failed to connect to qemu-host.." && exit 59
done
# Configure serial ports local bus
SERIAL_OPTS="-serial mon:stdio \ bus=$(getPciBus)
-device virtio-serial-pci,id=virtio-serial0,bus=pcie.0,addr=0x3 \
-chardev socket,id=charchannel0,host=127.0.0.1,port=$CHR_PORT,reconnect=10 \ # Managed interactive mode separates the console and QEMU monitor into
# reconnecting sockets; other runs keep the simple combined stdio monitor.
if enabled "${SHUTDOWN:-Y}" && interactive; then
CONSOLE_SOCKET="$QEMU_DIR/console.sock"
MONITOR_SOCKET="$QEMU_DIR/monitor.sock"
SERIAL_OPTS="-chardev socket,id=console0,path=$CONSOLE_SOCKET,reconnect-ms=1000 \
-serial chardev:console0 \
-chardev socket,id=monitor0,path=$MONITOR_SOCKET,server=on,wait=off \
-mon chardev=monitor0,mode=readline"
else
SERIAL_OPTS="-serial mon:stdio"
fi
SERIAL_OPTS+=" \
-device virtio-serial-pci,id=virtio-serial0,bus=$bus,addr=0x3 \
-chardev socket,id=charchannel0,path=$HOST_AGENT_SOCKET,reconnect-ms=1000 \
-device virtserialport,bus=virtio-serial0.0,nr=1,chardev=charchannel0,id=channel0,name=vchannel" -device virtserialport,bus=virtio-serial0.0,nr=1,chardev=charchannel0,id=channel0,name=vchannel"
return 0
}
validateHostMac
configureHostCpuName
buildHostArguments
startHostBinary
waitForSocket "$HOST_API_SOCKET" 58
waitForSocket "$HOST_AGENT_SOCKET" 59
configureSerialPorts
return 0 return 0
-47
View File
@@ -1,47 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
: "${COM_PORT:="2210"}" # Comm port
: "${WEB_PORT:="5000"}" # Webserver port
: "${CHR_PORT:="12345"}" # Character port
: "${WSD_PORT:="8004"}" # Websockets port
# Sanitize port variables
COM_PORT=$(strip "$COM_PORT")
WEB_PORT=$(strip "$WEB_PORT")
CHR_PORT=$(strip "$CHR_PORT")
WSD_PORT=$(strip "$WSD_PORT")
WEB_PID="/run/nginx.pid"
WSD_PID="$QEMU_DIR/websocketd.pid"
cp -r /var/www/* "$QEMU_DIR"
rm -f "$WSD_PID" "$WEB_PID"
html "Starting $APP for $ENGINE..."
if ! disabled "${WEB:-}"; then
mkdir -p /etc/nginx/sites-enabled
cp /etc/nginx/default.conf /etc/nginx/sites-enabled/web.conf
sed -i "s/listen 5000 default_server;/listen $WEB_PORT default_server;/g" /etc/nginx/sites-enabled/web.conf
sed -i "s/proxy_pass http:\/\/127.0.0.1:8004\/;/proxy_pass http:\/\/127.0.0.1:$WSD_PORT\/;/g" /etc/nginx/sites-enabled/web.conf
# shellcheck disable=SC2143
if [ -f /proc/net/if_inet6 ] && [[ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null)" != "1" ]] && [ -n "$(ifconfig -a | grep inet6)" ]; then
sed -i "s/listen $WEB_PORT default_server;/listen [::]:$WEB_PORT default_server ipv6only=off;/g" /etc/nginx/sites-enabled/web.conf
fi
# Start webserver
nginx -e stderr
# Start websocket server
websocketd --address 127.0.0.1 --port="$WSD_PORT" /run/socket.sh >/var/log/websocketd.log &
echo "$!" > "$WSD_PID"
fi
return 0
+50 -10
View File
@@ -2,14 +2,20 @@
set -Eeuo pipefail set -Eeuo pipefail
lastmsg="" lastmsg=""
path="/run/shm/msg.html" lastcmd=""
status="/run/shm/msg.html"
command="/run/shm/status.cmd"
dir=$(dirname -- "$status")
status_name=$(basename -- "$status")
command_name=$(basename -- "$command")
refresh() { # websocketd clients use s: for status updates and c: for control commands.
refreshStatus() {
[ ! -f "$path" ] && return 0 [ ! -f "$status" ] && return 0
[ ! -s "$path" ] && return 0 [ ! -s "$status" ] && return 0
msg=$(< "$path") msg=$(< "$status") || return 0
msg="${msg%$'\n'}" msg="${msg%$'\n'}"
[ -z "$msg" ] && return 0 [ -z "$msg" ] && return 0
@@ -17,15 +23,49 @@ refresh() {
lastmsg="$msg" lastmsg="$msg"
echo "s: $msg" echo "s: $msg"
return 0 return 0
} }
refresh refreshCommand() {
inotifywait -m "$path" | [ ! -f "$command" ] && return 0
while read -r fp event fn; do [ ! -s "$command" ] && return 0
cmd=$(< "$command") || return 0
cmd="${cmd%$'\n'}"
[ -z "$cmd" ] && return 0
[[ "$cmd" == "$lastcmd" ]] && return 0
lastcmd="$cmd"
echo "c: $cmd"
return 0
}
refreshStatus
refreshCommand
# Watch the directory because status and command updates use atomic rename.
inotifywait \
-m -q \
-e close_write,moved_to \
--format '%e %f' \
"$dir" |
while read -r event file; do
case "$file" in
"$status_name" )
case "${event,,}" in case "${event,,}" in
"modify"* ) refresh ;; "close_write"* | "moved_to"* )
"delete_self" ) echo "c: vnc" ;; refreshStatus ;;
esac ;;
"$command_name" )
case "${event,,}" in
"close_write"* | "moved_to"* )
refreshCommand ;;
esac ;;
esac esac
done done
-6
View File
@@ -1,6 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# You can override this hook to execute a script before startup!
return 0
-370
View File
@@ -1,370 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Helper functions
info () { printf "%b%s%b" "\E[1;34m \E[1;36m" "${1:-}" "\E[0m\n"; }
error () { printf "%b%s%b" "\E[1;31m " "ERROR: ${1:-}" "\E[0m\n" >&2; }
warn () { printf "%b%s%b" "\E[1;31m " "Warning: ${1:-}" "\E[0m\n" >&2; }
strip() {
local value="${1:-}"
# Remove surrounding whitespace
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
# Remove leading/trailing single/double quotes
value="${value%\"}"
value="${value#\"}"
value="${value%\'}"
value="${value#\'}"
# Remove surrounding whitespace again
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
printf '%s' "$value"
}
enabled() {
case "$(strip "${1:-}")" in
Y|y|YES|Yes|yes|TRUE|True|true|1|ON|On|on) return 0 ;;
*) return 1 ;;
esac
}
disabled() {
case "$(strip "${1:-}")" in
N|n|NO|No|no|FALSE|False|false|0|OFF|Off|off) return 0 ;;
*) return 1 ;;
esac
}
formatBytes() {
local result
result=$(numfmt --to=iec --suffix=B "$1" | sed -r 's/([A-Z])/ \1/' | sed 's/ B/ bytes/g;')
local unit="${result//[0-9. ]}"
result="${result//[a-zA-Z ]/}"
if [[ "${2:-}" == "up" ]]; then
if [[ "$result" == *"."* ]]; then
result="${result%%.*}"
result=$((result+1))
fi
else
if [[ "${2:-}" == "down" ]]; then
result="${result%%.*}"
fi
fi
echo "$result $unit"
return 0
}
isAlive() {
local pid="$1"
[ -z "$pid" ] && return 1
if kill -0 "$pid" 2>/dev/null; then
return 0
fi
return 1
}
waitPid() {
local i=0
local pid="$1"
local timeout="${2:-10}"
while [ -n "$pid" ] && isAlive "$pid"; do
sleep 0.2
i=$((i + 1))
(( i >= timeout * 5 )) && return 1
done
return 0
}
waitPidFile() {
local i=0
local pid=""
local file="$1"
local timeout="${2:-10}"
[ ! -s "$file" ] && return 0
! read -r pid <"$file" && return 0
[ -z "$pid" ] && return 0
while [ -s "$file" ] && isAlive "$pid"; do
sleep 0.2
i=$((i + 1))
(( i >= timeout * 5 )) && return 1
done
rm -f -- "$file"
return 0
}
pKill() {
local pid="$1"
local timeout="${2:-10}"
{ kill -15 -- "$pid" || :; } 2>/dev/null
if ! waitPid "$pid" "$timeout"; then
warn "Timed out while waiting for PID $pid"
fi
return 0
}
fWait() {
local i=0
local name="$1"
local timeout="${2:-10}"
[ -z "$name" ] && return 0
while pgrep -f -l "$name" >/dev/null; do
sleep 0.2
i=$((i + 1))
if (( i >= timeout * 5 )); then
warn "Timed out while waiting for process: $name"
break
fi
done
return 0
}
fKill() {
local name="$1"
local timeout="${2:-10}"
[ -z "$name" ] && return 0
{ pkill -f "$name" || :; } 2>/dev/null
fWait "$name" "$timeout"
return 0
}
sKill() {
local pid=""
local file="$1"
[ ! -s "$file" ] && return 0
! read -r pid <"$file" && return 0
[ -z "$pid" ] && return 0
if isAlive "$pid"; then
{ kill -15 -- "$pid" || :; } 2>/dev/null
fi
return 0
}
mKill() {
local timeout=10
local files=("$@")
for file in "${files[@]}"; do
sKill "$file"
done
for file in "${files[@]}"; do
if ! waitPidFile "$file" "$timeout"; then
warn "Timed out while waiting for PID file: $file"
fi
done
return 0
}
setOwner() {
local file="$1"
local dir uid gid
[ ! -f "$file" ] && return 1
dir=$(dirname -- "$file")
uid=$(stat -c '%u' "$dir")
gid=$(stat -c '%g' "$dir")
! chown "$uid:$gid" "$file" && return 1
return 0
}
makeDir() {
local path="$1"
local dir uid gid
[ -d "$path" ] && return 0
! mkdir -p "$path" && return 1
dir=$(dirname -- "$path")
uid=$(stat -c '%u' "$dir")
gid=$(stat -c '%g' "$dir")
! chown "$uid:$gid" "$path" && return 1
return 0
}
escape () {
local s
s=${1//&/\&amp;}
s=${s//</\&lt;}
s=${s//>/\&gt;}
s=${s//'"'/\&quot;}
printf -- %s "$s"
return 0
}
html() {
local title
local body
local script
local footer
title=$(escape "$APP")
title="<title>$title</title>"
footer=$(escape "$FOOTER1")
body=$(escape "$1")
if [[ "$body" == *"..." ]]; then
body="<p class=\"loading\">${body/.../}</p>"
fi
[ -n "${2:-}" ] && script="$2" || script=""
local HTML
HTML=$(<"$TEMPLATE")
HTML="${HTML/\[1\]/$title}"
HTML="${HTML/\[2\]/$script}"
HTML="${HTML/\[3\]/$body}"
HTML="${HTML/\[4\]/$footer}"
HTML="${HTML/\[5\]/$FOOTER2}"
echo "$HTML" > "$PAGE"
echo "$body" > "$INFO"
return 0
}
cpu() {
local ret
local cpu=""
ret=$(lscpu)
if grep -qi "model name" <<< "$ret"; then
cpu=$(echo "$ret" | grep -m 1 -i 'model name' | cut -f 2 -d ":" | awk '{$1=$1}1' | sed 's# @.*##g' | sed s/"(R)"//g | sed 's/[^[:alnum:] ]\+/ /g' | sed 's/ */ /g')
fi
if [ -z "${cpu// /}" ] && grep -qi "model:" <<< "$ret"; then
cpu=$(echo "$ret" | grep -m 1 -i 'model:' | cut -f 2 -d ":" | awk '{$1=$1}1' | sed 's# @.*##g' | sed s/"(R)"//g | sed 's/[^[:alnum:] ]\+/ /g' | sed 's/ */ /g')
fi
cpu="${cpu// CPU/}"
cpu="${cpu// [0-9][0-9][0-9] Core}"
cpu="${cpu// [0-9][0-9] Core}"
cpu="${cpu// [0-9] Core}"
cpu="${cpu//[0-9][0-9]th Gen }"
cpu="${cpu//[0-9]th Gen }"
cpu="${cpu// Processor/}"
cpu="${cpu// Quad core/}"
cpu="${cpu// Dual core/}"
cpu="${cpu// Octa core/}"
cpu="${cpu// Hexa core/}"
cpu="${cpu// Core TM/ Core}"
cpu="${cpu// with Radeon Graphics/}"
cpu="${cpu// with Radeon Vega Graphics/}"
cpu="${cpu// with Radeon Vega Mobile Gfx/}"
cpu="${cpu// w Radeon [0-9][0-9][0-9]M Graphics/}"
[ -z "${cpu// /}" ] && cpu="Unknown"
echo "$cpu"
return 0
}
hasDisk() {
enabled "${DISK_DISABLE:-}" && return 1
[ -b "/disk" ] && return 0
[ -b "/disk1" ] && return 0
[ -b "/dev/disk1" ] && return 0
[ -b "${DEVICE:-}" ] && return 0
[ -z "${DISK_NAME:-}" ] && DISK_NAME="data"
[ -s "$STORAGE/$DISK_NAME.img" ] && return 0
[ -s "$STORAGE/$DISK_NAME.qcow2" ] && return 0
return 1
}
getCountry() {
local url=$1
local query=$2
local rc json result
{ json=$(curl -m 5 -H "Accept: application/json" -sfk "$url"); rc=$?; } || :
(( rc != 0 )) && return 0
{ result=$(echo "$json" | jq -r "$query" 2> /dev/null); rc=$?; } || :
(( rc != 0 )) && return 0
[[ ${#result} -ne 2 ]] && return 0
[[ "${result^^}" == "XX" ]] && return 0
COUNTRY="${result^^}"
return 0
}
setCountry() {
[[ "${TZ,,}" == "asia/harbin" ]] && COUNTRY="CN"
[[ "${TZ,,}" == "asia/beijing" ]] && COUNTRY="CN"
[[ "${TZ,,}" == "asia/urumqi" ]] && COUNTRY="CN"
[[ "${TZ,,}" == "asia/kashgar" ]] && COUNTRY="CN"
[[ "${TZ,,}" == "asia/shanghai" ]] && COUNTRY="CN"
[[ "${TZ,,}" == "asia/chongqing" ]] && COUNTRY="CN"
[ -z "$COUNTRY" ] && getCountry "https://api.ipapi.is" ".location.country_code"
[ -z "$COUNTRY" ] && getCountry "https://ifconfig.co/json" ".country_iso"
[ -z "$COUNTRY" ] && getCountry "https://api.ip2location.io" ".country_code"
[ -z "$COUNTRY" ] && getCountry "https://ipinfo.io/json" ".country"
[ -z "$COUNTRY" ] && getCountry "https://api.ipquery.io/?format=json" ".location.country_code"
[ -z "$COUNTRY" ] && getCountry "https://api.myip.com" ".cc"
return 0
}
addPackage() {
local pkg=$1
local desc=$2
if apt-mark showinstall | grep -qx "$pkg"; then
return 0
fi
MSG="Installing $desc..."
info "$MSG" && html "$MSG"
[ -z "$COUNTRY" ] && setCountry
if [[ "${COUNTRY^^}" == "CN" ]]; then
sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources
fi
DEBIAN_FRONTEND=noninteractive apt-get -qq update
DEBIAN_FRONTEND=noninteractive apt-get -qq --no-install-recommends -y install "$pkg" > /dev/null
return 0
}
return 0
+12 -2
View File
@@ -1,4 +1,5 @@
server { server {
listen 5000 default_server; listen 5000 default_server;
autoindex on; autoindex on;
@@ -9,6 +10,9 @@ server {
error_log /dev/null; error_log /dev/null;
access_log /dev/null; access_log /dev/null;
auth_basic off;
auth_basic_user_file /etc/nginx/.htpasswd;
include /etc/nginx/mime.types; include /etc/nginx/mime.types;
gzip on; gzip on;
@@ -19,7 +23,13 @@ server {
gzip_disable "msie6"; gzip_disable "msie6";
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; 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;
add_header Cache-Control "no-cache"; set $cache_control "no-cache";
if ($uri = /msg.html) {
set $cache_control "no-store";
}
add_header Cache-Control $cache_control always;
location / { location / {
@@ -39,6 +49,6 @@ server {
proxy_read_timeout 3600s; proxy_read_timeout 3600s;
proxy_send_timeout 3600s; proxy_send_timeout 3600s;
proxy_pass http://127.0.0.1:8004/; proxy_pass http://unix:/run/shm/status-ws.sock:/;
} }
} }
-167
View File
@@ -1,167 +0,0 @@
body {
color: white;
background-color: #125bdb;
font-smoothing: antialiased;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: Verdana, Geneva, sans-serif;
}
#info {
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.25);
}
#content {
text-align: center;
padding: 20px;
margin-top: 50px;
}
footer {
width: 98%;
position: fixed;
bottom: 0px;
height: 40px;
text-align: center;
color: #0c8aeb;
text-shadow: 0 0 1px #0c8aeb;
}
#empty {
height: 40px;
/* Same height as footer */
}
a,
a:hover,
a:active,
a:visited {
color: white;
}
footer a:link,
footer a:visited,
footer a:active {
color: #0c8aeb;
}
footer a:hover {
color: #73e6ff;
}
.loading:after {
content: " .";
animation: dots 1s steps(5, end) infinite;
}
@keyframes dots {
0%,
20% {
color: rgba(0, 0, 0, 0);
text-shadow: 0.25em 0 0 rgba(0, 0, 0, 0), 0.5em 0 0 rgba(0, 0, 0, 0);
}
40% {
color: white;
text-shadow: 0.25em 0 0 rgba(0, 0, 0, 0), 0.5em 0 0 rgba(0, 0, 0, 0);
}
60% {
text-shadow: 0.25em 0 0 white, 0.5em 0 0 rgba(0, 0, 0, 0);
}
80%,
100% {
text-shadow: 0.25em 0 0 white, 0.5em 0 0 white;
}
}
.spinner_LWk7 {
animation: spinner_GWy6 1.2s linear infinite, spinner_BNNO 1.2s linear infinite
}
.spinner_yOMU {
animation: spinner_GWy6 1.2s linear infinite, spinner_pVqn 1.2s linear infinite;
animation-delay: .15s
}
.spinner_KS4S {
animation: spinner_GWy6 1.2s linear infinite, spinner_6uKB 1.2s linear infinite;
animation-delay: .3s
}
.spinner_zVee {
animation: spinner_GWy6 1.2s linear infinite, spinner_Qw4x 1.2s linear infinite;
animation-delay: .45s
}
@keyframes spinner_GWy6 {
0%,
50% {
width: 9px;
height: 9px
}
10% {
width: 11px;
height: 11px
}
}
@keyframes spinner_BNNO {
0%,
50% {
x: 1.5px;
y: 1.5px
}
10% {
x: .5px;
y: .5px
}
}
@keyframes spinner_pVqn {
0%,
50% {
x: 13.5px;
y: 1.5px
}
10% {
x: 12.5px;
y: .5px
}
}
@keyframes spinner_6uKB {
0%,
50% {
x: 13.5px;
y: 13.5px
}
10% {
x: 12.5px;
y: 12.5px
}
}
@keyframes spinner_Qw4x {
0%,
50% {
x: 1.5px;
y: 13.5px
}
10% {
x: .5px;
y: 12.5px
}
}
-1
View File
@@ -1 +0,0 @@
<svg id="Capa_1" enable-background="new 0 0 511.962 511.962" height="512" viewBox="0 0 511.962 511.962" width="512" xmlns="http://www.w3.org/2000/svg"><g><path d="m489.965 120.063c0-5.77-3.31-11.028-8.512-13.524l-218.984-105.063c-4.102-1.967-8.875-1.967-12.977 0l-218.985 105.063c-5.202 2.496-8.511 7.755-8.511 13.524l-.003 271.834c0 5.77 3.31 11.028 8.512 13.524l218.989 105.064c2.051.983 4.27 1.476 6.488 1.476 2.219 0 4.438-.492 6.488-1.476l218.989-105.064c5.202-2.496 8.512-7.755 8.512-13.524z" fill="#4e6ba6"/><path d="m489.965 120.063c0-5.77-3.31-11.028-8.512-13.524l-218.984-105.063c-2.051-.984-4.269-1.476-6.488-1.476v511.962c2.219 0 4.438-.492 6.488-1.476l218.989-105.064c5.202-2.496 8.512-7.755 8.512-13.524z" fill="#28487a"/><path d="m425.812 160.441c0-2.27-.519-4.457-1.457-6.432l-336.701-.095c-.967 1.999-1.504 4.22-1.504 6.526l-.002 191.081c0 5.769 3.31 11.028 8.512 13.524l154.833 74.285c2.051.983 4.27 1.476 6.488 1.476 2.219 0 4.438-.492 6.488-1.476l154.834-74.285c5.202-2.496 8.512-7.755 8.512-13.524z" fill="#8dc2eb"/><path d="m424.354 154.009h-168.373v286.798c2.219 0 4.438-.492 6.488-1.476l154.834-74.285c5.202-2.496 8.512-7.755 8.512-13.524l-.003-191.081c0-2.27-.52-4.458-1.458-6.432z" fill="#5e9ff6"/><path d="m417.3 146.916-154.831-74.284c-4.102-1.967-8.875-1.967-12.977 0l-154.831 74.284c-3.122 1.498-5.555 3.996-7.007 6.998l168.328 80.812 168.374-80.717c-1.448-3.044-3.9-5.579-7.056-7.093z" fill="#ecf9fd"/><path d="m417.3 146.916-154.831-74.284c-2.051-.983-4.27-1.476-6.488-1.476v163.569l168.374-80.717c-1.447-3.043-3.899-5.578-7.055-7.092z" fill="#d9f3fc"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

-34
View File
@@ -1,34 +0,0 @@
<!DOCTYPE html>
<html>
<head>
[1]
<meta http-equiv="Cache-Control" content="no-cache" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="css/style.css" />
<link rel="icon" href="img/favicon.svg" type="image/x-icon">
[2]
</head>
<body>
<div id="page">
<div id="content">
<svg id="spinner" width="64" height="64" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<rect class="spinner_LWk7" fill="#0c8aeb" x="1.5" y="1.5" rx="1" width="9" height="9"/>
<rect class="spinner_yOMU" fill="#0c8aeb" x="13.5" y="1.5" rx="1" width="9" height="9"/>
<rect class="spinner_KS4S" fill="#0c8aeb" x="13.5" y="13.5" rx="1" width="9" height="9"/>
<rect class="spinner_zVee" fill="#0c8aeb" x="1.5" y="13.5" rx="1" width="9" height="9"/>
</svg>
<h1 id="info">[3]</h1>
</div>
<div id="empty">
</div>
<footer id="footer">
[4]<br />
[5]
</footer>
</div>
<script type="text/javascript" src="js/script.js"></script>
</body>
</html>
+459 -37
View File
@@ -1,15 +1,90 @@
var timer;
var request; var request;
var failureTimer;
var navigationTimer;
var booting = false; var booting = false;
var portal = false;
var portalUrl = "";
var interval = 1000; var interval = 1000;
var lastStatus = "";
function abortRequest() {
if (!request) {
return false;
}
request.onreadystatechange = null;
request.abort();
request = null;
return true;
}
function clearFailure() {
if (!failureTimer) {
return false;
}
clearTimeout(failureTimer);
failureTimer = null;
return true;
}
function connectionLost() {
if (booting || (portal && portalUrl.length == 0) ||
document.hidden || failureTimer) {
return false;
}
failureTimer = setTimeout(function() {
if (booting || (portal && portalUrl.length == 0) ||
document.hidden) {
failureTimer = null;
return;
}
setStopped();
}, interval * 3);
return true;
}
function clearNavigation() {
if (!navigationTimer) {
return false;
}
clearTimeout(navigationTimer);
navigationTimer = null;
return true;
}
function visibilityChanged() {
clearFailure();
clearNavigation();
if (document.hidden) {
return false;
}
getInfo();
return true;
}
function getInfo() { function getInfo() {
var url = "msg.html"; var url = "msg.html";
try { try {
if (request) { abortRequest();
request.abort();
}
if (window.XMLHttpRequest) { if (window.XMLHttpRequest) {
request = new XMLHttpRequest(); request = new XMLHttpRequest();
@@ -34,67 +109,126 @@ function getURL() {
return protocol + "//" + window.location.host + path; return protocol + "//" + window.location.host + path;
} }
function redirect(url) {
if (document.hidden || navigationTimer) {
return false;
}
navigationTimer = setTimeout(function() {
navigationTimer = null;
window.location.assign(url);
}, 3000);
return true;
}
function beginPortal(url) {
portal = true;
portalUrl = url || "";
booting = false;
clearFailure();
setInfo("Connecting to web portal", true);
schedule();
if (portalUrl.length > 0) {
redirect(portalUrl);
}
return true;
}
function processCommand(msg) {
if (msg == "portal") {
return beginPortal("");
}
if (msg.indexOf("portal ") == 0) {
return beginPortal(msg.substring(7));
}
console.warn("Unknown command: " + msg);
return false;
}
function processMsg(msg) { function processMsg(msg) {
rememberStatus(msg);
setInfo(msg);
if (msg.toLowerCase().indexOf("href=") !== -1) { if (msg.toLowerCase().indexOf("href=") !== -1) {
var div = document.createElement("div"); var div = document.createElement("div");
div.innerHTML = msg; div.innerHTML = msg;
var url = div.querySelector("a").href; return beginPortal(div.querySelector("a").href);
setTimeout(() => {
window.location.assign(url);
}, 3000);
} }
setInfo(msg);
return true; return true;
} }
function processInfo() { function processInfo() {
try {
if (request.readyState != 4) { if (request.readyState != 4) {
return true; return true;
} }
var msg = request.responseText; var response = request;
if (msg == null || msg.length == 0) { request = null;
if (booting) { var status = response.status;
if (status == 502 || status == 503 || status == 504) {
connectionLost();
schedule(); schedule();
return true; return true;
} }
window.location.reload(); var msg = response.responseText;
if (msg == null || msg.length == 0) {
connectionLost();
schedule();
return false; return false;
} }
var notFound = (request.status == 404); var notFound = (status == 404);
if (request.status == 200) { if (status == 200) {
if (msg.toLowerCase().indexOf("<html>") !== -1) { if (msg.toLowerCase().indexOf("<html>") !== -1) {
notFound = true; notFound = true;
} else { } else {
clearFailure();
processMsg(msg); processMsg(msg);
if (msg.toLowerCase().indexOf("href=") == -1) {
if (portalUrl.length > 0) {
setInfo("Connecting to web portal", true);
redirect(portalUrl);
} else {
schedule(); schedule();
} }
return true; return true;
} }
} }
if (notFound) { if (notFound) {
clearFailure();
booting = false;
setInfo("Connecting to web portal", true); setInfo("Connecting to web portal", true);
if (portalUrl.length > 0) {
redirect(portalUrl);
} else {
portal = true;
reload(); reload();
}
return true; return true;
} }
setError("Error: Received statuscode " + request.status); setError("Error: Received statuscode " + status);
return false; return false;
} catch (e) {
setError("Error: " + e.message);
return false;
}
} }
function extractContent(s) { function extractContent(s) {
@@ -103,20 +237,248 @@ function extractContent(s) {
return span.textContent || span.innerText; return span.textContent || span.innerText;
}; };
function escapeContent(s) {
var span = document.createElement('span');
span.textContent = s;
return span.innerHTML;
}
function rememberStatus(msg) {
var text = extractContent(msg).trim();
if (text.length == 0) {
return false;
}
if (text.includes("Booting ")) {
booting = true;
}
lastStatus = text;
return true;
}
function parseSize(value, unit) {
var powers = {
"B": 0,
"KB": 1,
"KIB": 1,
"MB": 2,
"MIB": 2,
"GB": 3,
"GIB": 3,
"TB": 4,
"TIB": 4,
"PB": 5,
"PIB": 5,
"EB": 6,
"EIB": 6
};
unit = unit.toUpperCase();
if (!Object.prototype.hasOwnProperty.call(powers, unit)) {
return null;
}
var bytes = Number(value) * Math.pow(1024, powers[unit]);
if (!Number.isFinite(bytes) || bytes < 0) {
return null;
}
return bytes;
}
function estimateProgress(bytes) {
var boundary = 512 * 1024 * 1024;
var previousBoundary = 0;
while (bytes > boundary) {
previousBoundary = boundary;
boundary *= 2;
}
if (previousBoundary === 0) {
return Math.min(bytes / boundary * 100, 99.9);
}
var rangeProgress =
(bytes - previousBoundary) /
(boundary - previousBoundary);
return Math.min(30 + Math.pow(rangeProgress, 2) * 70, 99.9);
}
function parseProgress(msg) {
var container = document.createElement("div");
container.innerHTML = msg;
var walker = document.createTreeWalker(
container,
NodeFilter.SHOW_TEXT
);
var node;
var lastNode = null;
while ((node = walker.nextNode())) {
if (node.nodeValue.trim() !== "") {
lastNode = node;
}
}
if (!lastNode) {
return {
message: msg,
progress: null,
size: null
};
}
var percentMatch = lastNode.nodeValue.match(
/\s+\((\d+(?:\.\d+)?)%\)\s*$/
);
if (percentMatch) {
var progress = Number(percentMatch[1]);
if (Number.isFinite(progress) && progress >= 0 && progress <= 100) {
lastNode.nodeValue = lastNode.nodeValue.slice(
0,
percentMatch.index
);
return {
message: container.innerHTML,
progress: progress,
size: null
};
}
}
var sizeMatch = lastNode.nodeValue.match(
/\s+\((\d+(?:\.\d+)?)\s+([KMGTPE]?i?B)\)\s*$/i
);
if (sizeMatch) {
var bytes = parseSize(sizeMatch[1], sizeMatch[2]);
if (bytes != null) {
var size = sizeMatch[1] + " " + sizeMatch[2];
return {
message: msg,
progress: estimateProgress(bytes),
size: size
};
}
}
return {
message: msg,
progress: null,
size: null
};
}
function resizeProgress() {
var info = document.getElementById("info");
var progress = document.getElementById("progress");
if (!info || !progress || progress.hidden) {
return false;
}
var style = window.getComputedStyle(info);
var measurement = document.createElement("span");
measurement.textContent = info.innerText;
measurement.style.position = "fixed";
measurement.style.left = "-9999px";
measurement.style.top = "-9999px";
measurement.style.visibility = "hidden";
measurement.style.whiteSpace = "nowrap";
measurement.style.fontFamily = style.fontFamily;
measurement.style.fontSize = style.fontSize;
measurement.style.fontWeight = style.fontWeight;
measurement.style.fontStyle = style.fontStyle;
measurement.style.letterSpacing = style.letterSpacing;
measurement.style.textTransform = style.textTransform;
document.body.appendChild(measurement);
var textWidth = measurement.getBoundingClientRect().width;
var loading = info.getElementsByClassName("loading").length > 0;
var dotsWidth = 0;
if (loading) {
dotsWidth = parseFloat(style.fontSize) * 0.75;
}
measurement.remove();
var maximumWidth = window.innerWidth * 0.8;
var width = Math.min(textWidth + dotsWidth + 100, maximumWidth);
progress.style.width = width + "px";
progress.style.transform = loading
? "translateX(" + (dotsWidth / 2) + "px)"
: "";
return true;
}
function setProgress(value, size) {
var progress = document.getElementById("progress");
var fill = document.getElementById("progress-fill");
if (value == null) {
progress.hidden = true;
progress.removeAttribute("title");
progress.removeAttribute("aria-valuenow");
progress.removeAttribute("aria-valuetext");
fill.style.width = "0%";
return true;
}
progress.hidden = false;
progress.title = size != null ? size : value + "%";
progress.setAttribute("aria-valuenow", value);
if (size != null) {
progress.setAttribute("aria-valuetext", size);
} else {
progress.removeAttribute("aria-valuetext");
}
fill.style.width = value + "%";
return true;
}
function setInfo(msg, loading, error) { function setInfo(msg, loading, error) {
try {
if (msg == null || msg.length == 0) { if (msg == null || msg.length == 0) {
return false; return false;
} }
if (msg.includes("Booting ")) { var parsed = parseProgress(msg);
booting = true; msg = parsed.message;
} setProgress(parsed.progress, parsed.size);
var el = document.getElementById("info"); var el = document.getElementById("info");
if (el.innerText == msg || el.innerHTML == msg) { if (el.innerText == msg || el.innerHTML == msg) {
var progressEl = document.getElementById("progress");
if (progressEl && !progressEl.hidden && !progressEl.style.width) {
resizeProgress();
}
return true; return true;
} }
@@ -138,17 +500,14 @@ function setInfo(msg, loading, error) {
if (msg.includes(p)) { if (msg.includes(p)) {
if (el.innerHTML.includes(p)) { if (el.innerHTML.includes(p)) {
el.getElementsByClassName('loading')[0].textContent = extractContent(msg); el.getElementsByClassName('loading')[0].textContent = extractContent(msg);
resizeProgress();
return true; return true;
} }
} }
el.innerHTML = msg; el.innerHTML = msg;
resizeProgress();
return true; return true;
} catch (e) {
console.log("Error: " + e.message);
return false;
}
} }
function setError(text) { function setError(text) {
@@ -156,14 +515,38 @@ function setError(text) {
return setInfo(text, false, true); return setInfo(text, false, true);
} }
function setStopped() {
var msg = "<span style=\"display:block\">The container has stopped.</span>";
msg += "<span style=\"display:block; margin-top:1em; font-size:0.85em; font-weight:normal\">Check the container log for more details.</span>";
if (lastStatus.length > 0) {
msg += "<span style=\"display:block; margin-top:1em; font-size:0.75em; font-weight:normal; color:rgba(255,255,255,0.65)\">[ Last status: " + escapeContent(lastStatus) + " ]</span>";
}
return setError(msg);
}
function schedule() { function schedule() {
setTimeout(getInfo, interval);
clearTimeout(timer);
timer = setTimeout(getInfo, interval);
} }
function reload() { function reload() {
setTimeout(() => {
window.location.reload(); if (document.hidden) {
return false;
}
clearNavigation();
navigationTimer = setTimeout(function() {
navigationTimer = null;
window.location.replace(window.location.href);
}, 3000); }, 3000);
return true;
} }
function connect() { function connect() {
@@ -171,19 +554,49 @@ function connect() {
var wsUrl = getURL() + "/status"; var wsUrl = getURL() + "/status";
var ws = new WebSocket(wsUrl); var ws = new WebSocket(wsUrl);
ws.onopen = function(e) {
clearFailure();
if (portalUrl.length > 0) {
redirect(portalUrl);
}
};
ws.onmessage = function(e) { ws.onmessage = function(e) {
clearFailure();
var pos = e.data.indexOf(":"); var pos = e.data.indexOf(":");
var cmd = e.data.substring(0, pos); var cmd = e.data.substring(0, pos);
var msg = e.data.substring(pos + 2); var msg = e.data.substring(pos + 2);
switch (cmd) { switch (cmd) {
case "s": case "s":
if (abortRequest()) {
schedule();
}
processMsg(msg); processMsg(msg);
break; break;
case "c":
abortRequest();
processCommand(msg);
break;
case "e": case "e":
if (abortRequest()) {
schedule();
}
rememberStatus(msg);
setError(msg); setError(msg);
break; break;
default: default:
console.warn("Unknown event: " + cmd); console.warn("Unknown event: " + cmd);
break; break;
@@ -191,18 +604,27 @@ function connect() {
}; };
ws.onclose = function(e) { ws.onclose = function(e) {
connectionLost();
if (portal && portalUrl.length == 0) {
return;
}
setTimeout(function() { setTimeout(function() {
connect(); connect();
}, interval); }, interval);
}; };
ws.onerror = function(e) { ws.onerror = function(e) {
connectionLost();
ws.close(); ws.close();
if (!booting) {
window.location.reload();
}
}; };
} }
window.addEventListener("resize", resizeProgress);
document.addEventListener("visibilitychange", visibilityChanged);
rememberStatus(document.getElementById("info").innerHTML);
schedule(); schedule();
connect(); connect();