2
0
mirror of https://github.com/acepanel/helper.git synced 2026-02-04 08:57:15 +08:00

feat: init

This commit is contained in:
2026-01-17 22:58:56 +08:00
commit 3799eaae4b
34 changed files with 3745 additions and 0 deletions

305
internal/system/detector.go Normal file
View 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
}

View 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()
}

View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,11 @@
package system
import "github.com/google/wire"
var ProviderSet = wire.NewSet(
NewExecutor,
NewDetector,
NewFirewall,
NewSystemd,
NewUserManager,
)