351 Commits
v1.1.5 ... main

Author SHA1 Message Date
ProxMenuxBot
013d1980a3 Update helpers_cache.json 2025-10-01 18:18:31 +00:00
MacRimi
df9f4a23b4 Update AppImage 2025-10-01 18:14:58 +02:00
MacRimi
c41da47a48 Update AppImage 2025-10-01 18:08:31 +02:00
MacRimi
e7214ad8df Update globals.css 2025-10-01 18:04:38 +02:00
MacRimi
d6671de842 Update AppImage 2025-10-01 18:01:54 +02:00
MacRimi
aad218db5d Update globals.css 2025-10-01 17:44:00 +02:00
MacRimi
724ba1e271 Update AppImge 2025-10-01 17:39:43 +02:00
MacRimi
97d554f638 update AppImage 2025-10-01 17:27:05 +02:00
MacRimi
c5a7655d26 Update AppImage 2025-10-01 17:10:37 +02:00
MacRimi
403e896e3e Update proxmox-dashboard.tsx 2025-10-01 17:03:56 +02:00
MacRimi
1a15f43cad Update proxmox-dashboard.tsx 2025-10-01 16:53:37 +02:00
MacRimi
399b460c53 Update globals.css 2025-09-30 00:11:24 +02:00
MacRimi
acc0362180 Update AppImage 2025-09-30 00:09:11 +02:00
MacRimi
00db93e03f Update AppImage 2025-09-29 23:56:33 +02:00
MacRimi
d1997794c8 Update globals.css 2025-09-29 23:40:45 +02:00
MacRimi
aa1ebe69f2 Update globals.css 2025-09-29 23:11:40 +02:00
MacRimi
4e7f5f56f1 Update AppImage 2025-09-29 22:59:10 +02:00
MacRimi
28cb7359ce Update system-overview.tsx 2025-09-29 22:38:25 +02:00
ProxMenuxBot
91c272d21c Update helpers_cache.json 2025-09-29 18:19:17 +00:00
MacRimi
3c00125e83 Update flask_server.py 2025-09-29 19:19:35 +02:00
MacRimi
f359848a2f Update AppRun 2025-09-29 19:12:56 +02:00
MacRimi
989769e5e8 Update AppRun 2025-09-29 19:07:35 +02:00
MacRimi
0f2f1b6211 Update system-overview.tsx 2025-09-29 19:02:59 +02:00
MacRimi
ffe8f4acc6 Update AppImage 2025-09-29 18:58:53 +02:00
MacRimi
edb09777de Update package.json 2025-09-29 18:47:18 +02:00
MacRimi
5262c7863e Update AppImage 2025-09-29 18:43:14 +02:00
MacRimi
54256826fe Update AppImage 2025-09-29 18:37:32 +02:00
MacRimi
3d3c224b3a Update flask_server.py 2025-09-29 18:31:09 +02:00
MacRimi
049eccb872 Update AppImage 2025-09-29 18:27:09 +02:00
MacRimi
269828c79e Update AppImage 2025-09-29 18:16:01 +02:00
MacRimi
b4e25ae66d Update AppImage 2025-09-29 18:07:30 +02:00
MacRimi
b20dd74d23 Update AppImage 2025-09-29 17:57:00 +02:00
MacRimi
bc3e2ec358 Update AppImage 2025-09-29 17:46:37 +02:00
MacRimi
6133a6d6d8 Update build_appimage.sh 2025-09-29 17:32:24 +02:00
MacRimi
46a16c04e6 Update AppImage 2025-09-29 17:21:59 +02:00
MacRimi
8469b3b26f Update AppImage 2025-09-29 17:05:42 +02:00
ProxMenuxBot
2ed04f57fe Update helpers_cache.json 2025-09-29 12:26:48 +00:00
MacRimi
b19bac679a Update flask_server.py 2025-09-29 00:00:01 +02:00
MacRimi
3c33d5982c Update AppImage 2025-09-28 23:51:06 +02:00
MacRimi
5b934eeb87 Update AppImage 2025-09-28 23:25:58 +02:00
MacRimi
795d96f8d5 Update AppImage 2025-09-28 23:09:31 +02:00
MacRimi
a8e7119b4a Update AppImage 2025-09-28 23:05:59 +02:00
MacRimi
38569ff7fc Update AppImage 2025-09-28 22:57:15 +02:00
MacRimi
e404557d62 Update AppImage 2025-09-28 22:53:42 +02:00
MacRimi
96cbc75a5e Update build_appimage.sh 2025-09-28 21:26:25 +02:00
MacRimi
c989af6cf0 Update build_appimage.sh 2025-09-28 21:18:52 +02:00
MacRimi
4eac9d03ea Update build_appimage.sh 2025-09-28 21:15:18 +02:00
MacRimi
6292009b0b Update AppImage 2025-09-28 21:08:58 +02:00
MacRimi
3272be967d Update build_appimage.sh 2025-09-28 21:01:48 +02:00
MacRimi
1c015da440 Update build_appimage.sh 2025-09-28 20:57:23 +02:00
MacRimi
0d047cc956 Update build_appimage.sh 2025-09-28 20:46:55 +02:00
MacRimi
e682070b85 update AppImage 2025-09-28 20:28:35 +02:00
MacRimi
9f08694d9b Update Appimage 2025-09-28 20:25:55 +02:00
MacRimi
70f0db73e5 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-09-28 20:20:46 +02:00
MacRimi
9dc8f44379 Create tsconfig.json 2025-09-28 20:20:31 +02:00
MacRimi
59f7ccd723 Update build-appimage.yml 2025-09-28 20:15:56 +02:00
MacRimi
0710e95a6d Update package.json 2025-09-28 20:15:33 +02:00
MacRimi
4d1b5e3919 Update build-appimage.yml 2025-09-28 20:12:21 +02:00
MacRimi
0cc2cb92dd Update build-appimage.yml 2025-09-28 20:10:08 +02:00
MacRimi
dba4d168f7 Update build-appimage.yml 2025-09-28 20:08:02 +02:00
MacRimi
d87ac7843c Update build-appimage.yml 2025-09-28 20:07:12 +02:00
MacRimi
040535b004 Update build-appimage.yml 2025-09-28 20:04:39 +02:00
MacRimi
c8acd2c0b1 Update build-appimage.yml 2025-09-28 19:56:03 +02:00
MacRimi
d67fecea6e Update build-appimage.yml 2025-09-28 19:46:03 +02:00
MacRimi
61f80f9ee6 Update build-appimage.yml 2025-09-28 19:44:51 +02:00
MacRimi
9da8f9a5d1 Update build-appimage.yml 2025-09-28 19:43:05 +02:00
MacRimi
f381468d5a Create build-appimage.yml 2025-09-28 19:41:13 +02:00
MacRimi
6ae97266e4 Create AppImage 2025-09-28 19:40:23 +02:00
MacRimi
66060f345c Create jd2_2.sh 2025-09-27 20:21:15 +02:00
MacRimi
c61f568170 Update nfs_lxc_server.sh 2025-09-27 18:50:50 +02:00
MacRimi
dcd108bda3 Update update-pve.sh 2025-09-27 18:28:56 +02:00
MacRimi
9d89f98987 Update customizable_post_install.sh 2025-09-27 18:26:23 +02:00
MacRimi
ca7b959fce Update auto_post_install.sh 2025-09-27 18:25:25 +02:00
MacRimi
4a30793595 Update uninstall-tools.sh 2025-09-27 18:24:02 +02:00
MacRimi
35e2d53f0f update remove subscription banner PVE 9 2025-09-27 18:16:12 +02:00
MacRimi
503efa4572 Create remove-banner-pve9_2.sh 2025-09-27 17:42:18 +02:00
ProxMenuxBot
b0c33d9dff Update helpers_cache.json 2025-09-27 00:56:57 +00:00
MacRimi
012b156b46 Update install_coral_pve9.sh 2025-09-26 00:17:32 +02:00
MacRimi
25d0d3bf59 Create install_coral_pve9.sh 2025-09-25 23:45:50 +02:00
ProxMenuxBot
0f1babc82b Update helpers_cache.json 2025-09-25 18:20:06 +00:00
ProxMenuxBot
e2b93ea785 Update helpers_cache.json 2025-09-24 18:19:23 +00:00
ProxMenuxBot
b1cedfa81e Update helpers_cache.json 2025-09-24 12:27:12 +00:00
ProxMenuxBot
701ee36f6a Update helpers_cache.json 2025-09-23 12:25:52 +00:00
ProxMenuxBot
4e5db86434 Update helpers_cache.json 2025-09-21 12:23:25 +00:00
ProxMenuxBot
f45e9e657c Update helpers_cache.json 2025-09-19 18:18:42 +00:00
ProxMenuxBot
4936fcdb1e Update helpers_cache.json 2025-09-18 18:18:59 +00:00
ProxMenuxBot
374e05c422 Update helpers_cache.json 2025-09-18 12:25:39 +00:00
ProxMenuxBot
9c00798373 Update helpers_cache.json 2025-09-17 18:18:09 +00:00
ProxMenuxBot
db82fce925 Update helpers_cache.json 2025-09-15 12:26:37 +00:00
ProxMenuxBot
acaa28e476 Update helpers_cache.json 2025-09-13 00:54:55 +00:00
ProxMenuxBot
f297ce5809 Update helpers_cache.json 2025-09-12 12:24:43 +00:00
ProxMenuxBot
3dc3fc5f67 Update helpers_cache.json 2025-09-12 00:57:45 +00:00
ProxMenuxBot
4884fc4418 Update helpers_cache.json 2025-09-11 18:15:27 +00:00
MacRimi
adc17842ec Update README.md 2025-09-11 18:18:42 +02:00
MacRimi
daa48b0b7c Update README.md 2025-09-11 18:17:16 +02:00
MacRimi
17c0362df3 Update cache.json 2025-09-10 20:29:19 +02:00
MacRimi
29b9a63fc9 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-09-10 20:28:00 +02:00
MacRimi
2a9fae160e Update cache.json 2025-09-10 20:27:58 +02:00
ProxMenuxBot
0c49a1e3bd Update helpers_cache.json 2025-09-10 18:18:53 +00:00
MacRimi
e896c41be1 Update main_menu.sh 2025-09-10 19:14:42 +02:00
MacRimi
187250fa24 update text ProxMenux 2025-09-10 19:06:04 +02:00
MacRimi
9035b18584 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-09-10 18:58:12 +02:00
MacRimi
4534d78978 update text ProxMenux 2025-09-10 18:57:49 +02:00
MacRimi
f4ab0e982c Update README.md 2025-09-10 18:53:41 +02:00
MacRimi
3e7c6629a6 update text ProxMenux 2025-09-10 18:47:55 +02:00
MacRimi
3ea17331fe Update install_proxmenux.sh 2025-09-10 18:24:14 +02:00
MacRimi
1057fcc271 Update install_proxmenux.sh 2025-09-10 18:19:13 +02:00
MacRimi
5a31c36097 update menu share 2025-09-10 18:05:06 +02:00
MacRimi
1677a69bba Update version.txt 2025-09-10 17:52:17 +02:00
MacRimi
315c49165d Update CHANGELOG.md 2025-09-10 17:50:53 +02:00
MacRimi
aae70e7ec0 Update CHANGELOG.md 2025-09-10 17:47:37 +02:00
MacRimi
5cb9e13ca7 Update CHANGELOG.md 2025-09-10 17:37:51 +02:00
MacRimi
0187010f94 Create main-menu.png 2025-09-10 16:28:47 +02:00
ProxMenuxBot
2c2ed21e59 Update helpers_cache.json 2025-09-10 12:24:52 +00:00
MacRimi
f8b2ccec40 Update commands_share.sh 2025-09-10 13:48:17 +02:00
MacRimi
e858dc582d Update commands_share.sh 2025-09-10 13:47:13 +02:00
MacRimi
dd737f4b46 Update commands_share.sh 2025-09-10 13:46:02 +02:00
MacRimi
f0bc238b6d Update commands_share.sh 2025-09-10 13:35:16 +02:00
MacRimi
af55424850 Update lxc-mount-manager_minimal.sh 2025-09-10 13:16:54 +02:00
MacRimi
902534baff update menu shared 2025-09-10 12:57:17 +02:00
MacRimi
6daa630040 Update nfs_host.sh 2025-09-10 12:53:09 +02:00
MacRimi
0b2b86673b Update lxc-mount-manager_minimal.sh 2025-09-10 12:31:49 +02:00
MacRimi
6aa5b58208 Update share_menu.sh 2025-09-10 12:17:03 +02:00
MacRimi
4430201cd2 Update lxc-mount-manager_minimal.sh 2025-09-10 12:16:25 +02:00
MacRimi
7c7963a83e Create lxc-mount-manager_minimal.sh 2025-09-10 11:10:02 +02:00
ProxMenuxBot
e2202cd2d8 Update helpers_cache.json 2025-09-09 18:17:13 +00:00
MacRimi
a931be83bc Update share-common.func 2025-09-09 19:20:15 +02:00
MacRimi
7350bea345 Update auto_post_install.sh 2025-09-09 19:16:13 +02:00
MacRimi
9b1e39dbb4 Update update-pve.sh 2025-09-09 19:14:27 +02:00
MacRimi
15cd118845 Update auto_post_install.sh 2025-09-09 19:06:33 +02:00
MacRimi
d58dff047c Update auto_post_install.sh 2025-09-08 19:24:49 +02:00
MacRimi
a2f83c896c Update common-functions.sh 2025-09-08 19:14:41 +02:00
MacRimi
6ef77c731c Update guia.md 2025-09-08 17:44:17 +02:00
MacRimi
29b0f61958 Update guia.md 2025-09-08 17:43:39 +02:00
MacRimi
e944b2ecdd Update guia.md 2025-09-08 17:24:23 +02:00
MacRimi
41819c46a3 Update guia.md 2025-09-08 17:21:58 +02:00
MacRimi
13f391a6f0 Update guia.md 2025-09-08 17:14:52 +02:00
MacRimi
85a3d44f2c Update guia.md 2025-09-08 17:05:26 +02:00
MacRimi
0792392058 Update guia.md 2025-09-08 16:42:46 +02:00
MacRimi
ff5083ada0 Update guia.md 2025-09-08 16:20:10 +02:00
MacRimi
62841677bc Update guia.md 2025-09-08 16:06:50 +02:00
MacRimi
1761cf53a2 Update guia.md 2025-09-08 15:27:32 +02:00
MacRimi
a771efc5fa Update guia.md 2025-09-08 15:08:02 +02:00
MacRimi
ed049da76a Update guia.md 2025-09-08 15:03:10 +02:00
MacRimi
5d1d357a2e Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-09-08 15:01:02 +02:00
MacRimi
30d0706a1c Create guia.md 2025-09-08 15:01:00 +02:00
ProxMenuxBot
e9667e1266 Update helpers_cache.json 2025-09-08 12:27:18 +00:00
MacRimi
73109483e7 Update share-common.func 2025-09-08 11:29:02 +02:00
MacRimi
a9c1acf204 Update lxc-mount-manager.sh 2025-09-08 11:15:52 +02:00
MacRimi
81c4f5814c Update samba_host.sh 2025-09-08 10:40:59 +02:00
MacRimi
c595f6d781 Update nfs_host.sh 2025-09-08 10:36:57 +02:00
MacRimi
24bb6b1d3d Update lxc-mount-manager.sh 2025-09-07 19:10:23 +02:00
MacRimi
49eeb6020d Update install_proxmenux.sh 2025-09-07 17:24:29 +02:00
MacRimi
7c272bd2a2 Update install_proxmenux.sh 2025-09-07 17:22:47 +02:00
MacRimi
cfbd865937 Update share-common.func 2025-09-07 09:29:43 +02:00
MacRimi
fe472f33ef Update lxc-mount-manager.sh 2025-09-07 09:28:09 +02:00
MacRimi
8d6b3d650f update menu share 2025-09-07 09:19:53 +02:00
MacRimi
3b0d5b5eb7 Update nfs_lxc_server.sh 2025-09-07 09:16:22 +02:00
MacRimi
875e8a99bd Update samba_client.sh 2025-09-07 09:06:47 +02:00
MacRimi
6c19d81844 Update samba_client.sh 2025-09-07 09:05:23 +02:00
MacRimi
ba535a931f Update nfs_client.sh 2025-09-07 09:04:13 +02:00
MacRimi
45dca5218d Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-09-07 09:00:39 +02:00
MacRimi
da3cb9971b Update share-common.func 2025-09-07 09:00:37 +02:00
ProxMenuxBot
b39270dc1e Update helpers_cache.json 2025-09-07 01:03:30 +00:00
MacRimi
ae8a7d0de9 Update commands_share.sh 2025-09-06 23:59:23 +02:00
MacRimi
2d501415bf Update commands_share.sh 2025-09-06 23:50:57 +02:00
MacRimi
da639ccaac Update commands_share.sh 2025-09-06 23:35:46 +02:00
MacRimi
a352770e2d Update share_menu.sh 2025-09-06 23:20:59 +02:00
MacRimi
e3e1899466 update menu share 2025-09-06 23:18:11 +02:00
MacRimi
e67288e623 Update lxc-mount-manager.sh 2025-09-06 22:55:28 +02:00
MacRimi
4019e49b07 Update share-common.func 2025-09-06 22:51:57 +02:00
MacRimi
cd8711f3bc Update lxc-mount-manager.sh 2025-09-06 22:50:30 +02:00
MacRimi
0d119379de Update lxc-mount-manager.sh 2025-09-06 22:46:52 +02:00
MacRimi
aa2b6ff112 Update lxc-mount-manager.sh 2025-09-06 22:44:09 +02:00
MacRimi
3482f7dc98 Update share-common.func 2025-09-06 22:42:17 +02:00
MacRimi
16c321f114 Update lxc-mount-manager.sh 2025-09-06 22:33:19 +02:00
MacRimi
a81e7f3c44 Update lxc-mount-manager.sh 2025-09-06 22:25:50 +02:00
MacRimi
d7cc001521 Update lxc-mount-manager.sh 2025-09-06 22:20:34 +02:00
MacRimi
eb11962231 Update share-common.func 2025-09-06 22:19:52 +02:00
MacRimi
9f73b8f159 Update lxc-mount-manager.sh 2025-09-06 22:17:08 +02:00
MacRimi
873a4abe24 Update share-common.func 2025-09-06 22:16:40 +02:00
MacRimi
56bc584f5e Update lxc-mount-manager.sh 2025-09-06 22:10:01 +02:00
MacRimi
2a9f2f3c2e Update lxc-mount-manager.sh 2025-09-06 22:09:11 +02:00
MacRimi
ee719cdd39 Update share-common.func 2025-09-06 22:06:02 +02:00
MacRimi
a571b57b30 Update share-common.func 2025-09-06 21:48:39 +02:00
MacRimi
5ee7a23bea Update share-common.func 2025-09-06 21:47:20 +02:00
MacRimi
fe159ea195 Update share-common.func 2025-09-06 21:42:30 +02:00
MacRimi
8fcdf6176b Update share-common.func 2025-09-06 21:31:03 +02:00
MacRimi
715166bbca Update share-common.func 2025-09-06 21:22:40 +02:00
MacRimi
1d58072c70 Update share-common.func 2025-09-06 21:21:39 +02:00
MacRimi
d667cde699 Update lxc-mount-manager.sh 2025-09-06 21:19:55 +02:00
MacRimi
4cd8889c38 Update share-common.func 2025-09-06 21:14:54 +02:00
MacRimi
93896f6fb7 Update share-common.func 2025-09-06 21:07:12 +02:00
MacRimi
3b3f0387bb Update share-common.func 2025-09-06 21:05:32 +02:00
MacRimi
2875c9af95 Update lxc-mount-manager.sh 2025-09-06 20:59:03 +02:00
MacRimi
93ef1bfccc update share menu 2025-09-06 20:57:04 +02:00
MacRimi
a886af1d87 Update samba_client.sh 2025-09-06 20:48:37 +02:00
MacRimi
d731ff3ae6 Update samba_host.sh 2025-09-06 20:40:45 +02:00
MacRimi
d44864637d Update nfs_host_auto.sh 2025-09-06 20:37:23 +02:00
MacRimi
674ee34ec6 Update nfs_host_auto.sh 2025-09-06 20:30:04 +02:00
MacRimi
a93eeda243 Update share-common.func 2025-09-06 19:56:24 +02:00
MacRimi
80fd92e2a1 Update share-common.func 2025-09-06 19:55:01 +02:00
MacRimi
d4ff2da473 Update share-common.func 2025-09-06 19:16:15 +02:00
MacRimi
9b7b271580 update menu shared 2025-09-06 19:13:52 +02:00
MacRimi
e1b340966a Update share-common.func 2025-09-06 18:56:10 +02:00
MacRimi
7ec4c331af Update nfs_client.sh 2025-09-06 18:39:35 +02:00
MacRimi
3102d596ee Update nfs_lxc_server.sh 2025-09-06 18:37:28 +02:00
MacRimi
af56dc546e Update nfs_host_auto.sh 2025-09-06 18:26:13 +02:00
MacRimi
15d47499fa Update nfs_host_auto.sh 2025-09-06 18:22:02 +02:00
MacRimi
53a34d0470 update nfs menu 2025-09-06 18:20:20 +02:00
MacRimi
3ee675cefe Update nfs_lxc_server.sh 2025-09-06 18:08:48 +02:00
MacRimi
d98c7bdc03 Update share_menu.sh 2025-09-06 16:48:07 +02:00
MacRimi
bb4f1ebed6 Update share_menu.sh 2025-09-06 16:44:58 +02:00
MacRimi
c8f73ea23b Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-09-06 16:41:55 +02:00
MacRimi
8292b12787 Create nfs_lxc_server.sh 2025-09-06 16:41:54 +02:00
ProxMenuxBot
0f518e3c35 Update helpers_cache.json 2025-09-06 12:22:12 +00:00
MacRimi
1c2f67d43d Update lxc-mount-manager.sh 2025-09-06 11:55:23 +02:00
MacRimi
a5560a3123 Update share-common.func 2025-09-06 11:50:48 +02:00
MacRimi
1332096360 Update share-common.func 2025-09-06 11:39:17 +02:00
MacRimi
80381a6375 Update share-common.func 2025-09-06 11:32:07 +02:00
MacRimi
acf92bd005 Update share-common.func 2025-09-06 11:30:23 +02:00
MacRimi
da4f8a3a19 Update share-common.func 2025-09-06 11:11:19 +02:00
MacRimi
3a332192e3 Update share-common.func 2025-09-06 11:08:23 +02:00
MacRimi
1fdb1d87cc Update share-common.func 2025-09-06 11:01:35 +02:00
MacRimi
b99aa55d7a Update share-common.func 2025-09-06 10:46:22 +02:00
MacRimi
de20da2dad Update lxc-mount-manager.sh 2025-09-06 10:10:37 +02:00
MacRimi
9444f0a68b Update nfs_host_auto.sh 2025-09-06 08:51:41 +02:00
MacRimi
48fd223a28 Update samba.sh 2025-09-04 20:38:11 +02:00
MacRimi
0845efe419 Update samba_client.sh 2025-09-04 20:37:18 +02:00
MacRimi
57b7ba91bc Update share_menu.sh 2025-09-04 20:34:43 +02:00
MacRimi
97af8a4892 Update share_menu.sh 2025-09-04 20:31:33 +02:00
MacRimi
d6f237e289 Update share_menu.sh 2025-09-04 20:30:36 +02:00
MacRimi
aba7109b35 Update samba_host.sh 2025-09-04 20:28:16 +02:00
MacRimi
d3ec71052e Update samba_client.sh 2025-09-04 20:26:51 +02:00
MacRimi
1be63f396b Update nfs_client.sh 2025-09-04 20:26:26 +02:00
MacRimi
9308742146 Update samba_client.sh 2025-09-04 20:23:00 +02:00
MacRimi
b32241082d Update share_menu.sh 2025-09-04 20:12:24 +02:00
MacRimi
1f8504d685 update shared 2025-09-04 20:04:08 +02:00
MacRimi
97c5c48150 Update nfs_host.sh 2025-09-03 23:13:21 +02:00
MacRimi
afe84dc46a Update nfs_client.sh 2025-09-03 23:10:01 +02:00
MacRimi
ffafd42f03 Update nfs.sh 2025-09-03 23:06:47 +02:00
MacRimi
7dca715c91 Update nfs.sh 2025-09-03 23:04:46 +02:00
MacRimi
7695e1d8dd Update nfs.sh 2025-09-03 22:55:45 +02:00
MacRimi
84b86d1db7 Update nfs.sh 2025-09-03 22:14:29 +02:00
MacRimi
bae3ef6460 Update nfs.sh 2025-09-03 22:06:34 +02:00
MacRimi
97c6ec8875 Update share-common.func 2025-09-03 16:47:03 +02:00
MacRimi
d33128dc26 Update share_menu.sh 2025-09-03 12:27:59 +02:00
MacRimi
10bdecabb6 Update share_menu.sh 2025-09-03 12:25:35 +02:00
MacRimi
de88f530c8 Update share_menu.sh 2025-09-03 12:23:54 +02:00
MacRimi
fb511b7596 Update share_menu.sh 2025-09-03 12:22:49 +02:00
MacRimi
322665ce91 Update share_menu.sh 2025-09-03 12:21:21 +02:00
MacRimi
baeca1fcfb Update share-common.func 2025-09-03 11:35:38 +02:00
MacRimi
095b98c36a Update share-common.func 2025-09-03 11:28:37 +02:00
MacRimi
29bb7e7608 Update share-common.func 2025-09-03 11:16:34 +02:00
MacRimi
e3d137efba Update share-common.func 2025-09-02 22:56:46 +02:00
MacRimi
207e915393 Update share-common.func 2025-09-02 22:45:33 +02:00
MacRimi
614e629a2b Update share-common.func 2025-09-02 22:44:42 +02:00
MacRimi
f35de5c749 Update share-common.func 2025-09-02 21:34:13 +02:00
MacRimi
c1623bd4df Update share-common.func 2025-09-02 21:23:57 +02:00
ProxMenuxBot
8690da5017 Update helpers_cache.json 2025-09-02 18:17:01 +00:00
MacRimi
696adcdc24 Update share-common.func 2025-09-02 18:48:57 +02:00
MacRimi
2756bd06c1 Update share-common.func 2025-09-02 18:48:20 +02:00
MacRimi
4893f6ea00 Update lxc-mount-manager.sh 2025-09-02 18:45:53 +02:00
MacRimi
35a7348197 Update lxc-mount-manager.sh 2025-09-02 18:45:04 +02:00
MacRimi
cdd6333d0a Update share_menu.sh 2025-09-02 18:43:27 +02:00
MacRimi
54399b5b5d Update share-common.func 2025-09-02 18:25:39 +02:00
MacRimi
f6b192cc1e Update lxc-mount-manager.sh 2025-09-02 18:22:34 +02:00
MacRimi
cd231b90d8 Update share-common.func 2025-09-02 17:21:09 +02:00
ProxMenuxBot
87fe788358 Update helpers_cache.json 2025-09-02 12:26:27 +00:00
MacRimi
3e9bd21ea8 update share menu 2025-09-02 00:02:16 +02:00
MacRimi
b6d4029797 Update local-shared-manager.sh 2025-09-01 19:10:46 +02:00
MacRimi
ec65e96148 Update share_menu.sh 2025-09-01 19:08:48 +02:00
MacRimi
926f1f971f update share menu 2025-09-01 19:06:52 +02:00
MacRimi
5d69fad73f Update share_menu.sh 2025-09-01 18:45:14 +02:00
MacRimi
a796761023 Update share-common.func 2025-09-01 17:39:45 +02:00
MacRimi
5d1338e485 Merge branch 'main' of https://github.com/MacRimi/ProxMenux 2025-09-01 17:21:41 +02:00
MacRimi
ce25a167f1 Update auto_post_install.sh 2025-09-01 17:21:39 +02:00
MacRimi
1c44969580 Update share_menu.sh 2025-09-01 14:43:46 +02:00
MacRimi
b6e04e3ede Update share-common.func 2025-09-01 14:30:16 +02:00
ProxMenuxBot
84c26be703 Update helpers_cache.json 2025-09-01 12:26:47 +00:00
MacRimi
d201160722 Update share-common.func 2025-09-01 14:08:10 +02:00
MacRimi
e112361b43 Update share-common.func 2025-09-01 13:53:54 +02:00
MacRimi
3e69795c9d Update lxc-mount-manager.sh 2025-09-01 13:33:48 +02:00
MacRimi
b11baf2e5d Update auto_post_install.sh 2025-09-01 12:42:46 +02:00
MacRimi
233770b553 Update auto_post_install.sh 2025-09-01 12:41:26 +02:00
MacRimi
187db73798 Update zimaos.sh 2025-09-01 12:20:45 +02:00
MacRimi
0e3fc6f682 Update zimaos.sh 2025-09-01 12:15:30 +02:00
MacRimi
d11e3a4ac4 Update auto_post_install.sh 2025-08-31 23:45:37 +02:00
MacRimi
d3b4ca3e66 Update customizable_post_install.sh 2025-08-31 21:46:28 +02:00
MacRimi
f37fbbfb8b Update menu share 2025-08-30 18:56:49 +02:00
MacRimi
52b7aac424 Update share-common.func 2025-08-30 18:05:02 +02:00
MacRimi
d42f3f8f0c Update share-common.func 2025-08-30 18:02:20 +02:00
MacRimi
91b5c7c9bc Update share-common.func 2025-08-30 17:51:45 +02:00
MacRimi
48feebc092 Update share-common.func 2025-08-30 16:59:46 +02:00
MacRimi
14e2d66d96 Update share-common.func 2025-08-30 11:43:02 +02:00
MacRimi
10d844a195 Update share-common.func 2025-08-30 11:36:52 +02:00
MacRimi
bbf91ae5d6 Update main_menu.sh 2025-08-30 09:40:42 +02:00
ProxMenuxBot
cb82eda49a Update helpers_cache.json 2025-08-30 00:57:37 +00:00
MacRimi
bc1dbb1c27 Update help_info_menu.sh 2025-08-30 00:17:51 +02:00
ProxMenuxBot
9496a7f1ce Update helpers_cache.json 2025-08-28 18:19:20 +00:00
ProxMenuxBot
7241fa31b4 Update helpers_cache.json 2025-08-28 12:26:37 +00:00
MacRimi
fed7216436 Update share-common.func 2025-08-27 18:15:26 +02:00
MacRimi
ffe7d7c4c6 Create group_manager.sh 2025-08-27 10:54:03 +02:00
MacRimi
f430ac8d6c Update upgrade_pve8_to_pve9.sh 2025-08-26 19:40:38 +02:00
MacRimi
70dfd7c9a3 Update upgrade_pve8_to_pve9.sh 2025-08-26 19:25:59 +02:00
MacRimi
ed3140932b Update upgrade_pve8_to_pve9.sh 2025-08-26 19:24:41 +02:00
MacRimi
3cd2bd6ce8 Update share-common.func 2025-08-26 17:46:48 +02:00
MacRimi
982bf45fc4 Update share-common.func 2025-08-26 17:37:09 +02:00
MacRimi
aaba8569fc Update share-common.func 2025-08-26 17:19:11 +02:00
MacRimi
4111e15eb9 Update share-common.func 2025-08-26 17:15:47 +02:00
MacRimi
2012478f26 Update share-common.func 2025-08-26 17:05:17 +02:00
MacRimi
88869d3239 Update share-common.func 2025-08-26 17:02:24 +02:00
ProxMenuxBot
f3c2549b18 Update helpers_cache.json 2025-08-26 12:28:24 +00:00
MacRimi
57e3b839d0 Update share-common.func 2025-08-26 13:56:00 +02:00
MacRimi
faf3f43413 Create share-common.func 2025-08-26 13:26:22 +02:00
ProxMenuxBot
52e5bb3386 Update helpers_cache.json 2025-08-26 01:02:34 +00:00
ProxMenuxBot
89405f6670 Update helpers_cache.json 2025-08-25 18:20:02 +00:00
ProxMenuxBot
73111c4139 Update helpers_cache.json 2025-08-25 12:27:14 +00:00
ProxMenuxBot
04e9c5db8c Update helpers_cache.json 2025-08-24 12:24:09 +00:00
MacRimi
69278902de Update customizable_post_install.sh 2025-08-24 10:55:25 +02:00
MacRimi
efa95b0858 Update customizable_post_install.sh 2025-08-24 10:29:00 +02:00
MacRimi
660128cd5c Update customizable_post_install.sh 2025-08-24 10:28:14 +02:00
MacRimi
ef1e052e47 Update customizable_post_install.sh 2025-08-24 10:06:20 +02:00
ProxMenuxBot
0b346bc343 Update helpers_cache.json 2025-08-24 06:19:28 +00:00
MacRimi
2272eaf833 Update lxc-unprivileged-to-privileged.sh 2025-08-22 19:08:03 +02:00
MacRimi
4adee98bce new menu lxc 2025-08-22 19:05:36 +02:00
MacRimi
cbdb2c0705 Rename lxc-manual-guide.sh to lxc-manual-guide.sh 2025-08-22 19:04:10 +02:00
MacRimi
4f438aabbf update manual lxc guide 2025-08-22 19:03:08 +02:00
MacRimi
b6ccc06963 Update lxc_menu.sh 2025-08-22 18:58:10 +02:00
MacRimi
5b89a15bfc menu lxc 2025-08-22 18:57:00 +02:00
MacRimi
5596ae551d Update storage_menu.sh 2025-08-22 18:34:12 +02:00
MacRimi
1360df592a Create backup_host4.sh 2025-08-22 18:17:01 +02:00
MacRimi
13684ff83c Update share_menu.sh 2025-08-22 18:10:34 +02:00
MacRimi
ae88f7870e Update share_menu.sh 2025-08-22 18:08:20 +02:00
MacRimi
810b6da60c Share menu 2025-08-22 18:05:14 +02:00
ProxMenuxBot
7bdf3e08f9 Update helpers_cache.json 2025-08-21 18:19:22 +00:00
MacRimi
fdad2a087f Update zimaos.sh 2025-08-21 19:39:50 +02:00
MacRimi
c437a8c426 Update zimaos.sh 2025-08-21 19:36:28 +02:00
MacRimi
ef861e6d1d Update zimaos.sh 2025-08-21 19:02:58 +02:00
MacRimi
928a008688 Update zimaos.sh 2025-08-21 18:59:54 +02:00
MacRimi
638a124adb Update zimaos.sh 2025-08-21 18:31:32 +02:00
MacRimi
c2a63ae9bb Update zimaos.sh 2025-08-21 18:30:43 +02:00
MacRimi
28cf31e6e7 Update zimaos.sh 2025-08-21 18:27:30 +02:00
MacRimi
3cf416167d Update select_nas_iso.sh 2025-08-21 18:22:57 +02:00
MacRimi
ebf03923a0 Update select_nas_iso.sh 2025-08-21 18:14:58 +02:00
MacRimi
82797d2421 Update select_nas_iso.sh 2025-08-21 18:05:45 +02:00
MacRimi
52b6be946c Create zimaos.sh 2025-08-21 18:00:58 +02:00
MacRimi
dc46724d7b Update select_linux_iso.sh 2025-08-20 23:02:38 +02:00
MacRimi
ed7d43b6a9 Update select_linux_iso.sh 2025-08-20 22:58:14 +02:00
MacRimi
6f3fc51278 Update system_utils.sh 2025-08-20 22:40:42 +02:00
MacRimi
a446acc282 Update customizable_post_install.sh 2025-08-20 22:39:28 +02:00
107 changed files with 18283 additions and 410 deletions

56
.github/workflows/build-appimage.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
name: Build ProxMenux Monitor AppImage
on:
push:
branches: [ main ]
paths: [ 'AppImage/**' ]
pull_request:
branches: [ main ]
paths: [ 'AppImage/**' ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
working-directory: AppImage
run: npm install --legacy-peer-deps
- name: Build Next.js app
working-directory: AppImage
run: npm run build
- name: Install Python dependencies
run: |
sudo apt-get update
sudo apt-get install -y python3 python3-pip python3-venv
- name: Make build script executable
working-directory: AppImage
run: chmod +x scripts/build_appimage.sh
- name: Build AppImage
working-directory: AppImage
run: ./scripts/build_appimage.sh
- name: Get version from package.json
id: version
working-directory: AppImage
run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Upload AppImage artifact
uses: actions/upload-artifact@v4
with:
name: ProxMenux-${{ steps.version.outputs.VERSION }}-AppImage
path: AppImage/dist/*.AppImage
retention-days: 30

22
AppImage/README.md Normal file
View File

@@ -0,0 +1,22 @@
# ProxMenux Monitor
A modern, responsive dashboard for monitoring Proxmox VE systems built with Next.js and React.
## Features
- **System Overview**: Real-time monitoring of CPU, memory, temperature, and active VMs/LXC containers
- **Storage Management**: Visual representation of storage distribution and disk performance metrics
- **Network Monitoring**: Network interface statistics and performance graphs
- **Virtual Machines**: Comprehensive view of VMs and LXC containers with resource usage
- **System Logs**: Real-time system log monitoring and filtering
- **Dark/Light Theme**: Toggle between themes with Proxmox-inspired design
- **Responsive Design**: Works seamlessly on desktop and mobile devices
## Technology Stack
- **Frontend**: Next.js 15, React 19, TypeScript
- **Styling**: Tailwind CSS with custom Proxmox-inspired theme
- **Charts**: Recharts for data visualization
- **UI Components**: Radix UI primitives with shadcn/ui
- **Backend**: Flask server for system data collection
- **Packaging**: AppImage for easy distribution

View File

@@ -0,0 +1,9 @@
import { ProxmoxDashboard } from "../../components/proxmox-dashboard"
export default function DashboardPage() {
return (
<main className="min-h-screen bg-background">
<ProxmoxDashboard />
</main>
)
}

124
AppImage/app/globals.css Normal file
View File

@@ -0,0 +1,124 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
}
@theme inline {
/* optional: --font-sans, --font-serif, --font-mono if they are applied in the layout.tsx */
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

46
AppImage/app/layout.tsx Normal file
View File

@@ -0,0 +1,46 @@
import type React from "react"
import type { Metadata } from "next"
import { GeistSans } from "geist/font/sans"
import { GeistMono } from "geist/font/mono"
import { Analytics } from "@vercel/analytics/next"
import { ThemeProvider } from "../components/theme-provider"
import { Suspense } from "react"
import "./globals.css"
export const metadata: Metadata = {
title: "ProxMenux Monitor",
description: "Proxmox System Dashboard and Monitor",
generator: "v0.app",
manifest: "/manifest.json",
icons: {
icon: [
{ url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" },
{ url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" },
],
apple: [{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" }],
},
viewport: "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no",
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "#ffffff" },
{ media: "(prefers-color-scheme: dark)", color: "#2b2f36" },
],
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en" suppressHydrationWarning>
<body className={`${GeistSans.variable} ${GeistMono.variable} antialiased bg-background text-foreground`}>
<Suspense fallback={<div>Loading...</div>}>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem disableTransitionOnChange>
{children}
</ThemeProvider>
<Analytics />
</Suspense>
</body>
</html>
)
}

7
AppImage/app/page.tsx Normal file
View File

@@ -0,0 +1,7 @@
"use client"
import { ProxmoxDashboard } from "../components/proxmox-dashboard"
export default function Home() {
return <ProxmoxDashboard />
}

View File

@@ -0,0 +1,239 @@
"use client"
import { useState, useEffect } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Badge } from "./ui/badge"
import { Wifi, Globe, Shield, Activity, Network, Router, AlertCircle } from "lucide-react"
interface NetworkData {
interfaces: NetworkInterface[]
traffic: {
bytes_sent: number
bytes_recv: number
packets_sent?: number
packets_recv?: number
}
}
interface NetworkInterface {
name: string
status: string
addresses: Array<{
ip: string
netmask: string
}>
}
const fetchNetworkData = async (): Promise<NetworkData | null> => {
try {
console.log("[v0] Fetching network data from Flask server...")
const response = await fetch("/api/network", {
method: "GET",
headers: {
"Content-Type": "application/json",
},
signal: AbortSignal.timeout(5000),
})
if (!response.ok) {
throw new Error(`Flask server responded with status: ${response.status}`)
}
const data = await response.json()
console.log("[v0] Successfully fetched network data from Flask:", data)
return data
} catch (error) {
console.error("[v0] Failed to fetch network data from Flask server:", error)
return null
}
}
export function NetworkMetrics() {
const [networkData, setNetworkData] = useState<NetworkData | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const fetchData = async () => {
setLoading(true)
setError(null)
const result = await fetchNetworkData()
if (!result) {
setError("Flask server not available. Please ensure the server is running.")
} else {
setNetworkData(result)
}
setLoading(false)
}
fetchData()
const interval = setInterval(fetchData, 30000)
return () => clearInterval(interval)
}, [])
if (loading) {
return (
<div className="space-y-6">
<div className="text-center py-8">
<div className="text-lg font-medium text-foreground mb-2">Loading network data...</div>
</div>
</div>
)
}
if (error || !networkData) {
return (
<div className="space-y-6">
<Card className="bg-red-500/10 border-red-500/20">
<CardContent className="p-6">
<div className="flex items-center gap-3 text-red-600">
<AlertCircle className="h-6 w-6" />
<div>
<div className="font-semibold text-lg mb-1">Flask Server Not Available</div>
<div className="text-sm">
{error || "Unable to connect to the Flask server. Please ensure the server is running and try again."}
</div>
</div>
</div>
</CardContent>
</Card>
</div>
)
}
const trafficInMB = (networkData.traffic.bytes_recv / (1024 * 1024)).toFixed(1)
const trafficOutMB = (networkData.traffic.bytes_sent / (1024 * 1024)).toFixed(1)
return (
<div className="space-y-6">
{/* Network Overview Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Network Traffic</CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-foreground">{trafficInMB} MB</div>
<div className="flex items-center space-x-2 mt-2">
<span className="text-xs text-green-500"> {trafficInMB} MB</span>
<span className="text-xs text-blue-500"> {trafficOutMB} MB</span>
</div>
<p className="text-xs text-muted-foreground mt-2">Total data transferred</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Active Interfaces</CardTitle>
<Network className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-foreground">
{networkData.interfaces.filter((i) => i.status === "up").length}
</div>
<div className="flex items-center mt-2">
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
Online
</Badge>
</div>
<p className="text-xs text-muted-foreground mt-2">{networkData.interfaces.length} total interfaces</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Firewall Status</CardTitle>
<Shield className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-foreground">Active</div>
<div className="flex items-center mt-2">
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
Protected
</Badge>
</div>
<p className="text-xs text-muted-foreground mt-2">System protected</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<Globe className="h-5 w-5 mr-2" />
Packets
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-foreground">
{networkData.traffic.packets_recv ? (networkData.traffic.packets_recv / 1000).toFixed(0) : "N/A"}K
</div>
<div className="flex items-center mt-2">
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
Received
</Badge>
</div>
<p className="text-xs text-muted-foreground mt-2">Total packets received</p>
</CardContent>
</Card>
</div>
{/* Network Interfaces */}
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<Router className="h-5 w-5 mr-2" />
Network Interfaces
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{networkData.interfaces.map((interface_, index) => (
<div
key={index}
className="flex items-center justify-between p-4 rounded-lg border border-border bg-card/50"
>
<div className="flex items-center space-x-4">
<Wifi className="h-5 w-5 text-muted-foreground" />
<div>
<div className="font-medium text-foreground">{interface_.name}</div>
<div className="text-sm text-muted-foreground">Network Interface</div>
</div>
</div>
<div className="flex items-center space-x-6">
<div className="text-center">
<div className="text-sm text-muted-foreground">IP Address</div>
<div className="text-sm font-medium text-foreground font-mono">
{interface_.addresses.length > 0 ? interface_.addresses[0].ip : "N/A"}
</div>
</div>
<div className="text-center">
<div className="text-sm text-muted-foreground">Netmask</div>
<div className="text-sm font-medium text-foreground">
{interface_.addresses.length > 0 ? interface_.addresses[0].netmask : "N/A"}
</div>
</div>
<Badge
variant="outline"
className={
interface_.status === "up"
? "bg-green-500/10 text-green-500 border-green-500/20"
: "bg-red-500/10 text-red-500 border-red-500/20"
}
>
{interface_.status.toUpperCase()}
</Badge>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,294 @@
"use client"
import { useState, useEffect, useMemo, useCallback } from "react"
import { Badge } from "./ui/badge"
import { Button } from "./ui/button"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"
import { SystemOverview } from "./system-overview"
import { StorageMetrics } from "./storage-metrics"
import { NetworkMetrics } from "./network-metrics"
import { VirtualMachines } from "./virtual-machines"
import { SystemLogs } from "./system-logs"
import { RefreshCw, AlertTriangle, CheckCircle, XCircle, Server } from "lucide-react"
import Image from "next/image"
import { ThemeToggle } from "./theme-toggle"
interface SystemStatus {
status: "healthy" | "warning" | "critical"
uptime: string
lastUpdate: string
serverName: string
nodeId: string
}
interface FlaskSystemData {
hostname: string
node_id: string
uptime: string
cpu_usage: number
memory_usage: number
temperature: number
load_average: number[]
}
export function ProxmoxDashboard() {
const [systemStatus, setSystemStatus] = useState<SystemStatus>({
status: "healthy",
uptime: "Loading...",
lastUpdate: new Date().toLocaleTimeString(),
serverName: "Loading...",
nodeId: "Loading...",
})
const [isRefreshing, setIsRefreshing] = useState(false)
const [isServerConnected, setIsServerConnected] = useState(true)
const [componentKey, setComponentKey] = useState(0)
const fetchSystemData = useCallback(async () => {
console.log("[v0] Fetching system data from Flask server...")
console.log("[v0] Current window location:", window.location.href)
const apiUrl = "/api/system"
console.log("[v0] API URL:", apiUrl)
try {
const response = await fetch(apiUrl, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
})
console.log("[v0] Response status:", response.status)
if (!response.ok) {
throw new Error(`Server responded with status: ${response.status}`)
}
const data: FlaskSystemData = await response.json()
console.log("[v0] System data received:", data)
let status: "healthy" | "warning" | "critical" = "healthy"
if (data.cpu_usage > 90 || data.memory_usage > 90) {
status = "critical"
} else if (data.cpu_usage > 75 || data.memory_usage > 75) {
status = "warning"
}
setSystemStatus({
status,
uptime: data.uptime,
lastUpdate: new Date().toLocaleTimeString(),
serverName: data.hostname,
nodeId: data.node_id,
})
setIsServerConnected(true)
} catch (error) {
console.error("[v0] Failed to fetch system data from Flask server:", error)
console.error("[v0] Error details:", {
message: error instanceof Error ? error.message : "Unknown error",
apiUrl,
windowLocation: window.location.href,
})
setIsServerConnected(false)
setSystemStatus((prev) => ({
...prev,
status: "critical",
serverName: "Server Offline",
nodeId: "Server Offline",
uptime: "N/A",
lastUpdate: new Date().toLocaleTimeString(),
}))
}
}, [])
useEffect(() => {
fetchSystemData()
const interval = setInterval(fetchSystemData, 5000)
return () => clearInterval(interval)
}, [fetchSystemData])
const refreshData = async () => {
setIsRefreshing(true)
await fetchSystemData()
setComponentKey((prev) => prev + 1)
await new Promise((resolve) => setTimeout(resolve, 500))
setIsRefreshing(false)
}
const statusIcon = useMemo(() => {
switch (systemStatus.status) {
case "healthy":
return <CheckCircle className="h-4 w-4 text-green-500" />
case "warning":
return <AlertTriangle className="h-4 w-4 text-yellow-500" />
case "critical":
return <XCircle className="h-4 w-4 text-red-500" />
}
}, [systemStatus.status])
const statusColor = useMemo(() => {
switch (systemStatus.status) {
case "healthy":
return "bg-green-500/10 text-green-500 border-green-500/20"
case "warning":
return "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
case "critical":
return "bg-red-500/10 text-red-500 border-red-500/20"
}
}, [systemStatus.status])
return (
<div className="min-h-screen bg-background">
{!isServerConnected && (
<div className="bg-red-500/10 border-b border-red-500/20 px-6 py-3">
<div className="container mx-auto">
<div className="flex items-center space-x-2 text-red-500 mb-2">
<XCircle className="h-5 w-5" />
<span className="font-medium">Flask Server Connection Failed</span>
</div>
<div className="text-sm text-red-500/80 space-y-1 ml-7">
<p> Check that the AppImage is running correctly</p>
<p> The Flask server should start automatically on port 8008</p>
<p>
Try accessing:{" "}
<a
href={`http://${typeof window !== "undefined" ? window.location.host : "localhost:8008"}/api/health`}
target="_blank"
rel="noopener noreferrer"
className="underline"
>
http://{typeof window !== "undefined" ? window.location.host : "localhost:8008"}/api/health
</a>
</p>
</div>
</div>
</div>
)}
<header className="border-b border-border bg-card sticky top-0 z-50 shadow-sm">
<div className="container mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 relative flex items-center justify-center bg-primary/10 overflow-hidden">
<Image
src="/images/proxmenux-logo.png"
alt="ProxMenux Logo"
width={40}
height={40}
className="object-contain"
priority
onError={(e) => {
console.log("[v0] Logo failed to load, using fallback icon")
const target = e.target as HTMLImageElement
target.style.display = "none"
const fallback = target.parentElement?.querySelector(".fallback-icon")
if (fallback) {
fallback.classList.remove("hidden")
}
}}
/>
<Server className="h-6 w-6 text-primary absolute fallback-icon hidden" />
</div>
<div>
<h1 className="text-xl font-semibold text-foreground">ProxMenux Monitor</h1>
<p className="text-sm text-muted-foreground">Proxmox System Dashboard</p>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="hidden md:flex items-center space-x-2">
<Server className="h-4 w-4 text-muted-foreground" />
<div className="text-sm">
<div className="font-medium text-foreground">{systemStatus.serverName}</div>
<div className="text-xs text-muted-foreground">{systemStatus.nodeId}</div>
</div>
</div>
<Badge variant="outline" className={statusColor}>
{statusIcon}
<span className="ml-1 capitalize">{systemStatus.status}</span>
</Badge>
<div className="text-sm text-muted-foreground">Uptime: {systemStatus.uptime}</div>
<Button
variant="outline"
size="sm"
onClick={refreshData}
disabled={isRefreshing}
className="border-border/50 bg-transparent hover:bg-secondary"
>
<RefreshCw className={`h-4 w-4 mr-2 ${isRefreshing ? "animate-spin" : ""}`} />
Refresh
</Button>
<ThemeToggle />
</div>
</div>
</div>
</header>
<div className="container mx-auto px-6 py-6">
<Tabs defaultValue="overview" className="space-y-6">
<TabsList className="grid w-full grid-cols-5 bg-card border border-border">
<TabsTrigger
value="overview"
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground"
>
Overview
</TabsTrigger>
<TabsTrigger
value="storage"
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground"
>
Storage
</TabsTrigger>
<TabsTrigger
value="network"
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground"
>
Network
</TabsTrigger>
<TabsTrigger
value="vms"
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground"
>
Virtual Machines
</TabsTrigger>
<TabsTrigger
value="logs"
className="data-[state=active]:bg-primary data-[state=active]:text-primary-foreground"
>
System Logs
</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-6">
<SystemOverview key={`overview-${componentKey}`} />
</TabsContent>
<TabsContent value="storage" className="space-y-6">
<StorageMetrics key={`storage-${componentKey}`} />
</TabsContent>
<TabsContent value="network" className="space-y-6">
<NetworkMetrics key={`network-${componentKey}`} />
</TabsContent>
<TabsContent value="vms" className="space-y-6">
<VirtualMachines key={`vms-${componentKey}`} />
</TabsContent>
<TabsContent value="logs" className="space-y-6">
<SystemLogs key={`logs-${componentKey}`} />
</TabsContent>
</Tabs>
<footer className="mt-12 pt-6 border-t border-border text-center text-sm text-muted-foreground">
<p>Last updated: {systemStatus.lastUpdate} ProxMenux Monitor v1.0.0</p>
</footer>
</div>
</div>
)
}

View File

@@ -0,0 +1,237 @@
"use client"
import { useState, useEffect } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Progress } from "./ui/progress"
import { Badge } from "./ui/badge"
import { HardDrive, Database, Archive, AlertTriangle, CheckCircle, Activity, AlertCircle } from "lucide-react"
interface StorageData {
total: number
used: number
available: number
disks: DiskInfo[]
}
interface DiskInfo {
name: string
mountpoint: string
fstype: string
total: number
used: number
available: number
usage_percent: number
health: string
temperature: number
}
const fetchStorageData = async (): Promise<StorageData | null> => {
try {
console.log("[v0] Fetching storage data from Flask server...")
const response = await fetch("/api/storage", {
method: "GET",
headers: {
"Content-Type": "application/json",
},
signal: AbortSignal.timeout(5000),
})
if (!response.ok) {
throw new Error(`Flask server responded with status: ${response.status}`)
}
const data = await response.json()
console.log("[v0] Successfully fetched storage data from Flask:", data)
return data
} catch (error) {
console.error("[v0] Failed to fetch storage data from Flask server:", error)
return null
}
}
export function StorageMetrics() {
const [storageData, setStorageData] = useState<StorageData | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const fetchData = async () => {
setLoading(true)
setError(null)
const result = await fetchStorageData()
if (!result) {
setError("Flask server not available. Please ensure the server is running.")
} else {
setStorageData(result)
}
setLoading(false)
}
fetchData()
const interval = setInterval(fetchData, 30000)
return () => clearInterval(interval)
}, [])
if (loading) {
return (
<div className="space-y-6">
<div className="text-center py-8">
<div className="text-lg font-medium text-foreground mb-2">Loading storage data...</div>
</div>
</div>
)
}
if (error || !storageData) {
return (
<div className="space-y-6">
<Card className="bg-red-500/10 border-red-500/20">
<CardContent className="p-6">
<div className="flex items-center gap-3 text-red-600">
<AlertCircle className="h-6 w-6" />
<div>
<div className="font-semibold text-lg mb-1">Flask Server Not Available</div>
<div className="text-sm">
{error || "Unable to connect to the Flask server. Please ensure the server is running and try again."}
</div>
</div>
</div>
</CardContent>
</Card>
</div>
)
}
const usagePercent = storageData.total > 0 ? (storageData.used / storageData.total) * 100 : 0
return (
<div className="space-y-6">
{/* Storage Overview Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Total Storage</CardTitle>
<HardDrive className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-foreground">{storageData.total.toFixed(1)} GB</div>
<Progress value={usagePercent} className="mt-2" />
<p className="text-xs text-muted-foreground mt-2">
{storageData.used.toFixed(1)} GB used {storageData.available.toFixed(1)} GB available
</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Used Storage</CardTitle>
<Database className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-foreground">{storageData.used.toFixed(1)} GB</div>
<Progress value={usagePercent} className="mt-2" />
<p className="text-xs text-muted-foreground mt-2">{usagePercent.toFixed(1)}% of total space</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<Archive className="h-5 w-5 mr-2" />
Available
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-foreground">{storageData.available.toFixed(1)} GB</div>
<div className="flex items-center mt-2">
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
{((storageData.available / storageData.total) * 100).toFixed(1)}% Free
</Badge>
</div>
<p className="text-xs text-muted-foreground mt-2">Available space</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<Activity className="h-5 w-5 mr-2" />
Disks
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-foreground">{storageData.disks.length}</div>
<div className="flex items-center space-x-2 mt-2">
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
{storageData.disks.filter((d) => d.health === "healthy").length} Healthy
</Badge>
</div>
<p className="text-xs text-muted-foreground mt-2">Storage devices</p>
</CardContent>
</Card>
</div>
{/* Disk Details */}
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<Database className="h-5 w-5 mr-2" />
Storage Devices
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{storageData.disks.map((disk, index) => (
<div
key={index}
className="flex items-center justify-between p-4 rounded-lg border border-border bg-card/50"
>
<div className="flex items-center space-x-4">
<HardDrive className="h-5 w-5 text-muted-foreground" />
<div>
<div className="font-medium text-foreground">{disk.name}</div>
<div className="text-sm text-muted-foreground">
{disk.fstype} {disk.mountpoint}
</div>
</div>
</div>
<div className="flex items-center space-x-6">
<div className="text-right">
<div className="text-sm font-medium text-foreground">
{disk.used.toFixed(1)} GB / {disk.total.toFixed(1)} GB
</div>
<Progress value={disk.usage_percent} className="w-24 mt-1" />
</div>
<div className="text-center">
<div className="text-sm text-muted-foreground">Temp</div>
<div className="text-sm font-medium text-foreground">{disk.temperature}°C</div>
</div>
<Badge
variant="outline"
className={
disk.health === "healthy"
? "bg-green-500/10 text-green-500 border-green-500/20"
: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
}
>
{disk.health === "healthy" ? (
<CheckCircle className="h-3 w-3 mr-1" />
) : (
<AlertTriangle className="h-3 w-3 mr-1" />
)}
{disk.health}
</Badge>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,275 @@
"use client"
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Badge } from "./ui/badge"
import { Button } from "./ui/button"
import { Input } from "./ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
import { ScrollArea } from "./ui/scroll-area"
import { FileText, Search, Download, AlertTriangle, Info, CheckCircle, XCircle } from "lucide-react"
import { useState } from "react"
const systemLogs = [
{
timestamp: "2024-01-15 14:32:15",
level: "info",
service: "pveproxy",
message: "User root@pam authenticated successfully",
source: "auth.log",
},
{
timestamp: "2024-01-15 14:31:45",
level: "warning",
service: "pvedaemon",
message: "VM 101 high memory usage detected (85%)",
source: "syslog",
},
{
timestamp: "2024-01-15 14:30:22",
level: "error",
service: "pve-cluster",
message: "Failed to connect to cluster node pve-02",
source: "cluster.log",
},
{
timestamp: "2024-01-15 14:29:18",
level: "info",
service: "pvestatd",
message: "Storage local: 1.25TB used, 750GB available",
source: "syslog",
},
{
timestamp: "2024-01-15 14:28:33",
level: "info",
service: "pve-firewall",
message: "Blocked connection attempt from 192.168.1.50",
source: "firewall.log",
},
{
timestamp: "2024-01-15 14:27:45",
level: "warning",
service: "smartd",
message: "SMART warning: /dev/nvme0n1 temperature high (55°C)",
source: "smart.log",
},
{
timestamp: "2024-01-15 14:26:12",
level: "info",
service: "pveproxy",
message: "Started backup job for VM 100",
source: "backup.log",
},
{
timestamp: "2024-01-15 14:25:38",
level: "error",
service: "qemu-server",
message: "VM 102 failed to start: insufficient memory",
source: "qemu.log",
},
{
timestamp: "2024-01-15 14:24:55",
level: "info",
service: "pvedaemon",
message: "VM 103 migrated successfully to node pve-01",
source: "migration.log",
},
{
timestamp: "2024-01-15 14:23:17",
level: "warning",
service: "pve-ha-lrm",
message: "Resource VM:104 state changed to error",
source: "ha.log",
},
]
export function SystemLogs() {
const [searchTerm, setSearchTerm] = useState("")
const [levelFilter, setLevelFilter] = useState("all")
const [serviceFilter, setServiceFilter] = useState("all")
const filteredLogs = systemLogs.filter((log) => {
const matchesSearch =
log.message.toLowerCase().includes(searchTerm.toLowerCase()) ||
log.service.toLowerCase().includes(searchTerm.toLowerCase())
const matchesLevel = levelFilter === "all" || log.level === levelFilter
const matchesService = serviceFilter === "all" || log.service === serviceFilter
return matchesSearch && matchesLevel && matchesService
})
const getLevelColor = (level: string) => {
switch (level) {
case "error":
return "bg-red-500/10 text-red-500 border-red-500/20"
case "warning":
return "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
case "info":
return "bg-blue-500/10 text-blue-500 border-blue-500/20"
default:
return "bg-gray-500/10 text-gray-500 border-gray-500/20"
}
}
const getLevelIcon = (level: string) => {
switch (level) {
case "error":
return <XCircle className="h-3 w-3 mr-1" />
case "warning":
return <AlertTriangle className="h-3 w-3 mr-1" />
case "info":
return <Info className="h-3 w-3 mr-1" />
default:
return <CheckCircle className="h-3 w-3 mr-1" />
}
}
const logCounts = {
total: systemLogs.length,
error: systemLogs.filter((log) => log.level === "error").length,
warning: systemLogs.filter((log) => log.level === "warning").length,
info: systemLogs.filter((log) => log.level === "info").length,
}
const uniqueServices = [...new Set(systemLogs.map((log) => log.service))]
return (
<div className="space-y-6">
{/* Log Statistics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Total Logs</CardTitle>
<FileText className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-foreground">{logCounts.total}</div>
<p className="text-xs text-muted-foreground mt-2">Last 24 hours</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Errors</CardTitle>
<XCircle className="h-4 w-4 text-red-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-500">{logCounts.error}</div>
<p className="text-xs text-muted-foreground mt-2">Requires attention</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Warnings</CardTitle>
<AlertTriangle className="h-4 w-4 text-yellow-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-yellow-500">{logCounts.warning}</div>
<p className="text-xs text-muted-foreground mt-2">Monitor closely</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Info</CardTitle>
<Info className="h-4 w-4 text-blue-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-blue-500">{logCounts.info}</div>
<p className="text-xs text-muted-foreground mt-2">Normal operations</p>
</CardContent>
</Card>
</div>
{/* Log Filters and Search */}
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<FileText className="h-5 w-5 mr-2" />
System Logs
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-col sm:flex-row gap-4 mb-6">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search logs..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 bg-background border-border"
/>
</div>
</div>
<Select value={levelFilter} onValueChange={setLevelFilter}>
<SelectTrigger className="w-full sm:w-[180px] bg-background border-border">
<SelectValue placeholder="Filter by level" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Levels</SelectItem>
<SelectItem value="error">Error</SelectItem>
<SelectItem value="warning">Warning</SelectItem>
<SelectItem value="info">Info</SelectItem>
</SelectContent>
</Select>
<Select value={serviceFilter} onValueChange={setServiceFilter}>
<SelectTrigger className="w-full sm:w-[180px] bg-background border-border">
<SelectValue placeholder="Filter by service" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Services</SelectItem>
{uniqueServices.map((service) => (
<SelectItem key={service} value={service}>
{service}
</SelectItem>
))}
</SelectContent>
</Select>
<Button variant="outline" className="border-border bg-transparent">
<Download className="h-4 w-4 mr-2" />
Export
</Button>
</div>
<ScrollArea className="h-[600px] w-full rounded-md border border-border">
<div className="space-y-2 p-4">
{filteredLogs.map((log, index) => (
<div
key={index}
className="flex items-start space-x-4 p-3 rounded-lg bg-card/50 border border-border/50"
>
<div className="flex-shrink-0">
<Badge variant="outline" className={getLevelColor(log.level)}>
{getLevelIcon(log.level)}
{log.level.toUpperCase()}
</Badge>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1">
<div className="text-sm font-medium text-foreground">{log.service}</div>
<div className="text-xs text-muted-foreground font-mono">{log.timestamp}</div>
</div>
<div className="text-sm text-foreground mb-1">{log.message}</div>
<div className="text-xs text-muted-foreground">Source: {log.source}</div>
</div>
</div>
))}
{filteredLogs.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
<FileText className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>No logs found matching your criteria</p>
</div>
)}
</div>
</ScrollArea>
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,485 @@
"use client"
import { useState, useEffect } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Progress } from "./ui/progress"
import { Badge } from "./ui/badge"
import { XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from "recharts"
import { Cpu, MemoryStick, Thermometer, Activity, Server, Zap, AlertCircle } from "lucide-react"
interface SystemData {
cpu_usage: number
memory_usage: number
memory_total: number
memory_used: number
temperature: number
uptime: string
load_average: number[]
hostname: string
node_id: string
timestamp: string
}
interface VMData {
vmid: number
name: string
status: string
cpu: number
mem: number
maxmem: number
disk: number
maxdisk: number
uptime: number
}
interface HistoricalData {
timestamp: string
cpu_usage: number
memory_used: number
memory_total: number
}
const historicalDataStore: HistoricalData[] = []
const MAX_HISTORICAL_POINTS = 24 // Store 24 data points for 24h view
const fetchSystemData = async (): Promise<SystemData | null> => {
try {
console.log("[v0] Fetching system data from Flask server...")
const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
const apiUrl = `${baseUrl}/api/system`
console.log("[v0] Fetching from URL:", apiUrl)
const response = await fetch(apiUrl, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
cache: "no-store",
})
console.log("[v0] Response status:", response.status)
console.log("[v0] Response ok:", response.ok)
console.log("[v0] Response headers:", Object.fromEntries(response.headers.entries()))
if (!response.ok) {
const errorText = await response.text()
console.error("[v0] Flask server error response:", errorText)
throw new Error(`Flask server responded with status: ${response.status}`)
}
const responseText = await response.text()
console.log("[v0] Raw response text:", responseText)
console.log("[v0] Response text length:", responseText.length)
console.log("[v0] First 100 chars:", responseText.substring(0, 100))
// Try to parse the JSON
let data
try {
data = JSON.parse(responseText)
console.log("[v0] Successfully parsed JSON:", data)
} catch (parseError) {
console.error("[v0] JSON parse error:", parseError)
console.error("[v0] Failed to parse response as JSON")
throw new Error("Invalid JSON response from server")
}
// Store historical data
historicalDataStore.push({
timestamp: data.timestamp,
cpu_usage: data.cpu_usage,
memory_used: data.memory_used,
memory_total: data.memory_total,
})
// Keep only last MAX_HISTORICAL_POINTS
if (historicalDataStore.length > MAX_HISTORICAL_POINTS) {
historicalDataStore.shift()
}
return data
} catch (error) {
console.error("[v0] Failed to fetch system data from Flask server:", error)
console.error("[v0] Error type:", error instanceof Error ? error.constructor.name : typeof error)
console.error("[v0] Error message:", error instanceof Error ? error.message : String(error))
console.error("[v0] Error stack:", error instanceof Error ? error.stack : "No stack trace")
return null
}
}
const fetchVMData = async (): Promise<VMData[]> => {
try {
console.log("[v0] Fetching VM data from Flask server...")
const baseUrl = typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8008` : ""
const apiUrl = `${baseUrl}/api/vms`
console.log("[v0] Fetching from URL:", apiUrl)
const response = await fetch(apiUrl, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
cache: "no-store",
})
console.log("[v0] VM Response status:", response.status)
if (!response.ok) {
const errorText = await response.text()
console.error("[v0] Flask server error response:", errorText)
throw new Error(`Flask server responded with status: ${response.status}`)
}
const data = await response.json()
console.log("[v0] Successfully fetched VM data from Flask:", data)
return Array.isArray(data) ? data : data.vms || []
} catch (error) {
console.error("[v0] Failed to fetch VM data from Flask server:", error)
console.error("[v0] Error type:", error instanceof Error ? error.constructor.name : typeof error)
console.error("[v0] Error message:", error instanceof Error ? error.message : String(error))
return []
}
}
const generateChartData = () => {
if (historicalDataStore.length === 0) {
return { cpuData: [], memoryData: [] }
}
const cpuData = historicalDataStore.map((point) => ({
time: new Date(point.timestamp).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" }),
value: point.cpu_usage,
}))
const memoryData = historicalDataStore.map((point) => ({
time: new Date(point.timestamp).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" }),
used: point.memory_used,
available: point.memory_total - point.memory_used,
}))
return { cpuData, memoryData }
}
export function SystemOverview() {
const [systemData, setSystemData] = useState<SystemData | null>(null)
const [vmData, setVmData] = useState<VMData[]>([])
const [chartData, setChartData] = useState(generateChartData())
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true)
setError(null)
const [systemResult, vmResult] = await Promise.all([fetchSystemData(), fetchVMData()])
if (!systemResult) {
setError("Flask server not available. Please ensure the server is running.")
setLoading(false)
return
}
setSystemData(systemResult)
setVmData(vmResult)
setChartData(generateChartData())
} catch (err) {
console.error("[v0] Error fetching data:", err)
setError("Failed to connect to Flask server. Please check your connection.")
} finally {
setLoading(false)
}
}
fetchData()
const interval = setInterval(() => {
fetchData()
}, 30000) // Update every 30 seconds
return () => {
clearInterval(interval)
}
}, [])
if (loading) {
return (
<div className="space-y-6">
<div className="text-center py-8">
<div className="text-lg font-medium text-foreground mb-2">Connecting to ProxMenux Monitor...</div>
<div className="text-sm text-muted-foreground">Fetching real-time system data</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{[...Array(4)].map((_, i) => (
<Card key={i} className="bg-card border-border animate-pulse">
<CardContent className="p-6">
<div className="h-4 bg-muted rounded w-1/2 mb-4"></div>
<div className="h-8 bg-muted rounded w-3/4 mb-2"></div>
<div className="h-2 bg-muted rounded w-full mb-2"></div>
<div className="h-3 bg-muted rounded w-2/3"></div>
</CardContent>
</Card>
))}
</div>
</div>
)
}
if (error || !systemData) {
return (
<div className="space-y-6">
<Card className="bg-red-500/10 border-red-500/20">
<CardContent className="p-6">
<div className="flex items-center gap-3 text-red-600">
<AlertCircle className="h-6 w-6" />
<div>
<div className="font-semibold text-lg mb-1">Flask Server Not Available</div>
<div className="text-sm">
{error || "Unable to connect to the Flask server. Please ensure the server is running and try again."}
</div>
<div className="text-sm mt-2">
<strong>Troubleshooting:</strong>
<ul className="list-disc list-inside mt-1 space-y-1">
<li>Check if the Flask server is running on the correct port</li>
<li>Verify network connectivity</li>
<li>Check server logs for errors</li>
</ul>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
)
}
const vmStats = {
total: vmData.length,
running: vmData.filter((vm) => vm.status === "running").length,
stopped: vmData.filter((vm) => vm.status === "stopped").length,
lxc: 0,
}
const getTemperatureStatus = (temp: number) => {
if (temp === 0) return { status: "N/A", color: "bg-gray-500/10 text-gray-500 border-gray-500/20" }
if (temp < 60) return { status: "Normal", color: "bg-green-500/10 text-green-500 border-green-500/20" }
if (temp < 75) return { status: "Warm", color: "bg-yellow-500/10 text-yellow-500 border-yellow-500/20" }
return { status: "Hot", color: "bg-red-500/10 text-red-500 border-red-500/20" }
}
const tempStatus = getTemperatureStatus(systemData.temperature)
return (
<div className="space-y-6">
{/* Key Metrics Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">CPU Usage</CardTitle>
<Cpu className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-foreground">{systemData.cpu_usage}%</div>
<Progress value={systemData.cpu_usage} className="mt-2" />
<p className="text-xs text-muted-foreground mt-2">Real-time data from Flask server</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Memory Usage</CardTitle>
<MemoryStick className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-foreground">{systemData.memory_used.toFixed(1)} GB</div>
<Progress value={systemData.memory_usage} className="mt-2" />
<p className="text-xs text-muted-foreground mt-2">
{systemData.memory_usage.toFixed(1)}% of {systemData.memory_total} GB
</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Temperature</CardTitle>
<Thermometer className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-foreground">
{systemData.temperature === 0 ? "N/A" : `${systemData.temperature}°C`}
</div>
<div className="flex items-center mt-2">
<Badge variant="outline" className={tempStatus.color}>
{tempStatus.status}
</Badge>
</div>
<p className="text-xs text-muted-foreground mt-2">
{systemData.temperature === 0 ? "No sensor available" : "Live temperature reading"}
</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Active VMs</CardTitle>
<Server className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-foreground">{vmStats.running}</div>
<div className="mt-2 flex flex-wrap gap-1">
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
{vmStats.running} Running
</Badge>
{vmStats.stopped > 0 && (
<Badge variant="outline" className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20">
{vmStats.stopped} Stopped
</Badge>
)}
</div>
<p className="text-xs text-muted-foreground mt-2">Total: {vmStats.total} VMs configured</p>
</CardContent>
</Card>
</div>
{/* Charts Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<Activity className="h-5 w-5 mr-2" />
CPU Usage (Last {historicalDataStore.length} readings)
</CardTitle>
</CardHeader>
<CardContent>
{chartData.cpuData.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={chartData.cpuData}>
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
<XAxis dataKey="time" stroke="hsl(var(--muted-foreground))" fontSize={12} />
<YAxis stroke="hsl(var(--muted-foreground))" fontSize={12} />
<Tooltip
contentStyle={{
backgroundColor: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
color: "hsl(var(--foreground))",
}}
/>
<Area type="monotone" dataKey="value" stroke="#3b82f6" fill="#3b82f6" fillOpacity={0.2} />
</AreaChart>
</ResponsiveContainer>
) : (
<div className="h-[300px] flex items-center justify-center text-muted-foreground">
Collecting data... Check back in a few minutes
</div>
)}
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<MemoryStick className="h-5 w-5 mr-2" />
Memory Usage (Last {historicalDataStore.length} readings)
</CardTitle>
</CardHeader>
<CardContent>
{chartData.memoryData.length > 0 ? (
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={chartData.memoryData}>
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
<XAxis dataKey="time" stroke="hsl(var(--muted-foreground))" fontSize={12} />
<YAxis stroke="hsl(var(--muted-foreground))" fontSize={12} />
<Tooltip
contentStyle={{
backgroundColor: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
color: "hsl(var(--foreground))",
}}
/>
<Area type="monotone" dataKey="used" stackId="1" stroke="#3b82f6" fill="#3b82f6" fillOpacity={0.6} />
<Area
type="monotone"
dataKey="available"
stackId="1"
stroke="#10b981"
fill="#10b981"
fillOpacity={0.6}
/>
</AreaChart>
</ResponsiveContainer>
) : (
<div className="h-[300px] flex items-center justify-center text-muted-foreground">
Collecting data... Check back in a few minutes
</div>
)}
</CardContent>
</Card>
</div>
{/* System Information */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<Server className="h-5 w-5 mr-2" />
System Information
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex justify-between">
<span className="text-muted-foreground">Hostname:</span>
<span className="text-foreground font-mono">{systemData.hostname}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Uptime:</span>
<span className="text-foreground">{systemData.uptime}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Node ID:</span>
<span className="text-foreground font-mono">{systemData.node_id}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Last Update:</span>
<span className="text-foreground">{new Date(systemData.timestamp).toLocaleTimeString()}</span>
</div>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<Zap className="h-5 w-5 mr-2" />
Performance Metrics
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex justify-between">
<span className="text-muted-foreground">Load Average:</span>
<span className="text-foreground font-mono">
{systemData.load_average.map((avg) => avg.toFixed(2)).join(", ")}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Total Memory:</span>
<span className="text-foreground">{systemData.memory_total} GB</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Available Memory:</span>
<span className="text-foreground">
{(systemData.memory_total - systemData.memory_used).toFixed(1)} GB
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">CPU Cores:</span>
<span className="text-foreground">{navigator.hardwareConcurrency || "N/A"}</span>
</div>
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -0,0 +1,7 @@
"use client"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import type { ThemeProviderProps } from "next-themes"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

View File

@@ -0,0 +1,39 @@
"use client"
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { useEffect, useState } from "react"
import { Button } from "./ui/button"
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
const handleThemeToggle = () => {
console.log("[v0] Current theme:", theme)
const newTheme = theme === "light" ? "dark" : "light"
console.log("[v0] Switching to theme:", newTheme)
setTheme(newTheme)
}
if (!mounted) {
return (
<Button variant="outline" size="sm" className="border-border bg-transparent w-9 h-9">
<Sun className="h-4 w-4" />
<span className="sr-only">Toggle theme</span>
</Button>
)
}
return (
<Button variant="outline" size="sm" onClick={handleThemeToggle} className="border-border bg-transparent w-9 h-9">
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
)
}

View File

@@ -0,0 +1,28 @@
import type * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
)
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
}
export { Badge, badgeVariants }

View File

@@ -0,0 +1,46 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
},
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,42 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)} {...props} />
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
),
)
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
),
)
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
),
)
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />,
)
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
),
)
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
)
})
Input.displayName = "Input"
export { Input }

View File

@@ -0,0 +1,25 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn("relative h-2 w-full overflow-hidden rounded-full bg-secondary", className)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }

View File

@@ -0,0 +1,40 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root ref={ref} className={cn("relative overflow-hidden", className)} {...props}>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">{children}</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@@ -0,0 +1,144 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label ref={ref} className={cn("px-2 py-1.5 text-sm font-semibold", className)} {...props} />
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@@ -0,0 +1,52 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className,
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className,
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className,
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@@ -0,0 +1,267 @@
"use client"
import { useState, useEffect } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Badge } from "./ui/badge"
import { Progress } from "./ui/progress"
import { Server, Play, Square, Monitor, Cpu, MemoryStick, AlertCircle } from "lucide-react"
interface VMData {
vmid: number
name: string
status: string
cpu: number
mem: number
maxmem: number
disk: number
maxdisk: number
uptime: number
}
const fetchVMData = async (): Promise<VMData[]> => {
try {
console.log("[v0] Fetching VM data from Flask server...")
const response = await fetch("/api/vms", {
method: "GET",
headers: {
"Content-Type": "application/json",
},
signal: AbortSignal.timeout(5000),
})
if (!response.ok) {
throw new Error(`Flask server responded with status: ${response.status}`)
}
const data = await response.json()
console.log("[v0] Successfully fetched VM data from Flask:", data)
return Array.isArray(data) ? data : []
} catch (error) {
console.error("[v0] Failed to fetch VM data from Flask server:", error)
throw error
}
}
export function VirtualMachines() {
const [vmData, setVmData] = useState<VMData[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true)
setError(null)
const result = await fetchVMData()
setVmData(result)
} catch (err) {
setError("Flask server not available. Please ensure the server is running.")
} finally {
setLoading(false)
}
}
fetchData()
const interval = setInterval(fetchData, 30000)
return () => clearInterval(interval)
}, [])
if (loading) {
return (
<div className="space-y-6">
<div className="text-center py-8">
<div className="text-lg font-medium text-foreground mb-2">Loading VM data...</div>
</div>
</div>
)
}
if (error) {
return (
<div className="space-y-6">
<Card className="bg-red-500/10 border-red-500/20">
<CardContent className="p-6">
<div className="flex items-center gap-3 text-red-600">
<AlertCircle className="h-6 w-6" />
<div>
<div className="font-semibold text-lg mb-1">Flask Server Not Available</div>
<div className="text-sm">
{error || "Unable to connect to the Flask server. Please ensure the server is running and try again."}
</div>
</div>
</div>
</CardContent>
</Card>
</div>
)
}
const runningVMs = vmData.filter((vm) => vm.status === "running").length
const stoppedVMs = vmData.filter((vm) => vm.status === "stopped").length
const totalCPU = vmData.reduce((sum, vm) => sum + (vm.cpu || 0), 0)
const totalMemory = vmData.reduce((sum, vm) => sum + (vm.maxmem || 0), 0)
const getStatusColor = (status: string) => {
switch (status) {
case "running":
return "bg-green-500/10 text-green-500 border-green-500/20"
case "stopped":
return "bg-red-500/10 text-red-500 border-red-500/20"
default:
return "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
}
}
const getStatusIcon = (status: string) => {
switch (status) {
case "running":
return <Play className="h-3 w-3 mr-1" />
case "stopped":
return <Square className="h-3 w-3 mr-1" />
default:
return null
}
}
const formatUptime = (seconds: number) => {
const days = Math.floor(seconds / 86400)
const hours = Math.floor((seconds % 86400) / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
return `${days}d ${hours}h ${minutes}m`
}
return (
<div className="space-y-6">
{/* VM Overview Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Total VMs</CardTitle>
<Server className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-foreground">{vmData.length}</div>
<div className="vm-badges mt-2">
<Badge variant="outline" className="vm-badge bg-green-500/10 text-green-500 border-green-500/20">
{runningVMs} Running
</Badge>
<Badge variant="outline" className="vm-badge bg-red-500/10 text-red-500 border-red-500/20">
{stoppedVMs} Stopped
</Badge>
</div>
<p className="text-xs text-muted-foreground mt-2">Virtual machines configured</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Total CPU</CardTitle>
<Cpu className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-foreground">{(totalCPU * 100).toFixed(0)}%</div>
<p className="text-xs text-muted-foreground mt-2">Allocated CPU usage</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Total Memory</CardTitle>
<MemoryStick className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-foreground">{(totalMemory / 1024 ** 3).toFixed(1)} GB</div>
<p className="text-xs text-muted-foreground mt-2">Allocated RAM</p>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Average Load</CardTitle>
<Monitor className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-foreground">
{runningVMs > 0 ? ((totalCPU / runningVMs) * 100).toFixed(0) : 0}%
</div>
<p className="text-xs text-muted-foreground mt-2">Average resource utilization</p>
</CardContent>
</Card>
</div>
{/* Virtual Machines List */}
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center">
<Server className="h-5 w-5 mr-2" />
Virtual Machines
</CardTitle>
</CardHeader>
<CardContent>
{vmData.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">No virtual machines found</div>
) : (
<div className="space-y-4">
{vmData.map((vm) => {
const cpuPercent = (vm.cpu * 100).toFixed(1)
const memPercent = vm.maxmem > 0 ? ((vm.mem / vm.maxmem) * 100).toFixed(1) : "0"
const memGB = (vm.mem / 1024 ** 3).toFixed(1)
const maxMemGB = (vm.maxmem / 1024 ** 3).toFixed(1)
return (
<div key={vm.vmid} className="p-6 rounded-lg border border-border bg-card/50">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-4">
<Server className="h-6 w-6 text-muted-foreground" />
<div>
<div className="font-semibold text-foreground text-lg flex items-center">
{vm.name}
<Badge
variant="outline"
className="ml-2 text-xs bg-purple-500/10 text-purple-500 border-purple-500/20"
>
VM
</Badge>
</div>
<div className="text-sm text-muted-foreground">ID: {vm.vmid}</div>
</div>
</div>
<div className="flex items-center space-x-3">
<Badge variant="outline" className={getStatusColor(vm.status)}>
{getStatusIcon(vm.status)}
{vm.status.toUpperCase()}
</Badge>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div>
<div className="text-sm text-muted-foreground mb-2">CPU Usage</div>
<div className="text-lg font-semibold text-foreground mb-1">{cpuPercent}%</div>
<Progress value={Number.parseFloat(cpuPercent)} className="h-2" />
</div>
<div>
<div className="text-sm text-muted-foreground mb-2">Memory Usage</div>
<div className="text-lg font-semibold text-foreground mb-1">
{memGB} GB / {maxMemGB} GB
</div>
<Progress value={Number.parseFloat(memPercent)} className="h-2" />
</div>
<div>
<div className="text-sm text-muted-foreground mb-2">Uptime</div>
<div className="text-lg font-semibold text-foreground">{formatUptime(vm.uptime)}</div>
</div>
</div>
</div>
)
})}
</div>
)}
</CardContent>
</Card>
</div>
)
}

6
AppImage/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

30
AppImage/next.config.mjs Normal file
View File

@@ -0,0 +1,30 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
trailingSlash: true,
eslint: {
ignoreDuringBuilds: true,
},
typescript: {
ignoreBuildErrors: true,
},
images: {
unoptimized: true,
},
experimental: {
esmExternals: 'loose',
},
webpack: (config, { isServer }) => {
if (!isServer) {
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
net: false,
tls: false,
};
}
return config;
},
};
export default nextConfig;

74
AppImage/package.json Normal file
View File

@@ -0,0 +1,74 @@
{
"name": "proxmenux-monitor",
"version": "1.0.0",
"description": "Proxmox System Monitoring Dashboard",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"export": "next build"
},
"dependencies": {
"@hookform/resolvers": "^3.10.0",
"@radix-ui/react-accordion": "1.2.2",
"@radix-ui/react-alert-dialog": "1.1.4",
"@radix-ui/react-aspect-ratio": "1.1.1",
"@radix-ui/react-avatar": "1.1.2",
"@radix-ui/react-checkbox": "1.1.3",
"@radix-ui/react-collapsible": "1.1.2",
"@radix-ui/react-context-menu": "2.2.4",
"@radix-ui/react-dialog": "1.1.4",
"@radix-ui/react-dropdown-menu": "2.1.4",
"@radix-ui/react-hover-card": "1.1.4",
"@radix-ui/react-label": "2.1.1",
"@radix-ui/react-menubar": "1.1.4",
"@radix-ui/react-navigation-menu": "1.2.3",
"@radix-ui/react-popover": "1.1.4",
"@radix-ui/react-progress": "1.1.1",
"@radix-ui/react-radio-group": "1.2.2",
"@radix-ui/react-scroll-area": "1.2.2",
"@radix-ui/react-select": "2.1.4",
"@radix-ui/react-separator": "1.1.1",
"@radix-ui/react-slider": "1.2.2",
"@radix-ui/react-slot": "1.1.1",
"@radix-ui/react-switch": "1.1.2",
"@radix-ui/react-tabs": "1.1.2",
"@radix-ui/react-toast": "1.2.4",
"@radix-ui/react-toggle": "1.1.1",
"@radix-ui/react-toggle-group": "1.1.1",
"@radix-ui/react-tooltip": "1.1.6",
"@vercel/analytics": "1.3.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "1.0.4",
"date-fns": "4.1.0",
"embla-carousel-react": "8.5.1",
"geist": "^1.3.1",
"input-otp": "1.4.1",
"lucide-react": "^0.454.0",
"next": "15.1.6",
"next-themes": "^0.4.6",
"react": "^19",
"react-day-picker": "9.8.0",
"react-dom": "^19",
"react-hook-form": "^7.60.0",
"react-resizable-panels": "^2.1.7",
"recharts": "2.15.4",
"sonner": "^1.7.4",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.9",
"zod": "3.25.67"
},
"devDependencies": {
"@types/node": "^22",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.4.20",
"postcss": "^8.5",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}

View File

@@ -0,0 +1,9 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
export default config;

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
AppImage/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,16 @@
{
"name": "ProxMenux Monitor",
"short_name": "ProxMenux",
"description": "Proxmox System Dashboard and Monitor",
"start_url": "/",
"display": "standalone",
"background_color": "#2b2f36",
"theme_color": "#2b2f36",
"icons": [
{
"src": "/images/proxmenux-logo.png",
"sizes": "256x256",
"type": "image/png"
}
]
}

45
AppImage/scripts/AppRun Normal file
View File

@@ -0,0 +1,45 @@
#!/bin/bash
# ProxMenux Monitor AppImage Entry Point
# This script is executed when the AppImage is run
# Get the directory where this AppImage is mounted
APPDIR="$(dirname "$(readlink -f "${0}")")"
export PATH="${APPDIR}/usr/bin:${PATH}"
export LD_LIBRARY_PATH="${APPDIR}/usr/lib:${LD_LIBRARY_PATH}"
export PYTHONPATH="${APPDIR}/usr/lib/python3/dist-packages:${APPDIR}/usr/lib/python3/site-packages:${PYTHONPATH}"
# Change to the AppImage directory
cd "${APPDIR}"
# Debug: Print directory structure for troubleshooting
echo "[v0] AppImage mounted at: ${APPDIR}"
echo "[v0] Contents of AppImage root:"
ls -la "${APPDIR}/" || echo "[v0] Cannot list AppImage root"
echo "[v0] Contents of web directory:"
ls -la "${APPDIR}/web/" || echo "[v0] Web directory not found"
echo "[v0] Looking for index.html:"
find "${APPDIR}" -name "index.html" -type f || echo "[v0] No index.html found"
echo "[v0] Python path: ${PYTHONPATH}"
echo "[v0] Checking Flask installation:"
python3 -c "import flask; print('Flask version:', flask.__version__)" 2>/dev/null || echo "[v0] Flask not found"
# Check for translation argument
if [[ "$1" == "--translate" ]]; then
echo "🌐 Starting ProxMenux Translation Service..."
exec python3 "${APPDIR}/usr/bin/translate_cli.py" "${@:2}"
else
echo "🚀 Starting ProxMenux Monitor Dashboard..."
echo "📊 Dashboard will be available at: http://localhost:8008"
echo "🔌 API endpoints at: http://localhost:8008/api/"
echo ""
echo "Press Ctrl+C to stop the server"
echo ""
# Start the Flask server
exec python3 "${APPDIR}/usr/bin/flask_server.py"
fi

View File

@@ -0,0 +1,338 @@
#!/bin/bash
# ProxMenux Monitor AppImage Builder
# This script creates a single AppImage with Flask server, Next.js dashboard, and translation support
set -e
WORK_DIR="/tmp/proxmenux_build"
APP_DIR="$WORK_DIR/ProxMenux.AppDir"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DIST_DIR="$SCRIPT_DIR/../dist"
APPIMAGE_ROOT="$SCRIPT_DIR/.."
VERSION=$(node -p "require('$APPIMAGE_ROOT/package.json').version")
APPIMAGE_NAME="ProxMenux-${VERSION}.AppImage"
echo "🚀 Building ProxMenux Monitor AppImage v${VERSION} with translation support..."
# Clean and create work directory
rm -rf "$WORK_DIR"
mkdir -p "$APP_DIR"
mkdir -p "$DIST_DIR"
# Download appimagetool if not exists
if [ ! -f "$WORK_DIR/appimagetool" ]; then
echo "📥 Downloading appimagetool..."
wget -q "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" -O "$WORK_DIR/appimagetool"
chmod +x "$WORK_DIR/appimagetool"
fi
# Create directory structure
mkdir -p "$APP_DIR/usr/bin"
mkdir -p "$APP_DIR/usr/lib/python3/dist-packages"
mkdir -p "$APP_DIR/usr/share/applications"
mkdir -p "$APP_DIR/usr/share/icons/hicolor/256x256/apps"
mkdir -p "$APP_DIR/web"
echo "🔨 Building Next.js application..."
cd "$APPIMAGE_ROOT"
if [ ! -f "package.json" ]; then
echo "❌ Error: package.json not found in AppImage directory"
exit 1
fi
# Install dependencies if node_modules doesn't exist
if [ ! -d "node_modules" ]; then
echo "📦 Installing dependencies..."
npm install
fi
echo "🏗️ Building Next.js static export..."
npm run export
echo "🔍 Checking export results..."
if [ -d "out" ]; then
echo "✅ Export directory found"
echo "📁 Contents of out directory:"
ls -la out/
if [ -f "out/index.html" ]; then
echo "✅ index.html found in out directory"
else
echo "❌ index.html NOT found in out directory"
echo "📁 Looking for HTML files:"
find out/ -name "*.html" -type f || echo "No HTML files found"
fi
else
echo "❌ Error: Next.js export failed - out directory not found"
echo "📁 Current directory contents:"
ls -la
echo "📁 Looking for any build outputs:"
find . -name "*.html" -type f 2>/dev/null || echo "No HTML files found anywhere"
exit 1
fi
# Return to script directory
cd "$SCRIPT_DIR"
# Copy Flask server
echo "📋 Copying Flask server..."
cp "$SCRIPT_DIR/flask_server.py" "$APP_DIR/usr/bin/"
echo "📋 Adding translation support..."
cat > "$APP_DIR/usr/bin/translate_cli.py" << 'PYEOF'
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ProxMenux translate CLI
stdin JSON -> {"text":"...", "dest_lang":"es", "context":"...", "cache_file":"/usr/local/share/proxmenux/cache.json"}
stdout JSON -> {"success":true,"text":"..."} or {"success":false,"error":"..."}
"""
import sys, json, re
from pathlib import Path
# Ensure embedded site-packages are discoverable
HERE = Path(__file__).resolve().parents[2] # .../AppDir
DIST = HERE / "usr" / "lib" / "python3" / "dist-packages"
SITE = HERE / "usr" / "lib" / "python3" / "site-packages"
for p in (str(DIST), str(SITE)):
if p not in sys.path:
sys.path.insert(0, p)
# Python 3.13 compat: inline 'cgi' shim
try:
import cgi
except Exception:
import types, html
def _parse_header(value: str):
value = str(value or "")
parts = [p.strip() for p in value.split(";")]
if not parts:
return "", {}
key = parts[0].lower()
params = {}
for item in parts[1:]:
if not item:
continue
if "=" in item:
k, v = item.split("=", 1)
k = k.strip().lower()
v = v.strip().strip('"').strip("'")
params[k] = v
else:
params[item.strip().lower()] = ""
return key, params
cgi = types.SimpleNamespace(parse_header=_parse_header, escape=html.escape)
try:
from googletrans import Translator
except Exception as e:
print(json.dumps({"success": False, "error": f"ImportError: {e}"}))
sys.exit(0)
def load_json_stdin():
try:
return json.load(sys.stdin)
except Exception as e:
print(json.dumps({"success": False, "error": f"Invalid JSON input: {e}"}))
sys.exit(0)
def ensure_cache(path: Path):
try:
path.parent.mkdir(parents=True, exist_ok=True)
if not path.exists():
path.write_text("{}", encoding="utf-8")
json.loads(path.read_text(encoding="utf-8") or "{}")
except Exception:
path.write_text("{}", encoding="utf-8")
def read_cache(path: Path):
try:
return json.loads(path.read_text(encoding="utf-8") or "{}")
except Exception:
return {}
def write_cache(path: Path, cache: dict):
tmp = path.with_suffix(".tmp")
tmp.write_text(json.dumps(cache, ensure_ascii=False), encoding="utf-8")
tmp.replace(path)
def clean_translated(s: str) -> str:
s = re.sub(r'^.*?(Translate:|Traducir:|Traduire:|Übersetzen:|Tradurre:|Traduzir:|翻译:|翻訳:)', '', s, flags=re.IGNORECASE | re.DOTALL).strip()
s = re.sub(r'^.*?(Context:|Contexto:|Contexte:|Kontext:|Contesto:|上下文:|コンテキスト:).*?:', '', s, flags=re.IGNORECASE | re.DOTALL).strip()
return s.strip()
def main():
req = load_json_stdin()
text = req.get("text", "")
dest = req.get("dest_lang", "en") or "en"
context = req.get("context", "")
cache_file = Path(req.get("cache_file", "")) if req.get("cache_file") else None
if dest == "en":
print(json.dumps({"success": True, "text": text}))
return
cache = {}
if cache_file:
ensure_cache(cache_file)
cache = read_cache(cache_file)
if text in cache and (dest in cache[text] or "notranslate" in cache[text]):
found = cache[text].get(dest) or cache[text].get("notranslate")
print(json.dumps({"success": True, "text": found}))
return
try:
full = (context + " " + text).strip() if context else text
tr = Translator()
result = tr.translate(full, dest=dest).text
result = clean_translated(result)
if cache_file:
cache.setdefault(text, {})
cache[text][dest] = result
write_cache(cache_file, cache)
print(json.dumps({"success": True, "text": result}))
except Exception as e:
print(json.dumps({"success": False, "error": str(e)}))
if __name__ == "__main__":
main()
PYEOF
chmod +x "$APP_DIR/usr/bin/translate_cli.py"
# Copy Next.js build
echo "📋 Copying web dashboard..."
if [ -d "$APPIMAGE_ROOT/out" ]; then
mkdir -p "$APP_DIR/web"
echo "📁 Copying from $APPIMAGE_ROOT/out to $APP_DIR/web"
cp -r "$APPIMAGE_ROOT/out"/* "$APP_DIR/web/"
if [ -f "$APP_DIR/web/index.html" ]; then
echo "✅ index.html copied successfully to $APP_DIR/web/"
else
echo "❌ index.html NOT found after copying"
echo "📁 Contents of $APP_DIR/web:"
ls -la "$APP_DIR/web/" || echo "Directory is empty or doesn't exist"
fi
if [ -d "$APPIMAGE_ROOT/public" ]; then
cp -r "$APPIMAGE_ROOT/public"/* "$APP_DIR/web/" 2>/dev/null || true
fi
cp "$APPIMAGE_ROOT/package.json" "$APP_DIR/web/"
echo "✅ Next.js static export copied successfully"
else
echo "❌ Error: Next.js export not found even after building"
exit 1
fi
# Copy AppRun script
echo "📋 Copying AppRun script..."
if [ -f "$SCRIPT_DIR/AppRun" ]; then
cp "$SCRIPT_DIR/AppRun" "$APP_DIR/AppRun"
chmod +x "$APP_DIR/AppRun"
echo "✅ AppRun script copied successfully"
else
echo "❌ Error: AppRun script not found at $SCRIPT_DIR/AppRun"
exit 1
fi
# Create desktop file
cat > "$APP_DIR/proxmenux-monitor.desktop" << EOF
[Desktop Entry]
Type=Application
Name=ProxMenux Monitor
Comment=Proxmox System Monitoring Dashboard with Translation Support
Exec=AppRun
Icon=proxmenux-monitor
Categories=System;Monitor;
Terminal=false
StartupNotify=true
EOF
# Copy desktop file to applications directory
cp "$APP_DIR/proxmenux-monitor.desktop" "$APP_DIR/usr/share/applications/"
# Download and set icon
echo "🎨 Setting up icon..."
if [ -f "$APPIMAGE_ROOT/public/images/proxmenux-logo.png" ]; then
cp "$APPIMAGE_ROOT/public/images/proxmenux-logo.png" "$APP_DIR/proxmenux-monitor.png"
else
wget -q "https://raw.githubusercontent.com/MacRimi/ProxMenux/main/images/logo.png" -O "$APP_DIR/proxmenux-monitor.png" || {
echo "⚠️ Could not download logo, creating placeholder..."
convert -size 256x256 xc:blue -fill white -gravity center -pointsize 24 -annotate +0+0 "PM" "$APP_DIR/proxmenux-monitor.png" 2>/dev/null || {
echo "⚠️ ImageMagick not available, skipping icon creation"
}
}
fi
if [ -f "$APP_DIR/proxmenux-monitor.png" ]; then
cp "$APP_DIR/proxmenux-monitor.png" "$APP_DIR/usr/share/icons/hicolor/256x256/apps/"
fi
echo "📦 Installing Python dependencies..."
pip3 install --target "$APP_DIR/usr/lib/python3/dist-packages" \
flask \
flask-cors \
psutil \
requests \
googletrans==4.0.0-rc1 \
httpx==0.13.3 \
httpcore==0.9.1 \
beautifulsoup4
cat > "$APP_DIR/usr/lib/python3/dist-packages/cgi.py" << 'PYEOF'
from typing import Tuple, Dict
try:
from html import escape as _html_escape
except Exception:
def _html_escape(s, quote=True): return s
__all__ = ["parse_header", "escape"]
def escape(s, quote=True):
return _html_escape(s, quote=quote)
def parse_header(value: str) -> Tuple[str, Dict[str, str]]:
if not isinstance(value, str):
value = str(value or "")
parts = [p.strip() for p in value.split(";")]
if not parts:
return "", {}
key = parts[0].lower()
params: Dict[str, str] = {}
for item in parts[1:]:
if not item:
continue
if "=" in item:
k, v = item.split("=", 1)
k = k.strip().lower()
v = v.strip().strip('"').strip("'")
params[k] = v
else:
params[item.strip().lower()] = ""
return key, params
PYEOF
# Build AppImage
echo "🔨 Building unified AppImage v${VERSION}..."
cd "$WORK_DIR"
export NO_CLEANUP=1
export APPIMAGE_EXTRACT_AND_RUN=1
ARCH=x86_64 ./appimagetool --no-appstream --verbose "$APP_DIR" "$APPIMAGE_NAME"
# Move to dist directory
mv "$APPIMAGE_NAME" "$DIST_DIR/"
echo "✅ Unified AppImage created: $DIST_DIR/$APPIMAGE_NAME"
echo ""
echo "📋 Usage:"
echo " Dashboard: ./$APPIMAGE_NAME"
echo " Translation: ./$APPIMAGE_NAME --translate"
echo ""
echo "🚀 Installation:"
echo " sudo cp $DIST_DIR/$APPIMAGE_NAME /usr/local/bin/proxmenux-monitor"
echo " sudo chmod +x /usr/local/bin/proxmenux-monitor"

View File

@@ -0,0 +1,582 @@
#!/usr/bin/env python3
"""
ProxMenux Flask Server
Provides REST API endpoints for Proxmox monitoring data
Runs on port 8008 and serves system metrics, storage info, network stats, etc.
Also serves the Next.js dashboard as static files
"""
from flask import Flask, jsonify, request, send_from_directory, send_file
from flask_cors import CORS
import psutil
import subprocess
import json
import os
import time
import socket
from datetime import datetime, timedelta
app = Flask(__name__)
CORS(app) # Enable CORS for Next.js frontend
@app.route('/')
def serve_dashboard():
"""Serve the main dashboard page from Next.js build"""
try:
# Detectar si estamos ejecutándose desde AppImage
appimage_root = os.environ.get('APPDIR')
if not appimage_root:
# Fallback: intentar detectar desde la ubicación del script
base_dir = os.path.dirname(os.path.abspath(__file__))
appimage_root = os.path.dirname(base_dir) # Subir un nivel desde usr/bin/
index_paths = [
os.path.join(appimage_root, 'web', 'index.html'), # Ruta principal para AppImage
os.path.join(appimage_root, 'usr', 'web', 'index.html'), # Fallback con usr/
os.path.join(appimage_root, 'web', 'out', 'index.html'), # Fallback si está en subcarpeta
os.path.join(appimage_root, 'usr', 'web', 'out', 'index.html'), # Fallback con usr/out/
]
print(f"[v0] Flask server looking for index.html in:")
for path in index_paths:
abs_path = os.path.abspath(path)
exists = os.path.exists(abs_path)
print(f"[v0] {abs_path} - {'EXISTS' if exists else 'NOT FOUND'}")
if exists:
print(f"[v0] Found index.html, serving from: {abs_path}")
return send_file(abs_path)
# If no Next.js build found, return error message with actual paths checked
actual_paths = [os.path.abspath(path) for path in index_paths]
return f'''
<!DOCTYPE html>
<html>
<head><title>ProxMenux Monitor - Build Error</title></head>
<body style="font-family: Arial; padding: 2rem; background: #0a0a0a; color: #fff;">
<h1>🚨 ProxMenux Monitor - Build Error</h1>
<p>Next.js application not found. The AppImage may not have been built correctly.</p>
<p>Expected paths checked:</p>
<ul>{''.join([f'<li>{path}</li>' for path in actual_paths])}</ul>
<p>API endpoints are still available:</p>
<ul>
<li><a href="/api/system" style="color: #4f46e5;">/api/system</a></li>
<li><a href="/api/system-info" style="color: #4f46e5;">/api/system-info</a></li>
<li><a href="/api/storage" style="color: #4f46e5;">/api/storage</a></li>
<li><a href="/api/network" style="color: #4f46e5;">/api/network</a></li>
<li><a href="/api/vms" style="color: #4f46e5;">/api/vms</a></li>
<li><a href="/api/health" style="color: #4f46e5;">/api/health</a></li>
</ul>
</body>
</html>
''', 500
except Exception as e:
print(f"Error serving dashboard: {e}")
return jsonify({'error': f'Dashboard not available: {str(e)}'}), 500
@app.route('/manifest.json')
def serve_manifest():
"""Serve PWA manifest"""
try:
manifest_paths = [
os.path.join(os.path.dirname(__file__), '..', 'web', 'public', 'manifest.json'),
os.path.join(os.path.dirname(__file__), '..', 'public', 'manifest.json')
]
for manifest_path in manifest_paths:
if os.path.exists(manifest_path):
return send_file(manifest_path)
# Return default manifest if not found
return jsonify({
"name": "ProxMenux Monitor",
"short_name": "ProxMenux",
"description": "Proxmox System Monitoring Dashboard",
"start_url": "/",
"display": "standalone",
"background_color": "#0a0a0a",
"theme_color": "#4f46e5",
"icons": [
{
"src": "/images/proxmenux-logo.png",
"sizes": "256x256",
"type": "image/png"
}
]
})
except Exception as e:
print(f"Error serving manifest: {e}")
return jsonify({}), 404
@app.route('/sw.js')
def serve_sw():
"""Serve service worker"""
return '''
const CACHE_NAME = 'proxmenux-v1';
const urlsToCache = [
'/',
'/api/system',
'/api/storage',
'/api/network',
'/api/health'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
});
''', 200, {'Content-Type': 'application/javascript'}
@app.route('/_next/<path:filename>')
def serve_next_static(filename):
"""Serve Next.js static files"""
try:
appimage_root = os.environ.get('APPDIR')
if not appimage_root:
base_dir = os.path.dirname(os.path.abspath(__file__))
appimage_root = os.path.dirname(base_dir)
static_paths = [
os.path.join(appimage_root, 'web', '_next'), # Ruta principal
os.path.join(appimage_root, 'usr', 'web', '_next'), # Fallback con usr/
os.path.join(appimage_root, 'web', 'out', '_next'), # Fallback con out/
os.path.join(appimage_root, 'usr', 'web', 'out', '_next'), # Fallback con usr/out/
]
for static_dir in static_paths:
file_path = os.path.join(static_dir, filename)
if os.path.exists(file_path):
return send_file(file_path)
return '', 404
except Exception as e:
print(f"Error serving Next.js static file {filename}: {e}")
return '', 404
@app.route('/<path:filename>')
def serve_static_files(filename):
"""Serve static files (icons, etc.)"""
try:
appimage_root = os.environ.get('APPDIR')
if not appimage_root:
base_dir = os.path.dirname(os.path.abspath(__file__))
appimage_root = os.path.dirname(base_dir)
public_paths = [
os.path.join(appimage_root, 'web'), # Raíz web para exportación estática
os.path.join(appimage_root, 'usr', 'web'), # Fallback con usr/
os.path.join(appimage_root, 'web', 'out'), # Fallback con out/
os.path.join(appimage_root, 'usr', 'web', 'out'), # Fallback con usr/out/
]
for public_dir in public_paths:
file_path = os.path.join(public_dir, filename)
if os.path.exists(file_path):
return send_from_directory(public_dir, filename)
return '', 404
except Exception as e:
print(f"Error serving static file {filename}: {e}")
return '', 404
@app.route('/images/<path:filename>')
def serve_images(filename):
"""Serve image files"""
try:
appimage_root = os.environ.get('APPDIR')
if not appimage_root:
base_dir = os.path.dirname(os.path.abspath(__file__))
appimage_root = os.path.dirname(base_dir)
image_paths = [
os.path.join(appimage_root, 'web', 'images'), # Ruta principal para exportación estática
os.path.join(appimage_root, 'usr', 'web', 'images'), # Fallback con usr/
os.path.join(appimage_root, 'web', 'public', 'images'), # Ruta con public/
os.path.join(appimage_root, 'usr', 'web', 'public', 'images'), # Fallback usr/public/
os.path.join(appimage_root, 'public', 'images'), # Ruta directa a public
os.path.join(appimage_root, 'usr', 'public', 'images'), # Fallback usr/public
]
print(f"[v0] Looking for image: {filename}")
for image_dir in image_paths:
file_path = os.path.join(image_dir, filename)
abs_path = os.path.abspath(file_path)
exists = os.path.exists(abs_path)
print(f"[v0] Checking: {abs_path} - {'FOUND' if exists else 'NOT FOUND'}")
if exists:
print(f"[v0] Serving image from: {abs_path}")
return send_from_directory(image_dir, filename)
print(f"[v0] Image not found: {filename}")
return '', 404
except Exception as e:
print(f"Error serving image {filename}: {e}")
return '', 404
def get_system_info():
"""Get basic system information"""
try:
# CPU usage
cpu_percent = psutil.cpu_percent(interval=1)
# Memory usage
memory = psutil.virtual_memory()
temp = 0
try:
if hasattr(psutil, "sensors_temperatures"):
temps = psutil.sensors_temperatures()
if temps:
# Priority order for temperature sensors
sensor_priority = ['coretemp', 'cpu_thermal', 'acpi', 'thermal_zone']
for sensor_name in sensor_priority:
if sensor_name in temps and temps[sensor_name]:
temp = temps[sensor_name][0].current
break
# If no priority sensor found, use first available
if temp == 0:
for name, entries in temps.items():
if entries:
temp = entries[0].current
break
except Exception as e:
print(f"Error reading temperature sensors: {e}")
temp = 0 # Use 0 to indicate no temperature available
# Uptime
boot_time = psutil.boot_time()
uptime_seconds = time.time() - boot_time
uptime_str = str(timedelta(seconds=int(uptime_seconds)))
# Load average
load_avg = os.getloadavg() if hasattr(os, 'getloadavg') else [0, 0, 0]
hostname = socket.gethostname()
node_id = f"pve-{hostname}"
# Try to get Proxmox node info if available
try:
result = subprocess.run(['pvesh', 'get', '/nodes', '--output-format', 'json'],
capture_output=True, text=True, timeout=5)
if result.returncode == 0:
nodes = json.loads(result.stdout)
if nodes and len(nodes) > 0:
node_id = nodes[0].get('node', node_id)
except Exception as e:
print(f"Note: pvesh not available or failed: {e}")
pass # Use default if pvesh not available
return {
'cpu_usage': round(cpu_percent, 1),
'memory_usage': round(memory.percent, 1),
'memory_total': round(memory.total / (1024**3), 1), # GB
'memory_used': round(memory.used / (1024**3), 1), # GB
'temperature': temp,
'uptime': uptime_str,
'load_average': list(load_avg),
'hostname': hostname,
'node_id': node_id,
'timestamp': datetime.now().isoformat()
}
except Exception as e:
print(f"Critical error getting system info: {e}")
return {
'error': f'Unable to access system information: {str(e)}',
'timestamp': datetime.now().isoformat()
}
def get_storage_info():
"""Get storage and disk information"""
try:
storage_data = {
'total': 0,
'used': 0,
'available': 0,
'disks': []
}
# Get disk usage for root partition
disk_usage = psutil.disk_usage('/')
storage_data['total'] = round(disk_usage.total / (1024**3), 1) # GB
storage_data['used'] = round(disk_usage.used / (1024**3), 1) # GB
storage_data['available'] = round(disk_usage.free / (1024**3), 1) # GB
# Get individual disk information
disk_partitions = psutil.disk_partitions()
for partition in disk_partitions:
try:
partition_usage = psutil.disk_usage(partition.mountpoint)
disk_temp = 0
try:
# Try to get disk temperature from sensors
if hasattr(psutil, "sensors_temperatures"):
temps = psutil.sensors_temperatures()
if temps:
for name, entries in temps.items():
if 'disk' in name.lower() or 'hdd' in name.lower() or 'sda' in name.lower():
if entries:
disk_temp = entries[0].current
break
except:
pass
disk_info = {
'name': partition.device,
'mountpoint': partition.mountpoint,
'fstype': partition.fstype,
'total': round(partition_usage.total / (1024**3), 1),
'used': round(partition_usage.used / (1024**3), 1),
'available': round(partition_usage.free / (1024**3), 1),
'usage_percent': round((partition_usage.used / partition_usage.total) * 100, 1),
'health': 'unknown', # Would need SMART data for real health
'temperature': disk_temp
}
storage_data['disks'].append(disk_info)
except PermissionError:
print(f"Permission denied accessing {partition.mountpoint}")
continue
except Exception as e:
print(f"Error accessing partition {partition.device}: {e}")
continue
if not storage_data['disks'] and storage_data['total'] == 0:
return {
'error': 'No storage data available - unable to access disk information',
'total': 0,
'used': 0,
'available': 0,
'disks': []
}
return storage_data
except Exception as e:
print(f"Error getting storage info: {e}")
return {
'error': f'Unable to access storage information: {str(e)}',
'total': 0,
'used': 0,
'available': 0,
'disks': []
}
def get_network_info():
"""Get network interface information"""
try:
network_data = {
'interfaces': [],
'traffic': {'incoming': 0, 'outgoing': 0}
}
# Get network interfaces
net_if_addrs = psutil.net_if_addrs()
net_if_stats = psutil.net_if_stats()
for interface_name, interface_addresses in net_if_addrs.items():
if interface_name == 'lo': # Skip loopback
continue
interface_info = {
'name': interface_name,
'status': 'up' if net_if_stats[interface_name].isup else 'down',
'addresses': []
}
for address in interface_addresses:
if address.family == 2: # IPv4
interface_info['addresses'].append({
'ip': address.address,
'netmask': address.netmask
})
network_data['interfaces'].append(interface_info)
# Get network I/O statistics
net_io = psutil.net_io_counters()
network_data['traffic'] = {
'bytes_sent': net_io.bytes_sent,
'bytes_recv': net_io.bytes_recv,
'packets_sent': net_io.packets_sent,
'packets_recv': net_io.packets_recv
}
return network_data
except Exception as e:
print(f"Error getting network info: {e}")
return {
'error': f'Unable to access network information: {str(e)}',
'interfaces': [],
'traffic': {'bytes_sent': 0, 'bytes_recv': 0, 'packets_sent': 0, 'packets_recv': 0}
}
def get_proxmox_vms():
"""Get Proxmox VM information (requires pvesh command)"""
try:
# Try to get VM list using pvesh command
result = subprocess.run(['pvesh', 'get', '/nodes/localhost/qemu', '--output-format', 'json'],
capture_output=True, text=True, timeout=10)
if result.returncode == 0:
vms = json.loads(result.stdout)
return vms
else:
return {
'error': 'pvesh command not available or failed - Proxmox API not accessible',
'vms': []
}
except Exception as e:
print(f"Error getting VM info: {e}")
return {
'error': f'Unable to access VM information: {str(e)}',
'vms': []
}
@app.route('/api/system', methods=['GET'])
def api_system():
"""Get system information"""
return jsonify(get_system_info())
@app.route('/api/storage', methods=['GET'])
def api_storage():
"""Get storage information"""
return jsonify(get_storage_info())
@app.route('/api/network', methods=['GET'])
def api_network():
"""Get network information"""
return jsonify(get_network_info())
@app.route('/api/vms', methods=['GET'])
def api_vms():
"""Get virtual machine information"""
return jsonify(get_proxmox_vms())
@app.route('/api/logs', methods=['GET'])
def api_logs():
"""Get system logs"""
try:
# Get recent system logs
result = subprocess.run(['journalctl', '-n', '100', '--output', 'json'],
capture_output=True, text=True, timeout=10)
if result.returncode == 0:
logs = []
for line in result.stdout.strip().split('\n'):
if line:
try:
log_entry = json.loads(line)
logs.append({
'timestamp': log_entry.get('__REALTIME_TIMESTAMP', ''),
'level': log_entry.get('PRIORITY', '6'),
'service': log_entry.get('_SYSTEMD_UNIT', 'system'),
'message': log_entry.get('MESSAGE', ''),
'source': 'journalctl'
})
except json.JSONDecodeError:
continue
return jsonify(logs)
else:
return jsonify({
'error': 'journalctl not available or failed',
'logs': []
})
except Exception as e:
print(f"Error getting logs: {e}")
return jsonify({
'error': f'Unable to access system logs: {str(e)}',
'logs': []
})
@app.route('/api/health', methods=['GET'])
def api_health():
"""Health check endpoint"""
return jsonify({
'status': 'healthy',
'timestamp': datetime.now().isoformat(),
'version': '1.0.0'
})
@app.route('/api/system-info', methods=['GET'])
def api_system_info():
"""Get system and node information for dashboard header"""
try:
hostname = socket.gethostname()
node_id = f"pve-{hostname}"
pve_version = None
# Try to get Proxmox version
try:
result = subprocess.run(['pveversion'], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
pve_version = result.stdout.strip().split('\n')[0]
except:
pass
# Try to get node info from Proxmox API
try:
result = subprocess.run(['pvesh', 'get', '/nodes', '--output-format', 'json'],
capture_output=True, text=True, timeout=5)
if result.returncode == 0:
nodes = json.loads(result.stdout)
if nodes and len(nodes) > 0:
node_info = nodes[0]
node_id = node_info.get('node', node_id)
hostname = node_info.get('node', hostname)
except:
pass
response = {
'hostname': hostname,
'node_id': node_id,
'status': 'online',
'timestamp': datetime.now().isoformat()
}
if pve_version:
response['pve_version'] = pve_version
else:
response['error'] = 'Proxmox version not available - pveversion command not found'
return jsonify(response)
except Exception as e:
print(f"Error getting system info: {e}")
return jsonify({
'error': f'Unable to access system information: {str(e)}',
'hostname': socket.gethostname(),
'status': 'error',
'timestamp': datetime.now().isoformat()
})
@app.route('/api/info', methods=['GET'])
def api_info():
"""Root endpoint with API information"""
return jsonify({
'name': 'ProxMenux Monitor API',
'version': '1.0.0',
'endpoints': [
'/api/system',
'/api/system-info',
'/api/storage',
'/api/network',
'/api/vms',
'/api/logs',
'/api/health'
]
})
if __name__ == '__main__':
print("Starting ProxMenux Flask Server on port 8008...")
print("Server will be accessible on all network interfaces (0.0.0.0:8008)")
print("API endpoints available at: /api/system, /api/storage, /api/network, /api/vms, /api/logs, /api/health")
app.run(host='0.0.0.0', port=8008, debug=False)

View File

@@ -0,0 +1,89 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: ["./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}"],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
chart: {
1: "hsl(var(--chart-1))",
2: "hsl(var(--chart-2))",
3: "hsl(var(--chart-3))",
4: "hsl(var(--chart-4))",
5: "hsl(var(--chart-5))",
},
sidebar: {
DEFAULT: "hsl(var(--sidebar-background))",
foreground: "hsl(var(--sidebar-foreground))",
primary: "hsl(var(--sidebar-primary))",
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
accent: "hsl(var(--sidebar-accent))",
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
border: "hsl(var(--sidebar-border))",
ring: "hsl(var(--sidebar-ring))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}

27
AppImage/tsconfig.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

View File

@@ -1,3 +1,57 @@
## 2025-01-10
### New version v1.1.6
![Shared Resources Menu](https://macrimi.github.io/ProxMenux/share/main-menu.png)
### Added
- **New Menu: Mount and Share Manager**
Introduced a comprehensive new menu for managing shared resources between Proxmox host and LXC containers:
**Host Configuration Options:**
- **Configure NFS Shared on Host** - Add, view, and remove NFS shared resources on the Proxmox server with automatic export management
- **Configure Samba Shared on Host** - Add, view, and remove Samba/CIFS shared resources on the Proxmox server with share configuration
- **Configure Local Shared on Host** - Create and manage local shared directories with proper permissions on the Proxmox host
**LXC Integration Options:**
- **Configure LXC Mount Points (Host ↔ Container)** - **Core feature** that enables mounting host directories into LXC containers with automatic permission handling. Includes the ability to **view existing mount points** for each container in a clear, organized way and **remove mount points** with proper verification that the process completed successfully. Especially optimized for **unprivileged containers** where UID/GID mapping is critical.
- **Configure NFS Client in LXC** - Set up NFS client inside privileged containers
- **Configure Samba Client in LXC** - Set up Samba client inside privileged containers
- **Configure NFS Server in LXC** - Install NFS server inside privileged containers
- **Configure Samba Server in LXC** - Install Samba server inside privileged containers
**Documentation & Support:**
- **Help & Info (commands)** - Comprehensive guides with step-by-step manual instructions for all sharing scenarios
The entire system is built around the **LXC Mount Points** functionality, which automatically detects filesystem types, handles permission mapping between host and container users, and provides seamless integration for both privileged and unprivileged containers.
---
### Improved
- **Log2RAM Auto-Detection Enhancement**
In the automatic post-install script, the Log2RAM installation function now prompts the user when automatic disk ssd/m2 detection fails.
This ensures Log2RAM can still be installed on systems where automatic disk detection doesn't work properly.
---
### Fixed
- **Proxmox Update Repository Verification**
Fixed an issue in the Proxmox update function where empty repository source files would cause errors during conflict verification. The function now properly handles empty `/etc/apt/sources.list.d/` files without throwing false warnings.
Thanks to **@JF_Car** for reporting this issue.
---
### Acknowledgments
Special thanks to **@JF_Car**, **@ghosthvj**, and **@jonatanc** for their testing, valuable feedback, and suggestions that helped refine the shared resources functionality and improve the overall user experience.
## 2025-08-20 ## 2025-08-20
### New version v1.1.5 ### New version v1.1.5

View File

@@ -1,6 +1,6 @@
<div align="center"> <div align="center">
<img src="https://github.com/MacRimi/ProxMenux/blob/main/images/main.png" <img src="https://github.com/MacRimi/ProxMenux/blob/main/images/main.png"
alt="ProxMenu Logo" alt="ProxMenux Logo"
style="max-width: 100%; height: auto;" > style="max-width: 100%; height: auto;" >
</div> </div>
@@ -70,6 +70,12 @@ Then, follow the on-screen options to manage your Proxmox server efficiently.
## ⭐ Support the Project! ## ⭐ Support the Project!
If you find **ProxMenux** useful, consider giving it a ⭐ on GitHub to help others discover it! If you find **ProxMenux** useful, consider giving it a ⭐ on GitHub to help others discover it!
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=MacRimi/ProxMenux&type=Date)](https://www.star-history.com/#MacRimi/ProxMenux&Date)
<div style="display: flex; justify-content: center; align-items: center;"> <div style="display: flex; justify-content: center; align-items: center;">
<a href="https://ko-fi.com/G2G313ECAN" target="_blank" style="display: flex; align-items: center; text-decoration: none;"> <a href="https://ko-fi.com/G2G313ECAN" target="_blank" style="display: flex; align-items: center; text-decoration: none;">
<img src="https://raw.githubusercontent.com/MacRimi/HWEncoderX/main/images/kofi.png" alt="Support me on Ko-fi" style="width:140px; margin-right:40px;"/> <img src="https://raw.githubusercontent.com/MacRimi/HWEncoderX/main/images/kofi.png" alt="Support me on Ko-fi" style="width:140px; margin-right:40px;"/>
@@ -77,5 +83,3 @@ If you find **ProxMenux** useful, consider giving it a ⭐ on GitHub to help oth
</div> </div>
Support the project on Ko-fi! Support the project on Ko-fi!

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management # ProxMenux - A menu-driven script for Proxmox VE management
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi
@@ -101,17 +101,17 @@ check_existing_installation() {
fi fi
} }
uninstall_proxmenu() { uninstall_proxmenux() {
local install_type="$1" local install_type="$1"
local force_clean="$2" local force_clean="$2"
if [ "$force_clean" != "force" ]; then if [ "$force_clean" != "force" ]; then
if ! whiptail --title "Uninstall ProxMenu" --yesno "Are you sure you want to uninstall ProxMenu?" 10 60; then if ! whiptail --title "Uninstall ProxMenux" --yesno "Are you sure you want to uninstall ProxMenux?" 10 60; then
return 1 return 1
fi fi
fi fi
echo "Uninstalling ProxMenu..." echo "Uninstalling ProxMenux..."
if [ -f "$VENV_PATH/bin/activate" ]; then if [ -f "$VENV_PATH/bin/activate" ]; then
echo "Removing googletrans and virtual environment..." echo "Removing googletrans and virtual environment..."
@@ -151,7 +151,7 @@ uninstall_proxmenu() {
sed -i '/This system is optimised by: ProxMenux/d' /etc/motd sed -i '/This system is optimised by: ProxMenux/d' /etc/motd
fi fi
echo "ProxMenu has been uninstalled." echo "ProxMenux has been uninstalled."
return 0 return 0
} }
@@ -168,7 +168,7 @@ handle_installation_change() {
if whiptail --title "Installation Type Change" \ if whiptail --title "Installation Type Change" \
--yesno "Switch from Translation to Normal Version?\n\nThis will remove translation components." 10 60; then --yesno "Switch from Translation to Normal Version?\n\nThis will remove translation components." 10 60; then
echo "Preparing for installation type change..." echo "Preparing for installation type change..."
uninstall_proxmenu "translation" "force" >/dev/null 2>&1 uninstall_proxmenux "translation" "force" >/dev/null 2>&1
return 0 return 0
else else
return 1 return 1
@@ -221,7 +221,7 @@ show_progress() {
local total="$2" local total="$2"
local message="$3" local message="$3"
echo -e "\n${BOLD}${BL}${TAB}Installing ProxMenu: Step $step of $total${CL}" echo -e "\n${BOLD}${BL}${TAB}Installing ProxMenux: Step $step of $total${CL}"
echo echo
msg_info2 "$message" msg_info2 "$message"
} }

View File

@@ -202,7 +202,7 @@
"it": "Giapponese", "it": "Giapponese",
"pt": "Japonês" "pt": "Japonês"
}, },
"Thank you for using ProxMenu. Goodbye!": { "Thank you for using ProxMenux. Goodbye!": {
"es": "Gracias por usar ProxMenu. ¡Adiós!", "es": "Gracias por usar ProxMenu. ¡Adiós!",
"fr": "Merci d'avoir utilisé ProxMenu. Au revoir!", "fr": "Merci d'avoir utilisé ProxMenu. Au revoir!",
"de": "Danke für die Nutzung von ProxMenu. Auf Wiedersehen!", "de": "Danke für die Nutzung von ProxMenu. Auf Wiedersehen!",
@@ -900,7 +900,11 @@
"it": "Trova il tuo dispositivo usando https://finds.synology.com" "it": "Trova il tuo dispositivo usando https://finds.synology.com"
}, },
"Help and Info Commands": { "Help and Info Commands": {
"es": "Comandos de ayuda e información" "es": "Comandos de ayuda e información",
"fr": "Aide et Informations (commandes)",
"de": "Hilfe & Informationen (Befehle)",
"it": "Aiuto e Informazioni (comandi)",
"pt": "Ajuda e Informações (comandos)"
}, },
"Create VM from template or script": { "Create VM from template or script": {
"es": "Crear VM a partir de plantilla o script", "es": "Crear VM a partir de plantilla o script",
@@ -2566,5 +2570,103 @@
"de": "Notfallwiederherstellung:", "de": "Notfallwiederherstellung:",
"it": "Ripristino di emergenza:", "it": "Ripristino di emergenza:",
"pt": "Recuperação de emergência:" "pt": "Recuperação de emergência:"
},
"Mount and Share Manager": {
"es": "Montajes y Recursos Compartidos",
"fr": "Gestionnaire de Montage et Partage",
"de": "Mount- und Share-Manager",
"it": "Gestore di Mount e Condivisioni",
"pt": "Gerenciador de Montagem e Compartilhamento"
},
"HOST": {
"es": "HOST",
"fr": "HÔTE",
"de": "HOST",
"it": "HOST",
"pt": "HOST"
},
"Configure NFS shared on Host": {
"es": "Configurar recursos NFS compartidos en el Host",
"fr": "Configurer les partages NFS sur l'hôte",
"de": "NFS-Freigaben auf Host konfigurieren",
"it": "Configurare condivisioni NFS su Host",
"pt": "Configurar compartilhamentos NFS no Host"
},
"Configure Samba shared on Host": {
"es": "Configurar recursos Samba compartidos en el Host",
"fr": "Configurer les partages Samba sur l'hôte",
"de": "Samba-Freigaben auf Host konfigurieren",
"it": "Configurare condivisioni Samba su Host",
"pt": "Configurar compartilhamentos Samba no Host"
},
"Configure Local Shared on Host": {
"es": "Configurar directorios locales compartidos en el Host",
"fr": "Configurer les répertoires locaux partagés sur l'hôte",
"de": "Lokale geteilte Verzeichnisse auf Host konfigurieren",
"it": "Configurare directory locali condivise su Host",
"pt": "Configurar diretórios locais compartilhados no Host"
},
"LXC": {
"es": "LXC",
"fr": "LXC",
"de": "LXC",
"it": "LXC",
"pt": "LXC"
},
"Configure LXC Mount Points (Host ↔ Container)": {
"es": "Configurar puntos de montaje LXC (Host ↔ LXC)",
"fr": "Configurer les points de montage LXC (Hôte ↔ LXC)",
"de": "LXC-Mount-Punkte konfigurieren (Host ↔ LXC)",
"it": "Configurare punti di mount LXC (Host ↔ LXC)",
"pt": "Configurar pontos de montagem LXC (Host ↔ LXC)"
},
"Configure NFS Client in LXC (only privileged)": {
"es": "Configurar cliente NFS en LXC (solo privilegiados)",
"fr": "Configurer le client NFS dans LXC (privilégiés uniquement)",
"de": "NFS-Client in LXC konfigurieren (nur privilegiert)",
"it": "Configurare client NFS in LXC (solo privilegiati)",
"pt": "Configurar cliente NFS em LXC (apenas privilegiados)"
},
"Configure Samba Client in LXC (only privileged)": {
"es": "Configurar cliente Samba en LXC (solo privilegiados)",
"fr": "Configurer le client Samba dans LXC (privilégiés uniquement)",
"de": "Samba-Client in LXC konfigurieren (nur privilegiert)",
"it": "Configurare client Samba in LXC (solo privilegiati)",
"pt": "Configurar cliente Samba em LXC (apenas privilegiados)"
},
"Configure NFS Server in LXC (only privileged)": {
"es": "Configurar servidor NFS en LXC (solo privilegiados)",
"fr": "Configurer le serveur NFS dans LXC (privilégiés uniquement)",
"de": "NFS-Server in LXC konfigurieren (nur privilegiert)",
"it": "Configurare server NFS in LXC (solo privilegiati)",
"pt": "Configurar servidor NFS em LXC (apenas privilegiados)"
},
"configure Samba Server in LXC (only privileged)": {
"es": "Configurar servidor Samba en LXC (solo privilegiados)",
"fr": "Configurer le serveur Samba dans LXC (privilégiés uniquement)",
"de": "Samba-Server in LXC konfigurieren (nur privilegiert)",
"it": "Configurare server Samba in LXC (solo privilegiati)",
"pt": "Configurar servidor Samba em LXC (apenas privilegiados)"
},
"Help & Info (commands)": {
"es": "Comandos de ayuda e información",
"fr": "Aide et Informations (commandes)",
"de": "Hilfe & Informationen (Befehle)",
"it": "Aiuto e Informazioni (comandi)",
"pt": "Ajuda e Informações (comandos)"
},
"English": {
"es": "Inglés",
"fr": "Anglais",
"de": "Englisch",
"it": "Inglese",
"pt": "Inglês"
},
"Language Change": {
"es": "Cambio de Idioma",
"fr": "Changement de Langue",
"de": "Sprachänderung",
"it": "Cambio Lingua",
"pt": "Mudança de Idioma"
} }
} }

View File

@@ -27,7 +27,7 @@
"type": "ct" "type": "ct"
}, },
{ {
"name": "Proxmox VE LXC Tag", "name": "PVE LXC Tag",
"slug": "add-iptag", "slug": "add-iptag",
"desc": "This script automatically adds IP address as tags to LXC containers or VM's using a systemd service. The service also updates the tags if a LXC/VM IP address is changed.", "desc": "This script automatically adds IP address as tags to LXC containers or VM's using a systemd service. The service also updates the tags if a LXC/VM IP address is changed.",
"script": "tools/pve/add-iptag.sh", "script": "tools/pve/add-iptag.sh",
@@ -84,7 +84,7 @@
5 5
], ],
"notes": [ "notes": [
"Adguard Home can be updated via the user interface." "AdGuard Home can only be updated via the user interface."
], ],
"type": "ct" "type": "ct"
}, },
@@ -365,6 +365,22 @@
"notes": [], "notes": [],
"type": "ct" "type": "ct"
}, },
{
"name": "Autocaliweb",
"slug": "autocaliweb",
"desc": "A modern web management system for eBooks, eComics and PDFs",
"script": "ct/autocaliweb.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/autocaliweb.sh",
"categories": [
13
],
"notes": [],
"type": "ct",
"default_credentials": {
"username": "admin",
"password": "admin123"
}
},
{ {
"name": "Baby Buddy", "name": "Baby Buddy",
"slug": "babybuddy", "slug": "babybuddy",
@@ -498,7 +514,9 @@
"categories": [ "categories": [
13 13
], ],
"notes": [], "notes": [
"Starting Booklore (Web UI) may take up to 2 minutes after a restart or fresh installation."
],
"type": "ct" "type": "ct"
}, },
{ {
@@ -617,7 +635,7 @@
"type": "ct" "type": "ct"
}, },
{ {
"name": "Proxmox VE LXC Cleaner", "name": "PVE LXC Cleaner",
"slug": "clean-lxcs", "slug": "clean-lxcs",
"desc": "This script provides options to delete logs and cache, and repopulate apt lists for Ubuntu and Debian systems.", "desc": "This script provides options to delete logs and cache, and repopulate apt lists for Ubuntu and Debian systems.",
"script": "tools/pve/clean-lxcs.sh", "script": "tools/pve/clean-lxcs.sh",
@@ -631,7 +649,7 @@
"type": "pve" "type": "pve"
}, },
{ {
"name": "Proxmox Clean Orphaned LVM", "name": "PVE Clean Orphaned LVM",
"slug": "clean-orphaned-lvm", "slug": "clean-orphaned-lvm",
"desc": "This script helps Proxmox users identify and remove orphaned LVM volumes that are no longer associated with any VM or LXC container. It scans all LVM volumes, detects unused ones, and provides an interactive prompt to delete them safely. System-critical volumes like root, swap, and data are excluded to prevent accidental deletion.", "desc": "This script helps Proxmox users identify and remove orphaned LVM volumes that are no longer associated with any VM or LXC container. It scans all LVM volumes, detects unused ones, and provides an interactive prompt to delete them safely. System-critical volumes like root, swap, and data are excluded to prevent accidental deletion.",
"script": "tools/pve/clean-orphaned-lvm.sh", "script": "tools/pve/clean-orphaned-lvm.sh",
@@ -797,7 +815,9 @@
2, 2,
3 3
], ],
"notes": [], "notes": [
"The file `/etc/sysconfig/CosmosCloud` is optional. If you need custom settings, you can create it yourself."
],
"type": "ct" "type": "ct"
}, },
{ {
@@ -819,9 +839,9 @@
} }
}, },
{ {
"name": "Proxmox VE Cron LXC Updater", "name": "PVE Cron LXC Updater",
"slug": "cron-update-lxcs", "slug": "cron-update-lxcs",
"desc": "This script will add/remove a crontab schedule that updates all LXCs every Sunday at midnight.", "desc": "This script will add/remove a crontab schedule that updates the operating system of all LXCs every Sunday at midnight.",
"script": "tools/pve/cron-update-lxcs.sh", "script": "tools/pve/cron-update-lxcs.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/cron-update-lxcs.sh", "script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/cron-update-lxcs.sh",
"categories": [ "categories": [
@@ -1118,7 +1138,8 @@
], ],
"notes": [ "notes": [
"Type `cat ~/matrix.creds` to see admin username/password.", "Type `cat ~/matrix.creds` to see admin username/password.",
"Synapse-Admin is running on port 5173" "Synapse-Admin is running on port 5173",
"For bridges Installation methods (WhatsApp, Signal, Discord, etc.), see: \u00b4https://docs.mau.fi/bridges/go/setup.html\u00b4"
], ],
"type": "ct" "type": "ct"
}, },
@@ -1204,6 +1225,22 @@
"notes": [], "notes": [],
"type": "ct" "type": "ct"
}, },
{
"name": "PVE LXC Execute Command",
"slug": "lxc-execute",
"desc": "This script allows administrators to execute a custom command inside one or multiple LXC containers on a Proxmox VE node. Containers can be selectively excluded via an interactive checklist. If a container is stopped, the script will automatically start it, run the command, and then shut it down again. Only Debian and Ubuntu based containers are supported.",
"script": "tools/pve/execute.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/execute.sh",
"categories": [
1
],
"notes": [
"Execute within the Proxmox shell.",
"Non-Debian/Ubuntu containers will be skipped automatically.",
"Stopped containers will be started temporarily to run the command, then shut down again."
],
"type": "pve"
},
{ {
"name": "Fenrus", "name": "Fenrus",
"slug": "fenrus", "slug": "fenrus",
@@ -1304,7 +1341,9 @@
"categories": [ "categories": [
14 14
], ],
"notes": [], "notes": [
"Flaresolverr is pinned to Version 3.3.25 because they add an breaking python package which doesn't work with debian 12.`"
],
"type": "ct" "type": "ct"
}, },
{ {
@@ -1378,22 +1417,7 @@
"type": "ct" "type": "ct"
}, },
{ {
"name": "Frigate", "name": "PVE LXC Filesystem Trim",
"slug": "frigate",
"desc": "Frigate is an open source NVR built around real-time AI object detection. All processing is performed locally on your own hardware, and your camera feeds never leave your home.",
"script": "ct/frigate.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/frigate.sh",
"categories": [
15
],
"notes": [
"Discussions (explore more advanced methods): `https://github.com/tteck/Proxmox/discussions/2711`",
"go2rtc Interface port:`1984`"
],
"type": "ct"
},
{
"name": "Proxmox VE LXC Filesystem Trim",
"slug": "fstrim", "slug": "fstrim",
"desc": "This maintains SSD performance by managing unused blocks. Thin-provisioned storage systems also require management to prevent unnecessary storage use. VMs automate fstrim, while LXC containers need manual or automated fstrim processes for optimal performance.", "desc": "This maintains SSD performance by managing unused blocks. Thin-provisioned storage systems also require management to prevent unnecessary storage use. VMs automate fstrim, while LXC containers need manual or automated fstrim processes for optimal performance.",
"script": "tools/pve/fstrim.sh", "script": "tools/pve/fstrim.sh",
@@ -1445,6 +1469,23 @@
], ],
"type": "ct" "type": "ct"
}, },
{
"name": "Ghostfolio",
"slug": "ghostfolio",
"desc": "Ghostfolio is an open source wealth management software built with web technology. The application empowers busy people to keep track of stocks, ETFs or cryptocurrencies and make solid, data-driven investment decisions.",
"script": "ct/ghostfolio.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/ghostfolio.sh",
"categories": [
23
],
"notes": [
"Create your first user account by visiting the web interface and clicking 'Get Started'. The first user will automatically get admin privileges.",
"Database and Redis credentials: `cat ~/ghostfolio.creds`",
"Optional: CoinGecko API keys can be added during installation or later in the .env file for enhanced cryptocurrency data.",
"Build process requires 4GB RAM (runtime: ~2GB). A temporary swap file will be created automatically if insufficient memory is detected."
],
"type": "ct"
},
{ {
"name": "Gitea-Mirror", "name": "Gitea-Mirror",
"slug": "gitea-mirror", "slug": "gitea-mirror",
@@ -1495,6 +1536,18 @@
], ],
"type": "addon" "type": "addon"
}, },
{
"name": "GlobaLeaks",
"slug": "globaleaks",
"desc": "GlobaLeaks is a free and open-source whistleblowing software enabling anyone to easily set up and maintain a secure reporting platform.",
"script": "ct/globaleaks.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/globaleaks.sh",
"categories": [
0
],
"notes": [],
"type": "ct"
},
{ {
"name": "GLPI", "name": "GLPI",
"slug": "glpi", "slug": "glpi",
@@ -1523,6 +1576,20 @@
"notes": [], "notes": [],
"type": "ct" "type": "ct"
}, },
{
"name": "GoAway",
"slug": "goaway",
"desc": "Lightweight DNS sinkhole written in Go with a modern dashboard client. Very good looking new alternative to Pi-Hole and Adguard Home.",
"script": "ct/goaway.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/goaway.sh",
"categories": [
5
],
"notes": [
"Type `cat ~/goaway.creds` to see login credentials."
],
"type": "ct"
},
{ {
"name": "Gokapi", "name": "Gokapi",
"slug": "gokapi", "slug": "gokapi",
@@ -1603,7 +1670,7 @@
{ {
"name": "Grist", "name": "Grist",
"slug": "grist", "slug": "grist",
"desc": "Grist is a modern, open source spreadsheet that goes beyond the grid", "desc": "Grist is like a spreadsheet + database hybrid. It lets you store structured data, use relational links between tables, apply formulas (even with Python), build custom layouts (cards, forms, dashboards), set fine-grained access rules, and visualize data with charts or pivot-tables.",
"script": "ct/grist.sh", "script": "ct/grist.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/grist.sh", "script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/grist.sh",
"categories": [ "categories": [
@@ -1672,6 +1739,21 @@
], ],
"type": "ct" "type": "ct"
}, },
{
"name": "Healthchecks",
"slug": "healthchecks",
"desc": "Healthchecks is a cron job monitoring service. It listens for HTTP requests and email messages (\"pings\") from your cron jobs and scheduled tasks (\"checks\"). When a ping does not arrive on time, Healthchecks sends out alerts. Healthchecks comes with a web dashboard, API, 25+ integrations for delivering notifications, monthly email reports, WebAuthn 2FA support, team management features: projects, team members, read-only access.",
"script": "ct/healthchecks.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/healthchecks.sh",
"categories": [
9
],
"notes": [
"if you change your LXC-IP, you need to update /etc/caddy/Caddyfile & /opt/healthchecks/hc/local_settings.py",
"Show credentials: `cat ~/healthchecks.creds`"
],
"type": "ct"
},
{ {
"name": "Heimdall Dashboard", "name": "Heimdall Dashboard",
"slug": "heimdall-dashboard", "slug": "heimdall-dashboard",
@@ -1735,6 +1817,7 @@
16 16
], ],
"notes": [ "notes": [
"Containerized version doesn't allow Home Assistant add-ons.",
"If the LXC is created Privileged, the script will automatically set up USB passthrough.", "If the LXC is created Privileged, the script will automatically set up USB passthrough.",
"config path: `/var/lib/docker/volumes/hass_config/_data`", "config path: `/var/lib/docker/volumes/hass_config/_data`",
"Portainer interface: $IP: 9443 - User & password must be set manually within 5 minutes, otherwise a restart of Portainer is required!", "Portainer interface: $IP: 9443 - User & password must be set manually within 5 minutes, otherwise a restart of Portainer is required!",
@@ -1814,7 +1897,7 @@
"type": "ct" "type": "ct"
}, },
{ {
"name": "Proxmox VE Host Backup", "name": "PVE Host Backup",
"slug": "host-backup", "slug": "host-backup",
"desc": "This script serves as a versatile backup utility, enabling users to specify both the backup path and the directory they want to work in. This flexibility empowers users to select the specific files and directories they wish to back up, making it compatible with a wide range of hosts, not limited to Proxmox.", "desc": "This script serves as a versatile backup utility, enabling users to specify both the backup path and the directory they want to work in. This flexibility empowers users to select the specific files and directories they wish to back up, making it compatible with a wide range of hosts, not limited to Proxmox.",
"script": "tools/pve/host-backup.sh", "script": "tools/pve/host-backup.sh",
@@ -1874,6 +1957,7 @@
13 13
], ],
"notes": [ "notes": [
"Please be aware that Immich releases are pinned to specific versions until compatibility has been confirmed by the Community Scripts maintainers; as a result, the version installed by the helper script may not be the most current version of Immich",
"During installation, you will be prompted with the option to install Intel OpenVINO for hardware-accelerated machine-learning. If you opt in, increase your LXC RAM after installation, as OpenVINO is memory-intensive", "During installation, you will be prompted with the option to install Intel OpenVINO for hardware-accelerated machine-learning. If you opt in, increase your LXC RAM after installation, as OpenVINO is memory-intensive",
"HW-accelerated video transcoding is supported, but must be enabled in Immich Settings", "HW-accelerated video transcoding is supported, but must be enabled in Immich Settings",
"To change upload location, edit 'IMMICH_MEDIA_LOCATION' in `/opt/immich/.env`, and create the symlink 'upload' in /opt/immich/app & /opt/immich/app/machine-learning to your new upload location", "To change upload location, edit 'IMMICH_MEDIA_LOCATION' in `/opt/immich/.env`, and create the symlink 'upload' in /opt/immich/app & /opt/immich/app/machine-learning to your new upload location",
@@ -2041,6 +2125,24 @@
"notes": [], "notes": [],
"type": "ct" "type": "ct"
}, },
{
"name": "Joplin Server",
"slug": "joplin-server",
"desc": "Joplin - the privacy-focused note taking app with sync capabilities for Windows, macOS, Linux, Android and iOS.",
"script": "ct/joplin-server.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/joplin-server.sh",
"categories": [
12
],
"notes": [
"Application can take some time to build, depending on your host speed. Please be patient."
],
"type": "ct",
"default_credentials": {
"username": "admin@localhost",
"password": "admin"
}
},
{ {
"name": "Jupyter Notebook", "name": "Jupyter Notebook",
"slug": "jupyternotebook", "slug": "jupyternotebook",
@@ -2091,7 +2193,7 @@
"notes": [ "notes": [
"WARNING: Installation sources scripts outside of Community Scripts repo. Please check the source before installing.", "WARNING: Installation sources scripts outside of Community Scripts repo. Please check the source before installing.",
"Kasm needs swap (on Proxmox host) and activated FUSE to be installed successfully!", "Kasm needs swap (on Proxmox host) and activated FUSE to be installed successfully!",
"Show password: `cat ~/kasm.creds`" "Show credentials: `cat ~/kasm.creds`"
], ],
"type": "ct" "type": "ct"
}, },
@@ -2110,7 +2212,7 @@
"type": "ct" "type": "ct"
}, },
{ {
"name": "Proxmox VE Kernel Clean", "name": "PVE Kernel Clean",
"slug": "kernel-clean", "slug": "kernel-clean",
"desc": "Cleaning unused kernel images is beneficial for reducing the length of the GRUB menu and freeing up disk space. By removing old, unused kernels, the system is able to conserve disk space and streamline the boot process.", "desc": "Cleaning unused kernel images is beneficial for reducing the length of the GRUB menu and freeing up disk space. By removing old, unused kernels, the system is able to conserve disk space and streamline the boot process.",
"script": "tools/pve/kernel-clean.sh", "script": "tools/pve/kernel-clean.sh",
@@ -2124,7 +2226,7 @@
"type": "pve" "type": "pve"
}, },
{ {
"name": "Proxmox VE Kernel Pin", "name": "PVE Kernel Pin",
"slug": "kernel-pin", "slug": "kernel-pin",
"desc": "Kernel Pin is an essential tool for effortlessly managing kernel pinning and unpinning.", "desc": "Kernel Pin is an essential tool for effortlessly managing kernel pinning and unpinning.",
"script": "tools/pve/kernel-pin.sh", "script": "tools/pve/kernel-pin.sh",
@@ -2148,7 +2250,6 @@
], ],
"notes": [ "notes": [
"First start can take a few minutes", "First start can take a few minutes",
"This script requires some extra steps after the installation, Please checkout the `https://github.com/community-scripts/ProxmoxVE/discussions/193`",
"When updating, if you had modified cache-ispn.xml: Re-apply your changes to the new file, otherwise leave it unchanged." "When updating, if you had modified cache-ispn.xml: Re-apply your changes to the new file, otherwise leave it unchanged."
], ],
"type": "ct", "type": "ct",
@@ -2224,7 +2325,7 @@
3 3
], ],
"notes": [ "notes": [
"After the initial installation: Enter your desired admin user and password and then click on Sign Up" "For admin username and password type `cat ~/komodo.creds` inside LXC."
], ],
"type": "ct" "type": "ct"
}, },
@@ -2252,6 +2353,18 @@
"notes": [], "notes": [],
"type": "ct" "type": "ct"
}, },
{
"name": "Leantime",
"slug": "leantime",
"desc": "Leantime is a goals focused project management system for non-project managers. Building with ADHD, Autism, and dyslexia in mind. ",
"script": "ct/leantime.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/leantime.sh",
"categories": [
12
],
"notes": [],
"type": "ct"
},
{ {
"name": "Librespeed Rust", "name": "Librespeed Rust",
"slug": "librespeed-rust", "slug": "librespeed-rust",
@@ -2330,6 +2443,24 @@
"notes": [], "notes": [],
"type": "ct" "type": "ct"
}, },
{
"name": "LiteLLM",
"slug": "litellm",
"desc": "LLM proxy to call 100+ LLMs in a unified interface & track spend, set budgets per virtual key/user",
"script": "ct/litellm.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/litellm.sh",
"categories": [
20
],
"notes": [
"Update master key in the config file"
],
"type": "ct",
"default_credentials": {
"username": "admin",
"password": "sk-1234"
}
},
{ {
"name": "lldap", "name": "lldap",
"slug": "lldap", "slug": "lldap",
@@ -2359,7 +2490,7 @@
"type": "ct" "type": "ct"
}, },
{ {
"name": "Container LXC Deletion", "name": "PVE LXC Deletion",
"slug": "lxc-delete", "slug": "lxc-delete",
"desc": "This script helps manage and delete LXC containers on a Proxmox VE server. It lists all available containers, allowing the user to select one or more for deletion through an interactive menu. Running containers are automatically stopped before deletion, and the user is asked to confirm each action. The script ensures a controlled and efficient container management process.", "desc": "This script helps manage and delete LXC containers on a Proxmox VE server. It lists all available containers, allowing the user to select one or more for deletion through an interactive menu. Running containers are automatically stopped before deletion, and the user is asked to confirm each action. The script ensures a controlled and efficient container management process.",
"script": "tools/pve/lxc-delete.sh", "script": "tools/pve/lxc-delete.sh",
@@ -2480,6 +2611,26 @@
"notes": [], "notes": [],
"type": "ct" "type": "ct"
}, },
{
"name": "MediaManager",
"slug": "mediamanager",
"desc": "A modern selfhosted media management system for your media library",
"script": "ct/mediamanager.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/mediamanager.sh",
"categories": [
14,
13
],
"notes": [
"During the installation, provide the email address of the first admin user",
"You're probably going to want to use a bind mount for the media directories"
],
"type": "ct",
"default_credentials": {
"username": "<email address>",
"password": "admin"
}
},
{ {
"name": "MediaMTX", "name": "MediaMTX",
"slug": "mediamtx", "slug": "mediamtx",
@@ -2556,7 +2707,7 @@
"type": "ct" "type": "ct"
}, },
{ {
"name": "Proxmox VE Processor Microcode", "name": "PVE Processor Microcode",
"slug": "microcode", "slug": "microcode",
"desc": "Processor Microcode is a layer of low-level software that runs on the processor and provides patches or updates to its firmware. Microcode updates can fix hardware bugs, improve performance, and enhance security features of the processor.\r\n\r\nIt's important to note that the availability of firmware update mechanisms, such as Intel's Management Engine (ME) or AMD's Platform Security Processor (PSP), may vary depending on the processor and its specific implementation. Therefore, it's recommended to consult the documentation for your processor to confirm whether firmware updates can be applied through the operating system.", "desc": "Processor Microcode is a layer of low-level software that runs on the processor and provides patches or updates to its firmware. Microcode updates can fix hardware bugs, improve performance, and enhance security features of the processor.\r\n\r\nIt's important to note that the availability of firmware update mechanisms, such as Intel's Management Engine (ME) or AMD's Platform Security Processor (PSP), may vary depending on the processor and its specific implementation. Therefore, it's recommended to consult the documentation for your processor to confirm whether firmware updates can be applied through the operating system.",
"script": "tools/pve/microcode.sh", "script": "tools/pve/microcode.sh",
@@ -2650,7 +2801,7 @@
} }
}, },
{ {
"name": "Proxmox VE Monitor-All", "name": "PVE Monitor-All",
"slug": "monitor-all", "slug": "monitor-all",
"desc": "This script will add Monitor-All to Proxmox VE, which will monitor the status of all your instances, both containers and virtual machines, excluding templates and user-defined ones, and automatically restart or reset them if they become unresponsive. This is particularly useful if you're experiencing problems with Home Assistant becoming non-responsive every few days/weeks. Monitor-All also maintains a log of the entire process, which can be helpful for troubleshooting and monitoring purposes.\r\n\r\n\ud83d\udec8 Virtual machines without the QEMU guest agent installed must be excluded.\r\n\ud83d\udec8 Prior to generating any new CT/VM not found in this repository, it's necessary to halt Proxmox VE Monitor-All by running systemctl stop ping-instances.", "desc": "This script will add Monitor-All to Proxmox VE, which will monitor the status of all your instances, both containers and virtual machines, excluding templates and user-defined ones, and automatically restart or reset them if they become unresponsive. This is particularly useful if you're experiencing problems with Home Assistant becoming non-responsive every few days/weeks. Monitor-All also maintains a log of the entire process, which can be helpful for troubleshooting and monitoring purposes.\r\n\r\n\ud83d\udec8 Virtual machines without the QEMU guest agent installed must be excluded.\r\n\ud83d\udec8 Prior to generating any new CT/VM not found in this repository, it's necessary to halt Proxmox VE Monitor-All by running systemctl stop ping-instances.",
"script": "tools/pve/monitor-all.sh", "script": "tools/pve/monitor-all.sh",
@@ -2700,6 +2851,18 @@
], ],
"type": "ct" "type": "ct"
}, },
{
"name": "MyIP",
"slug": "myip",
"desc": "The best IP Toolbox. Easy to check what's your IPs, IP geolocation, check for DNS leaks, examine WebRTC connections, speed test, ping test, MTR test, check website availability, whois search and more!",
"script": "ct/myip.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/myip.sh",
"categories": [
4
],
"notes": [],
"type": "ct"
},
{ {
"name": "Mylar3", "name": "Mylar3",
"slug": "mylar3", "slug": "mylar3",
@@ -2799,7 +2962,7 @@
"type": "ct" "type": "ct"
}, },
{ {
"name": "Proxmox VE Netdata", "name": "PVE Netdata",
"slug": "netdata", "slug": "netdata",
"desc": "Netdata is an open-source, real-time performance monitoring tool designed to provide insights into the performance and health of systems and applications. It is often used by system administrators, DevOps professionals, and developers to monitor and troubleshoot issues on servers and other devices.", "desc": "Netdata is an open-source, real-time performance monitoring tool designed to provide insights into the performance and health of systems and applications. It is often used by system administrators, DevOps professionals, and developers to monitor and troubleshoot issues on servers and other devices.",
"script": "tools/addon/netdata.sh", "script": "tools/addon/netdata.sh",
@@ -2965,11 +3128,11 @@
"notes": [ "notes": [
"This uses Docker under the hood, as this can not easily be installed bare-metal. ", "This uses Docker under the hood, as this can not easily be installed bare-metal. ",
"The initial starting process can be take 1-2min. ", "The initial starting process can be take 1-2min. ",
"Application credentials: `cat /opt/.npm_pwd`" "Application credentials: `cat /opt/.npm_pwd` - if file not exist in LXC check docker logs for password with `docker logs npmplus`"
], ],
"type": "ct", "type": "ct",
"default_credentials": { "default_credentials": {
"username": "root", "username": "admin@example.org",
"password": null "password": null
} }
}, },
@@ -3220,15 +3383,17 @@
"categories": [ "categories": [
20 20
], ],
"notes": [], "notes": [
"Script contains optional installation of Ollama."
],
"type": "ct" "type": "ct"
}, },
{ {
"name": "OpenWrt", "name": "OpenWrt",
"slug": "openwrt", "slug": "openwrt-vm",
"desc": "OpenWrt is a powerful open-source firmware that can transform a wide range of networking devices into highly customizable and feature-rich routers, providing users with greater control and flexibility over their network infrastructure.", "desc": "OpenWrt is a powerful open-source firmware that can transform a wide range of networking devices into highly customizable and feature-rich routers, providing users with greater control and flexibility over their network infrastructure.",
"script": "vm/openwrt.sh", "script": "vm/openwrt-vm.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/vm/openwrt.sh", "script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/vm/openwrt-vm.sh",
"categories": [ "categories": [
4, 4,
2 2
@@ -3364,8 +3529,7 @@
11 11
], ],
"notes": [ "notes": [
"This LXC is very memory-hungry when updating; it requires at least 6GB RAM, but RAM may be reduced to as low as 2GB when running normally", "To use a bind mount for storage, create symlinks to your mount for both `uploads` and `temp-uploads` in `/opt/palmr_data`, and uncomment `CUSTOM_PATH` to add the path to your bind mount",
"To use a bind mount for storage, create symlinks to your mount for both `uploads` and `temp-uploads` in `/opt/palmr_data`",
"To use Palmr with a reverse proxy, uncomment `SECURE_SITE` in `/opt/palmr/apps/server/.env`" "To use Palmr with a reverse proxy, uncomment `SECURE_SITE` in `/opt/palmr/apps/server/.env`"
], ],
"type": "ct" "type": "ct"
@@ -3406,14 +3570,10 @@
12 12
], ],
"notes": [ "notes": [
"Show Login Credentials, type `cat ~/paperless.creds` in the LXC console", "Show Login Credentials, type `cat ~/paperless-ngx.creds` in the LXC console",
"Script installs English as default OCR language. To install additional languages, use `apt-get install tesseract-ocr-[lang]`, where [lang] is the language code (e.g. `apt-get install tesseract-ocr-deu`)." "Script installs English as default OCR language. To install additional languages, use `apt-get install tesseract-ocr-[lang]`, where [lang] is the language code (e.g. `apt-get install tesseract-ocr-deu`)."
], ],
"type": "ct", "type": "ct"
"default_credentials": {
"username": "admin",
"password": null
}
}, },
{ {
"name": "Part-DB", "name": "Part-DB",
@@ -3448,7 +3608,7 @@
} }
}, },
{ {
"name": "Proxmox Backup Server Processor Microcode", "name": "PBS Processor Microcode",
"slug": "pbs-microcode", "slug": "pbs-microcode",
"desc": "Processor Microcode is a layer of low-level software that runs on the processor and provides patches or updates to its firmware. Microcode updates can fix hardware bugs, improve performance, and enhance security features of the processor. This script is adapted for the Proxmox Backup Server environment and will only run on bare metal systems. If running in a virtualized environment, the script will exit. Note that firmware update mechanisms, such as Intel's Management Engine (ME) or AMD's Platform Security Processor (PSP), may vary depending on your processor and its implementation. Please consult your processor's documentation to verify if firmware updates can be applied through the operating system.", "desc": "Processor Microcode is a layer of low-level software that runs on the processor and provides patches or updates to its firmware. Microcode updates can fix hardware bugs, improve performance, and enhance security features of the processor. This script is adapted for the Proxmox Backup Server environment and will only run on bare metal systems. If running in a virtualized environment, the script will exit. Note that firmware update mechanisms, such as Intel's Management Engine (ME) or AMD's Platform Security Processor (PSP), may vary depending on your processor and its implementation. Please consult your processor's documentation to verify if firmware updates can be applied through the operating system.",
"script": "tools/pve/pbs_microcode.sh", "script": "tools/pve/pbs_microcode.sh",
@@ -3462,6 +3622,23 @@
], ],
"type": "pve" "type": "pve"
}, },
{
"name": "PBS 4 Upgrade",
"slug": "pbs4-upgrade",
"desc": "This script guides you through upgrading Proxmox Backup Server from version 3.x (Debian 12 Bookworm) to version 4.0 (Debian 13 Trixie). It adjusts the Debian base sources, configures PBS 4 repositories in deb822 format, updates enterprise/no-subscription/test repos, runs a full system upgrade, and finalizes with a reboot.",
"script": "tools/pve/pbs4-upgrade.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/pbs4-upgrade.sh",
"categories": [
1
],
"notes": [
"Execute this script directly on the PBS 3.x host as root.",
"Ensure you have a verified backup of /etc/proxmox-backup before starting.",
"Do not run this on an already upgraded PBS 4.x system.",
"A reboot is strongly recommended after upgrade to activate the new kernel and services."
],
"type": "pve"
},
{ {
"name": "PeaNUT", "name": "PeaNUT",
"slug": "peanut", "slug": "peanut",
@@ -3561,6 +3738,21 @@
"password": "ipamadmin" "password": "ipamadmin"
} }
}, },
{
"name": "PhpMyAdmin",
"slug": "phpmyadmin",
"desc": "phpMyAdmin is a free software tool written in PHP, intended to handle the administration of MySQL over the Web. phpMyAdmin supports a wide range of operations on MySQL and MariaDB. Frequently used operations (managing databases, tables, columns, relations, indexes, users, permissions, etc) can be performed via the user interface, while you still have the ability to directly execute any SQL statement.",
"script": "tools/addon/phpmyadmin.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/addon/phpmyadmin.sh",
"categories": [
8
],
"notes": [
"Execute within an existing LXC Console",
"To update or uninstall run bash call again"
],
"type": "addon"
},
{ {
"name": "Pi.Alert", "name": "Pi.Alert",
"slug": "pialert", "slug": "pialert",
@@ -3570,9 +3762,7 @@
"categories": [ "categories": [
4 4
], ],
"notes": [ "notes": [],
"WARNING: Installation sources scripts outside of Community Scripts repo. Please check the source before installing."
],
"type": "ct" "type": "ct"
}, },
{ {
@@ -3645,8 +3835,7 @@
13 13
], ],
"notes": [ "notes": [
"With Privileged/Unprivileged Hardware Acceleration Support", "With Privileged/Unprivileged Hardware Acceleration Support"
"WARNING: Installation sources scripts outside of Community Scripts repo. Please check the source before installing."
], ],
"type": "ct" "type": "ct"
}, },
@@ -3688,8 +3877,7 @@
"notes": [ "notes": [
"If the LXC is created Privileged, the script will automatically set up USB passthrough.", "If the LXC is created Privileged, the script will automatically set up USB passthrough.",
"config path: `/var/lib/containers/storage/volumes/hass_config/_data`", "config path: `/var/lib/containers/storage/volumes/hass_config/_data`",
"Options to Install Portainer or Portainer Agent", "Options to Install Portainer or Portainer Agent"
"WARNING: Installation sources scripts outside of Community Scripts repo. Please check the source before installing."
], ],
"type": "ct" "type": "ct"
}, },
@@ -3708,9 +3896,9 @@
"type": "ct" "type": "ct"
}, },
{ {
"name": "Proxmox Backup Server Post Install", "name": "PBS Post Install",
"slug": "post-pbs-install", "slug": "post-pbs-install",
"desc": "The script will give options to Disable the Enterprise Repo, Add/Correct PBS Sources, Enable the No-Subscription Repo, Add Test Repo, Disable Subscription Nag, Update Proxmox Backup Server and Reboot PBS.", "desc": "The script is designed for Proxmox Backup Server (PBS) and will give options to Disable the Enterprise Repo, Add/Correct PBS Sources, Enable the No-Subscription Repo, Add Test Repo, Disable Subscription Nag, Update Proxmox Backup Server and Reboot PBS.",
"script": "tools/pve/post-pbs-install.sh", "script": "tools/pve/post-pbs-install.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/post-pbs-install.sh", "script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/post-pbs-install.sh",
"categories": [ "categories": [
@@ -3724,9 +3912,9 @@
"type": "pve" "type": "pve"
}, },
{ {
"name": "Proxmox Mail Gateway Post Install", "name": "PMG Post Install",
"slug": "post-pmg-install", "slug": "post-pmg-install",
"desc": "The script will give options to Disable the Enterprise Repo, Add/Correct PMG Sources, Enable the No-Subscription Repo, Add Test Repo, Disable Subscription Nag, Update Proxmox Mail Gateway and Reboot PMG.", "desc": "The script is designed for Proxmox Mail Gateway and will give options to Disable the Enterprise Repo, Add/Correct PMG Sources, Enable the No-Subscription Repo, Add Test Repo, Disable Subscription Nag, Update Proxmox Mail Gateway and Reboot PMG.",
"script": "tools/pve/post-pmg-install.sh", "script": "tools/pve/post-pmg-install.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/post-pmg-install.sh", "script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/post-pmg-install.sh",
"categories": [ "categories": [
@@ -3740,7 +3928,7 @@
"type": "pve" "type": "pve"
}, },
{ {
"name": "Proxmox VE Post Install", "name": "PVE Post Install",
"slug": "post-pve-install", "slug": "post-pve-install",
"desc": "This script provides options for managing Proxmox VE repositories, including disabling the Enterprise Repo, adding or correcting PVE sources, enabling the No-Subscription Repo, adding the test Repo, disabling the subscription nag, updating Proxmox VE, and rebooting the system.", "desc": "This script provides options for managing Proxmox VE repositories, including disabling the Enterprise Repo, adding or correcting PVE sources, enabling the No-Subscription Repo, adding the test Repo, disabling the subscription nag, updating Proxmox VE, and rebooting the system.",
"script": "tools/pve/post-pve-install.sh", "script": "tools/pve/post-pve-install.sh",
@@ -3861,7 +4049,7 @@
"type": "ct" "type": "ct"
}, },
{ {
"name": "Proxmox Backup Server", "name": "Proxmox Backup Server (PBS)",
"slug": "proxmox-backup-server", "slug": "proxmox-backup-server",
"desc": "Proxmox Backup Server is an enterprise backup solution, for backing up and restoring VMs, containers, and physical hosts. By supporting incremental, fully deduplicated backups, Proxmox Backup Server significantly reduces network load and saves valuable storage space.", "desc": "Proxmox Backup Server is an enterprise backup solution, for backing up and restoring VMs, containers, and physical hosts. By supporting incremental, fully deduplicated backups, Proxmox Backup Server significantly reduces network load and saves valuable storage space.",
"script": "ct/proxmox-backup-server.sh", "script": "ct/proxmox-backup-server.sh",
@@ -3871,7 +4059,7 @@
], ],
"notes": [ "notes": [
"Set a root password if using autologin. This will be the PBS password. `passwd root`", "Set a root password if using autologin. This will be the PBS password. `passwd root`",
"Advanced Install is only possible without root password and root SSH access, you can configure this after installation." "Advanced Install is only possible with disabled IPV6! Otherwise the installation sometimes stuck."
], ],
"type": "ct", "type": "ct",
"default_credentials": { "default_credentials": {
@@ -3880,7 +4068,7 @@
} }
}, },
{ {
"name": "Proxmox Datacenter Manager", "name": "Proxmox Datacenter Manager (PDM)",
"slug": "proxmox-datacenter-manager", "slug": "proxmox-datacenter-manager",
"desc": "The Proxmox Datacenter Manager project has been developed with the objective of providing a centralized overview of all your individual nodes and clusters. It also enables basic management like migrations of virtual guests without any cluster network requirements. ", "desc": "The Proxmox Datacenter Manager project has been developed with the objective of providing a centralized overview of all your individual nodes and clusters. It also enables basic management like migrations of virtual guests without any cluster network requirements. ",
"script": "ct/proxmox-datacenter-manager.sh", "script": "ct/proxmox-datacenter-manager.sh",
@@ -3895,7 +4083,7 @@
"type": "ct" "type": "ct"
}, },
{ {
"name": "Proxmox Mail Gateway", "name": "Proxmox Mail Gateway (PMG)",
"slug": "proxmox-mail-gateway", "slug": "proxmox-mail-gateway",
"desc": "Proxmox Mail Gateway is the leading open-source email security solution helping you to protect your mail server against all email threats from the moment they emerge.", "desc": "Proxmox Mail Gateway is the leading open-source email security solution helping you to protect your mail server against all email threats from the moment they emerge.",
"script": "ct/proxmox-mail-gateway.sh", "script": "ct/proxmox-mail-gateway.sh",
@@ -4122,6 +4310,32 @@
], ],
"type": "ct" "type": "ct"
}, },
{
"name": "Redlib",
"slug": "alpine-redlib",
"desc": "An alternative private front-end to Reddit. Redlib hopes to provide an easier way to browse Reddit, without the ads, trackers, and bloat.",
"script": "ct/alpine-redlib.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/alpine-redlib.sh",
"categories": [
10
],
"notes": [],
"type": "ct"
},
{
"name": "Resilio Sync",
"slug": "resiliosync",
"desc": "Fast, reliable, and simple file sync and share solution, powered by P2P technology. Sync files across all your devices without storing them in the cloud.",
"script": "ct/resiliosync.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/resiliosync.sh",
"categories": [
11
],
"notes": [
"After free registration, you will receive a license keyfile to your email address. Upload it into any LXC directory and select on first run."
],
"type": "ct"
},
{ {
"name": "RevealJS", "name": "RevealJS",
"slug": "revealjs", "slug": "revealjs",
@@ -4161,7 +4375,8 @@
], ],
"notes": [ "notes": [
"Check our configuration guide for help: `https://github.com/community-scripts/ProxmoxVE/discussions/2388`", "Check our configuration guide for help: `https://github.com/community-scripts/ProxmoxVE/discussions/2388`",
"Login credentials: `cat ~/rustdesk.creds`" "To set admin password on Debian, type `cd /var/lib/rustdesk-api && rustdesk-api reset-admin-pwd <yournewpasswordhere>` inside LXC.",
"To see admin password on Alpine, type `cat ~/rustdesk.creds` inside LXC."
], ],
"type": "ct" "type": "ct"
}, },
@@ -4190,7 +4405,7 @@
"type": "ct" "type": "ct"
}, },
{ {
"name": "Proxmox VE CPU Scaling Governor", "name": "PVE CPU Scaling Governor",
"slug": "scaling-governor", "slug": "scaling-governor",
"desc": "The CPU scaling governor determines how the CPU frequency is adjusted based on the workload, with the goal of either conserving power or improving performance. By scaling the frequency up or down, the operating system can optimize the CPU usage and conserve energy when possible. Generic Scaling Governors", "desc": "The CPU scaling governor determines how the CPU frequency is adjusted based on the workload, with the goal of either conserving power or improving performance. By scaling the frequency up or down, the operating system can optimize the CPU usage and conserve energy when possible. Generic Scaling Governors",
"script": "tools/pve/scaling-governor.sh", "script": "tools/pve/scaling-governor.sh",
@@ -4203,6 +4418,32 @@
], ],
"type": "pve" "type": "pve"
}, },
{
"name": "Scraparr",
"slug": "scraparr",
"desc": "Scraparr is a Prometheus exporter for the *arr suite (Sonarr, Radarr, Lidarr, etc.). It provides metrics that can be scraped by Prometheus to monitor and visualize the health and performance of your *arr applications.",
"script": "ct/scraparr.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/scraparr.sh",
"categories": [
14
],
"notes": [
"Edit config file then restart the scraparr service: `systemctl restart scraparr`"
],
"type": "ct"
},
{
"name": "SearXNG",
"slug": "searxng",
"desc": "SearXNG is a free internet metasearch engine which aggregates results from up to 215 search services. Users are neither tracked nor profiled. Additionally, SearXNG can be used over Tor for online anonymity.",
"script": "ct/searxng.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/searxng.sh",
"categories": [
0
],
"notes": [],
"type": "ct"
},
{ {
"name": "seelf", "name": "seelf",
"slug": "seelf", "slug": "seelf",
@@ -4264,6 +4505,20 @@
"password": "admin" "password": "admin"
} }
}, },
{
"name": "SigNoz",
"slug": "signoz",
"desc": "SigNoz is an open-source Datadog or New Relic alternative. Get APM, logs, traces, metrics, exceptions, & alerts in a single tool.",
"script": "ct/signoz.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/signoz.sh",
"categories": [
9
],
"notes": [
"The first user you register will be the admin user."
],
"type": "ct"
},
{ {
"name": "Silverbullet", "name": "Silverbullet",
"slug": "silverbullet", "slug": "silverbullet",
@@ -4395,6 +4650,18 @@
"password": "null" "password": "null"
} }
}, },
{
"name": "Stylus",
"slug": "stylus",
"desc": "Stylus (style + status) is a lightweight status page for infrastructure and networks. Configure a set of bash scripts that test the various parts of your infrastructure, set up visualizations with minimal configuration, and Stylus will generate you a dashboard for your system.",
"script": "ct/stylus.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/stylus.sh",
"categories": [
4
],
"notes": [],
"type": "ct"
},
{ {
"name": "Suwayomi-Server", "name": "Suwayomi-Server",
"slug": "suwayomi-server", "slug": "suwayomi-server",
@@ -4416,7 +4683,7 @@
"script": "ct/swizzin.sh", "script": "ct/swizzin.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/swizzin.sh", "script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/swizzin.sh",
"categories": [ "categories": [
15 13
], ],
"notes": [ "notes": [
"Installation might take a long time if choosing to install many apps. Be patient.", "Installation might take a long time if choosing to install many apps. Be patient.",
@@ -4541,6 +4808,20 @@
], ],
"type": "ct" "type": "ct"
}, },
{
"name": "Telegraf",
"slug": "telegraf",
"desc": "Telegraf collects and sends time series data from databases, systems, and IoT sensors. It has no external dependencies, is easy to install, and requires minimal memory.",
"script": "ct/telegraf.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/telegraf.sh",
"categories": [
9
],
"notes": [
"Make sure to configure an output for the telegraf config and start the service with `systemctl start telegraf`."
],
"type": "ct"
},
{ {
"name": "The Lounge", "name": "The Lounge",
"slug": "the-lounge", "slug": "the-lounge",
@@ -4595,6 +4876,24 @@
"notes": [], "notes": [],
"type": "ct" "type": "ct"
}, },
{
"name": "Tracktor",
"slug": "tracktor",
"desc": "Tracktor is an open-source web application for comprehensive vehicle management.\nEasily track fuel consumption, maintenance, insurance, and regulatory documents for all your vehicles in one place.",
"script": "ct/tracktor.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/tracktor.sh",
"categories": [
9
],
"notes": [
"Please check and update the '/opt/tracktor.env' file if using behind reverse proxy."
],
"type": "ct",
"default_credentials": {
"username": null,
"password": "123456"
}
},
{ {
"name": "Traefik", "name": "Traefik",
"slug": "traefik", "slug": "traefik",
@@ -4649,6 +4948,18 @@
], ],
"type": "ct" "type": "ct"
}, },
{
"name": "Tunarr",
"slug": "tunarr",
"desc": "Create a classic TV experience using your own media - IPTV backed by Plex/Jellyfin/Emby.",
"script": "ct/tunarr.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/tunarr.sh",
"categories": [
13
],
"notes": [],
"type": "ct"
},
{ {
"name": "TurnKey", "name": "TurnKey",
"slug": "turnkey", "slug": "turnkey",
@@ -4747,6 +5058,18 @@
], ],
"type": "vm" "type": "vm"
}, },
{
"name": "UHF Server",
"slug": "uhf",
"desc": "UHF Server is a powerful companion app that lets you seamlessly schedule and record your favorite shows from the UHF app.",
"script": "ct/uhf.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/uhf.sh",
"categories": [
13
],
"notes": [],
"type": "ct"
},
{ {
"name": "Umami", "name": "Umami",
"slug": "umami", "slug": "umami",
@@ -4836,21 +5159,22 @@
"type": "ct" "type": "ct"
}, },
{ {
"name": "Proxmox VE LXC Updater", "name": "PVE LXC Updater",
"slug": "update-lxcs", "slug": "update-lxcs",
"desc": "This script has been created to simplify and speed up the process of updating all LXC containers across various Linux distributions, such as Ubuntu, Debian, Devuan, Alpine Linux, CentOS-Rocky-Alma, Fedora, and ArchLinux. It's designed to automatically skip templates and specific containers during the update, enhancing its convenience and usability.", "desc": "This script has been created to simplify and speed up the process of updating the operating system running inside LXC containers across various Linux distributions, such as Ubuntu, Debian, Devuan, Alpine Linux, CentOS-Rocky-Alma, Fedora, and ArchLinux. It's designed to automatically skip templates and specific containers during the update, enhancing its convenience and usability.",
"script": "tools/pve/update-lxcs.sh", "script": "tools/pve/update-lxcs.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/update-lxcs.sh", "script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/update-lxcs.sh",
"categories": [ "categories": [
1 1
], ],
"notes": [ "notes": [
"Execute within the Proxmox shell" "Execute within the Proxmox shell",
"The script updates only the operating system of the LXC container. It DOES NOT update the application installed within the container!"
], ],
"type": "pve" "type": "pve"
}, },
{ {
"name": "Proxmox Update Repositories", "name": "PVE Update Repositories",
"slug": "update-repo", "slug": "update-repo",
"desc": "This script updates repository links in LXC containers, replacing old links from the tteck repository with links to the new community-scripts repository to fix issues related to updating scripts.", "desc": "This script updates repository links in LXC containers, replacing old links from the tteck repository with links to the new community-scripts repository to fix issues related to updating scripts.",
"script": "tools/pve/update-repo.sh", "script": "tools/pve/update-repo.sh",
@@ -4863,6 +5187,20 @@
], ],
"type": "pve" "type": "pve"
}, },
{
"name": "UpSnap",
"slug": "upsnap",
"desc": "UpSnap is a self-hosted web app that lets you wake up, manage and monitor devices on your network with ease. Built with SvelteKit, Go and PocketBase, it offers a clean dashboard, scheduled wake-ups, device discovery and secure user management.",
"script": "ct/upsnap.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/upsnap.sh",
"categories": [
4
],
"notes": [
"The first user you register will be the admin user."
],
"type": "ct"
},
{ {
"name": "Uptime Kuma", "name": "Uptime Kuma",
"slug": "uptimekuma", "slug": "uptimekuma",
@@ -4904,6 +5242,20 @@
], ],
"type": "ct" "type": "ct"
}, },
{
"name": "Verdaccio",
"slug": "verdaccio",
"desc": "Verdaccio is a lightweight private npm proxy registry built with Node.js. It allows you to host your own npm registry with minimal configuration, providing a private npm repository for your projects. Verdaccio supports npm, yarn, and pnpm, and can cache packages from the public npm registry, allowing for faster installs and protection against npm registry outages. It includes a web interface for browsing packages, authentication and authorization features, and can be easily integrated into your development workflow.",
"script": "ct/verdaccio.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/verdaccio.sh",
"categories": [
20
],
"notes": [
"To create the first user, run: npm adduser --registry http://<container-ip>:4873"
],
"type": "ct"
},
{ {
"name": "VictoriaMetrics", "name": "VictoriaMetrics",
"slug": "victoriametrics", "slug": "victoriametrics",
@@ -4942,6 +5294,20 @@
"notes": [], "notes": [],
"type": "ct" "type": "ct"
}, },
{
"name": "Warracker",
"slug": "warracker",
"desc": "Warracker is an open source, self-hostable warranty tracker to monitor expirations, store receipts, files. You own the data, your rules!",
"script": "ct/warracker.sh",
"script_url": "https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/warracker.sh",
"categories": [
12
],
"notes": [
"The first user you register will be the admin user."
],
"type": "ct"
},
{ {
"name": "Wastebin", "name": "Wastebin",
"slug": "wastebin", "slug": "wastebin",
@@ -5171,8 +5537,10 @@
9 9
], ],
"notes": [ "notes": [
"Database credentials: `cat zabbix.creds`", "Database credentials: `cat ~/zabbix.creds`",
"Zabbix agent 2 is used by default" "You can choose between Zabbix agent (classic) and agent2 (modern) during installation",
"For agent2 the PostgreSQL plugin is installed by default; all plugins are optional",
"If agent2 with NVIDIA plugin is installed in an environment without GPU, the installer disables it automatically"
], ],
"type": "ct", "type": "ct",
"default_credentials": { "default_credentials": {

8
menu
View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management # ProxMenux - A menu-driven script for Proxmox VE management
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi
@@ -13,13 +13,13 @@
# This script serves as the main entry point for ProxMenux, # This script serves as the main entry point for ProxMenux,
# a menu-driven tool designed for Proxmox VE management. # a menu-driven tool designed for Proxmox VE management.
# #
# - Displays the ProxMenu logo on startup. # - Displays the ProxMenux logo on startup.
# - Loads necessary configurations and language settings. # - Loads necessary configurations and language settings.
# - Checks for available updates and installs them if confirmed. # - Checks for available updates and installs them if confirmed.
# - Downloads and executes the latest main menu script. # - Downloads and executes the latest main menu script.
# #
# Key Features: # Key Features:
# - Ensures ProxMenu is always up-to-date by fetching the latest version. # - Ensures ProxMenux is always up-to-date by fetching the latest version.
# - Uses whiptail for interactive menus and language selection. # - Uses whiptail for interactive menus and language selection.
# - Loads utility functions and translation support. # - Loads utility functions and translation support.
# - Maintains a cache system to improve performance. # - Maintains a cache system to improve performance.
@@ -67,7 +67,7 @@ check_updates() {
if whiptail --title "$(translate "Update Available")" \ if whiptail --title "$(translate "Update Available")" \
--yesno "$(translate "New version available") ($REMOTE_VERSION)\n\n$(translate "Do you want to update now?")" \ --yesno "$(translate "New version available") ($REMOTE_VERSION)\n\n$(translate "Do you want to update now?")" \
10 60 --defaultno; then 10 60 --defaultno; then
msg_warn "$(translate "Starting ProxMenu update...")" msg_warn "$(translate "Starting ProxMenux update...")"
if wget -qO "$INSTALL_SCRIPT" "$REPO_URL/install_proxmenux.sh"; then if wget -qO "$INSTALL_SCRIPT" "$REPO_URL/install_proxmenux.sh"; then
chmod +x "$INSTALL_SCRIPT" chmod +x "$INSTALL_SCRIPT"

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management # ProxMenux - A menu-driven script for Proxmox VE management
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management # ProxMenux - A menu-driven script for Proxmox VE management
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management # ProxMenux - A menu-driven script for Proxmox VE management
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - Network Management and Repair Tool # ProxMenux - Network Management and Repair Tool
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi

View File

@@ -92,6 +92,9 @@ cleanup_duplicate_repos_pve9() {
local cleaned_count=0 local cleaned_count=0
declare -A seen_repos declare -A seen_repos
if [ ! -s "$sources_file" ]; then
return 0
fi
while IFS= read -r line || [[ -n "$line" ]]; do while IFS= read -r line || [[ -n "$line" ]]; do
if [[ "$line" =~ ^[[:space:]]*# ]] || [[ -z "$line" ]]; then if [[ "$line" =~ ^[[:space:]]*# ]] || [[ -z "$line" ]]; then

View File

@@ -0,0 +1,257 @@
#!/bin/bash
# ==========================================================
# Remove Subscription Banner - Proxmox VE 9.x (Clean Version)
# ==========================================================
set -euo pipefail
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
TOOLS_JSON="/usr/local/share/proxmenux/installed_tools.json"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
ensure_tools_json() {
[ -f "$TOOLS_JSON" ] || echo "{}" > "$TOOLS_JSON"
}
register_tool() {
command -v jq >/dev/null 2>&1 || return 0
local tool="$1" state="$2"
ensure_tools_json
jq --arg t "$tool" --argjson v "$state" '.[$t]=$v' "$TOOLS_JSON" \
> "$TOOLS_JSON.tmp" && mv "$TOOLS_JSON.tmp" "$TOOLS_JSON"
}
JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
MIN_JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js"
GZ_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz"
MOBILE_TPL="/usr/share/pve-yew-mobile-gui/index.html.tpl"
APT_HOOK="/etc/apt/apt.conf.d/no-nag-script"
PATCH_BIN="/usr/local/bin/pve-remove-nag.sh"
MARK_JS="PROXMENUX_NAG_REMOVED_v2"
MARK_MOBILE="<!-- PROXMENUX: MOBILE NAG PATCH v2 -->"
verify_js_integrity() {
local file="$1"
[ -f "$file" ] || return 1
[ -s "$file" ] || return 1
grep -Eq 'Ext|function|var|const|let' "$file" || return 1
if LC_ALL=C grep -qP '\x00' "$file" 2>/dev/null; then
return 1
fi
return 0
}
create_backup() {
local file="$1"
local backup_dir="$BASE_DIR/backups"
local timestamp
timestamp=$(date +%Y%m%d_%H%M%S)
local backup_file="$backup_dir/$(basename "$file").backup.$timestamp"
mkdir -p "$backup_dir"
if [ -f "$file" ]; then
cp -a "$file" "$backup_file"
ls -t "$backup_dir"/"$(basename "$file")".backup.* 2>/dev/null | tail -n +6 | xargs -r rm -f 2>/dev/null || true
echo "$backup_file"
fi
}
# ----------------------------------------------------
create_patch_script() {
cat > "$PATCH_BIN" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
MIN_JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js"
GZ_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz"
MOBILE_TPL="/usr/share/pve-yew-mobile-gui/index.html.tpl"
MARK_JS="PROXMENUX_NAG_REMOVED_v2"
MARK_MOBILE="<!-- PROXMENUX: MOBILE NAG PATCH v2 -->"
BASE_DIR="/usr/local/share/proxmenux"
verify_js_integrity() {
local file="$1"
[ -f "$file" ] && [ -s "$file" ] && grep -Eq 'Ext|function' "$file" && ! LC_ALL=C grep -qP '\x00' "$file" 2>/dev/null
}
patch_web() {
[ -f "$JS_FILE" ] || return 0
grep -q "$MARK_JS" "$JS_FILE" && return 0
local backup_dir="$BASE_DIR/backups"
mkdir -p "$backup_dir"
local backup="$backup_dir/$(basename "$JS_FILE").backup.$(date +%Y%m%d_%H%M%S)"
cp -a "$JS_FILE" "$backup"
trap "cp -a '$backup' '$JS_FILE' 2>/dev/null || true" ERR
sed -i '1s|^|/* '"$MARK_JS"' */\n|' "$JS_FILE"
local patterns_found=0
if grep -q "res\.data\.status\.toLowerCase() !== 'active'" "$JS_FILE"; then
sed -i "s/res\.data\.status\.toLowerCase() !== 'active'/false/g" "$JS_FILE"
patterns_found=$((patterns_found + 1))
fi
if grep -q "subscriptionActive: ''" "$JS_FILE"; then
sed -i "s/subscriptionActive: ''/subscriptionActive: true/g" "$JS_FILE"
patterns_found=$((patterns_found + 1))
fi
if grep -q "title: gettext('No valid subscription')" "$JS_FILE"; then
sed -i "s/title: gettext('No valid subscription')/title: gettext('Community Edition')/g" "$JS_FILE"
patterns_found=$((patterns_found + 1))
fi
if grep -q "icon: Ext\.Msg\.WARNING" "$JS_FILE"; then
sed -i "s/icon: Ext\.Msg\.WARNING/icon: Ext.Msg.INFO/g" "$JS_FILE"
patterns_found=$((patterns_found + 1))
fi
if grep -q "subscription = !(" "$JS_FILE"; then
sed -i "s/subscription = !(/subscription = false \&\& (/g" "$JS_FILE"
patterns_found=$((patterns_found + 1))
fi
# Si nada coincidió (cambio upstream), restaura y sal limpio
if [ "${patterns_found:-0}" -eq 0 ]; then
cp -a "$backup" "$JS_FILE"
return 0
fi
# Verificación final
if ! verify_js_integrity "$JS_FILE"; then
cp -a "$backup" "$JS_FILE"
return 1
fi
# Limpiar artefactos/cachés
rm -f "$MIN_JS_FILE" "$GZ_FILE" 2>/dev/null || true
find /var/cache/pve-manager/ -name "*.js*" -delete 2>/dev/null || true
find /var/lib/pve-manager/ -name "*.js*" -delete 2>/dev/null || true
find /var/cache/nginx/ -type f -delete 2>/dev/null || true
trap - ERR
}
patch_mobile() {
[ -f "$MOBILE_TPL" ] || return 0
grep -q "$MARK_MOBILE" "$MOBILE_TPL" && return 0
local backup_dir="$BASE_DIR/backups"
mkdir -p "$backup_dir"
cp -a "$MOBILE_TPL" "$backup_dir/$(basename "$MOBILE_TPL").backup.$(date +%Y%m%d_%H%M%S)"
cat >> "$MOBILE_TPL" <<EOM
$MARK_MOBILE
<script>
(function() {
'use strict';
function removeSubscriptionElements() {
try {
const dialogs = document.querySelectorAll('dialog.pwt-outer-dialog');
dialogs.forEach(d => {
const text = (d.textContent || '').toLowerCase();
if (text.includes('subscription') || text.includes('no valid')) { d.remove(); }
});
const cards = document.querySelectorAll('.pwt-card.pwt-p-2.pwt-d-flex.pwt-interactive.pwt-justify-content-center');
cards.forEach(c => {
const text = (c.textContent || '').toLowerCase();
const hasButton = c.querySelector('button');
if (!hasButton && (text.includes('subscription') || text.includes('no valid'))) { c.remove(); }
});
const alerts = document.querySelectorAll('[class*="alert"], [class*="warning"], [class*="notice"]');
alerts.forEach(a => {
const text = (a.textContent || '').toLowerCase();
if (text.includes('subscription') || text.includes('no valid')) { a.remove(); }
});
} catch (e) { console.warn('Error removing subscription elements:', e); }
}
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', removeSubscriptionElements); }
else { removeSubscriptionElements(); }
const observer = new MutationObserver(removeSubscriptionElements);
if (document.body) {
observer.observe(document.body, { childList: true, subtree: true });
const interval = setInterval(removeSubscriptionElements, 500);
setTimeout(() => { try { observer.disconnect(); clearInterval(interval); } catch(e){} }, 30000);
}
})();
</script>
EOM
}
reload_services() {
systemctl is-active --quiet pveproxy 2>/dev/null && {
systemctl reload pveproxy 2>/dev/null || systemctl restart pveproxy 2>/dev/null || true
}
systemctl is-active --quiet nginx 2>/dev/null && {
systemctl reload nginx 2>/dev/null || true
}
systemctl is-active --quiet pvedaemon 2>/dev/null && {
systemctl reload pvedaemon 2>/dev/null || true
}
find /var/cache/pve-manager/ -type f -delete 2>/dev/null || true
find /var/lib/pve-manager/ -type f -delete 2>/dev/null || true
}
main() {
patch_web || return 1
patch_mobile
reload_services
}
main
EOF
chmod 755 "$PATCH_BIN"
}
# ----------------------------------------------------
create_apt_hook() {
cat > "$APT_HOOK" <<'EOF'
/* ProxMenux: reapply nag patch after upgrades */
DPkg::Post-Invoke { "/usr/local/bin/pve-remove-nag.sh || true"; };
EOF
chmod 644 "$APT_HOOK"
apt-config dump >/dev/null 2>&1 || { msg_warn "APT hook syntax issue"; rm -f "$APT_HOOK"; }
}
remove_subscription_banner_pve9() {
local pve_version
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+' | head -1 || true)
local pve_major="${pve_version%%.*}"
msg_info "$(translate "Detected Proxmox VE ${pve_version:-9.x} removing subscription banner")"
create_patch_script
create_apt_hook
if ! "$PATCH_BIN"; then
msg_error "$(translate "Error applying patches")"
return 1
fi
register_tool "subscription_banner" true
msg_ok "$(translate "Subscription banner removed successfully.")"
msg_ok "$(translate "Refresh your browser to see changes.")"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
remove_subscription_banner_pve9
fi

View File

@@ -0,0 +1,901 @@
#!/usr/bin/env bash
# ==========================================================
# ProxMenux - Global Share Functions (reusable)
# File: scripts/global/share_common.func
# ==========================================================
if [[ -n "${__PROXMENUX_SHARE_COMMON__}" ]]; then
return 0
fi
__PROXMENUX_SHARE_COMMON__=1
: "${PROXMENUX_DEFAULT_SHARE_GROUP:=sharedfiles}"
: "${PROXMENUX_SHARE_MAP_DB:=/usr/local/share/proxmenux/share-map.db}"
mkdir -p "$(dirname "$PROXMENUX_SHARE_MAP_DB")" 2>/dev/null || true
touch "$PROXMENUX_SHARE_MAP_DB" 2>/dev/null || true
pmx_share_map_get() {
local key="$1"
awk -F'=' -v k="$key" '$1==k {print $2}' "$PROXMENUX_SHARE_MAP_DB" 2>/dev/null | tail -n1
}
pmx_share_map_set() {
local key="$1" val="$2"
sed -i "\|^${key}=|d" "$PROXMENUX_SHARE_MAP_DB" 2>/dev/null || true
echo "${key}=${val}" >> "$PROXMENUX_SHARE_MAP_DB"
}
pmx_choose_or_create_group() {
local default_group="${1:-$PROXMENUX_DEFAULT_SHARE_GROUP}"
local choice group_name groups menu_args gid_min
gid_min="$(awk '/^\s*GID_MIN\s+[0-9]+/ {print $2}' /etc/login.defs 2>/dev/null | tail -n1)"
[[ -z "$gid_min" ]] && gid_min=1000
choice=$(whiptail --title "$(translate "Shared Group")" \
--menu "$(translate "Choose a group policy for this shared directory:")" 18 78 6 \
"1" "$(translate "Use default group:") $default_group $(translate "(recommended)")" \
"2" "$(translate "Create a new group for isolation")" \
"3" "$(translate "Select an existing group")" \
3>&1 1>&2 2>&3) || { echo ""; return 1; }
case "$choice" in
1)
pmx_ensure_host_group "$default_group" >/dev/null || { echo ""; return 1; }
echo "$default_group"
;;
2)
group_name=$(whiptail --inputbox "$(translate "Enter new group name:")" 10 70 "sharedfiles-project" \
--title "$(translate "New Group")" 3>&1 1>&2 2>&3) || { echo ""; return 1; }
if [[ -z "$group_name" ]]; then
msg_error "$(translate "Group name cannot be empty.")"
echo ""; return 1
fi
if ! [[ "$group_name" =~ ^[a-zA-Z_][a-zA-Z0-9_-]*$ ]]; then
msg_error "$(translate "Invalid group name. Use letters, digits, underscore or hyphen, and start with a letter or underscore.")"
echo ""; return 1
fi
pmx_ensure_host_group "$group_name" >/dev/null || { echo ""; return 1; }
echo "$group_name"
;;
3)
groups=$(getent group | awk -F: -v MIN="$gid_min" '
$3 >= MIN && $1 != "nogroup" && $1 !~ /^pve/ {print $0}
' | sort -t: -k1,1)
if [[ -z "$groups" ]]; then
whiptail --title "$(translate "Groups")" --msgbox "$(translate "No user groups found.")" 8 60
echo ""; return 1
fi
menu_args=()
while IFS=: read -r gname _ gid members; do
menu_args+=("$gname" "GID=$gid")
done <<< "$groups"
group_name=$(whiptail --title "$(translate "Existing Groups")" \
--menu "$(translate "Select an existing group:")" 20 70 12 \
"${menu_args[@]}" 3>&1 1>&2 2>&3) || { echo ""; return 1; }
pmx_ensure_host_group "$group_name" >/dev/null || { echo ""; return 1; }
echo "$group_name"
;;
*)
echo ""; return 1
;;
esac
}
pmx_ensure_host_group() {
local group_name="$1"
local suggested_gid="${2:-}"
local base_gid=101000
local new_gid gid
if getent group "$group_name" >/dev/null 2>&1; then
gid="$(getent group "$group_name" | cut -d: -f3)"
echo "$gid"
return 0
fi
if [[ -n "$suggested_gid" ]]; then
if getent group "$suggested_gid" >/dev/null 2>&1; then
msg_error "$(translate "GID already in use:") $suggested_gid"
echo ""
return 1
fi
if ! groupadd -g "$suggested_gid" "$group_name" >/dev/null 2>&1; then
msg_error "$(translate "Failed to create group:") $group_name"
echo ""
return 1
fi
msg_ok "$(translate "Group created:") $group_name"
else
new_gid="$base_gid"
while getent group "$new_gid" >/dev/null 2>&1; do
new_gid=$((new_gid+1))
done
if ! groupadd -g "$new_gid" "$group_name" >/dev/null 2>&1; then
msg_error "$(translate "Failed to create group:") $group_name"
echo ""
return 1
fi
msg_ok "$(translate "Group created:") $group_name"
fi
gid="$(getent group "$group_name" | cut -d: -f3)"
if [[ -z "$gid" ]]; then
msg_error "$(translate "Failed to resolve group GID for") $group_name"
echo ""
return 1
fi
echo "$gid"
return 0
}
pmx_prepare_host_shared_dir() {
local dir="$1" group_name="$2"
[[ -z "$dir" || -z "$group_name" ]] && { msg_error "$(translate "Internal error: missing arguments in pmx_prepare_host_shared_dir")"; return 1; }
if [[ ! -d "$dir" ]]; then
if mkdir -p "$dir" 2>/dev/null; then
msg_ok "$(translate "Created directory on host:") $dir"
else
msg_error "$(translate "Failed to create directory on host:") $dir"
return 1
fi
fi
chown -R root:"$group_name" "$dir" 2>/dev/null || true
chmod -R 2775 "$dir" 2>/dev/null || true
if command -v setfacl >/dev/null 2>&1; then
setfacl -R -m d:g:"$group_name":rwx -m d:o::rx -m g:"$group_name":rwx "$dir" 2>/dev/null || true
msg_ok "$(translate "Default ACLs applied for group inheritance.")"
fi
return 0
}
pmx_select_host_mount_point() {
local title="${1:-$(translate "Select Mount Point")}"
local default_path="${2:-/mnt/shared}"
local context="${3:-local}"
local choice folder_name result existing_dirs mount_point
while true; do
choice=$(whiptail --title "$title" --menu "$(translate "Where do you want the host folder?")" 16 76 3 \
"1" "$(translate "Create new folder in /mnt")" \
"2" "$(translate "Enter custom pathr")" 3>&1 1>&2 2>&3) || { echo ""; return 1; }
case "$choice" in
1)
folder_name=$(whiptail --inputbox "$(translate "Enter folder name for /mnt:")" 10 70 "$(basename "$default_path")" --title "$(translate "Folder Name")" 3>&1 1>&2 2>&3) || { echo ""; return 1; }
[[ -z "$folder_name" ]] && continue
mount_point="/mnt/$folder_name"
echo "$mount_point"; return 0
;;
2)
result=$(whiptail --inputbox "$(translate "Enter full path:")" 10 80 "$default_path" --title "$(translate "Custom Path")" 3>&1 1>&2 2>&3) || { echo ""; return 1; }
[[ -z "$result" ]] && continue
echo "$result"; return 0
;;
esac
done
}
select_host_directory_() {
local method choice result
method=$(whiptail --title "$(translate "Select Host Directory")" --menu "$(translate "How do you want to select the HOST folder to mount?")" 15 70 4 \
"mnt" "$(translate "Select from /mnt directories")" \
"manual" "$(translate "Enter path manually")" 3>&1 1>&2 2>&3) || return 1
case "$method" in
mnt|srv|media)
local base_path="/$method"
local host_dirs=("$base_path"/*)
local options=()
for dir in "${host_dirs[@]}"; do
if [[ -d "$dir" ]]; then
options+=("$dir" "$(basename "$dir")")
fi
done
if [[ ${#options[@]} -eq 0 ]]; then
msg_error "$(translate "No directories found in") $base_path"
return 1
fi
result=$(whiptail --title "$(translate "Select Host Folder")" \
--menu "$(translate "Select the folder to mount:")" 20 80 10 "${options[@]}" 3>&1 1>&2 2>&3)
;;
manual)
result=$(whiptail --title "$(translate "Enter Path")" \
--inputbox "$(translate "Enter the full path to the host folder:")" 10 70 "/mnt/" 3>&1 1>&2 2>&3)
;;
esac
if [[ -z "$result" ]]; then
return 1
fi
if [[ ! -d "$result" ]]; then
msg_error "$(translate "The selected path is not a valid directory:") $result"
return 1
fi
echo "$result"
}
select_host_directory__() {
local method result
method=$(whiptail --title "$(translate "Select Host Directory")" \
--menu "$(translate "How do you want to select the HOST folder to mount?")" 15 70 4 \
"mnt" "$(translate "Select from /mnt directories")" \
"manual" "$(translate "Enter path manually")" \
3>&1 1>&2 2>&3) || return 1
case "$method" in
mnt|srv|media)
local base_path="/$method"
local host_dirs=("$base_path"/*)
local options=()
for dir in "${host_dirs[@]}"; do
[[ -d "$dir" ]] && options+=("$dir" "$(basename "$dir")")
done
if [[ ${#options[@]} -eq 0 ]]; then
msg_error "$(translate "No directories found in") $base_path"
return 1
fi
result=$(whiptail --title "$(translate "Select Host Folder")" \
--menu "$(translate "Select the folder to mount:")" 20 80 10 \
"${options[@]}" 3>&1 1>&2 2>&3) || return 1
;;
manual)
result=$(whiptail --title "$(translate "Enter Path")" \
--inputbox "$(translate "Enter the full path to the host folder:")" \
10 70 "/mnt/" 3>&1 1>&2 2>&3) || return 1
;;
*)
return 1
;;
esac
[[ -z "$result" ]] && return 1
[[ ! -d "$result" ]] && {
msg_error "$(translate "The selected path is not a valid directory:") $result"
return 1
}
echo "$result"
}
select_host_directory() {
local method result
method=$(whiptail --title "$(translate "Select Host Directory")" \
--menu "$(translate "How do you want to select the HOST folder to mount?")" 15 70 4 \
"mnt" "$(translate "Select from /mnt directories")" \
"manual" "$(translate "Enter path manually")" \
3>&1 1>&2 2>&3) || return 1
case "$method" in
mnt|srv|media)
local base_path="/$method"
local host_dirs=("$base_path"/*)
local options=()
for dir in "${host_dirs[@]}"; do
[[ -d "$dir" ]] && options+=("$dir" "$(basename "$dir")")
done
if [[ ${#options[@]} -eq 0 ]]; then
msg_error "$(translate "No directories found in") $base_path"
return 1
fi
result=$(whiptail --title "$(translate "Select Host Folder")" \
--menu "$(translate "Select the folder to mount:")" 20 80 10 \
"${options[@]}" 3>&1 1>&2 2>&3) || return 1
;;
manual)
result=$(whiptail --title "$(translate "Enter Path")" \
--inputbox "$(translate "Enter the full path to the host folder:")" \
10 70 "/mnt/" 3>&1 1>&2 2>&3) || return 1
;;
*)
return 1
;;
esac
[[ -z "$result" ]] && return 1
[[ ! -d "$result" ]] && {
msg_error "$(translate "The selected path is not a valid directory:") $result"
return 1
}
echo "$result"
}
select_lxc_container() {
local ct_list ctid ct_status
ct_list=$(pct list | awk 'NR>1 {print $1, $2, $3}')
if [[ -z "$ct_list" ]]; then
dialog --title "$(translate "Error")" \
--msgbox "$(translate "No LXC containers available")" 8 50
return 1
fi
local options=()
while read -r id name status; do
if [[ -n "$id" ]]; then
options+=("$id" "$name ($status)")
fi
done <<< "$ct_list"
ctid=$(dialog --title "$(translate "Select LXC Container")" \
--menu "\n$(translate "Select container:")" 25 80 15 \
"${options[@]}" 3>&1 1>&2 2>&3)
if [[ -z "$ctid" ]]; then
return 1
fi
echo "$ctid"
return 0
}
select_container_mount_point_() {
local ctid="$1"
local host_dir="$2"
local choice mount_point existing_dirs options
while true; do
choice=$(whiptail --title "$(translate "Configure Mount Point inside LXC")" \
--menu "$(translate "Where to mount inside container?")" 18 70 5 \
"1" "$(translate "Create new directory in /mnt")" \
"2" "$(translate "Enter path manually")" \
"3" "$(translate "Cancel")" 3>&1 1>&2 2>&3) || return 1
case "$choice" in
1)
mount_point=$(whiptail --inputbox "$(translate "Enter folder name for /mnt:")" 10 60 "shared" 3>&1 1>&2 2>&3) || continue
[[ -z "$mount_point" ]] && continue
mount_point="/mnt/$mount_point"
pct exec "$ctid" -- mkdir -p "$mount_point" 2>/dev/null
;;
2)
mount_point=$(whiptail --inputbox "$(translate "Enter full path:")" 10 70 "/mnt/shared" 3>&1 1>&2 2>&3) || continue
[[ -z "$mount_point" ]] && continue
mount_point="/mnt/$mount_point"
pct exec "$ctid" -- mkdir -p "$mount_point" 2>/dev/null
;;
3)
return 1
;;
esac
if pct exec "$ctid" -- test -d "$mount_point" 2>/dev/null; then
echo "$mount_point"
return 0
else
whiptail --msgbox "$(translate "Could not create or access directory:") $mount_point" 8 70
continue
fi
done
}
select_container_mount_point() {
local ctid="$1"
local host_dir="$2"
local choice mount_point base_name
base_name=$(basename "$host_dir")
while true; do
choice=$(whiptail --title "$(translate "Configure Mount Point inside LXC")" \
--menu "$(translate "Where to mount inside container?")" 18 70 5 \
"1" "$(translate "Create new directory in /mnt")" \
"2" "$(translate "Enter path manually")" \
"3" "$(translate "Cancel")" 3>&1 1>&2 2>&3) || return 1
case "$choice" in
1)
mount_point=$(whiptail --inputbox "$(translate "Enter folder name for /mnt:")" \
10 60 "$base_name" 3>&1 1>&2 2>&3) || continue
[[ -z "$mount_point" ]] && continue
mount_point="/mnt/$mount_point"
pct exec "$ctid" -- mkdir -p "$mount_point" 2>/dev/null
;;
2)
mount_point=$(whiptail --inputbox "$(translate "Enter full path:")" \
10 70 "/mnt/$base_name" 3>&1 1>&2 2>&3) || continue
[[ -z "$mount_point" ]] && continue
pct exec "$ctid" -- mkdir -p "$mount_point" 2>/dev/null
;;
3)
return 1
;;
esac
if pct exec "$ctid" -- test -d "$mount_point" 2>/dev/null; then
echo "$mount_point"
return 0
else
whiptail --msgbox "$(translate "Could not create or access directory:") $mount_point" 8 70
continue
fi
done
}
# ==========================================================
# CLIENT MOUNT FUNCTIONS (NFS/SAMBA COMMON)
# ==========================================================
# Check if container is privileged (required for client mounts)
select_privileged_lxc() {
# === Select CT ===
local ct_list ctid ct_status conf unpriv
ct_list=$(pct list | awk 'NR>1 {print $1, $3}')
if [[ -z "$ct_list" ]]; then
dialog --backtitle "ProxMenux" --title "$(translate "Error")" \
--msgbox "$(translate "No CTs available in the system.")" 8 50
return 1
fi
ctid=$(dialog --backtitle "ProxMenux" --title "$(translate "Select CT")" \
--menu "$(translate "Select the CT to manage NFS/Samba client:")" 20 70 12 \
$ct_list 3>&1 1>&2 2>&3)
if [[ -z "$ctid" ]]; then
dialog --backtitle "ProxMenux" --title "$(translate "Error")" \
--msgbox "$(translate "No CT was selected.")" 8 50
return 1
fi
# === Start CT if not running ===
ct_status=$(pct status "$ctid" | awk '{print $2}')
if [[ "$ct_status" != "running" ]]; then
show_proxmenux_logo
echo -e
msg_info "$(translate "Starting CT") $ctid..."
pct start "$ctid"
sleep 2
if [[ "$(pct status "$ctid" | awk '{print $2}')" != "running" ]]; then
msg_error "$(translate "Failed to start the CT.")"
echo -e ""
msg_success "$(translate 'Press Enter to continue...')"
read -r
return 1
fi
msg_ok "$(translate "CT started successfully.")"
fi
# === Check privileged/unprivileged ===
conf="/etc/pve/lxc/${ctid}.conf"
unpriv=$(awk '/^unprivileged:/ {print $2}' "$conf" 2>/dev/null)
if [[ "$unpriv" == "1" ]]; then
dialog --backtitle "ProxMenux" --title "$(translate "Privileged Container Required")" \
--msgbox "\n$(translate "Network share mounting (NFS/Samba) requires a PRIVILEGED container.")\n\n$(translate "Selected container") $ctid $(translate "is UNPRIVILEGED.")\n\n$(translate "For unprivileged containers, use instead:")\n • $(translate "Configure LXC mount points")\n • $(translate "Mount shares on HOST first")\n • $(translate "Then bind-mount to container")" 15 75
exit 1
fi
# Export CTID if all good
echo "$ctid"
CTID="$ctid"
return 0
}
# Common mount point selection for containers
pmx_select_container_mount_point() {
local ctid="$1"
local share_name="${2:-shared}"
while true; do
local choice=$(whiptail --title "$(translate "Select Mount Point")" --menu "$(translate "Where do you want to mount inside container?")" 15 70 3 \
"existing" "$(translate "Select from existing folders in /mnt")" \
"new" "$(translate "Create new folder in /mnt")" \
"custom" "$(translate "Enter custom path")" 3>&1 1>&2 2>&3)
case "$choice" in
existing)
local existing_dirs=$(pct exec "$ctid" -- find /mnt -mindepth 1 -maxdepth 1 -type d 2>/dev/null | sort)
if [[ -z "$existing_dirs" ]]; then
whiptail --title "$(translate "No Folders")" --msgbox "$(translate "No folders found in /mnt. Please create a new folder.")" 8 60
continue
fi
local options=()
while IFS= read -r dir; do
if [[ -n "$dir" ]]; then
local name=$(basename "$dir")
if pct exec "$ctid" -- [ "$(ls -A "$dir" 2>/dev/null | wc -l)" -eq 0 ]; then
local status="$(translate "Empty")"
else
local status="$(translate "Contains files")"
fi
options+=("$dir" "$name ($status)")
fi
done <<< "$existing_dirs"
local mount_point=$(whiptail --title "$(translate "Select Existing Folder")" --menu "$(translate "Choose a folder to mount:")" 20 80 10 "${options[@]}" 3>&1 1>&2 2>&3)
if [[ -n "$mount_point" ]]; then
if pct exec "$ctid" -- [ "$(ls -A "$mount_point" 2>/dev/null | wc -l)" -gt 0 ]; then
local file_count=$(pct exec "$ctid" -- ls -A "$mount_point" 2>/dev/null | wc -l || true)
if ! whiptail --yesno "$(translate "WARNING: The selected directory is not empty!")\n\n$(translate "Directory:"): $mount_point\n$(translate "Contains:"): $file_count $(translate "files/folders")\n\n$(translate "Mounting here will hide existing files until unmounted.")\n\n$(translate "Do you want to continue?")" 14 70 --title "$(translate "Directory Not Empty")"; then
continue
fi
fi
echo "$mount_point"
return 0
fi
;;
new)
local folder_name=$(whiptail --inputbox "$(translate "Enter new folder name:")" 10 60 "$share_name" --title "$(translate "New Folder in /mnt")" 3>&1 1>&2 2>&3)
if [[ -n "$folder_name" ]]; then
local mount_point="/mnt/$folder_name"
echo "$mount_point"
return 0
fi
;;
custom)
local mount_point=$(whiptail --inputbox "$(translate "Enter full path for mount point:")" 10 70 "/mnt/${share_name}" --title "$(translate "Custom Path")" 3>&1 1>&2 2>&3)
if [[ -n "$mount_point" ]]; then
echo "$mount_point"
return 0
fi
;;
*)
return 1
;;
esac
done
}
# Common server discovery function
pmx_discover_network_servers() {
local service_type="$1" # "NFS" or "Samba"
local port="$2" # "2049" for NFS, "139,445" for Samba
local host_ip=$(hostname -I | awk '{print $1}')
local network=$(echo "$host_ip" | cut -d. -f1-3).0/24
# Install nmap if needed
if ! which nmap >/dev/null 2>&1; then
apt-get install -y nmap &>/dev/null
fi
local servers
if [[ "$service_type" == "Samba" ]]; then
servers=$(nmap -p 139,445 --open "$network" 2>/dev/null | grep -B 4 -E "(139|445)/tcp open" | grep "Nmap scan report" | awk '{print $5}' | sort -u || true)
else
servers=$(nmap -p 2049 --open "$network" 2>/dev/null | grep -B 4 "2049/tcp open" | grep "Nmap scan report" | awk '{print $5}' | sort -u || true)
fi
if [[ -z "$servers" ]]; then
whiptail --title "$(translate "No Servers Found")" --msgbox "$(translate "No") $service_type $(translate "servers found on the network.")\n\n$(translate "You can add servers manually.")" 10 60
return 1
fi
local options=()
while IFS= read -r server; do
if [[ -n "$server" ]]; then
if [[ "$service_type" == "Samba" ]]; then
# Try to get NetBIOS name for Samba
local nb_name=$(nmblookup -A "$server" 2>/dev/null | awk '/<00> -.*B <ACTIVE>/ {print $1; exit}')
if [[ -z "$nb_name" || "$nb_name" == "$server" || "$nb_name" == "address" || "$nb_name" == "-" ]]; then
nb_name="Unknown"
fi
options+=("$server" "$nb_name ($server)")
else
# For NFS, show export count
local exports_count=$(showmount -e "$server" 2>/dev/null | tail -n +2 | wc -l || echo "0")
options+=("$server" "NFS Server ($exports_count exports)")
fi
fi
done <<< "$servers"
if [[ ${#options[@]} -eq 0 ]]; then
whiptail --title "$(translate "No Valid Servers")" --msgbox "$(translate "No accessible") $service_type $(translate "servers found.")" 8 50
return 1
fi
local selected_server=$(whiptail --title "$(translate "Select") $service_type $(translate "Server")" --menu "$(translate "Choose a server:")" 20 80 10 "${options[@]}" 3>&1 1>&2 2>&3)
if [[ -n "$selected_server" ]]; then
echo "$selected_server"
return 0
else
return 1
fi
}
# Common server selection function
pmx_select_server() {
local service_type="$1" # "NFS" or "Samba"
local port="$2" # "2049" for NFS, "139,445" for Samba
local method=$(whiptail --title "$(translate "$service_type Server Selection")" --menu "$(translate "How do you want to select the") $service_type $(translate "server?")" 15 70 3 \
"auto" "$(translate "Auto-discover servers on network")" \
"manual" "$(translate "Enter server IP/hostname manually")" \
"recent" "$(translate "Select from recent servers")" 3>&1 1>&2 2>&3)
local result_code=$?
if [[ $result_code -ne 0 ]]; then
return 1
fi
case "$method" in
auto)
local discovered_server
discovered_server=$(pmx_discover_network_servers "$service_type" "$port")
local discover_result=$?
if [[ $discover_result -eq 0 && -n "$discovered_server" ]]; then
echo "$discovered_server"
return 0
else
return 1
fi
;;
manual)
local server=$(whiptail --inputbox "$(translate "Enter") $service_type $(translate "server IP or hostname:")" 10 60 --title "$(translate "$service_type Server")" 3>&1 1>&2 2>&3)
local input_result=$?
if [[ $input_result -eq 0 && -n "$server" ]]; then
echo "$server"
return 0
else
return 1
fi
;;
recent)
local fs_type
if [[ "$service_type" == "NFS" ]]; then
fs_type="nfs"
else
fs_type="cifs"
fi
# Fix the recent servers detection for NFS
local recent
if [[ "$service_type" == "NFS" ]]; then
recent=$(grep "$fs_type" /etc/fstab 2>/dev/null | awk '{print $1}' | cut -d: -f1 | sort -u || true)
else
recent=$(grep "$fs_type" /etc/fstab 2>/dev/null | awk '{print $1}' | cut -d/ -f3 | sort -u || true)
fi
if [[ -z "$recent" ]]; then
whiptail --title "$(translate "No Recent Servers")" --msgbox "\n$(translate "No recent") $service_type $(translate "servers found.")" 8 50
return 1
fi
local options=()
while IFS= read -r server; do
[[ -n "$server" ]] && options+=("$server" "$(translate "Recent") $service_type $(translate "server")")
done <<< "$recent"
local selected_server=$(whiptail --title "$(translate "Recent") $service_type $(translate "Servers")" --menu "$(translate "Choose a recent server:")" 20 70 10 "${options[@]}" 3>&1 1>&2 2>&3)
local select_result=$?
if [[ $select_result -eq 0 && -n "$selected_server" ]]; then
echo "$selected_server"
return 0
else
return 1
fi
;;
*)
return 1
;;
esac
}
# Common mount options configuration
pmx_configure_mount_options() {
local service_type="$1" # "NFS" or "CIFS"
local mount_type
if [[ "$service_type" == "NFS" ]]; then
mount_type=$(whiptail --title "$(translate "Mount Options")" --menu "$(translate "Select mount configuration:")" 15 70 4 \
"default" "$(translate "Default options")" \
"readonly" "$(translate "Read-only mount")" \
"performance" "$(translate "Performance optimized")" \
"custom" "$(translate "Custom options")" 3>&1 1>&2 2>&3)
case "$mount_type" in
default)
echo "rw,hard,intr,rsize=8192,wsize=8192,timeo=14"
;;
readonly)
echo "ro,hard,intr,rsize=8192,timeo=14"
;;
performance)
echo "rw,hard,intr,rsize=1048576,wsize=1048576,timeo=14,retrans=2"
;;
custom)
local options=$(whiptail --inputbox "$(translate "Enter custom mount options:")" 10 70 "rw,hard,intr" --title "$(translate "Custom Options")" 3>&1 1>&2 2>&3)
echo "${options:-rw,hard,intr}"
;;
*)
echo "rw,hard,intr,rsize=8192,wsize=8192,timeo=14"
;;
esac
else
# CIFS options
mount_type=$(whiptail --title "$(translate "Mount Options")" --menu "$(translate "Select mount configuration:")" 15 70 4 \
"default" "$(translate "Default options")" \
"readonly" "$(translate "Read-only mount")" \
"performance" "$(translate "Performance optimized")" \
"custom" "$(translate "Custom options")" 3>&1 1>&2 2>&3)
case "$mount_type" in
default)
echo "rw,file_mode=0664,dir_mode=0775,iocharset=utf8"
;;
readonly)
echo "ro,file_mode=0444,dir_mode=0555,iocharset=utf8"
;;
performance)
echo "rw,file_mode=0664,dir_mode=0775,iocharset=utf8,cache=strict,rsize=1048576,wsize=1048576"
;;
custom)
local options=$(whiptail --inputbox "$(translate "Enter custom mount options:")" 10 70 "rw,file_mode=0664,dir_mode=0775" --title "$(translate "Custom Options")" 3>&1 1>&2 2>&3)
echo "${options:-rw,file_mode=0664,dir_mode=0775}"
;;
*)
echo "rw,file_mode=0664,dir_mode=0775,iocharset=utf8"
;;
esac
fi
}
# Common permanent mount question
pmx_ask_permanent_mount() {
if whiptail --yesno "$(translate "Do you want to make this mount permanent?")\n\n$(translate "This will add the mount to /etc/fstab so it persists after reboot.")" 10 70 --title "$(translate "Permanent Mount")"; then
echo "true"
else
echo "false"
fi
}

View File

@@ -158,8 +158,6 @@ EOF
msg_ok "$(translate "Non-free firmware warnings disabled")" msg_ok "$(translate "Non-free firmware warnings disabled")"
fi fi
cleanup_duplicate_repos
update_output=$(apt-get update 2>&1) update_output=$(apt-get update 2>&1)
update_exit_code=$? update_exit_code=$?
@@ -240,14 +238,16 @@ EOF
return 0 return 0
fi fi
msg_info "$(translate "Removing conflicting utilities...")" msg_info "$(translate "Cleaning up unused time synchronization services...")"
if /usr/bin/env DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::='--force-confdef' purge ntp openntpd systemd-timesyncd > /dev/null 2>&1; then if /usr/bin/env DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::='--force-confdef' purge ntp openntpd systemd-timesyncd > /dev/null 2>&1; then
msg_ok "$(translate "Conflicting utilities removed")" msg_ok "$(translate "Old time services removed successfully")"
else else
msg_warn "$(translate "Some conflicting utilities may not have been removed")" msg_warn "$(translate "Some old time services could not be removed (not installed)")"
fi fi
msg_info "$(translate "Updating packages...")" msg_info "$(translate "Updating packages...")"
apt-get install pv -y > /dev/null 2>&1 apt-get install pv -y > /dev/null 2>&1
msg_ok "$(translate "Packages updated successfully")" msg_ok "$(translate "Packages updated successfully")"
@@ -330,6 +330,7 @@ EOF
echo -e "${TAB}${GN}🖥️ $(translate "Proxmox VE")${CL}: ${BL}$target_version (Debian $OS_CODENAME)${CL}" echo -e "${TAB}${GN}🖥️ $(translate "Proxmox VE")${CL}: ${BL}$target_version (Debian $OS_CODENAME)${CL}"
msg_ok "$(translate "Proxmox VE 9.x configuration completed.")" msg_ok "$(translate "Proxmox VE 9.x configuration completed.")"
} }
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then

View File

@@ -0,0 +1,128 @@
#!/bin/bash
# ProxMenux - Coral TPU Installer (PVE 9.x)
# =========================================
# Author : MacRimi
# License : MIT
# Version : 1.3 (PVE9, silent build)
# Last Updated: 25/09/2025
# =========================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
LOG_FILE="/tmp/coral_install.log"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
pre_install_prompt() {
if ! dialog --title "$(translate 'Coral TPU Installation')" --yesno \
"\n$(translate 'Installing Coral TPU drivers requires rebooting the server after installation. Do you want to proceed?')" 10 70; then
exit 0
fi
}
install_coral_host() {
show_proxmenux_logo
: >"$LOG_FILE"
msg_info "$(translate 'Installing build dependencies...')"
apt-get update -qq >>"$LOG_FILE" 2>&1
apt-get install -y git devscripts dh-dkms dkms proxmox-headers-$(uname -r) >>"$LOG_FILE" 2>&1
if [[ $? -ne 0 ]]; then msg_error "$(translate 'Error installing build dependencies. Check /tmp/coral_install.log')"; exit 1; fi
msg_ok "$(translate 'Build dependencies installed.')"
cd /tmp || exit 1
rm -rf gasket-driver >>"$LOG_FILE" 2>&1
msg_info "$(translate 'Cloning Google Coral driver repository...')"
git clone https://github.com/google/gasket-driver.git >>"$LOG_FILE" 2>&1
if [[ $? -ne 0 ]]; then msg_error "$(translate 'Could not clone the repository. Check /tmp/coral_install.log')"; exit 1; fi
msg_ok "$(translate 'Repository cloned successfully.')"
cd /tmp/gasket-driver || exit 1
msg_info "$(translate 'Patching source for kernel compatibility...')"
sed -i 's/\.llseek = no_llseek/\.llseek = noop_llseek/' src/gasket_core.c
sed -i 's/^MODULE_IMPORT_NS(DMA_BUF);/MODULE_IMPORT_NS("DMA_BUF");/' src/gasket_page_table.c
sed -i "s/\(linux-headers-686-pae | linux-headers-amd64 | linux-headers-generic | linux-headers\)/\1 | proxmox-headers-$(uname -r) | pve-headers-$(uname -r)/" debian/control
if [[ $? -ne 0 ]]; then msg_error "$(translate 'Patching failed. Check /tmp/coral_install.log')"; exit 1; fi
msg_ok "$(translate 'Source patched successfully.')"
msg_info "$(translate 'Building DKMS package...')"
debuild -us -uc -tc -b >>"$LOG_FILE" 2>&1
if [[ $? -ne 0 ]]; then msg_error "$(translate 'Failed to build DKMS package. Check /tmp/coral_install.log')"; exit 1; fi
msg_ok "$(translate 'DKMS package built successfully.')"
msg_info "$(translate 'Installing DKMS package...')"
dpkg -i ../gasket-dkms_*.deb >>"$LOG_FILE" 2>&1 || true
if ! dpkg -s gasket-dkms >/dev/null 2>&1; then
msg_error "$(translate 'Failed to install DKMS package. Check /tmp/coral_install.log')"; exit 1
fi
msg_ok "$(translate 'DKMS package installed.')"
msg_info "$(translate 'Compiling Coral TPU drivers for current kernel...')"
dkms remove -m gasket -v 1.0 -k "$(uname -r)" >>"$LOG_FILE" 2>&1 || true
dkms add -m gasket -v 1.0 >>"$LOG_FILE" 2>&1 || true
dkms build -m gasket -v 1.0 -k "$(uname -r)" >>"$LOG_FILE" 2>&1
if [[ $? -ne 0 ]]; then
sed -n '1,200p' /var/lib/dkms/gasket/1.0/build/make.log >>"$LOG_FILE" 2>&1 || true
msg_error "$(translate 'DKMS build failed. Check /tmp/coral_install.log')"; exit 1
fi
dkms install -m gasket -v 1.0 -k "$(uname -r)" >>"$LOG_FILE" 2>&1
if [[ $? -ne 0 ]]; then msg_error "$(translate 'DKMS install failed. Check /tmp/coral_install.log')"; exit 1; fi
msg_ok "$(translate 'Drivers compiled and installed via DKMS.')"
msg_info "$(translate 'Loading modules...')"
modprobe gasket >>"$LOG_FILE" 2>&1 || true
modprobe apex >>"$LOG_FILE" 2>&1 || true
if lsmod | grep -q '\bapex\b'; then
msg_ok "$(translate 'Modules loaded.')"
msg_success "$(translate 'Coral TPU drivers installed and loaded successfully.')"
else
msg_warn "$(translate 'Installation finished but drivers are not loaded. Please check dmesg and /tmp/coral_install.log')"
fi
echo "---- dmesg | grep -i apex (last lines) ----" >>"$LOG_FILE"
dmesg | grep -i apex | tail -n 20 >>"$LOG_FILE" 2>&1
}
restart_prompt() {
if whiptail --title "$(translate 'Coral TPU Installation')" --yesno \
"$(translate 'The installation requires a server restart to apply changes. Do you want to restart now?')" 10 70; then
msg_warn "$(translate 'Restarting the server...')"
reboot
else
msg_success "$(translate 'Completed. Press Enter to return to menu...')"
read -r
fi
}
pre_install_prompt
install_coral_host
restart_prompt

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management # ProxMenux - A menu-driven script for Proxmox VE management
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi
@@ -197,13 +197,12 @@ show_vm_ct_commands() {
echo -e "\n${YELLOW}$(translate 'Listing relevant CT users and their mapped UID/GID on host...')${NC}\n" echo -e "\n${YELLOW}$(translate 'Listing relevant CT users and their mapped UID/GID on host...')${NC}\n"
# Obtener el shift de UID del CT (por defecto 100000 si no está configurado)
UID_SHIFT=$(grep "^lxc.idmap" /etc/pve/lxc/"$id".conf | grep 'u 0' | awk '{print $5}') UID_SHIFT=$(grep "^lxc.idmap" /etc/pve/lxc/"$id".conf | grep 'u 0' | awk '{print $5}')
UID_SHIFT=${UID_SHIFT:-100000} UID_SHIFT=${UID_SHIFT:-100000}
# Obtener todos los usuarios y filtrar solo root o UID >= 1000
pct exec "$id" -- getent passwd | while IFS=: read -r username _ uid gid _ home _; do pct exec "$id" -- getent passwd | while IFS=: read -r username _ uid gid _ home _; do
if [ "$uid" -eq 0 ] || [ "$uid" -ge 1000 ]; then if [ "$uid" -eq 0 ] || [ "$uid" -eq 65534 ] || [ "$uid" -ge 30 ]; then
real_uid=$((UID_SHIFT + uid)) real_uid=$((UID_SHIFT + uid))
real_gid=$((UID_SHIFT + gid)) real_gid=$((UID_SHIFT + gid))
echo -e "${GREEN}$(translate 'User')${NC}: $username" echo -e "${GREEN}$(translate 'User')${NC}: $username"

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management # ProxMenux - A menu-driven script for Proxmox VE management
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management # ProxMenux - A menu-driven script for Proxmox VE management
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Revision : @Blaspt (USB passthrough via udev rule with persistent /dev/coral) # Revision : @Blaspt (USB passthrough via udev rule with persistent /dev/coral)

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ProxMenu - A menu-driven script for Proxmox VE management # ProxMenux - A menu-driven script for Proxmox VE management
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi

157
scripts/lxc/jd2_2.sh Normal file
View File

@@ -0,0 +1,157 @@
#!/bin/bash
# Script para instalar JDownloader en un contenedor LXC desde el host Proxmox
# Autor: MacRimi
# Mostrar lista de CTs
CT_LIST=$(pct list | awk 'NR>1 {print $1, $3}')
if [ -z "$CT_LIST" ]; then
whiptail --title "Error" --msgbox "No hay contenedores LXC disponibles en el sistema." 8 50
exit 1
fi
# Seleccionar CT
CTID=$(whiptail --title "Instalación de JDownloader" --menu "Selecciona el contenedor donde instalar JDownloader:" 20 60 10 $CT_LIST 3>&1 1>&2 2>&3)
if [ -z "$CTID" ]; then
whiptail --title "Cancelado" --msgbox "No se ha seleccionado ningún contenedor." 8 40
exit 1
fi
# Solicitar email
EMAIL=$(whiptail --title "Cuenta My JDownloader" --inputbox "Introduce tu correo electrónico para vincular JDownloader:" 10 60 3>&1 1>&2 2>&3)
if [ -z "$EMAIL" ]; then
whiptail --title "Error" --msgbox "No se ha introducido ningún correo." 8 40
exit 1
fi
# Solicitar contraseña con confirmación
while true; do
PASSWORD=$(whiptail --title "Cuenta My JDownloader" --passwordbox "Introduce tu contraseña de My JDownloader:" 10 60 3>&1 1>&2 2>&3)
[ -z "$PASSWORD" ] && whiptail --title "Error" --msgbox "No se ha introducido ninguna contraseña." 8 40 && exit 1
CONFIRM=$(whiptail --title "Confirmación de contraseña" --passwordbox "Repite tu contraseña para confirmar:" 10 60 3>&1 1>&2 2>&3)
[ "$PASSWORD" = "$CONFIRM" ] && break
whiptail --title "Error" --msgbox "Las contraseñas no coinciden. Intenta de nuevo." 8 50
done
# Confirmación final
whiptail --title "Confirmar datos" --yesno "¿Deseas continuar con los siguientes datos?\n\nCorreo: $EMAIL\nContraseña: (oculta)\n\nEsta información se usará para vincular el contenedor con tu cuenta de My.JDownloader." 14 60
[ $? -ne 0 ] && whiptail --title "Cancelado" --msgbox "Instalación cancelada por el usuario." 8 40 && exit 1
clear
echo "🔍 Detectando sistema operativo dentro del CT $CTID..."
OS_ID=$(pct exec "$CTID" -- awk -F= '/^ID=/{gsub("\"",""); print $2}' /etc/os-release)
echo "Sistema detectado: $OS_ID"
echo "🧰 Preparando entorno..."
case "$OS_ID" in
debian)
# Repositorio adicional para Java 8
pct exec "$CTID" -- wget -q http://www.mirbsd.org/~tg/Debs/sources.txt/wtf-bookworm.sources
pct exec "$CTID" -- mv wtf-bookworm.sources /etc/apt/sources.list.d/
pct exec "$CTID" -- apt update -y
pct exec "$CTID" -- apt install -y openjdk-8-jdk wget
JAVA_PATH="/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java"
;;
ubuntu)
pct exec "$CTID" -- apt update -y
pct exec "$CTID" -- apt install -y openjdk-8-jdk wget
JAVA_PATH="/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java"
;;
alpine)
pct exec "$CTID" -- apk update
pct exec "$CTID" -- apk add openjdk8 wget
JAVA_PATH="/usr/lib/jvm/java-1.8-openjdk/bin/java"
;;
*)
echo "❌ Sistema operativo no soportado: $OS_ID"
exit 1
;;
esac
# Crear carpeta de instalación
pct exec "$CTID" -- mkdir -p /opt/jdownloader
pct exec "$CTID" -- bash -lc '
set -e
mkdir -p /opt/jdownloader
cd /opt/jdownloader
if [ ! -f JDownloader.jar ]; then
if ls JDownloader.jar.backup.* >/dev/null 2>&1; then
cp -a "$(ls -t JDownloader.jar.backup.* | head -1)" JDownloader.jar
else
curl -fSLo JDownloader.jar https://installer.jdownloader.org/JDownloader.jar
fi
fi
chown root:root JDownloader.jar
chmod 0644 JDownloader.jar
'
# Crear archivo de configuración JSON para My JDownloader
pct exec "$CTID" -- bash -c "mkdir -p /opt/jdownloader/cfg && cat > /opt/jdownloader/cfg/org.jdownloader.api.myjdownloader.MyJDownloaderSettings.json" <<EOF
{
"email" : "$EMAIL",
"password" : "$PASSWORD",
"enabled" : true
}
EOF
# Crear servicio según sistema
if [[ "$OS_ID" == "alpine" ]]; then
# Servicio OpenRC para Alpine
pct exec "$CTID" -- bash -c 'cat > /etc/init.d/jdownloader <<EOF
#!/sbin/openrc-run
command="/usr/bin/java"
command_args="-jar /opt/jdownloader/JDownloader.jar -norestart"
pidfile="/var/run/jdownloader.pid"
name="JDownloader"
depend() {
need net
}
EOF'
pct exec "$CTID" -- chmod +x /etc/init.d/jdownloader
pct exec "$CTID" -- rc-update add jdownloader default
pct exec "$CTID" -- rc-service jdownloader start
else
# Servicio systemd para Debian/Ubuntu
pct exec "$CTID" -- bash -lc 'cat > /etc/systemd/system/jdownloader.service <<'"'"'EOF'"'"'
[Unit]
Description=JDownloader
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/jdownloader
ExecStartPre=/usr/bin/test -s /opt/jdownloader/JDownloader.jar
ExecStart=/usr/bin/java -jar /opt/jdownloader/JDownloader.jar -norestart
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable jdownloader
systemctl restart jdownloader
systemctl status jdownloader --no-pager || true
'
pct exec "$CTID" -- systemctl daemon-reexec
pct exec "$CTID" -- systemctl daemon-reload
pct exec "$CTID" -- systemctl enable jdownloader
pct exec "$CTID" -- systemctl start jdownloader
fi
pct exec "$CTID" -- reboot
echo -e "\n\033[1;32m✅ JDownloader se ha instalado correctamente en el CT $CTID y está funcionando como servicio.\033[0m"
echo -e "\n➡ Accede a \033[1;34mhttps://my.jdownloader.org\033[0m con tu cuenta para gestionarlo.\n"

View File

@@ -0,0 +1,284 @@
#!/bin/bash
# ==========================================================
# ProxMenux - Manual LXC Conversion Guide
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# Version : 1.0
# Last Updated: 19/08/2025
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
# ==========================================================
show_command() {
local step="$1"
local description="$2"
local command="$3"
local note="$4"
local command_extra="$5"
echo -e "${BGN}${step}.${CL} ${BL}${description}${CL}"
echo ""
echo -e "${TAB}${command}"
echo -e
[[ -n "$note" ]] && echo -e "${TAB}${DARK_GRAY}${note}${CL}"
[[ -n "$command_extra" ]] && echo -e "${TAB}${YW}${command_extra}${CL}"
echo ""
}
show_privileged_to_unprivileged_guide() {
clear
show_proxmenux_logo
msg_title "$(translate "Manual Guide: Convert LXC Privileged to Unprivileged")"
echo -e "${TAB}${BL}------------------------------------------------------------------------${CL}"
echo -e
echo -e "${TAB}${BGN}$(translate "Source:")${CL} ${BL}https://forum.proxmox.com/threads/converting-between-privileged-and-unprivileged-containers.97243/${CL}"
echo -e
echo -e
echo -e "${TAB}${BOLD}$(translate "IMPORTANT PREREQUISITES:")${CL}"
echo -e
echo -e "${TAB}${BGN}$(translate "Container must be stopped before conversion")${CL}"
echo -e "${TAB}${BGN}$(translate "Create a backup of your container before proceeding")${CL}"
echo -e "${TAB}${BGN}$(translate "This process changes file ownership inside the container")${CL}"
echo -e "${TAB}${BGN}$(translate "Process may take several minutes depending on container size")${CL}"
echo -e "${TAB}${BGN}$(translate "Works with LVM, ZFS, and BTRFS storage types")${CL}"
echo -e
echo -e "${TAB}${BL}------------------------------------------------------------------------${CL}"
echo -e
show_command "1" \
"$(translate "List all containers to identify the privileged one:")" \
"pct list" \
"$(translate "Look for containers without 'unprivileged: 1' in their config")"
show_command "2" \
"$(translate "Stop the container if it's running:")" \
"pct stop <container-id>" \
"$(translate "Replace <container-id> with your actual container ID")" \
"$(translate "Example: pct stop 114")"
show_command "3" \
"$(translate "Create a backup of the container configuration:")" \
"cp /etc/pve/lxc/<container-id>.conf /etc/pve/lxc/<container-id>.conf.bak" \
"$(translate "This creates a backup in case you need to revert changes")" \
"$(translate "Example: cp /etc/pve/lxc/114.conf /etc/pve/lxc/114.conf.bak")"
show_command "4" \
"$(translate "Get the container's storage information:")" \
"grep '^rootfs:' /etc/pve/lxc/<container-id>.conf" \
"$(translate "This shows the storage type and disk identifier")" \
"$(translate "Example output: rootfs: local-lvm:vm-114-disk-0,size=8G")"
show_command "5" \
"$(translate "Get the actual disk path:")" \
"pvesm path <storage-identifier>" \
"$(translate "Replace <storage-identifier> with the value from step 4")" \
"$(translate "Example: pvesm path local-lvm:vm-114-disk-0")"
echo -e "${TAB}${BOLD}$(translate "STEP 6: Choose commands based on your storage type")${CL}"
echo -e
echo -e "${TAB}${BGN}$(translate "If pvesm path returned a DIRECTORY (ZFS/BTRFS):")${CL}"
echo -e "${TAB}${YW}$(translate "Example: /rpool/data/subvol-114-disk-0")${CL}"
echo -e
show_command "6a" \
"$(translate "For ZFS/BTRFS - Set the mount path:")" \
"MOUNT_PATH=\"/rpool/data/subvol-<container-id>-disk-0\"" \
"$(translate "Replace with your actual path from step 5")" \
"$(translate "Example: MOUNT_PATH=\"/rpool/data/subvol-114-disk-0\"")"
echo -e "${TAB}${BGN}$(translate "If pvesm path returned a DEVICE (LVM):")${CL}"
echo -e "${TAB}${YW}$(translate "Example: /dev/pve/vm-114-disk-0")${CL}"
echo -e
show_command "6b" \
"$(translate "For LVM - Create mount directory and mount:")" \
"mkdir -p /tmp/lxc_convert_<container-id>\nmount -o loop /dev/path/to/disk /tmp/lxc_convert_<container-id>\nMOUNT_PATH=\"/tmp/lxc_convert_<container-id>\"" \
"$(translate "Replace paths with your actual values from step 5")" \
"$(translate "Example: mkdir -p /tmp/lxc_convert_114")"
show_command "7" \
"$(translate "Convert file ownership (this takes time):")" \
"find \"\$MOUNT_PATH\" -type f | while read file; do\n if [ -e \"\$file\" ]; then\n CURRENT_UID=\$(stat -c '%u' \"\$file\")\n CURRENT_GID=\$(stat -c '%g' \"\$file\")\n NEW_UID=\$((100000 + CURRENT_UID))\n NEW_GID=\$((100000 + CURRENT_GID))\n chown \"\$NEW_UID:\$NEW_GID\" \"\$file\"\n fi\ndone" \
"$(translate "This converts all file UIDs/GIDs by adding 100000")" \
"$(translate "Process may take several minutes for large containers")"
show_command "8" \
"$(translate "Convert directory ownership:")" \
"find \"\$MOUNT_PATH\" -type d | while read dir; do\n if [ -e \"\$dir\" ]; then\n CURRENT_UID=\$(stat -c '%u' \"\$dir\")\n CURRENT_GID=\$(stat -c '%g' \"\$dir\")\n NEW_UID=\$((100000 + CURRENT_UID))\n NEW_GID=\$((100000 + CURRENT_GID))\n chown \"\$NEW_UID:\$NEW_GID\" \"\$dir\"\n fi\ndone" \
"$(translate "This converts all directory UIDs/GIDs by adding 100000")"
echo -e "${TAB}${BOLD}$(translate "STEP 9: Cleanup (LVM only)")${CL}"
echo -e "${TAB}${YW}$(translate "Only run this if you used LVM (step 6b):")${CL}"
echo -e
show_command "9" \
"$(translate "Unmount and cleanup (LVM only):")" \
"umount /tmp/lxc_convert_<container-id>\nrmdir /tmp/lxc_convert_<container-id>" \
"$(translate "Only needed if you mounted the filesystem in step 6b")" \
"$(translate "Skip this step for ZFS/BTRFS")"
show_command "10" \
"$(translate "Add unprivileged flag to container configuration:")" \
"echo 'unprivileged: 1' >> /etc/pve/lxc/<container-id>.conf" \
"$(translate "This marks the container as unprivileged")"
show_command "11" \
"$(translate "Start the converted container:")" \
"pct start <container-id>" \
"$(translate "The container should now start as unprivileged")"
show_command "12" \
"$(translate "Verify the conversion:")" \
"pct config <container-id> | grep unprivileged" \
"$(translate "Should show 'unprivileged: 1'")"
echo -e "${TAB}${BL}------------------------------------------------------------------------${CL}"
echo -e
echo -e "${TAB}${BOLD}$(translate "STORAGE TYPE IDENTIFICATION:")${CL}"
echo -e
echo -e "${TAB}${BGN}$(translate "LVM:")${CL} ${YW}pvesm path returns /dev/xxx (block device)${CL}"
echo -e "${TAB}${BGN}$(translate "ZFS:")${CL} ${YW}pvesm path returns /rpool/xxx (directory)${CL}"
echo -e "${TAB}${BGN}$(translate "BTRFS:")${CL} ${YW}pvesm path returns directory path${CL}"
echo -e
echo -e "${TAB}${BOLD}$(translate "TROUBLESHOOTING:")${CL}"
echo -e
echo -e "${TAB}${BGN}$(translate "If mount fails (LVM):")${CL} ${YW}Check that the container is stopped and disk path is correct${CL}"
echo -e "${TAB}${BGN}$(translate "If path not accessible (ZFS/BTRFS):")${CL} ${YW}Verify the dataset/subvolume exists and is mounted${CL}"
echo -e "${TAB}${BGN}$(translate "If container won't start:")${CL} ${YW}Check /var/log/pve/tasks/ for detailed error messages${CL}"
echo -e "${TAB}${BGN}$(translate "To revert changes:")${CL} ${YW}cp /etc/pve/lxc/<container-id>.conf.bak /etc/pve/lxc/<container-id>.conf${CL}"
echo -e
echo -e
msg_success "$(translate "Press Enter to return to menu...")"
echo -e
read -r
}
show_unprivileged_to_privileged_guide() {
clear
show_proxmenux_logo
msg_title "$(translate "Manual Guide: Convert LXC Unprivileged to Privileged")"
echo -e "${TAB}${BL}------------------------------------------------------------------------${CL}"
echo -e
echo -e "${TAB}${RD}$(translate "SECURITY WARNING:")${CL} ${YW}$(translate "Privileged containers have full root access to the host system!")${CL}"
echo -e "${TAB}${YW}$(translate "Only convert to privileged if absolutely necessary for your use case.")${CL}"
echo -e
echo -e
echo -e "${TAB}${BOLD}$(translate "IMPORTANT PREREQUISITES:")${CL}"
echo -e
echo -e "${TAB}${BGN}$(translate "Container must be stopped before conversion")${CL}"
echo -e "${TAB}${BGN}$(translate "Create a backup of your container before proceeding")${CL}"
echo -e "${TAB}${BGN}$(translate "Understand the security implications of privileged containers")${CL}"
echo -e "${TAB}${BGN}$(translate "This is a simple configuration change")${CL}"
echo -e
echo -e "${TAB}${BL}------------------------------------------------------------------------${CL}"
echo -e
show_command "1" \
"$(translate "List all containers to identify the unprivileged one:")" \
"pct list" \
"$(translate "Look for containers with 'unprivileged: 1' in their config")"
show_command "2" \
"$(translate "Check if container is unprivileged:")" \
"pct config <container-id> | grep unprivileged" \
"$(translate "Should show 'unprivileged: 1' if it's unprivileged")" \
"$(translate "Example: pct config 110 | grep unprivileged")"
show_command "3" \
"$(translate "Stop the container if it's running:")" \
"pct stop <container-id>" \
"$(translate "Replace <container-id> with your actual container ID")" \
"$(translate "Example: pct stop 110")"
show_command "4" \
"$(translate "Create a backup of the container configuration:")" \
"cp /etc/pve/lxc/<container-id>.conf /etc/pve/lxc/<container-id>.conf.bak" \
"$(translate "This creates a backup in case you need to revert changes")" \
"$(translate "Example: cp /etc/pve/lxc/110.conf /etc/pve/lxc/110.conf.bak")"
show_command "5" \
"$(translate "Remove the unprivileged flag from configuration:")" \
"sed -i '/^unprivileged: 1/d' /etc/pve/lxc/<container-id>.conf" \
"$(translate "This removes the 'unprivileged: 1' line from the config")" \
"$(translate "Example: sed -i '/^unprivileged: 1/d' /etc/pve/lxc/110.conf")"
show_command "6" \
"$(translate "Add explicit privileged flag (optional but recommended):")" \
"echo 'unprivileged: 0' >> /etc/pve/lxc/<container-id>.conf" \
"$(translate "This explicitly marks the container as privileged")"
show_command "7" \
"$(translate "Start the converted container:")" \
"pct start <container-id>" \
"$(translate "The container should now start as privileged")"
show_command "8" \
"$(translate "Verify the conversion:")" \
"pct config <container-id> | grep unprivileged" \
"$(translate "Should show 'unprivileged: 0' or no unprivileged line")"
echo -e "${TAB}${BL}------------------------------------------------------------------------${CL}"
echo -e
echo -e
echo -e "${TAB}${BOLD}$(translate "SECURITY CONSIDERATIONS:")${CL}"
echo -e
echo -e "${TAB}${RD}$(translate "Privileged containers can access host devices directly")${CL}"
echo -e "${TAB}${RD}$(translate "Root inside container = root on host system")${CL}"
echo -e "${TAB}${RD}$(translate "Use only when unprivileged containers cannot meet your needs")${CL}"
echo -e "${TAB}${RD}$(translate "Consider security implications for production environments")${CL}"
echo -e
echo -e
echo -e "${TAB}${BOLD}$(translate "TROUBLESHOOTING:")${CL}"
echo -e
echo -e "${TAB}${BGN}$(translate "If container won't start:")${CL} ${YW}Check /var/log/pve/tasks/ for detailed error messages${CL}"
echo -e "${TAB}${BGN}$(translate "To revert changes:")${CL} ${YW}cp /etc/pve/lxc/<container-id>.conf.bak /etc/pve/lxc/<container-id>.conf${CL}"
echo -e "${TAB}${BGN}$(translate "If config issues occur:")${CL} ${YW}Manually edit /etc/pve/lxc/<container-id>.conf${CL}"
echo -e
echo -e
echo -e
msg_success "$(translate "Press Enter to return to menu...")"
echo -e
read -r
}
show_lxc_conversion_manual_menu() {
while true; do
CHOICE=$(dialog --title "$(translate "LXC Conversion Manual Guides")" \
--menu "$(translate "Select conversion guide:")" 18 70 10 \
"1" "$(translate "Convert Privileged to Unprivileged")" \
"2" "$(translate "Convert Unprivileged to Privileged")" \
"3" "$(translate "Return to Main Menu")" \
3>&1 1>&2 2>&3)
case $CHOICE in
1) show_privileged_to_unprivileged_guide ;;
2) show_unprivileged_to_privileged_guide ;;
3) return ;;
*) return ;;
esac
done
}
# Main execution
show_lxc_conversion_manual_menu

View File

@@ -232,6 +232,10 @@ convert_direct_method() {
fi fi
msg_ok "$(translate 'Direct conversion completed for container') $CONTAINER_ID" msg_ok "$(translate 'Direct conversion completed for container') $CONTAINER_ID"
echo -e
msg_success "Press Enter to continue..."
read -r
} }
cleanup_and_finalize() { cleanup_and_finalize() {
@@ -258,7 +262,7 @@ main() {
msg_ok "$(translate 'Converted container ID:') $CONTAINER_ID" msg_ok "$(translate 'Converted container ID:') $CONTAINER_ID"
msg_ok "$(translate 'LXC conversion from privileged to unprivileged completed successfully!')" msg_ok "$(translate 'LXC conversion from privileged to unprivileged completed successfully!')"
echo -e echo -e
msg_success "$(translate "Press Enter to continue")" msg_success "$(translate "Press Enter to return to menu...")"
read -r read -r
exit 0 exit 0
} }

View File

@@ -0,0 +1,144 @@
#!/bin/bash
# ==========================================================
# ProxMenu - LXC Unprivileged to Privileged Converter
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# Version : 2.0
# Last Updated: 19/08/2025
# ==========================================================
# Description:
# This script converts an unprivileged LXC container to a privileged one
# by directly modifying the configuration file.
# WARNING: This reduces security. Use only when necessary.
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
# ==========================================================
select_unprivileged_container() {
CONTAINERS=$(pct list | awk 'NR>1 {print $1, $3}' | while read id name; do
if pct config "$id" | grep -q "^unprivileged: 1"; then
echo "$id" "$name"
fi
done | xargs -n2)
if [ -z "$CONTAINERS" ]; then
msg_error "$(translate 'No unprivileged containers available in Proxmox.')"
exit 1
fi
cleanup
CONTAINER_ID=$(whiptail --title "$(translate 'Select Unprivileged Container')" \
--menu "$(translate 'Select the unprivileged LXC container to convert:')" 20 70 10 $CONTAINERS 3>&1 1>&2 2>&3)
if [ -z "$CONTAINER_ID" ]; then
msg_error "$(translate 'No container selected. Exiting.')"
exit 1
fi
msg_ok "$(translate 'Unprivileged container selected:') $CONTAINER_ID"
}
show_backup_warning() {
if ! whiptail --title "$(translate 'Backup Recommendation')" \
--yes-button "$(translate 'Continue')" \
--no-button "$(translate 'Exit')" \
--yesno "$(translate 'It is recommended to create a backup before continuing.')" \
12 70; then
msg_info "$(translate 'Operation cancelled by user to create backup.')"
exit 0
fi
}
convert_to_privileged() {
CONF_FILE="/etc/pve/lxc/$CONTAINER_ID.conf"
CONTAINER_STATUS=$(pct status "$CONTAINER_ID" | awk '{print $2}')
if [ "$CONTAINER_STATUS" == "running" ]; then
msg_info "$(translate 'Stopping container') $CONTAINER_ID..."
pct shutdown "$CONTAINER_ID"
# Wait for container to stop
for i in {1..10}; do
sleep 1
if [ "$(pct status "$CONTAINER_ID" | awk '{print $2}')" != "running" ]; then
break
fi
done
# Verify container stopped
if [ "$(pct status "$CONTAINER_ID" | awk '{print $2}')" == "running" ]; then
msg_error "$(translate 'Failed to stop the container.')"
exit 1
fi
msg_ok "$(translate 'Container stopped.')"
else
msg_ok "$(translate 'Container is already stopped.')"
fi
msg_info "$(translate 'Creating backup of configuration file...')"
cp "$CONF_FILE" "$CONF_FILE.bak"
msg_ok "$(translate 'Configuration backup created:') $CONF_FILE.bak"
msg_info "$(translate 'Converting container to privileged...')"
sed -i '/^unprivileged: 1/d' "$CONF_FILE"
echo "unprivileged: 0" >> "$CONF_FILE"
msg_ok "$(translate 'Container successfully converted to privileged.')"
echo -e
msg_success "Press Enter to continue..."
read -r
}
finalize_conversion() {
if whiptail --yesno "$(translate 'Do you want to start the privileged container') $CONTAINER_ID $(translate 'now?')" 10 60; then
msg_info "$(translate 'Starting privileged container...')"
pct start "$CONTAINER_ID"
msg_ok "$(translate 'Privileged container') $CONTAINER_ID $(translate 'started successfully.')"
fi
}
main() {
show_proxmenux_logo
msg_title "$(translate "LXC Unprivileged to Privileged conversion")"
msg_info "$(translate 'Starting LXC Unprivileged to Privileged conversion process...')"
select_unprivileged_container
show_backup_warning
convert_to_privileged
finalize_conversion
msg_ok "$(translate 'LXC conversion from unprivileged to privileged completed successfully!')"
msg_ok "$(translate 'Converted container ID:') $CONTAINER_ID"
echo -e
msg_success "$(translate "Press Enter to return to menu...")"
read -r
exit 0
}
# Execute main function
main

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management # ProxMenux - A menu-driven script for Proxmox VE management
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi
@@ -197,7 +197,7 @@ show_version_info() {
info_message+="$(translate "No installation information available.")\n" info_message+="$(translate "No installation information available.")\n"
fi fi
info_message+="\n$(translate "ProxMenu files:")\n" info_message+="\n$(translate "ProxMenux files:")\n"
[ -f "$INSTALL_DIR/$MENU_SCRIPT" ] && info_message+="$MENU_SCRIPT$INSTALL_DIR/$MENU_SCRIPT\n" || info_message+="$MENU_SCRIPT\n" [ -f "$INSTALL_DIR/$MENU_SCRIPT" ] && info_message+="$MENU_SCRIPT$INSTALL_DIR/$MENU_SCRIPT\n" || info_message+="$MENU_SCRIPT\n"
[ -f "$UTILS_FILE" ] && info_message+="✓ utils.sh → $UTILS_FILE\n" || info_message+="✗ utils.sh\n" [ -f "$UTILS_FILE" ] && info_message+="✓ utils.sh → $UTILS_FILE\n" || info_message+="✗ utils.sh\n"
[ -f "$CONFIG_FILE" ] && info_message+="✓ config.json → $CONFIG_FILE\n" || info_message+="✗ config.json\n" [ -f "$CONFIG_FILE" ] && info_message+="✓ config.json → $CONFIG_FILE\n" || info_message+="✗ config.json\n"
@@ -237,8 +237,8 @@ uninstall_proxmenu() {
install_type=$(detect_installation_type) install_type=$(detect_installation_type)
if ! dialog --clear --backtitle "ProxMenux Configuration" \ if ! dialog --clear --backtitle "ProxMenux Configuration" \
--title "Uninstall ProxMenu" \ --title "Uninstall ProxMenux" \
--yesno "\n$(translate "Are you sure you want to uninstall ProxMenu?")" 8 60; then --yesno "\n$(translate "Are you sure you want to uninstall ProxMenux?")" 8 60; then
return return
fi fi

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management # ProxMenux - A menu-driven script for Proxmox VE management
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - LXC Conversion Management Menu # ProxMenux - LXC Conversion Management Menu
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi
@@ -35,37 +35,33 @@ show_main_menu() {
"1" "$(translate 'Convert Privileged to Unprivileged')" \ "1" "$(translate 'Convert Privileged to Unprivileged')" \
"2" "$(translate 'Convert Unprivileged to Privileged')" \ "2" "$(translate 'Convert Unprivileged to Privileged')" \
"3" "$(translate 'Show Container Privilege Status')" \ "3" "$(translate 'Show Container Privilege Status')" \
"4" "$(translate 'Exit')" 3>&1 1>&2 2>&3) "4" "$(translate "Help & Info (commands)")" \
"5" "$(translate 'Exit')" 3>&1 1>&2 2>&3)
case $CHOICE in case $CHOICE in
1) 1)
bash <(curl -fsSL "$REPO_URL/scripts/lcx/lxc-privileged-to-unprivileged.sh") bash <(curl -s "$REPO_URL/scripts/lxc/lxc-privileged-to-unprivileged.sh")
;; ;;
2) 2)
convert_unprivileged_to_privileged bash <(curl -s "$REPO_URL/scripts/lxc/lxc-unprivileged-to-privileged.sh")
;; ;;
3) 3)
show_container_status show_container_status
;; ;;
4) 4)
exit 0 bash <(curl -s "$REPO_URL/scripts/lxc/lxc-conversion-manual-guide.sh")
;;
5)
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
;; ;;
*) *)
exit 0 exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
;;
esac esac
} }
convert_unprivileged_to_privileged() {
bash <(curl -fsSL "$REPO_URL/scripts/lcx/lxc-privileged-to-unprivileged.sh")
}
show_container_status() { show_container_status() {
show_proxmenux_logo
msg_info "$(translate 'Gathering container privilege information...')" msg_info "$(translate 'Gathering container privilege information...')"
@@ -75,6 +71,7 @@ show_container_status() {
echo "=================================" >> "$TEMP_FILE" echo "=================================" >> "$TEMP_FILE"
echo "" >> "$TEMP_FILE" echo "" >> "$TEMP_FILE"
pct list | awk 'NR>1 {print $1, $3}' | while read id name; do pct list | awk 'NR>1 {print $1, $3}' | while read id name; do
if pct config "$id" | grep -q "^unprivileged: 1"; then if pct config "$id" | grep -q "^unprivileged: 1"; then
status="$(translate 'Unprivileged')" status="$(translate 'Unprivileged')"
@@ -87,17 +84,21 @@ show_container_status() {
printf "ID: %-4s | %-20s | %-12s | %s\n" "$id" "$name" "$status" "$running_status" >> "$TEMP_FILE" printf "ID: %-4s | %-20s | %-12s | %s\n" "$id" "$name" "$status" "$running_status" >> "$TEMP_FILE"
done done
echo "" >> "$TEMP_FILE"
echo "$(translate 'Legend:')" >> "$TEMP_FILE"
echo "$(translate 'Privileged: Full host access (less secure)')" >> "$TEMP_FILE"
echo "$(translate 'Unprivileged: Limited access (more secure)')" >> "$TEMP_FILE"
cleanup cleanup
dialog --backtitle "ProxMenux" --title "$(translate 'Container Status')" --textbox "$TEMP_FILE" 25 80 dialog --title "$(translate 'Container Status')" --textbox "$TEMP_FILE" 25 80
rm -f "$TEMP_FILE" rm -f "$TEMP_FILE"
show_main_menu
show_main_menu
} }
show_main_menu
show_main_menu

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management # ProxMenux - A menu-driven script for Proxmox VE management
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi
@@ -99,14 +99,15 @@ show_menu() {
--title "$(translate "$menu_title")" \ --title "$(translate "$menu_title")" \
--menu "$(translate "Select an option:")" 20 70 10 \ --menu "$(translate "Select an option:")" 20 70 10 \
1 "$(translate "Settings post-install Proxmox")" \ 1 "$(translate "Settings post-install Proxmox")" \
2 "$(translate "Help and Info Commands")" \ 2 "$(translate "Hardware: GPUs and Coral-TPU")" \
3 "$(translate "Hardware: GPUs and Coral-TPU")" \ 3 "$(translate "Create VM from template or script")" \
4 "$(translate "Create VM from template or script")" \ 4 "$(translate "Disk and Storage Manager")" \
5 "$(translate "Disk and Storage Manager")" \ 5 "$(translate "Mount and Share Manager")" \
6 "$(translate "Proxmox VE Helper Scripts")" \ 6 "$(translate "Proxmox VE Helper Scripts")" \
7 "$(translate "Network Management")" \ 7 "$(translate "Network Management")" \
8 "$(translate "Utilities and Tools")" \ 8 "$(translate "Utilities and Tools")" \
9 "$(translate "Settings")" \ h "$(translate "Help and Info Commands")" \
s "$(translate "Settings")" \
0 "$(translate "Exit")" 2>"$TEMP_FILE" 0 "$(translate "Exit")" 2>"$TEMP_FILE"
local EXIT_STATUS=$? local EXIT_STATUS=$?
@@ -122,15 +123,16 @@ show_menu() {
case $OPTION in case $OPTION in
1) exec bash <(curl -s "$REPO_URL/scripts/menus/menu_post_install.sh") ;; 1) exec bash <(curl -s "$REPO_URL/scripts/menus/menu_post_install.sh") ;;
2) exec bash <(curl -s "$REPO_URL/scripts/help_info_menu.sh") ;; 2) exec bash <(curl -s "$REPO_URL/scripts/menus/hw_grafics_menu.sh") ;;
3) exec bash <(curl -s "$REPO_URL/scripts/menus/hw_grafics_menu.sh") ;; 3) exec bash <(curl -s "$REPO_URL/scripts/menus/create_vm_menu.sh") ;;
4) exec bash <(curl -s "$REPO_URL/scripts/menus/create_vm_menu.sh") ;; 4) exec bash <(curl -s "$REPO_URL/scripts/menus/storage_menu.sh") ;;
5) exec bash <(curl -s "$REPO_URL/scripts/menus/storage_menu.sh") ;; 5) exec bash <(curl -s "$REPO_URL/scripts/menus/share_menu.sh") ;;
6) exec bash <(curl -s "$REPO_URL/scripts/menus/menu_Helper_Scripts.sh") ;; 6) exec bash <(curl -s "$REPO_URL/scripts/menus/menu_Helper_Scripts.sh") ;;
7) exec bash <(curl -s "$REPO_URL/scripts/menus/network_menu.sh") ;; 7) exec bash <(curl -s "$REPO_URL/scripts/menus/network_menu.sh") ;;
8) exec bash <(curl -s "$REPO_URL/scripts/menus/utilities_menu.sh") ;; 8) exec bash <(curl -s "$REPO_URL/scripts/menus/utilities_menu.sh") ;;
9) exec bash <(curl -s "$REPO_URL/scripts/menus/config_menu.sh") ;; h) bash <(curl -s "$REPO_URL/scripts/help_info_menu.sh") ;;
0) clear; msg_ok "$(translate "Thank you for using ProxMenu. Goodbye!")"; rm -f "$TEMP_FILE"; exit 0 ;; s) exec bash <(curl -s "$REPO_URL/scripts/menus/config_menu.sh") ;;
0) clear; msg_ok "$(translate "Thank you for using ProxMenux. Goodbye!")"; rm -f "$TEMP_FILE"; exit 0 ;;
*) msg_warn "$(translate "Invalid option")"; sleep 2 ;; *) msg_warn "$(translate "Invalid option")"; sleep 2 ;;
esac esac
done done

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management # ProxMenux - A menu-driven script for Proxmox VE management
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi
@@ -71,7 +71,7 @@ show_menu() {
7) exec bash <(curl -s "$REPO_URL/scripts/menus/network_menu.sh") ;; 7) exec bash <(curl -s "$REPO_URL/scripts/menus/network_menu.sh") ;;
8) exec bash <(curl -s "$REPO_URL/scripts/menus/utilities_menu.sh") ;; 8) exec bash <(curl -s "$REPO_URL/scripts/menus/utilities_menu.sh") ;;
9) exec bash <(curl -s "$REPO_URL/scripts/menus/config_menu.sh") ;; 9) exec bash <(curl -s "$REPO_URL/scripts/menus/config_menu.sh") ;;
0) clear; msg_ok "$(translate "Thank you for using ProxMenu. Goodbye!")"; rm -f "$TEMP_FILE"; exit 0 ;; 0) clear; msg_ok "$(translate "Thank you for using ProxMenux. Goodbye!")"; rm -f "$TEMP_FILE"; exit 0 ;;
*) msg_warn "$(translate "Invalid option")"; sleep 2 ;; *) msg_warn "$(translate "Invalid option")"; sleep 2 ;;
esac esac
done done

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management # ProxMenux - A menu-driven script for Proxmox VE management
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management # ProxMenux - A menu-driven script for Proxmox VE management
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - Network Management and Repair Tool # ProxMenux - Network Management and Repair Tool
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi

View File

@@ -0,0 +1,88 @@
#!/bin/bash
# ==========================================================
# ProxMenux - Network Storage Manager Menu
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT
# Version : 1.2
# Last Updated: $(date +%d/%m/%Y)
# ==========================================================
# Configuration
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
# ==========================================================
while true; do
OPTION=$(dialog --colors --backtitle "ProxMenux" \
--title "$(translate "Mount and Share Manager")" \
--menu "\n$(translate "Select an option:")" 25 80 15 \
"" "\Z4──────────────────────── $(translate "HOST") ─────────────────────────\Zn" \
"1" "$(translate "Configure NFS shared on Host")" \
"2" "$(translate "Configure Samba shared on Host")" \
"3" "$(translate "Configure Local Shared on Host")" \
"" "\Z4──────────────────────── $(translate "LXC") ─────────────────────────\Zn" \
"4" "$(translate "Configure LXC Mount Points (Host ↔ Container)")" \
"" "" \
"5" "$(translate "Configure NFS Client in LXC (only privileged)")" \
"6" "$(translate "Configure Samba Client in LXC (only privileged)")" \
"7" "$(translate "Configure NFS Server in LXC (only privileged)")" \
"8" "$(translate "configure Samba Server in LXC (only privileged)")" \
"" "" \
"h" "$(translate "Help & Info (commands)")" \
"0" "$(translate "Return to Main Menu")" \
2>&1 >/dev/tty
) || { exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh"); }
case "$OPTION" in
lxctitle|hosttitle)
continue
;;
1)
bash <(curl -s "$REPO_URL/scripts/share/nfs_host.sh")
;;
2)
bash <(curl -s "$REPO_URL/scripts/share/samba_host.sh")
;;
3)
bash <(curl -s "$REPO_URL/scripts/share/local-shared-manager.sh")
;;
4)
bash <(curl -s "$REPO_URL/scripts/share/lxc-mount-manager_minimal.sh")
;;
5)
bash <(curl -s "$REPO_URL/scripts/share/nfs_client.sh")
;;
6)
bash <(curl -s "$REPO_URL/scripts/share/samba_client.sh")
;;
7)
bash <(curl -s "$REPO_URL/scripts/share/nfs_lxc_server.sh")
;;
8)
bash <(curl -s "$REPO_URL/scripts/share/samba_lxc_server.sh")
;;
h)
bash <(curl -s "$REPO_URL/scripts/share/commands_share.sh")
;;
0)
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
;;
*)
exec bash <(curl -s "$REPO_URL/scripts/menus/main_menu.sh")
;;
esac
done

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management # ProxMenux - A menu-driven script for Proxmox VE management
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management # ProxMenux - A menu-driven script for Proxmox VE management
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi
@@ -36,15 +36,12 @@ while true; do
case $OPTION in case $OPTION in
1) 1)
clear
bash <(curl -s "$REPO_URL/scripts/storage/disk-passthrough.sh") bash <(curl -s "$REPO_URL/scripts/storage/disk-passthrough.sh")
;; ;;
2) 2)
clear
bash <(curl -s "$REPO_URL/scripts/storage/disk-passthrough_ct.sh") bash <(curl -s "$REPO_URL/scripts/storage/disk-passthrough_ct.sh")
;; ;;
3) 3)
clear
bash <(curl -s "$REPO_URL/scripts/storage/import-disk-image.sh") bash <(curl -s "$REPO_URL/scripts/storage/import-disk-image.sh")
;; ;;
4) 4)

View File

@@ -143,7 +143,7 @@ remove_subscription_banner() {
msg_warn "Banner removal cancelled by user." msg_warn "Banner removal cancelled by user."
return 1 return 1
fi fi
bash <(curl -fsSL "$REPO_URL/scripts/global/remove-banner-pve9.sh") bash <(curl -fsSL "$REPO_URL/scripts/global/remove-banner-pve9_2.sh")
else else
if ! whiptail --title "Proxmox VE 8.x Subscription Banner Removal" \ if ! whiptail --title "Proxmox VE 8.x Subscription Banner Removal" \
--yesno "Do you want to remove the Proxmox subscription banner from the web interface for PVE $pve_version?" 10 70; then --yesno "Do you want to remove the Proxmox subscription banner from the web interface for PVE $pve_version?" 10 70; then
@@ -479,47 +479,38 @@ apply_network_optimizations() {
# Core buffers & queues # Core buffers & queues
net.core.netdev_max_backlog = 8192 net.core.netdev_max_backlog = 8192
net.core.optmem_max = 8192 net.core.rmem_max = 16777216
net.core.rmem_max = 16777216 net.core.wmem_max = 16777216
net.core.wmem_max = 16777216 net.core.somaxconn = 8192
net.core.somaxconn = 8151
# IPv4 security hardening # IPv4
net.ipv4.conf.all.accept_redirects = 0 net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.accept_source_route = 0 net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.log_martians = 0 net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.all.rp_filter = 1 net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.secure_redirects = 0 net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.accept_redirects = 0 net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.default.accept_source_route = 0 net.ipv4.conf.default.accept_source_route = 0
net.ipv4.conf.default.log_martians = 0 net.ipv4.conf.default.secure_redirects = 0
net.ipv4.conf.default.rp_filter = 1 net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.default.secure_redirects = 0 net.ipv4.conf.default.log_martians = 1
net.ipv4.conf.default.send_redirects = 0
# ICMP handling # rp_filter: loose multi-homed/bridges
net.ipv4.icmp_echo_ignore_broadcasts = 1 net.ipv4.conf.all.rp_filter = 2
net.ipv4.conf.default.rp_filter = 2
# ICMP
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1 net.ipv4.icmp_ignore_bogus_error_responses = 1
# TCP/IP tuning # TCP/IP
net.ipv4.ip_local_port_range = 1024 65535 net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_base_mss = 1024 net.ipv4.tcp_mtu_probing = 1
net.ipv4.tcp_fin_timeout = 10 net.ipv4.tcp_rfc1337 = 1
net.ipv4.tcp_keepalive_intvl = 30 net.ipv4.tcp_sack = 1
net.ipv4.tcp_keepalive_probes= 3 net.ipv4.tcp_rmem = 8192 87380 16777216
net.ipv4.tcp_keepalive_time = 240 net.ipv4.tcp_wmem = 8192 65536 16777216
net.ipv4.tcp_limit_output_bytes = 65536
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.tcp_mtu_probing = 1
net.ipv4.tcp_rfc1337 = 1
net.ipv4.tcp_rmem = 8192 87380 16777216
net.ipv4.tcp_sack = 1
net.ipv4.tcp_slow_start_after_idle = 0
net.ipv4.tcp_syn_retries = 3
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_wmem = 8192 65536 16777216
# Unix sockets # Unix sockets
net.unix.max_dgram_qlen = 4096 net.unix.max_dgram_qlen = 4096
@@ -655,7 +646,8 @@ install_log2ram_auto() {
else else
ROOT_PART=$(lsblk -no NAME,MOUNTPOINT | grep ' /$' | awk '{print $1}') ROOT_PART=$(lsblk -no NAME,MOUNTPOINT | grep ' /$' | awk '{print $1}')
SYSTEM_DISK=$(lsblk -no PKNAME /dev/$ROOT_PART 2>/dev/null) #SYSTEM_DISK=$(lsblk -no PKNAME /dev/$ROOT_PART 2>/dev/null)
SYSTEM_DISK=$(lsblk -no PKNAME /dev/$ROOT_PART 2>/dev/null | grep -E '^[a-z]+' | head -n1)
SYSTEM_DISK=${SYSTEM_DISK:-sda} SYSTEM_DISK=${SYSTEM_DISK:-sda}
if [[ "$SYSTEM_DISK" == nvme* || "$(cat /sys/block/$SYSTEM_DISK/queue/rotational 2>/dev/null)" == "0" ]]; then if [[ "$SYSTEM_DISK" == nvme* || "$(cat /sys/block/$SYSTEM_DISK/queue/rotational 2>/dev/null)" == "0" ]]; then
is_ssd=true is_ssd=true
@@ -665,11 +657,16 @@ install_log2ram_auto() {
if [[ "$is_ssd" == true ]]; then if [[ "$is_ssd" == true ]]; then
msg_ok "$(translate "System disk is SSD or M.2. Proceeding with Log2RAM setup.")" msg_ok "$(translate "System disk is SSD or M.2. Proceeding with Log2RAM setup.")"
else else
msg_warn "$(translate "System disk is not SSD/M.2. Skipping Log2RAM installation.")" if whiptail --yesno "$(translate "Do you want to install Log2RAM anyway to reduce log write load?")" \
return 0 10 70 --title "Log2RAM"; then
:
else
return 0
fi
fi fi
if [[ -f /etc/log2ram.conf ]] && command -v log2ram >/dev/null 2>&1 && systemctl list-units --all | grep -q log2ram; then if [[ -f /etc/log2ram.conf ]] && command -v log2ram >/dev/null 2>&1 && systemctl list-units --all | grep -q log2ram; then
msg_ok "$(translate "Log2RAM is already installed and configured correctly.")" msg_ok "$(translate "Log2RAM is already installed and configured correctly.")"
register_tool "log2ram" true register_tool "log2ram" true
@@ -700,7 +697,7 @@ install_log2ram_auto() {
if ! command -v git >/dev/null 2>&1; then if ! command -v git >/dev/null 2>&1; then
apt-get update -qq >/dev/null 2>&1 apt-get update -qq >/dev/null 2>&1
apt-get install -y git >/dev/null 2>&1 apt-get install -y git >/dev/null 2>&1
msg_ok "$(translate "Git installed successfully")" #msg_ok "$(translate "Git installed successfully")"
fi fi
if ! git clone https://github.com/azlux/log2ram.git /tmp/log2ram >/dev/null 2>>/tmp/log2ram_install.log; then if ! git clone https://github.com/azlux/log2ram.git /tmp/log2ram >/dev/null 2>>/tmp/log2ram_install.log; then
@@ -846,6 +843,7 @@ run_complete_optimization() {
ensure_tools_json ensure_tools_json
apt_upgrade apt_upgrade
cleanup
remove_subscription_banner remove_subscription_banner
#configure_time_sync #configure_time_sync
skip_apt_languages skip_apt_languages
@@ -857,7 +855,7 @@ run_complete_optimization() {
configure_kernel_panic configure_kernel_panic
force_apt_ipv4 force_apt_ipv4
apply_network_optimizations apply_network_optimizations
disable_rpc #disable_rpc
customize_bashrc customize_bashrc
install_log2ram_auto install_log2ram_auto
setup_persistent_network setup_persistent_network

View File

@@ -605,7 +605,7 @@ install_system_utils_() {
command -v "$1" >/dev/null 2>&1 command -v "$1" >/dev/null 2>&1
} }
ensure_repositories() { ensure_repositories_() {
local sources_file="/etc/apt/sources.list" local sources_file="/etc/apt/sources.list"
local need_update=false local need_update=false
@@ -635,6 +635,106 @@ EOF
} }
ensure_repositories() {
local pve_version need_update=false
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+' | head -1)
if [[ -z "$pve_version" ]]; then
msg_error "Unable to detect Proxmox version."
return 1
fi
if (( pve_version >= 9 )); then
# ===== PVE 9 (Debian 13 - trixie) =====
# proxmox.sources (no-subscription) ─ create if missing
if [[ ! -f /etc/apt/sources.list.d/proxmox.sources ]]; then
cat > /etc/apt/sources.list.d/proxmox.sources <<'EOF'
Enabled: true
Types: deb
URIs: http://download.proxmox.com/debian/pve
Suites: trixie
Components: pve-no-subscription
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
EOF
need_update=true
fi
# debian.sources ─ create if missing
if [[ ! -f /etc/apt/sources.list.d/debian.sources ]]; then
cat > /etc/apt/sources.list.d/debian.sources <<'EOF'
Types: deb
URIs: http://deb.debian.org/debian/
Suites: trixie trixie-updates
Components: main contrib non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
Types: deb
URIs: http://security.debian.org/debian-security/
Suites: trixie-security
Components: main contrib non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
EOF
need_update=true
fi
# apt-get update only if needed or lists are empty
if [[ "$need_update" == true ]] || [[ ! -d /var/lib/apt/lists || -z "$(ls -A /var/lib/apt/lists 2>/dev/null)" ]]; then
msg_info "$(translate "Updating APT package lists...")"
apt-get update >/dev/null 2>&1 || apt-get update
fi
else
# ===== PVE 8 (Debian 12 - bookworm) =====
local sources_file="/etc/apt/sources.list"
# Debian base (create or append minimal lines if missing)
if ! grep -qE 'deb .* bookworm .* main' "$sources_file" 2>/dev/null; then
{
echo "deb http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware"
echo "deb http://deb.debian.org/debian bookworm-updates main contrib non-free non-free-firmware"
echo "deb http://security.debian.org/debian-security bookworm-security main contrib non-free non-free-firmware"
} >> "$sources_file"
need_update=true
fi
# Proxmox no-subscription list (classic) if missing
if [[ ! -f /etc/apt/sources.list.d/pve-no-subscription.list ]]; then
echo "deb http://download.proxmox.com/debian/pve bookworm pve-no-subscription" \
> /etc/apt/sources.list.d/pve-no-subscription.list
need_update=true
fi
fi
# apt-get update only if needed or lists are empty
if [[ "$need_update" == true ]] || [[ ! -d /var/lib/apt/lists || -z "$(ls -A /var/lib/apt/lists 2>/dev/null)" ]]; then
msg_info "$(translate "Updating APT package lists...")"
apt-get update >/dev/null 2>&1 || apt-get update
fi
return 0
}
install_single_package() { install_single_package() {
local package="$1" local package="$1"
local command_name="${2:-$package}" local command_name="${2:-$package}"
@@ -695,7 +795,7 @@ EOF
) )
local selected local selected
selected=$(dialog --clear --backtitle "ProxMenu - $(translate "System Utilities")" \ selected=$(dialog --clear --backtitle "ProxMenux - $(translate "System Utilities")" \
--title "$(translate "Select utilities to install")" \ --title "$(translate "Select utilities to install")" \
--checklist "$(translate "Use SPACE to select/deselect, ENTER to confirm")" \ --checklist "$(translate "Use SPACE to select/deselect, ENTER to confirm")" \
20 70 12 "${utilities[@]}" 2>&1 >/dev/tty) 20 70 12 "${utilities[@]}" 2>&1 >/dev/tty)
@@ -707,7 +807,7 @@ EOF
local selected="$1" local selected="$1"
if [ -z "$selected" ]; then if [ -z "$selected" ]; then
dialog --clear --backtitle "ProxMenu" \ dialog --clear --backtitle "ProxMenux" \
--title "$(translate "No Selection")" \ --title "$(translate "No Selection")" \
--msgbox "$(translate "No utilities were selected")" 8 40 --msgbox "$(translate "No utilities were selected")" 8 40
return return
@@ -875,7 +975,7 @@ EOF
local selected local selected
selected=$( selected=$(
dialog --clear --backtitle "ProxMenu - $(translate "System Utilities")" \ dialog --clear --backtitle "ProxMenux - $(translate "System Utilities")" \
--title "$(translate "Select utilities to install")" \ --title "$(translate "Select utilities to install")" \
--checklist "$(translate "Use SPACE to select/deselect, ENTER to confirm")" \ --checklist "$(translate "Use SPACE to select/deselect, ENTER to confirm")" \
20 70 12 "${utilities[@]}" 3>&1 1>&2 2>&3 20 70 12 "${utilities[@]}" 3>&1 1>&2 2>&3
@@ -892,7 +992,7 @@ EOF
if [[ -z "$selected" ]]; then if [[ -z "$selected" ]]; then
dialog --clear --backtitle "ProxMenu" \ dialog --clear --backtitle "ProxMenux" \
--title "$(translate "No Selection")" \ --title "$(translate "No Selection")" \
--msgbox "$(translate "No utilities were selected")" 8 40 --msgbox "$(translate "No utilities were selected")" 8 40
return 0 return 0
@@ -951,7 +1051,7 @@ EOF
local dlg_status=$? local dlg_status=$?
if [[ $dlg_status -ne 0 ]]; then if [[ $dlg_status -ne 0 ]]; then
dialog --clear --backtitle "ProxMenu" \ dialog --clear --backtitle "ProxMenux" \
--title "$(translate "Canceled")" \ --title "$(translate "Canceled")" \
--msgbox "$(translate "Action canceled by user")" 8 40 --msgbox "$(translate "Action canceled by user")" 8 40
@@ -1225,47 +1325,36 @@ apply_network_optimizations() {
# Core buffers & queues # Core buffers & queues
net.core.netdev_max_backlog = 8192 net.core.netdev_max_backlog = 8192
net.core.optmem_max = 8192 net.core.rmem_max = 16777216
net.core.rmem_max = 16777216 net.core.wmem_max = 16777216
net.core.wmem_max = 16777216 net.core.somaxconn = 8192
net.core.somaxconn = 8151
# IPv4 security hardening net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.accept_source_route = 0 net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.log_martians = 0 net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.all.rp_filter = 1 net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.secure_redirects = 0 net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.accept_redirects = 0 net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.default.accept_source_route = 0 net.ipv4.conf.default.accept_source_route = 0
net.ipv4.conf.default.log_martians = 0 net.ipv4.conf.default.secure_redirects = 0
net.ipv4.conf.default.rp_filter = 1 net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.default.secure_redirects = 0 net.ipv4.conf.default.log_martians = 1
net.ipv4.conf.default.send_redirects = 0
# ICMP handling net.ipv4.conf.all.rp_filter = 2
net.ipv4.icmp_echo_ignore_broadcasts = 1 net.ipv4.conf.default.rp_filter = 2
# ICMP
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1 net.ipv4.icmp_ignore_bogus_error_responses = 1
# TCP/IP tuning # TCP/IP
net.ipv4.ip_local_port_range = 1024 65535 net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_base_mss = 1024 net.ipv4.tcp_mtu_probing = 1
net.ipv4.tcp_fin_timeout = 10 net.ipv4.tcp_rfc1337 = 1
net.ipv4.tcp_keepalive_intvl = 30 net.ipv4.tcp_sack = 1
net.ipv4.tcp_keepalive_probes= 3 net.ipv4.tcp_rmem = 8192 87380 16777216
net.ipv4.tcp_keepalive_time = 240 net.ipv4.tcp_wmem = 8192 65536 16777216
net.ipv4.tcp_limit_output_bytes = 65536
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.tcp_mtu_probing = 1
net.ipv4.tcp_rfc1337 = 1
net.ipv4.tcp_rmem = 8192 87380 16777216
net.ipv4.tcp_sack = 1
net.ipv4.tcp_slow_start_after_idle = 0
net.ipv4.tcp_syn_retries = 3
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_wmem = 8192 65536 16777216
# Unix sockets # Unix sockets
net.unix.max_dgram_qlen = 4096 net.unix.max_dgram_qlen = 4096
@@ -1817,7 +1906,7 @@ EOF
install_fail2ban() { install_fail2ban_() {
msg_info2 "$(translate "Installing and configuring Fail2Ban to protect the web interface...")" msg_info2 "$(translate "Installing and configuring Fail2Ban to protect the web interface...")"
@@ -1965,6 +2054,127 @@ EOF
install_fail2ban() {
msg_info2 "$(translate "Installing and configuring Fail2Ban to protect Proxmox web interface and SSH...")"
local deb_codename
deb_codename=$(grep -oP '^VERSION_CODENAME=\K.*' /etc/os-release 2>/dev/null)
if ! grep -RqsE "debian.*(bookworm|trixie)" /etc/apt/sources.list /etc/apt/sources.list.d 2>/dev/null; then
msg_warn "$(translate "Debian repositories missing; creating default source file")"
local src="/etc/apt/sources.list.d/debian.sources"
cat > "$src" <<EOF
Types: deb
URIs: http://deb.debian.org/debian
Suites: ${deb_codename} ${deb_codename}-updates
Components: main contrib non-free non-free-firmware
Types: deb
URIs: http://security.debian.org/debian-security
Suites: ${deb_codename}-security
Components: main contrib non-free non-free-firmware
EOF
msg_ok "$(translate "Debian repositories configured for ${deb_codename}")"
fi
msg_info "$(translate "Installing Fail2Ban...")"
if ! DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null 2>&1 || \
! DEBIAN_FRONTEND=noninteractive apt-get install -y fail2ban >/dev/null 2>&1; then
msg_error "$(translate "Failed to install Fail2Ban")"
return 1
fi
msg_ok "$(translate "Fail2Ban installed successfully")"
mkdir -p /etc/fail2ban/filter.d /etc/fail2ban/jail.d
msg_info "$(translate "Configuring Proxmox filter...")"
cat > /etc/fail2ban/filter.d/proxmox.conf <<'EOF'
[Definition]
failregex = pvedaemon\[.*authentication failure; rhost=<HOST> user=.* msg=.*
ignoreregex =
EOF
msg_ok "$(translate "Proxmox filter configured")"
msg_info "$(translate "Configuring Proxmox jail...")"
cat > /etc/fail2ban/jail.d/proxmox.conf <<'EOF'
[proxmox]
enabled = true
port = 8006
filter = proxmox
logpath = /var/log/daemon.log
maxretry = 3
bantime = 3600
findtime = 600
EOF
msg_ok "$(translate "Proxmox jail configured")"
msg_info "$(translate "Configuring global Fail2Ban settings and SSH jail...")"
cat > /etc/fail2ban/jail.local <<'EOF'
[DEFAULT]
ignoreip = 127.0.0.1
bantime = 86400
maxretry = 2
findtime = 1800
backend = systemd
banaction = nftables
banaction_allports = nftables[type=allports]
[sshd]
enabled = true
filter = sshd
logpath = /var/log/auth.log
maxretry = 2
findtime = 3600
bantime = 32400
EOF
msg_ok "$(translate "Global settings and SSH jail configured")"
touch /var/log/auth.log /var/log/daemon.log
chown root:adm /var/log/auth.log /var/log/daemon.log 2>/dev/null || true
chmod 640 /var/log/auth.log /var/log/daemon.log 2>/dev/null || true
systemctl daemon-reload
systemctl enable --now fail2ban >/dev/null 2>&1
sleep 2
if systemctl is-active --quiet fail2ban; then
msg_ok "$(translate "Fail2Ban is running correctly")"
else
msg_error "$(translate "Fail2Ban is NOT running!")"
journalctl -u fail2ban --no-pager -n 20
fi
if [ -S /var/run/fail2ban/fail2ban.sock ]; then
msg_ok "$(translate "Fail2Ban socket exists!")"
else
msg_warn "$(translate "Warning: Fail2Ban socket does not exist!")"
fi
if fail2ban-client ping >/dev/null 2>&1; then
msg_ok "$(translate "fail2ban-client successfully communicated with the server")"
else
msg_error "$(translate "fail2ban-client could not communicate with the server")"
fi
msg_success "$(translate "Fail2Ban installation and configuration completed successfully!")"
}
# ========================================================== # ==========================================================
@@ -2692,7 +2902,7 @@ remove_subscription_banner() {
if [[ "$pve_version" -ge 9 ]]; then if [[ "$pve_version" -ge 9 ]]; then
bash <(curl -fsSL "$REPO_URL/scripts/global/remove-banner-pve9.sh") bash <(curl -fsSL "$REPO_URL/scripts/global/remove-banner-pve9_2.sh")
else else
bash <(curl -fsSL "$REPO_URL/scripts/global/remove-banner-pve8.sh") bash <(curl -fsSL "$REPO_URL/scripts/global/remove-banner-pve8.sh")
@@ -3080,124 +3290,19 @@ add_repo_test() {
configure_figurine_() { configure_figurine_() {
msg_info2 "$(translate "Installing and configuring Figurine...")" msg_info2 "$(translate "Installing and configuring Figurine...")"
# Variables
local version="1.3.0" local version="1.3.0"
local file="figurine_linux_amd64_v${version}.tar.gz" local file="figurine_linux_amd64_v${version}.tar.gz"
local url="https://github.com/arsham/figurine/releases/download/v${version}/${file}" local url="https://github.com/arsham/figurine/releases/download/v${version}/${file}"
local temp_dir local temp_dir; temp_dir=$(mktemp -d)
temp_dir=$(mktemp -d)
local install_dir="/usr/local/bin"
local profile_script="/etc/profile.d/figurine.sh"
local bin_path="${install_dir}/figurine"
msg_info "$(translate "Downloading Figurine v${version}...")"
if command -v figurine &> /dev/null; then
rm -f "$bin_path" > /dev/null 2>&1
msg_ok "$(translate "Previous installation removed")"
fi
wget -qO "${temp_dir}/${file}" "$url" > /dev/null 2>&1
msg_ok "$(translate "Download completed")"
msg_info "$(translate "Extracting package...")"
tar -xf "${temp_dir}/${file}" -C "${temp_dir}" > /dev/null 2>&1
msg_ok "$(translate "Extraction successful")"
if [[ ! -f "${temp_dir}/deploy/figurine" ]]; then
msg_error "$(translate "Binary not found in extracted content.")"
return 1
fi
msg_info "$(translate "Installing binary to ${install_dir}...")"
mv "${temp_dir}/deploy/figurine" "$bin_path" > /dev/null 2>&1
chmod +x "$bin_path" > /dev/null 2>&1
msg_ok "$(translate "Binary installed")"
msg_info "$(translate "Creating figurine welcome message at ${profile_script}...")"
cat << 'EOF' > "$profile_script"
/usr/local/bin/figurine -f "3d.flf" $(hostname)
EOF
chmod +x "$profile_script" > /dev/null 2>&1
msg_ok "$(translate "Welcome message script created")"
local bashrc="$HOME/.bashrc"
if ! grep -q "alias aptup=" "$bashrc"; then
msg_info "$(translate "Adding useful aliases to ~/.bashrc...")"
cat <<EOF >> "$bashrc"
# ProxMenux Figurine aliases and tools
alias aptup='apt update && apt dist-upgrade'
alias lxcclean='curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/clean-lxcs.sh | bash'
alias lxcupdate='curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/update-lxcs.sh | bash'
alias kernelclean='curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/kernel-clean.sh | bash'
alias cpugov='curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/scaling-governor.sh | bash'
alias updatecerts='pvecm updatecerts'
alias seqwrite='sync; fio --randrepeat=1 --ioengine=libaio --direct=1 --name=test --filename=test --bs=4M --size=32G --readwrite=write --ramp_time=4'
alias seqread='sync; fio --randrepeat=1 --ioengine=libaio --direct=1 --name=test --filename=test --bs=4M --size=32G --readwrite=read --ramp_time=4'
alias ranwrite='sync; fio --randrepeat=1 --ioengine=libaio --direct=1 --name=test --filename=test --bs=4k --size=4G --readwrite=randwrite --ramp_time=4'
alias ranread='sync; fio --randrepeat=1 --ioengine=libaio --direct=1 --name=test --filename=test --bs=4k --size=4G --readwrite=randread --ramp_time=4'
alias l='ls -CF'
alias la='ls -A'
alias ll='ls -alF'
alias ls='ls --color=auto'
alias grep='grep --color=auto'
alias fgrep='fgrep --color=auto'
alias egrep='egrep --color=auto'
EOF
msg_ok "$(translate "Aliases added to .bashrc")"
sed -i '/\${marker_begin}/d;/\${marker_end}/d' .bashrc
else
msg_info "$(translate "Aliases already present. Skipping addition.")"
msg_ok "$(translate "Aliases added to .bashrc")"
fi
msg_info "$(translate "Cleaning up temporary files...")"
rm -rf "$temp_dir" > /dev/null 2>&1
msg_ok "$(translate "Cleanup completed")"
msg_success "$(translate "Figurine installation and configuration completed successfully.")"
}
configure_figurine() {
msg_info2 "$(translate "Installing and configuring Figurine...")"
local version="1.3.0"
local file="figurine_linux_amd64_v${version}.tar.gz"
local url="https://github.com/arsham/figurine/releases/download/v${version}/${file}"
local temp_dir
temp_dir=$(mktemp -d)
local install_dir="/usr/local/bin" local install_dir="/usr/local/bin"
local profile_script="/etc/profile.d/figurine.sh" local profile_script="/etc/profile.d/figurine.sh"
local bin_path="${install_dir}/figurine" local bin_path="${install_dir}/figurine"
local bashrc="/root/.bashrc" local bashrc="/root/.bashrc"
local marker_begin="# BEGIN PMX_FIGURINE"
local marker_end="# END PMX_FIGURINE"
cleanup_dir() { rm -rf "$temp_dir" 2>/dev/null || true; }
trap cleanup_dir EXIT
cleanup() { [[ -f "$bashrc" ]] || touch "$bashrc"
rm -rf "$temp_dir" 2>/dev/null || true
}
trap cleanup EXIT
if command -v figurine &>/dev/null; then if command -v figurine &>/dev/null; then
msg_info "$(translate "Updating Figurine binary...")" msg_info "$(translate "Updating Figurine binary...")"
@@ -3205,13 +3310,12 @@ configure_figurine() {
msg_info "$(translate "Downloading Figurine v${version}...")" msg_info "$(translate "Downloading Figurine v${version}...")"
fi fi
if ! wget -qO "${temp_dir}/${file}" "$url" > /dev/null 2>&1; then if ! wget -qO "${temp_dir}/${file}" "$url"; then
msg_error "$(translate "Failed to download Figurine")" msg_error "$(translate "Failed to download Figurine")"
return 1 return 1
fi fi
if ! tar -xf "${temp_dir}/${file}" -C "${temp_dir}"; then
if ! tar -xf "${temp_dir}/${file}" -C "${temp_dir}" > /dev/null 2>&1; then
msg_error "$(translate "Failed to extract package")" msg_error "$(translate "Failed to extract package")"
return 1 return 1
fi fi
@@ -3223,39 +3327,54 @@ configure_figurine() {
fi fi
msg_info "$(translate "Installing binary to ${install_dir}...")" msg_info "$(translate "Installing binary to ${install_dir}...")"
install -m 0755 -o root -g root "${temp_dir}/deploy/figurine" "$bin_path"
install -m 0755 -o root -g root "${temp_dir}/deploy/figurine" "$bin_path" > /dev/null 2>&1
cat > "$profile_script" << 'EOF' cat > "$profile_script" << 'EOF'
/usr/local/bin/figurine -f "3d.flf" $(hostname) /usr/local/bin/figurine -f "3d.flf" $(hostname)
EOF EOF
chmod +x "$profile_script" > /dev/null 2>&1 chmod +x "$profile_script"
ensure_aliases() {
local bashrc="/root/.bashrc"
[[ -f "$bashrc" ]] || touch "$bashrc"
if grep -q "^${marker_begin}$" "$bashrc" 2>/dev/null; then local -a ALIASES=(
sed -i "/^${marker_begin}$/,/^${marker_end}$/d" "$bashrc" "aptup=apt update && apt dist-upgrade"
fi "lxcclean=curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/clean-lxcs.sh | bash"
"lxcupdate=curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/update-lxcs.sh | bash"
"kernelclean=curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/kernel-clean.sh | bash"
"cpugov=curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/scaling-governor.sh | bash"
"lxctrim=curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/fstrim.sh | bash"
"updatecerts=pvecm updatecerts"
"seqwrite=sync; fio --randrepeat=1 --ioengine=libaio --direct=1 --name=test --filename=test --bs=4M --size=32G --readwrite=write --ramp_time=4"
"seqread=sync; fio --randrepeat=1 --ioengine=libaio --direct=1 --name=test --filename=test --bs=4M --size=32G --readwrite=read --ramp_time=4"
"ranwrite=sync; fio --randrepeat=1 --ioengine=libaio --direct=1 --name=test --filename=test --bs=4k --size=4G --readwrite=randwrite --ramp_time=4"
"ranread=sync; fio --randrepeat=1 --ioengine=libaio --direct=1 --name=test --filename=test --bs=4k --size=4G --readwrite=randread --ramp_time=4"
)
for entry in "${ALIASES[@]}"; do
local name="${entry%%=*}"
local cmd="${entry#*=}"
local esc_cmd
esc_cmd=$(printf "%s" "$cmd" | sed -e 's/[\\/&]/\\&/g')
if grep -Eq "^alias[[:space:]]+$name=" "$bashrc"; then
if ! grep -Eq "^alias[[:space:]]+$name='${esc_cmd}'$" "$bashrc"; then
sed -i -E "s|^alias[[:space:]]+$name=.*$|alias $name='${esc_cmd}'|" "$bashrc"
fi
else
printf "alias %s='%s'\n" "$name" "$cmd" >> "$bashrc"
fi
done
cat >> "$bashrc" << 'EOF' awk '!seen[$0]++' "$bashrc" > "${bashrc}.tmp" && mv "${bashrc}.tmp" "$bashrc"
${marker_begin} }
# ProxMenux Figurine aliases and tools
alias aptup='apt update && apt dist-upgrade' ensure_aliases
alias lxcclean='curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/clean-lxcs.sh | bash'
alias lxcupdate='curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/update-lxcs.sh | bash'
alias kernelclean='curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/kernel-clean.sh | bash'
alias cpugov='curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/scaling-governor.sh | bash'
alias lxctrim='curl -fsSL -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/fstrim.sh | bash'
alias updatecerts='pvecm updatecerts'
alias seqwrite='sync; fio --randrepeat=1 --ioengine=libaio --direct=1 --name=test --filename=test --bs=4M --size=32G --readwrite=write --ramp_time=4'
alias seqread='sync; fio --randrepeat=1 --ioengine=libaio --direct=1 --name=test --filename=test --bs=4M --size=32G --readwrite=read --ramp_time=4'
alias ranwrite='sync; fio --randrepeat=1 --ioengine=libaio --direct=1 --name=test --filename=test --bs=4k --size=4G --readwrite=randwrite --ramp_time=4'
alias ranread='sync; fio --randrepeat=1 --ioengine=libaio --direct=1 --name=test --filename=test --bs=4k --size=4G --readwrite=randread --ramp_time=4'
${marker_end}
EOF
msg_ok "$(translate "Aliases added to .bashrc")" msg_ok "$(translate "Aliases added to .bashrc")"
sed -i '/\${marker_begin}/d;/\${marker_end}/d' .bashrc
msg_success "$(translate "Figurine installation and configuration completed successfully.")" msg_success "$(translate "Figurine installation and configuration completed successfully.")"
register_tool "figurine" true register_tool "figurine" true
@@ -3264,6 +3383,112 @@ EOF
configure_figurine() {
msg_info2 "$(translate "Installing and configuring Figurine...")"
local version="1.3.0"
local file="figurine_linux_amd64_v${version}.tar.gz"
local url="https://github.com/arsham/figurine/releases/download/v${version}/${file}"
local temp_dir; temp_dir=$(mktemp -d)
local install_dir="/usr/local/bin"
local profile_script="/etc/profile.d/figurine.sh"
local bin_path="${install_dir}/figurine"
local bashrc="/root/.bashrc"
cleanup_dir() { rm -rf "$temp_dir" 2>/dev/null || true; }
trap cleanup_dir EXIT
[[ -f "$bashrc" ]] || touch "$bashrc"
if command -v figurine &>/dev/null; then
msg_info "$(translate "Updating Figurine binary...")"
else
msg_info "$(translate "Downloading Figurine v${version}...")"
fi
if ! wget -qO "${temp_dir}/${file}" "$url"; then
msg_error "$(translate "Failed to download Figurine")"
return 1
fi
if ! tar -xf "${temp_dir}/${file}" -C "${temp_dir}"; then
msg_error "$(translate "Failed to extract package")"
return 1
fi
msg_ok "$(translate "Extraction successful")"
if [[ ! -f "${temp_dir}/deploy/figurine" ]]; then
msg_error "$(translate "Binary not found in extracted content.")"
return 1
fi
msg_info "$(translate "Installing binary to ${install_dir}...")"
install -m 0755 -o root -g root "${temp_dir}/deploy/figurine" "$bin_path"
cat > "$profile_script" << 'EOF'
/usr/local/bin/figurine -f "3d.flf" $(hostname)
EOF
chmod +x "$profile_script"
ensure_aliases() {
local bashrc="/root/.bashrc"
[[ -f "$bashrc" ]] || touch "$bashrc"
local -a ALIASES=(
"aptup=apt update && apt dist-upgrade"
"lxcclean=curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/clean-lxcs.sh | bash"
"lxcupdate=curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/update-lxcs.sh | bash"
"kernelclean=curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/kernel-clean.sh | bash"
"cpugov=curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/scaling-governor.sh | bash"
"lxctrim=curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/pve/fstrim.sh | bash"
"updatecerts=pvecm updatecerts"
"seqwrite=sync; fio --randrepeat=1 --ioengine=libaio --direct=1 --name=test --filename=test --bs=4M --size=32G --readwrite=write --ramp_time=4"
"seqread=sync; fio --randrepeat=1 --ioengine=libaio --direct=1 --name=test --filename=test --bs=4M --size=32G --readwrite=read --ramp_time=4"
"ranwrite=sync; fio --randrepeat=1 --ioengine=libaio --direct=1 --name=test --filename=test --bs=4k --size=4G --readwrite=randwrite --ramp_time=4"
"ranread=sync; fio --randrepeat=1 --ioengine=libaio --direct=1 --name=test --filename=test --bs=4k --size=4G --readwrite=randread --ramp_time=4"
)
for entry in "${ALIASES[@]}"; do
local name="${entry%%=*}"
local cmd="${entry#*=}"
local esc_cmd
esc_cmd=$(printf "%s" "$cmd" | sed -e 's/[\\/&]/\\&/g')
if grep -Eq "^alias[[:space:]]+$name=" "$bashrc"; then
if ! grep -Eq "^alias[[:space:]]+$name='${esc_cmd}'$" "$bashrc"; then
sed -i -E "s|^alias[[:space:]]+$name=.*$|alias $name='${esc_cmd}'|" "$bashrc"
fi
else
printf "alias %s='%s'\n" "$name" "$cmd" >> "$bashrc"
fi
done
awk '!seen[$0]++' "$bashrc" > "${bashrc}.tmp" && mv "${bashrc}.tmp" "$bashrc"
}
ensure_aliases
msg_ok "$(translate "Aliases added to .bashrc")"
msg_success "$(translate "Figurine installation and configuration completed successfully.")"
register_tool "figurine" true
}
# ========================================================== # ==========================================================

View File

@@ -150,7 +150,7 @@ uninstall_apt_upgrade() {
################################################################ ################################################################
uninstall_subscription_banner() { uninstall_subscription_banner_() {
msg_info "$(translate "Restoring subscription banner...")" msg_info "$(translate "Restoring subscription banner...")"
# Remove APT hook # Remove APT hook
@@ -163,6 +163,111 @@ uninstall_subscription_banner() {
register_tool "subscription_banner" false register_tool "subscription_banner" false
} }
uninstall_subscription_banner() {
msg_info "$(translate "Restoring subscription banner...")"
local JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
local MOBILE_TPL="/usr/share/pve-manager/templates/index.html.tpl"
local PATCH_BIN="/usr/local/bin/pve-remove-nag.sh"
local BASE_DIR="/opt/pve-nag-buster"
local MIN_JS_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.min.js"
local GZ_FILE="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.gz"
local MARK_MOBILE=" PVE9 Mobile Subscription Banner Removal "
local restored=false
# Remove APT hook (both old and new versions)
for hook in /etc/apt/apt.conf.d/*nag* /etc/apt/apt.conf.d/no-nag-script; do
if [[ -e "$hook" ]]; then
rm -f "$hook"
msg_ok "$(translate "Removed APT hook: $hook")"
fi
done
# Remove patch script
if [[ -f "$PATCH_BIN" ]]; then
rm -f "$PATCH_BIN"
msg_ok "$(translate "Removed patch script: $PATCH_BIN")"
fi
# Restore JavaScript file from backups (new script method)
if [[ -d "$BASE_DIR/backups" ]]; then
local latest_backup
latest_backup=$(ls -t "$BASE_DIR/backups"/proxmoxlib.js.backup.* 2>/dev/null | head -1)
if [[ -n "$latest_backup" && -f "$latest_backup" ]]; then
if [[ -s "$latest_backup" ]] && grep -q "Ext\|function" "$latest_backup" && ! grep -q $'\0' "$latest_backup"; then
cp -a "$latest_backup" "$JS_FILE"
msg_ok "$(translate "Restored from backup: $latest_backup")"
restored=true
fi
fi
fi
# Restore from old script backups (if new method didn't work)
if [[ "$restored" == false ]]; then
local old_backup
old_backup=$(ls -t "${JS_FILE}".backup.pve9.* "${JS_FILE}".backup.* 2>/dev/null | head -1)
if [[ -n "$old_backup" && -f "$old_backup" ]]; then
if [[ -s "$old_backup" ]] && grep -q "Ext\|function" "$old_backup" && ! grep -q $'\0' "$old_backup"; then
cp -a "$old_backup" "$JS_FILE"
msg_ok "$(translate "Restored from old backup: $old_backup")"
restored=true
fi
fi
fi
# Restore mobile template if patched
if [[ -f "$MOBILE_TPL" ]] && grep -q "$MARK_MOBILE" "$MOBILE_TPL"; then
local mobile_backup
mobile_backup=$(ls -t "$BASE_DIR/backups"/index.html.tpl.backup.* 2>/dev/null | head -1)
if [[ -n "$mobile_backup" && -f "$mobile_backup" ]]; then
cp -a "$mobile_backup" "$MOBILE_TPL"
msg_ok "$(translate "Restored mobile template from backup")"
else
# Remove the patch manually if no backup available
sed -i "/^$MARK_MOBILE$/,\$d" "$MOBILE_TPL"
msg_ok "$(translate "Removed mobile template patches")"
fi
fi
# If no valid backups found, reinstall packages
if [[ "$restored" == false ]]; then
msg_info "$(translate "No valid backups found, reinstalling packages...")"
if apt --reinstall install proxmox-widget-toolkit -y >/dev/null 2>&1; then
msg_ok "$(translate "Reinstalled proxmox-widget-toolkit")"
restored=true
else
msg_error "$(translate "Failed to reinstall proxmox-widget-toolkit")"
fi
# Also try to reinstall pve-manager if mobile template was patched
if [[ -f "$MOBILE_TPL" ]] && grep -q "$MARK_MOBILE" "$MOBILE_TPL"; then
apt --reinstall install pve-manager -y >/dev/null 2>&1 || true
fi
fi
rm -f "$MIN_JS_FILE" "$GZ_FILE" 2>/dev/null || true
find /var/cache/pve-manager/ -name "*.js*" -delete 2>/dev/null || true
find /var/lib/pve-manager/ -name "*.js*" -delete 2>/dev/null || true
find /var/cache/nginx/ -type f -delete 2>/dev/null || true
register_tool "subscription_banner" false
if [[ "$restored" == true ]]; then
msg_ok "$(translate "Subscription banner restored successfully")"
msg_ok "$(translate "Refresh your browser to see changes (server restart may be required)")"
else
msg_error "$(translate "Failed to restore subscription banner completely")"
return 1
fi
}
################################################################ ################################################################
uninstall_time_sync() { uninstall_time_sync() {
@@ -583,7 +688,6 @@ migrate_installed_tools() {
return return
fi fi
clear
show_proxmenux_logo show_proxmenux_logo
msg_info "$(translate 'Detecting previous optimizations...')" msg_info "$(translate 'Detecting previous optimizations...')"

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management # ProxMenux - A menu-driven script for Proxmox VE management
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi

View File

@@ -0,0 +1,472 @@
#!/bin/bash
# ==========================================================
# ProxMenux - A menu-driven script for Proxmox VE management
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT (https://raw.githubusercontent.com/MacRimi/ProxMenux/main/LICENSE)
# Version : 1.5
# Last Updated: 04/08/2025
# ==========================================================
# Configuration ============================================
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
# ==========================================================
show_command() {
local step="$1"
local description="$2"
local command="$3"
local note="$4"
local command_extra="$5"
echo -e "${BGN}${step}.${CL} ${BL}${description}${CL}"
echo ""
echo -e "${TAB}${command}"
echo -e
[[ -n "$note" ]] && echo -e "${TAB}${DARK_GRAY}${note}${CL}"
[[ -n "$command_extra" ]] && echo -e "${TAB}${YW}${command_extra}${CL}"
echo ""
}
show_how_to_enter_lxc() {
clear
show_proxmenux_logo
msg_title "$(translate "How to Access an LXC Terminal from Proxmox Host")"
msg_info2 "$(translate "Use these commands on your Proxmox host to access an LXC container's terminal:")"
echo -e
show_command "1" \
"$(translate "Get a list of all your containers:")" \
"pct list" \
"" \
""
show_command "2" \
"$(translate "Enter the container's terminal")" \
"pct enter ${CUS}<container-id>${CL}" \
"$(translate "Replace <container-id> with the actual ID.")"\
"$(translate "For example: pct enter 101")"
show_command "3" \
"$(translate "To exit the container's terminal, press:")" \
"CTRL + D" \
"" \
""
echo -e ""
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
show_host_mount_resources_help() {
clear
show_proxmenux_logo
msg_title "$(translate "Mount Remote Resources on Proxmox Host")"
msg_info2 "$(translate "How to mount NFS and Samba shares directly on the Proxmox host. Proxmox already has the necessary tools installed.")"
echo -e
echo -e "${BOLD}${BL}=== MOUNT NFS SHARE ===${CL}"
echo -e
show_command "1" \
"$(translate "Create mount point:")" \
"mkdir -p ${CUS}/mnt/nfs_share${CL}" \
"$(translate "Replace with your preferred path.")" \
""
show_command "2" \
"$(translate "Mount NFS share:")" \
"mount -t nfs ${CUS}192.168.1.100${CL}:${CUS}/path/to/share${CL} ${CUS}/mnt/nfs_share${CL}" \
"$(translate "Replace IP and paths with your values.")" \
""
show_command "3" \
"$(translate "Make permanent (optional):")" \
"echo '${CUS}192.168.1.100${CL}:${CUS}/path/to/share${CL} ${CUS}/mnt/nfs_share${CL} nfs4 rw,hard,intr,_netdev,rsize=1048576,wsize=1048576,timeo=600,retrans=2 0 0' >> /etc/fstab" \
"$(translate "_netdev waits for network before mounting.")" \
""
echo -e "${BOLD}${BL}=== MOUNT SAMBA SHARE ===${CL}"
echo -e
show_command "4" \
"$(translate "Create mount point:")" \
"mkdir -p ${CUS}/mnt/samba_share${CL}" \
"$(translate "Replace with your preferred path.")" \
""
show_command "5" \
"$(translate "Mount Samba share:")" \
"mount -t cifs //${CUS}192.168.1.100${CL}/${CUS}sharename${CL} ${CUS}/mnt/samba_share${CL} -o username=${CUS}user${CL}" \
"$(translate "You will be prompted for password. Replace IP, share and user.")" \
""
show_command "6" \
"$(translate "Make permanent (optional):")" \
"echo '//${CUS}192.168.1.100${CL}/${CUS}sharename${CL} ${CUS}/mnt/samba_share${CL} cifs username=${CUS}user${CL},password=${CUS}pass${CL},_netdev 0 0' >> /etc/fstab" \
"$(translate "Replace with your credentials.")" \
""
echo -e "${BOLD}${BL}=== CREATE LOCAL DIRECTORY ===${CL}"
echo -e
show_command "7" \
"$(translate "Create directory:")" \
"mkdir -p ${CUS}/mnt/local_share${CL}" \
"$(translate "Creates a local directory on Proxmox host.")" \
""
show_command "8" \
"$(translate "Set permissions:")" \
"chmod 755 ${CUS}/mnt/local_share${CL}" \
"$(translate "Sets basic read/write permissions.")" \
""
show_command "9" \
"$(translate "Verify mounts:")" \
"df -h" \
"$(translate "Shows all mounted filesystems.")" \
""
echo -e ""
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
show_host_to_lxc_mount_help() {
clear
show_proxmenux_logo
msg_title "$(translate "Mount Host Directory to LXC Container")"
msg_info2 "$(translate "How to mount a Proxmox host directory into an LXC container. Execute these commands on the Proxmox host.")"
echo -e
show_command "1" \
"$(translate "Add mount point to container:")" \
"pct set ${CUS}<container-id>${CL} -mp0 ${CUS}/host/directory${CL},mp=${CUS}/container/path${CL},backup=0,shared=1" \
"$(translate "Replace container-id, host directory and container path.")" \
"$(translate "Example: pct set 101 -mp0 /mnt/shared,mp=/mnt/shared,,backup=0,shared=1")"
show_command "2" \
"$(translate "Restart container:")" \
"pct reboot ${CUS}<container-id>${CL}" \
"$(translate "Required to activate the mount point.")" \
""
show_command "3" \
"$(translate "Verify mount inside container:")" \
"pct enter ${CUS}<container-id>${CL}
df -h | grep ${CUS}/container/path${CL}" \
"$(translate "Check if the directory is mounted.")" \
""
show_command "4" \
"$(translate "Remove mount point (if needed):")" \
"pct set ${CUS}<container-id>${CL} --delete mp0" \
"$(translate "Removes the mount point. Use mp1, mp2, etc. for other mounts.")" \
""
echo -e "${BOR}"
echo -e "${BOLD}$(translate "Notes:")${CL}"
echo -e "${TAB}${BGN}$(translate "Mount indices:")${CL} ${BL}Use mp0, mp1, mp2, etc. for multiple mounts${CL}"
echo -e "${TAB}${BGN}$(translate "Permissions:")${CL} ${BL}May need adjustment depending on directory type${CL}"
echo -e "${TAB}${BGN}$(translate "Container types:")${CL} ${BL}Works with both privileged and unprivileged containers${CL}"
echo -e ""
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
show_nfs_server_help() {
clear
show_proxmenux_logo
msg_title "$(translate "NFS Server Installation")"
msg_info2 "$(translate "How to install and configure an NFS server in an LXC container.")"
echo -e
show_command "1" \
"$(translate "Update and install packages:")" \
"apt-get update && apt-get install -y nfs-kernel-server" \
"" \
""
show_command "2" \
"$(translate "Create export directory:")" \
"mkdir -p ${CUS}/mnt/nfs_export${CL}" \
"$(translate "Replace with your preferred path.")" \
""
show_command "3" \
"$(translate "Set directory permissions:")" \
"chmod 755 ${CUS}/mnt/nfs_export${CL}" \
"" \
""
show_command "4.1" \
"$(translate "Configure exports (safe root_squash):")" \
"echo '${CUS}/mnt/nfs_export${CL} ${CUS}192.168.1.0/24${CL}(rw,sync,no_subtree_check,root_squash)' >> /etc/exports" \
"$(translate "Replace directory path and network range.")" \
""
show_command "4.2" \
"$(translate "Or Configure exports (map all users):")" \
"echo '${CUS}/mnt/nfs_export${CL} ${CUS}192.168.1.0/24${CL}(rw,sync,no_subtree_check,all_squash,anonuid=0,anongid=0)' >> /etc/exports" \
"$(translate "Replace directory path and network range.")" \
""
show_command "5" \
"$(translate "Apply configuration:")" \
"exportfs -ra" \
"" \
""
show_command "6" \
"$(translate "Start and enable service:")" \
"systemctl restart nfs-kernel-server
systemctl enable nfs-kernel-server" \
"" \
""
show_command "7" \
"$(translate "Verify exports:")" \
"showmount -e localhost" \
"$(translate "Shows available NFS exports.")" \
""
echo -e "${BOR}"
echo -e "${BOLD}$(translate "Export Options:")${CL}"
echo -e "${TAB}${BGN}$(translate "rw:")${CL} ${BL}Read-write access${CL}"
echo -e "${TAB}${BGN}$(translate "sync:")${CL} ${BL}Synchronous writes${CL}"
echo -e "${TAB}${BGN}$(translate "no_subtree_check:")${CL} ${BL}Improves performance${CL}"
echo -e ""
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
show_samba_server_help() {
clear
show_proxmenux_logo
msg_title "$(translate "Samba Server Installation")"
msg_info2 "$(translate "How to install and configure a Samba server in an LXC container.")"
echo -e
show_command "1" \
"$(translate "Update and install packages:")" \
"apt-get update && apt-get install -y samba" \
"" \
""
show_command "2" \
"$(translate "Create share directory:")" \
"mkdir -p ${CUS}/mnt/samba_share${CL}" \
"$(translate "Replace with your preferred path.")" \
""
show_command "3" \
"$(translate "Set directory permissions:")" \
"chmod 755 ${CUS}/mnt/samba_share${CL}" \
"" \
""
show_command "4" \
"$(translate "Create Samba user:")" \
"adduser ${CUS}sambauser${CL}
smbpasswd -a ${CUS}sambauser${CL}" \
"$(translate "Replace with your username. You'll be prompted for password.")" \
""
show_command "5" \
"$(translate "Configure share:")" \
"cat >> /etc/samba/smb.conf << EOF
[shared]
comment = Shared folder
path = ${CUS}/mnt/samba_share${CL}
read only = no
browseable = yes
valid users = ${CUS}sambauser${CL}
EOF" \
"$(translate "Replace path and username.")" \
""
show_command "6" \
"$(translate "Restart and enable service:")" \
"systemctl restart smbd
systemctl enable smbd" \
"" \
""
show_command "7" \
"$(translate "Test configuration:")" \
"smbclient -L localhost -U ${CUS}sambauser${CL}" \
"$(translate "Lists available shares. You'll be prompted for password.")" \
""
echo -e "${BOR}"
echo -e "${BOLD}$(translate "Connection Examples:")${CL}"
echo -e "${TAB}${BGN}$(translate "Windows:")${CL} ${YW}\\\\<server-ip>\\shared${CL}"
echo -e "${TAB}${BGN}$(translate "Linux:")${CL} ${YW}smbclient //server-ip/shared -U sambauser${CL}"
echo -e ""
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
show_nfs_client_help() {
clear
show_proxmenux_logo
msg_title "$(translate "NFS Client Configuration")"
msg_info2 "$(translate "How to configure an NFS client in an LXC container.")"
echo -e
show_command "1" \
"$(translate "Update and install packages:")" \
"apt-get update && apt-get install -y nfs-common" \
"" \
""
show_command "2" \
"$(translate "Create mount point:")" \
"mkdir -p ${CUS}/mnt/nfsmount${CL}" \
"$(translate "Replace with your preferred path.")" \
""
show_command "3" \
"$(translate "Mount NFS share:")" \
"mount -t nfs ${CUS}192.168.1.100${CL}:${CUS}/mnt/nfs_export${CL} ${CUS}/mnt/nfsmount${CL}" \
"$(translate "Replace server IP and paths.")" \
""
show_command "4" \
"$(translate "Test access:")" \
"ls -la ${CUS}/mnt/nfsmount${CL}" \
"$(translate "Verify you can access the mounted share.")" \
""
show_command "5" \
"$(translate "Make permanent (optional):")" \
"echo '${CUS}192.168.1.100${CL}:${CUS}/path/to/share${CL} ${CUS}/mnt/nfs_share${CL} nfs4 rw,hard,intr,_netdev,rsize=1048576,wsize=1048576,timeo=600,retrans=2 0 0' >> /etc/fstab" \
"$(translate "Replace with your server IP and paths.")" \
""
show_command "6" \
"$(translate "Verify mount:")" \
"df -h | grep nfs" \
"$(translate "Shows NFS mounts.")" \
""
echo -e ""
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
show_samba_client_help() {
clear
show_proxmenux_logo
msg_title "$(translate "Samba Client Configuration")"
msg_info2 "$(translate "How to configure a Samba client in an LXC container.")"
echo -e
show_command "1" \
"$(translate "Update and install packages:")" \
"apt-get update && apt-get install -y cifs-utils" \
"" \
""
show_command "2" \
"$(translate "Create mount point:")" \
"mkdir -p ${CUS}/mnt/sambamount${CL}" \
"$(translate "Replace with your preferred path.")" \
""
show_command "3" \
"$(translate "Mount Samba share:")" \
"mount -t cifs //${CUS}192.168.1.100${CL}/${CUS}shared${CL} ${CUS}/mnt/sambamount${CL} -o username=${CUS}sambauser${CL}" \
"$(translate "Replace server IP, share name and username. You'll be prompted for password.")" \
""
show_command "4" \
"$(translate "Test access:")" \
"ls -la ${CUS}/mnt/sambamount${CL}" \
"$(translate "Verify you can access the mounted share.")" \
""
show_command "5" \
"$(translate "Create credentials file (optional):")" \
"cat > /etc/samba/credentials << EOF
username=${CUS}sambauser${CL}
password=${CUS}your_password${CL}
EOF
chmod 600 /etc/samba/credentials" \
"$(translate "Secure way to store credentials.")" \
""
show_command "6" \
"$(translate "Mount with credentials file:")" \
"mount -t cifs //${CUS}192.168.1.100${CL}/${CUS}shared${CL} ${CUS}/mnt/sambamount${CL} -o credentials=/etc/samba/credentials" \
"$(translate "No password prompt needed.")" \
""
show_command "7" \
"$(translate "Make permanent (optional):")" \
"echo '//${CUS}192.168.1.100${CL}/${CUS}shared${CL} ${CUS}/mnt/sambamount${CL} cifs credentials=/etc/samba/credentials,_netdev 0 0' >> /etc/fstab" \
"$(translate "Replace with your values.")" \
""
show_command "8" \
"$(translate "Verify mount:")" \
"df -h | grep cifs" \
"$(translate "Shows CIFS/Samba mounts.")" \
""
echo -e ""
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
show_help_menu() {
while true; do
CHOICE=$(dialog --title "$(translate "Help & Information")" \
--menu "$(translate "Select help topic:")" 24 80 14 \
"0" "$(translate "How to Access an LXC Terminal")" \
"1" "$(translate "Mount Remote Resources on Proxmox Host")" \
"2" "$(translate "Mount Host Directory to LXC Container")" \
"3" "$(translate "NFS Server Installation")" \
"4" "$(translate "Samba Server Installation")" \
"5" "$(translate "NFS Client Configuration")" \
"6" "$(translate "Samba Client Configuration")" \
"7" "$(translate "Return to Main Menu")" \
3>&1 1>&2 2>&3)
case $CHOICE in
0) show_how_to_enter_lxc ;;
1) show_host_mount_resources_help ;;
2) show_host_to_lxc_mount_help ;;
3) show_nfs_server_help ;;
4) show_samba_server_help ;;
5) show_nfs_client_help ;;
6) show_samba_client_help ;;
7) return ;;
*) return ;;
esac
done
}
show_help_menu

View File

@@ -0,0 +1,271 @@
#!/usr/bin/env bash
# ==========================================================
# ProxMenux - Shared Groups Manager
# ==========================================================
# Author : MacRimi
# Description : Manage host groups for shared directories
# ==========================================================
# Configuration
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
pmx_list_groups() {
local groups
groups=$(getent group | awk -F: '$3 >= 1000 && $1 != "nogroup" && $1 !~ /^pve/ {print $1 ":" $3}')
if [[ -z "$groups" ]]; then
whiptail --title "$(translate "Groups")" --msgbox "$(translate "No user groups found.")" 8 60
return
fi
show_proxmenux_logo
msg_title "$(translate "Existing Groups")"
echo "$groups" | column -t -s: | while read -r name gid; do
members=$(getent group "$name" | awk -F: '{print $4}')
echo -e "${BL}$name${CL} (GID: $gid) -> ${YW}${members:-no members}${CL}"
done
echo ""
msg_success "$(translate "Press Enter to continue...")"
read -r
}
pmx_create_group() {
group_name=$(dialog --inputbox "$(translate "Enter new group name:")" 10 60 "sharedfiles-new" \
--title "$(translate "New Group")" 3>&1 1>&2 2>&3) || return
[[ -z "$group_name" ]] && return
if getent group "$group_name" >/dev/null; then
dialog --title "$(translate "Error")" --msgbox "$(translate "Group already exists.")" 8 50
return
fi
if groupadd "$group_name"; then
show_proxmenux_logo
msg_title "$(translate "Create Group")"
msg_ok "$(translate "Group created successfully:") $group_name"
else
show_proxmenux_logo
msg_title "$(translate "Create Group")"
msg_error "$(translate "Failed to create group.")"
fi
echo -e
msg_success "$(translate "Press Enter to continue...")"
read -r
}
pmx_edit_group() {
local groups group_name action
groups=$(getent group | awk -F: '$3 >= 1000 && $1 != "nogroup" && $1 !~ /^pve/ {print $1}')
if [[ -z "$groups" ]]; then
dialog --title "$(translate "Error")" --msgbox "$(translate "No groups available to edit.")" 8 50
return
fi
local menu_options=""
while read -r group; do
if [[ -n "$group" ]]; then
local gid=$(getent group "$group" | cut -d: -f3)
menu_options="$menu_options $group \"GID:$gid\""
fi
done <<< "$groups"
group_name=$(eval "dialog --title \"$(translate "Edit Group")\" --menu \
\"$(translate "Select a group:")\" 20 60 10 \
$menu_options 3>&1 1>&2 2>&3")
if [[ -z "$group_name" ]]; then
return
fi
action=$(dialog --title "$(translate "Edit Group")" --menu \
"$(translate "What do you want to edit in group:") $group_name" 15 60 3 \
"rename" "$(translate "Rename group")" \
"gid" "$(translate "Change GID")" \
"users" "$(translate "Add/Remove users")" 3>&1 1>&2 2>&3)
if [[ -z "$action" ]]; then
return
fi
case "$action" in
rename)
new_name=$(dialog --inputbox "$(translate "Enter new group name:")" 10 60 \
"$group_name" --title "$(translate "Rename Group")" 3>&1 1>&2 2>&3)
if [[ -n "$new_name" && "$new_name" != "$group_name" ]]; then
if groupmod -n "$new_name" "$group_name" 2>/dev/null; then
show_proxmenux_logo
msg_title "$(translate "Rename Group")"
msg_ok "$(translate "Group renamed to:") $new_name"
else
show_proxmenux_logo
msg_title "$(translate "Rename Group")"
msg_error "$(translate "Failed to rename group")"
fi
fi
;;
gid)
current_gid=$(getent group "$group_name" | cut -d: -f3)
new_gid=$(dialog --inputbox "$(translate "Enter new GID:")" 10 60 \
"$current_gid" --title "$(translate "Change GID")" 3>&1 1>&2 2>&3)
if [[ -n "$new_gid" && "$new_gid" != "$current_gid" ]]; then
if groupmod -g "$new_gid" "$group_name" 2>/dev/null; then
show_proxmenux_logo
msg_title "$(translate "Change GID")"
msg_ok "$(translate "GID changed to:") $new_gid"
else
show_proxmenux_logo
msg_title "$(translate "Change GID")"
msg_error "$(translate "Failed to change GID")"
fi
fi
;;
users)
user_action=$(dialog --title "$(translate "User Management")" --menu \
"$(translate "Choose an action for group:") $group_name" 15 60 2 \
"add" "$(translate "Add user to group")" \
"remove" "$(translate "Remove user from group")" 3>&1 1>&2 2>&3)
case "$user_action" in
add)
username=$(dialog --inputbox "$(translate "Enter username to add:")" 10 60 \
--title "$(translate "Add User")" 3>&1 1>&2 2>&3)
if [[ -n "$username" ]]; then
if id "$username" >/dev/null 2>&1; then
if usermod -aG "$group_name" "$username" 2>/dev/null; then
show_proxmenux_logo
msg_title "$(translate "Add User")"
msg_ok "$(translate "User added:") $username"
else
show_proxmenux_logo
msg_title "$(translate "Add User")"
msg_error "$(translate "Failed to add user")"
fi
else
show_proxmenux_logo
msg_title "$(translate "Add User")"
msg_error "$(translate "User does not exist:") $username"
fi
fi
;;
remove)
members=$(getent group "$group_name" | awk -F: '{print $4}' | tr ',' ' ')
if [[ -z "$members" ]]; then
dialog --title "$(translate "Info")" --msgbox "$(translate "No users in this group.")" 8 50
return
fi
local user_options=""
for user in $members; do
user_options="$user_options $user \"\""
done
username=$(eval "dialog --title \"$(translate "Remove User")\" --menu \
\"$(translate "Select user to remove:")\" 15 60 5 \
$user_options 3>&1 1>&2 2>&3")
if [[ -n "$username" ]]; then
if gpasswd -d "$username" "$group_name" 2>/dev/null; then
show_proxmenux_logo
msg_title "$(translate "Remove User")"
msg_ok "$(translate "User removed:") $username"
else
show_proxmenux_logo
msg_title "$(translate "Remove User")"
msg_error "$(translate "Failed to remove user")"
fi
fi
;;
esac
;;
esac
echo -e
msg_success "$(translate "Press Enter to continue...")"
read -r
}
pmx_delete_group() {
local groups group_name menu_options
groups=$(getent group | awk -F: '$3 >= 1000 && $1 != "nogroup" && $1 !~ /^pve/ {print $1}')
if [[ -z "$groups" ]]; then
dialog --title "$(translate "Error")" --msgbox "$(translate "No groups available to delete.")" 8 50
return
fi
menu_options=""
while read -r group; do
if [[ -n "$group" ]]; then
menu_options="$menu_options $group \"\""
fi
done <<< "$groups"
group_name=$(eval "dialog --title \"$(translate "Delete Group")\" --menu \
\"$(translate "Select a group to delete:")\" 20 60 10 \
$menu_options 3>&1 1>&2 2>&3") || return
if dialog --yesno "$(translate "Are you sure you want to delete group:") $group_name ?" 10 60; then
if groupdel "$group_name" 2>/dev/null; then
show_proxmenux_logo
msg_title "$(translate "Deleting Groups")"
msg_ok "$(translate "Group deleted:") $group_name"
else
show_proxmenux_logo
msg_title "$(translate "Deleting Groups")"
msg_ok "$(translate "Group deleted:") $group_name"
msg_error "$(translate "Failed to delete group")"
fi
fi
echo -e
msg_success "$(translate "Press Enter to continue...")"
read -r
}
pmx_manage_groups() {
while true; do
CHOICE=$(dialog --title "$(translate "Shared Groups Manager")" \
--menu "$(translate "Select an option:")" 20 70 10 \
"list" "$(translate "View existing groups")" \
"create" "$(translate "Create new group")" \
"edit" "$(translate "Edit existing group")" \
"delete" "$(translate "Delete a group")" \
"exit" "$(translate "Exit")" \
3>&1 1>&2 2>&3) || return 0
case "$CHOICE" in
list) pmx_list_groups ;;
create) pmx_create_group ;;
edit) pmx_edit_group ;;
delete) pmx_delete_group ;;
exit) return 0 ;;
esac
done
}
pmx_manage_groups

1292
scripts/share/guia.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,93 @@
#!/bin/bash
# ==========================================================
# ProxMenux - Local Shared Directory Manager
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT
# Version : 1.0
# Last Updated: $(date +%d/%m/%Y)
# ==========================================================
# Configuration
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
SHARE_COMMON_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main/scripts/global/share-common.func"
if ! source <(curl -s "$SHARE_COMMON_URL" 2>/dev/null); then
SHARE_COMMON_LOADED=false
else
SHARE_COMMON_LOADED=true
fi
load_language
initialize_cache
# ==========================================================
create_shared_directory() {
SHARED_DIR=$(pmx_select_host_mount_point "$(translate "Select Shared Directory Location")" "/mnt/shared")
[[ -z "$SHARED_DIR" ]] && return
if [[ -d "$SHARED_DIR" ]]; then
if ! whiptail --yesno "$(translate "Directory already exists. Continue with permission setup?")" 10 70 --title "$(translate "Directory Exists")"; then
return
fi
fi
SHARE_GROUP=$(pmx_choose_or_create_group "sharedfiles") || return 1
SHARE_GID=$(pmx_ensure_host_group "$SHARE_GROUP" 101000) || return 1
if command -v setfacl >/dev/null 2>&1; then
setfacl -k /mnt 2>/dev/null || true
setfacl -b /mnt 2>/dev/null || true
fi
chmod 755 /mnt 2>/dev/null || true
pmx_prepare_host_shared_dir "$SHARED_DIR" "$SHARE_GROUP" || return 1
if command -v setfacl >/dev/null 2>&1; then
setfacl -b -R "$SHARED_DIR" 2>/dev/null || true
fi
chown root:"$SHARE_GROUP" "$SHARED_DIR"
chmod 2775 "$SHARED_DIR"
pmx_share_map_set "$SHARED_DIR" "$SHARE_GROUP"
show_proxmenux_logo
msg_title "$(translate "Create Shared Directory")"
echo -e ""
echo -e "${TAB}${BOLD}$(translate "Shared Directory Created:")${CL}"
echo -e "${TAB}${BGN}$(translate "Directory:")${CL} ${BL}$SHARED_DIR${CL}"
echo -e "${TAB}${BGN}$(translate "Group:")${CL} ${BL}$SHARE_GROUP (GID: $SHARE_GID)${CL}"
echo -e "${TAB}${BGN}$(translate "Permissions:")${CL} ${BL}2775 (rwxrwsr-x)${CL}"
echo -e "${TAB}${BGN}$(translate "Owner:")${CL} ${BL}root:$SHARE_GROUP${CL}"
echo -e "${TAB}${BGN}$(translate "ACL Status:")${CL} ${BL}$(translate "Cleaned and set for POSIX inheritance")${CL}"
echo -e ""
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
create_shared_directory

View File

@@ -0,0 +1,480 @@
#!/bin/bash
# ==========================================================
# ProxMenux - LXC Mount Manager
# ==========================================================
# Author : MacRimi
# Copyright : (c) 2024 MacRimi
# License : MIT
# Version : 3.1-enhanced
# Last Updated: $(date +%d/%m/%Y)
# ==========================================================
BASE_DIR="/usr/local/share/proxmenux"
source "$BASE_DIR/utils.sh"
SHARE_COMMON_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main/scripts/global/share-common.func"
if ! source <(curl -s "$SHARE_COMMON_URL" 2>/dev/null); then
SHARE_COMMON_LOADED=false
else
SHARE_COMMON_LOADED=true
fi
load_language
initialize_cache
# ==========================================================
get_container_uid_shift() {
local ctid="$1"
local conf="/etc/pve/lxc/${ctid}.conf"
local uid_shift
if [[ ! -f "$conf" ]]; then
echo "100000"
return 0
fi
local unpriv
unpriv=$(grep "^unprivileged:" "$conf" | awk '{print $2}')
if [[ "$unpriv" == "1" ]]; then
uid_shift=$(grep "^lxc.idmap" "$conf" | grep 'u 0' | awk '{print $5}' | head -1)
echo "${uid_shift:-100000}"
return 0
fi
echo "0"
return 0
}
setup_container_access() {
local ctid="$1" group_name="$2" host_gid="$3" host_dir="$4"
local uid_shift mapped_gid
if [[ ! "$ctid" =~ ^[0-9]+$ ]]; then
msg_error "$(translate 'Invalid container ID format:') $ctid"
return 1
fi
uid_shift=$(get_container_uid_shift "$ctid")
# ===================================================================
# CONTAINER TYPE DETECTION AND STRATEGY
# ===================================================================
if [[ "$uid_shift" -eq 0 ]]; then
msg_ok "$(translate "PRIVILEGED container detected - using direct UID/GID mapping")"
mapped_gid="$host_gid"
container_type="privileged"
else
msg_ok "$(translate "UNPRIVILEGED container detected - using mapped UID/GID")"
mapped_gid=$((uid_shift + host_gid))
container_type="unprivileged"
msg_ok "UID shift: $uid_shift, Host GID: $host_gid → Container GID: $mapped_gid"
fi
# ===================================================================
# STEP 1: ACL TOOLS (only for unprivileged containers)
# ===================================================================
if [[ "$container_type" == "unprivileged" ]]; then
if ! command -v setfacl >/dev/null 2>&1; then
msg_info "$(translate "Installing ACL tools (REQUIRED for unprivileged containers)...")"
apt-get update >/dev/null 2>&1
apt-get install -y acl >/dev/null 2>&1
if command -v setfacl >/dev/null 2>&1; then
msg_ok "$(translate "ACL tools installed successfully")"
else
msg_error "$(translate "Failed to install ACL tools - permissions may not work correctly")"
fi
else
msg_ok "$(translate "ACL tools already available")"
fi
else
msg_ok "$(translate "Privileged container - ACL tools not required (using POSIX permissions)")"
fi
# ===================================================================
# STEP 2: CONTAINER GROUP CONFIGURATION
# ===================================================================
msg_info "$(translate "Configuring container group with") $container_type $(translate "strategy...")"
pct exec "$ctid" -- sh -c "
# Remove existing group if GID is wrong
if getent group $group_name >/dev/null 2>&1; then
current_gid=\$(getent group $group_name | cut -d: -f3)
if [ \"\$current_gid\" != \"$mapped_gid\" ]; then
groupdel $group_name 2>/dev/null || true
fi
fi
# Create group with correct GID
groupadd -g $mapped_gid $group_name 2>/dev/null || true
" 2>/dev/null
msg_ok "$(translate "Container group configured:") $group_name (GID: $mapped_gid)"
# ===================================================================
# STEP 3: USER PROCESSING (different strategies)
# ===================================================================
local container_users
container_users=$(pct exec "$ctid" -- getent passwd | awk -F: '{print $1 ":" $3}' 2>/dev/null)
local users_added=0
local acls_applied=0
if [[ "$container_type" == "privileged" ]]; then
msg_ok "$(translate "Privileged container:") $users_added $(translate "users added to group (no ACLs needed)")"
else
msg_info "$(translate "Using UNPRIVILEGED strategy: mapped UIDs + ACL permissions")"
while IFS=: read -r username ct_uid; do
if [[ -n "$username" && "$ct_uid" =~ ^[0-9]+$ ]]; then
local host_uid=$((uid_shift + ct_uid))
if pct exec "$ctid" -- usermod -aG "$group_name" "$username" 2>/dev/null; then
users_added=$((users_added + 1))
if command -v setfacl >/dev/null 2>&1; then
setfacl -m u:$host_uid:rwx "$host_dir" 2>/dev/null
setfacl -m d:u:$host_uid:rwx "$host_dir" 2>/dev/null
acls_applied=$((acls_applied + 1))
fi
case "$username" in
root|www-data|ncp|nobody|ubuntu|debian)
msg_ok "$(translate "Configured user:") $username (CT_UID:$ct_uid → HOST_UID:$host_uid)"
;;
esac
fi
fi
done <<< "$container_users"
msg_ok "$(translate "Unprivileged container:") $users_added $(translate "users added,") $acls_applied $(translate "ACL entries applied")"
fi
# ===================================================================
# STEP 4: DIRECTORY PERMISSIONS
# ===================================================================
msg_info "$(translate "Setting optimal directory permissions...")"
chmod 2775 "$host_dir" 2>/dev/null || true
chgrp "$group_name" "$host_dir" 2>/dev/null || true
msg_ok "$(translate "Host directory permissions:") 2775 root:$group_name"
# ===================================================================
# STEP 5: VERIFICATION
# ===================================================================
msg_info "$(translate "Verifying configuration...")"
if [[ "$container_type" == "unprivileged" ]] && command -v getfacl >/dev/null 2>&1; then
local acl_count=$(getfacl "$host_dir" 2>/dev/null | grep "^user:" | grep -v "^user::" | wc -l)
msg_ok "$(translate "ACL entries configured:") $acl_count"
# Show sample ACL entries
if [[ $acl_count -gt 0 ]]; then
echo -e "${TAB}${BGN}$(translate " ACL entries:")${CL}"
getfacl "$host_dir" 2>/dev/null | grep "^user:" | grep -v "^user::" | head -3 | while read acl_line; do
echo -e "${TAB} ${BL}$acl_line${CL}"
done
fi
fi
local test_users=("www-data" "root" "ncp" "nobody")
local successful_tests=0
for test_user in "${test_users[@]}"; do
if pct exec "$ctid" -- id "$test_user" >/dev/null 2>&1; then
if pct exec "$ctid" -- su -s /bin/bash "$test_user" -c "ls '$4' >/dev/null 2>&1" 2>/dev/null; then
successful_tests=$((successful_tests + 1))
fi
fi
done
if [[ $successful_tests -gt 0 ]]; then
msg_ok "$(translate "Access verification:") $successful_tests $(translate "users can access mount point")"
fi
if [[ "$container_type" == "privileged" ]]; then
msg_ok "$(translate "PRIVILEGED container configuration completed - using direct POSIX permissions")"
else
msg_ok "$(translate "UNPRIVILEGED container configuration completed - using ACL permissions")"
fi
return 0
}
get_next_mp_index() {
local ctid="$1"
local conf="/etc/pve/lxc/${ctid}.conf"
if [[ ! "$ctid" =~ ^[0-9]+$ ]] || [[ ! -f "$conf" ]]; then
echo "0"
return 0
fi
local used idx next=0
used=$(awk -F: '/^mp[0-9]+:/ {print $1}' "$conf" | sed 's/mp//' | sort -n)
for idx in $used; do
[[ "$idx" -ge "$next" ]] && next=$((idx+1))
done
echo "$next"
}
add_bind_mount() {
local ctid="$1" host_path="$2" ct_path="$3"
local mpidx result
if [[ ! "$ctid" =~ ^[0-9]+$ ]]; then
msg_error "$(translate 'Invalid container ID format:') $ctid"
return 1
fi
if [[ -z "$ctid" || -z "$host_path" || -z "$ct_path" ]]; then
msg_error "$(translate "Missing arguments")"
return 1
fi
if pct config "$ctid" | grep -q "$host_path"; then
echo -e
msg_warn "$(translate "Directory already mounted in container configuration.")"
echo -e ""
msg_success "$(translate 'Press Enter to return to menu...')"
read -r
return 1
fi
mpidx=$(get_next_mp_index "$ctid")
result=$(pct set "$ctid" -mp${mpidx} "$host_path,mp=$ct_path,shared=1,backup=0,acl=1" 2>&1)
if [[ $? -eq 0 ]]; then
msg_ok "$(translate "Successfully mounted:") $host_path$ct_path"
return 0
else
msg_error "$(translate "Error mounting folder:") $result"
return 1
fi
}
mount_host_directory_to_lxc() {
# Step 1: Select container
local container_id
container_id=$(select_lxc_container)
if [[ $? -ne 0 || -z "$container_id" ]]; then
return 1
fi
show_proxmenux_logo
msg_title "$(translate 'Mount Host Directory to LXC Container')"
# Step 1.1: Ensure running
ct_status=$(pct status "$container_id" | awk '{print $2}')
if [[ "$ct_status" != "running" ]]; then
msg_info "$(translate "Starting container") $container_id..."
if pct start "$container_id"; then
sleep 3
msg_ok "$(translate "Container started")"
else
msg_error "$(translate "Failed to start container")"
echo -e ""
msg_success "$(translate 'Press Enter to continue...')"
read -r
return 1
fi
fi
msg_ok "$(translate 'Container selected and running')"
sleep 2
# Step 2: Select host directory
local host_dir
host_dir=$(select_host_directory)
if [[ -z "$host_dir" ]]; then
return 1
fi
msg_ok "$(translate 'Host directory selected')"
# Step 3: Setup group
local group_name="sharedfiles"
local group_gid
group_gid=$(pmx_ensure_host_group "$group_name")
if [[ -z "$group_gid" ]]; then
return 1
fi
# Set basic permissions
chown -R root:"$group_name" "$host_dir" 2>/dev/null || true
chmod -R 2775 "$host_dir" 2>/dev/null || true
msg_ok "$(translate 'Host group configured')"
# Step 4: Select container mount point
local ct_mount_point
ct_mount_point=$(select_container_mount_point "$container_id" "$host_dir")
if [[ -z "$ct_mount_point" ]]; then
return 1
fi
# Step 5: Confirmation
local uid_shift container_type
uid_shift=$(get_container_uid_shift "$container_id")
if [[ "$uid_shift" -eq 0 ]]; then
container_type="$(translate 'Privileged')"
else
container_type="$(translate 'Unprivileged')"
fi
local confirm_msg="$(translate "Mount Configuration:")
$(translate "Container ID:"): $container_id ($container_type)
$(translate "Host Directory:"): $host_dir
$(translate "Container Mount Point:"): $ct_mount_point
$(translate "Shared Group:"): $group_name (GID: $group_gid)
$(translate "Proceed?")"
if ! whiptail --title "$(translate "Confirm Mount")" --yesno "$confirm_msg" 16 70; then
return 1
fi
# Step 6: Add mount
if ! add_bind_mount "$container_id" "$host_dir" "$ct_mount_point"; then
return 1
fi
# Step 7: Setup access (handles both privileged and unprivileged)
setup_container_access "$container_id" "$group_name" "$group_gid" "$host_dir"
# Step 8: Final setup
pct exec "$container_id" -- chgrp "$group_name" "$ct_mount_point" 2>/dev/null || true
pct exec "$container_id" -- chmod 2775 "$ct_mount_point" 2>/dev/null || true
# Step 9: Summary
echo -e ""
echo -e "${TAB}${BOLD}$(translate 'Mount Added Successfully:')${CL}"
echo -e "${TAB}${BGN}$(translate 'Container:')${CL} ${BL}$container_id ($container_type)${CL}"
echo -e "${TAB}${BGN}$(translate 'Host Directory:')${CL} ${BL}$host_dir${CL}"
echo -e "${TAB}${BGN}$(translate 'Mount Point:')${CL} ${BL}$ct_mount_point${CL}"
echo -e "${TAB}${BGN}$(translate 'Group:')${CL} ${BL}$group_name (GID: $group_gid)${CL}"
if [[ "$uid_shift" -eq 0 ]]; then
echo -e "${TAB}${BGN}$(translate 'Permission Strategy:')${CL} ${BL}POSIX (direct mapping)${CL}"
else
echo -e "${TAB}${BGN}$(translate 'Permission Strategy:')${CL} ${BL}ACL (mapped UIDs)${CL}"
fi
echo -e ""
if whiptail --yesno "$(translate "Restart container to activate mount?")" 8 60; then
msg_info "$(translate 'Restarting container...')"
if pct reboot "$container_id"; then
sleep 5
msg_ok "$(translate 'Container restarted successfully')"
echo -e
echo -e "${TAB}${BOLD}$(translate 'Testing access and read/write:')${CL}"
test_user=$(pct exec "$container_id" -- sh -c "id -u ncp >/dev/null 2>&1 && echo ncp || echo www-data")
if pct exec "$container_id" -- su -s /bin/bash $test_user -c "touch $ct_mount_point/test_access.txt" 2>/dev/null; then
msg_ok "$(translate "Mount access and read/write successful (tested as $test_user)")"
rm -f "$host_dir/test_access.txt" 2>/dev/null || true
else
msg_warn "$(translate "⚠ Access test failed - check permissions (user: $test_user)")"
fi
else
msg_warn "$(translate 'Failed to restart - restart manually')"
fi
fi
echo -e ""
msg_success "$(translate 'Press Enter to continue...')"
read -r
}
# Main menu
main_menu() {
while true; do
choice=$(dialog --title "$(translate 'LXC Mount Manager')" \
--menu "\n$(translate 'Choose an option:')" 25 80 15 \
"1" "$(translate 'Mount Host Directory to LXC')" \
"2" "$(translate 'View Mount Points')" \
"3" "$(translate 'Remove Mount Point')" \
"4" "$(translate 'Exit')" 3>&1 1>&2 2>&3)
case $choice in
1)
mount_host_directory_to_lxc
;;
2)
msg_info2 "$(translate 'Feature coming soon...')"
read -p "$(translate 'Press Enter to continue...')"
;;
3)
msg_info2 "$(translate 'Feature coming soon...')"
read -p "$(translate 'Press Enter to continue...')"
;;
4|"")
exit 0
;;
esac
done
}
#main_menu
mount_host_directory_to_lxc

File diff suppressed because it is too large Load Diff

687
scripts/share/nfs_client.sh Normal file
View File

@@ -0,0 +1,687 @@
#!/bin/bash
# ==========================================================
# ProxMenux CT - NFS Client Manager for Proxmox LXC
# ==========================================================
# Based on ProxMenux by MacRimi
# ==========================================================
# Description:
# This script allows you to manage NFS client mounts inside Proxmox CTs:
# - Mount NFS shares (temporary and permanent)
# - View current mounts
# - Unmount and remove NFS shares
# - Auto-discover NFS servers
# ==========================================================
# Configuration
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
# Load shared functions
SHARE_COMMON_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main/scripts/global/share-common.func"
if ! source <(curl -s "$SHARE_COMMON_URL" 2>/dev/null); then
msg_error "$(translate "Could not load shared functions. Script cannot continue.")"
exit 1
fi
load_language
initialize_cache
select_privileged_lxc
install_nfs_client() {
if pct exec "$CTID" -- dpkg -s nfs-common &>/dev/null; then
return 0
fi
show_proxmenux_logo
msg_title "$(translate "Installing NFS Client in LXC")"
msg_info "$(translate "Installing NFS client packages...")"
if ! pct exec "$CTID" -- apt-get update >/dev/null 2>&1; then
msg_error "$(translate "Failed to update package list.")"
msg_success "$(translate "Press Enter to return to menu...")"
read -r
return 1
fi
if ! pct exec "$CTID" -- apt-get install -y nfs-common >/dev/null 2>&1; then
msg_error "$(translate "Failed to install NFS client packages.")"
msg_success "$(translate "Press Enter to return to menu...")"
read -r
return 1
fi
if ! pct exec "$CTID" -- which showmount >/dev/null 2>&1; then
msg_error "$(translate "showmount command not found after installation.")"
msg_success "$(translate "Press Enter to return to menu...")"
read -r
return 1
fi
if ! pct exec "$CTID" -- which mount.nfs >/dev/null 2>&1; then
msg_error "$(translate "mount.nfs command not found after installation.")"
msg_success "$(translate "Press Enter to return to menu...")"
read -r
return 1
fi
msg_ok "$(translate "NFS client installed successfully.")"
return 0
}
discover_nfs_servers() {
show_proxmenux_logo
msg_title "$(translate "Mount NFS Client in LXC")"
msg_info "$(translate "Scanning network for NFS servers...")"
HOST_IP=$(hostname -I | awk '{print $1}')
NETWORK=$(echo "$HOST_IP" | cut -d. -f1-3).0/24
if ! which nmap >/dev/null 2>&1; then
apt-get install -y nmap &>/dev/null
fi
SERVERS=$(nmap -p 2049 --open "$NETWORK" 2>/dev/null | grep -B 4 "2049/tcp open" | grep "Nmap scan report" | awk '{print $5}' | sort -u || true)
if [[ -z "$SERVERS" ]]; then
cleanup
whiptail --title "$(translate "No Servers Found")" --msgbox "$(translate "No NFS servers found on the network.")\n\n$(translate "You can add servers manually.")" 10 60
return 1
fi
OPTIONS=()
while IFS= read -r server; do
if [[ -n "$server" ]]; then
EXPORTS_COUNT=$(showmount -e "$server" 2>/dev/null | tail -n +2 | wc -l || echo "0")
SERVER_INFO="NFS Server ($EXPORTS_COUNT exports)"
OPTIONS+=("$server" "$SERVER_INFO")
fi
done <<< "$SERVERS"
if [[ ${#OPTIONS[@]} -eq 0 ]]; then
cleanup
whiptail --title "$(translate "No Valid Servers")" --msgbox "$(translate "No accessible NFS servers found.")" 8 50
return 1
fi
msg_ok "$(translate "NFS servers detected")"
NFS_SERVER=$(whiptail --title "$(translate "Select NFS Server")" --menu "$(translate "Choose an NFS server:")" 20 80 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3)
[[ -n "$NFS_SERVER" ]] && return 0 || return 1
}
select_nfs_server() {
METHOD=$(whiptail --backtitle "ProxMenux" --title "$(translate "NFS Server Selection")" --menu "$(translate "How do you want to select the NFS server?")" 15 70 3 \
"auto" "$(translate "Auto-discover servers on network")" \
"manual" "$(translate "Enter server IP/hostname manually")" 3>&1 1>&2 2>&3)
case "$METHOD" in
auto)
discover_nfs_servers || return 1
;;
manual)
NFS_SERVER=$(whiptail --inputbox "$(translate "Enter NFS server IP or hostname:")" 10 60 --title "$(translate "NFS Server")" 3>&1 1>&2 2>&3)
[[ -z "$NFS_SERVER" ]] && return 1
;;
*)
return 1
;;
esac
return 0
}
select_nfs_export() {
if ! pct exec "$CTID" -- which showmount >/dev/null 2>&1; then
whiptail --title "$(translate "NFS Client Error")" \
--msgbox "$(translate "showmount command is not working properly.")\n\n$(translate "Please check the installation.")" \
10 60
return 1
fi
if ! pct exec "$CTID" -- ping -c 1 -W 3 "$NFS_SERVER" >/dev/null 2>&1; then
whiptail --title "$(translate "Connection Error")" \
--msgbox "$(translate "Cannot reach server") $NFS_SERVER\n\n$(translate "Please check:")\n• $(translate "Server IP/hostname is correct")\n• $(translate "Network connectivity")\n• $(translate "Server is online")" \
12 70
return 1
fi
if ! pct exec "$CTID" -- nc -z -w 3 "$NFS_SERVER" 2049 2>/dev/null; then
whiptail --title "$(translate "NFS Port Error")" \
--msgbox "$(translate "NFS port (2049) is not accessible on") $NFS_SERVER\n\n$(translate "Please check:")\n• $(translate "NFS server is running")\n• $(translate "Firewall settings")\n• $(translate "NFS service is enabled")" \
12 70
return 1
fi
EXPORTS_OUTPUT=$(pct exec "$CTID" -- showmount -e "$NFS_SERVER" 2>&1)
EXPORTS_RESULT=$?
if [[ $EXPORTS_RESULT -ne 0 ]]; then
ERROR_MSG=$(echo "$EXPORTS_OUTPUT" | grep -i "error\|failed\|denied" | head -1)
if echo "$EXPORTS_OUTPUT" | grep -qi "connection refused\|network unreachable"; then
whiptail --title "$(translate "Network Error")" \
--msgbox "$(translate "Network connection failed to") $NFS_SERVER\n\n$(translate "Error:"): $ERROR_MSG\n\n$(translate "Please check:")\n• $(translate "Server is running")\n• $(translate "Network connectivity")\n• $(translate "Firewall settings")" \
14 80
else
whiptail --title "$(translate "NFS Error")" \
--msgbox "$(translate "Failed to connect to") $NFS_SERVER\n\n$(translate "Error:"): $ERROR_MSG" \
12 80
fi
return 1
fi
EXPORTS=$(echo "$EXPORTS_OUTPUT" | tail -n +2 | awk '{print $1}' | grep -v "^$")
if [[ -z "$EXPORTS" ]]; then
whiptail --title "$(translate "No Exports Found")" \
--msgbox "$(translate "No exports found on server") $NFS_SERVER\n\n$(translate "Server response:")\n$(echo "$EXPORTS_OUTPUT" | head -10)\n\n$(translate "You can enter the export path manually.")" \
16 80
NFS_EXPORT=$(whiptail --inputbox "$(translate "Enter NFS export path (e.g., /mnt/shared):")" 10 60 --title "$(translate "Export Path")" 3>&1 1>&2 2>&3)
[[ -z "$NFS_EXPORT" ]] && return 1
return 0
fi
# Build options for whiptail
OPTIONS=()
while IFS= read -r export_line; do
if [[ -n "$export_line" ]]; then
EXPORT_PATH=$(echo "$export_line" | awk '{print $1}')
# Get allowed clients if available
CLIENTS=$(echo "$EXPORTS_OUTPUT" | grep "^$EXPORT_PATH" | awk '{for(i=2;i<=NF;i++) printf "%s ", $i; print ""}' | sed 's/[[:space:]]*$//')
if [[ -n "$CLIENTS" ]]; then
OPTIONS+=("$EXPORT_PATH" "$CLIENTS")
else
OPTIONS+=("$EXPORT_PATH" "$(translate "NFS export")")
fi
fi
done <<< "$EXPORTS"
if [[ ${#OPTIONS[@]} -eq 0 ]]; then
whiptail --title "$(translate "No Available Exports")" \
--msgbox "$(translate "No accessible exports found.")\n\n$(translate "You can enter the export path manually.")" \
10 70
NFS_EXPORT=$(whiptail --inputbox "$(translate "Enter NFS export path (e.g., /mnt/shared):")" 10 60 --title "$(translate "Export Path")" 3>&1 1>&2 2>&3)
[[ -n "$NFS_EXPORT" ]] && return 0 || return 1
fi
NFS_EXPORT=$(whiptail --title "$(translate "Select NFS Export")" --menu "$(translate "Choose an export to mount:")" 20 70 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3)
[[ -n "$NFS_EXPORT" ]] && return 0 || return 1
}
select_mount_point() {
while true; do
METHOD=$(whiptail --title "$(translate "Select Mount Point")" --menu "$(translate "Where do you want to mount the NFS export?")" 15 70 3 \
"1" "$(translate "Create new folder in /mnt")" \
"2" "$(translate "Select from existing folders in /mnt")" \
"3" "$(translate "Enter custom path")" 3>&1 1>&2 2>&3)
case "$METHOD" in
1)
# Create default name from server and export
EXPORT_NAME=$(basename "$NFS_EXPORT")
DEFAULT_NAME="nfs_${NFS_SERVER}_${EXPORT_NAME}"
FOLDER_NAME=$(whiptail --inputbox "$(translate "Enter new folder name:")" 10 60 "$DEFAULT_NAME" --title "$(translate "New Folder in /mnt")" 3>&1 1>&2 2>&3)
if [[ -n "$FOLDER_NAME" ]]; then
MOUNT_POINT="/mnt/$FOLDER_NAME"
return 0
fi
;;
2)
DIRS=$(pct exec "$CTID" -- find /mnt -maxdepth 1 -mindepth 1 -type d 2>/dev/null)
if [[ -z "$DIRS" ]]; then
whiptail --title "$(translate "No Folders")" --msgbox "$(translate "No folders found in /mnt. Please create a new folder.")" 8 60
continue
fi
OPTIONS=()
while IFS= read -r dir; do
if [[ -n "$dir" ]]; then
name=$(basename "$dir")
if pct exec "$CTID" -- [ "$(ls -A "$dir" 2>/dev/null | wc -l)" -eq 0 ]; then
status="$(translate "Empty")"
else
status="$(translate "Contains files")"
fi
OPTIONS+=("$dir" "$name ($status)")
fi
done <<< "$DIRS"
MOUNT_POINT=$(whiptail --title "$(translate "Select Existing Folder")" --menu "$(translate "Choose a folder to mount the export:")" 20 80 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3)
if [[ -n "$MOUNT_POINT" ]]; then
if pct exec "$CTID" -- [ "$(ls -A "$MOUNT_POINT" 2>/dev/null | wc -l)" -gt 0 ]; then
FILE_COUNT=$(pct exec "$CTID" -- ls -A "$MOUNT_POINT" 2>/dev/null | wc -l)
if ! whiptail --yesno "$(translate "WARNING: The selected directory is not empty!")\n\n$(translate "Directory:"): $MOUNT_POINT\n$(translate "Contains:"): $FILE_COUNT $(translate "files/folders")\n\n$(translate "Mounting here will hide existing files until unmounted.")\n\n$(translate "Do you want to continue?")" 14 70 --title "$(translate "Directory Not Empty")"; then
continue
fi
fi
return 0
fi
;;
3)
MOUNT_POINT=$(whiptail --inputbox "$(translate "Enter full path for mount point:")" 10 70 "/mnt/nfs_share" --title "$(translate "Custom Path")" 3>&1 1>&2 2>&3)
if [[ -n "$MOUNT_POINT" ]]; then
return 0
fi
;;
*)
return 1
;;
esac
done
}
configure_mount_options() {
MOUNT_TYPE=$(whiptail --title "$(translate "Mount Options")" --menu "$(translate "Select mount configuration:")" 15 70 4 \
"1" "$(translate "Default options read/write")" \
"2" "$(translate "Read-only mount")" \
"3" "$(translate "Custom options")" 3>&1 1>&2 2>&3)
case "$MOUNT_TYPE" in
1)
MOUNT_OPTIONS="rw,hard,rsize=1048576,wsize=1048576,timeo=600,retrans=2"
;;
2)
MOUNT_OPTIONS="ro,hard,rsize=1048576,wsize=1048576,timeo=600,retrans=2"
;;
3)
MOUNT_OPTIONS=$(whiptail --inputbox "$(translate "Enter custom mount options:")" \
10 70 "rw,hard,rsize=1048576,wsize=1048576,timeo=600,retrans=2" \
--title "$(translate "Custom Options")" 3>&1 1>&2 2>&3)
[[ $? -ne 0 ]] && return 1
[[ -z "$MOUNT_OPTIONS" ]] && MOUNT_OPTIONS="rw,hard"
;;
*)
MOUNT_OPTIONS="rw,hard,rsize=65536,wsize=65536,timeo=600,retrans=2"
;;
esac
if whiptail --yesno "$(translate "Do you want to make this mount permanent?")\n\n$(translate "This will add the mount to /etc/fstab so it persists after reboot.")" 10 70 --title "$(translate "Permanent Mount")"; then
PERMANENT_MOUNT=true
else
PERMANENT_MOUNT=false
fi
}
validate_export_exists() {
local server="$1"
local export="$2"
VALIDATION_OUTPUT=$(pct exec "$CTID" -- showmount -e "$server" 2>/dev/null | grep "^$export[[:space:]]")
if [[ -n "$VALIDATION_OUTPUT" ]]; then
return 0
else
show_proxmenux_logo
echo -e
msg_error "$(translate "Export not found on server:") $export"
msg_success "$(translate "Press Enter to return to menu...")"
read -r
return 1
fi
}
mount_nfs_share() {
# Step 0: Install NFS client first
install_nfs_client || return
# Step 1: Select server
select_nfs_server || return
show_proxmenux_logo
msg_title "$(translate "Mount NFS Share on Host")"
msg_ok "$(translate "NFS server Selected")"
# Step 2: Select export
select_nfs_export || return
msg_ok "$(translate "NFS export Selected")"
# Step 2.5: Validate export exists
if ! validate_export_exists "$NFS_SERVER" "$NFS_EXPORT"; then
echo -e ""
msg_error "$(translate "Cannot proceed with invalid export path.")"
msg_success "$(translate "Press Enter to return to menu...")"
read -r
return
fi
# Step 3: Select mount point
select_mount_point || return
# Step 4: Configure mount options
configure_mount_options || return
if ! pct exec "$CTID" -- test -d "$MOUNT_POINT"; then
if pct exec "$CTID" -- mkdir -p "$MOUNT_POINT"; then
msg_ok "$(translate "Mount point created.")"
else
msg_error "$(translate "Failed to create mount point.")"
return 1
fi
fi
if pct exec "$CTID" -- mount | grep -q "$MOUNT_POINT"; then
msg_warn "$(translate "Something is already mounted at") $MOUNT_POINT"
if ! whiptail --yesno "$(translate "Do you want to unmount it first?")" 8 60 --title "$(translate "Already Mounted")"; then
return
fi
pct exec "$CTID" -- umount "$MOUNT_POINT" 2>/dev/null || true
fi
# Build mount command
NFS_PATH="$NFS_SERVER:$NFS_EXPORT"
msg_info "$(translate "Testing NFS connection...")"
if pct exec "$CTID" -- mount -t nfs -o "$MOUNT_OPTIONS" "$NFS_PATH" "$MOUNT_POINT"; then
msg_ok "$(translate "NFS share mounted successfully!")"
# Test write access
if pct exec "$CTID" -- touch "$MOUNT_POINT/.test_write" 2>/dev/null; then
pct exec "$CTID" -- rm "$MOUNT_POINT/.test_write" 2>/dev/null
msg_ok "$(translate "Write access confirmed.")"
else
msg_warn "$(translate "Read-only access (or no write permissions).")"
fi
# Add to fstab if permanent
if [[ "$PERMANENT_MOUNT" == "true" ]]; then
pct exec "$CTID" -- sed -i "\|$MOUNT_POINT|d" /etc/fstab
FSTAB_ENTRY="$NFS_PATH $MOUNT_POINT nfs $MOUNT_OPTIONS 0 0"
pct exec "$CTID" -- bash -c "echo '$FSTAB_ENTRY' >> /etc/fstab"
msg_ok "$(translate "Added to /etc/fstab for permanent mounting.")"
fi
# Show mount information
echo -e ""
echo -e "${TAB}${BOLD}$(translate "Mount Information:")${CL}"
echo -e "${TAB}${BGN}$(translate "Server:")${CL} ${BL}$NFS_SERVER${CL}"
echo -e "${TAB}${BGN}$(translate "Export:")${CL} ${BL}$NFS_EXPORT${CL}"
echo -e "${TAB}${BGN}$(translate "Mount Point:")${CL} ${BL}$MOUNT_POINT${CL}"
echo -e "${TAB}${BGN}$(translate "Options:")${CL} ${BL}$MOUNT_OPTIONS${CL}"
echo -e "${TAB}${BGN}$(translate "Permanent:")${CL} ${BL}$PERMANENT_MOUNT${CL}"
else
msg_error "$(translate "Failed to mount NFS share.")"
echo -e "${TAB}$(translate "Please check:")"
echo -e "${TAB}$(translate "Server is accessible:"): $NFS_SERVER"
echo -e "${TAB}$(translate "Export exists:"): $NFS_EXPORT"
echo -e "${TAB}$(translate "Network connectivity")"
echo -e "${TAB}$(translate "NFS server is running")"
echo -e "${TAB}$(translate "Export permissions allow access")"
fi
echo -e ""
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
view_nfs_mounts() {
show_proxmenux_logo
msg_title "$(translate "Current NFS Mounts")"
echo -e "$(translate "NFS mounts in CT") $CTID:"
echo "=================================="
# Show currently mounted NFS shares - VERSIÓN CORREGIDA
CURRENT_MOUNTS=$(pct exec "$CTID" -- mount | grep -E "type nfs|:.*on.*nfs" 2>/dev/null || true)
if [[ -n "$CURRENT_MOUNTS" ]]; then
echo -e "${BOLD}$(translate "Currently Mounted:")${CL}"
echo "$CURRENT_MOUNTS"
echo ""
else
# Verificar si hay montajes NFS en fstab que estén activos
ACTIVE_NFS_MOUNTS=$(pct exec "$CTID" -- grep "nfs" /etc/fstab 2>/dev/null | grep -v "^#" | while read -r line; do
MOUNT_POINT=$(echo "$line" | awk '{print $2}')
if pct exec "$CTID" -- mount | grep -q "$MOUNT_POINT"; then
echo "$MOUNT_POINT"
fi
done)
if [[ -n "$ACTIVE_NFS_MOUNTS" ]]; then
echo -e "${BOLD}$(translate "Currently Mounted:")${CL}"
while IFS= read -r mount_point; do
if [[ -n "$mount_point" ]]; then
MOUNT_INFO=$(pct exec "$CTID" -- mount | grep "$mount_point")
echo "$MOUNT_INFO"
fi
done <<< "$ACTIVE_NFS_MOUNTS"
echo ""
else
echo "$(translate "No NFS shares currently mounted.")"
echo ""
fi
fi
# Show fstab entries
FSTAB_NFS=$(pct exec "$CTID" -- grep "nfs" /etc/fstab 2>/dev/null || true)
if [[ -n "$FSTAB_NFS" ]]; then
echo -e "${BOLD}$(translate "Permanent Mounts (fstab):")${CL}"
echo "$FSTAB_NFS"
echo ""
echo -e "${TAB}${BOLD}$(translate "Mount Details:")${CL}"
while IFS= read -r fstab_line; do
if [[ -n "$fstab_line" && ! "$fstab_line" =~ ^# ]]; then
NFS_PATH=$(echo "$fstab_line" | awk '{print $1}')
MOUNT_POINT=$(echo "$fstab_line" | awk '{print $2}')
OPTIONS=$(echo "$fstab_line" | awk '{print $4}')
# Extract server and export from NFS path
SERVER=$(echo "$NFS_PATH" | cut -d: -f1)
EXPORT=$(echo "$NFS_PATH" | cut -d: -f2)
echo -e "${TAB}${BGN}$(translate "Server:")${CL} ${BL}$SERVER${CL}"
echo -e "${TAB}${BGN}$(translate "Export:")${CL} ${BL}$EXPORT${CL}"
echo -e "${TAB}${BGN}$(translate "Mount Point:")${CL} ${BL}$MOUNT_POINT${CL}"
echo -e "${TAB}${BGN}$(translate "Options:")${CL} ${BL}$OPTIONS${CL}"
# Check if currently mounted
if pct exec "$CTID" -- mount | grep -q "$MOUNT_POINT"; then
echo -e "${TAB}${BGN}$(translate "Status:")${CL} ${GN}$(translate "Mounted")${CL}"
else
echo -e "${TAB}${BGN}$(translate "Status:")${CL} ${RD}$(translate "Not Mounted")${CL}"
fi
echo ""
fi
done <<< "$FSTAB_NFS"
else
echo "$(translate "No permanent NFS mounts configured.")"
fi
echo ""
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
unmount_nfs_share() {
# Get current NFS mounts
MOUNTS=$(pct exec "$CTID" -- mount | grep -E "type nfs|:.*on.*nfs" | awk '{print $3}' | sort -u || true)
FSTAB_MOUNTS=$(pct exec "$CTID" -- grep -E "nfs" /etc/fstab 2>/dev/null | grep -v "^#" | awk '{print $2}' | sort -u || true)
# Combine and deduplicate
ALL_MOUNTS=$(echo -e "$MOUNTS\n$FSTAB_MOUNTS" | sort -u | grep -v "^$" || true)
if [[ -z "$ALL_MOUNTS" ]]; then
dialog --backtitle "ProxMenux" --title "$(translate "No Mounts")" --msgbox "\n$(translate "No NFS mounts found.")" 8 50
return
fi
OPTIONS=()
while IFS= read -r mount_point; do
[[ -n "$mount_point" ]] && OPTIONS+=("$mount_point" "")
done <<< "$ALL_MOUNTS"
SELECTED_MOUNT=$(dialog --backtitle "ProxMenux" --title "$(translate "Unmount NFS Share")" --menu "$(translate "Select mount point to unmount:")" 20 80 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3)
[[ -z "$SELECTED_MOUNT" ]] && return
if whiptail --yesno "$(translate "Are you sure you want to unmount this NFS share?")\n\n$(translate "Mount Point:"): $SELECTED_MOUNT\n\n$(translate "This will remove the mount from /etc/fstab.")" 12 80 --title "$(translate "Confirm Unmount")"; then
show_proxmenux_logo
msg_title "$(translate "Unmount NFS Share")"
# Remove from fstab
pct exec "$CTID" -- sed -i "\|[[:space:]]$SELECTED_MOUNT[[:space:]]|d" /etc/fstab
msg_ok "$(translate "Removed from /etc/fstab.")"
echo -e ""
msg_ok "$(translate "NFS share unmount successfully. Reboot LXC required to take effect.")"
fi
echo -e ""
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
test_nfs_connectivity() {
show_proxmenux_logo
msg_title "$(translate "Test NFS Connectivity")"
echo -e "$(translate "NFS Client Status in CT") $CTID:"
echo "=================================="
# Check if NFS client is installed
if pct exec "$CTID" -- dpkg -s nfs-common &>/dev/null; then
echo "$(translate "NFS Client: INSTALLED")"
# Check showmount
if pct exec "$CTID" -- which showmount >/dev/null 2>&1; then
echo "$(translate "NFS Client Tools: AVAILABLE")"
else
echo "$(translate "NFS Client Tools: NOT AVAILABLE")"
fi
# Check rpcbind service
if pct exec "$CTID" -- systemctl is-active --quiet rpcbind 2>/dev/null; then
echo "$(translate "RPC Bind Service: RUNNING")"
else
echo "$(translate "RPC Bind Service: STOPPED")"
msg_warn "$(translate "Starting rpcbind service...")"
pct exec "$CTID" -- systemctl start rpcbind 2>/dev/null || true
fi
echo ""
echo "$(translate "Current NFS mounts:")"
CURRENT_MOUNTS=$(pct exec "$CTID" -- mount | grep -E "type nfs|:.*on.*nfs" 2>/dev/null || true)
if [[ -n "$CURRENT_MOUNTS" ]]; then
echo "$CURRENT_MOUNTS"
else
# Check for active NFS mounts from fstab
ACTIVE_NFS_MOUNTS=$(pct exec "$CTID" -- grep "nfs" /etc/fstab 2>/dev/null | grep -v "^#" | while read -r line; do
MOUNT_POINT=$(echo "$line" | awk '{print $2}')
if pct exec "$CTID" -- mount | grep -q "$MOUNT_POINT"; then
pct exec "$CTID" -- mount | grep "$MOUNT_POINT"
fi
done)
if [[ -n "$ACTIVE_NFS_MOUNTS" ]]; then
echo "$ACTIVE_NFS_MOUNTS"
else
echo "$(translate "No NFS mounts active.")"
fi
fi
echo ""
echo "$(translate "Testing network connectivity...")"
# Test connectivity to known NFS servers from fstab
FSTAB_SERVERS=$(pct exec "$CTID" -- grep "nfs" /etc/fstab 2>/dev/null | awk '{print $1}' | cut -d: -f1 | sort -u || true)
if [[ -n "$FSTAB_SERVERS" ]]; then
while IFS= read -r server; do
if [[ -n "$server" ]]; then
echo -n "$(translate "Testing") $server: "
if pct exec "$CTID" -- ping -c 1 -W 2 "$server" >/dev/null 2>&1; then
echo -e "\033[1;92m$(translate "Reachable")\033[0m"
# Test NFS port
echo -n " $(translate "NFS port 2049"): "
if pct exec "$CTID" -- nc -z -w 2 "$server" 2049 2>/dev/null; then
echo -e "\033[1;92m$(translate "Open")\033[0m"
else
echo -e "\033[1;91m$(translate "Closed")\033[0m"
fi
# Try to list exports
echo -n " $(translate "Export list test"): "
if pct exec "$CTID" -- showmount -e "$server" >/dev/null 2>&1; then
echo -e "\033[1;92m$(translate "Available")\033[0m"
else
echo -e "\033[1;91m$(translate "Failed")\033[0m"
fi
else
echo -e "\033[1;91m$(translate "Unreachable")\033[0m"
fi
fi
done <<< "$FSTAB_SERVERS"
else
echo "$(translate "No NFS servers configured to test.")"
fi
else
echo "$(translate "NFS Client: NOT INSTALLED")"
echo ""
echo "$(translate "Run 'Mount NFS Share' to install NFS client automatically.")"
fi
echo ""
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
# === Main Menu ===
while true; do
CHOICE=$(dialog --backtitle "ProxMenux" --title "$(translate "NFS Client Manager - CT") $CTID" \
--menu "$(translate "Choose an option:")" 20 70 12 \
"1" "$(translate "Mount NFS Share")" \
"2" "$(translate "View Current Mounts")" \
"3" "$(translate "Unmount NFS Share")" \
"4" "$(translate "Test NFS Connectivity")" \
"5" "$(translate "Exit")" \
3>&1 1>&2 2>&3)
RETVAL=$?
if [[ $RETVAL -ne 0 ]]; then
exit 0
fi
case $CHOICE in
1) mount_nfs_share ;;
2) view_nfs_mounts ;;
3) unmount_nfs_share ;;
4) test_nfs_connectivity ;;
5) exit 0 ;;
*) exit 0 ;;
esac
done

852
scripts/share/nfs_host.sh Normal file
View File

@@ -0,0 +1,852 @@
#!/bin/bash
# ==========================================================
# ProxMenux Host - NFS Host Manager for Proxmox Host
# ==========================================================
# Based on ProxMenux by MacRimi
# ==========================================================
# Description:
# This script allows you to manage NFS client mounts on Proxmox Host:
# - Mount external NFS shares on the host
# - Configure permanent mounts
# - Auto-discover NFS servers
# - Integrate with Proxmox storage system
# ==========================================================
# Configuration
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
load_language
initialize_cache
# Load common share functions
SHARE_COMMON_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main/scripts/global/share-common.func"
if ! source <(curl -s "$SHARE_COMMON_URL" 2>/dev/null); then
msg_warn "$(translate "Could not load shared functions. Using fallback methods.")"
SHARE_COMMON_LOADED=false
else
SHARE_COMMON_LOADED=true
fi
discover_nfs_servers() {
show_proxmenux_logo
msg_title "$(translate "Mount NFS Share on Host")"
msg_info "$(translate "Scanning network for NFS servers...")"
HOST_IP=$(hostname -I | awk '{print $1}')
NETWORK=$(echo "$HOST_IP" | cut -d. -f1-3).0/24
if ! which nmap >/dev/null 2>&1; then
apt-get install -y nmap &>/dev/null
fi
SERVERS=$(nmap -p 2049 --open "$NETWORK" 2>/dev/null | grep -B 4 "2049/tcp open" | grep "Nmap scan report" | awk '{print $5}' | sort -u || true)
if [[ -z "$SERVERS" ]]; then
cleanup
dialog --clear --title "$(translate "No Servers Found")" --msgbox "$(translate "No NFS servers found on the network.")\n\n$(translate "You can add servers manually.")" 10 60
return 1
fi
OPTIONS=()
while IFS= read -r server; do
if [[ -n "$server" ]]; then
EXPORTS_COUNT=$(showmount -e "$server" 2>/dev/null | tail -n +2 | wc -l || echo "0")
SERVER_INFO="NFS Server ($EXPORTS_COUNT exports)"
OPTIONS+=("$server" "$SERVER_INFO")
fi
done <<< "$SERVERS"
if [[ ${#OPTIONS[@]} -eq 0 ]]; then
cleanup
dialog --clear --title "$(translate "No Valid Servers")" --msgbox "$(translate "No accessible NFS servers found.")" 8 50
return 1
fi
cleanup
NFS_SERVER=$(whiptail --backtitle "ProxMenux" --title "$(translate "Select NFS Server")" --menu "$(translate "Choose an NFS server:")" 20 80 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3)
[[ -n "$NFS_SERVER" ]] && return 0 || return 1
}
select_nfs_server() {
METHOD=$(dialog --backtitle "ProxMenux" --title "$(translate "NFS Server Selection")" --menu "$(translate "How do you want to select the NFS server?")" 15 70 3 \
"auto" "$(translate "Auto-discover servers on network")" \
"manual" "$(translate "Enter server IP/hostname manually")" \
"recent" "$(translate "Select from recent servers")" 3>&1 1>&2 2>&3)
case "$METHOD" in
auto)
discover_nfs_servers || return 1
;;
manual)
clear
NFS_SERVER=$(whiptail --inputbox "$(translate "Enter NFS server IP or hostname:")" 10 60 --title "$(translate "NFS Server")" 3>&1 1>&2 2>&3)
[[ -z "$NFS_SERVER" ]] && return 1
;;
recent)
clear
RECENT=$(grep "nfs" /etc/fstab 2>/dev/null | awk '{print $1}' | cut -d: -f1 | sort -u || true)
if [[ -z "$RECENT" ]]; then
dialog --title "$(translate "No Recent Servers")" --msgbox "\n$(translate "No recent NFS servers found.")" 8 50
return 1
fi
OPTIONS=()
while IFS= read -r server; do
[[ -n "$server" ]] && OPTIONS+=("$server" "$(translate "Recent NFS server")")
done <<< "$RECENT"
NFS_SERVER=$(whiptail --title "$(translate "Recent NFS Servers")" --menu "$(translate "Choose a recent server:")" 20 70 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3)
[[ -n "$NFS_SERVER" ]] && return 0 || return 1
;;
*)
return 1
;;
esac
return 0
}
select_nfs_export() {
if ! which showmount >/dev/null 2>&1; then
whiptail --title "$(translate "NFS Client Error")" \
--msgbox "$(translate "showmount command is not working properly.")\n\n$(translate "Please check the installation.")" \
10 60
return 1
fi
if ! ping -c 1 -W 3 "$NFS_SERVER" >/dev/null 2>&1; then
whiptail --title "$(translate "Connection Error")" \
--msgbox "$(translate "Cannot reach server") $NFS_SERVER\n\n$(translate "Please check:")\n• $(translate "Server IP/hostname is correct")\n• $(translate "Network connectivity")\n• $(translate "Server is online")" \
12 70
return 1
fi
if ! nc -z -w 3 "$NFS_SERVER" 2049 2>/dev/null; then
whiptail --title "$(translate "NFS Port Error")" \
--msgbox "$(translate "NFS port (2049) is not accessible on") $NFS_SERVER\n\n$(translate "Please check:")\n• $(translate "NFS server is running")\n• $(translate "Firewall settings")\n• $(translate "NFS service is enabled")" \
12 70
return 1
fi
EXPORTS_OUTPUT=$(showmount -e "$NFS_SERVER" 2>&1)
EXPORTS_RESULT=$?
if [[ $EXPORTS_RESULT -ne 0 ]]; then
ERROR_MSG=$(echo "$EXPORTS_OUTPUT" | grep -i "error\|failed\|denied" | head -1)
if echo "$EXPORTS_OUTPUT" | grep -qi "connection refused\|network unreachable"; then
whiptail --title "$(translate "Network Error")" \
--msgbox "$(translate "Network connection failed to") $NFS_SERVER\n\n$(translate "Error:"): $ERROR_MSG\n\n$(translate "Please check:")\n• $(translate "Server is running")\n• $(translate "Network connectivity")\n• $(translate "Firewall settings")" \
14 80
else
whiptail --title "$(translate "NFS Error")" \
--msgbox "$(translate "Failed to connect to") $NFS_SERVER\n\n$(translate "Error:"): $ERROR_MSG" \
12 80
fi
return 1
fi
EXPORTS=$(echo "$EXPORTS_OUTPUT" | tail -n +2 | awk '{print $1}' | grep -v "^$")
if [[ -z "$EXPORTS" ]]; then
whiptail --title "$(translate "No Exports Found")" \
--msgbox "$(translate "No exports found on server") $NFS_SERVER\n\n$(translate "Server response:")\n$(echo "$EXPORTS_OUTPUT" | head -10)\n\n$(translate "You can enter the export path manually.")" \
16 80
NFS_EXPORT=$(whiptail --inputbox "$(translate "Enter NFS export path (e.g., /mnt/shared):")" 10 60 --title "$(translate "Export Path")" 3>&1 1>&2 2>&3)
[[ -z "$NFS_EXPORT" ]] && return 1
return 0
fi
OPTIONS=()
while IFS= read -r export_line; do
if [[ -n "$export_line" ]]; then
EXPORT_PATH=$(echo "$export_line" | awk '{print $1}')
CLIENTS=$(echo "$EXPORTS_OUTPUT" | grep "^$EXPORT_PATH" | awk '{for(i=2;i<=NF;i++) printf "%s ", $i; print ""}' | sed 's/[[:space:]]*$//')
if [[ -n "$CLIENTS" ]]; then
OPTIONS+=("$EXPORT_PATH" "$CLIENTS")
else
OPTIONS+=("$EXPORT_PATH" "$(translate "NFS export")")
fi
fi
done <<< "$EXPORTS"
if [[ ${#OPTIONS[@]} -eq 0 ]]; then
whiptail --title "$(translate "No Available Exports")" \
--msgbox "$(translate "No accessible exports found.")\n\n$(translate "You can enter the export path manually.")" \
10 70
NFS_EXPORT=$(whiptail --inputbox "$(translate "Enter NFS export path (e.g., /mnt/shared):")" 10 60 --title "$(translate "Export Path")" 3>&1 1>&2 2>&3)
[[ -n "$NFS_EXPORT" ]] && return 0 || return 1
fi
NFS_EXPORT=$(whiptail --title "$(translate "Select NFS Export")" --menu "$(translate "Choose an export to mount:")" 20 70 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3)
[[ -n "$NFS_EXPORT" ]] && return 0 || return 1
}
select_host_mount_point() {
local export_name=$(basename "$NFS_EXPORT")
local default_path="/mnt/shared_nfs_${export_name}"
MOUNT_POINT=$(pmx_select_host_mount_point "$(translate "NFS Mount Point")" "$default_path")
[[ -n "$MOUNT_POINT" ]] && return 0 || return 1
}
configure_host_mount_options() {
MOUNT_TYPE=$(whiptail --title "$(translate "Mount Options")" --menu "$(translate "Select mount configuration:")" 15 70 4 \
"1" "$(translate "Default options read/write")" \
"2" "$(translate "Read-only mount")" \
"3" "$(translate "Enter custom options")" 3>&1 1>&2 2>&3)
[[ $? -ne 0 ]] && return 1
case "$MOUNT_TYPE" in
1)
MOUNT_OPTIONS="rw,hard,nofail,rsize=131072,wsize=131072,timeo=600,retrans=2"
;;
2)
MOUNT_OPTIONS="ro,hard,nofail,rsize=131072,wsize=131072,timeo=600,retrans=2"
;;
3)
MOUNT_OPTIONS=$(whiptail --inputbox "$(translate "Enter custom mount options:")" \
10 70 "rw,hard,nofail,rsize=131072,wsize=131072,timeo=600,retrans=2" \
--title "$(translate "Custom Options")" 3>&1 1>&2 2>&3)
[[ $? -ne 0 ]] && return 1
[[ -z "$MOUNT_OPTIONS" ]] && MOUNT_OPTIONS="rw,hard,nofail"
;;
*)
MOUNT_OPTIONS="rw,hard,nofail,rsize=131072,wsize=131072,timeo=600,retrans=2"
;;
esac
if whiptail --yesno "$(translate "Do you want to make this mount permanent?")\n\n$(translate "This will add the mount to /etc/fstab so it persists after reboot.")" 10 70 --title "$(translate "Permanent Mount")"; then
PERMANENT_MOUNT=true
else
if [[ $? -eq 1 ]]; then
PERMANENT_MOUNT=false
else
return 1
fi
fi
TEMP_MOUNT="/tmp/nfs_test_$$"
mkdir -p "$TEMP_MOUNT" 2>/dev/null
NFS_PATH="$NFS_SERVER:$NFS_EXPORT"
if timeout 10 mount -t nfs -o ro,soft,timeo=5 "$NFS_PATH" "$TEMP_MOUNT" 2>/dev/null; then
umount "$TEMP_MOUNT" 2>/dev/null || true
rmdir "$TEMP_MOUNT" 2>/dev/null || true
msg_ok "$(translate "NFS export is accessible")"
if whiptail --yesno "$(translate "Do you want to add this as Proxmox storage?")\n\n$(translate "This will make the NFS share available as storage in Proxmox web interface.")" 10 70 --title "$(translate "Proxmox Storage")"; then
PROXMOX_STORAGE=true
STORAGE_ID=$(whiptail --inputbox "$(translate "Enter storage ID for Proxmox:")" 10 60 "nfs-$(echo $NFS_SERVER | tr '.' '-')" --title "$(translate "Storage ID")" 3>&1 1>&2 2>&3)
STORAGE_ID_RESULT=$?
if [[ $STORAGE_ID_RESULT -ne 0 ]]; then
if whiptail --yesno "$(translate "Storage ID input was cancelled.")\n\n$(translate "Do you want to continue without Proxmox storage integration?")" 10 70 --title "$(translate "Continue Without Storage")"; then
PROXMOX_STORAGE=false
else
return 1
fi
else
[[ -z "$STORAGE_ID" ]] && STORAGE_ID="nfs-$(echo $NFS_SERVER | tr '.' '-')"
fi
else
DIALOG_RESULT=$?
if [[ $DIALOG_RESULT -eq 1 ]]; then
PROXMOX_STORAGE=false
else
return 1
fi
fi
else
rmdir "$TEMP_MOUNT" 2>/dev/null || true
msg_warn "$(translate "NFS export accessibility test failed")"
if whiptail --yesno "$(translate "The NFS export could not be validated for accessibility.")\n\n$(translate "This might be due to:")\n• $(translate "Network connectivity issues")\n• $(translate "Export permission restrictions")\n• $(translate "Firewall blocking access")\n\n$(translate "Do you want to continue mounting anyway?")\n$(translate "(Proxmox storage integration will be skipped)")" 16 80 --title "$(translate "Export Validation Failed")"; then
PROXMOX_STORAGE=false
msg_info2 "$(translate "Continuing without Proxmox storage integration due to accessibility issues.")"
sleep 2
else
return 1
fi
fi
return 0
}
validate_host_export_exists() {
local server="$1"
local export="$2"
VALIDATION_OUTPUT=$(showmount -e "$server" 2>/dev/null | grep "^$export[[:space:]]")
if [[ -n "$VALIDATION_OUTPUT" ]]; then
return 0
else
show_proxmenux_logo
echo -e
msg_error "$(translate "Export not found on server:") $export"
return 1
fi
}
add_proxmox_nfs_storage() {
local storage_id="$1"
local server="$2"
local export="$3"
local content="${4:-backup,iso,vztmpl}"
msg_info "$(translate "Starting Proxmox storage integration...")"
if ! command -v pvesm >/dev/null 2>&1; then
show_proxmenux_logo
msg_error "$(translate "pvesm command not found. This should not happen on Proxmox.")"
echo "Press Enter to continue..."
read -r
return 1
fi
msg_ok "$(translate "pvesm command found")"
# Check if storage ID already exists
if pvesm status "$storage_id" >/dev/null 2>&1; then
msg_warn "$(translate "Storage ID already exists:") $storage_id"
if ! whiptail --yesno "$(translate "Storage ID already exists. Do you want to remove and recreate it?")" 8 60 --title "$(translate "Storage Exists")"; then
return 0
fi
pvesm remove "$storage_id" 2>/dev/null || true
fi
msg_ok "$(translate "Storage ID is available")"
# Let Proxmox handle NFS version negotiation automatically
if pvesm_output=$(pvesm add nfs "$storage_id" \
--server "$server" \
--export "$export" \
--content "$content" 2>&1); then
msg_ok "$(translate "NFS storage added successfully!")"
# Get the actual NFS version that Proxmox negotiated
local nfs_version="Auto-negotiated"
if pvesm config "$storage_id" 2>/dev/null | grep -q "options.*vers="; then
nfs_version="v$(pvesm config "$storage_id" | grep "options" | grep -o "vers=[0-9.]*" | cut -d= -f2)"
fi
echo -e ""
echo -e "${TAB}${BGN}$(translate "Storage ID:")${CL} ${BL}$storage_id${CL}"
echo -e "${TAB}${BGN}$(translate "Server:")${CL} ${BL}$server${CL}"
echo -e "${TAB}${BGN}$(translate "Export:")${CL} ${BL}$export${CL}"
echo -e "${TAB}${BGN}$(translate "Content Types:")${CL} ${BL}$content${CL}"
echo -e "${TAB}${BGN}$(translate "NFS Version:")${CL} ${BL}$nfs_version${CL}"
echo -e ""
msg_ok "$(translate "Storage is now available in Proxmox web interface under Datacenter > Storage")"
return 0
else
msg_error "$(translate "Failed to add NFS storage to Proxmox.")"
echo "$(translate "Error details:"): $pvesm_output"
msg_warn "$(translate "The NFS share is still mounted, but not added as Proxmox storage.")"
msg_info2 "$(translate "You can add it manually through:")"
echo -e "${TAB}$(translate "Proxmox web interface: Datacenter > Storage > Add > NFS")"
echo -e "${TAB}$(translate "Command line:"): pvesm add nfs $storage_id --server $server --export $export --content backup,iso,vztmpl"
return 1
fi
}
prepare_host_directory() {
local mount_point="$1"
if [[ "$SHARE_COMMON_LOADED" == "true" ]]; then
# Use common functions for advanced directory preparation
local group_name
group_name=$(pmx_choose_or_create_group "sharedfiles")
if [[ -n "$group_name" ]]; then
local host_gid
host_gid=$(pmx_ensure_host_group "$group_name")
if [[ -n "$host_gid" ]]; then
pmx_prepare_host_shared_dir "$mount_point" "$group_name"
pmx_share_map_set "$mount_point" "$group_name"
msg_ok "$(translate "Directory prepared with shared group:") $group_name (GID: $host_gid)"
return 0
fi
fi
msg_warn "$(translate "Failed to use shared functions, using basic directory creation.")"
fi
# Fallback: basic directory creation
if ! test -d "$mount_point"; then
if mkdir -p "$mount_point"; then
msg_ok "$(translate "Mount point created on host.")"
return 0
else
msg_error "$(translate "Failed to create mount point on host.")"
return 1
fi
fi
return 0
}
mount_host_nfs_share() {
if ! which showmount >/dev/null 2>&1; then
msg_error "$(translate "NFS client tools not found. Please check Proxmox installation.")"
return 1
fi
# Step 1:
select_nfs_server || return
# Step 2:
select_nfs_export || return
# Step 2.5:
if ! validate_host_export_exists "$NFS_SERVER" "$NFS_EXPORT"; then
echo -e ""
msg_error "$(translate "Cannot proceed with invalid export path.")"
msg_success "$(translate "Press Enter to return to menu...")"
read -r
return
fi
# Step 3:
select_host_mount_point || return
# Step 4:
configure_host_mount_options || return
show_proxmenux_logo
msg_title "$(translate "Mount NFS Share on Host")"
msg_ok "$(translate "NFS server selected")"
prepare_host_directory "$MOUNT_POINT" || return 1
if mount | grep -q "$MOUNT_POINT"; then
msg_warn "$(translate "Something is already mounted at") $MOUNT_POINT"
if ! whiptail --yesno "$(translate "Do you want to unmount it first?")" 8 60 --title "$(translate "Already Mounted")"; then
return
fi
umount "$MOUNT_POINT" 2>/dev/null || true
fi
NFS_PATH="$NFS_SERVER:$NFS_EXPORT"
if mount -t nfs -o "$MOUNT_OPTIONS" "$NFS_PATH" "$MOUNT_POINT" > /dev/null 2>&1; then
msg_ok "$(translate "NFS share mounted successfully on host!")"
if touch "$MOUNT_POINT/.test_write" 2>/dev/null; then
rm "$MOUNT_POINT/.test_write" 2>/dev/null
msg_ok "$(translate "Write access confirmed.")"
else
msg_warn "$(translate "Read-only access (or no write permissions).")"
fi
if [[ "$PERMANENT_MOUNT" == "true" ]]; then
sed -i "\|$MOUNT_POINT|d" /etc/fstab
FSTAB_ENTRY="$NFS_PATH $MOUNT_POINT nfs $MOUNT_OPTIONS 0 0"
echo "$FSTAB_ENTRY" >> /etc/fstab
msg_ok "$(translate "Added to /etc/fstab for permanent mounting.")"
msg_info "$(translate "Reloading systemd configuration...")"
systemctl daemon-reload 2>/dev/null || true
msg_ok "$(translate "Systemd configuration reloaded.")"
fi
if [[ "$PROXMOX_STORAGE" == "true" ]]; then
add_proxmox_nfs_storage "$STORAGE_ID" "$NFS_SERVER" "$NFS_EXPORT" "$MOUNT_CONTENT"
fi
echo -e ""
echo -e "${TAB}${BOLD}$(translate "Host Mount Information:")${CL}"
echo -e "${TAB}${BGN}$(translate "Server:")${CL} ${BL}$NFS_SERVER${CL}"
echo -e "${TAB}${BGN}$(translate "Export:")${CL} ${BL}$NFS_EXPORT${CL}"
echo -e "${TAB}${BGN}$(translate "Host Mount Point:")${CL} ${BL}$MOUNT_POINT${CL}"
echo -e "${TAB}${BGN}$(translate "Options:")${CL} ${BL}$MOUNT_OPTIONS${CL}"
echo -e "${TAB}${BGN}$(translate "Permanent:")${CL} ${BL}$PERMANENT_MOUNT${CL}"
if [[ "$PROXMOX_STORAGE" == "true" ]]; then
echo -e "${TAB}${BGN}$(translate "Proxmox Storage ID:")${CL} ${BL}$STORAGE_ID${CL}"
fi
else
msg_error "$(translate "Failed to mount NFS share on host.")"
echo -e "${TAB}$(translate "Please check:")"
echo -e "${TAB}$(translate "Server is accessible:"): $NFS_SERVER"
echo -e "${TAB}$(translate "Export exists:"): $NFS_EXPORT"
echo -e "${TAB}$(translate "Network connectivity")"
echo -e "${TAB}$(translate "NFS server is running")"
echo -e "${TAB}$(translate "Export permissions allow access")"
fi
echo -e ""
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
view_host_nfs_mounts() {
show_proxmenux_logo
msg_title "$(translate "Current NFS Mounts on Host")"
echo -e "$(translate "NFS mounts on Proxmox host:"):"
echo "=================================="
CURRENT_MOUNTS=$(mount | grep -E "type nfs|:.*on.*nfs" 2>/dev/null || true)
if [[ -n "$CURRENT_MOUNTS" ]]; then
echo -e "${BOLD}$(translate "Currently Mounted:")${CL}"
echo "$CURRENT_MOUNTS"
echo ""
else
echo "$(translate "No NFS shares currently mounted on host.")"
echo ""
fi
FSTAB_NFS=$(grep "nfs" /etc/fstab 2>/dev/null || true)
if [[ -n "$FSTAB_NFS" ]]; then
echo -e "${BOLD}$(translate "Permanent Mounts (fstab):")${CL}"
echo "$FSTAB_NFS"
echo ""
echo -e "${TAB}${BOLD}$(translate "Mount Details:")${CL}"
while IFS= read -r fstab_line; do
if [[ -n "$fstab_line" && ! "$fstab_line" =~ ^# ]]; then
NFS_PATH=$(echo "$fstab_line" | awk '{print $1}')
MOUNT_POINT=$(echo "$fstab_line" | awk '{print $2}')
OPTIONS=$(echo "$fstab_line" | awk '{print $4}')
SERVER=$(echo "$NFS_PATH" | cut -d: -f1)
EXPORT=$(echo "$NFS_PATH" | cut -d: -f2)
echo -e "${TAB}${BGN}$(translate "Server:")${CL} ${BL}$SERVER${CL}"
echo -e "${TAB}${BGN}$(translate "Export:")${CL} ${BL}$EXPORT${CL}"
echo -e "${TAB}${BGN}$(translate "Host Mount Point:")${CL} ${BL}$MOUNT_POINT${CL}"
echo -e "${TAB}${BGN}$(translate "Options:")${CL} ${BL}$OPTIONS${CL}"
if mount | grep -q "$MOUNT_POINT"; then
echo -e "${TAB}${BGN}$(translate "Status:")${CL} ${GN}$(translate "Mounted")${CL}"
else
echo -e "${TAB}${BGN}$(translate "Status:")${CL} ${RD}$(translate "Not Mounted")${CL}"
fi
echo ""
fi
done <<< "$FSTAB_NFS"
else
echo "$(translate "No NFS mounts found in fstab.")"
fi
echo ""
echo "$(translate "Proxmox NFS Storage Status:")"
if which pvesm >/dev/null 2>&1; then
NFS_STORAGES=$(pvesm status 2>/dev/null | grep "nfs" || true)
if [[ -n "$NFS_STORAGES" ]]; then
echo "$NFS_STORAGES"
else
echo "$(translate "No NFS storage configured in Proxmox.")"
fi
else
echo "$(translate "pvesm command not available.")"
fi
echo ""
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
unmount_host_nfs_share() {
MOUNTS=$(mount | grep -E "type nfs|:.*on.*nfs" | awk '{print $3}' | sort -u || true)
FSTAB_MOUNTS=$(grep -E "nfs" /etc/fstab 2>/dev/null | grep -v "^#" | awk '{print $2}' | sort -u || true)
ALL_MOUNTS=$(echo -e "$MOUNTS\n$FSTAB_MOUNTS" | sort -u | grep -v "^$" || true)
if [[ -z "$ALL_MOUNTS" ]]; then
dialog --backtitle "ProxMenux" --title "$(translate "No Mounts")" --msgbox "\n$(translate "No NFS mounts found on host.")" 8 50
return
fi
OPTIONS=()
while IFS= read -r mount_point; do
if [[ -n "$mount_point" ]]; then
NFS_PATH=$(mount | grep "$mount_point" | awk '{print $1}' || grep "$mount_point" /etc/fstab | awk '{print $1}' || echo "Unknown")
SERVER=$(echo "$NFS_PATH" | cut -d: -f1)
EXPORT=$(echo "$NFS_PATH" | cut -d: -f2)
OPTIONS+=("$mount_point" "$SERVER:$EXPORT")
fi
done <<< "$ALL_MOUNTS"
SELECTED_MOUNT=$(dialog --backtitle "ProxMenux" --title "$(translate "Unmount NFS Share")" --menu "$(translate "Select mount point to unmount:")" 20 80 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3)
[[ -z "$SELECTED_MOUNT" ]] && return
NFS_PATH=$(mount | grep "$SELECTED_MOUNT" | awk '{print $1}' || grep "$SELECTED_MOUNT" /etc/fstab | awk '{print $1}' || echo "Unknown")
SERVER=$(echo "$NFS_PATH" | cut -d: -f1)
EXPORT=$(echo "$NFS_PATH" | cut -d: -f2)
PROXMOX_STORAGE=""
if which pvesm >/dev/null 2>&1; then
NFS_STORAGES=$(pvesm status 2>/dev/null | grep "nfs" | awk '{print $1}' || true)
while IFS= read -r storage_id; do
if [[ -n "$storage_id" ]]; then
STORAGE_INFO=$(pvesm config "$storage_id" 2>/dev/null || true)
STORAGE_SERVER=$(echo "$STORAGE_INFO" | grep "server" | awk '{print $2}')
STORAGE_EXPORT=$(echo "$STORAGE_INFO" | grep "export" | awk '{print $2}')
if [[ "$STORAGE_SERVER" == "$SERVER" && "$STORAGE_EXPORT" == "$EXPORT" ]]; then
PROXMOX_STORAGE="$storage_id"
break
fi
fi
done <<< "$NFS_STORAGES"
fi
CONFIRMATION_MSG="$(translate "Are you sure you want to unmount this NFS share?")\n\n$(translate "Mount Point:"): $SELECTED_MOUNT\n$(translate "Server:"): $SERVER\n$(translate "Export:"): $EXPORT\n\n$(translate "This will:")\n• $(translate "Unmount the NFS share")\n• $(translate "Remove from /etc/fstab")"
if [[ -n "$PROXMOX_STORAGE" ]]; then
CONFIRMATION_MSG="$CONFIRMATION_MSG\n• $(translate "Remove Proxmox storage:"): $PROXMOX_STORAGE"
fi
CONFIRMATION_MSG="$CONFIRMATION_MSG\n• $(translate "Remove mount point directory")"
if whiptail --yesno "$CONFIRMATION_MSG" 16 80 --title "$(translate "Confirm Unmount")"; then
show_proxmenux_logo
msg_title "$(translate "Unmount NFS Share from Host")"
if [[ -n "$PROXMOX_STORAGE" ]]; then
if pvesm remove "$PROXMOX_STORAGE" 2>/dev/null; then
msg_ok "$(translate "Proxmox storage removed successfully.")"
else
msg_warn "$(translate "Failed to remove Proxmox storage, continuing with unmount...")"
fi
fi
if mount | grep -q "$SELECTED_MOUNT"; then
if umount "$SELECTED_MOUNT"; then
msg_ok "$(translate "Successfully unmounted.")"
else
msg_warn "$(translate "Failed to unmount. Trying force unmount...")"
if umount -f "$SELECTED_MOUNT" 2>/dev/null; then
msg_ok "$(translate "Force unmount successful.")"
else
msg_error "$(translate "Failed to unmount. Mount point may be busy.")"
echo -e "${TAB}$(translate "Try closing any applications using the mount point.")"
fi
fi
fi
msg_info "$(translate "Removing from /etc/fstab...")"
sed -i "\|[[:space:]]$SELECTED_MOUNT[[:space:]]|d" /etc/fstab
msg_ok "$(translate "Removed from /etc/fstab.")"
echo -e ""
msg_ok "$(translate "NFS share unmounted successfully from host!")"
if [[ -n "$PROXMOX_STORAGE" ]]; then
echo -e "${TAB}${BGN}$(translate "Proxmox storage removed:")${CL} ${BL}$PROXMOX_STORAGE${CL}"
fi
echo -e "${TAB}${BGN}$(translate "Mount point unmounted:")${CL} ${BL}$SELECTED_MOUNT${CL}"
echo -e "${TAB}${BGN}$(translate "Removed from fstab:")${CL} ${BL}Yes${CL}"
fi
echo -e ""
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
manage_proxmox_storage() {
if ! command -v pvesm >/dev/null 2>&1; then
dialog --backtitle "ProxMenux" --title "$(translate "Error")" --msgbox "\n$(translate "pvesm command not found. This should not happen on Proxmox.")" 8 60
return
fi
NFS_STORAGES=$(pvesm status 2>/dev/null | awk '$2 == "nfs" {print $1}')
if [[ -z "$NFS_STORAGES" ]]; then
dialog --backtitle "ProxMenux" --title "$(translate "No NFS Storage")" --msgbox "\n$(translate "No NFS storage found in Proxmox.")" 8 60
return
fi
OPTIONS=()
while IFS= read -r storage_id; do
if [[ -n "$storage_id" ]]; then
STORAGE_INFO=$(pvesm config "$storage_id" 2>/dev/null || true)
SERVER=$(echo "$STORAGE_INFO" | grep "server" | awk '{print $2}')
EXPORT=$(echo "$STORAGE_INFO" | grep "export" | awk '{print $2}')
if [[ -n "$SERVER" && -n "$EXPORT" ]]; then
OPTIONS+=("$storage_id" "$SERVER:$EXPORT")
else
OPTIONS+=("$storage_id" "$(translate "NFS Storage")")
fi
fi
done <<< "$NFS_STORAGES"
SELECTED_STORAGE=$(dialog --backtitle "ProxMenux" --title "$(translate "Manage Proxmox NFS Storage")" --menu "$(translate "Select storage to manage:")" 20 80 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3)
[[ -z "$SELECTED_STORAGE" ]] && return
STORAGE_INFO=$(pvesm config "$SELECTED_STORAGE" 2>/dev/null || true)
SERVER=$(echo "$STORAGE_INFO" | grep "server" | awk '{print $2}')
EXPORT=$(echo "$STORAGE_INFO" | grep "export" | awk '{print $2}')
CONTENT=$(echo "$STORAGE_INFO" | grep "content" | awk '{print $2}')
FSTAB_NFS=$(grep "nfs" /etc/fstab 2>/dev/null || true)
if [[ -n "$FSTAB_NFS" ]]; then
while IFS= read -r fstab_line; do
if [[ -n "$fstab_line" && ! "$fstab_line" =~ ^# ]]; then
NFS_PATH=$(echo "$fstab_line" | awk '{print $1}')
MOUNT_POINT=$(echo "$fstab_line" | awk '{print $2}')
OPTIONS=$(echo "$fstab_line" | awk '{print $4}')
SERVER=$(echo "$NFS_PATH" | cut -d: -f1)
EXPORT=$(echo "$NFS_PATH" | cut -d: -f2)
fi
done <<< "$FSTAB_NFS"
fi
if whiptail --yesno "$(translate "Are you sure you want to REMOVE storage") $SELECTED_STORAGE?\n\n$(translate "Server:"): $SERVER\n$(translate "Export:"): $EXPORT\n\n$(translate "WARNING: This will permanently remove the storage from Proxmox configuration.")\n$(translate "The NFS mount on the host will NOT be affected.")" 14 80 --title "$(translate "Remove Storage")"; then
show_proxmenux_logo
msg_title "$(translate "Remove Storage")"
if pvesm remove "$SELECTED_STORAGE" 2>/dev/null; then
msg_ok "$(translate "Storage removed successfully from Proxmox.")"
echo -e ""
msg_success "$(translate "Press Enter to return to menu...")"
read -r
else
msg_error "$(translate "Failed to remove storage.")"
fi
fi
}
test_host_nfs_connectivity() {
show_proxmenux_logo
msg_title "$(translate "Test NFS Connectivity on Host")"
echo -e "$(translate "NFS Client Status on Proxmox Host:"):"
echo "=================================="
if which showmount >/dev/null 2>&1; then
echo "$(translate "NFS Client Tools: AVAILABLE")"
if systemctl is-active --quiet rpcbind 2>/dev/null; then
echo "$(translate "RPC Bind Service: RUNNING")"
else
echo "$(translate "RPC Bind Service: STOPPED")"
msg_warn "$(translate "Starting rpcbind service...")"
systemctl start rpcbind 2>/dev/null || true
fi
echo ""
echo "$(translate "Current NFS mounts on host:")"
CURRENT_MOUNTS=$(mount | grep -E "type nfs|:.*on.*nfs" 2>/dev/null || true)
if [[ -n "$CURRENT_MOUNTS" ]]; then
echo "$CURRENT_MOUNTS"
else
echo "$(translate "No NFS mounts active on host.")"
fi
echo ""
echo "$(translate "Testing network connectivity...")"
FSTAB_SERVERS=$(grep "nfs" /etc/fstab 2>/dev/null | awk '{print $1}' | cut -d: -f1 | sort -u || true)
if [[ -n "$FSTAB_SERVERS" ]]; then
while IFS= read -r server; do
if [[ -n "$server" ]]; then
echo -n "$(translate "Testing") $server: "
if ping -c 1 -W 2 "$server" >/dev/null 2>&1; then
echo -e "${GN}$(translate "Reachable")${CL}"
echo -n " $(translate "NFS port 2049"): "
if nc -z -w 2 "$server" 2049 2>/dev/null; then
echo -e "${GN}$(translate "Open")${CL}"
else
echo -e "${RD}$(translate "Closed")${CL}"
fi
echo -n " $(translate "Export list test"): "
if showmount -e "$server" >/dev/null 2>&1; then
echo -e "${GN}$(translate "Available")${CL}"
else
echo -e "${RD}$(translate "Failed")${CL}"
fi
else
echo -e "${RD}$(translate "Unreachable")${CL}"
fi
fi
done <<< "$FSTAB_SERVERS"
else
echo "$(translate "No NFS servers configured to test.")"
fi
echo ""
echo "$(translate "Proxmox NFS Storage Status:")"
if which pvesm >/dev/null 2>&1; then
NFS_STORAGES=$(pvesm status 2>/dev/null | grep "nfs" || true)
if [[ -n "$NFS_STORAGES" ]]; then
echo "$NFS_STORAGES"
else
echo "$(translate "No NFS storage configured in Proxmox.")"
fi
else
echo "$(translate "pvesm command not available.")"
fi
else
echo "$(translate "NFS Client Tools: NOT AVAILABLE")"
echo ""
echo "$(translate "This is unusual for Proxmox. NFS client tools should be installed.")"
fi
echo ""
echo "$(translate "ProxMenux Extensions:")"
if [[ "$SHARE_COMMON_LOADED" == "true" ]]; then
echo "$(translate "Shared Functions: LOADED")"
if [[ -f "$PROXMENUX_SHARE_MAP_DB" ]]; then
MAPPED_DIRS=$(wc -l < "$PROXMENUX_SHARE_MAP_DB" 2>/dev/null || echo "0")
echo "$(translate "Mapped directories:"): $MAPPED_DIRS"
fi
else
echo "$(translate "Shared Functions: NOT LOADED (using fallback methods)")"
fi
echo ""
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
# === Main Menu ===
while true; do
CHOICE=$(dialog --backtitle "ProxMenux" --title "$(translate "NFS Host Manager - Proxmox Host")" \
--menu "$(translate "Choose an option:")" 22 80 14 \
"1" "$(translate "Mount NFS Share on Host")" \
"2" "$(translate "View Current Host NFS Mounts")" \
"3" "$(translate "Unmount NFS Share from Host")" \
"4" "$(translate "Remove Proxmox NFS Storage")" \
"5" "$(translate "Test NFS Connectivity")" \
"6" "$(translate "Exit")" \
3>&1 1>&2 2>&3)
RETVAL=$?
if [[ $RETVAL -ne 0 ]]; then
exit 0
fi
case $CHOICE in
1) mount_host_nfs_share ;;
2) view_host_nfs_mounts ;;
3) unmount_host_nfs_share ;;
4) manage_proxmox_storage ;;
5) test_host_nfs_connectivity ;;
6) exit 0 ;;
*) exit 0 ;;
esac
done

View File

@@ -0,0 +1,572 @@
#!/bin/bash
# ==========================================================
# ProxMenux CT - NFS Manager for Proxmox LXC (Simple + Universal)
# ==========================================================
# Based on ProxMenux by MacRimi
# ==========================================================
# Description:
# This script allows you to manage NFS shares inside Proxmox CTs:
# - Create NFS exports with universal sharedfiles group
# - View configured exports
# - Delete existing exports
# - Check NFS service status
# ==========================================================
# Configuration
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
# Load shared functions
SHARE_COMMON_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main/scripts/global/share-common.func"
if ! source <(curl -s "$SHARE_COMMON_URL" 2>/dev/null); then
msg_error "$(translate "Could not load shared functions. Script cannot continue.")"
exit 1
fi
load_language
initialize_cache
select_privileged_lxc
setup_universal_sharedfiles_group() {
local ctid="$1"
msg_info "$(translate "Setting sharedfiles group with UID remapping...")"
if ! pct exec "$ctid" -- getent group sharedfiles >/dev/null 2>&1; then
pct exec "$ctid" -- groupadd -g 101000 sharedfiles
msg_ok "$(translate "Created sharedfiles group (GID: 101000)")"
else
local current_gid=$(pct exec "$ctid" -- getent group sharedfiles | cut -d: -f3)
if [[ "$current_gid" != "101000" ]]; then
pct exec "$ctid" -- groupmod -g 101000 sharedfiles
msg_ok "$(translate "Updated sharedfiles group to GID: 101000")"
else
msg_ok "$(translate "Sharedfiles group already exists (GID: 101000)")"
fi
fi
local lxc_users=$(pct exec "$ctid" -- awk -F: '$3 >= 1000 && $3 < 65534 {print $1 ":" $3}' /etc/passwd)
if [[ -n "$lxc_users" ]]; then
msg_info "$(translate "Adding existing users to sharedfiles group...")"
while IFS=: read -r username uid; do
if [[ -n "$username" ]]; then
pct exec "$ctid" -- usermod -a -G sharedfiles "$username" 2>/dev/null || true
msg_ok "$(translate "Added user") $username (UID: $uid) $(translate "to sharedfiles group")"
fi
done <<< "$lxc_users"
fi
msg_info "$(translate "Creating UID remapping for unprivileged container compatibility...")"
local remapped_count=0
if [[ -n "$lxc_users" ]]; then
while IFS=: read -r username uid; do
if [[ -n "$uid" ]]; then
local remapped_uid=$((uid + 100000))
local remapped_username="remap_${uid}"
if ! pct exec "$ctid" -- id "$remapped_username" >/dev/null 2>&1; then
pct exec "$ctid" -- useradd -u "$remapped_uid" -g sharedfiles -s /bin/false -M "$remapped_username" 2>/dev/null || true
msg_ok "$(translate "Created remapped user") $remapped_username (UID: $remapped_uid)"
((remapped_count++))
else
pct exec "$ctid" -- usermod -g sharedfiles "$remapped_username" 2>/dev/null || true
fi
fi
done <<< "$lxc_users"
fi
local common_uids=(33 1000 1001 1002)
for base_uid in "${common_uids[@]}"; do
local remapped_uid=$((base_uid + 100000))
local remapped_username="remap_${base_uid}"
if ! pct exec "$ctid" -- id "$remapped_username" >/dev/null 2>&1; then
pct exec "$ctid" -- useradd -u "$remapped_uid" -g sharedfiles -s /bin/false -M "$remapped_username" 2>/dev/null || true
msg_ok "$(translate "Created common remapped user") $remapped_username (UID: $remapped_uid)"
((remapped_count++))
fi
done
msg_ok "$(translate "Universal sharedfiles group configured with") $remapped_count $(translate "remapped users")"
}
select_mount_point() {
while true; do
METHOD=$(whiptail --backtitle "ProxMenux" --title "$(translate "Select Folder")" \
--menu "$(translate "How do you want to select the folder to export?")" 15 60 5 \
"auto" "$(translate "Select from folders inside /mnt")" \
"manual" "$(translate "Enter path manually")" \
3>&1 1>&2 2>&3)
if [[ $? -ne 0 ]]; then
return 1
fi
case "$METHOD" in
auto)
DIRS=$(pct exec "$CTID" -- find /mnt -maxdepth 1 -mindepth 1 -type d 2>/dev/null)
if [[ -z "$DIRS" ]]; then
whiptail --title "$(translate "No Folders")" --msgbox "$(translate "No folders found inside /mnt in the CT.")" 8 60
continue
fi
OPTIONS=()
while IFS= read -r dir; do
name=$(basename "$dir")
OPTIONS+=("$dir" "$name")
done <<< "$DIRS"
MOUNT_POINT=$(whiptail --backtitle "ProxMenux" --title "$(translate "Select Folder")" \
--menu "$(translate "Choose a folder to export:")" 20 60 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3)
if [[ $? -ne 0 ]]; then
return 1
fi
[[ -n "$MOUNT_POINT" ]] && return 0
;;
manual)
CT_NAME=$(pct config "$CTID" | awk -F: '/hostname/ {print $2}' | xargs)
DEFAULT_MOUNT_POINT="/mnt/${CT_NAME}_nfs"
MOUNT_POINT=$(whiptail --title "$(translate "Mount Point")" \
--inputbox "$(translate "Enter the mount point for the NFS export (e.g., /mnt/mynfs):")" \
10 70 "$DEFAULT_MOUNT_POINT" 3>&1 1>&2 2>&3)
if [[ $? -ne 0 ]]; then
return 1
fi
if [[ -z "$MOUNT_POINT" ]]; then
whiptail --title "$(translate "Error")" \
--msgbox "$(translate "No mount point was specified.")" 8 50
continue
fi
pct exec "$CTID" -- mkdir -p "$MOUNT_POINT" 2>/dev/null
return 0
;;
esac
done
}
get_network_config() {
NETWORK=$(whiptail --backtitle "ProxMenux" --title "$(translate "Network Configuration")" --menu "\n$(translate "Select network access level:")" 15 70 4 \
"1" "$(translate "Local network only (192.168.0.0/16)")" \
"2" "$(translate "Specific subnet (enter manually)")" \
"3" "$(translate "Specific host (enter IP)")" 3>&1 1>&2 2>&3)
case "$NETWORK" in
1)
NETWORK_RANGE="192.168.0.0/16"
;;
2)
clear
NETWORK_RANGE=$(whiptail --inputbox "$(translate "Enter subnet (e.g., 192.168.0.0/24):")" 10 60 "192.168.0.0/24" --title "$(translate "Subnet")" 3>&1 1>&2 2>&3)
[[ -z "$NETWORK_RANGE" ]] && return 1
;;
3)
dialog
NETWORK_RANGE=$(whiptail --inputbox "$(translate "Enter host IP (e.g., 192.168.0.100):")" 10 60 --title "$(translate "Host IP")" 3>&1 1>&2 2>&3)
[[ -z "$NETWORK_RANGE" ]] && return 1
;;
*)
return 1
;;
esac
return 0
}
select_export_options() {
EXPORT_OPTIONS=$(whiptail --title "$(translate "Export Options")" --menu \
"\n$(translate "Select export permissions:")" 15 70 3 \
"1" "$(translate "Read-Write (universal)")" \
"2" "$(translate "Read-Only")" \
"3" "$(translate "Custom options")" 3>&1 1>&2 2>&3)
case "$EXPORT_OPTIONS" in
1)
OPTIONS="rw,sync,no_subtree_check,no_root_squash"
;;
2)
OPTIONS="ro,sync,no_subtree_check,no_root_squash"
;;
3)
OPTIONS=$(whiptail --inputbox "$(translate "Enter custom NFS options:")" \
10 70 "rw,sync,no_subtree_check,no_root_squash" \
--title "$(translate "Custom Options")" 3>&1 1>&2 2>&3)
[[ -z "$OPTIONS" ]] && OPTIONS="rw,sync,no_subtree_check,no_root_squash"
;;
*)
OPTIONS="rw,sync,no_subtree_check,no_root_squash"
;;
esac
}
create_nfs_export() {
show_proxmenux_logo
msg_title "$(translate "Create LXC server NFS")"
sleep 2
select_mount_point || return
get_network_config || return
select_export_options || return
msg_ok "$(translate "Directory successfully.")"
if ! pct exec "$CTID" -- dpkg -s nfs-kernel-server &>/dev/null; then
msg_info "$(translate "Installing NFS server packages inside the CT...")"
pct exec "$CTID" -- bash -c "apt-get update && apt-get install -y nfs-kernel-server nfs-common rpcbind"
pct exec "$CTID" -- systemctl enable --now rpcbind nfs-kernel-server
msg_ok "$(translate "NFS server installed successfully.")"
else
msg_ok "$(translate "NFS server is already installed.")"
fi
setup_universal_sharedfiles_group "$CTID"
msg_info "$(translate "Setting directory ownership and permissions...")"
pct exec "$CTID" -- chown root:sharedfiles "$MOUNT_POINT"
pct exec "$CTID" -- chmod 2775 "$MOUNT_POINT"
msg_ok "$(translate "Directory configured with sharedfiles group ownership")"
EXPORT_LINE="$MOUNT_POINT $NETWORK_RANGE($OPTIONS)"
if pct exec "$CTID" -- grep -q "^$MOUNT_POINT " /etc/exports; then
if dialog --yesno "$(translate "Do you want to update the existing export?")" \
10 60 --title "$(translate "Update Export")"; then
pct exec "$CTID" -- sed -i "\|^$MOUNT_POINT |d" /etc/exports
pct exec "$CTID" -- bash -c "echo '$EXPORT_LINE' >> /etc/exports"
show_proxmenux_logo
msg_title "$(translate "Create LXC server NFS")"
msg_ok "$(translate "Directory successfully.")"
msg_ok "$(translate "Export updated successfully.")"
msg_ok "$(translate "NFS server is already installed.")"
msg_ok "$(translate "Directory configured with sharedfiles group ownership")"
fi
else
pct exec "$CTID" -- bash -c "echo '$EXPORT_LINE' >> /etc/exports"
msg_ok "$(translate "Export added successfully.")"
fi
pct exec "$CTID" -- systemctl restart rpcbind nfs-kernel-server
pct exec "$CTID" -- exportfs -ra
CT_IP=$(pct exec "$CTID" -- hostname -I | awk '{print $1}')
echo -e ""
msg_ok "$(translate "NFS export created successfully!")"
echo -e ""
echo -e "${TAB}${BOLD}$(translate "Connection details:")${CL}"
echo -e "${TAB}${BGN}$(translate "Server IP:")${CL} ${BL}$CT_IP${CL}"
echo -e "${TAB}${BGN}$(translate "Export path:")${CL} ${BL}$MOUNT_POINT${CL}"
echo -e "${TAB}${BGN}$(translate "Mount options:")${CL} ${BL}$OPTIONS${CL}"
echo -e "${TAB}${BGN}$(translate "Network access:")${CL} ${BL}$NETWORK_RANGE${CL}"
echo -e "${TAB}${BGN}$(translate "NFS Version:")${CL} ${BL}Auto-negotiation (NFSv3/NFSv4)${CL}"
echo -e ""
echo -e "${TAB}${BOLD}$(translate "Mount Examples:")${CL}"
echo -e "${TAB}${BGN}$(translate "Auto-negotiate:")${CL} ${BL}mount -t nfs $CT_IP:$MOUNT_POINT /mnt/nfs${CL}"
echo -e "${TAB}${BGN}$(translate "Force NFSv4:")${CL} ${BL}mount -t nfs4 $CT_IP:$MOUNT_POINT /mnt/nfs${CL}"
echo -e "${TAB}${BGN}$(translate "Force NFSv3:")${CL} ${BL}mount -t nfs -o vers=3 $CT_IP:$MOUNT_POINT /mnt/nfs${CL}"
echo ""
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
view_exports() {
show_proxmenux_logo
msg_title "$(translate "View Current Exports")"
echo -e "$(translate "Current NFS exports in CT") $CTID:"
echo "=================================="
if pct exec "$CTID" -- test -f /etc/exports; then
EXPORTS=$(pct exec "$CTID" -- cat /etc/exports | grep -v '^#' | grep -v '^$')
if [[ -n "$EXPORTS" ]]; then
echo "$EXPORTS"
echo ""
echo "$(translate "Active exports:")"
pct exec "$CTID" -- showmount -e localhost 2>/dev/null || echo "$(translate "No active exports or showmount not available")"
echo ""
echo "$(translate "Universal Group Configuration:")"
echo "=================================="
if pct exec "$CTID" -- getent group sharedfiles >/dev/null 2>&1; then
local group_members=$(pct exec "$CTID" -- getent group sharedfiles | cut -d: -f4)
local sharedfiles_gid=$(pct exec "$CTID" -- getent group sharedfiles | cut -d: -f3)
echo "$(translate "Shared group: sharedfiles (GID:") $sharedfiles_gid)"
local member_count=$(echo "$group_members" | tr ',' '\n' | wc -l)
echo "$(translate "Total members:") $member_count $(translate "users")"
local remapped_users=$(pct exec "$CTID" -- getent passwd | grep "^remap_" | wc -l)
if [[ "$remapped_users" -gt 0 ]]; then
echo "$(translate "Remapped users:") $remapped_users $(translate "users (for unprivileged compatibility)")"
fi
echo "$(translate "Universal compatibility: ENABLED")"
echo "$(translate "NFS Version: Auto-negotiation (NFSv3/NFSv4)")"
else
echo "$(translate "Universal group: NOT CONFIGURED")"
fi
CT_IP=$(pct exec "$CTID" -- hostname -I | awk '{print $1}')
echo ""
echo "=================================="
echo -e "${TAB}${BOLD}$(translate "Connection Details:")${CL}"
echo -e "${TAB}${BGN}$(translate "Server IP:")${CL} ${BL}$CT_IP${CL}"
while IFS= read -r export_line; do
if [[ -n "$export_line" ]]; then
EXPORT_PATH=$(echo "$export_line" | awk '{print $1}')
echo -e "${TAB}${BGN}$(translate "Export path:")${CL} ${BL}$EXPORT_PATH${CL}"
echo ""
fi
done <<< "$EXPORTS"
else
echo "$(translate "No exports configured.")"
fi
else
echo "$(translate "/etc/exports file does not exist.")"
fi
echo ""
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
delete_export() {
if ! pct exec "$CTID" -- test -f /etc/exports; then
dialog --title "$(translate "Error")" --msgbox "\n$(translate "No exports file found.")" 8 50
return
fi
EXPORTS=$(pct exec "$CTID" -- awk '!/^#|^$/ {print NR, $0}' /etc/exports)
if [[ -z "$EXPORTS" ]]; then
dialog --title "$(translate "No Exports")" --msgbox "$(translate "No exports found in /etc/exports.")" 8 60
return
fi
OPTIONS=()
while read -r line; do
[[ -z "$line" ]] && continue
NUM=$(echo "$line" | awk '{print $1}')
EXPORT_LINE=$(echo "$line" | cut -d' ' -f2-)
EXPORT_PATH=$(echo "$EXPORT_LINE" | awk '{print $1}')
EXPORT_CLIENT=$(echo "$EXPORT_LINE" | awk '{print $2}' | cut -d'(' -f1)
[[ -z "$EXPORT_PATH" || -z "$EXPORT_CLIENT" ]] && continue
OPTIONS+=("$NUM" "$EXPORT_PATH $EXPORT_CLIENT")
done <<< "$EXPORTS"
SELECTED_NUM=$(dialog --title "$(translate "Delete Export")" --menu "$(translate "Select an export to delete:")" 20 70 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3)
[ -z "$SELECTED_NUM" ] && return
EXPORT_LINE=$(echo "$EXPORTS" | awk -v num="$SELECTED_NUM" '$1 == num {$1=""; print substr($0,2)}')
if whiptail --yesno "$(translate "Are you sure you want to delete this export?")\n\n$EXPORT_LINE" 10 70 --title "$(translate "Confirm Deletion")"; then
show_proxmenux_logo
msg_title "$(translate "Delete Export")"
pct exec "$CTID" -- sed -i "${SELECTED_NUM}d" /etc/exports
pct exec "$CTID" -- exportfs -ra
pct exec "$CTID" -- systemctl restart nfs-kernel-server
msg_ok "$(translate "Export deleted and NFS service restarted.")"
fi
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
check_nfs_status() {
show_proxmenux_logo
msg_title "$(translate "Check NFS Status")"
echo -e "$(translate "NFS Service Status in CT") $CTID:"
echo "=================================="
if pct exec "$CTID" -- dpkg -s nfs-kernel-server &>/dev/null; then
echo "$(translate "NFS Server: INSTALLED")"
if pct exec "$CTID" -- systemctl is-active --quiet nfs-kernel-server; then
echo "$(translate "NFS Service: RUNNING")"
else
echo "$(translate "NFS Service: STOPPED")"
fi
if pct exec "$CTID" -- systemctl is-active --quiet rpcbind; then
echo "$(translate "RPC Bind Service: RUNNING")"
else
echo "$(translate "RPC Bind Service: STOPPED")"
fi
echo ""
echo "$(translate "NFS Version Configuration:")"
echo "$(translate "Version: Auto-negotiation (NFSv3/NFSv4)")"
echo "$(translate "Client determines best version to use")"
echo ""
echo "$(translate "Universal Group Configuration:")"
if pct exec "$CTID" -- getent group sharedfiles >/dev/null 2>&1; then
echo "$(translate "Shared group: CONFIGURED")"
local group_members=$(pct exec "$CTID" -- getent group sharedfiles | cut -d: -f4)
local sharedfiles_gid=$(pct exec "$CTID" -- getent group sharedfiles | cut -d: -f3)
echo "$(translate "Group GID:") $sharedfiles_gid"
local member_count=$(echo "$group_members" | tr ',' '\n' | wc -l)
echo "$(translate "Total members:") $member_count $(translate "users")"
local remapped_users=$(pct exec "$CTID" -- getent passwd | grep "^remap_" | wc -l)
echo "$(translate "Remapped users:") $remapped_users $(translate "users")"
echo "$(translate "Universal compatibility: ENABLED")"
else
echo "$(translate "Universal group: NOT CONFIGURED")"
fi
echo ""
echo "$(translate "Listening ports:")"
pct exec "$CTID" -- ss -tlnp | grep -E ':(111|2049|20048)' || echo "$(translate "No NFS ports found")"
else
echo "$(translate "NFS Server: NOT INSTALLED")"
fi
echo ""
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
uninstall_nfs() {
if ! pct exec "$CTID" -- dpkg -s nfs-kernel-server &>/dev/null; then
dialog --title "$(translate "NFS Not Installed")" --msgbox "\n$(translate "NFS server is not installed in this CT.")" 8 60
return
fi
if ! whiptail --title "$(translate "Uninstall NFS Server")" \
--yesno "$(translate "WARNING: This will completely remove NFS server from the CT.")\n\n$(translate "This action will:")\n$(translate "• Stop all NFS services")\n$(translate "• Remove all exports")\n$(translate "• Uninstall NFS packages")\n$(translate "• Remove universal sharedfiles group")\n$(translate "• Clean up remapped users")\n\n$(translate "Are you sure you want to continue?")" \
18 70; then
return
fi
show_proxmenux_logo
msg_title "$(translate "Uninstall NFS Server")"
msg_info "$(translate "Stopping NFS services...")"
pct exec "$CTID" -- systemctl stop nfs-kernel-server 2>/dev/null || true
pct exec "$CTID" -- systemctl stop rpcbind 2>/dev/null || true
pct exec "$CTID" -- systemctl disable nfs-kernel-server 2>/dev/null || true
pct exec "$CTID" -- systemctl disable rpcbind 2>/dev/null || true
msg_ok "$(translate "NFS services stopped and disabled.")"
if pct exec "$CTID" -- test -f /etc/exports; then
pct exec "$CTID" -- truncate -s 0 /etc/exports
msg_ok "$(translate "Exports cleared.")"
fi
msg_info "$(translate "Removing remapped users...")"
local remapped_users=$(pct exec "$CTID" -- getent passwd | grep "^remap_" | cut -d: -f1)
if [[ -n "$remapped_users" ]]; then
while IFS= read -r username; do
if [[ -n "$username" ]]; then
pct exec "$CTID" -- userdel "$username" 2>/dev/null || true
msg_ok "$(translate "Removed remapped user:") $username"
fi
done <<< "$remapped_users"
fi
if pct exec "$CTID" -- getent group sharedfiles >/dev/null 2>&1; then
local regular_members=$(pct exec "$CTID" -- getent group sharedfiles | cut -d: -f4 | tr ',' '\n' | grep -v "^remap_" | wc -l)
if [[ "$regular_members" -eq 0 ]]; then
pct exec "$CTID" -- groupdel sharedfiles 2>/dev/null || true
msg_ok "$(translate "Removed sharedfiles group.")"
else
msg_warn "$(translate "Kept sharedfiles group (has regular users assigned).")"
fi
fi
pct exec "$CTID" -- apt-get remove --purge -y nfs-kernel-server nfs-common 2>/dev/null || true
pct exec "$CTID" -- apt-get autoremove -y 2>/dev/null || true
msg_ok "$(translate "NFS packages removed.")"
msg_info "$(translate "Cleaning up remaining processes...")"
pct exec "$CTID" -- pkill -f nfs 2>/dev/null || true
pct exec "$CTID" -- pkill -f rpc 2>/dev/null || true
sleep 2
msg_ok "$(translate "Universal NFS server has been completely uninstalled!")"
echo -e ""
echo -e "${TAB}${BOLD}$(translate "Uninstallation Summary:")${CL}"
echo -e "${TAB}${BGN}$(translate "Services:")${CL} ${BL}$(translate "Stopped and disabled")${CL}"
echo -e "${TAB}${BGN}$(translate "Packages:")${CL} ${BL}$(translate "Removed")${CL}"
echo -e "${TAB}${BGN}$(translate "Exports:")${CL} ${BL}$(translate "Cleared")${CL}"
echo -e "${TAB}${BGN}$(translate "Universal Group:")${CL} ${BL}$(translate "Cleaned up")${CL}"
echo -e "${TAB}${BGN}$(translate "Remapped Users:")${CL} ${BL}$(translate "Removed")${CL}"
echo -e
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
# === Main Menu ===
while true; do
CHOICE=$(dialog --title "$(translate "NFS LXC Manager - CT") $CTID" --menu "$(translate "Choose an option:")" 20 70 12 \
"1" "$(translate "Create Universal NFS Export")" \
"2" "$(translate "View Current Exports")" \
"3" "$(translate "Delete Export")" \
"4" "$(translate "Check NFS Status")" \
"5" "$(translate "Uninstall NFS Server")" \
"6" "$(translate "Exit")" 3>&1 1>&2 2>&3)
case $CHOICE in
1) create_nfs_export ;;
2) view_exports ;;
3) delete_export ;;
4) check_nfs_status ;;
5) uninstall_nfs ;;
6) exit 0 ;;
*) exit 0 ;;
esac
done

File diff suppressed because it is too large Load Diff

1266
scripts/share/samba_host.sh Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,576 @@
#!/bin/bash
# ==========================================================
# ProxMenux CT - Samba Manager for Proxmox LXC
# ==========================================================
# Based on ProxMenux by MacRimi
# ==========================================================
# Description:
# This script allows you to manage Samba shares inside Proxmox CTs:
# - Create shared folders
# - View configured shares
# - Delete existing shares
# - Check Samba service status
# ==========================================================
# Configuration
REPO_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main"
BASE_DIR="/usr/local/share/proxmenux"
UTILS_FILE="$BASE_DIR/utils.sh"
VENV_PATH="/opt/googletrans-env"
CREDENTIALS_DIR="/etc/samba/credentials"
if [[ -f "$UTILS_FILE" ]]; then
source "$UTILS_FILE"
fi
SHARE_COMMON_URL="https://raw.githubusercontent.com/MacRimi/ProxMenux/main/scripts/global/share-common.func"
if ! source <(curl -s "$SHARE_COMMON_URL" 2>/dev/null); then
msg_error "$(translate "Could not load shared functions. Script cannot continue.")"
exit 1
fi
load_language
initialize_cache
select_privileged_lxc
select_mount_point() {
while true; do
METHOD=$(whiptail --backtitle "ProxMenux" --title "$(translate "Select Folder")" \
--menu "$(translate "How do you want to select the folder to share?")" 15 60 5 \
"auto" "$(translate "Select from folders inside /mnt")" \
"manual" "$(translate "Enter path manually")" \
3>&1 1>&2 2>&3)
if [[ $? -ne 0 ]]; then
return 1
fi
case "$METHOD" in
auto)
DIRS=$(pct exec "$CTID" -- find /mnt -maxdepth 1 -mindepth 1 -type d 2>/dev/null)
if [[ -z "$DIRS" ]]; then
whiptail --title "$(translate "No Folders")" --msgbox "$(translate "No folders found inside /mnt in the CT.")" 8 60
continue
fi
OPTIONS=()
while IFS= read -r dir; do
name=$(basename "$dir")
OPTIONS+=("$dir" "$name")
done <<< "$DIRS"
MOUNT_POINT=$(whiptail --title "$(translate "Select Folder")" \
--menu "$(translate "Choose a folder to share:")" 20 60 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3)
if [[ $? -ne 0 ]]; then
return 1
fi
[[ -n "$MOUNT_POINT" ]] && return 0
;;
manual)
CT_NAME=$(pct config "$CTID" | awk -F: '/hostname/ {print $2}' | xargs)
DEFAULT_MOUNT_POINT="/mnt/${CT_NAME}_share"
MOUNT_POINT=$(whiptail --title "$(translate "Mount Point")" \
--inputbox "$(translate "Enter the mount point for the shared folder (e.g., /mnt/myshare):")" \
10 70 "$DEFAULT_MOUNT_POINT" 3>&1 1>&2 2>&3)
if [[ $? -ne 0 ]]; then
return 1
fi
clear
if [[ -z "$MOUNT_POINT" ]]; then
whiptail --title "$(translate "Error")" --msgbox "\n$(translate "No mount point was specified.")" 8 50
continue
else
return 0
fi
;;
esac
done
}
create_share() {
show_proxmenux_logo
msg_title "$(translate "Create Samba server service")"
sleep 2
select_mount_point || return
if ! pct exec "$CTID" -- test -d "$MOUNT_POINT"; then
if whiptail --yesno "$(translate "The directory does not exist in the CT.")\n\n$MOUNT_POINT\n\n$(translate "Do you want to create it?")" 12 70 --title "$(translate "Create Directory")"; then
pct exec "$CTID" -- mkdir -p "$MOUNT_POINT"
msg_ok "$(translate "Directory created successfully.")"
else
msg_error "$(translate "Directory does not exist and was not created.")"
return
fi
fi
if pct exec "$CTID" -- dpkg -s samba &>/dev/null; then
SAMBA_INSTALLED=true
else
SAMBA_INSTALLED=false
fi
if [ "$SAMBA_INSTALLED" = false ]; then
echo -e "${TAB}$(translate "Installing Samba server packages inside the CT...")"
pct exec "$CTID" -- bash -c "apt-get update && apt-get install -y samba samba-common-bin acl"
USERNAME=$(whiptail --inputbox "$(translate "Enter the Samba username:")" 10 60 "proxmenux" --title "$(translate "Samba User")" 3>&1 1>&2 2>&3)
[[ -z "$USERNAME" ]] && msg_error "$(translate "No username provided.")" && return
while true; do
PASSWORD1=$(whiptail --passwordbox "$(translate "Enter the password for Samba user:")" 10 60 --title "$(translate "Samba Password")" 3>&1 1>&2 2>&3)
[[ -z "$PASSWORD1" ]] && msg_error "$(translate "No password provided.")" && return
PASSWORD2=$(whiptail --passwordbox "$(translate "Confirm the password:")" 10 60 --title "$(translate "Confirm Password")" 3>&1 1>&2 2>&3)
[[ -z "$PASSWORD2" ]] && msg_error "$(translate "Password confirmation is required.")" && return
if [[ "$PASSWORD1" != "$PASSWORD2" ]]; then
whiptail --title "$(translate "Password Mismatch")" --msgbox "$(translate "The passwords do not match. Please try again.")" 10 60
else
PASSWORD="$PASSWORD1"
break
fi
done
if ! pct exec "$CTID" -- id "$USERNAME" &>/dev/null; then
pct exec "$CTID" -- adduser --disabled-password --gecos "" "$USERNAME"
fi
pct exec "$CTID" -- bash -c "echo -e '$PASSWORD\n$PASSWORD' | smbpasswd -a '$USERNAME'"
msg_ok "$(translate "Samba server installed successfully.")"
else
USERNAME=$(pct exec "$CTID" -- pdbedit -L | awk -F: '{print $1}' | head -n1)
msg_ok "$(translate "Samba server is already installed.")"
echo -e "$(translate "Detected existing Samba user:") $USERNAME"
fi
IS_MOUNTED=$(pct exec "$CTID" -- mount | grep "$MOUNT_POINT" || true)
if [[ -n "$IS_MOUNTED" ]]; then
msg_info "$(translate "Detected a mounted directory from host. Setting up shared group...")"
SHARE_GID=999
GROUP_EXISTS=$(pct exec "$CTID" -- getent group sharedfiles || true)
GID_IN_USE=$(pct exec "$CTID" -- getent group "$SHARE_GID" | cut -d: -f1 || true)
if [[ -z "$GROUP_EXISTS" ]]; then
if [[ -z "$GID_IN_USE" ]]; then
pct exec "$CTID" -- groupadd -g "$SHARE_GID" sharedfiles
msg_ok "$(translate "Group 'sharedfiles' created with GID $SHARE_GID")"
else
pct exec "$CTID" -- groupadd sharedfiles
msg_warn "$(translate "GID $SHARE_GID already in use. Group 'sharedfiles' created with dynamic GID.")"
fi
else
msg_ok "$(translate "Group 'sharedfiles' already exists inside the CT")"
fi
if pct exec "$CTID" -- getent group sharedfiles >/dev/null; then
pct exec "$CTID" -- usermod -aG sharedfiles "$USERNAME"
pct exec "$CTID" -- chown root:sharedfiles "$MOUNT_POINT"
pct exec "$CTID" -- chmod 2775 "$MOUNT_POINT"
else
msg_error "$(translate "Group 'sharedfiles' was not created successfully. Skipping chown/usermod.")"
fi
HAS_ACCESS=$(pct exec "$CTID" -- su -s /bin/bash -c "test -w '$MOUNT_POINT' && echo yes || echo no" "$USERNAME" 2>/dev/null)
if [ "$HAS_ACCESS" = "no" ]; then
pct exec "$CTID" -- setfacl -R -m "u:$USERNAME:rwx" "$MOUNT_POINT"
msg_warn "$(translate "ACL permissions applied to allow write access for user:") $USERNAME"
else
msg_ok "$(translate "Write access confirmed for user:") $USERNAME"
fi
else
msg_ok "$(translate "No shared mount detected. Applying standard local access.")"
pct exec "$CTID" -- chown -R "$USERNAME:$USERNAME" "$MOUNT_POINT"
pct exec "$CTID" -- chmod -R 755 "$MOUNT_POINT"
HAS_ACCESS=$(pct exec "$CTID" -- su -s /bin/bash -c "test -w '$MOUNT_POINT' && echo yes || echo no" "$USERNAME" 2>/dev/null)
if [ "$HAS_ACCESS" = "no" ]; then
pct exec "$CTID" -- setfacl -R -m "u:$USERNAME:rwx" "$MOUNT_POINT"
msg_warn "$(translate "ACL permissions applied for local access for user:") $USERNAME"
else
msg_ok "$(translate "Write access confirmed for user:") $USERNAME"
fi
fi
SHARE_OPTIONS=$(whiptail --title "$(translate "Share Options")" --menu "$(translate "Select share permissions:")" 15 70 3 \
"rw" "$(translate "Read-Write access")" \
"ro" "$(translate "Read-Only access")" \
"custom" "$(translate "Custom options")" 3>&1 1>&2 2>&3)
SHARE_NAME=$(basename "$MOUNT_POINT")
case "$SHARE_OPTIONS" in
rw)
CONFIG=$(cat <<EOF
[$SHARE_NAME]
comment = Shared folder for $USERNAME
path = $MOUNT_POINT
read only = no
writable = yes
browseable = yes
guest ok = no
valid users = $USERNAME
force group = sharedfiles
create mask = 0664
directory mask = 2775
force create mode = 0664
force directory mode = 2775
veto files = /lost+found/
EOF
)
;;
ro)
CONFIG=$(cat <<EOF
[$SHARE_NAME]
comment = Read-only shared folder for $USERNAME
path = $MOUNT_POINT
read only = yes
writable = no
browseable = yes
guest ok = no
valid users = $USERNAME
force group = sharedfiles
veto files = /lost+found/
EOF
)
;;
custom)
CUSTOM_CONFIG=$(whiptail --inputbox "$(translate "Enter custom Samba configuration for this share:")" 15 80 "read only = no\nwritable = yes\nbrowseable = yes\nguest ok = no" --title "$(translate "Custom Configuration")" 3>&1 1>&2 2>&3)
CONFIG=$(cat <<EOF
[$SHARE_NAME]
comment = Custom shared folder for $USERNAME
path = $MOUNT_POINT
valid users = $USERNAME
force group = sharedfiles
$CUSTOM_CONFIG
veto files = /lost+found/
EOF
)
;;
*)
CONFIG=$(cat <<EOF
[$SHARE_NAME]
comment = Shared folder for $USERNAME
path = $MOUNT_POINT
read only = no
writable = yes
browseable = yes
guest ok = no
valid users = $USERNAME
force group = sharedfiles
create mask = 0664
directory mask = 2775
force create mode = 0664
force directory mode = 2775
veto files = /lost+found/
EOF
)
;;
esac
if pct exec "$CTID" -- grep -q "\[$SHARE_NAME\]" /etc/samba/smb.conf; then
msg_warn "$(translate "The share already exists in smb.conf:") [$SHARE_NAME]"
if whiptail --yesno "$(translate "Do you want to update the existing share?")" 10 60 --title "$(translate "Update Share")"; then
pct exec "$CTID" -- sed -i "/^\[$SHARE_NAME\]/,/^$/d" /etc/samba/smb.conf
pct exec "$CTID" -- bash -c "echo '$CONFIG' >> /etc/samba/smb.conf"
msg_ok "$(translate "Share updated successfully.")"
else
return
fi
else
msg_ok "$(translate "Adding new share to smb.conf...")"
pct exec "$CTID" -- bash -c "echo '$CONFIG' >> /etc/samba/smb.conf"
msg_ok "$(translate "Share added successfully.")"
fi
pct exec "$CTID" -- systemctl restart smbd.service
CT_IP=$(pct exec "$CTID" -- hostname -I | awk '{print $1}')
echo -e ""
msg_ok "$(translate "Samba share created successfully!")"
echo -e ""
echo -e "${TAB}${BOLD}$(translate "Connection details:")${CL}"
echo -e "${TAB}${BGN}$(translate "Server IP:")${CL} ${BL}$CT_IP${CL}"
echo -e "${TAB}${BGN}$(translate "Share name:")${CL} ${BL}$SHARE_NAME${CL}"
echo -e "${TAB}${BGN}$(translate "Share path:")${CL} ${BL}$MOUNT_POINT${CL}"
echo -e "${TAB}${BGN}$(translate "Username:")${CL} ${BL}$USERNAME${CL}"
echo -e
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
view_shares() {
show_proxmenux_logo
msg_title "$(translate "View Current Shares")"
echo -e "$(translate "Current Samba shares in CT") $CTID:"
echo "=================================="
if pct exec "$CTID" -- test -f /etc/samba/smb.conf; then
SHARES=$(pct exec "$CTID" -- awk '/^\[.*\]/ && !/^\[global\]/ && !/^\[homes\]/ && !/^\[printers\]/ {print $0}' /etc/samba/smb.conf)
if [[ -n "$SHARES" ]]; then
while IFS= read -r share_line; do
if [[ -n "$share_line" ]]; then
SHARE_NAME=$(echo "$share_line" | sed 's/\[//g' | sed 's/\]//g')
SHARE_PATH=$(pct exec "$CTID" -- awk "/^\[$SHARE_NAME\]/,/^$/ {if(/path =/) print \$3}" /etc/samba/smb.conf)
echo "$share_line -> $SHARE_PATH"
fi
done <<< "$SHARES"
CT_IP=$(pct exec "$CTID" -- hostname -I | awk '{print $1}')
USERNAME=$(pct exec "$CTID" -- pdbedit -L | awk -F: '{print $1}' | head -n1)
echo ""
echo "=================================="
echo -e "${TAB}${BOLD}$(translate "Connection Details:")${CL}"
echo -e "${TAB}${BGN}$(translate "Server IP:")${CL} ${BL}$CT_IP${CL}"
echo -e "${TAB}${BGN}$(translate "Username:")${CL} ${BL}$USERNAME${CL}"
echo ""
echo -e "${TAB}${BOLD}$(translate "Available Shares:")${CL}"
while IFS= read -r share_line; do
if [[ -n "$share_line" ]]; then
SHARE_NAME=$(echo "$share_line" | sed 's/\[//g' | sed 's/\]//g')
SHARE_PATH=$(pct exec "$CTID" -- awk "/^\[$SHARE_NAME\]/,/^$/ {if(/path =/) print \$3}" /etc/samba/smb.conf)
echo -e "${TAB}${BGN}$(translate "Share name:")${CL} ${BL}$SHARE_NAME${CL}"
echo -e "${TAB}${BGN}$(translate "Share path:")${CL} ${BL}$SHARE_PATH${CL}"
echo -e "${TAB}${BGN}$(translate "Windows path:")${CL} ${YW}\\\\$CT_IP\\$SHARE_NAME${CL}"
echo -e "${TAB}${BGN}$(translate "Linux/Mac path:")${CL} ${YW}smb://$CT_IP/$SHARE_NAME${CL}"
echo ""
fi
done <<< "$SHARES"
else
echo "$(translate "No shares configured.")"
fi
else
echo "$(translate "/etc/samba/smb.conf file does not exist.")"
fi
echo ""
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
delete_share() {
if ! pct exec "$CTID" -- test -f /etc/samba/smb.conf; then
dialog --backtitle "ProxMenux" --title "$(translate "Error")" --msgbox "\n$(translate "No smb.conf file found.")" 8 50
return
fi
SHARES=$(pct exec "$CTID" -- awk '/^\[.*\]/ && !/^\[global\]/ && !/^\[homes\]/ && !/^\[printers\]/ {gsub(/\[|\]/, ""); print NR, $0}' /etc/samba/smb.conf)
if [[ -z "$SHARES" ]]; then
dialog --backtitle "ProxMenux" --title "$(translate "No Shares")" --msgbox "\n$(translate "No shares found in smb.conf.")" 8 60
return
fi
OPTIONS=()
while read -r line; do
[[ -z "$line" ]] && continue
NUM=$(echo "$line" | awk '{print $1}')
SHARE_NAME=$(echo "$line" | awk '{print $2}')
SHARE_PATH=$(pct exec "$CTID" -- awk "/^\[$SHARE_NAME\]/,/^$/ {if(/path =/) print \$3}" /etc/samba/smb.conf)
[[ -z "$SHARE_NAME" ]] && continue
OPTIONS+=("$SHARE_NAME" "$SHARE_NAME -> $SHARE_PATH")
done <<< "$SHARES"
SELECTED_SHARE=$(dialog --backtitle "ProxMenux" --title "$(translate "Delete Share")" --menu "$(translate "Select a share to delete:")" 20 70 10 "${OPTIONS[@]}" 3>&1 1>&2 2>&3)
[ -z "$SELECTED_SHARE" ] && return
SHARE_PATH=$(pct exec "$CTID" -- awk "/^\[$SELECTED_SHARE\]/,/^$/ {if(/path =/) print \$3}" /etc/samba/smb.conf)
if whiptail --yesno "$(translate "Are you sure you want to delete this share?")\n\n$(translate "Share name:"): $SELECTED_SHARE\n$(translate "Share path:"): $SHARE_PATH" 12 70 --title "$(translate "Confirm Deletion")"; then
show_proxmenux_logo
msg_title "$(translate "Delete Share")"
pct exec "$CTID" -- sed -i "/^\[$SELECTED_SHARE\]/,/^$/d" /etc/samba/smb.conf
pct exec "$CTID" -- systemctl restart smbd.service
msg_ok "$(translate "Share deleted and Samba service restarted.")"
fi
echo -e
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
check_samba_status() {
show_proxmenux_logo
msg_title "$(translate "Check Samba Status")"
echo -e "$(translate "Samba Service Status in CT") $CTID:"
echo "=================================="
if pct exec "$CTID" -- dpkg -s samba &>/dev/null; then
echo "$(translate "Samba Server: INSTALLED")"
if pct exec "$CTID" -- systemctl is-active --quiet smbd; then
echo "$(translate "Samba Service: RUNNING")"
else
echo "$(translate "Samba Service: STOPPED")"
fi
if pct exec "$CTID" -- systemctl is-active --quiet nmbd; then
echo "$(translate "NetBIOS Service: RUNNING")"
else
echo "$(translate "NetBIOS Service: STOPPED")"
fi
echo ""
echo "$(translate "Listening ports:")"
pct exec "$CTID" -- ss -tlnp | grep -E ':(139|445)' || echo "$(translate "No Samba ports found")"
echo ""
echo "$(translate "Samba users:")"
pct exec "$CTID" -- pdbedit -L 2>/dev/null || echo "$(translate "No Samba users found")"
else
echo "$(translate "Samba Server: NOT INSTALLED")"
fi
echo ""
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
uninstall_samba() {
if ! pct exec "$CTID" -- dpkg -s samba &>/dev/null; then
dialog --backtitle "ProxMenux" --title "$(translate "Samba Not Installed")" --msgbox "\n$(translate "Samba server is not installed in this CT.")" 8 60
return
fi
if ! whiptail --title "$(translate "Uninstall Samba Server")" \
--yesno "$(translate "WARNING: This will completely remove Samba server from the CT.")\n\n$(translate "This action will:")\n$(translate "• Stop all Samba services")\n$(translate "• Remove all shares")\n$(translate "• Remove all Samba users")\n$(translate "• Uninstall Samba packages")\n$(translate "• Remove Samba groups")\n\n$(translate "Are you sure you want to continue?")" \
18 70; then
return
fi
show_proxmenux_logo
msg_title "$(translate "Uninstall Samba Server")"
msg_info "$(translate "Stopping Samba services...")"
pct exec "$CTID" -- systemctl stop smbd 2>/dev/null || true
pct exec "$CTID" -- systemctl stop nmbd 2>/dev/null || true
pct exec "$CTID" -- systemctl disable smbd 2>/dev/null || true
pct exec "$CTID" -- systemctl disable nmbd 2>/dev/null || true
msg_ok "$(translate "Samba services stopped and disabled.")"
if pct exec "$CTID" -- test -f /etc/samba/smb.conf; then
pct exec "$CTID" -- cp /etc/samba/smb.conf /etc/samba/smb.conf.backup.$(date +%Y%m%d_%H%M%S)
msg_ok "$(translate "Samba configuration backed up.")"
fi
SAMBA_USERS=$(pct exec "$CTID" -- pdbedit -L 2>/dev/null | awk -F: '{print $1}' || true)
if [[ -n "$SAMBA_USERS" ]]; then
while IFS= read -r user; do
if [[ -n "$user" ]]; then
pct exec "$CTID" -- smbpasswd -x "$user" 2>/dev/null || true
fi
done <<< "$SAMBA_USERS"
msg_ok "$(translate "Samba users removed.")"
fi
pct exec "$CTID" -- apt-get remove --purge -y samba samba-common-bin samba-common 2>/dev/null || true
pct exec "$CTID" -- apt-get autoremove -y 2>/dev/null || true
msg_ok "$(translate "Samba packages removed.")"
if pct exec "$CTID" -- getent group sharedfiles >/dev/null 2>&1; then
GROUP_USERS=$(pct exec "$CTID" -- getent group sharedfiles | cut -d: -f4)
if [[ -z "$GROUP_USERS" ]]; then
pct exec "$CTID" -- groupdel sharedfiles 2>/dev/null || true
msg_ok "$(translate "Samba group removed.")"
else
msg_warn "$(translate "Samba group kept (has users assigned).")"
fi
fi
msg_info "$(translate "Cleaning up Samba directories...")"
pct exec "$CTID" -- pkill -f smbd 2>/dev/null || true
pct exec "$CTID" -- pkill -f nmbd 2>/dev/null || true
pct exec "$CTID" -- rm -rf /var/lib/samba 2>/dev/null || true
pct exec "$CTID" -- rm -rf /var/cache/samba 2>/dev/null || true
sleep 2
msg_ok "$(translate "Samba server has been completely uninstalled!")"
echo -e ""
echo -e "${TAB}${BOLD}$(translate "Uninstallation Summary:")${CL}"
echo -e "${TAB}${BGN}$(translate "Services:")${CL} ${BL}$(translate "Stopped and disabled")${CL}"
echo -e "${TAB}${BGN}$(translate "Packages:")${CL} ${BL}$(translate "Removed")${CL}"
echo -e "${TAB}${BGN}$(translate "Users:")${CL} ${BL}$(translate "Removed")${CL}"
echo -e "${TAB}${BGN}$(translate "Configuration:")${CL} ${BL}$(translate "Backed up and cleared")${CL}"
echo -e "${TAB}${BGN}$(translate "Groups:")${CL} ${BL}$(translate "Cleaned up")${CL}"
echo -e
msg_success "$(translate "Press Enter to return to menu...")"
read -r
}
# === Main Menu ===
while true; do
CHOICE=$(dialog --backtitle "ProxMenux" --title "$(translate "Samba Manager - CT") $CTID" --menu "$(translate "Choose an option:")" 20 70 12 \
"1" "$(translate "Create Samba server service")" \
"2" "$(translate "View Current Shares")" \
"3" "$(translate "Delete Share")" \
"4" "$(translate "Check Samba Status")" \
"5" "$(translate "Uninstall Samba Server")" \
"6" "$(translate "Exit")" 3>&1 1>&2 2>&3)
case $CHOICE in
1) create_share ;;
2) view_shares ;;
3) delete_share ;;
4) check_samba_status ;;
5) uninstall_samba ;;
6) exit 0 ;;
*) exit 0 ;;
esac
done

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management # ProxMenux - A menu-driven script for Proxmox VE management
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi

View File

@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management # ProxMenux - A menu-driven script for Proxmox VE management
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management # ProxMenux - A menu-driven script for Proxmox VE management
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management # ProxMenux - A menu-driven script for Proxmox VE management
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - Mount independent disk on Proxmox host # ProxMenux - Mount independent disk on Proxmox host
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - Mount point from host into LXC container (CT) # ProxMenux - Mount point from host into LXC container (CT)
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# License : MIT # License : MIT

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - Mount independent disk on Proxmox host # ProxMenux - Mount independent disk on Proxmox host
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management # ProxMenux - A menu-driven script for Proxmox VE management
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management # ProxMenux - A menu-driven script for Proxmox VE management
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi

View File

@@ -56,7 +56,7 @@ install_system_utils() {
} }
ensure_repositories() { ensure_repositories_() {
local sources_file="/etc/apt/sources.list" local sources_file="/etc/apt/sources.list"
local need_update=false local need_update=false
@@ -85,6 +85,98 @@ EOF
return 0 return 0
} }
ensure_repositories() {
local pve_version need_update=false
pve_version=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+' | head -1)
if [[ -z "$pve_version" ]]; then
msg_error "Unable to detect Proxmox version."
return 1
fi
if (( pve_version >= 9 )); then
# ===== PVE 9 (Debian 13 - trixie) =====
# proxmox.sources (no-subscription) ─ create if missing
if [[ ! -f /etc/apt/sources.list.d/proxmox.sources ]]; then
cat > /etc/apt/sources.list.d/proxmox.sources <<'EOF'
Enabled: true
Types: deb
URIs: http://download.proxmox.com/debian/pve
Suites: trixie
Components: pve-no-subscription
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
EOF
need_update=true
fi
# debian.sources ─ create if missing
if [[ ! -f /etc/apt/sources.list.d/debian.sources ]]; then
cat > /etc/apt/sources.list.d/debian.sources <<'EOF'
Types: deb
URIs: http://deb.debian.org/debian/
Suites: trixie trixie-updates
Components: main contrib non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
Types: deb
URIs: http://security.debian.org/debian-security/
Suites: trixie-security
Components: main contrib non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
EOF
need_update=true
fi
# apt-get update only if needed or lists are empty
if [[ "$need_update" == true ]] || [[ ! -d /var/lib/apt/lists || -z "$(ls -A /var/lib/apt/lists 2>/dev/null)" ]]; then
msg_info "$(translate "Updating APT package lists...")"
apt-get update >/dev/null 2>&1 || apt-get update
fi
else
# ===== PVE 8 (Debian 12 - bookworm) =====
local sources_file="/etc/apt/sources.list"
# Debian base (create or append minimal lines if missing)
if ! grep -qE 'deb .* bookworm .* main' "$sources_file" 2>/dev/null; then
{
echo "deb http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware"
echo "deb http://deb.debian.org/debian bookworm-updates main contrib non-free non-free-firmware"
echo "deb http://security.debian.org/debian-security bookworm-security main contrib non-free non-free-firmware"
} >> "$sources_file"
need_update=true
fi
# Proxmox no-subscription list (classic) if missing
if [[ ! -f /etc/apt/sources.list.d/pve-no-subscription.list ]]; then
echo "deb http://download.proxmox.com/debian/pve bookworm pve-no-subscription" \
> /etc/apt/sources.list.d/pve-no-subscription.list
need_update=true
fi
fi
# apt-get update only if needed or lists are empty
if [[ "$need_update" == true ]] || [[ ! -d /var/lib/apt/lists || -z "$(ls -A /var/lib/apt/lists 2>/dev/null)" ]]; then
msg_info "$(translate "Updating APT package lists...")"
apt-get update >/dev/null 2>&1 || apt-get update
fi
return 0
}
install_single_package() { install_single_package() {
local package="$1" local package="$1"
local command_name="${2:-$package}" local command_name="${2:-$package}"

View File

@@ -72,7 +72,7 @@ ask_run_mode() {
msg_ok "$(translate "Run mode: Unattended")" msg_ok "$(translate "Run mode: Unattended")"
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
export APT_LISTCHANGES_FRONTEND=none export APT_LISTCHANGES_FRONTEND=none
return 0 exit 0
fi fi
@@ -617,13 +617,12 @@ run_pve8to9_check() {
# Error 1: systemd-boot meta-package # Error 1: systemd-boot meta-package
if grep -q 'systemd-boot meta-package installed' "$tmp"; then if grep -q 'systemd-boot meta-package installed' "$tmp"; then
repair_commands+=("apt install systemd-boot-efi systemd-boot-tools -y && apt remove systemd-boot -y") repair_commands+=("apt remove -y systemd-boot")
repair_descriptions+=("$(translate "Fix systemd-boot meta-package conflict")") repair_descriptions+=("$(translate "Remove obsolete systemd-boot meta-package")")
echo -e "${YW}$(translate "Fix systemd-boot:") ${CL}apt install systemd-boot-efi systemd-boot-tools -y && apt remove systemd-boot -y" echo -e "${YW}$(translate "Fix systemd-boot:") ${CL}apt remove -y systemd-boot"
fi fi
# Error 2: Kernel version mismatch # Error 2: Kernel version mismatch
if grep -q -E '(kernel.*mismatch|kernel.*version)' "$tmp"; then if grep -q -E 'FAIL:.*(kernel.*mismatch|kernel.*version.*mismatch)' "$tmp"; then
repair_commands+=("update-grub") repair_commands+=("update-grub")
repair_descriptions+=("$(translate "Update kernel to compatible version")") repair_descriptions+=("$(translate "Update kernel to compatible version")")
echo -e "${YW}$(translate "Fix kernel version:") ${CL}update-grub" echo -e "${YW}$(translate "Fix kernel version:") ${CL}update-grub"
@@ -651,18 +650,12 @@ run_pve8to9_check() {
fi fi
# Error 6: Disk space issues # Error 6: Disk space issues
if grep -q -E '(disk.*space|storage.*full|no.*space)' "$tmp"; then if grep -q -E 'FAIL:.*(disk space|no space left|storage.*full)' "$tmp"; then
repair_commands+=("apt clean && apt autoremove -y && journalctl --vacuum-time=7d") repair_commands+=("apt clean && apt autoremove -y && journalctl --vacuum-time=7d")
repair_descriptions+=("$(translate "Free up disk space")") repair_descriptions+=("$(translate "Free up disk space")")
echo -e "${YW}$(translate "Fix disk space:") ${CL}apt clean && apt autoremove -y && journalctl --vacuum-time=7d" echo -e "${YW}$(translate "Fix disk space:") ${CL}apt clean && apt autoremove -y && journalctl --vacuum-time=7d"
fi fi
# Error 7: Network/DNS issues
if grep -q -E '(network.*error|dns.*problem|connection.*failed)' "$tmp"; then
repair_commands+=("systemctl restart networking && systemctl restart systemd-resolved")
repair_descriptions+=("$(translate "Fix network connectivity")")
echo -e "${YW}$(translate "Fix network:") ${CL}systemctl restart networking && systemctl restart systemd-resolved"
fi
echo -e echo -e
@@ -990,47 +983,45 @@ run_pve8to9_check2() {
# Error 1: systemd-boot meta-package # Error 1: systemd-boot meta-package
if grep -q 'systemd-boot meta-package installed' "$tmp"; then if grep -q 'systemd-boot meta-package installed' "$tmp"; then
repair_commands+=("apt install systemd-boot-efi systemd-boot-tools -y && apt remove systemd-boot -y") repair_commands+=("apt remove -y systemd-boot")
repair_descriptions+=("$(translate "Fix systemd-boot meta-package conflict")") repair_descriptions+=("$(translate "Remove obsolete systemd-boot meta-package")")
echo -e "${YW}$(translate "Fix systemd-boot:") ${CL}apt install systemd-boot-efi systemd-boot-tools -y && apt remove systemd-boot -y" echo -e "${YW}$(translate "Fix systemd-boot:") ${CL}apt remove -y systemd-boot"
fi
# Error 2: Kernel version mismatch
if grep -q -E 'FAIL:.*(kernel.*mismatch|kernel.*version.*mismatch)' "$tmp"; then
repair_commands+=("update-grub")
repair_descriptions+=("$(translate "Update kernel to compatible version")")
echo -e "${YW}$(translate "Fix kernel version:") ${CL}update-grub"
fi fi
# Error 3: Ceph version incompatible
# Error 2: Ceph version incompatible
if grep -q -E '(ceph.*version|ceph.*incompatible)' "$tmp"; then if grep -q -E '(ceph.*version|ceph.*incompatible)' "$tmp"; then
repair_commands+=("ceph versions && pveceph upgrade") repair_commands+=("ceph versions && pveceph upgrade")
repair_descriptions+=("$(translate "Upgrade Ceph to compatible version")") repair_descriptions+=("$(translate "Upgrade Ceph to compatible version")")
echo -e "${YW}$(translate "Fix Ceph version:") ${CL}ceph versions && pveceph upgrade" echo -e "${YW}$(translate "Fix Ceph version:") ${CL}ceph versions && pveceph upgrade"
fi fi
# Error 3: Repository configuration issues # Error 4: Repository configuration issues
if grep -q -E '(repository.*issue|repo.*problem|sources.*error)' "$tmp"; then if grep -q -E '(repository.*issue|repo.*problem|sources.*error)' "$tmp"; then
repair_commands+=("cleanup_duplicate_repos && configure_repositories") repair_commands+=("cleanup_duplicate_repos && configure_repositories")
repair_descriptions+=("$(translate "Fix repository configuration")") repair_descriptions+=("$(translate "Fix repository configuration")")
echo -e "${YW}$(translate "Fix repositories:") ${CL}cleanup_duplicate_repos && configure_repositories" echo -e "${YW}$(translate "Fix repositories:") ${CL}cleanup_duplicate_repos && configure_repositories"
fi fi
# Error 4: Package conflicts # Error 5: Package conflicts
if grep -q -E '(package.*conflict|dependency.*problem)' "$tmp"; then if grep -q -E '(package.*conflict|dependency.*problem)' "$tmp"; then
repair_commands+=("apt update && apt autoremove -y && apt autoclean") repair_commands+=("apt update && apt autoremove -y && apt autoclean")
repair_descriptions+=("$(translate "Resolve package conflicts")") repair_descriptions+=("$(translate "Resolve package conflicts")")
echo -e "${YW}$(translate "Fix package conflicts:") ${CL}apt update && apt autoremove -y && apt autoclean" echo -e "${YW}$(translate "Fix package conflicts:") ${CL}apt update && apt autoremove -y && apt autoclean"
fi fi
# Error 5: Disk space issues # Error 6: Disk space issues
if grep -q -E '(disk.*space|storage.*full|no.*space)' "$tmp"; then if grep -q -E 'FAIL:.*(disk space|no space left|storage.*full)' "$tmp"; then
repair_commands+=("apt clean && apt autoremove -y && journalctl --vacuum-time=7d") repair_commands+=("apt clean && apt autoremove -y && journalctl --vacuum-time=7d")
repair_descriptions+=("$(translate "Free up disk space")") repair_descriptions+=("$(translate "Free up disk space")")
echo -e "${YW}$(translate "Fix disk space:") ${CL}apt clean && apt autoremove -y && journalctl --vacuum-time=7d" echo -e "${YW}$(translate "Fix disk space:") ${CL}apt clean && apt autoremove -y && journalctl --vacuum-time=7d"
fi fi
# Error 6: Network/DNS issues
if grep -q -E '(network.*error|dns.*problem|connection.*failed)' "$tmp"; then
repair_commands+=("systemctl restart networking && systemctl restart systemd-resolved")
repair_descriptions+=("$(translate "Fix network connectivity")")
echo -e "${YW}$(translate "Fix network:") ${CL}systemctl restart networking && systemctl restart systemd-resolved"
fi
echo -e echo -e

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# ========================================================== # ==========================================================
# ProxMenu - A menu-driven script for Proxmox VE management # ProxMenux - A menu-driven script for Proxmox VE management
# ========================================================== # ==========================================================
# Author : MacRimi # Author : MacRimi
# Copyright : (c) 2024 MacRimi # Copyright : (c) 2024 MacRimi

Some files were not shown because too many files have changed in this diff Show More