Compare commits

..
Author SHA1 Message Date
ssongliu 0d548c7df9 fix: Fix http image registry edit exception (#9460) 2025-07-08 18:18:00 +08:00
ssongliu 8ec5a5bd4b fix: Fix menu internationalization issues (#9456) 2025-07-08 06:05:55 +00:00
ssongliu 2e8bcc09e6 fix: Fix dashboard refresh exception issue (#9455) 2025-07-08 04:15:53 +00:00
ssongliu 29a304e2c8 fix: Optimize container image repo restart detection logic (#9447) 2025-07-08 02:47:53 +00:00
ssongliu b1adc37920 fix: Modify OneDrive default connection settings (#9440) 2025-07-07 10:05:50 +00:00
CityFun 03fac3ec06 fix: Resolve the issue of lost GPU configuration after upgrade ollama (#9362) 2025-07-01 07:47:33 +00:00
蘭 6e465ac215 fix: Fix the issue of multiple selection moving folder errors (#9319)
refs: #8612
2025-06-27 06:03:24 +00:00
ssongliu 21bbadc37b fix: Resolve abnormal state after Docker restart (#9305) 2025-06-26 11:19:22 +00:00
ssongliu 3061d640ce fix: Modify the service status judgment logic (#9301) 2025-06-26 17:18:14 +08:00
ssongliu 874717cbcd fix: Remove code comments (#9297) 2025-06-26 06:39:22 +00:00
ssongliu 6850acabae fix: Optimize supervisor initialization logic (#9295) 2025-06-26 06:29:23 +00:00
ssongliu 890ea42749 fix: Fix ClamAV service name detection issue (#9292) 2025-06-26 14:27:57 +08:00
ssongliu 6aaf61cde4 fix: Increase systemctl operation timeout (#9281) 2025-06-25 08:21:22 +00:00
wanghe-fit2cloud 73930c5e29 feat: Compatible with system upgrades for v1.10.30 and earlier versions 2025-06-24 17:32:23 +08:00
ssongliu 8f8d280e90 Revert "fix: Fix the issue with upgrade failures (#9262)" (#9264)
This reverts commit b46192024f.
2025-06-24 17:24:42 +08:00
ssongliu b46192024f fix: Fix the issue with upgrade failures (#9262) 2025-06-24 08:51:13 +00:00
ssongliu 8f939ad5be fix: Make firewall operations compatible with OpenWrt (#9257) 2025-06-24 06:21:13 +00:00
ssongliu a6363ce4e5 fix: Fix the issue with cronjob report loading failure (#9254)
Refs #8684
2025-06-24 05:47:14 +00:00
ssongliu fd490b2642 fix: Modify the image repository validation rules (#9253)
Refs #8870
2025-06-24 05:45:13 +00:00
ssongliu 7624670ee0 fix: Add a restart prompt for HTTP-type mirror repositories (#9252)
Refs #8978
2025-06-24 05:43:14 +00:00
ssongliu 8c3145e1c6 fix: Fix the problem of ip changes in container upgrades for macvlan … (#9251)
Refs #8950
2025-06-24 05:41:13 +00:00
ssongliu bec7b6989f fix: Fix the issue of abnormal highlighting in the cronjob script (#9246)
Refs #8683
2025-06-23 10:40:14 +00:00
CityFun 326f2654e6 feat: add PbootCMS extensions template for php runtime (#9243) 2025-06-23 18:37:47 +08:00
CityFun 708499a335 feat: fix issue with build frontend failed (#8994) 2025-06-10 08:56:35 +00:00
CityFun bdeb842939 feat: change 1panel v1 docs url (#8993) 2025-06-10 08:16:34 +00:00
王贺 edefdce3ff docs: Update the installation script (#8979) 2025-06-10 11:03:13 +08:00
longjuan b348c36571 feat: add name attrs to login inputs for password managers to recognize (#8953) 2025-06-09 11:26:02 +08:00
玲珑芯ExquisiteCore 6ae150b64e feat: Move current status to first position in MySQL settings and set… (#8882)
* feat: Move current status to first position in MySQL and Redis settings and set as default tab
2025-06-03 10:22:00 +08:00
maninhill 3687b9bea2 chore: Update README.md 2025-05-25 16:42:01 +08:00
maninhill 711431da82 Update README.md 2025-05-25 16:40:02 +08:00
Alan e93afc33aa ini filetype support (#8628) 2025-05-14 16:52:41 +08:00
CityFun 82dd993751 fix: Fix issue with update website config (#8610) 2025-05-12 10:41:58 +00:00
Wankko Ree 9ba293e154 fix: the docker image which no tag but used be pruned (#8529) 2025-05-03 13:25:24 +08:00
igophper 8fa9f128d8 fix(cronjob): prevent unnecessary log updates and clean log formatting (#8481)
Refs #8425
2025-04-28 09:09:48 +08:00
maninhill ed9f1599e6 chore: Update README.md 2025-04-25 11:38:18 +08:00
igophper 787a29769d fix(editor): reset edit state after not saving content (#8474)
Refs #8461
2025-04-25 11:13:23 +08:00
巴山夜语andgcsong023 6b379389d3 WIP: refactor(service): Refactor OpenRC service manager (#8416)
* refactor(service): 重构 OpenRC 服务管理器

- 更新 IsEnabled 和 IsActive 检查逻辑,使用更可靠的命令
- 修复 ServiceExists 检查,直接使用文件路径判断
- 优化 FindServices 函数,扫描 /etc/init.d 目录
- 调整 BuildCommand 函数,支持 OpenRC 特定操作
- 修改 ParseStatus 函数,使用更新后的正则表达式

* feat(backend): 优化 Fail2ban 初始化配置以支持 Alpine 系统

- 增加对 Alpine 系统的特殊配置支持
- 改进防火墙类型检测逻辑,支持多种防火墙服务
- 增加 SSH 端口和认证日志路径的自动检测
- 优化配置文件模板,提高兼容性和安全性

* refactor(backend): 重构 SSH 日志解析功能

- 改进了对不同日志格式的支持,包括 secure, auth 和 messages 文件
- 优化了日志解析逻辑,提高了代码的可读性和可维护性
- 增加了对 RFC3339 时间格式的支持
- 改善了对失败登录尝试的解析,包括无效用户和连接关闭的情况
- 重构了日期解析和 IP 地址验证的逻辑

* refactor(upgrade): 优化升级服务中的初始化脚本选择逻辑

- 新增 selectInitScript 函数,根据系统初始化管理器类型选择合适的初始化脚本
- 支持 systemd、openrc 和 sysvinit 三种初始化管理器
- 对于 sysvinit,增加对 /etc/rc.common 文件存在性的判断,以区分不同的初始化脚本
- 默认情况下使用当前服务名称作为初始化脚本名称

* fix(upgrade): 修复升级时初始化脚本更新问题

- 修改了 criticalUpdates 数组中的服务脚本更新逻辑
- 在 selectInitScript 函数中增加了复制脚本文件的逻辑,以应对服务名和脚本名不一致的情况

* feat(snap): 添加初始化脚本到快照

- 在创建快照时,将服务脚本复制到 initscript 目录
- 然后将整个 initscript 目录复制到快照的目标目录
- 添加了日志输出,便于调试和记录

* refactor(backend): 重构快照恢复流程

- 移除了未使用的 import 语句
- 删除了注释掉的代码块
- 修改了 1Panel 服务恢复的逻辑,增加了对当前服务名称的获取
- 快照恢复过程中,根据宿主机类型,自动选择初始化服务脚本
- 添加了日志输出以提高可追踪性
- 优化了文件路径的处理方式

---------

Co-authored-by: gcsong023 <gcsong023@users.noreply.github.com>
2025-04-23 17:04:08 +08:00
ChengPlay 5bcbf32823 Fix issue with errors when reverse proxy source text includes comments. (#8444)
Refs https://github.com/1Panel-dev/1Panel/issues/8442
2025-04-22 07:51:06 +00:00
maninhill 0aa90e1b2d chore: Update README.md 2025-04-19 09:59:23 +08:00
maninhill a7b87211b4 chore: Update README.md 2025-04-19 09:53:46 +08:00
maninhill 2c666933a5 chore: Update README.md 2025-04-19 09:43:31 +08:00
maninhill f280b533c6 Update README.md 2025-04-19 09:42:05 +08:00
wanghe-fit2cloud 47a16bc9a9 chore(build): optimize build script 2025-04-18 14:48:50 +08:00
ChengPlay 713202b6eb feat: update golang.org/x/net version (#8412) 2025-04-17 03:02:20 +00:00
巴山夜语andgcsong023 79020abb1c feat(systemctl): implement service manager initialization and command execution (#8380)
* feat(systemctl): 实现服务管理器初始化和命令执行

- 新增 systemctl 包,实现对 systemd、openrc 和 sysvinit 三种服务管理器的支持
- 添加服务状态检查、启动、停止、重启和启用/禁用功能
- 实现服务发现和智能服务名处理
- 添加配置文件查看功能
- 优化错误处理和日志记录

* refactor(system): 重构系统服务管理逻辑

- 引入 systemctl 工具包以统一处理系统服务
- 优化服务状态获取、配置文件路径解析等逻辑
- 重构 HostToolService 中的 GetToolStatus 方法
- 更新 DockerService、SettingService 等相关服务的处理方式
- 调整快照创建和恢复过程中的服务处理逻辑

* feat(utils): 添加目录复制功能并优化文件复制逻辑

- 新增 CopyDirs 函数,用于复制整个目录及其内容
- 添加对符号链接的复制支持
- 实现通用的 Copy 函数,根据文件类型自动选择 CopyFile 或 CopyDirs
- 在 CopyFile 函数中增加对源文件是目录的检查和错误提示

* refactortoolbox: 重构 Fail2ban 和 Pure-FTPd 的管理逻辑

- 优化了 Fail2ban 和 Pure-FTPd 的启动、停止、重启等操作的实现
- 改进了 Fail2ban 版本信息的获取方法
- 统一了错误处理和日志记录的格式
- 调整了部分导入的包,提高了代码的可维护性

* build: 禁用 CGO 以提高构建性能和兼容性

- 在 Linux 后端构建命令中添加 CGO_ENABLED=0 环境变量
- 此修改可以提高构建速度,并确保生成的二进制文件在没有 C 库依赖的环境中也能运行

* refactor(docker): 重构 Docker 服务的重启和操作逻辑

- 添加 isDockerSnapInstalled 函数来判断 Docker 是否通过 Snap 安装
- 在 OperateDocker 和 restartDocker 函数中增加对 Snap 安装的处理
- 移除未使用的 getDockerRestartCommand 函数

* fix(service): 优化快照恢复后的服务重启逻辑

- 在使用 systemd 管理服务时,增加 daemon-reload 操作以确保服务配置更新
- 重启 1panel 服务,以应用快照恢复的更改

* refactor(server): 支持非 systemd 系统的恢复操作

- 增加 isSystemd 函数判断系统是否为 systemd 类型
- 根据系统类型选择性地恢复服务文件
- 兼容 systemd 和非 systemd 系统的恢复流程

* fix(upgrade): 优化升级过程中的服务重启逻辑

- 移动服务重启逻辑到版本号更新之后,修复因提前重启导致的版本号未更新BUG。
- 在 systemctl 重启之前添加 daemon-reload 命令

---------

Co-authored-by: gcsong023 <gcsong023@users.noreply.github.com>
2025-04-17 10:26:13 +08:00
chiichen 83ef41cf1a feat: basic support riscv64 (#8369)
* feat: add riscv64

* feat: correct arch related codes
2025-04-17 10:24:04 +08:00
dependabot[bot] 91675c69e9 chore(deps): bump golang.org/x/crypto from 0.32.0 to 0.35.0 (#8400)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.32.0 to 0.35.0.
- [Commits](https://github.com/golang/crypto/compare/v0.32.0...v0.35.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.35.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-15 11:41:42 +08:00
ChengPlay 0e68065b40 feat: change commandPlaceHolder translate (#8391) 2025-04-14 08:56:12 +00:00
ssongliu 600b769083 fix: Fix the mysql root account permission switchover exception (#8378) 2025-04-11 07:08:02 +00:00
zhengkunwang e7670f96de feat: update mcp proxy_pass (#8374) 2025-04-10 16:45:24 +08:00
zhengkunwang 342c04b30a fix: fix issue with mcp bind website (#8356) 2025-04-09 03:55:56 +00:00
zhengkunwang 5ab8377ac1 feat: update allowPort check (#8355) 2025-04-09 02:47:54 +00:00
ssongliu 74eb2c8dd6 fix: Fix the problem that jumping to a directory that does not exist … (#8353) 2025-04-08 14:33:54 +00:00
zhengkunwang f136d97941 change mcp helper (#8352) 2025-04-08 10:59:53 +00:00
ssongliu a5629e3f42 fix: Fix the problem of failed MySQL permission modification (#8351) 2025-04-08 10:49:53 +00:00
zhengkunwang 515d6ca500 fix: fix bug with mcp-server status error (#8349) 2025-04-08 09:21:53 +00:00
zhengkunwang c0ccc0318d feat: add warn text for mcp server (#8346) 2025-04-08 05:55:52 +00:00
zhengkunwang 8196c11010 feat: import mcp server support env (#8344) 2025-04-08 03:11:52 +00:00
ssongliu 170bd22062 fix: Modify database verification rules (#8342) 2025-04-08 02:39:52 +00:00
zhengkunwang 097a33c68f style: update style for mcp server (#8338) 2025-04-07 11:09:51 +00:00
zhengkunwang 3be3bf9926 feat: remove env from mcp config (#8337) 2025-04-07 10:03:50 +00:00
zhengkunwang 038f2b4f54 feat: change mcp list style (#8336) 2025-04-07 09:35:50 +00:00
zhengkunwang f84e284c20 feat: change DNSTypes order (#8335) 2025-04-07 09:01:50 +00:00
ssongliu 48e75b5916 fix: Set bridge as the default network for container creation. (#8332) 2025-04-07 07:47:49 +00:00
zhengkunwang a12ceaee6e feat: add config for mcp server (#8331) 2025-04-07 07:41:51 +00:00
zhengkunwang 9050680115 feat: update mcp form label (#8327) 2025-04-07 02:49:49 +00:00
zhengkunwang 0022a9b508 fix: fix issue with install app no label (#8326) 2025-04-07 02:47:49 +00:00
zhengkunwang 3437d62f31 feat: Website SSL DNS Type support WestCN (#8305) 2025-04-02 10:21:54 +00:00
ssongliu dc880835d0 fix: Modify the container terminal connection mode (#8301) 2025-04-02 10:19:54 +00:00
ssongliu 1663bf8677 fix: Regenerates the interface document (#8298) 2025-04-02 09:07:54 +00:00
ssongliu aed236cbc9 fix: Ignore the scheduled task backup content failed to be uploaded (#8297)
Refs #7869
2025-04-02 09:05:54 +00:00
ssongliu 90be74504e feat: Support the retention of remote backup files when deleting sche… (#8296)
Refs #8236
2025-04-02 09:03:55 +00:00
zhengkunwang bba8aab18c feat: Add MCP management (#8299) 2025-04-02 09:01:54 +00:00
ssongliu 47d135ecca fix: Optimize database password verification and password change (#8294) 2025-04-02 16:03:44 +08:00
ssongliu d96fe49993 fix: Resolve cloudns reference error (#8293) 2025-04-02 03:41:53 +00:00
ssongliu ac396b61ce fix: Modify the panel password verification rule (#8291)
Refs #4290
2025-04-02 03:39:54 +00:00
ssongliu 6798d0c7ee fix: Fixed an update failure when the container network is bridge (#8290)
Refs #8209
2025-04-02 03:37:53 +00:00
亖 1490ed7541 dev:修复注释路由前缀 (#8292) 2025-04-02 10:10:00 +08:00
白铭骢 (Mingcong Bai) 48b3032de6 chore(deps): update modernc.org/sqlite to v1.29.7 (#8260)
With modernc.org/sqlite@v1.29.1, 1Panel fails to build on LoongArch with
the following error:

../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/defs.go:13:35: undefined: sqlite3_index_constraint
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/defs.go:14:35: undefined: sqlite3_index_orderby
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/defs.go:15:35: undefined: sqlite3_index_constraint_usage
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/mutex.go:19:5: undefined: Xsqlite3_threadsafe
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/mutex.go:29:11: undefined: Xsqlite3_config
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/mutex.go:29:32: undefined: SQLITE_CONFIG_MUTEX
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/mutex.go:29:122: undefined: SQLITE_OK
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/mutex.go:30:8: undefined: Xsqlite3_errstr
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/mutex.go:40:17: undefined: Sqlite3_mutex_methods
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/mutex.go:239:46: undefined: SQLITE_OK
../../go/pkg/mod/modernc.org/sqlite@v1.29.1/lib/mutex.go:239:46: too many errors

Upstream introduced LoongArch (loong64) target support in
modernc.org/sqlite@v1.29.7. Take a conservative approach and bump
modernc.org/sqlite to v1.29.7, this will allow for 1Panel to build on
LoongArch (both frontend and backend).

Signed-off-by: Mingcong Bai <jeffbai@aosc.io>
2025-03-27 13:45:10 +08:00
sunsky ed12bec460 feat: add DNS provider ClouDNS (#8207)
* feat: add DNS provider ClouDNS

* feat: ClouDNS account parameter validation
2025-03-25 10:12:14 +08:00
dependabot[bot] 3c4a1ea614 chore(deps): bump github.com/golang-jwt/jwt/v4 from 4.5.1 to 4.5.2 (#8229)
Bumps [github.com/golang-jwt/jwt/v4](https://github.com/golang-jwt/jwt) from 4.5.1 to 4.5.2.
- [Release notes](https://github.com/golang-jwt/jwt/releases)
- [Changelog](https://github.com/golang-jwt/jwt/blob/main/VERSION_HISTORY.md)
- [Commits](https://github.com/golang-jwt/jwt/compare/v4.5.1...v4.5.2)

---
updated-dependencies:
- dependency-name: github.com/golang-jwt/jwt/v4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-24 17:38:30 +08:00
ssongliu 79eb0288b4 fix: Fix the problem that the terminal exit fails (#8189) 2025-03-19 03:43:21 +00:00
蘭 99775e1eb4 fix: Fix some issues (#8178) 2025-03-18 17:16:23 +08:00
zhengkunwang db848b41d2 feat: update buildx and containerd version (#8183) 2025-03-18 17:13:36 +08:00
david082321 b5b11dff86 Update tw.ts (#8161) 2025-03-17 10:58:19 +08:00
zhengkunwang 9cf4bfc606 feat: update golang.org/x/net version to v0.37.0 (#8146) 2025-03-13 18:30:25 +08:00
ThinkStu 221162d44f Update zh.ts (#8136)
correct typos
2025-03-12 22:43:20 +08:00
蘭 617970a1f8 fix: license setting data (#8132) 2025-03-12 16:25:32 +08:00
ssongliu 5610c73d67 fix: Fix the problem that services exit unexpectedly when the contain… (#8127) 2025-03-12 03:19:03 +00:00
ssongliu 51fe02661f fix: Fix the problem of incorrect calculation of container memory res… (#8125)
Refs #8033
2025-03-12 02:13:03 +00:00
lm379andlm379 8f491b2933 feat: allow tencent cloud cos dual stack domain (#8123)
Co-authored-by: lm379 <lm379@qq.com>
2025-03-11 23:58:23 +08:00
王贺 b7e16bd6d5 Update .goreleaser.yaml (#8118) 2025-03-11 13:50:48 +08:00
ssongliu 38ca2ddd01 fix(container): Fix cpu surge when the container terminal executes exit (#8115) 2025-03-10 23:47:50 +08:00
ssongliu 402b41100b fix(container): Fix cpu surge when the container terminal executes exit (#8096)
Refs #7574
2025-03-07 11:06:59 +00:00
ssongliu 766beae4e1 fix(container): Fix the problem that a container cannot create a read… (#8095)
Refs #8074
2025-03-07 07:44:59 +00:00
maninhill 05f72e9dcf chore: Update README.md 2025-03-06 11:40:56 +08:00
igophper 23553c68ae fix(fail2ban): fix the issue of set fail2ban log path err (#8076)
Refs #8075
2025-03-06 09:44:48 +08:00
zhengkunwang 4dd1eb7a1e feat: Modify Swagger route response for unauthenticated access (#8057) 2025-03-04 05:48:57 +00:00
蘭 db307732c6 feat: Adjust license (#8056) 2025-03-03 14:32:56 +00:00
igophperandssongliu 33d4ade1d4 refactor(container): optimize memory calculation and concurrent data handling (#8038)
* refactor(container): optimize memory calculation and concurrent data handling

Refs #8033

* Update container.go

---------

Co-authored-by: ssongliu <73214554+ssongliu@users.noreply.github.com>
2025-03-03 17:38:32 +08:00
igophper 186075ff9a style: optimize code editor file path style (#8039)
Refs #8015
2025-03-03 10:23:24 +08:00
zhengkunwang 34df745d1c style: Change wget page style (#8016) 2025-02-26 10:16:54 +00:00
zhengkunwang ec363ac157 feat: update default app.yaml (#8011) 2025-02-26 15:35:54 +08:00
蘭 193b0f3cd4 fix: Update swagger docs (#8002)
Refs #8001
2025-02-26 10:55:40 +08:00
zhengkunwang 67c1bf2924 feat: Add certificate validation when creating AI Proxy (#7994) 2025-02-25 15:21:16 +08:00
ssongliu b1373ff77e feat: Support gpu of schema v11 (#7988) 2025-02-25 14:16:34 +08:00
dependabot[bot] 55d0e27212 chore(deps): bump github.com/go-jose/go-jose/v4 from 4.0.4 to 4.0.5
Bumps [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose) from 4.0.4 to 4.0.5.
- [Release notes](https://github.com/go-jose/go-jose/releases)
- [Changelog](https://github.com/go-jose/go-jose/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-jose/go-jose/compare/v4.0.4...v4.0.5)

---
updated-dependencies:
- dependency-name: github.com/go-jose/go-jose/v4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-25 09:34:14 +08:00
zhengkunwang 1c052b7c13 fix: Fix the issue with AI proxy certificate res error (#7983) 2025-02-24 22:24:51 +08:00
蘭 6c8d48c2dc feat: API interface IP whitelist supports IPv6 (#7981)
Refs #7836
2025-02-24 10:05:13 +00:00
ssongliu b61dae638b fix(forward): Fix the problem of duplicate creation of port forwardin… (#7975)
Refs #7969
2025-02-24 09:21:14 +00:00
zhengkunwang 872a93aa07 fix: Fix the issue with the homepage route refresh (#7974) 2025-02-24 15:00:20 +08:00
igophper b1f337020c fix(container/log): enable close-on-click-modal and wrap log update in nextTick (#7960)
Refs #7547
2025-02-24 13:34:51 +08:00
maninhill 56858dd74c chore: Update README.md 2025-02-24 10:54:05 +08:00
Johannes Plunien 358415f737 feat: Support freemyip.com DNS provider (#7958) 2025-02-24 10:21:37 +08:00
david082321 5e73260103 feat: Update zh-Hant translation (#7966) 2025-02-22 23:16:38 +08:00
zhengkunwang e169f46784 fix: Fixed the issue of no echo after binding the SSL to the AI. (#7953) 2025-02-21 09:29:51 +00:00
ssongliu 39416abd47 fix: Fix the issue of inconsistent deletion status for the ollama model (#7950)
Refs #7948
2025-02-21 08:51:51 +00:00
wanghe-fit2cloud 640a0ca815 chore: Optimize some command outputs 2025-02-21 16:45:56 +08:00
ssongliu 8e54cd5162 style: Adjust the width display of the model list (#7946) 2025-02-21 07:54:29 +00:00
ssongliu 0f0099accb fix: Fix the issue of loading errors for editing container port data (#7945)
Refs #7938
2025-02-21 04:01:49 +00:00
182 changed files with 7700 additions and 3253 deletions
+6 -4
View File
@@ -10,7 +10,7 @@ before:
- ./ci/script.sh
- sed -i 's@ORIGINAL_VERSION=.*@ORIGINAL_VERSION=v{{ .Version }}@g' 1pctl
- go mod tidy
builds:
- main: ./cmd/server/main.go
binary: 1panel
@@ -31,17 +31,19 @@ builds:
- arm
- ppc64le
- s390x
- riscv64
archives:
- format: tar.gz
- formats: [ 'tar.gz' ]
name_template: "1panel-v{{ .Version }}-{{ .Os }}-{{ .Arch }}{{- if .Arm }}v{{ .Arm }}{{ end }}"
wrap_in_directory: true
files:
- 1pctl
- 1panel.service
- install.sh
- GeoIP.mmdb
- 1panel.service
- initscript/*
- lang/*
- GeoIP.mmdb
checksum:
name_template: 'checksums.txt'
+1 -1
View File
@@ -23,7 +23,7 @@ build_frontend:
build_backend_on_linux:
cd $(SERVER_PATH) \
&& GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(APP_NAME) $(MAIN)
&& CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(APP_NAME) $(MAIN)
build_backend_on_darwin:
cd $(SERVER_PATH) \
+16 -5
View File
@@ -1,5 +1,5 @@
<p align="center"><a href="https://1panel.pro"><img src="https://resource.1panel.pro/img/1panel-logo.png" alt="1Panel" width="300" /></a></p>
<p align="center"><b>Top-Rated Web-based Linux Server Management Tool</b><br>Best VPS control panel<br>新一代的 Linux 服务器运维管理面板</p>
<p align="center"><b>Simplify Linux Server Management</b></p>
<p align="center">
<a href="https://trendshift.io/repositories/2462" target="_blank"><img src="https://trendshift.io/api/badge/repositories/2462" alt="1Panel-dev%2F1Panel | Trendshift" style="width: 240px; height: auto;" /></a>
</p>
@@ -31,13 +31,14 @@
------------------------------
1Panel is an open-source, modern web-based control panel for Linux server management.
1Panel provides an intuitive web interface and MCP Server to manage websites, files, containers, databases, and LLMs on a Linux server.
- **Efficient Management**: Through a user-friendly web graphical interface, 1Panel enables users to effortlessly manage their Linux servers. Key features include host monitoring, file management, database administration, and container management.
- **Efficient Management**: Through a user-friendly web graphical interface, 1Panel enables users to effortlessly manage their Linux servers. Key features include host monitoring, file management, database administration, container management, LLMs management.
- **Rapid Website Deployment**: With deep integration of the popular open-source website building software WordPress, 1Panel streamlines the process of domain binding and SSL certificate configuration, all achievable with just one click.
- **Application Store**: 1Panel curates a wide range of high-quality open-source tools and applications, facilitating easy installation and updates for its users.
- **Security and Reliability**: By leveraging containerization and secure application deployment practices, 1Panel minimizes vulnerability exposure. It further enhances security through integrated firewall management and log auditing capabilities.
- **One-Click Backup & Restore**: Data protection is made simple with 1Panel's one-click backup and restore functionality, supporting various cloud storage solutions to ensure data integrity and availability.
- **MCP Server**: [1Panel MCP Server](https://github.com/1Panel-dev/mcp-1panel) allow user to execute server operations via natural language.
## Quick Start
@@ -49,7 +50,7 @@ curl -sSL https://resource.1panel.pro/quick_start.sh -o quick_start.sh && bash q
Please refer to our [documentation](https://docs.1panel.pro/quick_start/) for more details.
中国用户请使用这个 [安装脚本](https://1panel.cn/docs/installation/online_installation/),其应用数量比国际版本更丰富。
中国用户请使用此 [安装脚本](https://1panel.cn/docs/v2/installation/online_installation/),默认安装 V2 版本,应用数量相较国际版本更丰富。
## Screenshot
@@ -61,12 +62,18 @@ Please refer to our [documentation](https://docs.1panel.pro/quick_start/) for mo
## Pro Edition
Compared to the OSS Edition, 1Panel Pro Edition provides users with a wealth of enhanced features and technical support services. Enhanced features include WAF enhancement, website tamper protection, website monitoring, GPU monitoring, custom logo and theme color, etc. [Click to view the detailed introduction of the Pro Edition](https://1panel.pro/pricing).
Compared to the OSS Edition, 1Panel Pro Edition provides users with a wealth of enhanced features and technical support services. Enhanced features include WAF enhancement, Website monitoring, Mobile APP, custom logo and theme, etc.
[Click to see Pro Edition details](https://1panel.pro/pricing).
## Security Information
If you discover any security issues, please refer to [SECURITY.md](/SECURITY.md).
## GitAds Sponsored
[![Sponsored by GitAds](https://gitads.dev/v1/ad-serve?source=1panel-dev/1panel@github)](https://gitads.dev/v1/ad-track?source=1panel-dev/1panel@github)
## License
Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
@@ -74,3 +81,7 @@ Licensed under The GNU General Public License version 3 (GPLv3) (the "License")
<https://www.gnu.org/licenses/gpl-3.0.html>
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
<!-- GitAds-Verify: 9M8AMBB11GYJ3RUNXGB5R1MR71KPATU9 -->
+1 -1
View File
@@ -391,7 +391,7 @@ func (b *BaseApi) ContainerOperation(c *gin.Context) {
// @Tags Container
// @Summary Container stats
// @Param id path integer true "container id"
// @Param id path string true "container id"
// @Success 200 {object} dto.ContainerStats
// @Security ApiKeyAuth
// @Security Timestamp
+2
View File
@@ -68,4 +68,6 @@ var (
favoriteService = service.NewIFavoriteService()
websiteCAService = service.NewIWebsiteCAService()
mcpServerService = service.NewIMcpServerService()
)
+12 -3
View File
@@ -410,11 +410,20 @@ func (b *BaseApi) CheckFile(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if _, err := os.Stat(req.Path); err != nil {
helper.SuccessWithData(c, false)
fileOp := files.NewFileOp()
if fileOp.Stat(req.Path) {
helper.SuccessWithData(c, true)
return
}
helper.SuccessWithData(c, true)
if req.WithInit {
if err := fileOp.CreateDir(req.Path, 0644); err != nil {
helper.SuccessWithData(c, false)
return
}
helper.SuccessWithData(c, true)
return
}
helper.SuccessWithData(c, false)
}
// @Tags File
+8 -8
View File
@@ -14,7 +14,7 @@ import (
// @Success 200 {object} response.HostToolRes
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/tool [post]
// @Router /hosts/tool [post]
func (b *BaseApi) GetToolStatus(c *gin.Context) {
var req request.HostToolReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -36,7 +36,7 @@ func (b *BaseApi) GetToolStatus(c *gin.Context) {
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/tool/create [post]
// @Router /hosts/tool/create [post]
// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 [type] 配置","formatEN":"create [type] config"}
func (b *BaseApi) InitToolConfig(c *gin.Context) {
var req request.HostToolCreate
@@ -58,7 +58,7 @@ func (b *BaseApi) InitToolConfig(c *gin.Context) {
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/tool/operate [post]
// @Router /hosts/tool/operate [post]
// @x-panel-log {"bodyKeys":["operate","type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] [type] ","formatEN":"[operate] [type]"}
func (b *BaseApi) OperateTool(c *gin.Context) {
var req request.HostToolReq
@@ -80,7 +80,7 @@ func (b *BaseApi) OperateTool(c *gin.Context) {
// @Success 200 {object} response.HostToolConfig
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/tool/config [post]
// @Router /hosts/tool/config [post]
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] 主机工具配置文件 ","formatEN":"[operate] tool config"}
func (b *BaseApi) OperateToolConfig(c *gin.Context) {
var req request.HostToolConfig
@@ -103,7 +103,7 @@ func (b *BaseApi) OperateToolConfig(c *gin.Context) {
// @Success 200 {string} logContent
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/tool/log [post]
// @Router /hosts/tool/log [post]
func (b *BaseApi) GetToolLog(c *gin.Context) {
var req request.HostToolLogReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -125,7 +125,7 @@ func (b *BaseApi) GetToolLog(c *gin.Context) {
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/tool/supervisor/process [post]
// @Router /hosts/tool/supervisor/process [post]
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] 守护进程 ","formatEN":"[operate] process"}
func (b *BaseApi) OperateProcess(c *gin.Context) {
var req request.SupervisorProcessConfig
@@ -147,7 +147,7 @@ func (b *BaseApi) OperateProcess(c *gin.Context) {
// @Success 200 {object} response.SupervisorProcessConfig
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/tool/supervisor/process [get]
// @Router /hosts/tool/supervisor/process [get]
func (b *BaseApi) GetProcess(c *gin.Context) {
configs, err := hostToolService.GetSupervisorProcessConfig()
if err != nil {
@@ -164,7 +164,7 @@ func (b *BaseApi) GetProcess(c *gin.Context) {
// @Success 200 {string} content
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/tool/supervisor/process/file [post]
// @Router /hosts/tool/supervisor/process/file [post]
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] Supervisor 进程文件 ","formatEN":"[operate] Supervisor Process Config file"}
func (b *BaseApi) GetProcessFile(c *gin.Context) {
var req request.SupervisorProcessFileReq
+5 -5
View File
@@ -76,7 +76,7 @@ func (b *BaseApi) CheckRepoStatus(c *gin.Context) {
// @Tags Container Image-repo
// @Summary Create image repo
// @Accept json
// @Param request body dto.ImageRepoDelete true "request"
// @Param request body dto.ImageRepoCreate true "request"
// @Produce json
// @Success 200
// @Security ApiKeyAuth
@@ -99,20 +99,20 @@ func (b *BaseApi) CreateRepo(c *gin.Context) {
// @Tags Container Image-repo
// @Summary Delete image repo
// @Accept json
// @Param request body dto.ImageRepoDelete true "request"
// @Param request body dto.OperateByID true "request"
// @Produce json
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /containers/repo/del [post]
// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"image_repos","output_column":"name","output_value":"names"}],"formatZH":"删除镜像仓库 [names]","formatEN":"delete image repo [names]"}
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"image_repos","output_column":"name","output_value":"name"}],"formatZH":"删除镜像仓库 [name]","formatEN":"delete image repo [name]"}
func (b *BaseApi) DeleteRepo(c *gin.Context) {
var req dto.ImageRepoDelete
var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := imageRepoService.BatchDelete(req); err != nil {
if err := imageRepoService.Delete(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
+167
View File
@@ -0,0 +1,167 @@
package v1
import (
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/gin-gonic/gin"
)
// @Tags McpServer
// @Summary List mcp servers
// @Accept json
// @Param request body request.McpServerSearch true "request"
// @Success 200 {object} response.McpServersRes
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /mcp/search [post]
func (b *BaseApi) PageMcpServers(c *gin.Context) {
var req request.McpServerSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
list := mcpServerService.Page(req)
helper.SuccessWithData(c, list)
}
// @Tags McpServer
// @Summary Create mcp server
// @Accept json
// @Param request body request.McpServerCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /mcp/server [post]
func (b *BaseApi) CreateMcpServer(c *gin.Context) {
var req request.McpServerCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := mcpServerService.Create(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags McpServer
// @Summary Update mcp server
// @Accept json
// @Param request body request.McpServerUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /mcp/server/update [post]
func (b *BaseApi) UpdateMcpServer(c *gin.Context) {
var req request.McpServerUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := mcpServerService.Update(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags McpServer
// @Summary Delete mcp server
// @Accept json
// @Param request body request.McpServerDelete true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /mcp/server/del [post]
func (b *BaseApi) DeleteMcpServer(c *gin.Context) {
var req request.McpServerDelete
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := mcpServerService.Delete(req.ID)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags McpServer
// @Summary Operate mcp server
// @Accept json
// @Param request body request.McpServerOperate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /mcp/server/op [post]
func (b *BaseApi) OperateMcpServer(c *gin.Context) {
var req request.McpServerOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := mcpServerService.Operate(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags McpServer
// @Summary Bind Domain for mcp server
// @Accept json
// @Param request body request.McpBindDomain true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /mcp/domain/bind [post]
func (b *BaseApi) BindMcpDomain(c *gin.Context) {
var req request.McpBindDomain
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := mcpServerService.BindDomain(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags McpServer
// @Summary Update bind Domain for mcp server
// @Accept json
// @Param request body request.McpBindDomainUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /mcp/domain/update [post]
func (b *BaseApi) UpdateMcpBindDomain(c *gin.Context) {
var req request.McpBindDomainUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := mcpServerService.UpdateBindDomain(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags McpServer
// @Summary Get bin Domain for mcp server
// @Accept json
// @Success 200 {object} response.McpBindDomainRes
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /mcp/domain/get [get]
func (b *BaseApi) GetMcpBindDomain(c *gin.Context) {
res, err := mcpServerService.GetBindDomain()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, res)
}
+4
View File
@@ -66,6 +66,10 @@ func (b *BaseApi) UpdateSetting(c *gin.Context) {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
if req.Key == "SecurityEntrance" {
entranceValue := base64.StdEncoding.EncodeToString([]byte(req.Value))
c.SetCookie("SecurityEntrance", entranceValue, 0, "", "", false, true)
}
helper.SuccessWithData(c, nil)
}
+8 -8
View File
@@ -12,7 +12,7 @@ import (
// @Success 200 {object} dto.SSHInfo
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/ssh/search [post]
// @Router /hosts/ssh/search [post]
func (b *BaseApi) GetSSHInfo(c *gin.Context) {
info, err := sshService.GetSSHInfo()
if err != nil {
@@ -29,7 +29,7 @@ func (b *BaseApi) GetSSHInfo(c *gin.Context) {
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/ssh/operate [post]
// @Router /hosts/ssh/operate [post]
// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] SSH ","formatEN":"[operation] SSH"}
func (b *BaseApi) OperateSSH(c *gin.Context) {
var req dto.Operate
@@ -51,7 +51,7 @@ func (b *BaseApi) OperateSSH(c *gin.Context) {
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/ssh/update [post]
// @Router /hosts/ssh/update [post]
// @x-panel-log {"bodyKeys":["key","value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改 SSH 配置 [key] => [value]","formatEN":"update SSH setting [key] => [value]"}
func (b *BaseApi) UpdateSSH(c *gin.Context) {
var req dto.SSHUpdate
@@ -73,7 +73,7 @@ func (b *BaseApi) UpdateSSH(c *gin.Context) {
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/conffile/update [post]
// @Router /hosts/conffile/update [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改 SSH 配置文件","formatEN":"update SSH conf"}
func (b *BaseApi) UpdateSSHByfile(c *gin.Context) {
var req dto.SSHConf
@@ -95,7 +95,7 @@ func (b *BaseApi) UpdateSSHByfile(c *gin.Context) {
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/ssh/generate [post]
// @Router /hosts/ssh/generate [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成 SSH 密钥 ","formatEN":"generate SSH secret"}
func (b *BaseApi) GenerateSSH(c *gin.Context) {
var req dto.GenerateSSH
@@ -117,7 +117,7 @@ func (b *BaseApi) GenerateSSH(c *gin.Context) {
// @Success 200 {string} data
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/ssh/secret [post]
// @Router /hosts/ssh/secret [post]
func (b *BaseApi) LoadSSHSecret(c *gin.Context) {
var req dto.GenerateLoad
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -139,7 +139,7 @@ func (b *BaseApi) LoadSSHSecret(c *gin.Context) {
// @Success 200 {object} dto.SSHLog
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/ssh/log [post]
// @Router /hosts/ssh/log [post]
func (b *BaseApi) LoadSSHLogs(c *gin.Context) {
var req dto.SearchSSHLog
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -159,7 +159,7 @@ func (b *BaseApi) LoadSSHLogs(c *gin.Context) {
// @Success 200 {string} data
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /host/ssh/conf [get]
// @Router /hosts/ssh/conf [get]
func (b *BaseApi) LoadSSHConf(c *gin.Context) {
data, err := sshService.LoadSSHConf()
if err != nil {
+71 -160
View File
@@ -10,7 +10,6 @@ import (
"time"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/service"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/copier"
@@ -69,12 +68,11 @@ func (b *BaseApi) WsSsh(c *gin.Context) {
<-quitChan
if wshandleError(wsConn, err) {
return
}
dt := time.Now().Add(time.Second)
_ = wsConn.WriteControl(websocket.CloseMessage, nil, dt)
}
func (b *BaseApi) RedisWsSsh(c *gin.Context) {
func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
global.LOG.Errorf("gin context http handler failed, err: %v", err)
@@ -96,193 +94,106 @@ func (b *BaseApi) RedisWsSsh(c *gin.Context) {
if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) {
return
}
source := c.Query("source")
var containerID string
var initCmd []string
switch source {
case "redis":
containerID, initCmd, err = loadRedisInitCmd(c)
case "ollama":
containerID, initCmd, err = loadOllamaInitCmd(c)
case "container":
containerID, initCmd, err = loadContainerInitCmd(c)
default:
if wshandleError(wsConn, fmt.Errorf("not support such source %s", source)) {
return
}
}
if wshandleError(wsConn, err) {
return
}
pidMap := loadMapFromDockerTop(containerID)
slave, err := terminal.NewCommand(initCmd)
if wshandleError(wsConn, err) {
return
}
defer killBash(containerID, strings.ReplaceAll(strings.Join(initCmd, " "), fmt.Sprintf("exec -it %s ", containerID), ""), pidMap)
defer slave.Close()
tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
if wshandleError(wsConn, err) {
return
}
quitChan := make(chan bool, 3)
tty.Start(quitChan)
go slave.Wait(quitChan)
<-quitChan
global.LOG.Info("websocket finished")
dt := time.Now().Add(time.Second)
_ = wsConn.WriteControl(websocket.CloseMessage, nil, dt)
}
func loadRedisInitCmd(c *gin.Context) (string, []string, error) {
name := c.Query("name")
from := c.Query("from")
commands := []string{"redis-cli"}
commands := []string{"exec", "-it"}
database, err := databaseService.Get(name)
if wshandleError(wsConn, errors.WithMessage(err, "no such database in db")) {
return
if err != nil {
return "", nil, fmt.Errorf("no such database in db, err: %v", err)
}
if from == "local" {
redisInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: name, Type: "redis"})
if wshandleError(wsConn, errors.WithMessage(err, "no such database in db")) {
return
if err != nil {
return "", nil, fmt.Errorf("no such app in db, err: %v", err)
}
name = redisInfo.ContainerName
commands = append(commands, []string{name, "redis-cli"}...)
if len(database.Password) != 0 {
commands = []string{"redis-cli", "-a", database.Password, "--no-auth-warning"}
commands = append(commands, []string{"-a", database.Password, "--no-auth-warning"}...)
}
} else {
itemPort := fmt.Sprintf("%v", database.Port)
commands = []string{"redis-cli", "-h", database.Address, "-p", itemPort}
if len(database.Password) != 0 {
commands = []string{"redis-cli", "-h", database.Address, "-p", itemPort, "-a", database.Password, "--no-auth-warning"}
}
name = "1Panel-redis-cli-tools"
commands = append(commands, []string{name, "redis-cli", "-h", database.Address, "-p", fmt.Sprintf("%v", database.Port)}...)
if len(database.Password) != 0 {
commands = append(commands, []string{"-a", database.Password, "--no-auth-warning"}...)
}
}
pidMap := loadMapFromDockerTop(name)
itemCmds := append([]string{"exec", "-it", name}, commands...)
slave, err := terminal.NewCommand(itemCmds)
if wshandleError(wsConn, err) {
return
}
defer killBash(name, strings.Join(commands, " "), pidMap)
defer slave.Close()
tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
if wshandleError(wsConn, err) {
return
}
quitChan := make(chan bool, 3)
tty.Start(quitChan)
go slave.Wait(quitChan)
<-quitChan
global.LOG.Info("websocket finished")
if wshandleError(wsConn, err) {
return
}
return name, commands, nil
}
func (b *BaseApi) OllamaWsSsh(c *gin.Context) {
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
global.LOG.Errorf("gin context http handler failed, err: %v", err)
return
}
defer wsConn.Close()
if global.CONF.System.IsDemo {
if wshandleError(wsConn, errors.New(" demo server, prohibit this operation!")) {
return
}
}
cols, err := strconv.Atoi(c.DefaultQuery("cols", "80"))
if wshandleError(wsConn, errors.WithMessage(err, "invalid param cols in request")) {
return
}
rows, err := strconv.Atoi(c.DefaultQuery("rows", "40"))
if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) {
return
}
func loadOllamaInitCmd(c *gin.Context) (string, []string, error) {
name := c.Query("name")
if cmd.CheckIllegal(name) {
if wshandleError(wsConn, errors.New(" The command contains illegal characters.")) {
return
}
return "", nil, fmt.Errorf("ollama model %s contains illegal characters", name)
}
container, err := service.LoadContainerName()
if wshandleError(wsConn, errors.WithMessage(err, " load container name for ollama failed")) {
return
}
commands := []string{"ollama", "run", name}
pidMap := loadMapFromDockerTop(container)
fmt.Println("pidMap")
for k, v := range pidMap {
fmt.Println(k, v)
}
itemCmds := append([]string{"exec", "-it", container}, commands...)
slave, err := terminal.NewCommand(itemCmds)
if wshandleError(wsConn, err) {
return
}
defer killBash(container, strings.Join(commands, " "), pidMap)
defer slave.Close()
tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
if wshandleError(wsConn, err) {
return
}
quitChan := make(chan bool, 3)
tty.Start(quitChan)
go slave.Wait(quitChan)
<-quitChan
global.LOG.Info("websocket finished")
if wshandleError(wsConn, err) {
return
ollamaInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: "", Type: "ollama"})
if err != nil {
return "", nil, fmt.Errorf("no such app in db, err: %v", err)
}
containerName := ollamaInfo.ContainerName
return containerName, []string{"exec", "-it", containerName, "ollama", "run", name}, nil
}
func (b *BaseApi) ContainerWsSsh(c *gin.Context) {
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
global.LOG.Errorf("gin context http handler failed, err: %v", err)
return
}
defer wsConn.Close()
if global.CONF.System.IsDemo {
if wshandleError(wsConn, errors.New(" demo server, prohibit this operation!")) {
return
}
}
func loadContainerInitCmd(c *gin.Context) (string, []string, error) {
containerID := c.Query("containerid")
command := c.Query("command")
user := c.Query("user")
if len(command) == 0 || len(containerID) == 0 {
if wshandleError(wsConn, errors.New("error param of command or containerID")) {
return
}
}
cols, err := strconv.Atoi(c.DefaultQuery("cols", "80"))
if wshandleError(wsConn, errors.WithMessage(err, "invalid param cols in request")) {
return
}
rows, err := strconv.Atoi(c.DefaultQuery("rows", "40"))
if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) {
return
}
cmds := []string{"exec", containerID, command}
if len(user) != 0 {
cmds = []string{"exec", "-u", user, containerID, command}
}
if cmd.CheckIllegal(user, containerID, command) {
if wshandleError(wsConn, errors.New(" The command contains illegal characters.")) {
return
}
return "", nil, fmt.Errorf("the command contains illegal characters. command: %s, user: %s, containerID: %s", command, user, containerID)
}
stdout, err := cmd.ExecWithCheck("docker", cmds...)
if wshandleError(wsConn, errors.WithMessage(err, stdout)) {
return
if len(command) == 0 || len(containerID) == 0 {
return "", nil, fmt.Errorf("error param of command: %s or containerID: %s", command, containerID)
}
commands := []string{"exec", "-it", containerID, command}
if len(user) != 0 {
commands = []string{"exec", "-it", "-u", user, containerID, command}
}
pidMap := loadMapFromDockerTop(containerID)
slave, err := terminal.NewCommand(commands)
if wshandleError(wsConn, err) {
return
}
defer killBash(containerID, command, pidMap)
defer slave.Close()
tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, true)
if wshandleError(wsConn, err) {
return
}
quitChan := make(chan bool, 3)
tty.Start(quitChan)
go slave.Wait(quitChan)
<-quitChan
global.LOG.Info("websocket finished")
if wshandleError(wsConn, err) {
return
}
return containerID, commands, nil
}
func wshandleError(ws *websocket.Conn, err error) bool {
+6 -5
View File
@@ -36,9 +36,10 @@ type OllamaBindDomainReq struct {
}
type OllamaBindDomainRes struct {
Domain string `json:"domain"`
SSLID uint `json:"sslID"`
AllowIPs []string `json:"allowIPs"`
WebsiteID uint `json:"websiteID"`
ConnUrl string `json:"connUrl"`
Domain string `json:"domain"`
SSLID uint `json:"sslID"`
AcmeAccountID uint `json:"acmeAccountID"`
AllowIPs []string `json:"allowIPs"`
WebsiteID uint `json:"websiteID"`
ConnUrl string `json:"connUrl"`
}
+7 -5
View File
@@ -71,14 +71,16 @@ type CronjobDownload struct {
}
type CronjobClean struct {
IsDelete bool `json:"isDelete"`
CleanData bool `json:"cleanData"`
CronjobID uint `json:"cronjobID" validate:"required"`
IsDelete bool `json:"isDelete"`
CleanData bool `json:"cleanData"`
CleanRemoteData bool `json:"cleanRemoteData"`
CronjobID uint `json:"cronjobID" validate:"required"`
}
type CronjobBatchDelete struct {
CleanData bool `json:"cleanData"`
IDs []uint `json:"ids" validate:"required"`
CleanData bool `json:"cleanData"`
CleanRemoteData bool `json:"cleanRemoteData"`
IDs []uint `json:"ids" validate:"required"`
}
type CronjobInfo struct {
+2 -1
View File
@@ -30,7 +30,8 @@ type PortRuleOperate struct {
}
type ForwardRuleOperate struct {
Rules []struct {
ForceDelete bool `json:"forceDelete"`
Rules []struct {
Operation string `json:"operation" validate:"required,oneof=add remove"`
Num string `json:"num"`
Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"`
+4 -4
View File
@@ -6,8 +6,8 @@ type ImageRepoCreate struct {
Name string `json:"name" validate:"required"`
DownloadUrl string `json:"downloadUrl"`
Protocol string `json:"protocol"`
Username string `json:"username" validate:"max=256"`
Password string `json:"password" validate:"max=256"`
Username string `json:"username"`
Password string `json:"password"`
Auth bool `json:"auth"`
}
@@ -15,8 +15,8 @@ type ImageRepoUpdate struct {
ID uint `json:"id"`
DownloadUrl string `json:"downloadUrl"`
Protocol string `json:"protocol"`
Username string `json:"username" validate:"max=256"`
Password string `json:"password" validate:"max=256"`
Username string `json:"username"`
Password string `json:"password"`
Auth bool `json:"auth"`
}
+10
View File
@@ -0,0 +1,10 @@
package dto
type DockerComposeService struct {
Image string `yaml:"image"`
ContainerName string `yaml:"container_name"`
Restart string `yaml:"restart"`
Ports []string `yaml:"ports"`
Environment []string `yaml:"environment"`
Command []string `yaml:"command"`
}
+8 -6
View File
@@ -76,7 +76,8 @@ type FileRename struct {
}
type FilePathCheck struct {
Path string `json:"path" validate:"required"`
Path string `json:"path" validate:"required"`
WithInit bool `json:"withInit"`
}
type FilePathsCheck struct {
@@ -91,11 +92,12 @@ type FileWget struct {
}
type FileMove struct {
Type string `json:"type" validate:"required"`
OldPaths []string `json:"oldPaths" validate:"required"`
NewPath string `json:"newPath" validate:"required"`
Name string `json:"name"`
Cover bool `json:"cover"`
Type string `json:"type" validate:"required"`
OldPaths []string `json:"oldPaths" validate:"required"`
NewPath string `json:"newPath" validate:"required"`
Name string `json:"name"`
Cover bool `json:"cover"`
CoverPaths []string `json:"coverPaths"`
}
type FileDownload struct {
+57
View File
@@ -0,0 +1,57 @@
package request
import "github.com/1Panel-dev/1Panel/backend/app/dto"
type McpServerSearch struct {
dto.PageInfo
Name string `json:"name"`
Sync bool `json:"sync"`
}
type McpServerCreate struct {
Name string `json:"name" validate:"required"`
Command string `json:"command" validate:"required"`
Environments []Environment `json:"environments"`
Volumes []Volume `json:"volumes"`
Port int `json:"port" validate:"required"`
ContainerName string `json:"containerName"`
BaseURL string `json:"baseUrl"`
SsePath string `json:"ssePath"`
HostIP string `json:"hostIP"`
}
type McpServerUpdate struct {
ID uint `json:"id" validate:"required"`
McpServerCreate
}
type Environment struct {
Key string `json:"key"`
Value string `json:"value"`
}
type Volume struct {
Source string `json:"source"`
Target string `json:"target"`
}
type McpServerDelete struct {
ID uint `json:"id" validate:"required"`
}
type McpServerOperate struct {
ID uint `json:"id" validate:"required"`
Operate string `json:"operate" validate:"required"`
}
type McpBindDomain struct {
Domain string `json:"domain" validate:"required"`
SSLID uint `json:"sslID"`
IPList string `json:"ipList"`
}
type McpBindDomainUpdate struct {
WebsiteID uint `json:"websiteID" validate:"required"`
SSLID uint `json:"sslID"`
IPList string `json:"ipList"`
}
+1
View File
@@ -53,4 +53,5 @@ type ExistFileInfo struct {
Path string `json:"path"`
Size float64 `json:"size"`
ModTime time.Time `json:"modTime"`
IsDir bool `json:"isDir"`
}
+26
View File
@@ -0,0 +1,26 @@
package response
import (
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
"github.com/1Panel-dev/1Panel/backend/app/model"
)
type McpServersRes struct {
Items []McpServerDTO `json:"items"`
Total int64 `json:"total"`
}
type McpServerDTO struct {
model.McpServer
Environments []request.Environment `json:"environments"`
Volumes []request.Volume `json:"volumes"`
}
type McpBindDomainRes struct {
Domain string `json:"domain"`
SSLID uint `json:"sslID"`
AcmeAccountID uint `json:"acmeAccountID"`
AllowIPs []string `json:"allowIPs"`
WebsiteID uint `json:"websiteID"`
ConnUrl string `json:"connUrl"`
}
+1
View File
@@ -71,6 +71,7 @@ type SettingInfo struct {
ApiKey string `json:"apiKey"`
IpWhiteList string `json:"ipWhiteList"`
ApiKeyValidityTime string `json:"apiKeyValidityTime"`
LicenseVerify string `json:"licenseVerify"`
}
type SettingUpdate struct {
+18
View File
@@ -0,0 +1,18 @@
package model
type McpServer struct {
BaseModel
Name string `json:"name"`
DockerCompose string `json:"dockerCompose"`
Command string `json:"command"`
ContainerName string `json:"containerName"`
Message string `json:"message"`
Port int `json:"port"`
Status string `json:"status"`
Env string `json:"env"`
BaseURL string `json:"baseUrl"`
SsePath string `json:"ssePath"`
WebsiteID int `json:"websiteID"`
Dir string `json:"dir"`
HostIP string `json:"hostIP"`
}
+7
View File
@@ -16,6 +16,7 @@ type IGroupRepo interface {
Delete(opts ...DBOption) error
CancelDefault(groupType string) error
WithByHostDefault() DBOption
WithByWebsiteDefault() DBOption
}
func NewIGroupRepo() IGroupRepo {
@@ -56,6 +57,12 @@ func (u *GroupRepo) WithByHostDefault() DBOption {
}
}
func (u *GroupRepo) WithByWebsiteDefault() DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("is_default = ? AND type = ?", 1, "website")
}
}
func (u *GroupRepo) Delete(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
+56
View File
@@ -0,0 +1,56 @@
package repo
import "github.com/1Panel-dev/1Panel/backend/app/model"
type McpServerRepo struct {
}
type IMcpServerRepo interface {
Page(page, size int, opts ...DBOption) (int64, []model.McpServer, error)
GetFirst(opts ...DBOption) (*model.McpServer, error)
Create(mcpServer *model.McpServer) error
Save(mcpServer *model.McpServer) error
DeleteBy(opts ...DBOption) error
List(opts ...DBOption) ([]model.McpServer, error)
}
func NewIMcpServerRepo() IMcpServerRepo {
return &McpServerRepo{}
}
func (m McpServerRepo) Page(page, size int, opts ...DBOption) (int64, []model.McpServer, error) {
var servers []model.McpServer
db := getDb(opts...).Model(&model.McpServer{})
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&servers).Error
return count, servers, err
}
func (m McpServerRepo) GetFirst(opts ...DBOption) (*model.McpServer, error) {
var mcpServer model.McpServer
if err := getDb(opts...).First(&mcpServer).Error; err != nil {
return nil, err
}
return &mcpServer, nil
}
func (m McpServerRepo) List(opts ...DBOption) ([]model.McpServer, error) {
var mcpServers []model.McpServer
if err := getDb(opts...).Find(&mcpServers).Error; err != nil {
return nil, err
}
return mcpServers, nil
}
func (m McpServerRepo) Create(mcpServer *model.McpServer) error {
return getDb().Create(mcpServer).Error
}
func (m McpServerRepo) Save(mcpServer *model.McpServer) error {
return getDb().Save(mcpServer).Error
}
func (m McpServerRepo) DeleteBy(opts ...DBOption) error {
return getDb(opts...).Delete(&model.McpServer{}).Error
}
+20
View File
@@ -245,6 +245,15 @@ func (u *AIToolService) BindDomain(req dto.OllamaBindDomain) error {
return err
}
}
if req.SSLID > 0 {
ssl, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(req.SSLID))
if err != nil {
return err
}
if ssl.Pem == "" {
return buserr.New("ErrSSL")
}
}
createWebsiteReq := request.WebsiteCreate{
PrimaryDomain: req.Domain,
Alias: strings.ToLower(req.Domain),
@@ -297,6 +306,8 @@ func (u *AIToolService) GetBindDomain(req dto.OllamaBindDomainReq) (*dto.OllamaB
res.Domain = website.PrimaryDomain
if website.WebsiteSSLID > 0 {
res.SSLID = website.WebsiteSSLID
ssl, _ := websiteSSLRepo.GetFirst(commonRepo.WithByID(website.WebsiteSSLID))
res.AcmeAccountID = ssl.AcmeAccountID
}
res.ConnUrl = fmt.Sprintf("%s://%s", strings.ToLower(website.Protocol), website.PrimaryDomain)
res.AllowIPs = GetAllowIps(website)
@@ -318,6 +329,15 @@ func (u *AIToolService) UpdateBindDomain(req dto.OllamaBindDomain) error {
return err
}
}
if req.SSLID > 0 {
ssl, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(req.SSLID))
if err != nil {
return err
}
if ssl.Pem == "" {
return buserr.New("ErrSSL")
}
}
websiteService := NewIWebsiteService()
website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID))
if err != nil {
+3
View File
@@ -839,6 +839,9 @@ func updateInstallInfoInDB(appKey, appName, param string, value interface{}) err
"param": strings.ReplaceAll(appInstall.Param, oldVal, newVal),
"env": strings.ReplaceAll(appInstall.Env, oldVal, newVal),
}, commonRepo.WithByID(appInstall.ID))
if appKey == "mysql" || appKey == "postgresql" {
return nil
}
}
if param == "user-password" {
oldVal = fmt.Sprintf("\"PANEL_DB_USER_PASSWORD\":\"%v\"", appInstall.UserPassword)
+31 -6
View File
@@ -442,17 +442,14 @@ func deleteLink(ctx context.Context, install *model.AppInstall, deleteDB bool, f
return appInstallResourceRepo.DeleteBy(ctx, appInstallResourceRepo.WithAppInstallId(install.ID))
}
func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string, error) {
if detail.DockerCompose == "" {
return "", nil
}
func handleUpgradeCompose(install model.AppInstall, detail model.AppDetail) (map[string]interface{}, error) {
composeMap := make(map[string]interface{})
if err := yaml.Unmarshal([]byte(detail.DockerCompose), &composeMap); err != nil {
return "", err
return nil, err
}
value, ok := composeMap["services"]
if !ok || value == nil {
return "", buserr.New(constant.ErrFileParse)
return nil, buserr.New("ErrFileParse")
}
servicesMap := value.(map[string]interface{})
if len(servicesMap) == 1 {
@@ -470,6 +467,34 @@ func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string
delete(servicesMap, oldServiceName)
}
}
serviceValue := servicesMap[install.ServiceName].(map[string]interface{})
oldComposeMap := make(map[string]interface{})
if err := yaml.Unmarshal([]byte(install.DockerCompose), &oldComposeMap); err != nil {
return nil, err
}
oldValue, ok := oldComposeMap["services"]
if !ok || oldValue == nil {
return nil, buserr.New("ErrFileParse")
}
oldValueMap := oldValue.(map[string]interface{})
oldServiceValue := oldValueMap[install.ServiceName].(map[string]interface{})
if oldServiceValue["deploy"] != nil {
serviceValue["deploy"] = oldServiceValue["deploy"]
}
servicesMap[install.ServiceName] = serviceValue
composeMap["services"] = servicesMap
return composeMap, nil
}
func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string, error) {
if detail.DockerCompose == "" {
return "", nil
}
composeMap, err := handleUpgradeCompose(install, detail)
if err != nil {
return "", err
}
envs := make(map[string]interface{})
if err := json.Unmarshal([]byte(install.Env), &envs); err != nil {
return "", err
+6 -13
View File
@@ -518,47 +518,40 @@ func (u *BackupService) loadAccessToken(backup *model.BackupAccount) error {
}
func (u *BackupService) loadRecordSize(records []model.BackupRecord) ([]dto.BackupFile, error) {
var datas []dto.BackupFile
data := make([]dto.BackupFile, len(records))
clientMap := make(map[string]loadSizeHelper)
var wg sync.WaitGroup
for i := 0; i < len(records); i++ {
var item dto.BackupFile
item.ID = records[i].ID
item.Name = records[i].FileName
data[i].ID = records[i].ID
data[i].Name = records[i].FileName
itemPath := path.Join(records[i].FileDir, records[i].FileName)
if _, ok := clientMap[records[i].Source]; !ok {
backup, err := backupRepo.Get(commonRepo.WithByType(records[i].Source))
if err != nil {
global.LOG.Errorf("load backup model %s from db failed, err: %v", records[i].Source, err)
clientMap[records[i].Source] = loadSizeHelper{}
datas = append(datas, item)
continue
}
client, err := u.NewClient(&backup)
if err != nil {
global.LOG.Errorf("load backup client %s from db failed, err: %v", records[i].Source, err)
clientMap[records[i].Source] = loadSizeHelper{}
datas = append(datas, item)
continue
}
item.Size, _ = client.Size(path.Join(strings.TrimLeft(backup.BackupPath, "/"), itemPath))
datas = append(datas, item)
data[i].Size, _ = client.Size(path.Join(strings.TrimLeft(backup.BackupPath, "/"), itemPath))
clientMap[records[i].Source] = loadSizeHelper{backupPath: strings.TrimLeft(backup.BackupPath, "/"), client: client, isOk: true}
continue
}
if clientMap[records[i].Source].isOk {
wg.Add(1)
go func(index int) {
item.Size, _ = clientMap[records[index].Source].client.Size(path.Join(clientMap[records[index].Source].backupPath, itemPath))
datas = append(datas, item)
data[i].Size, _ = clientMap[records[index].Source].client.Size(path.Join(clientMap[records[index].Source].backupPath, itemPath))
wg.Done()
}(i)
} else {
datas = append(datas, item)
}
}
wg.Wait()
return datas, nil
return data, nil
}
func loadLocalDir() (string, error) {
+5 -2
View File
@@ -2,13 +2,14 @@ package service
import (
"fmt"
"github.com/1Panel-dev/1Panel/backend/constant"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/app/dto"
@@ -169,8 +170,9 @@ func handleMysqlRecover(req dto.CommonRecover, isRollback bool) error {
Timeout: 300,
}); err != nil {
global.LOG.Errorf("rollback mysql db %s from %s failed, err: %v", req.DetailName, rollbackFile, err)
} else {
global.LOG.Infof("rollback mysql db %s from %s successful", req.DetailName, rollbackFile)
}
global.LOG.Infof("rollback mysql db %s from %s successful", req.DetailName, rollbackFile)
_ = os.RemoveAll(rollbackFile)
} else {
_ = os.RemoveAll(rollbackFile)
@@ -186,6 +188,7 @@ func handleMysqlRecover(req dto.CommonRecover, isRollback bool) error {
Timeout: 300,
}); err != nil {
global.LOG.Errorf("recover mysql db %s from %s failed, err: %v", req.DetailName, req.File, err)
return err
}
isOk = true
+5 -2
View File
@@ -2,13 +2,14 @@ package service
import (
"fmt"
"github.com/1Panel-dev/1Panel/backend/constant"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/utils/common"
pgclient "github.com/1Panel-dev/1Panel/backend/utils/postgresql/client"
@@ -157,8 +158,9 @@ func handlePostgresqlRecover(req dto.CommonRecover, isRollback bool) error {
Timeout: 300,
}); err != nil {
global.LOG.Errorf("rollback postgresql db %s from %s failed, err: %v", req.DetailName, rollbackFile, err)
} else {
global.LOG.Infof("rollback postgresql db %s from %s successful", req.DetailName, rollbackFile)
}
global.LOG.Infof("rollback postgresql db %s from %s successful", req.DetailName, rollbackFile)
_ = os.RemoveAll(rollbackFile)
} else {
_ = os.RemoveAll(rollbackFile)
@@ -171,6 +173,7 @@ func handlePostgresqlRecover(req dto.CommonRecover, isRollback bool) error {
Username: dbInfo.Username,
Timeout: 300,
}); err != nil {
global.LOG.Errorf("recover postgresql db %s from %s failed, err: %v", req.DetailName, req.File, err)
return err
}
isOk = true
+103 -129
View File
@@ -1,7 +1,6 @@
package service
import (
"bufio"
"fmt"
"os"
"os/exec"
@@ -28,14 +27,14 @@ import (
)
const (
clamServiceNameCentOs = "clamd@scan.service"
clamServiceNameUbuntu = "clamav-daemon.service"
freshClamService = "clamav-freshclam.service"
resultDir = "clamav"
clamServiceKey = "clam"
freshClamServiceKey = "freshclam"
resultDir = "clamav"
)
type ClamService struct {
serviceName string
serviceName string
freshClamService string
}
type IClamService interface {
@@ -63,23 +62,32 @@ func (c *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) {
var baseInfo dto.ClamBaseInfo
baseInfo.Version = "-"
baseInfo.FreshVersion = "-"
exist1, _ := systemctl.IsExist(clamServiceNameCentOs)
if exist1 {
c.serviceName = clamServiceNameCentOs
baseInfo.IsExist = true
baseInfo.IsActive, _ = systemctl.IsActive(clamServiceNameCentOs)
clamSvc, err := systemctl.GetServiceName(clamServiceKey)
if err != nil {
baseInfo.IsExist = false
return baseInfo, nil
}
exist2, _ := systemctl.IsExist(clamServiceNameUbuntu)
if exist2 {
c.serviceName = clamServiceNameUbuntu
baseInfo.IsExist = true
baseInfo.IsActive, _ = systemctl.IsActive(clamServiceNameUbuntu)
c.serviceName = clamSvc
isExist, err := systemctl.IsExist(c.serviceName)
if err != nil {
baseInfo.IsExist = false
}
freshExist, _ := systemctl.IsExist(freshClamService)
if freshExist {
baseInfo.FreshIsExist = true
baseInfo.FreshIsActive, _ = systemctl.IsActive(freshClamService)
baseInfo.IsExist = isExist
baseInfo.IsActive, _ = systemctl.IsActive(clamSvc)
freshSvc, err := systemctl.GetServiceName(freshClamServiceKey)
if err != nil {
baseInfo.FreshIsExist = false
return baseInfo, nil
}
c.freshClamService = freshSvc
freshisExist, err := systemctl.IsExist(c.freshClamService)
if err != nil {
baseInfo.FreshIsExist = false
}
baseInfo.FreshIsExist = freshisExist
baseInfo.FreshIsActive, _ = systemctl.IsActive(freshSvc)
if !cmd.Which("clamdscan") {
baseInfo.IsActive = false
}
@@ -110,22 +118,27 @@ func (c *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) {
}
func (c *ClamService) Operate(operate string) error {
var err error
switch operate {
case "start", "restart", "stop":
stdout, err := cmd.Execf("systemctl %s %s", operate, c.serviceName)
if err != nil {
return fmt.Errorf("%s the %s failed, err: %s", operate, c.serviceName, stdout)
}
return nil
case "fresh-start", "fresh-restart", "fresh-stop":
stdout, err := cmd.Execf("systemctl %s %s", strings.TrimPrefix(operate, "fresh-"), freshClamService)
if err != nil {
return fmt.Errorf("%s the %s failed, err: %s", operate, c.serviceName, stdout)
}
return nil
case "start":
err = systemctl.Start(c.serviceName)
case "stop":
err = systemctl.Stop(c.serviceName)
case "restart":
err = systemctl.Restart(c.serviceName)
case "fresh-start":
err = systemctl.Start(c.freshClamService)
case "fresh-stop":
err = systemctl.Stop(c.freshClamService)
case "fresh-restart":
err = systemctl.Restart(c.freshClamService)
default:
return fmt.Errorf("not support such operation: %v", operate)
return fmt.Errorf("unsupported operation: %s", operate)
}
if err != nil {
return fmt.Errorf("%s %s failed: %v", operate, c.serviceName, err)
}
return nil
}
func (c *ClamService) SearchWithPage(req dto.SearchClamWithPage) (int64, interface{}, error) {
@@ -432,102 +445,80 @@ func (c *ClamService) LoadFile(req dto.ClamFileReq) (string, error) {
filePath := ""
switch req.Name {
case "clamd":
if c.serviceName == clamServiceNameUbuntu {
filePath = "/etc/clamav/clamd.conf"
} else {
filePath = "/etc/clamd.d/scan.conf"
}
filePath = c.getConfigPath("clamd")
case "clamd-log":
filePath = c.loadLogPath("clamd-log")
if len(filePath) != 0 {
break
}
if c.serviceName == clamServiceNameUbuntu {
filePath = "/var/log/clamav/clamav.log"
} else {
filePath = "/var/log/clamd.scan"
}
case "freshclam":
if c.serviceName == clamServiceNameUbuntu {
filePath = "/etc/clamav/freshclam.conf"
} else {
filePath = "/etc/freshclam.conf"
}
filePath = c.getConfigPath("freshclam")
case "freshclam-log":
filePath = c.loadLogPath("freshclam-log")
if len(filePath) != 0 {
break
}
if c.serviceName == clamServiceNameUbuntu {
filePath = "/var/log/clamav/freshclam.log"
} else {
filePath = "/var/log/freshclam.log"
}
default:
return "", fmt.Errorf("not support such type")
return "", fmt.Errorf("unsupported file type")
}
if _, err := os.Stat(filePath); err != nil {
content, err := systemctl.ViewConfig(filePath, systemctl.ConfigOption{TailLines: req.Tail})
if err != nil {
return "", buserr.New("ErrHttpReqNotFound")
}
var tail string
if req.Tail != "0" {
tail = req.Tail
} else {
tail = "+1"
}
cmd := exec.Command("tail", "-n", tail, filePath)
stdout, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("tail -n %v failed, err: %v", req.Tail, err)
}
return string(stdout), nil
return content, nil
}
func (c *ClamService) UpdateFile(req dto.UpdateByNameAndFile) error {
filePath := ""
service := ""
var (
filePath string
service string
)
switch req.Name {
case "clamd":
if c.serviceName == clamServiceNameUbuntu {
service = clamServiceNameUbuntu
filePath = "/etc/clamav/clamd.conf"
} else {
service = clamServiceNameCentOs
filePath = "/etc/clamd.d/scan.conf"
}
filePath = c.getConfigPath("clamd")
service = c.serviceName
case "freshclam":
if c.serviceName == clamServiceNameUbuntu {
filePath = "/etc/clamav/freshclam.conf"
} else {
filePath = "/etc/freshclam.conf"
}
service = "clamav-freshclam.service"
filePath = c.getConfigPath("freshclam")
service = c.freshClamService
default:
return fmt.Errorf("not support such type")
return fmt.Errorf("unsupported file type")
}
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_TRUNC, 0640)
if err != nil {
return err
}
defer file.Close()
write := bufio.NewWriter(file)
_, _ = write.WriteString(req.File)
write.Flush()
_ = systemctl.Restart(service)
if _, err := file.WriteString(req.File); err != nil {
return err
}
if err := systemctl.Restart(service); err != nil {
return fmt.Errorf("restart %s failed: %v", service, err)
}
return nil
}
func (c *ClamService) getConfigPath(confType string) string {
switch confType {
case "clamd":
if _, err := os.Stat("/etc/clamav/clamd.conf"); err == nil {
return "/etc/clamav/clamd.conf"
}
return "/etc/clamd.d/scan.conf"
case "freshclam":
if _, err := os.Stat("/etc/clamav/freshclam.conf"); err == nil {
return "/etc/clamav/freshclam.conf"
}
return "/etc/freshclam.conf"
default:
return ""
}
}
func StopAllCronJob(withCheck bool) bool {
if withCheck {
isActive := false
exist1, _ := systemctl.IsExist(clamServiceNameCentOs)
if exist1 {
isActive, _ = systemctl.IsActive(clamServiceNameCentOs)
}
exist2, _ := systemctl.IsExist(clamServiceNameUbuntu)
if exist2 {
isActive, _ = systemctl.IsActive(clamServiceNameUbuntu)
isexist, _ := systemctl.IsExist(clamServiceKey)
if isexist {
isActive, _ = systemctl.IsActive(clamServiceKey)
}
if isActive {
return false
@@ -590,42 +581,25 @@ func loadResultFromLog(pathItem string) dto.ClamLog {
return data
}
func (c *ClamService) loadLogPath(name string) string {
confPath := ""
if name == "clamd-log" {
if c.serviceName == clamServiceNameUbuntu {
confPath = "/etc/clamav/clamd.conf"
} else {
confPath = "/etc/clamd.d/scan.conf"
}
} else {
if c.serviceName == clamServiceNameUbuntu {
confPath = "/etc/clamav/freshclam.conf"
} else {
confPath = "/etc/freshclam.conf"
}
}
if _, err := os.Stat(confPath); err != nil {
return ""
configKey := "clamd"
searchPrefix := "LogFile "
if name != "clamd-log" {
configKey = "freshclam"
searchPrefix = "UpdateLogFile "
}
confPath := c.getConfigPath(configKey)
content, err := os.ReadFile(confPath)
if err != nil {
global.LOG.Debugf("Failed to read %s config: %v", configKey, err)
return ""
}
lines := strings.Split(string(content), "\n")
if name == "clamd-log" {
for _, line := range lines {
if strings.HasPrefix(line, "LogFile ") {
return strings.Trim(strings.ReplaceAll(line, "LogFile ", ""), " ")
}
}
} else {
for _, line := range lines {
if strings.HasPrefix(line, "UpdateLogFile ") {
return strings.Trim(strings.ReplaceAll(line, "UpdateLogFile ", ""), " ")
}
for _, line := range strings.Split(string(content), "\n") {
if strings.HasPrefix(line, searchPrefix) {
return strings.TrimSpace(strings.TrimPrefix(line, searchPrefix))
}
}
return ""
}
+34 -25
View File
@@ -255,14 +255,14 @@ func (u *ContainerService) ContainerListStats() ([]dto.ContainerListStats, error
if err != nil {
return nil, err
}
var datas []dto.ContainerListStats
datas := make([]dto.ContainerListStats, len(list))
var wg sync.WaitGroup
wg.Add(len(list))
for i := 0; i < len(list); i++ {
go func(item types.Container) {
datas = append(datas, loadCpuAndMem(client, item.ID))
go func(index int, item types.Container) {
datas[index] = loadCpuAndMem(client, item.ID)
wg.Done()
}(list[i])
}(i, list[i])
}
wg.Wait()
return datas, nil
@@ -450,12 +450,14 @@ func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.Contai
networkSettings := oldContainer.NetworkSettings
bridgeNetworkSettings := networkSettings.Networks[data.Network]
if bridgeNetworkSettings.IPAMConfig != nil {
ipv4Address := bridgeNetworkSettings.IPAMConfig.IPv4Address
data.Ipv4 = ipv4Address
ipv6Address := bridgeNetworkSettings.IPAMConfig.IPv6Address
data.Ipv6 = ipv6Address
if data.Network != "bridge" {
data.Ipv4 = bridgeNetworkSettings.IPAMConfig.IPv4Address
data.Ipv6 = bridgeNetworkSettings.IPAMConfig.IPv6Address
}
} else {
data.Ipv4 = bridgeNetworkSettings.IPAddress
if data.Network != "bridge" {
data.Ipv4 = bridgeNetworkSettings.IPAddress
}
}
data.Cmd = oldContainer.Config.Cmd
@@ -556,10 +558,7 @@ func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error {
hostConf := oldContainer.HostConfig
var networkConf network.NetworkingConfig
if oldContainer.NetworkSettings != nil {
for networkKey := range oldContainer.NetworkSettings.Networks {
networkConf.EndpointsConfig = map[string]*network.EndpointSettings{networkKey: {}}
break
}
networkConf.EndpointsConfig = oldContainer.NetworkSettings.Networks
}
if err := client.ContainerRemove(ctx, req.Name, container.RemoveOptions{Force: true}); err != nil {
return err
@@ -937,15 +936,27 @@ func calculateCPUPercentUnix(stats *types.StatsJSON) float64 {
}
return cpuPercent
}
func calculateMemPercentUnix(memStats types.MemoryStats) float64 {
memPercent := 0.0
memUsage := float64(memStats.Usage)
memLimit := float64(memStats.Limit)
memUsage := calculateMemUsageUnixNoCache(memStats)
memLimit := memStats.Limit
if memUsage > 0.0 && memLimit > 0.0 {
memPercent = (memUsage / memLimit) * 100.0
memPercent = (float64(memUsage) / float64(memLimit)) * 100.0
}
return memPercent
}
func calculateMemUsageUnixNoCache(mem types.MemoryStats) uint64 {
if v, isCgroup1 := mem.Stats["total_inactive_file"]; isCgroup1 && v < mem.Usage {
return mem.Usage - v
}
if v := mem.Stats["inactive_file"]; v < mem.Usage {
return mem.Usage - v
}
return mem.Usage
}
func calculateBlockIO(blkio types.BlkioStats) (blkRead float64, blkWrite float64) {
for _, bioEntry := range blkio.IoServiceBytesRecursive {
switch strings.ToLower(bioEntry.Op) {
@@ -1063,7 +1074,7 @@ func loadCpuAndMem(client *client.Client, container string) dto.ContainerListSta
data.PercpuUsage = len(stats.CPUStats.CPUUsage.PercpuUsage)
data.MemoryCache = stats.MemoryStats.Stats["cache"]
data.MemoryUsage = stats.MemoryStats.Usage
data.MemoryUsage = calculateMemUsageUnixNoCache(stats.MemoryStats)
data.MemoryLimit = stats.MemoryStats.Limit
data.MemoryPercent = calculateMemPercentUnix(stats.MemoryStats)
@@ -1183,9 +1194,10 @@ func loadConfigInfo(isCreate bool, req dto.ContainerOperate, oldContainer *types
for _, volume := range req.Volumes {
if volume.Type == "volume" {
hostConf.Mounts = append(hostConf.Mounts, mount.Mount{
Type: mount.Type(volume.Type),
Source: volume.SourceDir,
Target: volume.ContainerDir,
Type: mount.Type(volume.Type),
Source: volume.SourceDir,
Target: volume.ContainerDir,
ReadOnly: volume.Mode == "ro",
})
config.Volumes[volume.ContainerDir] = struct{}{}
} else {
@@ -1200,10 +1212,7 @@ func reCreateAfterUpdate(name string, client *client.Client, config *container.C
var oldNetworkConf network.NetworkingConfig
if networkConf != nil {
for networkKey := range networkConf.Networks {
oldNetworkConf.EndpointsConfig = map[string]*network.EndpointSettings{networkKey: {}}
break
}
oldNetworkConf.EndpointsConfig = networkConf.Networks
}
oldContainer, err := client.ContainerCreate(ctx, config, hostConf, &oldNetworkConf, &v1.Platform{}, name)
@@ -1262,12 +1271,12 @@ func loadContainerPortForInfo(itemPorts []types.Port) []dto.PortHelper {
var exposedPorts []dto.PortHelper
samePortMap := make(map[string]dto.PortHelper)
ports := transPortToStr(itemPorts)
var itemPort dto.PortHelper
for _, item := range ports {
itemStr := strings.Split(item, "->")
if len(itemStr) < 2 {
continue
}
var itemPort dto.PortHelper
lastIndex := strings.LastIndex(itemStr[0], ":")
if lastIndex == -1 {
itemPort.HostPort = itemStr[0]
+11 -2
View File
@@ -114,8 +114,17 @@ func (u *CronjobService) CleanRecord(req dto.CronjobClean) error {
if err != nil {
return err
}
if !req.CleanRemoteData {
for key := range accountMap {
if key != constant.Local {
delete(accountMap, key)
}
}
}
cronjob.RetainCopies = 0
u.removeExpiredBackup(cronjob, accountMap, model.BackupRecord{})
if len(accountMap) != 0 {
u.removeExpiredBackup(cronjob, accountMap, model.BackupRecord{})
}
} else {
u.removeExpiredLog(cronjob)
}
@@ -249,7 +258,7 @@ func (u *CronjobService) Delete(req dto.CronjobBatchDelete) error {
global.Cron.Remove(cron.EntryID(idItem))
}
global.LOG.Infof("stop cronjob entryID: %s", cronjob.EntryIDs)
if err := u.CleanRecord(dto.CronjobClean{CronjobID: id, CleanData: req.CleanData, IsDelete: true}); err != nil {
if err := u.CleanRecord(dto.CronjobClean{CronjobID: id, CleanData: req.CleanData, CleanRemoteData: req.CleanRemoteData, IsDelete: true}); err != nil {
return err
}
if err := cronjobRepo.Delete(commonRepo.WithByID(id)); err != nil {
+8 -1
View File
@@ -322,7 +322,8 @@ func (u *CronjobService) uploadCronjobBackFile(cronjob model.Cronjob, accountMap
if len(account) != 0 {
global.LOG.Debugf("start upload file to %s, dir: %s", account, path.Join(accountMap[account].backupPath, cloudSrc))
if _, err := accountMap[account].client.Upload(file, path.Join(accountMap[account].backupPath, cloudSrc)); err != nil {
return "", err
global.LOG.Errorf("upload file to %s failed, err: %v", account, err)
continue
}
global.LOG.Debugf("upload successful!")
}
@@ -349,12 +350,18 @@ func (u *CronjobService) removeExpiredBackup(cronjob model.Cronjob, accountMap m
if cronjob.Type == "snapshot" {
for _, account := range accounts {
if len(account) != 0 {
if _, ok := accountMap[account]; !ok {
continue
}
_, _ = accountMap[account].client.Delete(path.Join(accountMap[account].backupPath, "system_snapshot", records[i].FileName))
}
}
_ = snapshotRepo.Delete(commonRepo.WithByName(strings.TrimSuffix(records[i].FileName, ".tar.gz")))
} else {
for _, account := range accounts {
if _, ok := accountMap[account]; !ok {
continue
}
if len(account) != 0 {
_, _ = accountMap[account].client.Delete(path.Join(accountMap[account].backupPath, records[i].FileDir, records[i].FileName))
}
+2 -1
View File
@@ -18,6 +18,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/ntp"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
"github.com/shirou/gopsutil/v3/mem"
)
@@ -121,7 +122,7 @@ func (u *DeviceService) Update(key, value string) error {
return err
}
go func() {
_, err := cmd.Exec("systemctl restart 1panel.service")
err := systemctl.Restart("1panel")
if err != nil {
global.LOG.Errorf("restart system for new time zone failed, err: %v", err)
}
+2 -2
View File
@@ -11,12 +11,12 @@ import (
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/utils/docker"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common"
fileUtils "github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/google/uuid"
@@ -285,7 +285,7 @@ func (u *DeviceService) Clean(req []dto.Clean) {
if restart {
go func() {
_, err := cmd.Exec("systemctl restart 1panel.service")
err := systemctl.Restart("1panel")
if err != nil {
global.LOG.Errorf("restart system port failed, err: %v", err)
}
+30 -26
View File
@@ -365,17 +365,18 @@ func (u *DockerService) UpdateConfByFile(req dto.DaemonJsonUpdateByFile) error {
func (u *DockerService) OperateDocker(req dto.DockerOperation) error {
service := "docker"
sudo := cmd.SudoHandleCmd()
dockerCmd, err := getDockerRestartCommand()
h, err := systemctl.DefaultHandler(service)
if err != nil {
return err
}
if req.Operation == "stop" {
isSocketActive, _ := systemctl.IsActive("docker.socket")
if isSocketActive {
std, err := cmd.Execf("%s systemctl stop docker.socket", sudo)
if err != nil {
global.LOG.Errorf("handle systemctl stop docker.socket failed, err: %v", std)
socketHandle, err := systemctl.DefaultHandler("docker.socket")
if err == nil {
status, err := socketHandle.IsActive()
if err == nil && status.IsActive {
if std, err := socketHandle.ExecuteAction("stop"); err != nil {
global.LOG.Errorf("handle stop docker.socket failed, err: %v", std)
}
}
}
}
@@ -386,13 +387,20 @@ func (u *DockerService) OperateDocker(req dto.DockerOperation) error {
}
}
stdout, err := cmd.Execf("%s %s %s", dockerCmd, req.Operation, service)
if isDockerSnapInstalled() {
command := fmt.Sprintf("snap %s docker", req.Operation)
stdout, err := cmd.Exec(command)
if err != nil {
return fmt.Errorf("failed to restart docker: %v", stdout)
}
return nil
}
result, err := h.ExecuteAction(req.Operation)
if err != nil {
return errors.New(stdout)
return errors.New(result.Output)
}
return nil
}
func changeLogOption(daemonMap map[string]interface{}, logMaxFile, logMaxSize string) {
if opts, ok := daemonMap["log-opts"]; ok {
if len(logMaxFile) != 0 || len(logMaxSize) != 0 {
@@ -451,31 +459,27 @@ func validateDockerConfig() error {
return nil
}
if err != nil || (stdout != "" && strings.TrimSpace(stdout) != "configuration OK") {
return fmt.Errorf("Docker configuration validation failed, err: %v", stdout)
return fmt.Errorf("docker configuration validation failed, err: %v", stdout)
}
return nil
}
func getDockerRestartCommand() (string, error) {
func isDockerSnapInstalled() bool {
stdout, err := cmd.Exec("which docker")
if err != nil {
return "", fmt.Errorf("failed to find docker: %v", err)
return false
}
dockerPath := stdout
if strings.Contains(dockerPath, "snap") {
return "snap", nil
}
return "systemctl", nil
stdout = strings.TrimSpace(stdout)
return strings.Contains(stdout, "snap")
}
func restartDocker() error {
restartCmd, err := getDockerRestartCommand()
if err != nil {
return err
if isDockerSnapInstalled() {
stdout, err := cmd.Exec("snap restart docker")
if err != nil {
return fmt.Errorf("failed to restart docker: %v", stdout)
}
return nil
}
stdout, err := cmd.Execf("%s restart docker", restartCmd)
if err != nil {
return fmt.Errorf("failed to restart Docker: %s", stdout)
}
return nil
return systemctl.Restart("docker")
}
+2
View File
@@ -46,4 +46,6 @@ var (
phpExtensionsRepo = repo.NewIPHPExtensionsRepo()
favoriteRepo = repo.NewIFavoriteRepo()
mcpServerRepo = repo.NewIMcpServerRepo()
)
+10 -1
View File
@@ -366,10 +366,18 @@ func (f *FileService) MvFile(m request.FileMove) error {
return buserr.New(constant.ErrMovePathFailed)
}
}
var errs []error
if m.Type == "cut" {
if len(m.CoverPaths) > 0 {
for _, src := range m.CoverPaths {
if err := fo.CopyAndReName(src, m.NewPath, "", true); err != nil {
errs = append(errs, err)
global.LOG.Errorf("cut copy file [%s] to [%s] failed, err: %s", src, m.NewPath, err.Error())
}
}
}
return fo.Cut(m.OldPaths, m.NewPath, m.Name, m.Cover)
}
var errs []error
if m.Type == "copy" {
for _, src := range m.OldPaths {
if err := fo.CopyAndReName(src, m.NewPath, m.Name, m.Cover); err != nil {
@@ -518,6 +526,7 @@ func (f *FileService) BatchCheckFiles(req request.FilePathsCheck) []response.Exi
Name: info.Name(),
Path: filePath,
ModTime: info.ModTime(),
IsDir: info.IsDir(),
})
}
}
+4
View File
@@ -381,6 +381,10 @@ func (u *FirewallService) OperateForwardRule(req dto.ForwardRuleOperate) error {
TargetIP: r.TargetIP,
TargetPort: r.TargetPort,
}, r.Operation); err != nil {
if req.ForceDelete {
global.LOG.Error(err)
continue
}
return err
}
}
+119 -89
View File
@@ -3,6 +3,12 @@ package service
import (
"bytes"
"fmt"
"os/exec"
"os/user"
"path"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
"github.com/1Panel-dev/1Panel/backend/app/dto/response"
"github.com/1Panel-dev/1Panel/backend/buserr"
@@ -14,11 +20,6 @@ import (
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
"github.com/pkg/errors"
"gopkg.in/ini.v1"
"os/exec"
"os/user"
"path"
"strconv"
"strings"
)
type HostToolService struct{}
@@ -39,93 +40,108 @@ func NewIHostToolService() IHostToolService {
}
func (h *HostToolService) GetToolStatus(req request.HostToolReq) (*response.HostToolRes, error) {
res := &response.HostToolRes{}
res.Type = req.Type
res := &response.HostToolRes{Type: req.Type}
switch req.Type {
case constant.Supervisord:
supervisorConfig := &response.Supervisor{}
if !cmd.Which(constant.Supervisord) {
supervisorConfig.IsExist = false
res.Config = supervisorConfig
return res, nil
}
supervisorConfig.IsExist = true
serviceExist, _ := systemctl.IsExist(constant.Supervisord)
if !serviceExist {
serviceExist, _ = systemctl.IsExist(constant.Supervisor)
if !serviceExist {
supervisorConfig.IsExist = false
res.Config = supervisorConfig
return res, nil
} else {
supervisorConfig.ServiceName = constant.Supervisor
}
} else {
supervisorConfig.ServiceName = constant.Supervisord
}
serviceNameSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName))
if serviceNameSet.ID != 0 || serviceNameSet.Value != "" {
supervisorConfig.ServiceName = serviceNameSet.Value
}
versionRes, _ := cmd.Exec("supervisord -v")
supervisorConfig.Version = strings.TrimSuffix(versionRes, "\n")
_, ctlRrr := exec.LookPath("supervisorctl")
supervisorConfig.CtlExist = ctlRrr == nil
active, _ := systemctl.IsActive(supervisorConfig.ServiceName)
if active {
supervisorConfig.Status = "running"
} else {
supervisorConfig.Status = "stopped"
}
pathSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorConfigPath))
if pathSet.ID != 0 || pathSet.Value != "" {
supervisorConfig.ConfigPath = pathSet.Value
res.Config = supervisorConfig
return res, nil
} else {
supervisorConfig.Init = true
}
servicePath := "/usr/lib/systemd/system/supervisor.service"
fileOp := files.NewFileOp()
if !fileOp.Stat(servicePath) {
servicePath = "/usr/lib/systemd/system/supervisord.service"
}
if fileOp.Stat(servicePath) {
startCmd, _ := ini_conf.GetIniValue(servicePath, "Service", "ExecStart")
if startCmd != "" {
args := strings.Fields(startCmd)
cIndex := -1
for i, arg := range args {
if arg == "-c" {
cIndex = i
break
}
}
if cIndex != -1 && cIndex+1 < len(args) {
supervisorConfig.ConfigPath = args[cIndex+1]
}
}
}
if supervisorConfig.ConfigPath == "" {
configPath := "/etc/supervisord.conf"
if !fileOp.Stat(configPath) {
configPath = "/etc/supervisor/supervisord.conf"
if fileOp.Stat(configPath) {
supervisorConfig.ConfigPath = configPath
}
}
}
res.Config = supervisorConfig
return h.getSupervisorStatus(res)
}
return res, nil
}
func (h *HostToolService) getSupervisorStatus(res *response.HostToolRes) (*response.HostToolRes, error) {
supervisorConfig := &response.Supervisor{}
if !cmd.Which(constant.Supervisord) {
supervisorConfig.IsExist = false
res.Config = supervisorConfig
return res, nil
}
supervisorConfig.IsExist = true
serviceName, err := h.determineServiceName()
if err != nil || serviceName == "" {
supervisorConfig.IsExist = false
res.Config = supervisorConfig
return res, nil
}
supervisorConfig.ServiceName = serviceName
if nameSetting, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName)); nameSetting.Value != "" {
supervisorConfig.ServiceName = nameSetting.Value
}
if version, err := cmd.Exec("supervisord -v"); err == nil {
supervisorConfig.Version = strings.TrimSpace(version)
}
_, errCtl := exec.LookPath("supervisorctl")
supervisorConfig.CtlExist = errCtl == nil
if active, err := systemctl.IsActive(supervisorConfig.ServiceName); err == nil && active {
supervisorConfig.Status = "running"
} else {
supervisorConfig.Status = "stopped"
}
h.resolveConfigPath(supervisorConfig)
res.Config = supervisorConfig
return res, nil
}
func (h *HostToolService) determineServiceName() (string, error) {
if setting, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName)); setting.Value != "" {
serviceName, err := systemctl.GetServiceName(setting.Value)
if err != nil {
return "", fmt.Errorf("get service name error: %v", err)
}
return serviceName, nil
}
if serviceName, err := systemctl.GetServiceName(constant.Supervisord); err == nil {
return serviceName, nil
}
return systemctl.GetServiceName(constant.Supervisor)
}
func (h *HostToolService) resolveConfigPath(config *response.Supervisor) {
if pathSetting, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorConfigPath)); pathSetting.Value != "" {
config.ConfigPath = pathSetting.Value
return
}
config.Init = true
if servicePath, err := systemctl.GetServicePath(config.ServiceName); err == nil {
if startCmd, _ := ini_conf.GetIniValue(servicePath, "Service", "ExecStart"); startCmd != "" {
if path := parseConfigPathFromCommand(startCmd); path != "" {
config.ConfigPath = path
return
}
}
}
defaultPaths := []string{
"/etc/supervisord.conf",
"/etc/supervisor/supervisord.conf",
"/usr/local/etc/supervisord.conf",
}
for _, path := range defaultPaths {
if systemctl.FileExist(path) {
config.ConfigPath = path
return
}
}
}
func parseConfigPathFromCommand(cmd string) string {
parts := strings.Fields(cmd)
for i, part := range parts {
if part == "-c" && i+1 < len(parts) {
return parts[i+1]
}
}
return ""
}
func (h *HostToolService) CreateToolConfig(req request.HostToolCreate) error {
switch req.Type {
case constant.Supervisord:
@@ -200,14 +216,28 @@ func (h *HostToolService) CreateToolConfig(req request.HostToolCreate) error {
}
func (h *HostToolService) OperateTool(req request.HostToolReq) error {
serviceName := req.Type
if req.Type == constant.Supervisord {
var serviceName string
var err error
switch req.Type {
case constant.Supervisord:
serviceNameSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName))
if serviceNameSet.ID != 0 || serviceNameSet.Value != "" {
serviceName = serviceNameSet.Value
} else {
serviceName = req.Type
}
default:
serviceName = req.Type
}
return systemctl.Operate(req.Operate, serviceName)
serviceName, err = systemctl.GetServiceName(serviceName)
if err != nil {
if errors.Is(err, systemctl.ErrServiceNotFound) {
return fmt.Errorf("%s service unavailable. Please ensure supervisor server is installed and configured", serviceName)
}
return err
}
_, err = systemctl.CustomAction(req.Operate, serviceName)
return err
}
func (h *HostToolService) OperateToolConfig(req request.HostToolConfig) (*response.HostToolConfig, error) {
+73 -47
View File
@@ -26,7 +26,7 @@ type IImageRepoService interface {
Login(req dto.OperateByID) error
Create(req dto.ImageRepoCreate) error
Update(req dto.ImageRepoUpdate) error
BatchDelete(req dto.ImageRepoDelete) error
Delete(req dto.OperateByID) error
}
func NewIImageRepoService() IImageRepoService {
@@ -89,31 +89,7 @@ func (u *ImageRepoService) Create(req dto.ImageRepoCreate) error {
if err := u.handleRegistries(req.DownloadUrl, "", "create"); err != nil {
return fmt.Errorf("create registry %s failed, err: %v", req.DownloadUrl, err)
}
if err := validateDockerConfig(); err != nil {
return err
}
if err := restartDocker(); err != nil {
return err
}
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
if err := func() error {
for range ticker.C {
select {
case <-ctx.Done():
cancel()
return errors.New("the docker service cannot be restarted")
default:
stdout, err := cmd.Exec("systemctl is-active docker")
if string(stdout) == "active\n" && err == nil {
global.LOG.Info("docker restart with new conf successful!")
return nil
}
}
}
return nil
}(); err != nil {
if err := stopBeforeUpdateRepo(); err != nil {
return err
}
}
@@ -131,15 +107,32 @@ func (u *ImageRepoService) Create(req dto.ImageRepoCreate) error {
return imageRepoRepo.Create(&imageRepo)
}
func (u *ImageRepoService) BatchDelete(req dto.ImageRepoDelete) error {
for _, id := range req.Ids {
if id == 1 {
return errors.New("The default value cannot be edit !")
}
func (u *ImageRepoService) Delete(req dto.OperateByID) error {
if req.ID == 1 {
return errors.New("The default value cannot be edit !")
}
if err := imageRepoRepo.Delete(commonRepo.WithIdsIn(req.Ids)); err != nil {
itemRepo, _ := imageRepoRepo.Get(commonRepo.WithByID(req.ID))
if itemRepo.ID == 0 {
return buserr.New("ErrRecordNotFound")
}
if itemRepo.Auth {
_, _ = cmd.Execf("docker logout -i %s", itemRepo.DownloadUrl)
}
if itemRepo.Protocol == "https" {
return imageRepoRepo.Delete(commonRepo.WithByID(req.ID))
}
if err := u.handleRegistries("", itemRepo.DownloadUrl, "delete"); err != nil {
return fmt.Errorf("delete registry %s failed, err: %v", itemRepo.DownloadUrl, err)
}
if err := validateDockerConfig(); err != nil {
return err
}
if err := imageRepoRepo.Delete(commonRepo.WithByID(req.ID)); err != nil {
return err
}
go func() {
_ = restartDocker()
}()
return nil
}
@@ -154,37 +147,39 @@ func (u *ImageRepoService) Update(req dto.ImageRepoUpdate) error {
if err != nil {
return err
}
needRestart := false
if repo.Protocol == "http" && req.Protocol == "https" {
if err := u.handleRegistries("", repo.DownloadUrl, "delete"); err != nil {
return fmt.Errorf("delete registry %s failed, err: %v", repo.DownloadUrl, err)
}
needRestart = true
}
if repo.Protocol == "http" && req.Protocol == "http" {
if err := u.handleRegistries(req.DownloadUrl, repo.DownloadUrl, "update"); err != nil {
return fmt.Errorf("update registry %s => %s failed, err: %v", repo.DownloadUrl, req.DownloadUrl, err)
if repo.DownloadUrl != req.DownloadUrl {
if err := u.handleRegistries(req.DownloadUrl, repo.DownloadUrl, "update"); err != nil {
return fmt.Errorf("update registry %s => %s failed, err: %v", repo.DownloadUrl, req.DownloadUrl, err)
}
needRestart = true
}
}
if repo.Protocol == "https" && req.Protocol == "http" {
if err := u.handleRegistries(req.DownloadUrl, "", "create"); err != nil {
return fmt.Errorf("create registry %s failed, err: %v", req.DownloadUrl, err)
}
needRestart = true
}
if repo.Auth != req.Auth || repo.DownloadUrl != req.DownloadUrl {
if repo.Auth {
_, _ = cmd.ExecWithCheck("docker", "logout", repo.DownloadUrl)
}
if req.Auth {
if err := u.CheckConn(req.DownloadUrl, req.Username, req.Password); err != nil {
return err
}
if needRestart {
if err := stopBeforeUpdateRepo(); err != nil {
return err
}
}
if err := validateDockerConfig(); err != nil {
return err
if repo.Auth {
_, _ = cmd.Execf("docker logout -i %s", repo.DownloadUrl)
}
if err := restartDocker(); err != nil {
return err
if req.Auth {
if err := u.CheckConn(req.DownloadUrl, req.Username, req.Password); err != nil {
return err
}
}
upMap := make(map[string]interface{})
@@ -256,3 +251,34 @@ func (u *ImageRepoService) handleRegistries(newHost, delHost, handle string) err
}
return nil
}
func stopBeforeUpdateRepo() error {
if err := validateDockerConfig(); err != nil {
return err
}
if err := restartDocker(); err != nil {
return err
}
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
if err := func() error {
for range ticker.C {
select {
case <-ctx.Done():
cancel()
return errors.New("the docker service cannot be restarted")
default:
stdout, err := cmd.Exec("systemctl is-active docker")
if string(stdout) == "active\n" && err == nil {
global.LOG.Info("docker restart with new conf successful!")
return nil
}
}
}
return nil
}(); err != nil {
return err
}
return nil
}
+643
View File
@@ -0,0 +1,643 @@
package service
import (
"context"
"errors"
"fmt"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
"github.com/1Panel-dev/1Panel/backend/app/dto/response"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/compose"
"github.com/1Panel-dev/1Panel/backend/utils/docker"
"github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/1Panel-dev/1Panel/backend/utils/nginx"
"github.com/1Panel-dev/1Panel/backend/utils/nginx/components"
"github.com/1Panel-dev/1Panel/backend/utils/nginx/parser"
"github.com/1Panel-dev/1Panel/cmd/server/mcp"
"github.com/1Panel-dev/1Panel/cmd/server/nginx_conf"
"github.com/subosito/gotenv"
"gopkg.in/yaml.v3"
"path"
"strconv"
"strings"
)
type McpServerService struct{}
type IMcpServerService interface {
Page(req request.McpServerSearch) response.McpServersRes
Create(create request.McpServerCreate) error
Update(req request.McpServerUpdate) error
Delete(id uint) error
Operate(req request.McpServerOperate) error
GetBindDomain() (response.McpBindDomainRes, error)
BindDomain(req request.McpBindDomain) error
UpdateBindDomain(req request.McpBindDomainUpdate) error
}
func NewIMcpServerService() IMcpServerService {
return &McpServerService{}
}
func (m McpServerService) Page(req request.McpServerSearch) response.McpServersRes {
var (
res response.McpServersRes
items []response.McpServerDTO
)
total, data, _ := mcpServerRepo.Page(req.PageInfo.Page, req.PageInfo.PageSize)
for _, item := range data {
_ = syncMcpServerContainerStatus(&item)
serverDTO := response.McpServerDTO{
McpServer: item,
Environments: make([]request.Environment, 0),
Volumes: make([]request.Volume, 0),
}
project, _ := docker.GetComposeProject(item.Name, path.Join(constant.McpDir, item.Name), []byte(item.DockerCompose), []byte(item.Env), true)
for _, service := range project.Services {
if service.Environment != nil {
for key, value := range service.Environment {
serverDTO.Environments = append(serverDTO.Environments, request.Environment{
Key: key,
Value: *value,
})
}
}
if service.Volumes != nil {
for _, volume := range service.Volumes {
serverDTO.Volumes = append(serverDTO.Volumes, request.Volume{
Source: volume.Source,
Target: volume.Target,
})
}
}
}
items = append(items, serverDTO)
}
res.Total = total
res.Items = items
return res
}
func (m McpServerService) Update(req request.McpServerUpdate) error {
mcpServer, err := mcpServerRepo.GetFirst(commonRepo.WithByID(req.ID))
if err != nil {
return err
}
if mcpServer.Port != req.Port {
if err := checkPortExist(req.Port); err != nil {
return err
}
}
if mcpServer.ContainerName != req.ContainerName {
if err := checkContainerName(req.ContainerName); err != nil {
return err
}
}
mcpServer.Name = req.Name
mcpServer.ContainerName = req.ContainerName
mcpServer.Port = req.Port
mcpServer.Command = req.Command
mcpServer.BaseURL = req.BaseURL
mcpServer.SsePath = req.SsePath
mcpServer.HostIP = req.HostIP
if err := handleCreateParams(mcpServer, req.Environments, req.Volumes); err != nil {
return err
}
env := handleEnv(mcpServer)
mcpDir := path.Join(constant.McpDir, mcpServer.Name)
envPath := path.Join(mcpDir, ".env")
if err := gotenv.Write(env, envPath); err != nil {
return err
}
dockerComposePath := path.Join(mcpDir, "docker-compose.yml")
if err := files.NewFileOp().SaveFile(dockerComposePath, mcpServer.DockerCompose, 0644); err != nil {
return err
}
mcpServer.Status = constant.RuntimeStarting
if err := mcpServerRepo.Save(mcpServer); err != nil {
return err
}
go startMcp(mcpServer)
return nil
}
func (m McpServerService) Create(create request.McpServerCreate) error {
servers, _ := mcpServerRepo.List()
for _, server := range servers {
if server.Port == create.Port {
return buserr.New("ErrPortInUsed")
}
if server.ContainerName == create.ContainerName {
return buserr.New("ErrContainerName")
}
if server.Name == create.Name {
return buserr.New(constant.ErrNameIsExist)
}
if server.SsePath == create.SsePath {
return buserr.New("ErrSsePath")
}
}
if err := checkPortExist(create.Port); err != nil {
return err
}
if err := checkContainerName(create.ContainerName); err != nil {
return err
}
mcpDir := path.Join(constant.McpDir, create.Name)
mcpServer := &model.McpServer{
Name: create.Name,
ContainerName: create.ContainerName,
Port: create.Port,
Command: create.Command,
Status: constant.RuntimeStarting,
BaseURL: create.BaseURL,
SsePath: create.SsePath,
Dir: mcpDir,
HostIP: create.HostIP,
}
if err := handleCreateParams(mcpServer, create.Environments, create.Volumes); err != nil {
return err
}
env := handleEnv(mcpServer)
filesOP := files.NewFileOp()
if !filesOP.Stat(mcpDir) {
_ = filesOP.CreateDir(mcpDir, 0644)
}
envPath := path.Join(mcpDir, ".env")
if err := gotenv.Write(env, envPath); err != nil {
return err
}
dockerComposePath := path.Join(mcpDir, "docker-compose.yml")
if err := filesOP.SaveFile(dockerComposePath, mcpServer.DockerCompose, 0644); err != nil {
return err
}
if err := mcpServerRepo.Create(mcpServer); err != nil {
return err
}
addProxy(mcpServer)
go startMcp(mcpServer)
return nil
}
func (m McpServerService) Delete(id uint) error {
mcpServer, err := mcpServerRepo.GetFirst(commonRepo.WithByID(id))
if err != nil {
return err
}
composePath := path.Join(constant.McpDir, mcpServer.Name, "docker-compose.yml")
_, _ = compose.Down(composePath)
_ = files.NewFileOp().DeleteDir(path.Join(constant.McpDir, mcpServer.Name))
websiteID := GetWebsiteID()
if websiteID > 0 {
websiteService := NewIWebsiteService()
delProxyReq := request.WebsiteProxyDel{
ID: websiteID,
Name: mcpServer.Name,
}
_ = websiteService.DeleteProxy(delProxyReq)
}
return mcpServerRepo.DeleteBy(commonRepo.WithByID(id))
}
func (m McpServerService) Operate(req request.McpServerOperate) error {
mcpServer, err := mcpServerRepo.GetFirst(commonRepo.WithByID(req.ID))
if err != nil {
return err
}
composePath := path.Join(mcpServer.Dir, "docker-compose.yml")
var out string
switch req.Operate {
case "start":
out, err = compose.Up(composePath)
mcpServer.Status = constant.RuntimeRunning
case "stop":
out, err = compose.Down(composePath)
mcpServer.Status = constant.RuntimeStopped
case "restart":
out, err = compose.Restart(composePath)
mcpServer.Status = constant.RuntimeRunning
}
if err != nil {
mcpServer.Status = constant.RuntimeError
mcpServer.Message = out
}
return mcpServerRepo.Save(mcpServer)
}
func (m McpServerService) GetBindDomain() (response.McpBindDomainRes, error) {
var res response.McpBindDomainRes
websiteID := GetWebsiteID()
if websiteID == 0 {
return res, nil
}
website, err := websiteRepo.GetFirst(commonRepo.WithByID(websiteID))
if err != nil {
return res, nil
}
res.WebsiteID = website.ID
res.Domain = website.PrimaryDomain
if website.WebsiteSSLID > 0 {
res.SSLID = website.WebsiteSSLID
ssl, _ := websiteSSLRepo.GetFirst(commonRepo.WithByID(website.WebsiteSSLID))
res.AcmeAccountID = ssl.AcmeAccountID
}
res.ConnUrl = fmt.Sprintf("%s://%s", strings.ToLower(website.Protocol), website.PrimaryDomain)
res.AllowIPs = GetAllowIps(website)
return res, nil
}
func (m McpServerService) BindDomain(req request.McpBindDomain) error {
nginxInstall, _ := getAppInstallByKey(constant.AppOpenresty)
if nginxInstall.ID == 0 {
return buserr.New("ErrOpenrestyInstall")
}
var (
ipList []string
err error
)
if len(req.IPList) > 0 {
ipList, err = common.HandleIPList(req.IPList)
if err != nil {
return err
}
}
if req.SSLID > 0 {
ssl, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(req.SSLID))
if err != nil {
return err
}
if ssl.Pem == "" {
return buserr.New("ErrSSL")
}
}
group, _ := groupRepo.Get(groupRepo.WithByWebsiteDefault())
createWebsiteReq := request.WebsiteCreate{
PrimaryDomain: req.Domain,
Alias: strings.ToLower(req.Domain),
Type: constant.Static,
WebsiteGroupID: group.ID,
}
websiteService := NewIWebsiteService()
if err := websiteService.CreateWebsite(createWebsiteReq); err != nil {
return err
}
website, err := websiteRepo.GetFirst(websiteRepo.WithAlias(strings.ToLower(req.Domain)))
if err != nil {
return err
}
_ = settingRepo.UpdateOrCreate("MCP_WEBSITE_ID", fmt.Sprintf("%d", website.ID))
if len(ipList) > 0 {
if err = ConfigAllowIPs(ipList, website); err != nil {
return err
}
}
if req.SSLID > 0 {
sslReq := request.WebsiteHTTPSOp{
WebsiteID: website.ID,
Enable: true,
Type: "existed",
WebsiteSSLID: req.SSLID,
HttpConfig: "HTTPSOnly",
}
if _, err = websiteService.OpWebsiteHTTPS(context.Background(), sslReq); err != nil {
return err
}
}
if err = addMCPProxy(website.ID); err != nil {
return err
}
return nil
}
func (m McpServerService) UpdateBindDomain(req request.McpBindDomainUpdate) error {
nginxInstall, _ := getAppInstallByKey(constant.AppOpenresty)
if nginxInstall.ID == 0 {
return buserr.New("ErrOpenrestyInstall")
}
var (
ipList []string
err error
)
if len(req.IPList) > 0 {
ipList, err = common.HandleIPList(req.IPList)
if err != nil {
return err
}
}
if req.SSLID > 0 {
ssl, err := websiteSSLRepo.GetFirst(commonRepo.WithByID(req.SSLID))
if err != nil {
return err
}
if ssl.Pem == "" {
return buserr.New("ErrSSL")
}
}
websiteService := NewIWebsiteService()
website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID))
if err != nil {
return err
}
if err = ConfigAllowIPs(ipList, website); err != nil {
return err
}
if req.SSLID > 0 {
sslReq := request.WebsiteHTTPSOp{
WebsiteID: website.ID,
Enable: true,
Type: "existed",
WebsiteSSLID: req.SSLID,
HttpConfig: "HTTPSOnly",
}
if _, err = websiteService.OpWebsiteHTTPS(context.Background(), sslReq); err != nil {
return err
}
}
if website.WebsiteSSLID > 0 && req.SSLID == 0 {
sslReq := request.WebsiteHTTPSOp{
WebsiteID: website.ID,
Enable: false,
}
if _, err = websiteService.OpWebsiteHTTPS(context.Background(), sslReq); err != nil {
return err
}
}
go updateMcpConfig(website.ID)
return nil
}
func updateMcpConfig(websiteID uint) {
servers, _ := mcpServerRepo.List()
if len(servers) == 0 {
return
}
website, _ := websiteRepo.GetFirst(commonRepo.WithByID(websiteID))
websiteDomain := website.Domains[0]
var baseUrl string
if website.Protocol == constant.ProtocolHTTP {
baseUrl = fmt.Sprintf("http://%s", websiteDomain.Domain)
} else {
baseUrl = fmt.Sprintf("https://%s", websiteDomain.Domain)
}
go func() {
for _, server := range servers {
if server.BaseURL != baseUrl {
server.BaseURL = baseUrl
server.HostIP = "127.0.0.1"
_ = updateMcpServer(&server)
}
}
}()
}
func addProxy(server *model.McpServer) {
websiteID := GetWebsiteID()
website, err := websiteRepo.GetFirst(commonRepo.WithByID(websiteID))
if err != nil {
global.LOG.Errorf("[mcp] add proxy failed, err: %v", err)
return
}
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
global.LOG.Errorf("[mcp] add proxy failed, err: %v", err)
return
}
fileOp := files.NewFileOp()
includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "proxy")
if !fileOp.Stat(includeDir) {
if err = fileOp.CreateDir(includeDir, 0644); err != nil {
return
}
}
config, err := parser.NewStringParser(string(nginx_conf.SSE)).Parse()
if err != nil {
return
}
includePath := path.Join(includeDir, server.Name+".conf")
config.FilePath = includePath
directives := config.Directives
location, ok := directives[0].(*components.Location)
if !ok {
return
}
location.UpdateDirective("proxy_pass", []string{fmt.Sprintf("http://127.0.0.1:%d%s", server.Port, server.SsePath)})
location.ChangePath("^~", server.SsePath)
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
global.LOG.Errorf("write config failed, err: %v", buserr.WithErr(constant.ErrUpdateBuWebsite, err))
return
}
nginxInclude := fmt.Sprintf("/www/sites/%s/proxy/*.conf", website.Alias)
if err = updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "include", Params: []string{nginxInclude}}}, &website); err != nil {
global.LOG.Errorf("update nginx config failed, err: %v", err)
return
}
}
func addMCPProxy(websiteID uint) error {
servers, _ := mcpServerRepo.List()
if len(servers) == 0 {
return nil
}
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
}
website, err := websiteRepo.GetFirst(commonRepo.WithByID(websiteID))
if err != nil {
return err
}
fileOp := files.NewFileOp()
includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "proxy")
if !fileOp.Stat(includeDir) {
if err = fileOp.CreateDir(includeDir, 0644); err != nil {
return err
}
}
config, err := parser.NewStringParser(string(nginx_conf.SSE)).Parse()
if err != nil {
return err
}
websiteDomain := website.Domains[0]
var baseUrl string
if website.Protocol == constant.ProtocolHTTP {
baseUrl = fmt.Sprintf("http://%s", websiteDomain.Domain)
} else {
baseUrl = fmt.Sprintf("https://%s", websiteDomain.Domain)
}
if websiteDomain.Port != 80 && websiteDomain.Port != 443 {
baseUrl = fmt.Sprintf("%s:%d", baseUrl, websiteDomain.Port)
}
for _, server := range servers {
includePath := path.Join(includeDir, server.Name+".conf")
config.FilePath = includePath
directives := config.Directives
location, ok := directives[0].(*components.Location)
if !ok {
err = errors.New("error")
return err
}
location.UpdateDirective("proxy_pass", []string{fmt.Sprintf("http://127.0.0.1:%d%s", server.Port, server.SsePath)})
location.ChangePath("^~", server.SsePath)
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
return buserr.WithErr(constant.ErrUpdateBuWebsite, err)
}
server.BaseURL = baseUrl
server.HostIP = "127.0.0.1"
go updateMcpServer(&server)
}
nginxInclude := fmt.Sprintf("/www/sites/%s/proxy/*.conf", website.Alias)
if err = updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "include", Params: []string{nginxInclude}}}, &website); err != nil {
return err
}
return nil
}
func updateMcpServer(mcpServer *model.McpServer) error {
env := handleEnv(mcpServer)
if err := gotenv.Write(env, path.Join(mcpServer.Dir, ".env")); err != nil {
return err
}
_ = mcpServerRepo.Save(mcpServer)
composePath := path.Join(constant.McpDir, mcpServer.Name, "docker-compose.yml")
_, _ = compose.Down(composePath)
if _, err := compose.Up(composePath); err != nil {
mcpServer.Status = constant.RuntimeError
mcpServer.Message = err.Error()
}
return mcpServerRepo.Save(mcpServer)
}
func handleEnv(mcpServer *model.McpServer) gotenv.Env {
env := make(gotenv.Env)
env["CONTAINER_NAME"] = mcpServer.ContainerName
env["COMMAND"] = mcpServer.Command
env["PANEL_APP_PORT_HTTP"] = strconv.Itoa(mcpServer.Port)
env["BASE_URL"] = mcpServer.BaseURL
env["SSE_PATH"] = mcpServer.SsePath
env["HOST_IP"] = mcpServer.HostIP
envStr, _ := gotenv.Marshal(env)
mcpServer.Env = envStr
return env
}
func handleCreateParams(mcpServer *model.McpServer, environments []request.Environment, volumes []request.Volume) error {
var composeContent []byte
if mcpServer.ID == 0 {
composeContent = mcp.DefaultMcpCompose
} else {
composeContent = []byte(mcpServer.DockerCompose)
}
composeMap := make(map[string]interface{})
if err := yaml.Unmarshal(composeContent, &composeMap); err != nil {
return err
}
services, serviceValid := composeMap["services"].(map[string]interface{})
if !serviceValid {
return buserr.New(constant.ErrFileParse)
}
serviceName := ""
serviceValue := make(map[string]interface{})
if mcpServer.ID > 0 {
serviceName = mcpServer.Name
serviceValue = services[serviceName].(map[string]interface{})
} else {
for name, service := range services {
serviceName = name
serviceValue = service.(map[string]interface{})
break
}
delete(services, serviceName)
}
delete(serviceValue, "environment")
if len(environments) > 0 {
envMap := make(map[string]string)
for _, env := range environments {
envMap[env.Key] = env.Value
}
serviceValue["environment"] = envMap
}
delete(serviceValue, "volumes")
if len(volumes) > 0 {
volumeList := make([]string, 0)
for _, volume := range volumes {
volumeList = append(volumeList, fmt.Sprintf("%s:%s", volume.Source, volume.Target))
}
serviceValue["volumes"] = volumeList
}
services[mcpServer.Name] = serviceValue
composeByte, err := yaml.Marshal(composeMap)
if err != nil {
return err
}
mcpServer.DockerCompose = string(composeByte)
return nil
}
func startMcp(mcpServer *model.McpServer) {
composePath := path.Join(constant.McpDir, mcpServer.Name, "docker-compose.yml")
if mcpServer.Status != constant.RuntimeNormal {
_, _ = compose.Down(composePath)
}
if out, err := compose.Up(composePath); err != nil {
mcpServer.Status = constant.RuntimeError
mcpServer.Message = out
} else {
mcpServer.Status = constant.RuntimeRunning
mcpServer.Message = ""
}
_ = syncMcpServerContainerStatus(mcpServer)
}
func syncMcpServerContainerStatus(mcpServer *model.McpServer) error {
containerNames := []string{mcpServer.ContainerName}
cli, err := docker.NewClient()
if err != nil {
return err
}
defer cli.Close()
containers, err := cli.ListContainersByName(containerNames)
if err != nil {
return err
}
if len(containers) == 0 {
mcpServer.Status = constant.RuntimeStopped
return mcpServerRepo.Save(mcpServer)
}
container := containers[0]
switch container.State {
case "exited":
mcpServer.Status = constant.RuntimeError
case "running":
mcpServer.Status = constant.RuntimeRunning
case "paused":
mcpServer.Status = constant.RuntimeStopped
case "restarting":
mcpServer.Status = constant.RuntimeReStarting
default:
if mcpServer.Status != constant.RuntimeBuildIng {
mcpServer.Status = constant.RuntimeStopped
}
}
return mcpServerRepo.Save(mcpServer)
}
func GetWebsiteID() uint {
websiteID, _ := settingRepo.Get(settingRepo.WithByKey("MCP_WEBSITE_ID"))
if websiteID.Value == "" {
return 0
}
websiteIDUint, _ := strconv.ParseUint(websiteID.Value, 10, 64)
return uint(websiteIDUint)
}
+6 -6
View File
@@ -21,10 +21,10 @@ import (
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/encrypt"
"github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
"github.com/gin-gonic/gin"
"github.com/robfig/cron/v3"
)
@@ -166,7 +166,7 @@ func (u *SettingService) UpdateBindInfo(req dto.BindInfo) error {
}
go func() {
time.Sleep(1 * time.Second)
_, err := cmd.Exec("systemctl restart 1panel.service")
err := systemctl.Restart("1panel")
if err != nil {
global.LOG.Errorf("restart system with new bind info failed, err: %v", err)
}
@@ -214,7 +214,7 @@ func (u *SettingService) UpdatePort(port uint) error {
}
go func() {
time.Sleep(1 * time.Second)
_, err := cmd.Exec("systemctl restart 1panel.service")
err := systemctl.Restart("1panel")
if err != nil {
global.LOG.Errorf("restart system port failed, err: %v", err)
}
@@ -237,7 +237,7 @@ func (u *SettingService) UpdateSSL(c *gin.Context, req dto.SSLUpdate) error {
c.SetCookie(constant.SessionName, sID, 0, "", "", false, true)
go func() {
_, err := cmd.Exec("systemctl restart 1panel.service")
err := systemctl.Restart("1panel")
if err != nil {
global.LOG.Errorf("restart system failed, err: %v", err)
}
@@ -335,7 +335,7 @@ func (u *SettingService) UpdateSSL(c *gin.Context, req dto.SSLUpdate) error {
c.SetCookie(constant.SessionName, sID, 0, "", "", true, true)
go func() {
time.Sleep(1 * time.Second)
_, err := cmd.Exec("systemctl restart 1panel.service")
err := systemctl.Restart("1panel")
if err != nil {
global.LOG.Errorf("restart system failed, err: %v", err)
}
@@ -552,7 +552,7 @@ func (u *SettingService) GenerateRSAKey() error {
return err
}
privateKeyPEM := exportPrivateKeyToPEM(privateKey)
publicKeyPEM, err := exportPublicKeyToPEM(&privateKey.PublicKey)
publicKeyPEM, _ := exportPublicKeyToPEM(&privateKey.PublicKey)
err = settingRepo.UpdateOrCreate("PASSWORD_PRIVATE_KEY", privateKeyPEM)
if err != nil {
return err
+2 -1
View File
@@ -503,7 +503,8 @@ func hasOs(name string) bool {
strings.Contains(name, "arm64") ||
strings.Contains(name, "armv7") ||
strings.Contains(name, "ppc64le") ||
strings.Contains(name, "s390x")
strings.Contains(name, "s390x") ||
strings.Contains(name, "riscv64")
}
func loadOs() string {
+31 -7
View File
@@ -17,6 +17,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
)
type snapHelper struct {
@@ -43,16 +44,39 @@ func snapPanel(snap snapHelper, targetDir string) {
defer snap.Wg.Done()
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": constant.Running})
status := constant.StatusDone
if err := common.CopyFile("/usr/local/bin/1panel", path.Join(targetDir, "1panel")); err != nil {
status = err.Error()
h, err := systemctl.DefaultHandler("1panel")
if err != nil {
status = fmt.Sprintf("initialize service handle failed: %v", err)
snap.Status.Panel = status
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": status})
return
}
if err := common.CopyFile("/usr/local/bin/1pctl", targetDir); err != nil {
binDir := systemctl.BinaryPath
servicePath, err := h.GetServicePath()
if err != nil {
status = fmt.Sprintf("get service path failed: %v", err)
snap.Status.Panel = status
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": status})
return
}
if err := common.CopyFile(path.Join(binDir, "1panel"), path.Join(targetDir, "1panel")); err != nil {
status = err.Error()
}
_, _ = cmd.Execf("cp -r /usr/local/bin/lang %s", targetDir)
if err := common.CopyFile("/etc/systemd/system/1panel.service", targetDir); err != nil {
if err := common.CopyFile(path.Join(binDir, "1pctl"), targetDir); err != nil {
status = err.Error()
}
if _, err := cmd.Execf("cp -r %s/lang %s", binDir, targetDir); err != nil {
status = err.Error()
}
initScriptDir := path.Join(constant.DataDir, "initscript")
if err := common.CopyFile(servicePath, initScriptDir); err != nil {
status = err.Error()
}
global.LOG.Debugf("from %s copy init script to %s", initScriptDir, path.Join(targetDir, "initscript"))
if err := common.CopyDirs(initScriptDir, path.Join(targetDir, "initscript")); err != nil { // copy init script to targetDir
status = err.Error()
}
snap.Status.Panel = status
@@ -260,7 +284,7 @@ func handleSnapTar(sourceDir, targetDir, name, exclusionRules string, secret str
commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read --exclude-from=<(find %s -type s -print) -zcf %s %s %s %s", sourceDir, " -"+exStr, path, extraCmd, targetDir+"/"+name)
global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******"))
} else {
commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read --exclude-from=<(find %s -type s -printf '%s' | sed 's|^|./|') -zcf %s %s -C %s .", sourceDir, "%P\n", targetDir+"/"+name, exStr, sourceDir)
commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read --exclude-from=<(find %s -type s -printf '%%P\n' | sed 's|^|./|') -zcf %s %s -C %s .", sourceDir, targetDir+"/"+name, exStr, sourceDir)
global.LOG.Debug(commands)
}
stdout, err := cmd.ExecWithTimeOut(commands, 30*time.Minute)
+34 -5
View File
@@ -15,6 +15,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
"github.com/pkg/errors"
)
@@ -91,8 +92,15 @@ func (u *SnapshotService) HandleSnapshotRecover(snap model.Snapshot, isRecover b
req.IsNew = true
}
h, err := systemctl.DefaultHandler("1panel")
if err != nil {
updateRecoverStatus(snap.ID, isRecover, "ServiceHandle", constant.StatusFailed, fmt.Sprintf("initialize service handle failed: %v", err))
return
}
if req.IsNew || snap.InterruptStep == "1PanelBinary" {
if err := recoverPanel(path.Join(snapFileDir, "1panel/1panel"), "/usr/local/bin"); err != nil {
binDir := systemctl.BinaryPath
if err := recoverPanel(path.Join(snapFileDir, "1panel/1panel"), binDir); err != nil {
updateRecoverStatus(snap.ID, isRecover, "1PanelBinary", constant.StatusFailed, err.Error())
return
}
@@ -100,16 +108,32 @@ func (u *SnapshotService) HandleSnapshotRecover(snap model.Snapshot, isRecover b
req.IsNew = true
}
if req.IsNew || snap.InterruptStep == "1PctlBinary" {
if err := recoverPanel(path.Join(snapFileDir, "1panel/1pctl"), "/usr/local/bin"); err != nil {
binDir := systemctl.BinaryPath
if err := recoverPanel(path.Join(snapFileDir, "1panel/1pctl"), binDir); err != nil {
updateRecoverStatus(snap.ID, isRecover, "1PctlBinary", constant.StatusFailed, err.Error())
return
}
_, _ = cmd.Execf("cp -r %s %s", path.Join(snapFileDir, "1panel/lang"), "/usr/local/bin/")
langDir := path.Join(binDir, "lang")
if err := os.RemoveAll(langDir); err != nil {
updateRecoverStatus(snap.ID, isRecover, "RemoveLang", constant.StatusFailed, fmt.Sprintf("remove lang dir failed: %v", err))
return
}
if err := common.CopyDirs(path.Join(snapFileDir, "1panel/lang"), langDir); err != nil {
updateRecoverStatus(snap.ID, isRecover, "CopyLang", constant.StatusFailed, fmt.Sprintf("copy lang files failed: %v", err))
return
}
global.LOG.Debug("recover 1pctl from snapshot file successful!")
req.IsNew = true
}
if req.IsNew || snap.InterruptStep == "1PanelService" {
if err := recoverPanel(path.Join(snapFileDir, "1panel/1panel.service"), "/etc/systemd/system"); err != nil {
servicePath, err := h.GetServicePath()
currentServiceName := h.GetServiceName()
if err != nil {
updateRecoverStatus(snap.ID, isRecover, "GetServicePath", constant.StatusFailed, fmt.Sprintf("get service path failed: %v", err))
return
}
global.LOG.Debugf("current service path: %s", servicePath)
if err := common.CopyFile(selectInitScript(path.Join(snapFileDir, "1panel/initscript"), currentServiceName), servicePath); err != nil {
updateRecoverStatus(snap.ID, isRecover, "1PanelService", constant.StatusFailed, err.Error())
return
}
@@ -147,7 +171,12 @@ func (u *SnapshotService) HandleSnapshotRecover(snap model.Snapshot, isRecover b
global.LOG.Debugf("remove the file %s after the operation is successful", path.Dir(snapFileDir))
_ = os.RemoveAll(path.Dir(snapFileDir))
}
_, _ = cmd.Exec("systemctl daemon-reload && systemctl restart 1panel.service")
if h.ManagerName() == "systemd" {
_, _ = cmd.Exec("systemctl daemon-reload")
}
if err := systemctl.Restart("1panel"); err != nil {
global.LOG.Errorf("restart 1panel service failed: %v", err)
}
}
func backupBeforeRecover(snap model.Snapshot) error {
+214 -162
View File
@@ -2,8 +2,7 @@ package service
import (
"fmt"
"github.com/1Panel-dev/1Panel/backend/utils/geo"
"github.com/gin-gonic/gin"
"net"
"os"
"os/user"
"path"
@@ -12,6 +11,9 @@ import (
"strings"
"time"
"github.com/1Panel-dev/1Panel/backend/utils/geo"
"github.com/gin-gonic/gin"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
@@ -59,26 +61,16 @@ func (u *SSHService) GetSSHInfo() (*dto.SSHInfo, error) {
if err != nil {
data.Status = constant.StatusDisable
data.Message = err.Error()
} else {
active, err := systemctl.IsActive(serviceName)
if !active {
data.Status = constant.StatusDisable
if err != nil {
data.Message = err.Error()
}
} else {
data.Status = constant.StatusEnable
}
}
out, err := systemctl.RunSystemCtl("is-enabled", serviceName)
if err != nil {
data.AutoStart = false
isEnabled, _ := systemctl.IsEnable(serviceName)
data.AutoStart = isEnabled
isActive, err := systemctl.IsActive(serviceName)
if isActive {
data.Status = constant.StatusEnable
} else {
if out == "alias\n" {
data.AutoStart, _ = systemctl.IsEnable("ssh")
} else {
data.AutoStart = out == "enabled\n"
data.Status = constant.StatusDisable
if err != nil {
data.Message = err.Error()
}
}
@@ -121,29 +113,26 @@ func (u *SSHService) OperateSSH(operation string) error {
if err != nil {
return err
}
sudo := cmd.SudoHandleCmd()
if operation == "enable" || operation == "disable" {
serviceName += ".service"
}
if operation == "stop" {
switch operation {
case "enable":
return systemctl.Enable(serviceName)
case "disable":
return systemctl.Disable(serviceName)
case "start":
return systemctl.Start(serviceName)
case "stop":
isSocketActive, _ := systemctl.IsActive(serviceName + ".socket")
if isSocketActive {
std, err := cmd.Execf("%s systemctl stop %s", sudo, serviceName+".socket")
if err != nil {
global.LOG.Errorf("handle systemctl stop %s.socket failed, err: %v", serviceName, std)
if err := systemctl.Stop(serviceName + ".socket"); err != nil {
global.LOG.Errorf("Failed to stop %s.socket: %v", serviceName, err)
}
}
return systemctl.Stop(serviceName)
case "restart":
return systemctl.Restart(serviceName)
}
stdout, err := cmd.Execf("%s systemctl %s %s", sudo, operation, serviceName)
if err != nil {
if strings.Contains(stdout, "alias name or linked unit file") {
stdout, err := cmd.Execf("%s systemctl %s ssh", sudo, operation)
if err != nil {
return fmt.Errorf("%s ssh(alias name or linked unit file) failed, stdout: %s, err: %v", operation, stdout, err)
}
}
return fmt.Errorf("%s %s failed, stdout: %s, err: %v", operation, serviceName, stdout, err)
if result, err := systemctl.CustomAction(operation, serviceName); err != nil {
return fmt.Errorf("failed to execute custom action: %v", result.Output)
}
return nil
}
@@ -198,7 +187,10 @@ func (u *SSHService) Update(req dto.SSHUpdate) error {
}
}
_, _ = cmd.Execf("%s systemctl restart %s", sudo, serviceName)
err = systemctl.Restart(serviceName)
if err != nil {
global.LOG.Errorf("handle restart %s failed, err: %v", serviceName, err)
}
return nil
}
@@ -216,8 +208,10 @@ func (u *SSHService) UpdateByFile(value string) error {
if _, err = file.WriteString(value); err != nil {
return err
}
sudo := cmd.SudoHandleCmd()
_, _ = cmd.Execf("%s systemctl restart %s", sudo, serviceName)
err = systemctl.Restart(serviceName)
if err != nil {
global.LOG.Errorf("handle restart %s failed, err: %v", serviceName, err)
}
return nil
}
@@ -298,7 +292,7 @@ func (u *SSHService) LoadLog(c *gin.Context, req dto.SearchSSHLog) (*dto.SSHLog,
if err != nil {
return err
}
if !info.IsDir() && (strings.HasPrefix(info.Name(), "secure") || strings.HasPrefix(info.Name(), "auth")) {
if !info.IsDir() && (strings.HasPrefix(info.Name(), "secure") || strings.HasPrefix(info.Name(), "auth") || strings.HasPrefix(info.Name(), "messages")) {
if !strings.HasSuffix(info.Name(), ".gz") {
fileList = append(fileList, sshFileItem{Name: pathItem, Year: info.ModTime().Year()})
return nil
@@ -326,7 +320,8 @@ func (u *SSHService) LoadLog(c *gin.Context, req dto.SearchSSHLog) (*dto.SSHLog,
nyc, _ := time.LoadLocation(common.LoadTimeZoneByCmd())
for _, file := range fileList {
commandItem := ""
if strings.HasPrefix(path.Base(file.Name), "secure") {
switch {
case strings.HasPrefix(path.Base(file.Name), "secure"):
switch req.Status {
case constant.StatusSuccess:
commandItem = fmt.Sprintf("cat %s | grep -a Accepted %s", file.Name, command)
@@ -335,8 +330,16 @@ func (u *SSHService) LoadLog(c *gin.Context, req dto.SearchSSHLog) (*dto.SSHLog,
default:
commandItem = fmt.Sprintf("cat %s | grep -aE '(Failed password for|Accepted)' %s", file.Name, command)
}
}
if strings.HasPrefix(path.Base(file.Name), "auth.log") {
case strings.HasPrefix(path.Base(file.Name), "messages"):
switch req.Status {
case constant.StatusSuccess:
commandItem = fmt.Sprintf("cat %s | grep -aE 'sshd.*Accepted (password|publickey)' %s", file.Name, command)
case constant.StatusFailed:
commandItem = fmt.Sprintf("cat %s | grep -aE 'sshd.*(Failed password for|Connection closed by authenticating user)' %s", file.Name, command)
default:
commandItem = fmt.Sprintf("cat %s | grep -aE 'sshd.*(Accepted|Failed password for|Connection closed)' %s", file.Name, command)
}
case strings.HasPrefix(path.Base(file.Name), "auth.log"):
switch req.Status {
case constant.StatusSuccess:
commandItem = fmt.Sprintf("cat %s | grep -a Accepted %s", file.Name, command)
@@ -432,107 +435,160 @@ func loadSSHData(c *gin.Context, command string, showCountFrom, showCountTo, cur
lines := strings.Split(string(stdout2), "\n")
for i := len(lines) - 1; i >= 0; i-- {
var itemData dto.SSHHistory
line := strings.TrimSpace(lines[i])
if line == "" {
continue
}
parts := strings.Fields(line)
if len(parts) < 12 {
continue
}
// 统一时间解析逻辑
var dateStr string
var timeIndex int
if strings.Contains(parts[0], "-") { // 处理RFC3339时间格式
t, err := time.Parse(time.RFC3339Nano, parts[0])
if err == nil {
dateStr = t.Format("2006 Jan 2 15:04:05")
timeIndex = 0
}
} else { // 处理系统日志格式
dateParts := parts[:3]
if len(dateParts) < 3 {
continue
}
dateStr = strings.Join(dateParts, " ")
timeIndex = 3
}
// 根据日志类型解析内容
switch {
case strings.Contains(lines[i], "Failed password for"):
itemData = loadFailedSecureDatas(lines[i])
if len(itemData.Address) != 0 {
if successCount+failedCount >= showCountFrom && successCount+failedCount < showCountTo {
itemData.Area, _ = geo.GetIPLocation(itemData.Address, common.GetLang(c))
itemData.Date = loadDate(currentYear, itemData.DateStr, nyc)
datas = append(datas, itemData)
}
failedCount++
}
case strings.Contains(lines[i], "Connection closed by authenticating user"):
itemData = loadFailedAuthDatas(lines[i])
if len(itemData.Address) != 0 {
if successCount+failedCount >= showCountFrom && successCount+failedCount < showCountTo {
itemData.Area, _ = geo.GetIPLocation(itemData.Address, common.GetLang(c))
itemData.Date = loadDate(currentYear, itemData.DateStr, nyc)
datas = append(datas, itemData)
}
failedCount++
}
case strings.Contains(lines[i], "Accepted "):
itemData = loadSuccessDatas(lines[i])
if len(itemData.Address) != 0 {
if successCount+failedCount >= showCountFrom && successCount+failedCount < showCountTo {
itemData.Area, _ = geo.GetIPLocation(itemData.Address, common.GetLang(c))
itemData.Date = loadDate(currentYear, itemData.DateStr, nyc)
datas = append(datas, itemData)
}
successCount++
}
case strings.Contains(line, "Failed password for"):
itemData = parseFailedPasswordLog(parts, timeIndex, dateStr)
case strings.Contains(line, "Connection closed by authenticating user"):
itemData = parseConnectionClosedLog(parts, timeIndex, dateStr)
case strings.Contains(line, "Accepted"):
itemData = parseAcceptedLog(parts, timeIndex, dateStr)
default:
continue
}
if itemData.Address == "" {
continue
}
total := successCount + failedCount
if total >= showCountFrom && total < showCountTo {
itemData.Area, _ = geo.GetIPLocation(itemData.Address, common.GetLang(c))
itemData.Date = parseLogDate(currentYear, dateStr, nyc)
datas = append(datas, itemData)
}
if itemData.Status == constant.StatusSuccess {
successCount++
} else {
failedCount++
}
if total >= showCountTo {
break
}
}
return datas, successCount, failedCount
}
func loadSuccessDatas(line string) dto.SSHHistory {
var data dto.SSHHistory
parts := strings.Fields(line)
index, dataStr := analyzeDateStr(parts)
if dataStr == "" {
return data
func parseFailedPasswordLog(parts []string, timeIndex int, dateStr string) dto.SSHHistory {
data := dto.SSHHistory{
DateStr: dateStr,
Status: constant.StatusFailed,
AuthMode: "publickey",
}
data.DateStr = dataStr
data.AuthMode = parts[4+index]
data.User = parts[6+index]
data.Address = parts[8+index]
data.Port = parts[10+index]
data.Status = constant.StatusSuccess
return data
}
func loadFailedAuthDatas(line string) dto.SSHHistory {
var data dto.SSHHistory
parts := strings.Fields(line)
index, dataStr := analyzeDateStr(parts)
if dataStr == "" {
return data
// 查找关键字段位置
for i := timeIndex; i < len(parts); i++ {
switch parts[i] {
case "for":
if i+1 < len(parts) {
data.User = parts[i+1]
}
case "from":
if i+1 < len(parts) {
data.Address = parts[i+1]
}
case "port":
if i+1 < len(parts) {
data.Port = parts[i+1]
}
case "password":
data.AuthMode = "password"
}
}
data.DateStr = dataStr
switch index {
case 1:
data.User = parts[9]
case 2:
data.User = parts[10]
default:
data.User = parts[7]
}
data.AuthMode = parts[6+index]
data.Address = parts[9+index]
data.Port = parts[11+index]
data.Status = constant.StatusFailed
if strings.Contains(line, ": ") {
data.Message = strings.Split(line, ": ")[1]
}
return data
}
func loadFailedSecureDatas(line string) dto.SSHHistory {
var data dto.SSHHistory
parts := strings.Fields(line)
index, dataStr := analyzeDateStr(parts)
if dataStr == "" {
return data
}
data.DateStr = dataStr
if strings.Contains(line, " invalid ") {
data.AuthMode = parts[4+index]
index += 2
} else {
data.AuthMode = parts[4+index]
}
data.User = parts[6+index]
data.Address = parts[8+index]
data.Port = parts[10+index]
data.Status = constant.StatusFailed
if strings.Contains(line, ": ") {
data.Message = strings.Split(line, ": ")[1]
if strings.Contains(strings.Join(parts, " "), "invalid user") {
data.Message = "invalid user attempt"
}
return data
}
func parseConnectionClosedLog(parts []string, timeIndex int, dateStr string) dto.SSHHistory {
data := dto.SSHHistory{
DateStr: dateStr,
Status: constant.StatusFailed,
}
// 解析Alpine格式的特殊字段
fieldStart := timeIndex + 5 // 跳过时间、主机、进程字段
for i := fieldStart; i < len(parts); i++ {
switch {
case parts[i] == "user":
if i+1 < len(parts) {
data.User = parts[i+1]
}
case parts[i] == "port":
if i+1 < len(parts) {
data.Port = parts[i+1]
}
case isIPAddress(parts[i]):
data.Address = parts[i]
}
}
return data
}
func parseAcceptedLog(parts []string, timeIndex int, dateStr string) dto.SSHHistory {
data := dto.SSHHistory{
DateStr: dateStr,
Status: constant.StatusSuccess,
AuthMode: "password", // 默认值
}
// 处理不同日志格式
fieldStart := timeIndex + 5 // 基础字段偏移
for i := fieldStart; i < len(parts); i++ {
switch {
case parts[i] == "for":
if i+1 < len(parts) {
data.User = parts[i+1]
}
case parts[i] == "from":
if i+1 < len(parts) {
data.Address = parts[i+1]
}
case parts[i] == "port":
if i+1 < len(parts) {
data.Port = parts[i+1]
}
case strings.Contains(parts[i], "ssh2:"):
data.AuthMode = "publickey"
case parts[i] == "publickey":
data.AuthMode = "publickey"
case parts[i] == "password":
data.AuthMode = "password"
}
}
return data
}
func handleGunzip(path string) error {
if _, err := cmd.Execf("gunzip %s", path); err != nil {
return err
@@ -541,40 +597,36 @@ func handleGunzip(path string) error {
}
func loadServiceName() (string, error) {
if exist, _ := systemctl.IsExist("sshd"); exist {
return "sshd", nil
} else if exist, _ := systemctl.IsExist("ssh"); exist {
return "ssh", nil
}
return "", errors.New("The ssh or sshd service is unavailable")
}
func loadDate(currentYear int, DateStr string, nyc *time.Location) time.Time {
itemDate, err := time.ParseInLocation("2006 Jan 2 15:04:05", fmt.Sprintf("%d %s", currentYear, DateStr), nyc)
serviceName, err := systemctl.GetServiceName("ssh")
if err != nil {
itemDate, _ = time.ParseInLocation("2006 Jan 2 15:04:05", DateStr, nyc)
if errors.Is(err, systemctl.ErrServiceNotFound) {
return "", fmt.Errorf("SSH service unavailable. Please ensure OpenSSH server is installed and configured")
}
return "", fmt.Errorf("failed to load SSH service name: %w", err)
}
return itemDate
return serviceName, nil
}
func analyzeDateStr(parts []string) (int, string) {
t, err := time.Parse(time.RFC3339Nano, parts[0])
if err == nil {
if len(parts) < 12 {
return 0, ""
}
return 0, t.Format("2006 Jan 2 15:04:05")
}
t, err = time.Parse(constant.DateTimeLayout, fmt.Sprintf("%s %s", parts[0], parts[1]))
if err == nil {
if len(parts) < 14 {
return 0, ""
}
return 1, t.Format("2006 Jan 2 15:04:05")
func parseLogDate(currentYear int, dateStr string, loc *time.Location) time.Time {
// 处理带年份和不带年份的情况
formats := []string{
"2006 Jan 2 15:04:05",
"Jan 2 15:04:05",
}
if len(parts) < 14 {
return 0, ""
for _, format := range formats {
t, err := time.ParseInLocation(format, dateStr, loc)
if err == nil {
if t.Year() == 0 {
return time.Date(currentYear, t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), 0, loc)
}
return t
}
}
return 2, fmt.Sprintf("%s %s %s", parts[0], parts[1], parts[2])
return time.Now().In(loc)
}
func isIPAddress(s string) bool {
return net.ParseIP(s) != nil
}
+154 -80
View File
@@ -6,6 +6,7 @@ import (
"net/http"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"time"
@@ -17,6 +18,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/files"
httpUtil "github.com/1Panel-dev/1Panel/backend/utils/http"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
)
type UpgradeService struct{}
@@ -103,123 +105,159 @@ func (u *UpgradeService) Upgrade(req dto.Upgrade) error {
}
downloadPath := fmt.Sprintf("%s/%s/%s/release", global.CONF.System.RepoUrl, mode, req.Version)
fileName := fmt.Sprintf("1panel-%s-%s-%s.tar.gz", req.Version, "linux", itemArch)
_ = settingRepo.Update("SystemStatus", "Upgrading")
serviceHandle, _ := systemctl.DefaultHandler("1panel")
currentServiceName := serviceHandle.GetServiceName()
if err := settingRepo.Update("SystemStatus", "Upgrading"); err != nil {
return fmt.Errorf("update system status failed: %w", err)
}
go func() {
defer func() {
if err := settingRepo.Update("SystemStatus", "Free"); err != nil {
global.LOG.Errorf("Reset system status failed: %v", err)
}
}()
_ = global.Cron.Stop()
defer func() {
global.Cron.Start()
}()
if err := fileOp.DownloadFileWithProxy(downloadPath+"/"+fileName, rootDir+"/"+fileName); err != nil {
global.LOG.Errorf("download service file failed, err: %v", err)
_ = settingRepo.Update("SystemStatus", "Free")
defer global.Cron.Start()
if err := fileOp.DownloadFileWithProxy(
fmt.Sprintf("%s/%s", downloadPath, fileName),
path.Join(rootDir, fileName),
); err != nil {
global.LOG.Errorf("Failed to download upgrade package: %v", err)
return
}
global.LOG.Info("download all file successful!")
defer func() {
_ = os.Remove(rootDir)
}()
if err := handleUnTar(rootDir+"/"+fileName, rootDir, ""); err != nil {
global.LOG.Errorf("decompress file failed, err: %v", err)
_ = settingRepo.Update("SystemStatus", "Free")
defer os.RemoveAll(rootDir)
if err := handleUnTar(path.Join(rootDir, fileName), rootDir, ""); err != nil {
global.LOG.Errorf("Failed to extract package: %v", err)
return
}
tmpDir := rootDir + "/" + strings.ReplaceAll(fileName, ".tar.gz", "")
tmpDir := path.Join(rootDir, strings.TrimSuffix(fileName, ".tar.gz"))
if err := u.handleBackup(fileOp, originalDir); err != nil {
global.LOG.Errorf("handle backup original file failed, err: %v", err)
_ = settingRepo.Update("SystemStatus", "Free")
return
}
global.LOG.Info("backup original data successful, now start to upgrade!")
if err := common.CopyFile(path.Join(tmpDir, "1panel"), "/usr/local/bin"); err != nil {
global.LOG.Errorf("upgrade 1panel failed, err: %v", err)
u.handleRollback(originalDir, 1)
global.LOG.Errorf("Backup failed: %v", err)
return
}
if err := common.CopyFile(path.Join(tmpDir, "1pctl"), "/usr/local/bin"); err != nil {
global.LOG.Errorf("upgrade 1pctl failed, err: %v", err)
u.handleRollback(originalDir, 2)
return
}
_, _ = cmd.Execf("cp -r %s /usr/local/bin", path.Join(tmpDir, "lang"))
geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo")
_, _ = cmd.Execf("mkdir %s && cp %s %s/", geoPath, path.Join(tmpDir, "GeoIP.mmdb"), geoPath)
binDir := systemctl.BinaryPath
servicePath, _ := serviceHandle.GetServicePath()
geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo/GeoIP.mmdb")
if _, err := cmd.Execf("sed -i -e 's#BASE_DIR=.*#BASE_DIR=%s#g' /usr/local/bin/1pctl", global.CONF.System.BaseDir); err != nil {
global.LOG.Errorf("upgrade basedir in 1pctl failed, err: %v", err)
criticalUpdates := []struct {
src string
dest string
step int
}{
{path.Join(tmpDir, "1panel"), path.Join(binDir, "1panel"), 1},
{path.Join(tmpDir, "1pctl"), path.Join(binDir, "1pctl"), 2},
{selectInitScript(path.Join(tmpDir, "initscript"), currentServiceName), servicePath, 3},
}
for _, update := range criticalUpdates {
if err := common.Copy(update.src, update.dest); err != nil {
global.LOG.Errorf("Update %s failed: %v", path.Base(update.dest), err)
u.handleRollback(originalDir, update.step)
return
}
}
if _, err := cmd.Execf("sed -i -e 's#BASE_DIR=.*#BASE_DIR=%s#g' /usr/local/bin/1pctl",
global.CONF.System.BaseDir); err != nil {
global.LOG.Errorf("Update base directory failed: %v", err)
u.handleRollback(originalDir, 2)
return
}
if err := common.CopyFile(path.Join(tmpDir, "1panel.service"), "/etc/systemd/system"); err != nil {
global.LOG.Errorf("upgrade 1panel.service failed, err: %v", err)
u.handleRollback(originalDir, 3)
return
langDir := path.Join(binDir, "lang")
if err := common.Copy(path.Join(tmpDir, "lang"), langDir); err != nil {
global.LOG.Errorf("Update language files failed: %v", err)
}
if err := common.Copy(path.Join(tmpDir, "GeoIP.mmdb"), geoPath); err != nil {
global.LOG.Warnf("Update GeoIP database failed: %v", err)
}
global.LOG.Info("upgrade successful!")
go writeLogs(req.Version)
_ = settingRepo.Update("SystemVersion", req.Version)
_ = settingRepo.Update("SystemStatus", "Free")
checkPointOfWal()
_, _ = cmd.ExecWithTimeOut("systemctl daemon-reload && systemctl restart 1panel.service", 1*time.Minute)
if err := settingRepo.Update("SystemVersion", req.Version); err != nil {
global.LOG.Errorf("Update system version failed: %v", err)
}
if serviceHandle.ManagerName() == "systemd" {
_, _ = cmd.Exec("systemctl daemon-reload")
}
if err := systemctl.Restart("1panel"); err != nil {
global.LOG.Errorf("Service restart failed: %v", err)
return
}
}()
return nil
}
func (u *UpgradeService) handleBackup(fileOp files.FileOp, originalDir string) error {
if err := fileOp.Copy("/usr/local/bin/1panel", originalDir); err != nil {
return err
global.LOG.Info("Initiating backup procedure...")
h, _ := systemctl.DefaultHandler("1panel")
binDir := systemctl.BinaryPath
servicePath, _ := h.GetServicePath()
geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo/GeoIP.mmdb")
backupItems := []struct {
src string
dest string
}{
{path.Join(binDir, "1panel"), originalDir},
{path.Join(binDir, "1pctl"), originalDir},
{servicePath, originalDir},
{path.Join(binDir, "lang"), originalDir},
{geoPath, originalDir},
}
if err := fileOp.Copy("/usr/local/bin/1pctl", originalDir); err != nil {
return err
for _, item := range backupItems {
if err := fileOp.Copy(item.src, item.dest); err != nil {
return fmt.Errorf("backup %s failed: %w", path.Base(item.src), err)
}
}
if err := fileOp.Copy("/etc/systemd/system/1panel.service", originalDir); err != nil {
return err
}
_, _ = cmd.Execf("cp -r /usr/local/bin/lang %s", originalDir)
_, _ = cmd.Execf("cp %s %s", path.Join(global.CONF.System.BaseDir, "1panel/geo/GeoIP.mmdb"), originalDir)
checkPointOfWal()
if err := handleTar(path.Join(global.CONF.System.BaseDir, "1panel/db"), originalDir, "db.tar.gz", "db/1Panel.db-*", ""); err != nil {
return err
if err := handleTar(
path.Join(global.CONF.System.BaseDir, "1panel/db"),
originalDir,
"db.tar.gz",
"db/1Panel.db-*",
"",
); err != nil {
return fmt.Errorf("database backup failed: %w", err)
}
return nil
}
func (u *UpgradeService) handleRollback(originalDir string, errStep int) {
_ = settingRepo.Update("SystemStatus", "Free")
global.LOG.Info("Initiating rollback procedure...")
h, _ := systemctl.DefaultHandler("1panel")
binDir := systemctl.BinaryPath
servicePath, _ := h.GetServicePath()
geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo/GeoIP.mmdb")
checkPointOfWal()
if _, err := os.Stat(path.Join(originalDir, "1Panel.db")); err == nil {
if err := common.CopyFile(path.Join(originalDir, "1Panel.db"), global.CONF.System.DbPath); err != nil {
global.LOG.Errorf("rollback 1panel db failed, err: %v", err)
rollbackSteps := []struct {
src string
dest string
}{
{path.Join(originalDir, "1panel"), path.Join(binDir, "1panel")},
{path.Join(originalDir, "1pctl"), path.Join(binDir, "1pctl")},
{path.Join(originalDir, filepath.Base(servicePath)), servicePath},
{path.Join(originalDir, "lang"), path.Join(binDir, "lang")},
{path.Join(originalDir, "GeoIP.mmdb"), geoPath},
}
for _, step := range rollbackSteps[:errStep] {
if err := common.CopyFile(step.src, step.dest); err != nil {
global.LOG.Errorf("Rollback %s failed: %v", path.Base(step.src), err)
}
}
if _, err := os.Stat(path.Join(originalDir, "db.tar.gz")); err == nil {
if err := handleUnTar(path.Join(originalDir, "db.tar.gz"), global.CONF.System.DbPath, ""); err != nil {
global.LOG.Errorf("rollback 1panel db failed, err: %v", err)
}
}
if err := common.CopyFile(path.Join(originalDir, "1panel"), "/usr/local/bin"); err != nil {
global.LOG.Errorf("rollback 1pctl failed, err: %v", err)
}
if errStep == 1 {
return
}
if err := common.CopyFile(path.Join(originalDir, "1pctl"), "/usr/local/bin"); err != nil {
global.LOG.Errorf("rollback 1panel failed, err: %v", err)
}
_, _ = cmd.Execf("cp -r %s /usr/local/bin", path.Join(originalDir, "lang"))
geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo")
_, _ = cmd.Execf("mkdir %s && cp %s %s/", geoPath, path.Join(originalDir, "GeoIP.mmdb"), geoPath)
if errStep == 2 {
return
}
if err := common.CopyFile(path.Join(originalDir, "1panel.service"), "/etc/systemd/system"); err != nil {
global.LOG.Errorf("rollback 1panel failed, err: %v", err)
if err := systemctl.Restart("1panel"); err != nil {
global.LOG.Errorf("Service restart during rollback failed: %v", err)
}
}
@@ -357,5 +395,41 @@ func loadArch() (string, error) {
if strings.Contains(std, "s390x") {
return "s390x", nil
}
if strings.Contains(std, "riscv64") {
return "riscv64", nil
}
return "", fmt.Errorf("unsupported such arch: %s", std)
}
func selectInitScript(path string, serviceName string) string {
path = strings.TrimSuffix(path, "/")
mgr := systemctl.GetGlobalManager().Name()
var serviceFileName string
switch mgr {
case "systemd":
serviceFileName = "1panel.service"
case "openrc":
serviceFileName = "1paneld.openrc"
case "sysvinit":
isWrt := systemctl.FileExist("/etc/rc.common")
if isWrt {
serviceFileName = "1paneld.procd"
} else {
serviceFileName = "1paneld.init"
}
default:
serviceFileName = serviceName
global.LOG.Warnf("[%s]unselect InitScript, used default: %s", mgr, serviceName)
}
sourcePath := filepath.Join(path, serviceFileName)
targetPath := filepath.Join(path, serviceName)
if serviceFileName != serviceName {
if _, err := cmd.Execf("cp %s %s", sourcePath, targetPath); err != nil {
global.LOG.Errorf("Failed to copy init script from %s to %s: %v",
serviceFileName, serviceName, err)
}
}
return targetPath
}
+22 -5
View File
@@ -427,7 +427,9 @@ func (w WebsiteService) UpdateWebsite(req request.WebsiteUpdate) error {
}
}
website.PrimaryDomain = req.PrimaryDomain
website.WebsiteGroupID = req.WebsiteGroupID
if req.WebsiteGroupID > 0 {
website.WebsiteGroupID = req.WebsiteGroupID
}
website.Remark = req.Remark
website.IPV6 = req.IPV6
@@ -496,6 +498,10 @@ func (w WebsiteService) DeleteWebsite(req request.WebsiteDelete) error {
if err := websiteDomainRepo.DeleteBy(ctx, websiteDomainRepo.WithWebsiteId(req.ID)); err != nil {
return err
}
websiteID := GetWebsiteID()
if req.ID == websiteID {
_ = settingRepo.Update("MCP_WEBSITE_ID", "0")
}
tx.Commit()
uploadDir := path.Join(global.CONF.System.BaseDir, fmt.Sprintf("1panel/uploads/website/%s", website.Alias))
@@ -1764,11 +1770,22 @@ func (w WebsiteService) GetProxies(id uint) (res []request.WebsiteProxyConfig, e
}
directives := config.GetDirectives()
location, ok := directives[0].(*components.Location)
if !ok {
err = errors.New("error")
return
var (
location *components.Location
ok bool
)
for _, directive := range directives {
if directive.GetName() == "location" {
location, ok = directive.(*components.Location)
if ok {
break
}
}
}
if location == nil {
return nil, buserr.New("ErrConfigParse")
}
proxyConfig.ProxyPass = location.ProxyPass
proxyConfig.Cache = location.Cache
if location.CacheTime > 0 {
+1
View File
@@ -23,6 +23,7 @@ type System struct {
Language string `mapstructure:"language"`
IsDemo bool `mapstructure:"is_demo"`
IsIntl bool `mapstructure:"is_intl"`
LicenseVerify string `mapstructure:"license_verify"`
AppRepo string `mapstructure:"app_repo"`
ChangeUserInfo string `mapstructure:"change_user_info"`
OneDriveID string `mapstructure:"one_drive_id"`
+1
View File
@@ -35,6 +35,7 @@ var WebUrlMap = map[string]struct{}{
"/ai": {},
"/ai/model": {},
"/ai/gpu": {},
"/ai/mcp": {},
"/containers": {},
"/containers/container": {},
+1
View File
@@ -17,4 +17,5 @@ var (
RuntimeDir = path.Join(DataDir, "runtime")
RecycleBinDir = "/.1panel_clash"
SSLLogDir = path.Join(global.CONF.System.DataDir, "log", "ssl")
McpDir = path.Join(DataDir, "mcp")
)
+1
View File
@@ -13,6 +13,7 @@ const (
RuntimeStopped = "stopped"
RuntimeUnhealthy = "unhealthy"
RuntimeCreating = "creating"
RuntimeReStarting = "restarting"
RuntimePHP = "php"
RuntimeNode = "node"
+2
View File
@@ -287,6 +287,8 @@ SystemMode: "mode: "
#ai-tool
ErrOpenrestyInstall: 'Please install Openresty first'
ErrSSL: "Certificate content is empty, please check the certificate!"
ErrSsePath: "SSE path is duplicated"
#mobile app
ErrVerifyToken: 'Token verification error, please reset and scan again.'
-63
View File
@@ -1,63 +0,0 @@
#cmd
AppVersion: "Lotfan noskhe-ye barname ra vared konid"
AppCommands: "Dasturat-e marbut be barname"
AppInit: "Barname ra shoru' konid"
AppKeyVal: "Kelid-e barname (faghat az zabane englisi poshtibani mikonad)"
AppCreateFileErr: "Khalgh-e file {{ .name }} namovaffagh bud {{ .err }}"
AppCreateDirErr: "Khalgh-e folder {{ .name }} namovaffagh bud {{ .err }}"
AppMissKey: "Kelid-e barname mojood nist, az -k estefade konid"
AppMissVersion: "Noskhe-ye barname mojood nist, az -v estefade konid"
AppVersionExist: "Noskhe ghablan mojood ast!"
AppCreateSuccessful: "Khalgh ba movaffaghiat anjam shod!"
AppWriteErr: "Neveshtan file {{ .name }} namovaffagh bud {{ .err }}"
SudoHelper: "Lotfan in eskript ra be onvane karbare root ya ba mojavez sudo ejra konid"
ListenIPCommands: "IP goosh dadan ra taghir dahid"
ListenIPv4: "Goosh dadan bar IPv4"
ListenIPv6: "Goosh dadan bar IPv6"
ListenChangeSuccessful: "Taghir movaffagh! Hala bar {{ .value }} goosh midahad"
ResetCommands: "E'ade setadad etela'at-e system"
ResetMFA: "Fa'al-sazi do marhale'i 1Panel ra laghv konid"
ResetHttps: "Voroud HTTPS baraye 1Panel ra laghv konid"
ResetEntrance: "Voroud amin 1Panel ra laghv konid"
ResetIPs: "Mahdudiyat IP-haye mojaz 1Panel ra laghv konid"
ResetDomain: "Sazgari domain 1Panel ra laghv konid"
RestoreCommands: "Bargasht be khadamate 1Panel va data"
RestoreNoSuchFile: "Hich file mojood baraye bargasht nist"
RestoreStep1: "(1/5) Shoru' bargasht be khadamate 1Panel va data az directory {{ .name }} ..."
RestoreStep2: "(2/5) Bargasht be binary 1Panel ba movaffaghiat anjam shod"
RestoreStep3: "(3/5) Bargasht be script 1Panel ba movaffaghiat anjam shod"
RestoreStep4: "(4/5) Bargasht be khadamat 1Panel ba movaffaghiat anjam shod"
RestoreStep5: "(5/5) Bargasht be data 1Panel ba movaffaghiat anjam shod"
RestoreSuccessful: "Bargasht ba movaffaghiat anjam shod! Khadamat dar hale restart ast, lotfan montazer bashid..."
UpdateCommands: "Be'rooz resani etela'at-e panel"
UpdateUser: "Be'rooz resani karbare panel"
UpdatePassword: "Be'rooz resani ramz-e panel"
UpdatePort: "Be'rooz resani port-e panel"
UpdateUserNull: "Khatta: karbare panel khalist!"
UpdateUserBlank: "Khatta: karbare panel shamel faseleh ast!"
UpdateUserFormat: "Khatta: format-e karbare panel na dorost ast! Faghat az englisi, chine'i, adad va _, ba tool-e 3-30 poshtibani mikonad"
UpdateUserErr: "Khatta: be'rooz resani karbare panel namovaffagh bud, {{ .err }}"
UpdateSuccessful: "Be'rooz resani ba movaffaghiat anjam shod!"
UpdateUserResult: "Karbare panel: {{ .name }}"
UpdatePasswordRead: "Khatta: khatayi dar khandan etela'at-e ramz-e panel, {{ .err }}"
UpdatePasswordNull: "Khatta: ramz-e panel khalist!"
UpdateUPasswordBlank: "Khatta: ramz-e panel shamel faseleh ast!"
UpdatePasswordFormat: "Khatta: ramz-e panel faghat az harf, adad, karakter-haye vizhe !@#$%*_,.? ba tool-e 8-30 poshtibani mikonad!"
UpdatePasswordLen: "Khatta: lotfan ramzi bishtar az 6 karakter vared konid!"
UpdatePasswordRe: "Teyid-e ramz:"
UpdatePasswordErr: "Khatta: be'rooz resani ramz-e panel namovaffagh bud, {{ .err }}"
UpdatePasswordSame: "Khatta: do ramz ba ham yeksan nistand, lotfan barrasi konid va dobare talash konid!"
UpdatePasswordResult: "Ramz-e panel: {{ .name }}"
UpdatePortFormat: "Khatta: shomareye port bayad bein 1 ta 65535 bashad!"
UpdatePortUsed: "Khatta: shomareye port ghablan estefade shode ast, lotfan barrasi konid va dobare talash konid!"
UpdatePortErr: "Khatta: be'rooz resani port-e panel namovaffagh bud, {{ .err }}"
UpdatePortResult: "Port-e panel: {{ .name }}"
UpdatePortFirewallAdd: "Ezafe kardane ghanoun-e port firewall namovaffagh bud, {{ .err }}, lotfan port {{ .name }} ra be dast az qavaneen firewall ezafe konid."
UpdatePortFirewallDel: "Khatta: hazf-e port firewall namovaffagh bud, {{ .err }}"
UpdatePortFirewallReload: "Reload firewall namovaffagh bud, {{ .err }}, lotfan firewall ra be dast reload konid."
UserInfo: "Daryaft etela'at-e panel"
UserInfoAddr: "Address-e panel: "
UserInfoPassHelp: "Nokte: baraye taghiri ramz, mitavanid dastur zir ra anjam dahid: "
DBConnErr: "Khatta: etesal be database namovaffagh bud, {{ .err }}"
SystemVersion: "noskhe: "
SystemMode: "halat: "
+42 -39
View File
@@ -1,17 +1,17 @@
ErrInvalidParams: "リクエストパラメーターエラー:{{.Detail}}"
ErrTokenParse: "トークン生成エラー:{{.Detail}}"
ErrInvalidParams: "リクエストパラメーターエラー: {{.Detail}}"
ErrTokenParse: "トークン生成エラー: {{.Detail}}"
ErrInitialPassword: "初期パスワードエラー"
ErrInternalServer: "サービス内部エラー:{{.Detail}}"
ErrInternalServer: "サービス内部エラー: {{.Detail}}"
ErrRecordExist: "レコードはすでに存在します"
ErrRecordNotFound: "記録が見つかりません"
ErrStructTransform: "タイプ変換障害:{{.Detail}}"
ErrNotLogin: "ユーザーはログインしません:{{.Detail}}"
ErrPasswordExpired: "現在のパスワードが期限切れになっています:{{.Detail}}"
ErrNotSupportType: "システムは現在のタイプをサポートしていません:{{.Detail}}"
ErrApiConfigStatusInvalid: "APIインターフェイスアクセス禁止:{{.Detail}}"
ErrApiConfigKeyInvalid: "APIインターフェイスキーエラー:{{.Detail}}"
ErrApiConfigIPInvalid: "APIインターフェイスIPはホワイトリストにありません:{{.Detail}}"
ErrApiConfigDisable: "このインターフェイスは、APIインターフェイスコールの使用を禁止しています:{{.Detail}}"
ErrStructTransform: "タイプ変換障害: {{.Detail}}"
ErrNotLogin: "ユーザーはログインしません: {{.Detail}}"
ErrPasswordExpired: "現在のパスワードが期限切れになっています: {{.Detail}}"
ErrNotSupportType: "システムは現在のタイプをサポートしていません: {{.Detail}}"
ErrApiConfigStatusInvalid: "APIインターフェイスアクセス禁止: {{.Detail}}"
ErrApiConfigKeyInvalid: "APIインターフェイスキーエラー: {{.Detail}}"
ErrApiConfigIPInvalid: "APIインターフェイスIPはホワイトリストにありません: {{.Detail}}"
ErrApiConfigDisable: "このインターフェイスは、APIインターフェイスコールの使用を禁止しています: {{.Detail}}"
#common
ErrNameIsExist: "名前はすでに存在しています"
@@ -92,10 +92,10 @@ ErrDomainIsExist: "ドメインはすでに存在しています"
ErrAliasIsExist: "エイリアスはすでに存在しています"
ErrAppDelete: '他のウェブサイトはこのアプリを使用します'
ErrGroupIsUsed: "グループは使用されており、削除することはできません"
ErrBackupMatch: "バックアップファイルは、ウェブサイトの現在の部分的なデータと一致しません:{{.Detail}}"
ErrBackupMatch: "バックアップファイルは、ウェブサイトの現在の部分的なデータと一致しません: {{.Detail}}"
ErrBackupExist: 'バックアップファイルは、存在しない元のデータの一部に対応しています。{{.Detail}}'
ErrPHPResource: 'ローカルランタイムはスイッチングをサポートしません!'
ErrPathPermission: 'インデックスディレクトリで1000:1000以外のアクセス許可を持つフォルダーが検出されたため、Webサイトにアクセスするとアクセスが拒否された場合があります。上の[保存]ボタンをクリックしてください'
ErrPathPermission: 'インデックスディレクトリで1000: 1000以外のアクセス許可を持つフォルダーが検出されたため、Webサイトにアクセスするとアクセスが拒否された場合があります。上の[保存]ボタンをクリックしてください'
ErrDomainIsUsed: "ドメインはすでにWebサイトで使用されています{{.Name}}"
ErrDomainFormat: "{{.name}}ドメイン形式エラー"
ErrDefaultAlias: "デフォルトは予約済みのコード名です。別のコード名を使用してください"
@@ -124,7 +124,7 @@ ErrDeleteCAWithSSL: "現在の組織の下に発行された証明書があり
ErrDeleteWithPanelSSL: "パネルSSL構成はこの証明書を使用しており、削除できません"
ErrDefaultCA: "デフォルトの証明書当局は削除できません"
ApplyWebSiteSSLLog: "{{.name}} Webサイト証明書の更新を開始します"
ErrUpdateWebsiteSSL: "{{.name}}ウェブサイトは証明書の更新に失敗しました:{{.err}}"
ErrUpdateWebsiteSSL: "{{.name}}ウェブサイトは証明書の更新に失敗しました: {{.err}}"
ApplyWebSiteSSLSuccess: "ウェブサイトの証明書を正常に更新します"
ErrExecShell: "スクリプトの実行に失敗した{{.err}}"
ExecShellStart: "スクリプトの実行を開始します"
@@ -146,7 +146,7 @@ ErrTypeOfRedis: "回復ファイルのタイプは、現在の永続性モード
ErrInUsed: "{{.Detail}}が使用されており、削除できません"
ErrObjectInUsed: "このオブジェクトは使用されており、削除することはできません"
ErrPortRules: "ポートの数が一致しません。再入力してください!"
ErrPgImagePull: "画像プルタイムアウト。画像の加速を構成するか、Postgres:16.0-Alpine画像を手動で引いて、再試行してください"
ErrPgImagePull: "画像プルタイムアウト。画像の加速を構成するか、Postgres: 16.0-Alpine画像を手動で引いて、再試行してください"
#runtime
ErrDirNotFound: "ビルドフォルダーは存在しません!ファイルの整合性を確認してください!"
@@ -253,37 +253,40 @@ UpdateCommands: "パネル情報を更新します"
UpdateUser: "パネルユーザーを更新します"
UpdatePassword: "パスワードを更新します"
UpdatePort: "パネルポートを更新します"
UpdateUserNull: "エラー:パネルユーザーは空です!"
UpdateUserBlank: "エラー:パネルユーザーにはスペースが含まれています!"
UpdateUserFormat: "エラー:無効なパネルユーザー形式!英語、中国語、数字、_、長さ3〜30のみをサポートしています"
UpdateUserErr: "エラー:パネルユーザーの更新に失敗しました{{.err}}"
UpdateUserNull: "エラー: パネルユーザーは空です!"
UpdateUserBlank: "エラー: パネルユーザーにはスペースが含まれています!"
UpdateUserFormat: "エラー: 無効なパネルユーザー形式!英語、中国語、数字、_、長さ3〜30のみをサポートしています"
UpdateUserErr: "エラー: パネルユーザーの更新に失敗しました{{.err}}"
UpdateSuccessful: "更新成功!"
UpdateUserResult: "パネルユーザー:{{.name}}"
UpdatePasswordRead: "エラー:パネルパスワード情報の読み取りに失敗しました、{{.err}}"
UpdatePasswordNull: "エラー:パネルパスワードは空です!"
UpdateUPasswordBlank: "エラー:パスワードにはスペースが含まれています!"
UpdatePasswordFormat: "エラー:パネルパスワードは、文字、数字、特殊文字のみをサポートします!@#$%*_、。、、長さ8-30!"
UpdatePasswordLen: "エラー:6文字より長くパスワードを入力してください!"
UpdatePasswordRe: "パスワードを認証する:"
UpdatePasswordErr: "エラー:{{.err}}のパスワードの更新に失敗しました"
UpdatePasswordSame: "エラー:2つのパスワードが一致しません。チェックして再試行してください!"
UpdatePasswordResult: "パネルパスワード:{{.name}}"
UpdatePortFormat: "エラー:入力ポート番号は1〜65535でなければなりません!"
UpdatePortUsed: "エラー:ポート番号はすでに使用されています。チェックして再試行してください!"
UpdatePortErr: "エラー:パネルポートの更新に失敗しました{{.err}}"
UpdatePortResult: "パネルポート:{{.name}}"
UpdateUserResult: "パネルユーザー: {{.name}}"
UpdatePasswordRead: "エラー: パネルパスワード情報の読み取りに失敗しました、{{.err}}"
UpdatePasswordNull: "エラー: パネルパスワードは空です!"
UpdateUPasswordBlank: "エラー: パスワードにはスペースが含まれています!"
UpdatePasswordFormat: "エラー: パネルパスワードは、文字、数字、特殊文字のみをサポートします!@#$%*_、。、、長さ8-30!"
UpdatePasswordLen: "エラー: 6文字より長くパスワードを入力してください!"
UpdatePasswordRe: "パスワードを認証する: "
UpdatePasswordErr: "エラー: {{.err}}のパスワードの更新に失敗しました"
UpdatePasswordSame: "エラー: 2つのパスワードが一致しません。チェックして再試行してください!"
UpdatePasswordResult: "パネルパスワード: {{.name}}"
UpdatePortFormat: "エラー: 入力ポート番号は1〜65535でなければなりません!"
UpdatePortUsed: "エラー: ポート番号はすでに使用されています。チェックして再試行してください!"
UpdatePortErr: "エラー: パネルポートの更新に失敗しました{{.err}}"
UpdatePortResult: "パネルポート: {{.name}}"
UpdatePortFirewallAdd: "ファイアウォールポートルールの追加に失敗した{{.err}}、ファイアウォールルールに{{.name}}ポートを手動で追加してください。"
UpdatePortFirewallDel: "エラー:ファイアウォールポートの削除に失敗しました、{{.err}}"
UpdatePortFirewallDel: "エラー: ファイアウォールポートの削除に失敗しました、{{.err}}"
UpdatePortFirewallReload: "ファイアウォールのリロードに失敗しました{{.err}}、ファイアウォールを手動でリロードしてください。"
UserInfo: "パネル情報を取得します"
UserInfoAddr: "パネルアドレス:"
UserInfoPassHelp: "ヒント:パスワードを変更するには、コマンドを実行できます。"
DBConnErr: "エラー:データベース接続の初期化に失敗しました、{{.err}}"
SystemVersion: "バージョン:"
SystemMode: "モード:"
UserInfoAddr: "パネルアドレス: "
UserInfoPassHelp: "ヒント: パスワードを変更するには、コマンドを実行できます。"
DBConnErr: "エラー: データベース接続の初期化に失敗しました、{{.err}}"
SystemVersion: "バージョン: "
SystemMode: "モード: "
#ai-tool
ErrOpenrestyInstall: 'まず Openresty をインストールしてください'
ErrSSL: "証明書の内容が空です。証明書を確認してください!"
ErrSsePath: "SSE パスが重複しています"
#mobile app
ErrVerifyToken: 'トークンの検証エラーです。リセット後、再度QRコードをスキャンしてください。'
+3
View File
@@ -287,6 +287,9 @@ SystemMode: "모드: "
#ai-tool
ErrOpenrestyInstall: '먼저 Openresty를 설치하세요'
ErrSSL: "인증서 내용이 비어 있습니다. 인증서를 확인하세요!"
ErrSsePath: "SSE 경로가 중복되었습니다"
#mobile app
ErrVerifyToken: '토큰 검증 오류가 발생했습니다. 재설정 후 다시 스캔해 주세요.'
+2
View File
@@ -286,6 +286,8 @@ SystemMode: "Mod: "
#ai-tool
ErrOpenrestyInstall: 'Sila pasang Openresty terlebih dahulu'
ErrSSL: "Kandungan sijil kosong, sila periksa sijil!"
ErrSsePath: "Laluan SSE bertindan"
#mobile app
ErrVerifyToken: 'Ralat pengesahan token, sila tetapkan semula dan imbas semula.'
+2
View File
@@ -284,6 +284,8 @@ SystemMode: "modo: "
#ai-tool
ErrOpenrestyInstall: 'Por favor, instale o Openresty primeiro'
ErrSSL: "O conteúdo do certificado está vazio, por favor, verifique o certificado!"
ErrSsePath: "Caminho SSE duplicado"
#mobile app
ErrVerifyToken: 'Erro de verificação do token, por favor, reinicie e escaneie novamente.'
+3 -1
View File
@@ -286,7 +286,9 @@ SystemVersion: "версия: "
SystemMode: "режим: "
#ai-tool
"ErrOpenrestyInstall": "Пожалуйста, установите Openresty сначала"
ErrOpenrestyInstall: "Пожалуйста, установите Openresty сначала"
ErrSSL: "Содержимое сертификата пустое, пожалуйста, проверьте сертификат!"
ErrSsePath: "Путь SSE дублируется"
#mobile app
ErrVerifyToken: 'шибка проверки токена, пожалуйста, сбросьте и отсканируйте снова.'
+29 -27
View File
@@ -8,7 +8,7 @@ ErrStructTransform: "類型轉換失敗: {{ .detail }}"
ErrNotLogin: "用戶未登入: {{ .detail }}"
ErrPasswordExpired: "目前密碼已過期: {{ .detail }}"
ErrNotSupportType: "系統暫不支援目前類型: {{ .detail }}"
ErrApiConfigStatusInvalid: "API 介面禁止訪問: {{ .detail }}"
ErrApiConfigStatusInvalid: "API 介面禁止瀏覽: {{ .detail }}"
ErrApiConfigKeyInvalid: "API 介面金鑰錯誤: {{ .detail }}"
ErrApiConfigIPInvalid: "呼叫 API 介面 IP 不在白名單: {{ .detail }}"
ErrApiConfigDisable: "此介面禁止使用 API 介面呼叫: {{ .detail }}"
@@ -243,7 +243,7 @@ ResetMFA: "取消 1Panel 兩步驗證"
ResetHttps: "取消 1Panel https 方式登入"
ResetEntrance: "取消 1Panel 安全入口"
ResetIPs: "取消 1Panel 授權 IP 限制"
ResetDomain: "取消 1Panel 訪問域名綁定"
ResetDomain: "取消 1Panel 瀏覽域名綁定"
RestoreCommands: "轉返 1Panel 服務及資料"
RestoreNoSuchFile: "暫無可轉返文件"
RestoreStep1: "(1/5)開始從 {{ .name }} 目錄轉返 1Panel 服務及資料... "
@@ -256,37 +256,39 @@ UpdateCommands: "修改面板資訊"
UpdateUser: "修改面板用戶"
UpdatePassword: "修改面板密碼"
UpdatePort: "修改面板埠"
UpdateUserNull: "錯誤:輸入面板用戶為空!"
UpdateUserBlank: "錯誤:輸入面板用戶中包含空格字元!"
UpdateUserFormat: "錯誤:輸入面板用戶錯誤!僅支援英文、中文、數字和_,長度3-30"
UpdateUserErr: "錯誤:面板用戶修改失敗,{{ .err }}"
UpdateUserNull: "錯誤: 輸入面板用戶為空!"
UpdateUserBlank: "錯誤: 輸入面板用戶中包含空格字元!"
UpdateUserFormat: "錯誤: 輸入面板用戶錯誤!僅支援英文、中文、數字和_,長度3-30"
UpdateUserErr: "錯誤: 面板用戶修改失敗,{{ .err }}"
UpdateSuccessful: "修改成功!"
UpdateUserResult: "面板用戶:{{ .name }}"
UpdatePasswordRead: "錯誤:面板密碼資訊讀取錯誤,{{ .err }}"
UpdatePasswordNull: "錯誤:輸入面板密碼為空!"
UpdateUPasswordBlank: "錯誤:輸入面板密碼中包含空格字元!"
UpdatePasswordFormat: "錯誤:面板密碼僅支援字母、數字、特殊字元(!@#$%*_,.?),長度 8-30 位!"
UpdatePasswordLen: "錯誤:請輸入 6 位以上密碼!"
UpdatePasswordRe: "確認密碼:"
UpdatePasswordErr: "錯誤:面板密碼修改失敗,{{ .err }}"
UpdatePasswordSame: "錯誤:兩次密碼不匹配,請檢查後重試!"
UpdatePasswordResult: "面板密碼:{{ .name }}"
UpdatePortFormat: "錯誤:輸入的埠號必須在 1 到 65535 之間!"
UpdatePortUsed: "錯誤:該埠號正被佔用,請檢查後重試!"
UpdatePortErr: "錯誤:面板埠修改失敗,{{ .err }}"
UpdatePortResult: "面板埠:{{ .name }}"
UpdateUserResult: "面板用戶: {{ .name }}"
UpdatePasswordRead: "錯誤: 面板密碼資訊讀取錯誤,{{ .err }}"
UpdatePasswordNull: "錯誤: 輸入面板密碼為空!"
UpdateUPasswordBlank: "錯誤: 輸入面板密碼中包含空格字元!"
UpdatePasswordFormat: "錯誤: 面板密碼僅支援字母、數字、特殊字元(!@#$%*_,.?),長度 8-30 位!"
UpdatePasswordLen: "錯誤: 請輸入 6 位以上密碼!"
UpdatePasswordRe: "確認密碼: "
UpdatePasswordErr: "錯誤: 面板密碼修改失敗,{{ .err }}"
UpdatePasswordSame: "錯誤: 兩次密碼不匹配,請檢查後重試!"
UpdatePasswordResult: "面板密碼: {{ .name }}"
UpdatePortFormat: "錯誤: 輸入的埠號必須在 1 到 65535 之間!"
UpdatePortUsed: "錯誤: 該埠號正被佔用,請檢查後重試!"
UpdatePortErr: "錯誤: 面板埠修改失敗,{{ .err }}"
UpdatePortResult: "面板埠: {{ .name }}"
UpdatePortFirewallAdd: "新增防火牆埠規則失敗,{{ .err }},請您手動將 {{ .name }} 埠新增至防火牆規則中。"
UpdatePortFirewallDel: "錯誤:防火牆埠刪除失敗,{{ .err }}"
UpdatePortFirewallDel: "錯誤: 防火牆埠刪除失敗,{{ .err }}"
UpdatePortFirewallReload: "防火牆重載失敗,{{ .err }},請您手動重載防火牆。"
UserInfo: "取得面板資訊"
UserInfoAddr: "面板地址:"
UserInfoPassHelp: "提示:修改密碼可執行指令:"
DBConnErr: "錯誤:初始化資料庫連線失敗,{{ .err }}"
SystemVersion: "版本:"
SystemMode: "模式:"
UserInfoAddr: "面板地址: "
UserInfoPassHelp: "提示: 修改密碼可執行指令: "
DBConnErr: "錯誤: 初始化資料庫連線失敗,{{ .err }}"
SystemVersion: "版本: "
SystemMode: "模式: "
#ai-tool
"ErrOpenrestyInstall": "請先安裝 Openresty"
ErrOpenrestyInstall: "請先安裝 Openresty"
ErrSSL: "證書內容為空,請檢查證書!"
ErrSsePath: "SSE 路徑重複"
#mobile app
ErrVerifyToken: '令牌驗證錯誤,請重置後再次掃碼'
+26 -24
View File
@@ -256,37 +256,39 @@ UpdateCommands: "修改面板信息"
UpdateUser: "修改面板用户"
UpdatePassword: "修改面板密码"
UpdatePort: "修改面板端口"
UpdateUserNull: "错误:输入面板用户为空!"
UpdateUserBlank: "错误:输入面板用户中包含空格字符!"
UpdateUserFormat: "错误:输入面板用户错误!仅支持英文、中文、数字和_,长度3-30"
UpdateUserErr: "错误:面板用户修改失败,{{ .err }}"
UpdateUserNull: "错误: 输入面板用户为空!"
UpdateUserBlank: "错误: 输入面板用户中包含空格字符!"
UpdateUserFormat: "错误: 输入面板用户错误!仅支持英文、中文、数字和_,长度3-30"
UpdateUserErr: "错误: 面板用户修改失败,{{ .err }}"
UpdateSuccessful: "修改成功!"
UpdateUserResult: "面板用户:{{ .name }}"
UpdatePasswordRead: "错误:面板密码信息读取错误,{{ .err }}"
UpdatePasswordNull: "错误:输入面板密码为空!"
UpdateUPasswordBlank: "错误:输入面板密码中包含空格字符!"
UpdatePasswordFormat: "错误:面板密码仅支持字母、数字、特殊字符(!@#$%*_,.?),长度 8-30 位!"
UpdatePasswordLen: "错误:请输入 6 位以上密码!"
UpdatePasswordRe: "确认密码:"
UpdatePasswordErr: "错误:面板密码修改失败,{{ .err }}"
UpdatePasswordSame: "错误:两次密码不匹配,请检查后重试!"
UpdatePasswordResult: "面板密码:{{ .name }}"
UpdatePortFormat: "错误:输入的端口号必须在 1 到 65535 之间!"
UpdatePortUsed: "错误:该端口号正被占用,请检查后重试!"
UpdatePortErr: "错误:面板端口修改失败,{{ .err }}"
UpdatePortResult: "面板端口:{{ .name }}"
UpdateUserResult: "面板用户: {{ .name }}"
UpdatePasswordRead: "错误: 面板密码信息读取错误,{{ .err }}"
UpdatePasswordNull: "错误: 输入面板密码为空!"
UpdateUPasswordBlank: "错误: 输入面板密码中包含空格字符!"
UpdatePasswordFormat: "错误: 面板密码仅支持字母、数字、特殊字符(!@#$%*_,.?),长度 8-30 位!"
UpdatePasswordLen: "错误: 请输入 6 位以上密码!"
UpdatePasswordRe: "确认密码: "
UpdatePasswordErr: "错误: 面板密码修改失败,{{ .err }}"
UpdatePasswordSame: "错误: 两次密码不匹配,请检查后重试!"
UpdatePasswordResult: "面板密码: {{ .name }}"
UpdatePortFormat: "错误: 输入的端口号必须在 1 到 65535 之间!"
UpdatePortUsed: "错误: 该端口号正被占用,请检查后重试!"
UpdatePortErr: "错误: 面板端口修改失败,{{ .err }}"
UpdatePortResult: "面板端口: {{ .name }}"
UpdatePortFirewallAdd: "添加防火墙端口规则失败,{{ .err }},请您手动将 {{ .name }} 端口添加至防火墙规则中。"
UpdatePortFirewallDel: "错误:防火墙端口删除失败,{{ .err }}"
UpdatePortFirewallDel: "错误: 防火墙端口删除失败,{{ .err }}"
UpdatePortFirewallReload: "防火墙重载失败,{{ .err }},请您手动重载防火墙。"
UserInfo: "获取面板信息"
UserInfoAddr: "面板地址:"
UserInfoPassHelp: "提示:修改密码可执行命令:"
DBConnErr: "错误:初始化数据库连接失败,{{ .err }}"
SystemVersion: "版本:"
SystemMode: "模式:"
UserInfoAddr: "面板地址: "
UserInfoPassHelp: "提示: 修改密码可执行命令: "
DBConnErr: "错误: 初始化数据库连接失败,{{ .err }}"
SystemVersion: "版本: "
SystemMode: "模式: "
#ai-tool
ErrOpenrestyInstall: '请先安装 Openresty'
ErrSSL: "证书内容为空,请检查证书!"
ErrSsePath: "SSE 路径重复"
#mobile app
ErrVerifyToken: '令牌验证错误,请重置后再次扫码'
+4 -1
View File
@@ -23,8 +23,11 @@ func Init() {
constant.SSLLogDir = path.Join(global.CONF.System.DataDir, "log", "ssl")
constant.McpDir = path.Join(constant.DataDir, "mcp")
dirs := []string{constant.DataDir, constant.ResourceDir, constant.AppResourceDir, constant.AppInstallDir,
global.CONF.System.Backup, constant.RuntimeDir, constant.LocalAppResourceDir, constant.RemoteAppResourceDir, constant.SSLLogDir}
global.CONF.System.Backup, constant.RuntimeDir, constant.LocalAppResourceDir, constant.RemoteAppResourceDir,
constant.SSLLogDir, constant.McpDir}
fileOp := files.NewFileOp()
for _, dir := range dirs {
+15
View File
@@ -84,6 +84,15 @@ func Init() {
global.CONF.System.ApiKeyValidityTime = apiKeyValidityTimeSetting.Value
}
if global.CONF.System.LicenseVerify == "" {
licenseVerify, err := settingRepo.Get(settingRepo.WithByKey("LicenseVerify"))
if err != nil {
global.LOG.Errorf("load service license verify from setting failed, err: %v", err)
}
global.CONF.System.LicenseVerify = licenseVerify.Value
}
handleLicenseVerify(global.CONF.System.LicenseVerify, settingRepo)
handleUserInfo(global.CONF.System.ChangeUserInfo, settingRepo)
handleCronjobStatus()
@@ -207,6 +216,12 @@ func loadLocalDir() {
global.LOG.Errorf("error type dir: %T", varMap["dir"])
}
func handleLicenseVerify(licenseVerify string, settingRepo repo.ISettingRepo) {
if err := settingRepo.Update("LicenseVerify", licenseVerify); err != nil {
global.LOG.Fatalf("init license verify before start failed, err: %v", err)
}
}
func handleUserInfo(tags string, settingRepo repo.ISettingRepo) {
if len(tags) == 0 {
return
+6
View File
@@ -105,6 +105,12 @@ func Init() {
migrations.AddOllamaModel,
migrations.AddAppMenu,
migrations.AddAppPanelName,
migrations.AddLicenseVerify,
migrations.AddMcpServer,
migrations.AddPbootCMSPHPExtensions,
migrations.DeleteV2Openresty,
migrations.UpdateOnedrive,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)
@@ -448,3 +448,64 @@ var AddAppPanelName = &gormigrate.Migration{
return nil
},
}
var AddLicenseVerify = &gormigrate.Migration{
ID: "20250226-add-license-verify",
Migrate: func(tx *gorm.DB) error {
if err := tx.Create(&model.Setting{Key: "LicenseVerify", Value: "LX"}).Error; err != nil {
return err
}
return nil
},
}
var AddMcpServer = &gormigrate.Migration{
ID: "20250401-add-mcp-server",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.McpServer{}); err != nil {
return err
}
return nil
},
}
var AddPbootCMSPHPExtensions = &gormigrate.Migration{
ID: "20250625-add-php-extensions",
Migrate: func(tx *gorm.DB) error {
item := &model.PHPExtensions{
Name: "PbootCMS",
}
if err := tx.Where("name = ?", "PbootCMS").FirstOrCreate(item, &model.PHPExtensions{
Name: "PbootCMS",
Extensions: "curl,gd,mbstring,mysqli,openssl,pdo_mysql,pdo_sqlite,sqlite3",
}).Error; err != nil {
return err
}
return nil
},
}
var DeleteV2Openresty = &gormigrate.Migration{
ID: "20250701-delete-v2-openresty",
Migrate: func(tx *gorm.DB) error {
return tx.Where("version = '1.27.1.2-0-1-focal'").Delete(&model.AppDetail{}).Error
},
}
var UpdateOnedrive = &gormigrate.Migration{
ID: "20250704-update-onedrive",
Migrate: func(tx *gorm.DB) error {
if err := tx.Model(&model.Setting{}).
Where("key = ?", "OneDriveID").
Update("value", "NTQ0NmNmZTMtNGM3OS00N2EwLWFlMjUtZmM2NDU0NzhlMmQ5").Error; err != nil {
return err
}
if err := tx.Model(&model.Setting{}).
Where("key = ?", "OneDriveSc").
Update("value", "bGRlOFF+WEVrR1M0b25Vb1VsRWpMYzE2MW9rTXZEM25KdnZ1MGN6MA==").Error; err != nil {
return err
}
return nil
},
}
+21 -2
View File
@@ -109,6 +109,15 @@ func handleNoRoute(c *gin.Context) {
c.Data(statusCode, "text/html; charset=utf-8", data)
}
func checkSession(c *gin.Context) bool {
sId, err := c.Cookie(constant.SessionName)
if err != nil {
return false
}
_, err = global.SESSION.Get(sId)
return err == nil
}
func setWebStatic(rootRouter *gin.RouterGroup) {
rootRouter.StaticFS("/public", http.FS(web.Favicon))
rootRouter.StaticFS("/favicon.ico", http.FS(web.Favicon))
@@ -136,10 +145,15 @@ func setWebStatic(rootRouter *gin.RouterGroup) {
})
}
rootRouter.GET("/", func(c *gin.Context) {
if !checkEntrance(c) {
if !checkEntrance(c) && !checkSession(c) {
handleNoRoute(c)
return
}
entrance = authService.GetSecurityEntrance()
if entrance != "" {
entranceValue := base64.StdEncoding.EncodeToString([]byte(entrance))
c.SetCookie("SecurityEntrance", entranceValue, 0, "", "", false, true)
}
staticServer := http.FileServer(http.FS(web.IndexHtml))
staticServer.ServeHTTP(c.Writer, c.Request)
})
@@ -174,7 +188,12 @@ func Routers() *gin.Engine {
swaggerRouter := Router.Group("1panel")
docs.SwaggerInfo.BasePath = "/api/v1"
swaggerRouter.Use(middleware.JwtAuth()).Use(middleware.SessionAuth()).GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
swaggerRouter.GET("/swagger/*any", func(c *gin.Context) {
if !checkSession(c) {
handleNoRoute(c)
return
}
}, ginSwagger.WrapHandler(swaggerfiles.Handler))
PublicGroup := Router.Group("")
{
PublicGroup.GET("/health", func(c *gin.Context) {
+2
View File
@@ -3,6 +3,7 @@ package viper
import (
"bytes"
"fmt"
"os"
"path"
"strings"
@@ -103,6 +104,7 @@ func Init() {
global.CONF.System.Entrance = entrance
global.CONF.System.Language = language
global.CONF.System.ChangeUserInfo = loadChangeInfo()
global.CONF.System.LicenseVerify = os.Getenv("LXWARE_LICENSE_VERIFY")
global.Viper = v
}
+20 -9
View File
@@ -7,10 +7,10 @@ import (
"github.com/1Panel-dev/1Panel/backend/app/repo"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/gin-gonic/gin"
"net"
"strconv"
"strings"
"time"
)
@@ -102,27 +102,38 @@ func isValid1PanelToken(panelToken string, panelTimestamp string) bool {
func isIPInWhiteList(clientIP string) bool {
ipWhiteString := global.CONF.System.IpWhiteList
ipWhiteList := strings.Split(ipWhiteString, "\n")
if len(ipWhiteString) == 0 {
global.LOG.Error("IP whitelist is empty")
return false
}
ipWhiteList, ipErr := common.HandleIPList(ipWhiteString)
if ipErr != nil {
global.LOG.Errorf("Failed to handle IP list: %v", ipErr)
return false
}
clientParsedIP := net.ParseIP(clientIP)
if clientParsedIP == nil {
return false
}
iPv4 := clientParsedIP.To4()
iPv6 := clientParsedIP.To16()
for _, cidr := range ipWhiteList {
if cidr == "0.0.0.0" {
if (iPv4 != nil && (cidr == "0.0.0.0" || cidr == "0.0.0.0/0" || iPv4.String() == cidr)) || (iPv6 != nil && (cidr == "::/0" || iPv6.String() == cidr)) {
return true
}
_, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
if cidr == clientIP {
return true
}
continue
}
if ipNet.Contains(net.ParseIP(clientIP)) {
if (iPv4 != nil && ipNet.Contains(iPv4)) || (iPv6 != nil && ipNet.Contains(iPv6)) {
return true
}
}
return false
}
func GenerateMD5(input string) string {
func GenerateMD5(param string) string {
hash := md5.New()
hash.Write([]byte(input))
hash.Write([]byte(param))
return hex.EncodeToString(hash.Sum(nil))
}
+1
View File
@@ -24,5 +24,6 @@ func commonGroups() []CommonRouter {
&ProcessRouter{},
&WebsiteCARouter{},
&AIToolsRouter{},
&McpServerRouter{},
}
}
+9 -1
View File
@@ -15,7 +15,6 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
baseApi := v1.ApiGroupApp.BaseApi
{
aiToolsRouter.GET("/ollama/exec", baseApi.OllamaWsSsh)
aiToolsRouter.POST("/ollama/close", baseApi.CloseOllamaModel)
aiToolsRouter.POST("/ollama/model", baseApi.CreateOllamaModel)
aiToolsRouter.POST("/ollama/model/recreate", baseApi.RecreateOllamaModel)
@@ -27,5 +26,14 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
aiToolsRouter.POST("/domain/bind", baseApi.BindDomain)
aiToolsRouter.POST("/domain/get", baseApi.GetBindDomain)
aiToolsRouter.POST("/domain/update", baseApi.UpdateBindDomain)
aiToolsRouter.POST("/mcp/search", baseApi.PageMcpServers)
aiToolsRouter.POST("/mcp/server", baseApi.CreateMcpServer)
aiToolsRouter.POST("/mcp/server/update", baseApi.UpdateMcpServer)
aiToolsRouter.POST("/mcp/server/del", baseApi.DeleteMcpServer)
aiToolsRouter.POST("/mcp/server/op", baseApi.OperateMcpServer)
aiToolsRouter.POST("/mcp/domain/bind", baseApi.BindMcpDomain)
aiToolsRouter.GET("/mcp/domain/get", baseApi.GetMcpBindDomain)
aiToolsRouter.POST("/mcp/domain/update", baseApi.UpdateMcpBindDomain)
}
}
+1 -1
View File
@@ -15,7 +15,7 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
Use(middleware.PasswordExpired())
baseApi := v1.ApiGroupApp.BaseApi
{
baRouter.GET("/exec", baseApi.ContainerWsSsh)
baRouter.GET("/exec", baseApi.ContainerWsSSH)
baRouter.GET("/stats/:id", baseApi.ContainerStats)
baRouter.POST("", baseApi.ContainerCreate)
-1
View File
@@ -38,7 +38,6 @@ func (s *DatabaseRouter) InitRouter(Router *gin.RouterGroup) {
cmdRouter.POST("/redis/persistence/conf", baseApi.LoadPersistenceConf)
cmdRouter.POST("/redis/status", baseApi.LoadRedisStatus)
cmdRouter.POST("/redis/conf", baseApi.LoadRedisConf)
cmdRouter.GET("/redis/exec", baseApi.RedisWsSsh)
cmdRouter.GET("/redis/check", baseApi.CheckHasCli)
cmdRouter.POST("/redis/install/cli", baseApi.InstallCli)
cmdRouter.POST("/redis/password", baseApi.ChangeRedisPassword)
+21
View File
@@ -0,0 +1,21 @@
package router
import (
v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1"
"github.com/1Panel-dev/1Panel/backend/middleware"
"github.com/gin-gonic/gin"
)
type McpServerRouter struct {
}
func (m *McpServerRouter) InitRouter(Router *gin.RouterGroup) {
mcpRouter := Router.Group("mcp")
mcpRouter.Use(middleware.JwtAuth()).Use(middleware.SessionAuth()).Use(middleware.PasswordExpired())
baseApi := v1.ApiGroupApp.BaseApi
{
mcpRouter.POST("/search", baseApi.PageMcpServers)
mcpRouter.POST("/server", baseApi.CreateMcpServer)
}
}
+1 -1
View File
@@ -47,9 +47,9 @@ func Start() {
session.Init()
gin.SetMode(gin.DebugMode)
cron.Run()
hook.Init()
InitOthers()
business.Init()
hook.Init()
rootRouter := router.Routers()
+9 -6
View File
@@ -12,7 +12,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/ai_tools/gpu/common"
"github.com/1Panel-dev/1Panel/backend/utils/ai_tools/gpu/schema_v12"
"github.com/1Panel-dev/1Panel/backend/utils/ai_tools/gpu/schema"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
)
@@ -28,7 +28,7 @@ func (n NvidiaSMI) LoadGpuInfo() (*common.GpuInfo, error) {
return nil, fmt.Errorf("calling nvidia-smi failed, err: %v", std)
}
data := []byte(std)
schema := "v11"
version := "v11"
buf := bytes.NewBuffer(data)
decoder := xml.NewDecoder(buf)
@@ -51,15 +51,18 @@ func (n NvidiaSMI) LoadGpuInfo() (*common.GpuInfo, error) {
parts := strings.Split(directive, " ")
s := strings.Trim(parts[len(parts)-1], "\" ")
if strings.HasPrefix(s, "nvsmi_device_") && strings.HasSuffix(s, ".dtd") {
schema = strings.TrimSuffix(strings.TrimPrefix(s, "nvsmi_device_"), ".dtd")
version = strings.TrimSuffix(strings.TrimPrefix(s, "nvsmi_device_"), ".dtd")
} else {
global.LOG.Debugf("Cannot find schema version in %q", directive)
}
break
}
if schema != "v12" {
return &common.GpuInfo{}, nil
if version == "v12" || version == "v11" {
return schema.Parse(data, version)
} else {
global.LOG.Errorf("don't support such schema version %s", version)
}
return schema_v12.Parse(data)
return &common.GpuInfo{}, nil
}
@@ -1,4 +1,4 @@
package schema_v12
package schema
import (
"encoding/xml"
@@ -6,7 +6,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/utils/ai_tools/gpu/common"
)
func Parse(buf []byte) (*common.GpuInfo, error) {
func Parse(buf []byte, version string) (*common.GpuInfo, error) {
var (
s smi
info common.GpuInfo
@@ -36,8 +36,13 @@ func Parse(buf []byte) (*common.GpuInfo, error) {
gpuItem.Temperature = s.Gpu[i].Temperature.GpuTemp
gpuItem.PerformanceState = s.Gpu[i].PerformanceState
gpuItem.PowerDraw = s.Gpu[i].GpuPowerReadings.PowerDraw
gpuItem.MaxPowerLimit = s.Gpu[i].GpuPowerReadings.MaxPowerLimit
if version == "v12" {
gpuItem.PowerDraw = s.Gpu[i].GpuPowerReadings.PowerDraw
gpuItem.MaxPowerLimit = s.Gpu[i].GpuPowerReadings.MaxPowerLimit
} else {
gpuItem.PowerDraw = s.Gpu[i].PowerReadings.PowerDraw
gpuItem.MaxPowerLimit = s.Gpu[i].PowerReadings.MaxPowerLimit
}
gpuItem.MemUsed = s.Gpu[i].FbMemoryUsage.Used
gpuItem.MemTotal = s.Gpu[i].FbMemoryUsage.Total
gpuItem.GPUUtil = s.Gpu[i].Utilization.GpuUtil
@@ -1,4 +1,4 @@
package schema_v12
package schema
type smi struct {
AttachedGpus string `xml:"attached_gpus"`
+13 -2
View File
@@ -6,6 +6,7 @@ import (
"net/http"
"net/url"
"os"
"regexp"
cosSDK "github.com/tencentyun/cos-go-sdk-v5"
)
@@ -18,6 +19,7 @@ type cosClient struct {
func NewCosClient(vars map[string]interface{}) (*cosClient, error) {
region := loadParamFromVars("region", vars)
endpoint := loadParamFromVars("endpoint", vars)
accessKey := loadParamFromVars("accessKey", vars)
secretKey := loadParamFromVars("secretKey", vars)
bucket := loadParamFromVars("bucket", vars)
@@ -26,7 +28,16 @@ func NewCosClient(vars map[string]interface{}) (*cosClient, error) {
scType = "Standard"
}
u, _ := url.Parse(fmt.Sprintf("https://cos.%s.myqcloud.com", region))
// 允许使用对象存储双栈域名
endpointType := "cos"
if len(endpoint) != 0 {
re := regexp.MustCompile(`.*cos-dualstack\..*`)
if re.MatchString(endpoint) {
endpointType = "cos-dualstack"
}
}
u, _ := url.Parse(fmt.Sprintf("https://%s.%s.myqcloud.com", endpointType, region))
b := &cosSDK.BaseURL{BucketURL: u}
client := cosSDK.NewClient(b, &http.Client{
Transport: &cosSDK.AuthorizationTransport{
@@ -36,7 +47,7 @@ func NewCosClient(vars map[string]interface{}) (*cosClient, error) {
})
if len(bucket) != 0 {
u2, _ := url.Parse(fmt.Sprintf("https://%s.cos.%s.myqcloud.com", bucket, region))
u2, _ := url.Parse(fmt.Sprintf("https://%s.%s.%s.myqcloud.com", bucket, endpointType, region))
b2 := &cosSDK.BaseURL{BucketURL: u2}
clientWithBucket := cosSDK.NewClient(b2, &http.Client{
Transport: &cosSDK.AuthorizationTransport{
+57 -2
View File
@@ -3,13 +3,12 @@ package common
import (
"crypto/rand"
"fmt"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/gin-gonic/gin"
"io"
mathRand "math/rand"
"net"
"os"
"path"
"path/filepath"
"reflect"
"regexp"
"sort"
@@ -18,6 +17,9 @@ import (
"time"
"unicode"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/gin-gonic/gin"
"golang.org/x/net/idna"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
@@ -124,6 +126,9 @@ func GetSortedVersions(versions []string) []string {
}
func CopyFile(src, dst string) error {
if fi, err := os.Stat(src); err == nil && fi.IsDir() {
return fmt.Errorf("src is a directory, use CopyDirs instead")
}
source, err := os.Open(src)
if err != nil {
return err
@@ -152,7 +157,57 @@ func CopyFile(src, dst string) error {
}
return nil
}
func CopyDirs(srcDir, dstDir string) error {
srcInfo, err := os.Stat(srcDir)
if err != nil {
return err
}
if !srcInfo.IsDir() {
return fmt.Errorf("%s is not a directory", srcDir)
}
if err := os.MkdirAll(dstDir, srcInfo.Mode()); err != nil {
return err
}
return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(srcDir, path)
if err != nil {
return err
}
dstPath := filepath.Join(dstDir, relPath)
if info.IsDir() {
return os.MkdirAll(dstPath, info.Mode())
}
if info.Mode()&os.ModeSymlink != 0 {
return copySymlink(path, dstPath)
}
return CopyFile(path, dstPath)
})
}
func copySymlink(src, dst string) error {
link, err := os.Readlink(src)
if err != nil {
return err
}
return os.Symlink(link, dst)
}
func Copy(src, dst string) error {
fi, err := os.Stat(src)
if err != nil {
return err
}
switch mode := fi.Mode(); {
case mode.IsDir():
return CopyDirs(src, dst)
case mode.IsRegular():
return CopyFile(src, dst)
default:
return fmt.Errorf("unsupported file type: %s", mode)
}
}
func IsCrossVersion(version1, version2 string) bool {
version1s := strings.Split(version1, ".")
version2s := strings.Split(version2, ".")
+20 -16
View File
@@ -349,26 +349,30 @@ func (f FileOp) DownloadFileWithProxy(url, dst string) error {
}
func (f FileOp) Cut(oldPaths []string, dst, name string, cover bool) error {
for _, p := range oldPaths {
var dstPath string
if name != "" {
dstPath = filepath.Join(dst, name)
if f.Stat(dstPath) {
dstPath = dst
}
} else {
base := filepath.Base(p)
dstPath = filepath.Join(dst, base)
if len(oldPaths) == 0 {
return nil
}
var dstPath string
coverFlag := ""
if name != "" {
dstPath = filepath.Join(dst, name)
if f.Stat(dstPath) {
dstPath = dst
}
coverFlag := ""
if cover {
coverFlag = "-f"
}
cmdStr := fmt.Sprintf(`mv %s '%s' '%s'`, coverFlag, p, dstPath)
if err := cmd.ExecCmd(cmdStr); err != nil {
return err
}
} else {
dstPath = dst
coverFlag = "-f"
}
var quotedPaths []string
for _, p := range oldPaths {
quotedPaths = append(quotedPaths, fmt.Sprintf("'%s'", p))
}
mvCommand := fmt.Sprintf("mv %s %s '%s'", coverFlag, strings.Join(quotedPaths, " "), dstPath)
if err := cmd.ExecCmd(mvCommand); err != nil {
return err
}
return nil
}
+4 -15
View File
@@ -10,6 +10,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
)
var ForwardListRegex = regexp.MustCompile(`^port=(\d{1,5}):proto=(.+?):toport=(\d{1,5}):toaddr=(.*)$`)
@@ -41,27 +42,15 @@ func (f *Firewall) Version() (string, error) {
}
func (f *Firewall) Start() error {
stdout, err := cmd.Exec("systemctl start firewalld")
if err != nil {
return fmt.Errorf("enable the firewall failed, err: %s", stdout)
}
return nil
return systemctl.Start("firewalld")
}
func (f *Firewall) Stop() error {
stdout, err := cmd.Exec("systemctl stop firewalld")
if err != nil {
return fmt.Errorf("stop the firewall failed, err: %s", stdout)
}
return nil
return systemctl.Stop("firewalld")
}
func (f *Firewall) Restart() error {
stdout, err := cmd.Exec("systemctl restart firewalld")
if err != nil {
return fmt.Errorf("restart the firewall failed, err: %s", stdout)
}
return nil
return systemctl.Restart("firewalld")
}
func (f *Firewall) Reload() error {
+3 -1
View File
@@ -46,7 +46,9 @@ func NewLocation(directive IDirective) *Location {
location.Host = params[1]
}
case "proxy_cache":
location.Cache = true
if params[0] != "off" {
location.Cache = true
}
case "if":
if params[0] == "(" && params[1] == "$uri" && params[2] == "~*" {
dirs := dir.GetBlock().GetDirectives()
-55
View File
@@ -1,11 +1,8 @@
package ssh
import (
"bytes"
"fmt"
"io"
"strings"
"sync"
"time"
gossh "golang.org/x/crypto/ssh"
@@ -82,58 +79,6 @@ func (c *ConnInfo) Close() {
_ = c.Client.Close()
}
type SshConn struct {
StdinPipe io.WriteCloser
ComboOutput *wsBufferWriter
Session *gossh.Session
}
func (c *ConnInfo) NewSshConn(cols, rows int) (*SshConn, error) {
sshSession, err := c.Client.NewSession()
if err != nil {
return nil, err
}
stdinP, err := sshSession.StdinPipe()
if err != nil {
return nil, err
}
comboWriter := new(wsBufferWriter)
sshSession.Stdout = comboWriter
sshSession.Stderr = comboWriter
modes := gossh.TerminalModes{
gossh.ECHO: 1,
gossh.TTY_OP_ISPEED: 14400,
gossh.TTY_OP_OSPEED: 14400,
}
if err := sshSession.RequestPty("xterm", rows, cols, modes); err != nil {
return nil, err
}
if err := sshSession.Shell(); err != nil {
return nil, err
}
return &SshConn{StdinPipe: stdinP, ComboOutput: comboWriter, Session: sshSession}, nil
}
func (s *SshConn) Close() {
if s.Session != nil {
s.Session.Close()
}
}
type wsBufferWriter struct {
buffer bytes.Buffer
mu sync.Mutex
}
func (w *wsBufferWriter) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
return w.buffer.Write(p)
}
func makePrivateKeySigner(privateKey []byte, passPhrase []byte) (gossh.Signer, error) {
if len(passPhrase) != 0 {
return gossh.ParsePrivateKeyWithPassphrase(privateKey, passPhrase)
+47 -13
View File
@@ -3,11 +3,14 @@ package ssl
import (
"crypto"
"encoding/json"
"github.com/go-acme/lego/v4/providers/dns/rainyun"
"github.com/go-acme/lego/v4/providers/dns/westcn"
"os"
"strings"
"time"
"github.com/go-acme/lego/v4/providers/dns/freemyip"
"github.com/go-acme/lego/v4/providers/dns/rainyun"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/go-acme/lego/v4/acme"
"github.com/go-acme/lego/v4/acme/api"
@@ -18,6 +21,7 @@ import (
"github.com/go-acme/lego/v4/providers/dns/alidns"
"github.com/go-acme/lego/v4/providers/dns/clouddns"
"github.com/go-acme/lego/v4/providers/dns/cloudflare"
"github.com/go-acme/lego/v4/providers/dns/cloudns"
"github.com/go-acme/lego/v4/providers/dns/dnspod"
"github.com/go-acme/lego/v4/providers/dns/godaddy"
"github.com/go-acme/lego/v4/providers/dns/huaweicloud"
@@ -73,6 +77,8 @@ const (
Volcengine DnsType = "Volcengine"
CloudFlare DnsType = "CloudFlare"
CloudDns DnsType = "CloudDns"
ClouDNS DnsType = "ClouDNS"
FreeMyIP DnsType = "FreeMyIP"
NameSilo DnsType = "NameSilo"
NameCheap DnsType = "NameCheap"
NameCom DnsType = "NameCom"
@@ -80,21 +86,26 @@ const (
TencentCloud DnsType = "TencentCloud"
HuaweiCloud DnsType = "HuaweiCloud"
RainYun DnsType = "RainYun"
WestCN DnsType = "WestCN"
)
type DNSParam struct {
ID string `json:"id"`
Token string `json:"token"`
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
Email string `json:"email"`
APIkey string `json:"apiKey"`
APIUser string `json:"apiUser"`
APISecret string `json:"apiSecret"`
SecretID string `json:"secretID"`
Region string `json:"region"`
ClientID string `json:"clientID"`
Password string `json:"password"`
ID string `json:"id"`
Token string `json:"token"`
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
Email string `json:"email"`
APIkey string `json:"apiKey"`
APIUser string `json:"apiUser"`
APISecret string `json:"apiSecret"`
SecretID string `json:"secretID"`
Region string `json:"region"`
ClientID string `json:"clientID"`
Password string `json:"password"`
AuthID string `json:"authID"`
SubAuthID string `json:"subAuthID"`
AuthPassword string `json:"authPassword"`
Username string `json:"username"`
}
var (
@@ -155,6 +166,21 @@ func (c *AcmeClient) UseDns(dnsType DnsType, params string, websiteSSL model.Web
clouddnsConfig.PollingInterval = pollingInterval
clouddnsConfig.TTL = ttl
p, err = clouddns.NewDNSProviderConfig(clouddnsConfig)
case ClouDNS:
cloudnsConfig := cloudns.NewDefaultConfig()
cloudnsConfig.AuthID = param.AuthID
cloudnsConfig.SubAuthID = param.SubAuthID
cloudnsConfig.AuthPassword = param.AuthPassword
cloudnsConfig.PropagationTimeout = propagationTimeout
cloudnsConfig.PollingInterval = pollingInterval
cloudnsConfig.TTL = ttl
p, err = cloudns.NewDNSProviderConfig(cloudnsConfig)
case FreeMyIP:
freeMyIpConfig := freemyip.NewDefaultConfig()
freeMyIpConfig.Token = param.Token
freeMyIpConfig.PropagationTimeout = propagationTimeout
freeMyIpConfig.PollingInterval = pollingInterval
p, err = freemyip.NewDNSProviderConfig(freeMyIpConfig)
case NameCheap:
namecheapConfig := namecheap.NewDefaultConfig()
namecheapConfig.APIKey = param.APIkey
@@ -210,6 +236,14 @@ func (c *AcmeClient) UseDns(dnsType DnsType, params string, websiteSSL model.Web
rainyunConfig.PollingInterval = pollingInterval
rainyunConfig.TTL = ttl
p, err = rainyun.NewDNSProviderConfig(rainyunConfig)
case WestCN:
westcnConfig := westcn.NewDefaultConfig()
westcnConfig.Username = param.Username
westcnConfig.Password = param.Password
westcnConfig.PropagationTimeout = propagationTimeout
westcnConfig.PollingInterval = pollingInterval
westcnConfig.TTL = ttl
p, err = westcn.NewDNSProviderConfig(westcnConfig)
}
if err != nil {
return err
+393
View File
@@ -0,0 +1,393 @@
package systemctl
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"go4.org/syncutil/singleflight"
"golang.org/x/sync/errgroup"
)
var (
aliasFile string
serviceAliases sync.Map
saveTimer *time.Timer
saveMutex sync.Mutex
afterSaveTime = 20 * time.Second
)
var (
ErrServiceNotFound = errors.New("service not found")
ErrDiscoveryTimeout = errors.New("service discovery timeout for: %w")
ErrServiceDiscovery = errors.New("service discovery failed for: %w")
ErrNoValidService = errors.New("no valid service found for: %w")
)
func loadPredefinedAliases() map[string][]string {
return map[string][]string{
"clam": {"clamav-daemon.service", "clamd@scan.service", "clamd"},
"freshclam": {"clamav-freshclam.service", "freshclam.service"},
"fail2ban": {"fail2ban.service", "fail2ban"},
"supervisor": {"supervisord.service", "supervisor.service", "supervisord", "supervisor"},
"ssh": {"sshd.service", "ssh.service", "sshd", "ssh"},
"1panel": {"1panel.service", "1paneld"},
"docker": {"docker.service", "dockerd"},
}
}
func InitializeServiceDiscovery() {
svcName := loadAliasesFromConfig()
if len(svcName) > 0 {
RegisterServiceAliases(svcName)
}
}
func RegisterServiceAliases(aliases map[string][]string) {
for key, values := range aliases {
existing, loaded := serviceAliases.LoadOrStore(key, values)
if loaded {
merged := append(existing.([]string), values...)
serviceAliases.Store(key, merged)
}
}
}
func loadAliasesFromConfig() map[string][]string {
data, err := os.ReadFile(aliasFile)
if err != nil {
return nil
}
var rawAliases map[string][]string
json.Unmarshal(data, &rawAliases)
validAliases := make(map[string][]string)
for key, aliases := range rawAliases {
valid := []string{}
for _, alias := range aliases {
confirmed, _ := confirmServiceExists(alias)
if confirmed {
valid = append(valid, alias)
}
}
if len(valid) > 0 {
validAliases[key] = valid
}
}
return validAliases
}
func cleanupKeywordAliases(keyword string) {
serviceAliases.Range(func(k, v interface{}) bool {
if k.(string) != keyword {
return true
}
aliases := v.([]string)
valid := make([]string, 0)
for _, alias := range aliases {
confirmed, _ := confirmServiceExists(alias)
if confirmed {
valid = append(valid, alias)
}
}
if len(valid) == 0 {
serviceAliases.Delete(k)
serviceExistenceCache.Delete(k)
} else {
serviceAliases.Store(k, valid)
}
return true
})
go scheduleSave()
}
func smartServiceName(keyword string) (string, error) {
mgr := GetGlobalManager()
processedName := handleServiceNaming(mgr, keyword)
confirmed, _ := confirmServiceExists(processedName)
if confirmed {
updateAliases(keyword, processedName)
return processedName, nil
}
candidates := append([]string{processedName}, getAliases(keyword)...)
if name, err := validateCandidatesConcurrently(candidates); err == nil {
updateAliases(keyword, name)
return name, nil
}
discoveredName, err := discoverAndSelectService(keyword)
if err != nil {
cleanupKeywordAliases(keyword)
return "", ErrServiceNotFound
}
updateAliases(keyword, discoveredName)
return discoveredName, nil
}
func handleServiceNaming(mgr ServiceManager, keyword string) string {
keyword = strings.ToLower(keyword)
if strings.HasSuffix(keyword, ".service.socket") {
keyword = strings.TrimSuffix(keyword, ".service.socket") + ".socket"
}
if mgr.Name() != "systemd" {
keyword = strings.TrimSuffix(keyword, ".service")
return keyword
}
if !strings.HasSuffix(keyword, ".service") &&
!strings.HasSuffix(keyword, ".socket") {
keyword += ".service"
}
return keyword
}
func validateCandidatesConcurrently(candidates []string) (string, error) {
var (
g errgroup.Group
found = make(chan string, 1)
)
for _, candidate := range candidates {
cand := candidate
g.Go(func() error {
confirmed, _ := confirmServiceExists(cand)
if confirmed {
select {
case found <- cand:
default:
}
return nil
}
return ErrServiceNotFound
})
}
resultErr := make(chan error, 1)
go func() {
defer close(found)
resultErr <- g.Wait()
}()
select {
case name := <-found:
return name, nil
case <-time.After(1000 * time.Millisecond):
return "", fmt.Errorf(ErrDiscoveryTimeout.Error(), candidates[0])
case err := <-resultErr:
if err != nil {
return "", ErrServiceNotFound
}
return "", ErrServiceNotFound
}
}
func discoverAndSelectService(keyword string) (string, error) {
discovered, err := discoverServices(keyword)
if err != nil {
return "", ErrServiceNotFound
}
if len(discovered) == 0 {
return "", ErrServiceNotFound
}
selected, err := selectBestMatch(keyword, discovered)
if err != nil {
return "", ErrServiceNotFound
}
confirmed, err := confirmServiceExists(selected)
if err != nil {
return "", fmt.Errorf("service existence check failed: %w", err)
}
if confirmed {
return selected, nil
}
return "", ErrServiceNotFound
}
func selectBestMatch(keyword string, candidates []string) (string, error) {
if len(candidates) == 0 {
return "", ErrServiceNotFound
}
lowerKeyword := strings.ToLower(keyword)
var exactMatch string
var firstContainMatch string
for _, name := range candidates {
if strings.EqualFold(name, keyword) {
exactMatch = name
break
}
}
if exactMatch != "" {
return exactMatch, nil
}
for _, name := range candidates {
if strings.Contains(strings.ToLower(name), lowerKeyword) {
firstContainMatch = name
global.LOG.Debugf("[%s] [keyword: %s] Found first contain match: %s", getManagerName(), keyword, firstContainMatch)
break
}
}
if firstContainMatch != "" {
return firstContainMatch, nil
}
return "", fmt.Errorf("%w: %q (no exact or partial match)", ErrNoValidService, keyword)
}
type cacheItem struct {
services []string
expires time.Time
exists bool
}
var (
discoveryCache sync.Map
discoveryGroup singleflight.Group
)
func discoverServices(keyword string) ([]string, error) {
result, err := discoveryGroup.Do(keyword, func() (interface{}, error) {
if cached, ok := discoveryCache.Load(keyword); ok {
item := cached.(cacheItem)
if time.Now().Before(item.expires) {
return item.services, nil
}
discoveryCache.Delete(keyword)
}
manager := GetGlobalManager()
results, err := manager.FindServices(keyword)
if err != nil {
global.LOG.Errorf("Find services failed for %s: %v", keyword, err)
return nil, fmt.Errorf("%w: %q (%v)", ErrServiceDiscovery, keyword, err)
} else {
discoveryCache.Store(keyword, cacheItem{
services: results,
expires: time.Now().Add(5 * time.Minute),
})
}
return results, err
})
if err != nil {
return nil, err
}
return result.([]string), nil
}
func updateAliases(keyword, alias string) {
if keyword == alias {
return
}
existing, _ := serviceAliases.LoadOrStore(keyword, []string{})
aliases := existing.([]string)
if contains(aliases, alias) {
return
}
serviceAliases.Store(keyword, append(aliases, alias))
go scheduleSave()
}
func scheduleSave() {
saveMutex.Lock()
defer saveMutex.Unlock()
if saveTimer != nil {
saveTimer.Stop()
}
dataSnapshot := make(map[string][]string)
serviceAliases.Range(func(k, v interface{}) bool {
dataSnapshot[k.(string)] = append([]string{}, v.([]string)...)
return true
})
aliasFile = filepath.Join(constant.ResourceDir, "svcaliases.json")
saveTimer = time.AfterFunc(afterSaveTime, func() {
tmpFile := aliasFile + ".tmp"
if err := saveAliasesToFile(dataSnapshot, tmpFile); err == nil {
os.Rename(tmpFile, aliasFile)
}
})
}
func saveAliasesToFile(data map[string][]string, path string) error {
fileData, err := json.MarshalIndent(data, "", " ")
if err != nil {
return fmt.Errorf("serialization failed: %w", err)
}
if err := os.WriteFile(path, fileData, 0644); err != nil {
return fmt.Errorf("file write failed: %w", err)
}
return nil
}
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
var serviceExistenceCache sync.Map
func confirmServiceExists(serviceName string) (bool, error) {
if val, ok := serviceExistenceCache.Load(serviceName); ok {
if item, ok := val.(cacheItem); ok && time.Now().Before(item.expires) {
return item.exists, nil
}
serviceExistenceCache.Delete(serviceName)
}
handler := NewServiceHandler(defaultServiceConfig(serviceName))
isExist, err := handler.IsExists()
if err != nil {
return false, fmt.Errorf("check service existence failed: %w", err)
}
serviceExistenceCache.Store(serviceName, cacheItem{
exists: isExist.IsExists,
expires: time.Now().Add(30 * time.Second),
})
return isExist.IsExists, nil
}
func getAliases(keyword string) []string {
predefined := loadPredefinedAliases()[keyword]
runtimeAliases, _ := serviceAliases.LoadOrStore(keyword, []string{})
merged := make(map[string]struct{})
for _, alias := range predefined {
merged[alias] = struct{}{}
}
for _, alias := range runtimeAliases.([]string) {
merged[alias] = struct{}{}
}
result := make([]string, 0, len(merged))
for k := range merged {
result = append(result, k)
}
return result
}
type ConfigOption struct {
TailLines string
}
func ViewConfig(path string, opt ConfigOption) (string, error) {
var cmd []string
if opt.TailLines != "" && opt.TailLines != "0" {
cmd = []string{"tail", "-n", opt.TailLines, path}
} else {
cmd = []string{"cat", path}
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
output, err := executeCommand(ctx, cmd[0], cmd[1:]...)
if err != nil {
return "", fmt.Errorf("view config failed: %w", err)
}
return string(output), nil
}
+283
View File
@@ -0,0 +1,283 @@
package systemctl
import (
"context"
"errors"
"fmt"
"path/filepath"
"strings"
"time"
"github.com/1Panel-dev/1Panel/backend/global"
)
type ServiceConfig struct {
ServiceName map[string]string
}
type ServiceHandler struct {
config *ServiceConfig
manager ServiceManager
}
func NewServiceHandler(serviceNames map[string]string) *ServiceHandler {
mgr := GetGlobalManager()
if mgr == nil {
global.LOG.Error("failed to get global service manager when creating ServiceHandler")
return nil
}
return &ServiceHandler{
config: &ServiceConfig{
ServiceName: serviceNames,
},
manager: mgr,
}
}
type ServiceStatus struct {
IsActive bool `json:"isActive"`
IsEnabled bool `json:"isEnabled"`
IsExists bool `json:"isExists"`
Output string `json:"output"`
}
type ServiceIsActive struct {
IsActive bool `json:"isActive"`
Output string `json:"output"`
}
type ServiceIsEnabled struct {
IsEnabled bool `json:"isEnabled"`
Output string `json:"output"`
}
type ServiceResult struct {
Success bool `json:"success"`
Message string `json:"message"`
Output string `json:"output"`
}
var (
BinaryPath = "/usr/local/bin" // 1panl service default path
ErrServiceNotExist = errors.New("service does not exist")
)
func defaultServiceConfig(serviceName string) map[string]string {
mgr := getManagerName()
if mgr == "" {
global.LOG.Error("failed to get manager name for default service config")
return nil
}
return map[string]string{
mgr: serviceName,
}
}
func (h *ServiceHandler) ManagerName() string { return h.manager.Name() }
func getManagerName() string {
if mgr := GetGlobalManager(); mgr != nil {
return mgr.Name()
}
global.LOG.Error("failed to get global service manager")
return ""
}
func (h *ServiceHandler) GetServiceName() string {
manager := h.ManagerName()
if manager == "" {
global.LOG.Error("manager name is empty when getting service name")
return ""
}
return h.config.ServiceName[manager]
}
func (h *ServiceHandler) GetServicePath() (string, error) {
manager := h.ManagerName()
serviceName := h.config.ServiceName[manager]
if serviceName == "" {
err := fmt.Errorf("service name not found for %s", manager)
global.LOG.Errorf("GetServicePath error: %v", err)
return "", err
}
cleanPath := filepath.Clean(serviceName)
if strings.Contains(cleanPath, "..") {
err := fmt.Errorf("invalid path: %q", cleanPath)
global.LOG.Errorf("GetServicePath security check failed: %v", err)
return "", err
}
switch manager {
case "systemd":
return findSystemdPath(cleanPath)
case "openrc", "sysvinit":
return checkInitDPath(cleanPath)
default:
err := fmt.Errorf("unsupported init system: %s", manager)
global.LOG.Errorf("GetServicePath error: %v", err)
return "", err
}
}
func findSystemdPath(name string) (string, error) {
paths := []string{"/etc/systemd/system", "/usr/lib/systemd/system",
"/usr/share/systemd/system", "/usr/local/lib/systemd/system"}
for _, p := range paths {
if path := filepath.Join(p, name); FileExist(path) {
return path, nil
}
}
err := fmt.Errorf("service path not found for %s", name)
global.LOG.Errorf("findSystemdPath error: %v", err)
return "", err
}
func checkInitDPath(name string) (string, error) {
path := filepath.Join("/etc/init.d", name)
if !FileExist(path) {
err := fmt.Errorf("service path not found for %s", name)
global.LOG.Errorf("checkInitDPath error: %v", err)
return "", err
}
return path, nil
}
func (h *ServiceHandler) ExecuteAction(action string) (ServiceResult, error) {
successMsg := fmt.Sprintf("%s : %s completed", action, h.GetServiceName())
return h.executeAction(action, successMsg)
}
func (h *ServiceHandler) IsExists() (ServiceStatus, error) {
manager := GetGlobalManager()
isExist, _ := manager.ServiceExists(h.config)
return ServiceStatus{
IsExists: isExist,
}, nil
}
func (h *ServiceHandler) IsActive() (ServiceStatus, error) {
manager := GetGlobalManager()
if manager == nil {
global.LOG.Error("service manager not initialized during active check")
return ServiceStatus{}, fmt.Errorf("service manager not initialized")
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
activeCmd, err := manager.BuildCommand("status", h.config)
if err != nil {
global.LOG.Errorf("Build status command failed: %v", err)
return ServiceStatus{}, fmt.Errorf("build status command failed: %w", err)
}
output, err := executeCommand(ctx, activeCmd[0], activeCmd[1:]...)
if err != nil {
if strings.Contains(err.Error(), "inactive") {
return ServiceStatus{
IsExists: false,
}, nil
}
global.LOG.Errorf("Active check execution failed: %v", err)
return ServiceStatus{}, fmt.Errorf("status check failed: %w", err)
}
isActive, err := manager.ParseStatus(string(output), h.config, "active")
if err != nil {
global.LOG.Warnf("Status parse error: %v", err)
}
return ServiceStatus{
IsActive: isActive,
Output: string(output),
}, nil
}
func (h *ServiceHandler) IsEnabled() (ServiceStatus, error) {
manager := GetGlobalManager()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
enabledCmd, err := manager.BuildCommand("is-enabled", h.config)
if err != nil {
if strings.Contains(err.Error(), "not found") {
return ServiceStatus{
IsEnabled: false,
}, nil
}
global.LOG.Errorf("Build is-enabled command failed: %v", err)
return ServiceStatus{}, fmt.Errorf("build enabled check command failed: %w", err)
}
output, err := executeCommand(ctx, enabledCmd[0], enabledCmd[1:]...)
if err != nil {
if strings.Contains(err.Error(), "disabled") {
return ServiceStatus{
IsEnabled: false,
}, nil
}
// // isEnabled, err := h.ParseStatus(string(output), h.config, "enabled")
// global.LOG.Errorf("Enabled check execution failed: %v", err)
// return ServiceStatus{}, fmt.Errorf("enabled check failed: %w", err)
}
isEnabled, err := manager.ParseStatus(string(output), h.config, "enabled")
if err != nil {
global.LOG.Warnf("Enabled status parse error: %v", err)
}
return ServiceStatus{
IsEnabled: isEnabled,
Output: string(output),
}, nil
}
func (h *ServiceHandler) StartService() (ServiceResult, error) {
return h.ExecuteAction("start")
}
func (h *ServiceHandler) StopService() (ServiceResult, error) {
return h.ExecuteAction("stop")
}
func (h *ServiceHandler) RestartService() (ServiceResult, error) {
return h.ExecuteAction("restart")
}
func (h *ServiceHandler) EnableService() (ServiceResult, error) {
return h.ExecuteAction("enable")
}
func (h *ServiceHandler) DisableService() (ServiceResult, error) {
return h.ExecuteAction("disable")
}
func (h *ServiceHandler) executeAction(action, successMsg string) (ServiceResult, error) {
manager := GetGlobalManager()
if manager == nil {
global.LOG.Error("service manager not initialized during action execution")
return ServiceResult{}, fmt.Errorf("service manager not initialized")
}
ctx, cancel := context.WithTimeout(context.Background(), defaultCommandTimeout)
defer cancel()
cmdArgs, err := manager.BuildCommand(action, h.config)
if err != nil {
global.LOG.Errorf("Build command failed for action %s: %v", action, err)
return ServiceResult{}, fmt.Errorf("build command failed: %w", err)
}
output, err := executeCommand(ctx, cmdArgs[0], cmdArgs[1:]...)
if err != nil {
global.LOG.Errorf("%s operation failed: %v", action, err)
return ServiceResult{
Success: false,
Message: fmt.Sprintf("%s failed", action),
Output: string(output),
}, fmt.Errorf("%s operation failed: %w", action, err)
}
global.LOG.Infof("[%s]: %s", manager.Name(), successMsg)
return ServiceResult{
Success: true,
Message: successMsg,
Output: string(output),
}, nil
}
+440
View File
@@ -0,0 +1,440 @@
package systemctl
import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/backend/global"
)
var (
managers = make(map[string]ServiceManager)
mu sync.RWMutex
globalManager ServiceManager
managerPriority = []string{"systemd", "openrc", "sysvinit"}
)
const (
defaultCommandTimeout = 60 * time.Second
serviceCheckTimeout = 5 * time.Second
)
type ServiceManager interface {
Name() string
IsAvailable() bool
ServiceExists(*ServiceConfig) (bool, error)
BuildCommand(string, *ServiceConfig) ([]string, error)
ParseStatus(string, *ServiceConfig, string) (bool, error)
FindServices(string) ([]string, error)
}
type baseManager struct {
name string
cmdTool string
activeRegex *regexp.Regexp
enabledRegex *regexp.Regexp
}
func (b *baseManager) Name() string { return b.name }
func isRootUser() bool {
return os.Geteuid() == 0
}
func (b *baseManager) buildBaseCommand() []string {
var cmdArgs []string
if !isRootUser() {
cmdArgs = append(cmdArgs, "sudo")
}
cmdArgs = append(cmdArgs, b.cmdTool)
return cmdArgs
}
func (b *baseManager) commonServiceExists(config *ServiceConfig, checkFn func(string) (bool, error)) (bool, error) {
if name := config.ServiceName[b.name]; name != "" {
exists, checkErr := checkFn(name)
if checkErr != nil {
return false, nil
}
return exists, nil
}
return false, nil
}
func (b *baseManager) ParseStatus(output string, _ *ServiceConfig, statusType string) (bool, error) {
if output == "" {
return false, nil
}
switch statusType {
case "active":
if b.activeRegex == nil {
return false, nil
}
return b.activeRegex.MatchString(output), nil
case "enabled":
if b.enabledRegex == nil {
return false, nil
}
return b.enabledRegex.MatchString(output), nil
default:
return false, nil
}
}
func registerManager(m ServiceManager) {
mu.Lock()
defer mu.Unlock()
managers[m.Name()] = m
}
func init() {
for _, mgr := range []ServiceManager{
newSystemdManager(),
newOpenrcManager(),
newSysvinitManager(),
} {
registerManager(mgr)
}
}
func InitializeGlobalManager() (err error) {
for _, name := range managerPriority {
if mgr, ok := managers[name]; ok && mgr.IsAvailable() {
if testManager(mgr) {
globalManager = mgr
global.LOG.Infof("Initialized service manager: %s", name)
return
}
}
}
err = fmt.Errorf("no available service manager found (tried: %v)", managerPriority)
return
}
func testManager(mgr ServiceManager) bool {
_, err := mgr.BuildCommand("status", &ServiceConfig{
ServiceName: map[string]string{mgr.Name(): "test-service"},
})
return err == nil
}
func GetGlobalManager() ServiceManager {
if globalManager == nil {
mu.Lock()
defer mu.Unlock()
if globalManager == nil {
return initializeWithRetry()
}
}
return globalManager
}
func initializeWithRetry() ServiceManager {
const (
maxRetries = 5
initialWait = 1 * time.Second
)
backoff := initialWait
for attempt := 1; attempt <= maxRetries; attempt++ {
if err := InitializeGlobalManager(); err == nil {
return globalManager
}
logMessage := fmt.Sprintf("Manager init attempt %d/%d failed", attempt, maxRetries)
if global.LOG != nil {
global.LOG.Warn(logMessage)
} else {
fmt.Printf("[WARN] %s\n", logMessage)
}
if attempt < maxRetries {
time.Sleep(backoff)
backoff *= 2
}
}
if global.LOG != nil {
global.LOG.Error("All manager initialization attempts failed")
} else {
fmt.Println("[FATAL] All manager initialization attempts failed")
}
panic("unable to initialize service manager")
}
func executeCommand(ctx context.Context, command string, args ...string) ([]byte, error) {
if _, ok := ctx.Deadline(); !ok {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, defaultCommandTimeout)
defer cancel()
}
cmd := exec.CommandContext(ctx, command, args...)
var buf bytes.Buffer
cmd.Stdout = &buf
cmd.Stderr = &buf
if err := cmd.Run(); err != nil {
return buf.Bytes(), &CommandError{
Cmd: cmd.String(),
Output: buf.String(),
Err: err,
}
}
return buf.Bytes(), nil
}
type systemdManager struct{ baseManager }
func newSystemdManager() ServiceManager {
return &systemdManager{baseManager{
name: "systemd",
cmdTool: "systemctl",
activeRegex: regexp.MustCompile(`(?i)Active:\s+active\b`),
enabledRegex: regexp.MustCompile(`(?i)^\s*enabled\s*$`),
}}
}
func (m *systemdManager) IsAvailable() bool {
_, err := exec.LookPath(m.cmdTool)
return err == nil
}
func (m *systemdManager) ServiceExists(config *ServiceConfig) (bool, error) {
return m.commonServiceExists(config, func(name string) (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), serviceCheckTimeout)
defer cancel()
out, cmdErr := executeCommand(ctx, m.cmdTool, "is-enabled", name)
if cmdErr == nil || strings.Contains(string(out), "disabled") {
return true, nil
}
return false, nil
})
}
func (m *systemdManager) BuildCommand(action string, config *ServiceConfig) ([]string, error) {
cmdArgs := m.buildBaseCommand()
service := config.ServiceName[m.name]
switch action {
case "is-enabled":
cmdArgs = append(cmdArgs, "is-enabled", service)
default:
cmdArgs = append(cmdArgs, action, service)
}
return cmdArgs, nil
}
func (m *systemdManager) ParseStatus(output string, config *ServiceConfig, statusType string) (bool, error) {
if strings.Contains(output, "could not be found") {
return false, nil
}
result, err := m.baseManager.ParseStatus(output, config, statusType)
if err != nil {
return false, nil
}
return result, nil
}
func (m *systemdManager) FindServices(keyword string) ([]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
out, err := executeCommand(ctx, m.cmdTool, "list-unit-files", "--type=service", "--no-legend")
if err != nil {
return nil, fmt.Errorf("failed to list systemd services: %w", err)
}
var services []string
lines := strings.Split(string(out), "\n")
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) < 1 {
continue
}
serviceName := fields[0]
if strings.Contains(serviceName, keyword) {
services = append(services, serviceName)
}
}
return services, nil
}
type sysvinitManager struct{ baseManager }
func newSysvinitManager() ServiceManager {
return &sysvinitManager{baseManager{
name: "sysvinit",
cmdTool: "service",
activeRegex: regexp.MustCompile(`(?i)(?:^|\s)\b(running|active)\b(?:$|\s)`),
enabledRegex: regexp.MustCompile(`(?i)(?:^|\s)\b(enabled)\b(?:$|\s)`),
}}
}
func (m *sysvinitManager) IsAvailable() bool {
_, err := exec.LookPath(m.cmdTool)
return err == nil
}
func (m *sysvinitManager) ServiceExists(config *ServiceConfig) (bool, error) {
return m.commonServiceExists(config, func(name string) (bool, error) {
_, err := os.Stat(filepath.Join("/etc/init.d", name))
if os.IsNotExist(err) {
return false, nil
} else if err != nil {
return false, fmt.Errorf("stat /etc/init.d/%s failed: %w", name, err)
}
return true, nil
})
}
func (m *sysvinitManager) BuildCommand(action string, config *ServiceConfig) ([]string, error) {
service := config.ServiceName[m.name]
switch action {
case "is-enabled":
return []string{
"sh",
"-c",
fmt.Sprintf("if ls /etc/rc*.d/S*%s >/dev/null 2>&1; then echo 'enabled'; else echo 'disabled'; fi", service)}, nil
case "is-active", "status":
return []string{
"sh",
"-c",
fmt.Sprintf("if service %s status >/dev/null 2>&1; then echo 'active'; else echo 'inactive'; fi", service),
}, nil
default:
cmdArgs := m.buildBaseCommand()
cmdArgs = append(cmdArgs, service, action)
return cmdArgs, nil
}
}
func (m *sysvinitManager) ParseStatus(output string, config *ServiceConfig, statusType string) (bool, error) {
serviceName := config.ServiceName[m.name]
switch statusType {
case "enabled":
if strings.Contains(output, "no such file or directory") {
return false, nil
}
return strings.TrimSpace(output) != "", nil
case "active":
if strings.Contains(output, "not found") {
return false, nil
}
if strings.Contains(output, "running") || strings.Contains(output, "active") {
return true, nil
}
default:
result, err := m.baseManager.ParseStatus(output, config, statusType)
if err != nil {
global.LOG.Debugf("[sysvinit] Status parse failed. [ServiceName:%s] Type: %s, Output: %q", serviceName, statusType, output)
}
return result, err
}
return false, fmt.Errorf("unsupported status type: %s", statusType)
}
func (m *sysvinitManager) FindServices(keyword string) ([]string, error) {
files, err := os.ReadDir("/etc/init.d/")
if err != nil {
return nil, fmt.Errorf("failed to read init.d directory: %w", err)
}
var services []string
for _, file := range files {
if strings.Contains(file.Name(), keyword) {
services = append(services, file.Name())
}
}
return services, nil
}
type openrcManager struct{ baseManager }
func newOpenrcManager() ServiceManager {
return &openrcManager{baseManager{
name: "openrc",
cmdTool: "rc-service",
// activeRegex: regexp.MustCompile(`(?i)^\s*status:\s+(started|running|active)\s*$`),
// enabledRegex: regexp.MustCompile(`(?i)^[^\|]+\|\s*(default|enabled)\b.*$`),
activeRegex: regexp.MustCompile(`(?i)(?:^|\s)\b(running|active)\b(?:$|\s)`),
enabledRegex: regexp.MustCompile(`(?i)(?:^|\s)\b(enabled)\b(?:$|\s)`),
}}
}
func (m *openrcManager) IsAvailable() bool {
_, err := exec.LookPath(m.cmdTool)
return err == nil
}
func (m *openrcManager) ServiceExists(config *ServiceConfig) (bool, error) {
return m.commonServiceExists(config, func(name string) (bool, error) {
_, err := os.Stat(filepath.Join("/etc/init.d", name))
if os.IsNotExist(err) {
return false, nil
} else if err != nil {
return false, fmt.Errorf("stat /etc/init.d/%s failed: %w", name, err)
}
return true, nil
})
}
func (m *openrcManager) BuildCommand(action string, config *ServiceConfig) ([]string, error) {
cmdArgs := m.buildBaseCommand()
service := config.ServiceName[m.name]
switch action {
case "is-enabled":
return []string{
"sh",
"-c",
fmt.Sprintf("if ls /etc/runlevels/default/%s >/dev/null 2>&1; then echo 'enabled'; else echo 'disabled'; fi", service)}, nil
case "is-active", "status":
return []string{
"sh",
"-c",
fmt.Sprintf("if service %s status >/dev/null 2>&1; then echo 'active'; else echo 'inactive'; fi", service),
}, nil
case "enable":
return []string{"rc-update", "add", service, "default"}, nil
case "disable":
return []string{"rc-update", "del", service, "default"}, nil
default:
cmdArgs = append(cmdArgs, service, action)
return cmdArgs, nil
}
}
func (m *openrcManager) ParseStatus(output string, config *ServiceConfig, statusType string) (bool, error) {
if strings.Contains(output, "does not exist") {
return false, nil
}
result, err := m.baseManager.ParseStatus(output, config, statusType)
if err != nil {
return false, nil
}
return result, nil
}
func (m *openrcManager) FindServices(keyword string) ([]string, error) {
files, err := os.ReadDir("/etc/init.d/")
if err != nil {
return nil, fmt.Errorf("failed to read init.d directory: %w", err)
}
keyword = strings.ToLower(keyword)
var services []string
for _, file := range files {
if strings.Contains(file.Name(), keyword) {
services = append(services, file.Name())
}
}
return services, nil
}
type CommandError struct {
Cmd string
Err error
Output string
}
func (e CommandError) Error() string {
return fmt.Sprintf("command %q failed: %v \nOutput: %s",
e.Cmd, e.Err, e.Output)
}
+121 -31
View File
@@ -2,63 +2,153 @@ package systemctl
import (
"fmt"
"github.com/pkg/errors"
"os/exec"
"strings"
"os"
"github.com/1Panel-dev/1Panel/backend/global"
)
func RunSystemCtl(args ...string) (string, error) {
cmd := exec.Command("systemctl", args...)
output, err := cmd.CombinedOutput()
func DefaultHandler(serviceName string) (*ServiceHandler, error) {
svcName, err := smartServiceName(serviceName)
if err != nil {
return string(output), fmt.Errorf("failed to run command: %w", err)
return nil, ErrServiceNotFound
}
return string(output), nil
return NewServiceHandler(defaultServiceConfig(svcName)), nil
}
func IsActive(serviceName string) (bool, error) {
out, err := RunSystemCtl("is-active", serviceName)
func GetServiceName(serviceName string) (string, error) {
serviceName, err := smartServiceName(serviceName)
if err != nil {
return false, err
return "", ErrServiceNotFound
}
return out == "active\n", nil
return serviceName, nil
}
func IsEnable(serviceName string) (bool, error) {
out, err := RunSystemCtl("is-enabled", serviceName)
func GetServicePath(serviceName string) (string, error) {
handler, err := DefaultHandler(serviceName)
if err != nil {
return false, err
return "", ErrServiceNotFound
}
return out == "enabled\n", nil
return handler.GetServicePath()
}
func CustomAction(action string, serviceName string) (ServiceResult, error) {
handler, err := DefaultHandler(serviceName)
if err != nil {
global.LOG.Errorf("CustomAction handler init failed: %v", err)
return ServiceResult{}, ErrServiceNotFound
}
result, err := handler.ExecuteAction(action)
if err != nil {
global.LOG.Errorf("CustomAction %s failed: %v", action, err)
return result, fmt.Errorf("%s operation failed: %w | Output: %s", action, err, result.Output)
}
return result, nil
}
func IsExist(serviceName string) (bool, error) {
out, err := RunSystemCtl("is-enabled", serviceName)
handler, err := DefaultHandler(serviceName)
if err != nil {
if strings.Contains(out, "disabled") {
return true, nil
}
return false, nil
}
return true, nil
result, _ := handler.IsExists()
if result.IsExists {
return true, nil
} else {
return false, nil
}
}
func handlerErr(out string, err error) error {
func Start(serviceName string) error {
handler, _ := DefaultHandler(serviceName)
result, err := handler.StartService()
if err != nil {
if out != "" {
return errors.New(out)
}
return err
global.LOG.Errorf("Service start failed: %v | Output: %s", err, result.Output)
return fmt.Errorf("start failed: %v | Output: %s", err, result.Output)
}
return nil
}
func Stop(serviceName string) error {
handler, err := DefaultHandler(serviceName)
if err != nil {
global.LOG.Errorf("Stop handler init failed: %v", err)
return fmt.Errorf("%s is not exist", serviceName)
}
result, err := handler.StopService()
if err != nil {
global.LOG.Errorf("Service stop failed: %v", err)
return fmt.Errorf("stop failed: %v | Output: %s", err, result.Output)
}
return nil
}
func Restart(serviceName string) error {
out, err := RunSystemCtl("restart", serviceName)
return handlerErr(out, err)
handler, err := DefaultHandler(serviceName)
if err != nil {
global.LOG.Errorf("Restart handler init failed: %v", err)
return fmt.Errorf("%s is not exist", serviceName)
}
result, err := handler.RestartService()
if err != nil {
global.LOG.Errorf("Service restart failed: %v", err)
return fmt.Errorf("restart failed: %v | Output: %s", err, result.Output)
}
return nil
}
func Operate(operate, serviceName string) error {
out, err := RunSystemCtl(operate, serviceName)
return handlerErr(out, err)
func Enable(serviceName string) error {
handler, err := DefaultHandler(serviceName)
if err != nil {
global.LOG.Errorf("Enable handler init failed: %v", err)
return fmt.Errorf("%s is not exist", serviceName)
}
result, err := handler.EnableService()
if err != nil {
global.LOG.Errorf("Service enable failed: %v | Output: %s", err, result.Output)
return fmt.Errorf("%s enable failed: %v ", serviceName, err)
}
return nil
}
func Disable(serviceName string) error {
handler, _ := DefaultHandler(serviceName)
result, err := handler.DisableService()
if err != nil {
global.LOG.Errorf("Service disable failed: %v", err)
return fmt.Errorf("disable failed: %v | Output: %s", err, result.Output)
}
return nil
}
func IsActive(serviceName string) (bool, error) {
handler, err := DefaultHandler(serviceName)
if err != nil {
return false, nil
}
status, err := handler.IsActive()
if err != nil {
return false, nil
}
return status.IsActive, nil
}
func IsEnable(serviceName string) (bool, error) {
handler, err := DefaultHandler(serviceName)
if err != nil {
return false, nil
}
status, err := handler.IsEnabled()
if err != nil {
return false, nil
}
return status.IsEnabled, nil
}
type LogOption struct {
TailLines string
}
func FileExist(path string) bool {
_, err := os.Stat(path)
return err == nil
}
+8 -2
View File
@@ -25,8 +25,13 @@ type LocalCommand struct {
pty *os.File
}
func NewCommand(commands []string) (*LocalCommand, error) {
cmd := exec.Command("docker", commands...)
func NewCommand(initCmd []string) (*LocalCommand, error) {
cmd := exec.Command("docker", initCmd...)
if term := os.Getenv("TERM"); term != "" {
cmd.Env = append(os.Environ(), "TERM="+term)
} else {
cmd.Env = append(os.Environ(), "TERM=xterm")
}
pty, err := pty.Start(cmd)
if err != nil {
@@ -90,4 +95,5 @@ func (lcmd *LocalCommand) Wait(quitChan chan bool) {
global.LOG.Errorf("ssh session wait failed, err: %v", err)
setQuit(quitChan)
}
setQuit(quitChan)
}
@@ -53,6 +53,11 @@ func (sws *LocalWsSession) handleSlaveEvent(exitCh chan bool) {
}
func (sws *LocalWsSession) masterWrite(data []byte) error {
defer func() {
if r := recover(); r != nil {
global.LOG.Errorf("A panic occurred during write ws message to master, error message: %v", r)
}
}()
sws.writeMutex.Lock()
defer sws.writeMutex.Unlock()
wsData, err := json.Marshal(WsMsg{
@@ -72,6 +77,7 @@ func (sws *LocalWsSession) masterWrite(data []byte) error {
func (sws *LocalWsSession) receiveWsMsg(exitCh chan bool) {
defer func() {
if r := recover(); r != nil {
setQuit(exitCh)
global.LOG.Errorf("A panic occurred during receive ws message, error message: %v", r)
}
}()
+115 -27
View File
@@ -3,6 +3,7 @@ package toolbox
import (
"fmt"
"os"
"regexp"
"strings"
"github.com/1Panel-dev/1Panel/backend/global"
@@ -22,15 +23,15 @@ type FirewallClient interface {
}
func NewFail2Ban() (*Fail2ban, error) {
isExist, _ := systemctl.IsExist("fail2ban.service")
isExist, _ := systemctl.IsExist("fail2ban")
if isExist {
if _, err := os.Stat(defaultPath); err != nil {
if err := initLocalFile(); err != nil {
return nil, err
}
stdout, err := cmd.Exec("systemctl restart fail2ban.service")
err := systemctl.Restart("fail2ban")
if err != nil {
global.LOG.Errorf("restart fail2ban failed, err: %s", stdout)
global.LOG.Errorf("restart fail2ban failed, err: %s", err)
return nil, err
}
}
@@ -47,20 +48,25 @@ func (f *Fail2ban) Status() (bool, bool, bool) {
}
func (f *Fail2ban) Version() string {
stdout, err := cmd.Exec("fail2ban-client version")
stdout, err := cmd.Exec("fail2ban-client --version")
if err != nil {
global.LOG.Errorf("load the fail2ban version failed, err: %s", stdout)
return "-"
}
return strings.ReplaceAll(stdout, "\n", "")
versionRe := regexp.MustCompile(`(?i)fail2ban[:\s-]*v?(\d+\.\d+\.\d+)`)
matches := versionRe.FindStringSubmatch(stdout)
if len(matches) > 1 {
return matches[1]
}
global.LOG.Errorf("Version regex failed to match output: %s", stdout)
return "-"
}
func (f *Fail2ban) Operate(operate string) error {
switch operate {
case "start", "restart", "stop", "enable", "disable":
stdout, err := cmd.Execf("systemctl %s fail2ban.service", operate)
stdout, err := systemctl.CustomAction(operate, "fail2ban")
if err != nil {
return fmt.Errorf("%s the fail2ban.service failed, err: %s", operate, stdout)
return fmt.Errorf("%s the fail2ban failed, err: %s", operate, stdout.Output)
}
return nil
case "reload":
@@ -136,7 +142,32 @@ func initLocalFile() error {
return err
}
defer f.Close()
initFile := `#DEFAULT-START
var initFile string
if systemctl.GetGlobalManager().Name() == "openrc" {
initFile = `[sshd]
enabled = true
filter = alpine-sshd
port = $ssh_port
logpath = $logpath
maxretry = 2
banaction = $banaction
[sshd-ddos]
enabled = true
filter = alpine-sshd-ddos
port = $ssh_port
logpath = /var/log/messages
maxretry = 2
[sshd-key]
enabled = true
filter = alpine-sshd-key
port = $ssh_port
logpath = /var/log/messages
maxretry = 2
`
} else {
initFile = `# DEFAULT-START
[DEFAULT]
bantime = 600
findtime = 300
@@ -146,36 +177,93 @@ action = %(action_mwl)s
#DEFAULT-END
[sshd]
ignoreip = 127.0.0.1/8
ignoreip = 127.0.0.1/8 ::1
enabled = true
filter = sshd
port = 22
port = $ssh_port
maxretry = 5
findtime = 300
bantime = 600
banaction = $banaction
action = %(action_mwl)s
logpath = $logpath`
banaction := ""
if active, _ := systemctl.IsActive("firewalld"); active {
banaction = "firewallcmd-ipset"
} else if active, _ := systemctl.IsActive("ufw"); active {
banaction = "ufw"
} else {
banaction = "iptables-allports"
logpath = $logpath
maxmatches = 3
`
}
// 检测防火墙类型
banaction := detectFirewall()
// 检测SSH端口(支持Alpine的sshd_config位置)
sshPort := detectSSHPort()
// 检测日志路径(兼容Alpine的多种日志位置)
logPath := detectAuthLogPath()
// 执行变量替换
initFile = strings.ReplaceAll(initFile, "$banaction", banaction)
logPath := ""
if _, err := os.Stat("/var/log/secure"); err == nil {
logPath = "/var/log/secure"
} else {
logPath = "/var/log/auth.log"
}
initFile = strings.ReplaceAll(initFile, "$logpath", logPath)
initFile = strings.ReplaceAll(initFile, "$ssh_port", sshPort)
if err := os.WriteFile(defaultPath, []byte(initFile), 0640); err != nil {
return err
}
return nil
}
func detectFirewall() string {
if active, _ := systemctl.IsActive("firewalld"); active {
return "firewallcmd-ipset"
}
if active, _ := systemctl.IsActive("ufw"); active {
return "ufw"
}
if active, _ := systemctl.IsActive("iptables"); active {
return "iptables-allports"
}
if active, _ := systemctl.IsActive("nftables"); active {
return "nftables-allports"
}
return "iptables-allports"
}
func detectAuthLogPath() string {
paths := []string{
"/var/log/auth.log", // 常见默认路径
"/var/log/messages", // Alpine系统日志
"/var/log/secure", // RHEL风格路径
"/var/log/syslog", // Debian风格路径
}
for _, path := range paths {
if _, err := os.Stat(path); err == nil {
return path
}
}
return "/var/log/auth.log"
}
func detectSSHPort() string {
// 检查标准sshd_config路径
configPaths := []string{
"/etc/ssh/sshd_config",
"/etc/sshd_config",
}
for _, path := range configPaths {
data, err := os.ReadFile(path)
if err != nil {
continue
}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
if strings.HasPrefix(strings.TrimSpace(line), "Port") {
parts := strings.Fields(line)
if len(parts) >= 2 {
return parts[1]
}
}
}
}
return "22" // 默认SSH端口
}
+4 -3
View File
@@ -4,7 +4,6 @@ import (
"bufio"
"errors"
"fmt"
"golang.org/x/crypto/bcrypt"
"os"
"os/user"
"path"
@@ -12,6 +11,8 @@ import (
"strings"
"time"
"golang.org/x/crypto/bcrypt"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
@@ -94,9 +95,9 @@ func (f *Ftp) Status() (bool, bool) {
func (f *Ftp) Operate(operate string) error {
switch operate {
case "start", "restart", "stop":
stdout, err := cmd.Execf("systemctl %s pure-ftpd.service", operate)
stdout, err := systemctl.CustomAction(operate, "pure-ftpd")
if err != nil {
return fmt.Errorf("%s the pure-ftpd.service failed, err: %s", operate, stdout)
return fmt.Errorf("%s the pure-ftpd service failed, err: %s", operate, stdout.Output)
}
return nil
default:
+17 -10
View File
@@ -9,22 +9,29 @@ if [ ! -f "1pctl" ]; then
wget https://github.com/1Panel-dev/installer/raw/main/1pctl
fi
if [ ! -f "1panel.service" ]; then
wget https://github.com/1Panel-dev/installer/raw/main/1panel.service
fi
if [ ! -f "install.sh" ]; then
wget https://github.com/1Panel-dev/installer/raw/main/install.sh
fi
if [ ! -f "GeoIP.mmdb" ]; then
wget https://resource.1panel.pro/geo/GeoIP.mmdb
if [ ! -d "initscript" ]; then
wget https://github.com/1Panel-dev/installer/raw/main/initscript/1panel.service
mkdir -p initscript && cd initscript
for file in 1panel.service 1paneld.init 1paneld.openrc 1paneld.procd; do
wget -q https://github.com/1Panel-dev/installer/raw/main/initscript/$file
done
cd ..
fi
if [ ! -f "lang.tar.gz" ]; then
wget https://resource.1panel.pro/language/lang.tar.gz
tar zxvf lang.tar.gz
rm -rf lang.tar.gz
if [ ! -d "lang" ]; then
mkdir -p lang && cd lang
for lang in en fa pt-BR ru zh; do
wget -q https://github.com/1Panel-dev/installer/raw/main/lang/$lang.sh
done
cd ..
fi
if [ ! -f "GeoIP.mmdb" ]; then
wget https://resource.fit2cloud.com/1panel/package/geo/GeoIP.mmdb
fi
chmod 755 1pctl install.sh
+16 -2
View File
@@ -58,8 +58,15 @@ var restoreCmd = &cobra.Command{
geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo")
_, _ = cmdUtils.Execf("mkdir %s && cp %s %s/", geoPath, path.Join(tmpPath, "GeoIP.mmdb"), geoPath)
fmt.Println(i18n.GetMsgByKeyForCmd("RestoreStep3"))
if err := common.CopyFile(path.Join(tmpPath, "1panel.service"), "/etc/systemd/system"); err != nil {
return err
if isSystemd() {
if err := common.CopyFile(path.Join(tmpPath, "1panel.service"), "/etc/systemd/system"); err != nil {
return err
}
} else {
if err := common.CopyFile(path.Join(tmpPath, "1paneld"), "/etc/init.d"); err != nil {
return err
}
}
fmt.Println(i18n.GetMsgByKeyForCmd("RestoreStep4"))
checkPointOfWal()
@@ -124,3 +131,10 @@ func handleUnTar(sourceFile, targetDir string) error {
}
return nil
}
func isSystemd() bool {
if _, err := os.Stat("/etc/systemd/system"); err == nil {
return true
}
return false
}
+635 -626
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+382 -376
View File
@@ -1578,6 +1578,8 @@ definitions:
type: object
dto.ForwardRuleOperate:
properties:
forceDelete:
type: boolean
rules:
items:
properties:
@@ -2480,6 +2482,8 @@ definitions:
type: object
dto.OllamaBindDomainRes:
properties:
acmeAccountID:
type: integer
allowIPs:
items:
type: string
@@ -3338,6 +3342,8 @@ definitions:
type: string
lastCleanTime:
type: string
licenseVerify:
type: string
localTime:
type: string
menuTabs:
@@ -8585,7 +8591,7 @@ paths:
in: path
name: id
required: true
type: integer
type: string
responses:
"200":
description: OK
@@ -11233,381 +11239,6 @@ paths:
formatEN: update group [name][type]
formatZH: 更新组 [name][type]
paramKeys: []
/host/conffile/update:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SSHConf'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Update host SSH setting by file
tags:
- SSH
x-panel-log:
BeforeFunctions: []
bodyKeys: []
formatEN: update SSH conf
formatZH: 修改 SSH 配置文件
paramKeys: []
/host/ssh/conf:
get:
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Load host SSH conf
tags:
- SSH
/host/ssh/generate:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.GenerateSSH'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Generate host SSH secret
tags:
- SSH
x-panel-log:
BeforeFunctions: []
bodyKeys: []
formatEN: generate SSH secret
formatZH: '生成 SSH 密钥 '
paramKeys: []
/host/ssh/log:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SearchSSHLog'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.SSHLog'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Load host SSH logs
tags:
- SSH
/host/ssh/operate:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.Operate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Operate SSH
tags:
- SSH
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operation
formatEN: '[operation] SSH'
formatZH: '[operation] SSH '
paramKeys: []
/host/ssh/search:
post:
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.SSHInfo'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Load host SSH setting info
tags:
- SSH
/host/ssh/secret:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.GenerateLoad'
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Load host SSH secret
tags:
- SSH
/host/ssh/update:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SSHUpdate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Update host SSH setting
tags:
- SSH
x-panel-log:
BeforeFunctions: []
bodyKeys:
- key
- value
formatEN: update SSH setting [key] => [value]
formatZH: 修改 SSH 配置 [key] => [value]
paramKeys: []
/host/tool:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolReq'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.HostToolRes'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get tool status
tags:
- Host tool
/host/tool/config:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolConfig'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.HostToolConfig'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get tool config
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operate
formatEN: '[operate] tool config'
formatZH: '[operate] 主机工具配置文件 '
paramKeys: []
/host/tool/create:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolCreate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Create Host tool Config
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- type
formatEN: create [type] config
formatZH: 创建 [type] 配置
paramKeys: []
/host/tool/log:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolLogReq'
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get tool logs
tags:
- Host tool
/host/tool/operate:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolReq'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Operate tool
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operate
- type
formatEN: '[operate] [type]'
formatZH: '[operate] [type] '
paramKeys: []
/host/tool/supervisor/process:
get:
consumes:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.SupervisorProcessConfig'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get Supervisor process config
tags:
- Host tool
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.SupervisorProcessConfig'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Create Supervisor process
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operate
formatEN: '[operate] process'
formatZH: '[operate] 守护进程 '
paramKeys: []
/host/tool/supervisor/process/file:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.SupervisorProcessFileReq'
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get Supervisor process config
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operate
formatEN: '[operate] Supervisor Process Config file'
formatZH: '[operate] Supervisor 进程文件 '
paramKeys: []
/hosts:
post:
consumes:
@@ -11874,6 +11505,32 @@ paths:
formatEN: update quick command [name]
formatZH: 更新快捷命令 [name]
paramKeys: []
/hosts/conffile/update:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SSHConf'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Update host SSH setting by file
tags:
- SSH
x-panel-log:
BeforeFunctions: []
bodyKeys: []
formatEN: update SSH conf
formatZH: 修改 SSH 配置文件
paramKeys: []
/hosts/del:
post:
consumes:
@@ -12193,6 +11850,157 @@ paths:
summary: Page host
tags:
- Host
/hosts/ssh/conf:
get:
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Load host SSH conf
tags:
- SSH
/hosts/ssh/generate:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.GenerateSSH'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Generate host SSH secret
tags:
- SSH
x-panel-log:
BeforeFunctions: []
bodyKeys: []
formatEN: generate SSH secret
formatZH: '生成 SSH 密钥 '
paramKeys: []
/hosts/ssh/log:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SearchSSHLog'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.SSHLog'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Load host SSH logs
tags:
- SSH
/hosts/ssh/operate:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.Operate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Operate SSH
tags:
- SSH
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operation
formatEN: '[operation] SSH'
formatZH: '[operation] SSH '
paramKeys: []
/hosts/ssh/search:
post:
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.SSHInfo'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Load host SSH setting info
tags:
- SSH
/hosts/ssh/secret:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.GenerateLoad'
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Load host SSH secret
tags:
- SSH
/hosts/ssh/update:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SSHUpdate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Update host SSH setting
tags:
- SSH
x-panel-log:
BeforeFunctions: []
bodyKeys:
- key
- value
formatEN: update SSH setting [key] => [value]
formatZH: 修改 SSH 配置 [key] => [value]
paramKeys: []
/hosts/test/byid/{id}:
post:
consumes:
@@ -12236,6 +12044,204 @@ paths:
summary: Test host conn by info
tags:
- Host
/hosts/tool:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolReq'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.HostToolRes'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get tool status
tags:
- Host tool
/hosts/tool/config:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolConfig'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.HostToolConfig'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get tool config
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operate
formatEN: '[operate] tool config'
formatZH: '[operate] 主机工具配置文件 '
paramKeys: []
/hosts/tool/create:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolCreate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Create Host tool Config
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- type
formatEN: create [type] config
formatZH: 创建 [type] 配置
paramKeys: []
/hosts/tool/log:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolLogReq'
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get tool logs
tags:
- Host tool
/hosts/tool/operate:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.HostToolReq'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Operate tool
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operate
- type
formatEN: '[operate] [type]'
formatZH: '[operate] [type] '
paramKeys: []
/hosts/tool/supervisor/process:
get:
consumes:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.SupervisorProcessConfig'
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get Supervisor process config
tags:
- Host tool
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.SupervisorProcessConfig'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Create Supervisor process
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operate
formatEN: '[operate] process'
formatZH: '[operate] 守护进程 '
paramKeys: []
/hosts/tool/supervisor/process/file:
post:
consumes:
- application/json
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.SupervisorProcessFileReq'
responses:
"200":
description: OK
schema:
type: string
security:
- ApiKeyAuth: []
- Timestamp: []
summary: Get Supervisor process config
tags:
- Host tool
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operate
formatEN: '[operate] Supervisor Process Config file'
formatZH: '[operate] Supervisor 进程文件 '
paramKeys: []
/hosts/tree:
post:
consumes:

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