mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-16 16:00:42 +00:00
* fix(dashboard): keep disk info working when a mount is stale or broken The dashboard, the file manager mount list and the alert disk list enumerated mounts with df. df calls statfs on every mount, so one bad mount took the whole disk list down: the request hung until the frontend timed out, or the list came back empty. - enumerate mounts from /proc/self/mountinfo instead of running df - read each mount through a guard with a 3s timeout that keeps at most one call per mount in flight - fail closed: a mount that cannot be read stays in the list with errorType and error, and an unreadable mount table is an error rather than an empty list - show failed mounts and their reasons on the dashboard and in the file manager - cmd: after killing a timed-out command, wait at most 5s for it instead of forever * fix(dashboard): distinguish remounted and hidden filesystems * fix(disk): simplify disk queries and bound timeout handling * fix(disk): prevent overlapping df collection tasks --------- Co-authored-by: ssongliu <sloooop1x@gmail.com>
465 lines
12 KiB
Go
465 lines
12 KiB
Go
package service
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
|
"github.com/1Panel-dev/1Panel/agent/global"
|
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/controller"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/ntp"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/psutil"
|
|
"github.com/shirou/gopsutil/v4/mem"
|
|
)
|
|
|
|
const defaultDNSPath = "/etc/resolv.conf"
|
|
const defaultHostPath = "/etc/hosts"
|
|
const defaultFstab = "/etc/fstab"
|
|
|
|
type DeviceService struct{}
|
|
|
|
type IDeviceService interface {
|
|
LoadBaseInfo() (dto.DeviceBaseInfo, error)
|
|
Update(key, value string) error
|
|
UpdateHosts(req []dto.HostHelper) error
|
|
UpdatePasswd(req dto.ChangePasswd) error
|
|
UpdateSwap(req dto.SwapHelper) error
|
|
UpdateByConf(req dto.UpdateByNameAndFile) error
|
|
LoadTimeZone() ([]string, error)
|
|
CheckDNS(key, value string) (bool, error)
|
|
LoadConf(name string) (string, error)
|
|
LoadUsers() ([]string, error)
|
|
|
|
Scan() dto.CleanData
|
|
Clean(req []dto.Clean)
|
|
}
|
|
|
|
func NewIDeviceService() IDeviceService {
|
|
return &DeviceService{}
|
|
}
|
|
|
|
func (u *DeviceService) LoadBaseInfo() (dto.DeviceBaseInfo, error) {
|
|
var baseInfo dto.DeviceBaseInfo
|
|
baseInfo.LocalTime = time.Now().Format("2006-01-02 15:04:05 MST -0700")
|
|
baseInfo.TimeZone = common.LoadTimeZoneByCmd()
|
|
baseInfo.DNS = loadDNS()
|
|
baseInfo.Hosts = loadHosts()
|
|
baseInfo.Hostname = loadHostname()
|
|
baseInfo.User = loadUser()
|
|
ntp, err := settingRepo.Get(settingRepo.WithByKey("NtpSite"))
|
|
if err == nil {
|
|
baseInfo.Ntp = ntp.Value
|
|
}
|
|
|
|
swapInfo, err := mem.SwapMemory()
|
|
if err != nil {
|
|
return baseInfo, err
|
|
}
|
|
baseInfo.SwapMemoryTotal = swapInfo.Total
|
|
baseInfo.SwapMemoryAvailable = swapInfo.Free
|
|
baseInfo.SwapMemoryUsed = swapInfo.Used
|
|
if baseInfo.SwapMemoryTotal != 0 {
|
|
baseInfo.SwapDetails = loadSwap()
|
|
}
|
|
disks := loadDiskInfo(false)
|
|
for _, item := range disks {
|
|
baseInfo.MaxSize += item.Free
|
|
}
|
|
|
|
return baseInfo, nil
|
|
}
|
|
|
|
func (u *DeviceService) LoadTimeZone() ([]string, error) {
|
|
std, err := cmd.NewCommandMgr(cmd.WithTimeout(10*time.Minute)).RunWithStdout("timedatectl", "list-timezones")
|
|
if err != nil {
|
|
return []string{}, err
|
|
}
|
|
return strings.Split(std, "\n"), nil
|
|
}
|
|
|
|
func (u *DeviceService) CheckDNS(key, value string) (bool, error) {
|
|
content, err := os.ReadFile(defaultDNSPath)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer func() { _ = u.UpdateByConf(dto.UpdateByNameAndFile{Name: "DNS", File: string(content)}) }()
|
|
if key == "form" {
|
|
if err := u.Update("DNS", value); err != nil {
|
|
return false, err
|
|
}
|
|
} else {
|
|
if err := u.UpdateByConf(dto.UpdateByNameAndFile{Name: "DNS", File: value}); err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
conn, err := net.DialTimeout("ip4:icmp", "www.baidu.com", time.Second*2)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer func() { _ = conn.Close() }()
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (u *DeviceService) Update(key, value string) error {
|
|
switch key {
|
|
case "TimeZone":
|
|
if cmd.CheckIllegal(value) {
|
|
return buserr.New("ErrCmdIllegal")
|
|
}
|
|
if err := ntp.UpdateSystemTimeZone(value); err != nil {
|
|
return err
|
|
}
|
|
go controller.RestartPanel(global.IsMaster, true, false)
|
|
case "DNS":
|
|
if err := updateDNS(strings.Split(value, ",")); err != nil {
|
|
return err
|
|
}
|
|
case "Hostname":
|
|
if cmd.CheckIllegal(value) {
|
|
return buserr.New("ErrCmdIllegal")
|
|
}
|
|
if err := cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).Run("hostnamectl", "set-hostname", value); err != nil {
|
|
return err
|
|
}
|
|
_, _ = psutil.HOST.GetHostInfo(true)
|
|
case "Ntp", "LocalTime":
|
|
if cmd.CheckIllegal(value) {
|
|
return buserr.New("ErrCmdIllegal")
|
|
}
|
|
ntpValue := value
|
|
if key == "LocalTime" {
|
|
ntpItem, err := settingRepo.Get(settingRepo.WithByKey("NtpSite"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ntpValue = ntpItem.Value
|
|
} else {
|
|
if err := settingRepo.Update("NtpSite", ntpValue); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
ntime, err := ntp.GetRemoteTime(ntpValue)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ts := ntime.Format(constant.DateTimeLayout)
|
|
if err := ntp.UpdateSystemTime(ts); err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return fmt.Errorf("not support such key %s", key)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (u *DeviceService) LoadUsers() ([]string, error) {
|
|
file, err := os.ReadFile("/etc/passwd")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var users []string
|
|
lines := strings.Split(string(file), "\n")
|
|
for _, line := range lines {
|
|
if strings.Contains(line, ":") {
|
|
users = append(users, strings.Split(line, ":")[0])
|
|
}
|
|
}
|
|
return users, nil
|
|
}
|
|
|
|
func (u *DeviceService) UpdateHosts(req []dto.HostHelper) error {
|
|
conf, err := os.ReadFile(defaultHostPath)
|
|
if err != nil {
|
|
return fmt.Errorf("read namesever conf of %s failed, err: %v", defaultHostPath, err)
|
|
}
|
|
lines := strings.Split(string(conf), "\n")
|
|
newFile := ""
|
|
for _, line := range lines {
|
|
if len(line) == 0 {
|
|
continue
|
|
}
|
|
parts := strings.Fields(line)
|
|
if len(parts) < 2 {
|
|
newFile += line + "\n"
|
|
continue
|
|
}
|
|
for index, item := range req {
|
|
if item.IP == parts[0] && item.Host == strings.Join(parts[1:], " ") {
|
|
newFile += line + "\n"
|
|
req = append(req[:index], req[index+1:]...)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
for _, item := range req {
|
|
newFile += fmt.Sprintf("%s %s \n", item.IP, item.Host)
|
|
}
|
|
file, err := os.OpenFile(defaultHostPath, os.O_WRONLY|os.O_TRUNC, 0640)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
write := bufio.NewWriter(file)
|
|
_, _ = write.WriteString(newFile)
|
|
write.Flush()
|
|
return nil
|
|
}
|
|
|
|
func (u *DeviceService) UpdatePasswd(req dto.ChangePasswd) error {
|
|
if cmd.CheckIllegal(req.User, req.Passwd) {
|
|
return buserr.New("ErrCmdIllegal")
|
|
}
|
|
cmdItem := cmd.ExecCommandWithOptionalSudo("chpasswd")
|
|
cmdItem.Stdin = strings.NewReader(req.User + ":" + req.Passwd)
|
|
if err := cmdItem.Run(); err != nil {
|
|
if strings.Contains(err.Error(), "does not exist") {
|
|
return buserr.New("ErrNotExistUser")
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (u *DeviceService) UpdateSwap(req dto.SwapHelper) error {
|
|
taskItem, err := task.NewTaskWithOps(req.Path, task.TaskSwapSet, task.TaskScopeSystem, req.TaskID, 1)
|
|
if err != nil {
|
|
global.LOG.Errorf("new task for create container failed, err: %v", err)
|
|
return err
|
|
}
|
|
|
|
taskItem.AddSubTask(i18n.GetMsgWithMap("SetSwap", map[string]interface{}{"path": req.Path, "size": common.LoadSizeUnit2F(float64(req.Size * 1024))}), func(t *task.Task) error {
|
|
if cmd.CheckIllegal(req.Path) {
|
|
return buserr.New("ErrCmdIllegal")
|
|
}
|
|
cmdMgr := cmd.NewCommandMgr(cmd.WithTask(*taskItem))
|
|
if !req.IsNew {
|
|
if err := cmdMgr.Run("swapoff", req.Path); err != nil {
|
|
return fmt.Errorf("handle swapoff %s failed, %v", req.Path, err)
|
|
}
|
|
}
|
|
if req.Size == 0 {
|
|
if req.Path == path.Join(global.Dir.BaseDir, ".1panel_swap") {
|
|
_ = os.Remove(path.Join(global.Dir.BaseDir, ".1panel_swap"))
|
|
}
|
|
return operateSwapWithFile(true, req)
|
|
}
|
|
taskItem.LogStart(i18n.GetMsgByKey("CreateSwap"))
|
|
if err := cmdMgr.Run("dd", "if=/dev/zero", fmt.Sprintf("of=%s", req.Path), "bs=1024", fmt.Sprintf("count=%d", req.Size)); err != nil {
|
|
return fmt.Errorf("handle dd %s failed, %v", req.Path, err)
|
|
}
|
|
|
|
taskItem.Log("chmod 0600 " + req.Path)
|
|
if err := cmdMgr.Run("chmod", "0600", req.Path); err != nil {
|
|
return fmt.Errorf("handle chmod 0600 %s failed, %v", req.Path, err)
|
|
}
|
|
taskItem.LogStart(i18n.GetMsgByKey("FormatSwap"))
|
|
if err := cmdMgr.Run("mkswap", "-f", req.Path); err != nil {
|
|
return fmt.Errorf("handle mkswap -f %s failed, %v", req.Path, err)
|
|
}
|
|
|
|
taskItem.LogStart(i18n.GetMsgByKey("EnableSwap"))
|
|
if err := cmdMgr.Run("swapon", req.Path); err != nil {
|
|
_ = cmdMgr.Run("swapoff", req.Path)
|
|
return fmt.Errorf("handle swapoff %s failed, %v", req.Path, err)
|
|
}
|
|
return operateSwapWithFile(false, req)
|
|
}, nil)
|
|
go func() {
|
|
_ = taskItem.Execute()
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
func (u *DeviceService) LoadConf(name string) (string, error) {
|
|
pathItem := ""
|
|
switch name {
|
|
case "DNS":
|
|
pathItem = defaultDNSPath
|
|
case "Hosts":
|
|
pathItem = defaultHostPath
|
|
default:
|
|
return "", fmt.Errorf("not support such name %s", name)
|
|
}
|
|
if _, err := os.Stat(pathItem); err != nil {
|
|
return "", err
|
|
}
|
|
content, err := os.ReadFile(pathItem)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(content), nil
|
|
}
|
|
|
|
func (u *DeviceService) UpdateByConf(req dto.UpdateByNameAndFile) error {
|
|
if req.Name != "DNS" && req.Name != "Hosts" {
|
|
return fmt.Errorf("not support such name %s", req.Name)
|
|
}
|
|
path := defaultDNSPath
|
|
if req.Name == "Hosts" {
|
|
path = defaultHostPath
|
|
}
|
|
file, err := os.OpenFile(path, 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()
|
|
return nil
|
|
}
|
|
|
|
func loadDNS() []string {
|
|
var list []string
|
|
dnsConf, err := os.ReadFile(defaultDNSPath)
|
|
if err == nil {
|
|
lines := strings.Split(string(dnsConf), "\n")
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, "nameserver ") {
|
|
list = append(list, strings.TrimPrefix(line, "nameserver "))
|
|
}
|
|
}
|
|
}
|
|
return list
|
|
}
|
|
|
|
func updateDNS(list []string) error {
|
|
conf, err := os.ReadFile(defaultDNSPath)
|
|
if err != nil {
|
|
return fmt.Errorf("read nameserver conf of %s failed, err: %v", defaultDNSPath, err)
|
|
}
|
|
lines := strings.Split(string(conf), "\n")
|
|
newFile := ""
|
|
for _, line := range lines {
|
|
if len(line) == 0 {
|
|
continue
|
|
}
|
|
parts := strings.Fields(line)
|
|
if len(parts) < 2 || parts[0] != "nameserver" {
|
|
newFile += line + "\n"
|
|
continue
|
|
}
|
|
itemNs := strings.Join(parts[1:], " ")
|
|
for index, item := range list {
|
|
if item == itemNs {
|
|
newFile += line + "\n"
|
|
list = append(list[:index], list[index+1:]...)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
for _, item := range list {
|
|
if len(item) != 0 {
|
|
newFile += fmt.Sprintf("nameserver %s \n", item)
|
|
}
|
|
}
|
|
file, err := os.OpenFile(defaultDNSPath, os.O_WRONLY|os.O_TRUNC, 0640)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
write := bufio.NewWriter(file)
|
|
_, _ = write.WriteString(newFile)
|
|
write.Flush()
|
|
return nil
|
|
}
|
|
|
|
func loadHosts() []dto.HostHelper {
|
|
var list []dto.HostHelper
|
|
hostConf, err := os.ReadFile(defaultHostPath)
|
|
if err == nil {
|
|
lines := strings.Split(string(hostConf), "\n")
|
|
for _, line := range lines {
|
|
parts := strings.Fields(line)
|
|
if len(parts) < 2 || strings.HasPrefix(strings.TrimPrefix(line, " "), "#") {
|
|
continue
|
|
}
|
|
list = append(list, dto.HostHelper{IP: parts[0], Host: strings.Join(parts[1:], " ")})
|
|
}
|
|
}
|
|
return list
|
|
}
|
|
|
|
func loadHostname() string {
|
|
std, err := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second)).RunWithStdout("hostname")
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.ReplaceAll(std, "\n", "")
|
|
}
|
|
|
|
func loadUser() string {
|
|
std, err := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second)).RunWithStdout("whoami")
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.ReplaceAll(std, "\n", "")
|
|
}
|
|
|
|
func loadSwap() []dto.SwapHelper {
|
|
var data []dto.SwapHelper
|
|
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second))
|
|
std, err := cmdMgr.RunWithOptionalSudoAndStdout("swapon", "--summary")
|
|
if err != nil {
|
|
return data
|
|
}
|
|
lines := strings.Split(std, "\n")
|
|
for index, line := range lines {
|
|
if index == 0 {
|
|
continue
|
|
}
|
|
parts := strings.Fields(line)
|
|
if len(parts) < 5 {
|
|
continue
|
|
}
|
|
sizeItem, _ := strconv.Atoi(parts[2])
|
|
data = append(data, dto.SwapHelper{Path: parts[0], Size: uint64(sizeItem), Used: parts[3]})
|
|
}
|
|
return data
|
|
}
|
|
|
|
func operateSwapWithFile(delete bool, req dto.SwapHelper) error {
|
|
conf, err := os.ReadFile(defaultFstab)
|
|
if err != nil {
|
|
return fmt.Errorf("read file %s failed, err: %v", defaultFstab, err)
|
|
}
|
|
lines := strings.Split(string(conf), "\n")
|
|
newFile := ""
|
|
for _, line := range lines {
|
|
if len(line) == 0 {
|
|
continue
|
|
}
|
|
parts := strings.Fields(line)
|
|
if len(parts) == 6 && parts[0] == req.Path {
|
|
continue
|
|
}
|
|
newFile += line + "\n"
|
|
}
|
|
if !delete {
|
|
newFile += fmt.Sprintf("%s swap swap defaults 0 0\n", req.Path)
|
|
}
|
|
file, err := os.OpenFile(defaultFstab, os.O_WRONLY|os.O_TRUNC, 0640)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
write := bufio.NewWriter(file)
|
|
_, _ = write.WriteString(newFile)
|
|
write.Flush()
|
|
return nil
|
|
}
|