mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-10-12 16:36:12 +08:00
Some checks failed
SonarCloud Scan / SonarCloud (push) Has been cancelled
* 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>
461 lines
12 KiB
Go
461 lines
12 KiB
Go
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 = 30 * 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 {
|
|
// global.LOG.Warnf("Service existence check failed %s: %v", b.name, checkErr)
|
|
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 nil, &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, "list-unit-files", name)
|
|
if cmdErr != nil {
|
|
return false, fmt.Errorf("systemctl list-unit-files failed: %w", cmdErr)
|
|
}
|
|
return bytes.Contains(out, []byte(name)), 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
|
|
}
|
|
// 关键逻辑:如果 find 命令有输出(找到符号链接),则服务已启用
|
|
return strings.TrimSpace(output) != "", nil
|
|
case "active":
|
|
// 关键逻辑:如果输出包含 "running" 或 "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)
|
|
}
|
|
|
|
func (e CommandError) Unwrap() error { return e.Err }
|
|
|
|
// ReinitializeManager for test
|
|
func ReinitializeManager() error {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
// initOnce = sync.Once{}
|
|
globalManager = nil
|
|
return InitializeGlobalManager()
|
|
}
|
|
|
|
// SetManagerPriority for test
|
|
func SetManagerPriority(order []string) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
managerPriority = order
|
|
}
|