mirror of
https://github.com/acepanel/helper.git
synced 2026-02-04 08:57:15 +08:00
feat: init
This commit is contained in:
305
internal/system/detector.go
Normal file
305
internal/system/detector.go
Normal file
@@ -0,0 +1,305 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/user"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/acepanel/helper/pkg/i18n"
|
||||
"github.com/acepanel/helper/pkg/types"
|
||||
)
|
||||
|
||||
// Detector 系统检测器接口
|
||||
type Detector interface {
|
||||
// Detect 检测系统信息
|
||||
Detect(ctx context.Context) (*types.SystemInfo, error)
|
||||
// CheckRoot 检查root权限
|
||||
CheckRoot() error
|
||||
// CheckCPUFeatures 检查CPU特性(x86-64-v2)
|
||||
CheckCPUFeatures(ctx context.Context) error
|
||||
// CheckPanelInstalled 检查面板是否已安装
|
||||
CheckPanelInstalled(path string) bool
|
||||
// ListDisks 列出可用磁盘
|
||||
ListDisks(ctx context.Context) ([]types.DiskInfo, error)
|
||||
// CheckDiskExists 检查磁盘是否存在
|
||||
CheckDiskExists(disk string) bool
|
||||
// IsSystemDisk 检查是否为系统盘
|
||||
IsSystemDisk(disk string) bool
|
||||
}
|
||||
|
||||
type detector struct {
|
||||
executor Executor
|
||||
}
|
||||
|
||||
// NewDetector 创建检测器
|
||||
func NewDetector(executor Executor) Detector {
|
||||
return &detector{executor: executor}
|
||||
}
|
||||
|
||||
func (d *detector) Detect(ctx context.Context) (*types.SystemInfo, error) {
|
||||
info := &types.SystemInfo{}
|
||||
|
||||
// 检测OS类型
|
||||
info.OS = d.detectOS()
|
||||
// 检测架构
|
||||
info.Arch = d.detectArch()
|
||||
// 检测内核版本
|
||||
info.KernelVersion = d.detectKernelVersion(ctx)
|
||||
// 检测是否64位
|
||||
info.Is64Bit = d.detect64Bit(ctx)
|
||||
// 检测内存
|
||||
info.Memory = d.detectMemory(ctx)
|
||||
// 检测Swap
|
||||
info.Swap = d.detectSwap(ctx)
|
||||
// 检测是否在中国
|
||||
info.InChina = d.detectInChina(ctx)
|
||||
// 检测SSH端口
|
||||
info.SSHPort = d.detectSSHPort()
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (d *detector) detectOS() types.OSType {
|
||||
// 读取 /etc/os-release
|
||||
file, err := os.Open("/etc/os-release")
|
||||
if err != nil {
|
||||
return types.OSUnknown
|
||||
}
|
||||
defer func(file *os.File) { _ = file.Close() }(file)
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
var id string
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "ID=") {
|
||||
id = strings.Trim(strings.TrimPrefix(line, "ID="), "\"")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
switch id {
|
||||
case "debian":
|
||||
return types.OSDebian
|
||||
case "ubuntu":
|
||||
return types.OSUbuntu
|
||||
case "rhel", "centos", "rocky", "almalinux", "fedora":
|
||||
return types.OSRHEL
|
||||
default:
|
||||
// 检查ID_LIKE
|
||||
_, _ = file.Seek(0, 0)
|
||||
scanner = bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "ID_LIKE=") {
|
||||
idLike := strings.Trim(strings.TrimPrefix(line, "ID_LIKE="), "\"")
|
||||
if strings.Contains(idLike, "debian") {
|
||||
return types.OSDebian
|
||||
}
|
||||
if strings.Contains(idLike, "rhel") || strings.Contains(idLike, "fedora") {
|
||||
return types.OSRHEL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return types.OSUnknown
|
||||
}
|
||||
|
||||
func (d *detector) detectArch() types.ArchType {
|
||||
arch := runtime.GOARCH
|
||||
switch arch {
|
||||
case "amd64":
|
||||
return types.ArchAMD64
|
||||
case "arm64":
|
||||
return types.ArchARM64
|
||||
default:
|
||||
return types.ArchUnknown
|
||||
}
|
||||
}
|
||||
|
||||
func (d *detector) detectKernelVersion(ctx context.Context) string {
|
||||
result, err := d.executor.Run(ctx, "uname", "-r")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(result.Stdout)
|
||||
}
|
||||
|
||||
func (d *detector) detect64Bit(ctx context.Context) bool {
|
||||
result, err := d.executor.Run(ctx, "getconf", "LONG_BIT")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(result.Stdout) == "64"
|
||||
}
|
||||
|
||||
func (d *detector) detectMemory(ctx context.Context) int64 {
|
||||
data, err := os.ReadFile("/proc/meminfo")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "MemTotal:") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 {
|
||||
kb, _ := strconv.ParseInt(fields[1], 10, 64)
|
||||
return kb / 1024 // 转换为MB
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (d *detector) detectSwap(ctx context.Context) int64 {
|
||||
data, err := os.ReadFile("/proc/meminfo")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "SwapTotal:") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 {
|
||||
kb, _ := strconv.ParseInt(fields[1], 10, 64)
|
||||
return kb / 1024 // 转换为MB
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (d *detector) detectInChina(ctx context.Context) bool {
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://perfops.cloudflareperf.com/cdn-cgi/trace", nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body)
|
||||
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
for scanner.Scan() {
|
||||
if scanner.Text() == "loc=CN" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *detector) detectSSHPort() int {
|
||||
file, err := os.Open("/etc/ssh/sshd_config")
|
||||
if err != nil {
|
||||
return 22
|
||||
}
|
||||
defer func(file *os.File) { _ = file.Close() }(file)
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if strings.HasPrefix(line, "Port ") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 {
|
||||
port, err := strconv.Atoi(fields[1])
|
||||
if err == nil {
|
||||
return port
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 22
|
||||
}
|
||||
|
||||
func (d *detector) CheckRoot() error {
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if currentUser.Uid != "0" {
|
||||
return errors.New(i18n.T().Get("Please run with root privileges"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *detector) CheckCPUFeatures(ctx context.Context) error {
|
||||
// 只有x86_64需要检查
|
||||
if runtime.GOARCH != "amd64" {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile("/proc/cpuinfo")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查是否支持ssse3 (x86-64-v2的标志之一)
|
||||
if !strings.Contains(string(data), "ssse3") {
|
||||
return errors.New(i18n.T().Get("CPU must support at least x86-64-v2 instruction set"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *detector) CheckPanelInstalled(path string) bool {
|
||||
_, err := os.Stat(path + "/panel/web")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (d *detector) ListDisks(ctx context.Context) ([]types.DiskInfo, error) {
|
||||
result, err := d.executor.Run(ctx, "lsblk", "-dno", "NAME,SIZE,TYPE")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var disks []types.DiskInfo
|
||||
lines := strings.Split(strings.TrimSpace(result.Stdout), "\n")
|
||||
for _, line := range lines {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 3 && fields[2] == "disk" {
|
||||
// 排除系统盘
|
||||
if d.IsSystemDisk(fields[0]) {
|
||||
continue
|
||||
}
|
||||
disks = append(disks, types.DiskInfo{
|
||||
Name: fields[0],
|
||||
Size: fields[1],
|
||||
Type: fields[2],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return disks, nil
|
||||
}
|
||||
|
||||
func (d *detector) CheckDiskExists(disk string) bool {
|
||||
_, err := os.Stat("/dev/" + disk)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (d *detector) IsSystemDisk(disk string) bool {
|
||||
// 系统盘通常以a结尾 (sda, vda, nvme0n1)
|
||||
matched, _ := regexp.MatchString(`^(sd|vd|hd)a$`, disk)
|
||||
if matched {
|
||||
return true
|
||||
}
|
||||
matched, _ = regexp.MatchString(`^nvme0n1$`, disk)
|
||||
return matched
|
||||
}
|
||||
87
internal/system/executor.go
Normal file
87
internal/system/executor.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// CommandResult 命令执行结果
|
||||
type CommandResult struct {
|
||||
ExitCode int
|
||||
Stdout string
|
||||
Stderr string
|
||||
}
|
||||
|
||||
// Executor 命令执行器接口
|
||||
type Executor interface {
|
||||
// Run 执行命令并等待完成
|
||||
Run(ctx context.Context, name string, args ...string) (*CommandResult, error)
|
||||
// RunWithInput 执行命令并提供输入
|
||||
RunWithInput(ctx context.Context, input string, name string, args ...string) (*CommandResult, error)
|
||||
// RunStream 执行命令并流式输出
|
||||
RunStream(ctx context.Context, stdout, stderr io.Writer, name string, args ...string) error
|
||||
}
|
||||
|
||||
type executor struct{}
|
||||
|
||||
// NewExecutor 创建执行器
|
||||
func NewExecutor() Executor {
|
||||
return &executor{}
|
||||
}
|
||||
|
||||
func (e *executor) Run(ctx context.Context, name string, args ...string) (*CommandResult, error) {
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err := cmd.Run()
|
||||
result := &CommandResult{
|
||||
Stdout: stdout.String(),
|
||||
Stderr: stderr.String(),
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
result.ExitCode = exitErr.ExitCode()
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (e *executor) RunWithInput(ctx context.Context, input string, name string, args ...string) (*CommandResult, error) {
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
cmd.Stdin = bytes.NewBufferString(input)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err := cmd.Run()
|
||||
result := &CommandResult{
|
||||
Stdout: stdout.String(),
|
||||
Stderr: stderr.String(),
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
result.ExitCode = exitErr.ExitCode()
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (e *executor) RunStream(ctx context.Context, stdout, stderr io.Writer, name string, args ...string) error {
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
cmd.Stdout = stdout
|
||||
cmd.Stderr = stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
96
internal/system/firewall.go
Normal file
96
internal/system/firewall.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/acepanel/helper/pkg/i18n"
|
||||
)
|
||||
|
||||
// Firewall 防火墙接口
|
||||
type Firewall interface {
|
||||
// Install 安装防火墙
|
||||
Install(ctx context.Context) error
|
||||
// Enable 启用防火墙
|
||||
Enable(ctx context.Context) error
|
||||
// AddPort 添加端口
|
||||
AddPort(ctx context.Context, port int, protocol string) error
|
||||
// RemovePort 移除端口
|
||||
RemovePort(ctx context.Context, port int, protocol string) error
|
||||
// Reload 重载配置
|
||||
Reload(ctx context.Context) error
|
||||
}
|
||||
|
||||
type firewall struct {
|
||||
executor Executor
|
||||
detector Detector
|
||||
}
|
||||
|
||||
// NewFirewall 创建防火墙管理器
|
||||
func NewFirewall(executor Executor, detector Detector) Firewall {
|
||||
return &firewall{
|
||||
executor: executor,
|
||||
detector: detector,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *firewall) Install(ctx context.Context) error {
|
||||
info, err := f.detector.Detect(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pkgMgr := NewPackageManager(info.OS, f.executor)
|
||||
if pkgMgr == nil {
|
||||
return fmt.Errorf("%s", i18n.T().Get("Unsupported operating system"))
|
||||
}
|
||||
return pkgMgr.Install(ctx, "firewalld")
|
||||
}
|
||||
|
||||
func (f *firewall) Enable(ctx context.Context) error {
|
||||
result, err := f.executor.Run(ctx, "systemctl", "enable", "--now", "firewalld")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("%s: %s", i18n.T().Get("Failed to enable firewalld"), result.Stderr)
|
||||
}
|
||||
|
||||
// 设置默认zone
|
||||
_, err = f.executor.Run(ctx, "firewall-cmd", "--set-default-zone=public")
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *firewall) AddPort(ctx context.Context, port int, protocol string) error {
|
||||
portStr := fmt.Sprintf("%d/%s", port, protocol)
|
||||
result, err := f.executor.Run(ctx, "firewall-cmd", "--permanent", "--zone=public", "--add-port="+portStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("%s %s: %s", i18n.T().Get("Failed to add port"), portStr, result.Stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *firewall) RemovePort(ctx context.Context, port int, protocol string) error {
|
||||
portStr := fmt.Sprintf("%d/%s", port, protocol)
|
||||
result, err := f.executor.Run(ctx, "firewall-cmd", "--permanent", "--zone=public", "--remove-port="+portStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("%s %s: %s", i18n.T().Get("Failed to remove port"), portStr, result.Stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *firewall) Reload(ctx context.Context) error {
|
||||
result, err := f.executor.Run(ctx, "firewall-cmd", "--reload")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("%s: %s", i18n.T().Get("Failed to reload firewall"), result.Stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
229
internal/system/package.go
Normal file
229
internal/system/package.go
Normal file
@@ -0,0 +1,229 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/acepanel/helper/pkg/i18n"
|
||||
"github.com/acepanel/helper/pkg/types"
|
||||
)
|
||||
|
||||
// PackageManager 包管理器接口
|
||||
type PackageManager interface {
|
||||
// UpdateCache 更新软件源缓存
|
||||
UpdateCache(ctx context.Context) error
|
||||
// Install 安装软件包
|
||||
Install(ctx context.Context, packages ...string) error
|
||||
// Remove 移除软件包
|
||||
Remove(ctx context.Context, packages ...string) error
|
||||
// IsInstalled 检查是否已安装
|
||||
IsInstalled(ctx context.Context, pkg string) bool
|
||||
// SetMirror 设置镜像源
|
||||
SetMirror(ctx context.Context, inChina bool) error
|
||||
// EnableEPEL 启用EPEL源 (仅RHEL系)
|
||||
EnableEPEL(ctx context.Context, inChina bool) error
|
||||
}
|
||||
|
||||
// NewPackageManager 根据OS类型创建包管理器
|
||||
func NewPackageManager(osType types.OSType, executor Executor) PackageManager {
|
||||
switch osType {
|
||||
case types.OSRHEL:
|
||||
return &dnfManager{executor: executor}
|
||||
case types.OSDebian, types.OSUbuntu:
|
||||
return &aptManager{executor: executor, osType: osType}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// dnfManager DNF包管理器 (RHEL系)
|
||||
type dnfManager struct {
|
||||
executor Executor
|
||||
}
|
||||
|
||||
func (m *dnfManager) UpdateCache(ctx context.Context) error {
|
||||
result, err := m.executor.Run(ctx, "dnf", "makecache", "-y")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("%s: %s", i18n.T().Get("dnf makecache failed"), result.Stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *dnfManager) Install(ctx context.Context, packages ...string) error {
|
||||
args := append([]string{"install", "-y"}, packages...)
|
||||
result, err := m.executor.Run(ctx, "dnf", args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("%s: %s", i18n.T().Get("dnf install failed"), result.Stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *dnfManager) Remove(ctx context.Context, packages ...string) error {
|
||||
args := append([]string{"remove", "-y"}, packages...)
|
||||
result, err := m.executor.Run(ctx, "dnf", args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("%s: %s", i18n.T().Get("dnf remove failed"), result.Stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *dnfManager) IsInstalled(ctx context.Context, pkg string) bool {
|
||||
result, _ := m.executor.Run(ctx, "rpm", "-q", pkg)
|
||||
return result != nil && result.ExitCode == 0
|
||||
}
|
||||
|
||||
func (m *dnfManager) SetMirror(ctx context.Context, inChina bool) error {
|
||||
if !inChina {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Rocky Linux
|
||||
m.sedReplace("/etc/yum.repos.d/[Rr]ocky*.repo",
|
||||
"s|^mirrorlist=|#mirrorlist=|g",
|
||||
"s|^#baseurl=http://dl.rockylinux.org/$contentdir|baseurl=https://mirrors.tencent.com/rocky|g",
|
||||
)
|
||||
|
||||
// AlmaLinux
|
||||
m.sedReplace("/etc/yum.repos.d/[Aa]lmalinux*.repo",
|
||||
"s|^mirrorlist=|#mirrorlist=|g",
|
||||
"s|^#baseurl=https://repo.almalinux.org|baseurl=https://mirrors.tencent.com|g",
|
||||
)
|
||||
|
||||
// CentOS Stream
|
||||
m.sedReplace("/etc/yum.repos.d/[Cc]ent*.repo",
|
||||
"s|^mirrorlist=|#mirrorlist=|g",
|
||||
"s|^#baseurl=http://mirror.centos.org/$contentdir|baseurl=https://mirrors.tencent.com/centos-stream|g",
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *dnfManager) EnableEPEL(ctx context.Context, inChina bool) error {
|
||||
// 启用CRB
|
||||
_, _ = m.executor.Run(ctx, "dnf", "config-manager", "--set-enabled", "crb")
|
||||
_, _ = m.executor.Run(ctx, "/usr/bin/crb", "enable")
|
||||
|
||||
// 安装EPEL
|
||||
result, _ := m.executor.Run(ctx, "dnf", "install", "-y", "epel-release")
|
||||
if result == nil || result.ExitCode != 0 {
|
||||
// 手动安装
|
||||
var url string
|
||||
if inChina {
|
||||
url = "https://mirrors.tencent.com/epel/epel-release-latest-9.noarch.rpm"
|
||||
} else {
|
||||
url = "https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm"
|
||||
}
|
||||
_, _ = m.executor.Run(ctx, "dnf", "install", "-y", url)
|
||||
}
|
||||
|
||||
if inChina {
|
||||
// 删除无镜像的repo
|
||||
_ = os.Remove("/etc/yum.repos.d/epel-cisco-openh264.repo")
|
||||
// 设置EPEL镜像
|
||||
m.sedReplace("/etc/yum.repos.d/epel*.repo",
|
||||
"s|^#baseurl=https://download.example/pub|baseurl=https://mirrors.tencent.com|g",
|
||||
"s|^metalink|#metalink|g",
|
||||
)
|
||||
}
|
||||
|
||||
_, _ = m.executor.Run(ctx, "dnf", "config-manager", "--set-enabled", "epel")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *dnfManager) sedReplace(filePattern string, expressions ...string) {
|
||||
for _, expr := range expressions {
|
||||
_, _ = m.executor.Run(context.Background(), "sed", "-i", expr, filePattern)
|
||||
}
|
||||
}
|
||||
|
||||
// aptManager APT包管理器 (Debian/Ubuntu)
|
||||
type aptManager struct {
|
||||
executor Executor
|
||||
osType types.OSType
|
||||
}
|
||||
|
||||
func (m *aptManager) UpdateCache(ctx context.Context) error {
|
||||
result, err := m.executor.Run(ctx, "apt-get", "update", "-y")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("%s: %s", i18n.T().Get("apt-get update failed"), result.Stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *aptManager) Install(ctx context.Context, packages ...string) error {
|
||||
args := append([]string{"install", "-y"}, packages...)
|
||||
result, err := m.executor.Run(ctx, "apt-get", args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("%s: %s", i18n.T().Get("apt-get install failed"), result.Stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *aptManager) Remove(ctx context.Context, packages ...string) error {
|
||||
args := append([]string{"purge", "--auto-remove", "-y"}, packages...)
|
||||
result, err := m.executor.Run(ctx, "apt-get", args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("%s: %s", i18n.T().Get("apt-get remove failed"), result.Stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *aptManager) IsInstalled(ctx context.Context, pkg string) bool {
|
||||
result, _ := m.executor.Run(ctx, "dpkg", "-s", pkg)
|
||||
return result != nil && result.ExitCode == 0 && strings.Contains(result.Stdout, "Status: install ok installed")
|
||||
}
|
||||
|
||||
func (m *aptManager) SetMirror(ctx context.Context, inChina bool) error {
|
||||
if !inChina {
|
||||
return nil
|
||||
}
|
||||
|
||||
if m.osType == types.OSDebian {
|
||||
// Debian
|
||||
m.sedReplace("/etc/apt/sources.list", "s/deb.debian.org/mirrors.tencent.com/g")
|
||||
m.sedReplace("/etc/apt/sources.list.d/debian.sources", "s/deb.debian.org/mirrors.tencent.com/g")
|
||||
m.sedReplace("/etc/apt/sources.list",
|
||||
"s|security.debian.org/\\? |security.debian.org/debian-security |g",
|
||||
"s|security.debian.org|mirrors.tencent.com|g",
|
||||
)
|
||||
} else {
|
||||
// Ubuntu
|
||||
m.sedReplace("/etc/apt/sources.list", "s@//.*archive.ubuntu.com@//mirrors.tencent.com@g")
|
||||
m.sedReplace("/etc/apt/sources.list.d/ubuntu.sources", "s@//.*archive.ubuntu.com@//mirrors.tencent.com@g")
|
||||
m.sedReplace("/etc/apt/sources.list", "s/security.ubuntu.com/mirrors.tencent.com/g")
|
||||
m.sedReplace("/etc/apt/sources.list.d/ubuntu.sources", "s/security.ubuntu.com/mirrors.tencent.com/g")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *aptManager) EnableEPEL(ctx context.Context, inChina bool) error {
|
||||
// APT不需要EPEL
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *aptManager) sedReplace(file string, expressions ...string) {
|
||||
for _, expr := range expressions {
|
||||
_, _ = m.executor.Run(context.Background(), "sed", "-i", expr, file)
|
||||
}
|
||||
}
|
||||
122
internal/system/systemd.go
Normal file
122
internal/system/systemd.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/acepanel/helper/pkg/i18n"
|
||||
)
|
||||
|
||||
// Systemd systemd服务管理接口
|
||||
type Systemd interface {
|
||||
// Start 启动服务
|
||||
Start(ctx context.Context, service string) error
|
||||
// Stop 停止服务
|
||||
Stop(ctx context.Context, service string) error
|
||||
// Enable 启用服务
|
||||
Enable(ctx context.Context, service string) error
|
||||
// Disable 禁用服务
|
||||
Disable(ctx context.Context, service string) error
|
||||
// Restart 重启服务
|
||||
Restart(ctx context.Context, service string) error
|
||||
// IsActive 检查服务是否运行
|
||||
IsActive(ctx context.Context, service string) bool
|
||||
// DaemonReload 重载systemd配置
|
||||
DaemonReload(ctx context.Context) error
|
||||
// WriteServiceFile 写入服务文件
|
||||
WriteServiceFile(name string, content string) error
|
||||
// RemoveServiceFile 删除服务文件
|
||||
RemoveServiceFile(name string) error
|
||||
}
|
||||
|
||||
type systemd struct {
|
||||
executor Executor
|
||||
}
|
||||
|
||||
// NewSystemd 创建systemd管理器
|
||||
func NewSystemd(executor Executor) Systemd {
|
||||
return &systemd{executor: executor}
|
||||
}
|
||||
|
||||
func (s *systemd) Start(ctx context.Context, service string) error {
|
||||
result, err := s.executor.Run(ctx, "systemctl", "start", service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("%s %s: %s", i18n.T().Get("Failed to start"), service, result.Stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *systemd) Stop(ctx context.Context, service string) error {
|
||||
result, err := s.executor.Run(ctx, "systemctl", "stop", service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("%s %s: %s", i18n.T().Get("Failed to stop"), service, result.Stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *systemd) Enable(ctx context.Context, service string) error {
|
||||
result, err := s.executor.Run(ctx, "systemctl", "enable", "--now", service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("%s %s: %s", i18n.T().Get("Failed to enable"), service, result.Stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *systemd) Disable(ctx context.Context, service string) error {
|
||||
result, err := s.executor.Run(ctx, "systemctl", "disable", service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("%s %s: %s", i18n.T().Get("Failed to disable"), service, result.Stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *systemd) Restart(ctx context.Context, service string) error {
|
||||
result, err := s.executor.Run(ctx, "systemctl", "restart", service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("%s %s: %s", i18n.T().Get("Failed to restart"), service, result.Stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *systemd) IsActive(ctx context.Context, service string) bool {
|
||||
result, _ := s.executor.Run(ctx, "systemctl", "is-active", service)
|
||||
return result != nil && strings.TrimSpace(result.Stdout) == "active"
|
||||
}
|
||||
|
||||
func (s *systemd) DaemonReload(ctx context.Context) error {
|
||||
result, err := s.executor.Run(ctx, "systemctl", "daemon-reload")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("%s: %s", i18n.T().Get("Failed to daemon-reload"), result.Stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *systemd) WriteServiceFile(name string, content string) error {
|
||||
path := fmt.Sprintf("/etc/systemd/system/%s.service", name)
|
||||
return os.WriteFile(path, []byte(content), 0644)
|
||||
}
|
||||
|
||||
func (s *systemd) RemoveServiceFile(name string) error {
|
||||
path := fmt.Sprintf("/etc/systemd/system/%s.service", name)
|
||||
return os.Remove(path)
|
||||
}
|
||||
87
internal/system/user.go
Normal file
87
internal/system/user.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/acepanel/helper/pkg/i18n"
|
||||
)
|
||||
|
||||
// UserManager 用户管理接口
|
||||
type UserManager interface {
|
||||
// UserExists 检查用户是否存在
|
||||
UserExists(ctx context.Context, username string) bool
|
||||
// GroupExists 检查组是否存在
|
||||
GroupExists(ctx context.Context, groupname string) bool
|
||||
// CreateUser 创建用户
|
||||
CreateUser(ctx context.Context, username, groupname string, nologin bool) error
|
||||
// CreateGroup 创建组
|
||||
CreateGroup(ctx context.Context, groupname string) error
|
||||
// EnsureUserAndGroup 确保用户和组存在
|
||||
EnsureUserAndGroup(ctx context.Context, username, groupname string) error
|
||||
}
|
||||
|
||||
type userManager struct {
|
||||
executor Executor
|
||||
}
|
||||
|
||||
// NewUserManager 创建用户管理器
|
||||
func NewUserManager(executor Executor) UserManager {
|
||||
return &userManager{executor: executor}
|
||||
}
|
||||
|
||||
func (u *userManager) UserExists(ctx context.Context, username string) bool {
|
||||
result, _ := u.executor.Run(ctx, "id", "-u", username)
|
||||
return result != nil && result.ExitCode == 0
|
||||
}
|
||||
|
||||
func (u *userManager) GroupExists(ctx context.Context, groupname string) bool {
|
||||
result, _ := u.executor.Run(ctx, "getent", "group", groupname)
|
||||
return result != nil && result.ExitCode == 0
|
||||
}
|
||||
|
||||
func (u *userManager) CreateUser(ctx context.Context, username, groupname string, nologin bool) error {
|
||||
args := []string{"-g", groupname}
|
||||
if nologin {
|
||||
args = append(args, "-s", "/sbin/nologin")
|
||||
}
|
||||
args = append(args, username)
|
||||
|
||||
result, err := u.executor.Run(ctx, "useradd", args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("%s %s: %s", i18n.T().Get("Failed to create user"), username, result.Stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *userManager) CreateGroup(ctx context.Context, groupname string) error {
|
||||
result, err := u.executor.Run(ctx, "groupadd", groupname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.ExitCode != 0 {
|
||||
return fmt.Errorf("%s %s: %s", i18n.T().Get("Failed to create group"), groupname, result.Stderr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *userManager) EnsureUserAndGroup(ctx context.Context, username, groupname string) error {
|
||||
// 确保组存在
|
||||
if !u.GroupExists(ctx, groupname) {
|
||||
if err := u.CreateGroup(ctx, groupname); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 确保用户存在
|
||||
if !u.UserExists(ctx, username) {
|
||||
if err := u.CreateUser(ctx, username, groupname, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
11
internal/system/wire.go
Normal file
11
internal/system/wire.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package system
|
||||
|
||||
import "github.com/google/wire"
|
||||
|
||||
var ProviderSet = wire.NewSet(
|
||||
NewExecutor,
|
||||
NewDetector,
|
||||
NewFirewall,
|
||||
NewSystemd,
|
||||
NewUserManager,
|
||||
)
|
||||
Reference in New Issue
Block a user