1
0
mirror of https://github.com/1Panel-dev/1Panel.git synced 2025-03-13 17:24:44 +08:00

feat: 工具箱增加 Swap 管理 (#3047)

This commit is contained in:
ssongliu 2023-11-27 12:02:08 +08:00 committed by GitHub
parent 038879819d
commit b5592327dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 762 additions and 40 deletions

View File

@ -73,7 +73,7 @@ func (b *BaseApi) LoadDeviceConf(c *gin.Context) {
// @Success 200
// @Security ApiKeyAuth
// @Router /toolbox/device/update/byconf [post]
func (b *BaseApi) UpdateDevicByFile(c *gin.Context) {
func (b *BaseApi) UpdateDeviceByFile(c *gin.Context) {
var req dto.UpdateByNameAndFile
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
@ -138,7 +138,7 @@ func (b *BaseApi) UpdateDeviceHost(c *gin.Context) {
// @Success 200
// @Security ApiKeyAuth
// @Router /toolbox/device/update/passwd [post]
func (b *BaseApi) UpdateDevicPasswd(c *gin.Context) {
func (b *BaseApi) UpdateDevicePasswd(c *gin.Context) {
var req dto.ChangePasswd
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
@ -159,6 +159,28 @@ func (b *BaseApi) UpdateDevicPasswd(c *gin.Context) {
helper.SuccessWithData(c, nil)
}
// @Tags Device
// @Summary Update device swap
// @Description 修改系统 Swap
// @Accept json
// @Param request body dto.SwapHelper true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /toolbox/device/update/swap [post]
// @x-panel-log {"bodyKeys":["operate","path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] 主机 swap [path]","formatEN":"[operate] device swap [path]"}
func (b *BaseApi) UpdateDeviceSwap(c *gin.Context) {
var req dto.SwapHelper
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := deviceService.UpdateSwap(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
// @Tags Device
// @Summary Check device DNS conf
// @Description 检查系统 DNS 配置可用性

View File

@ -8,6 +8,12 @@ type DeviceBaseInfo struct {
LocalTime string `json:"localTime"`
Ntp string `json:"ntp"`
User string `json:"user"`
SwapMemoryTotal uint64 `json:"swapMemoryTotal"`
SwapMemoryAvailable uint64 `json:"swapMemoryAvailable"`
SwapMemoryUsed uint64 `json:"swapMemoryUsed"`
SwapDetails []SwapHelper `json:"swapDetails"`
}
type HostHelper struct {
@ -15,6 +21,14 @@ type HostHelper struct {
Host string `json:"host"`
}
type SwapHelper struct {
Path string `json:"path" validate:"required"`
Size uint64 `json:"size"`
Used string `json:"used"`
IsNew bool `json:"isNew"`
}
type TimeZoneOptions struct {
From string `json:"from"`
Zones []string `json:"zones"`

View File

@ -6,6 +6,8 @@ import (
"fmt"
"net"
"os"
"path"
"strconv"
"strings"
"time"
@ -14,10 +16,12 @@ 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/shirou/gopsutil/v3/mem"
)
const defaultDNSPath = "/etc/resolv.conf"
const defaultHostPath = "/etc/hosts"
const defaultFstab = "/etc/fstab"
type DeviceService struct{}
@ -26,6 +30,7 @@ type IDeviceService interface {
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)
@ -47,6 +52,14 @@ func (u *DeviceService) LoadBaseInfo() (dto.DeviceBaseInfo, error) {
ntp, _ := settingRepo.Get(settingRepo.WithByKey("NtpSite"))
baseInfo.Ntp = ntp.Value
swapInfo, _ := mem.SwapMemory()
baseInfo.SwapMemoryTotal = swapInfo.Total
baseInfo.SwapMemoryAvailable = swapInfo.Free
baseInfo.SwapMemoryUsed = swapInfo.Used
if baseInfo.SwapMemoryTotal != 0 {
baseInfo.SwapDetails = loadSwap()
}
return baseInfo, nil
}
@ -104,11 +117,20 @@ func (u *DeviceService) Update(key, value string) error {
if err != nil {
return errors.New(std)
}
case "LocalTime":
if err := settingRepo.Update("NtpSite", value); err != nil {
return err
case "Ntp", "LocalTime":
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(value)
ntime, err := ntp.GetRemoteTime(ntpValue)
if err != nil {
return err
}
@ -168,6 +190,35 @@ func (u *DeviceService) UpdatePasswd(req dto.ChangePasswd) error {
return nil
}
func (u *DeviceService) UpdateSwap(req dto.SwapHelper) error {
if !req.IsNew {
std, err := cmd.Execf("%s swapoff %s", cmd.SudoHandleCmd(), req.Path)
if err != nil {
return fmt.Errorf("handle swapoff %s failed, err: %s", req.Path, std)
}
}
if req.Size == 0 {
if req.Path == path.Join(global.CONF.System.BaseDir, ".1panel_swap") {
_ = os.Remove(path.Join(global.CONF.System.BaseDir, ".1panel_swap"))
}
return operateSwapWithFile(true, req)
}
std1, err := cmd.Execf("%s dd if=/dev/zero of=%s bs=1024 count=%d", cmd.SudoHandleCmd(), req.Path, req.Size)
if err != nil {
return fmt.Errorf("handle dd path %s failed, err: %s", req.Path, std1)
}
std2, err := cmd.Execf("%s mkswap -f %s", cmd.SudoHandleCmd(), req.Path)
if err != nil {
return fmt.Errorf("handle dd path %s failed, err: %s", req.Path, std2)
}
std3, err := cmd.Execf("%s swapon %s", cmd.SudoHandleCmd(), req.Path)
if err != nil {
_, _ = cmd.Execf("%s swapoff %s", cmd.SudoHandleCmd(), req.Path)
return fmt.Errorf("handle dd path %s failed, err: %s", req.Path, std3)
}
return operateSwapWithFile(false, req)
}
func (u *DeviceService) LoadConf(name string) (string, error) {
pathItem := ""
switch name {
@ -291,3 +342,55 @@ func loadUser() string {
}
return strings.ReplaceAll(std, "\n", "")
}
func loadSwap() []dto.SwapHelper {
var data []dto.SwapHelper
std, err := cmd.Execf("%s swapon --show --summary", cmd.SudoHandleCmd())
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
}

View File

@ -20,8 +20,9 @@ func (s *ToolboxRouter) InitToolboxRouter(Router *gin.RouterGroup) {
toolboxRouter.GET("/device/zone/options", baseApi.LoadTimeOption)
toolboxRouter.POST("/device/update/conf", baseApi.UpdateDeviceConf)
toolboxRouter.POST("/device/update/host", baseApi.UpdateDeviceHost)
toolboxRouter.POST("/device/update/passwd", baseApi.UpdateDevicPasswd)
toolboxRouter.POST("/device/update/byconf", baseApi.UpdateDevicByFile)
toolboxRouter.POST("/device/update/passwd", baseApi.UpdateDevicePasswd)
toolboxRouter.POST("/device/update/swap", baseApi.UpdateDeviceSwap)
toolboxRouter.POST("/device/update/byconf", baseApi.UpdateDeviceByFile)
toolboxRouter.POST("/device/check/dns", baseApi.CheckDNS)
toolboxRouter.POST("/device/conf", baseApi.LoadDeviceConf)

View File

@ -10331,6 +10331,49 @@ const docTemplate = `{
}
}
},
"/toolbox/device/update/swap": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改系统 Swap",
"consumes": [
"application/json"
],
"tags": [
"Device"
],
"summary": "Update device swap",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SwapHelper"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"operate",
"path"
],
"formatEN": "[operate] device swap [path]",
"formatZH": "[operate] 主机 swap [path]",
"paramKeys": []
}
}
},
"/toolbox/device/zone/options": {
"get": {
"security": [
@ -13748,6 +13791,9 @@ const docTemplate = `{
"openStdin": {
"type": "boolean"
},
"privileged": {
"type": "boolean"
},
"publishAllPorts": {
"type": "boolean"
},
@ -14519,6 +14565,21 @@ const docTemplate = `{
"ntp": {
"type": "string"
},
"swapDetails": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.SwapHelper"
}
},
"swapMemoryAvailable": {
"type": "integer"
},
"swapMemoryTotal": {
"type": "integer"
},
"swapMemoryUsed": {
"type": "integer"
},
"timeZone": {
"type": "string"
},
@ -16653,6 +16714,32 @@ const docTemplate = `{
}
}
},
"dto.SwapHelper": {
"type": "object",
"required": [
"operate",
"path"
],
"properties": {
"operate": {
"type": "string",
"enum": [
"create",
"delete",
"update"
]
},
"path": {
"type": "string"
},
"size": {
"type": "integer"
},
"used": {
"type": "string"
}
}
},
"dto.TreeChild": {
"type": "object",
"properties": {

View File

@ -10324,6 +10324,49 @@
}
}
},
"/toolbox/device/update/swap": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改系统 Swap",
"consumes": [
"application/json"
],
"tags": [
"Device"
],
"summary": "Update device swap",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SwapHelper"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"operate",
"path"
],
"formatEN": "[operate] device swap [path]",
"formatZH": "[operate] 主机 swap [path]",
"paramKeys": []
}
}
},
"/toolbox/device/zone/options": {
"get": {
"security": [
@ -13741,6 +13784,9 @@
"openStdin": {
"type": "boolean"
},
"privileged": {
"type": "boolean"
},
"publishAllPorts": {
"type": "boolean"
},
@ -14512,6 +14558,21 @@
"ntp": {
"type": "string"
},
"swapDetails": {
"type": "array",
"items": {
"$ref": "#/definitions/dto.SwapHelper"
}
},
"swapMemoryAvailable": {
"type": "integer"
},
"swapMemoryTotal": {
"type": "integer"
},
"swapMemoryUsed": {
"type": "integer"
},
"timeZone": {
"type": "string"
},
@ -16646,6 +16707,32 @@
}
}
},
"dto.SwapHelper": {
"type": "object",
"required": [
"operate",
"path"
],
"properties": {
"operate": {
"type": "string",
"enum": [
"create",
"delete",
"update"
]
},
"path": {
"type": "string"
},
"size": {
"type": "integer"
},
"used": {
"type": "string"
}
}
},
"dto.TreeChild": {
"type": "object",
"properties": {

View File

@ -433,6 +433,8 @@ definitions:
type: string
openStdin:
type: boolean
privileged:
type: boolean
publishAllPorts:
type: boolean
restartPolicy:
@ -954,6 +956,16 @@ definitions:
type: string
ntp:
type: string
swapDetails:
items:
$ref: '#/definitions/dto.SwapHelper'
type: array
swapMemoryAvailable:
type: integer
swapMemoryTotal:
type: integer
swapMemoryUsed:
type: integer
timeZone:
type: string
user:
@ -2395,6 +2407,24 @@ definitions:
required:
- id
type: object
dto.SwapHelper:
properties:
operate:
enum:
- create
- delete
- update
type: string
path:
type: string
size:
type: integer
used:
type: string
required:
- operate
- path
type: object
dto.TreeChild:
properties:
id:
@ -11107,6 +11137,34 @@ paths:
summary: Update device passwd
tags:
- Device
/toolbox/device/update/swap:
post:
consumes:
- application/json
description: 修改系统 Swap
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SwapHelper'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Update device swap
tags:
- Device
x-panel-log:
BeforeFunctions: []
bodyKeys:
- operate
- path
formatEN: '[operate] device swap [path]'
formatZH: '[operate] 主机 swap [path]'
paramKeys: []
/toolbox/device/zone/options:
get:
consumes:

View File

@ -7,6 +7,19 @@ export namespace Toolbox {
user: string;
timeZone: string;
localTime: string;
swapMemoryTotal: number;
swapMemoryAvailable: number;
swapMemoryUsed: number;
swapDetails: Array<SwapHelper>;
}
export interface SwapHelper {
path: string;
size: number;
used: string;
isNew: boolean;
}
export interface HostHelper {
ip: string;

View File

@ -2,6 +2,7 @@ import http from '@/api';
import { UpdateByFile } from '../interface';
import { Toolbox } from '../interface/toolbox';
import { Base64 } from 'js-base64';
import { TimeoutEnum } from '@/enums/http-enum';
// device
export const getDeviceBase = () => {
@ -19,6 +20,9 @@ export const updateDeviceHost = (param: Array<Toolbox.TimeZoneOptions>) => {
export const updateDevicePasswd = (user: string, passwd: string) => {
return http.post(`/toolbox/device/update/passwd`, { user: user, passwd: Base64.encode(passwd) });
};
export const updateDeviceSwap = (params: Toolbox.SwapHelper) => {
return http.post(`/toolbox/device/update/swap`, params, TimeoutEnum.T_5M);
};
export const updateDeviceByConf = (name: string, file: string) => {
return http.post(`/toolbox/device/update/byconf`, { name: name, file: file });
};

View File

@ -884,13 +884,30 @@ const message = {
emptyTerminal: 'No terminal is currently connected',
},
toolbox: {
swap: {
swap: 'Swap Partition',
swapHelper1:
'The size of the swap should be 1 to 2 times the physical memory, adjustable based on specific requirements;',
swapHelper2:
'Before creating a swap file, ensure that the system disk has sufficient available space, as the swap file size will occupy the corresponding disk space;',
swapHelper3:
'Swap can help alleviate memory pressure, but it is only an alternative. Excessive reliance on swap may lead to a decrease in system performance. It is recommended to prioritize increasing memory or optimizing application memory usage;',
swapHelper4: 'It is advisable to regularly monitor the usage of swap to ensure normal system operation.',
swapDeleteHelper:
'This operation will remove the Swap partition {0}. For system security reasons, the corresponding file will not be automatically deleted. If deletion is required, please proceed manually!',
saveHelper: 'Please save the current settings first!',
saveSwap:
'Saving the current configuration will adjust the Swap partition {0} size to {1}. Do you want to continue?',
saveSwapHelper: 'The minimum partition size is 40 KB. Please modify and try again!',
swapOff: 'The minimum partition size is 40 KB. Setting it to 0 will disable the Swap partition.',
},
device: {
dnsHelper: 'Server Address Domain Resolution',
hostsHelper: 'Hostname Resolution',
hosts: 'Domain',
toolbox: 'Toolbox',
hostname: 'Hostname',
passwd: 'Host Password',
passwd: 'System Password',
passwdHelper: 'Input characters cannot include $ and &',
timeZone: 'System Time Zone',
localTime: 'Server Time',

View File

@ -845,13 +845,26 @@ const message = {
emptyTerminal: '暫無終端連接',
},
toolbox: {
swap: {
swap: 'Swap',
swapHelper1: 'Swap 的大小應該是物理內存的 1 2 可根據具體情況進行調整',
swapHelper2: '在創建 Swap 文件之前請確保系統硬盤有足夠的可用空間Swap 文件的大小將佔用相應的磁盤空間',
swapHelper3:
'Swap 可以幫助緩解內存壓力但僅是一個備選項過多依賴可能導致系統性能下降建議優先考慮增加內存或者優化應用程序內存使用',
swapHelper4: '建議定期監控 Swap 的使用情況以確保系統正常運行',
swapDeleteHelper: '此操作將移除 Swap 分區 {0}出於系統安全考慮不會自動刪除該文件如需刪除請手動操作',
saveHelper: '請先保存當前設置',
saveSwap: '儲存當前配置將調整 Swap 分區 {0} 大小到 {1}是否繼續',
saveSwapHelper: '分區大小最小值為 40 KB請修改後重試',
swapOff: '分區大小最小值為 40 KB設置為 0 則關閉 Swap 分區',
},
device: {
dnsHelper: '伺服器地址域名解析',
hostsHelper: '主機名解析',
hosts: '域名',
toolbox: '工具箱',
hostname: '主機名',
passwd: '主機密碼',
passwd: '系統密碼',
passwdHelper: '輸入的字符不能包含 $ &',
timeZone: '系統時區',
localTime: '伺服器時間',
@ -863,6 +876,7 @@ const message = {
ntpALi: '阿里',
ntpGoogle: '谷歌',
syncSite: 'NTP 伺服器',
syncSiteHelper: '該操作將使用 {0} 作為源進行系統時間同步是否繼續',
hostnameHelper: '主機名修改依賴於 hostnamectl 命令如未安裝可能導致修改失敗',
userHelper: '用戶名依賴於 whoami 命令獲取如未安裝可能導致獲取失敗',
passwordHelper: '密碼修改依賴於 chpasswd 命令如未安裝可能導致修改失敗',
@ -1090,7 +1104,6 @@ const message = {
systemIP: '服務器地址',
systemIPWarning: '當前未設置服務器地址請先在面板設置中設置',
defaultNetwork: '默認網卡',
syncSiteHelper: '該操作將使用 {0} 作為源進行系統時間同步是否繼續',
changePassword: '密碼修改',
oldPassword: '原密碼',
newPassword: '新密碼',

View File

@ -846,13 +846,26 @@ const message = {
emptyTerminal: '暂无终端连接',
},
toolbox: {
swap: {
swap: 'Swap 分区',
swapHelper1: 'Swap 的大小应该是物理内存的 1 2 可根据具体情况进行调整',
swapHelper2: '在创建 Swap 文件之前请确保系统硬盘有足够的可用空间Swap 文件的大小将占用相应的磁盘空间',
swapHelper3:
'Swap 可以帮助缓解内存压力但仅是一个备选项过多依赖可能导致系统性能下降建议优先考虑增加内存或者优化应用程序内存使用',
swapHelper4: '建议定期监控 Swap 的使用情况以确保系统正常运行',
swapDeleteHelper: '此操作将移除 Swap 分区 {0}出于系统安全考虑不会自动删除该文件如需删除请手动操作',
saveHelper: '请先保存当前设置',
saveSwap: '保存当前配置将调整 Swap 分区 {0} 大小到 {1}是否继续',
saveSwapHelper: '分区大小最小值为 40 KB请修改后重试',
swapOff: '分区大小最小值为 40 KB设置成 0 则关闭 Swap 分区',
},
device: {
dnsHelper: '服务器地址域名解析',
hostsHelper: '主机名解析',
hosts: '域名',
toolbox: '工具箱',
hostname: '主机名',
passwd: '主机密码',
passwd: '系统密码',
passwdHelper: '输入字符不能包含 $ &',
timeZone: '系统时区',
localTime: '服务器时间',
@ -864,6 +877,7 @@ const message = {
ntpALi: '阿里',
ntpGoogle: '谷歌',
syncSite: 'NTP 服务器',
syncSiteHelper: '该操作将使用 {0} 作为源进行系统时间同步是否继续',
hostnameHelper: '主机名修改依赖于 hostnamectl 命令如未安装可能导致修改失败',
userHelper: '用户名依赖于 whoami 命令获取如未安装可能导致获取失败',
passwordHelper: '密码修改依赖于 chpasswd 命令如未安装可能导致修改失败',
@ -1091,7 +1105,6 @@ const message = {
systemIP: '服务器地址',
systemIPWarning: '当前未设置服务器地址请先在面板设置中设置',
defaultNetwork: '默认网卡',
syncSiteHelper: '该操作将使用 {0} 作为源进行系统时间同步是否继续',
changePassword: '密码修改',
oldPassword: '原密码',
newPassword: '新密码',

View File

@ -134,10 +134,25 @@ export function loadZero(i: number) {
export function computeSize(size: number): string {
const num = 1024.0;
if (size < num) return size + ' B';
if (size < Math.pow(num, 2)) return (size / num).toFixed(2) + ' KB';
if (size < Math.pow(num, 3)) return (size / Math.pow(num, 2)).toFixed(2) + ' MB';
if (size < Math.pow(num, 4)) return (size / Math.pow(num, 3)).toFixed(2) + ' GB';
return (size / Math.pow(num, 4)).toFixed(2) + ' TB';
if (size < Math.pow(num, 2)) return formattedNumber((size / num).toFixed(2)) + ' KB';
if (size < Math.pow(num, 3)) return formattedNumber((size / Math.pow(num, 2)).toFixed(2)) + ' MB';
if (size < Math.pow(num, 4)) return formattedNumber((size / Math.pow(num, 3)).toFixed(2)) + ' GB';
return formattedNumber((size / Math.pow(num, 4)).toFixed(2)) + ' TB';
}
export function splitSize(size: number): any {
const num = 1024.0;
if (size < num) return { size: Number(size), unit: 'B' };
if (size < Math.pow(num, 2)) return { size: formattedNumber((size / num).toFixed(2)), unit: 'KB' };
if (size < Math.pow(num, 3))
return { size: formattedNumber((size / Number(Math.pow(num, 2).toFixed(2))).toFixed(2)), unit: 'MB' };
if (size < Math.pow(num, 4))
return { size: formattedNumber((size / Number(Math.pow(num, 3))).toFixed(2)), unit: 'GB' };
return { size: formattedNumber((size / Number(Math.pow(num, 4))).toFixed(2)), unit: 'TB' };
}
export function formattedNumber(num: string) {
return num.endsWith('.00') ? Number(num.slice(0, -3)) : Number(num);
}
export function computeSizeFromMB(size: number): string {

View File

@ -203,13 +203,51 @@ const rules = reactive({
name: [Rules.requiredInput],
driver: [Rules.requiredSelect],
subnet: [{ validator: checkCidr, trigger: 'blur' }],
gateway: [Rules.ip],
gateway: [{ validator: checkGateway, trigger: 'blur' }],
scope: [{ validator: checkCidr, trigger: 'blur' }],
subnetV6: [{ validator: checkFixedCidrV6, trigger: 'blur' }],
gatewayV6: [Rules.ipV6],
gatewayV6: [{ validator: checkGatewayV6, trigger: 'blur' }],
scopeV6: [{ validator: checkFixedCidrV6, trigger: 'blur' }],
});
function checkGateway(rule: any, value: any, callback: any) {
if (value === '') {
callback();
}
const reg =
/^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/;
if (!reg.test(value) && value !== '') {
return callback(new Error(i18n.global.t('commons.rule.formatErr')));
}
callback();
}
function checkGatewayV6(rule: any, value: any, callback: any) {
if (value === '') {
callback();
} else {
const IPv4SegmentFormat = '(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])';
const IPv4AddressFormat = `(${IPv4SegmentFormat}[.]){3}${IPv4SegmentFormat}`;
const IPv6SegmentFormat = '(?:[0-9a-fA-F]{1,4})';
const IPv6AddressRegExp = new RegExp(
'^(' +
`(?:${IPv6SegmentFormat}:){7}(?:${IPv6SegmentFormat}|:)|` +
`(?:${IPv6SegmentFormat}:){6}(?:${IPv4AddressFormat}|:${IPv6SegmentFormat}|:)|` +
`(?:${IPv6SegmentFormat}:){5}(?::${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,2}|:)|` +
`(?:${IPv6SegmentFormat}:){4}(?:(:${IPv6SegmentFormat}){0,1}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,3}|:)|` +
`(?:${IPv6SegmentFormat}:){3}(?:(:${IPv6SegmentFormat}){0,2}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,4}|:)|` +
`(?:${IPv6SegmentFormat}:){2}(?:(:${IPv6SegmentFormat}){0,3}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,5}|:)|` +
`(?:${IPv6SegmentFormat}:){1}(?:(:${IPv6SegmentFormat}){0,4}:${IPv4AddressFormat}|(:${IPv6SegmentFormat}){1,6}|:)|` +
`(?::((?::${IPv6SegmentFormat}){0,5}:${IPv4AddressFormat}|(?::${IPv6SegmentFormat}){1,7}|:))` +
')(%[0-9a-zA-Z-.:]{1,})?$',
);
if (!IPv6AddressRegExp.test(value) && value !== '') {
return callback(new Error(i18n.global.t('commons.rule.formatErr')));
}
callback();
}
}
function checkCidr(rule: any, value: any, callback: any) {
if (value === '') {
callback();

View File

@ -71,7 +71,7 @@
{{ $t('home.free') }}: {{ formatNumber(currentInfo.swapMemoryAvailable / 1024 / 1024) }} MB
</el-tag>
<el-tag class="tagClass">
{{ $t('home.percent') }}: {{ formatNumber(100 - currentInfo.swapMemoryUsedPercent * 100) }}%
{{ $t('home.percent') }}: {{ formatNumber(currentInfo.swapMemoryUsedPercent * 100) }}%
</el-tag>
</div>
<template #reference>

View File

@ -24,6 +24,15 @@
</template>
</el-input>
</el-form-item>
<el-form-item label="Swap" prop="swap">
<el-input disabled v-model="form.swapItem">
<template #append>
<el-button @click="onChangeSwap" icon="Setting">
{{ $t('commons.button.set') }}
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('toolbox.device.hostname')" prop="hostname">
<el-input disabled v-model="form.hostname">
<template #append>
@ -34,7 +43,7 @@
</el-input>
</el-form-item>
<el-form-item :label="$t('toolbox.device.passwd')" prop="passwd">
<el-input disabled v-model="form.passwd">
<el-input disabled v-model="form.passwd" type="password">
<template #append>
<el-button @click="onChangePasswd" icon="Setting">
{{ $t('commons.button.set') }}
@ -42,6 +51,15 @@
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('toolbox.device.syncSite')" prop="ntp">
<el-input disabled v-model="form.ntp">
<template #append>
<el-button @click="onChangeNtp" icon="Setting">
{{ $t('commons.button.set') }}
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('toolbox.device.timeZone')" prop="timeZone">
<el-input disabled v-model="form.timeZone">
<template #append>
@ -54,8 +72,8 @@
<el-form-item :label="$t('toolbox.device.localTime')" prop="localTime">
<el-input disabled v-model="form.localTime">
<template #append>
<el-button @click="onChangeLocalTime" icon="Setting">
{{ $t('commons.button.set') }}
<el-button @click="onChangeLocalTime" icon="Refresh">
{{ $t('commons.button.sync') }}
</el-button>
</template>
</el-input>
@ -66,9 +84,10 @@
</template>
</LayoutContent>
<Swap ref="swapRef" @search="search" />
<Passwd ref="passwdRef" @search="search" />
<TimeZone ref="timeZoneRef" @search="search" />
<LocalTime ref="localTimeRef" @search="search" />
<Ntp ref="ntpRef" @search="search" />
<DNS ref="dnsRef" @search="search" />
<Hostname ref="hostnameRef" @search="search" />
<Hosts ref="hostsRef" @search="search" />
@ -77,19 +96,23 @@
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue';
import Swap from '@/views/toolbox/device/swap/index.vue';
import Passwd from '@/views/toolbox/device/passwd/index.vue';
import TimeZone from '@/views/toolbox/device/time-zone/index.vue';
import LocalTime from '@/views/toolbox/device/local-time/index.vue';
import Ntp from '@/views/toolbox/device/ntp/index.vue';
import DNS from '@/views/toolbox/device/dns/index.vue';
import Hostname from '@/views/toolbox/device/hostname/index.vue';
import Hosts from '@/views/toolbox/device/hosts/index.vue';
import { getDeviceBase } from '@/api/modules/toolbox';
import { getDeviceBase, updateDevice } from '@/api/modules/toolbox';
import i18n from '@/lang';
import { computeSize } from '@/utils/util';
import { MsgSuccess } from '@/utils/message';
const loading = ref(false);
const swapRef = ref();
const timeZoneRef = ref();
const localTimeRef = ref();
const ntpRef = ref();
const passwdRef = ref();
const dnsRef = ref();
const hostnameRef = ref();
@ -106,13 +129,27 @@ const form = reactive({
timeZone: '',
localTime: '',
ntp: '',
swapItem: '',
});
const onChangeTimeZone = () => {
timeZoneRef.value.acceptParams({ timeZone: form.timeZone });
};
const onChangeLocalTime = () => {
localTimeRef.value.acceptParams({ localTime: form.localTime, ntpSite: form.ntp });
const onChangeNtp = () => {
ntpRef.value.acceptParams({ ntpSite: form.ntp });
};
const onChangeLocalTime = async () => {
loading.value = true;
await updateDevice('LocalTime', '')
.then(() => {
loading.value = false;
search();
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
})
.catch(() => {
loading.value = false;
});
};
const onChangePasswd = () => {
passwdRef.value.acceptParams({ user: form.user });
@ -126,6 +163,9 @@ const onChangeHostname = () => {
const onChangeHost = () => {
hostsRef.value.acceptParams({ hosts: form.hosts });
};
const onChangeSwap = () => {
swapRef.value.acceptParams();
};
const search = async () => {
const res = await getDeviceBase();
@ -138,6 +178,10 @@ const search = async () => {
form.dnsItem = form.dns ? i18n.global.t('toolbox.device.dnsHelper') : i18n.global.t('setting.unSetting');
form.hosts = res.data.hosts || [];
form.hostItem = form.hosts ? i18n.global.t('toolbox.device.hostsHelper') : i18n.global.t('setting.unSetting');
form.swapItem = res.data.swapMemoryTotal
? computeSize(res.data.swapMemoryTotal)
: i18n.global.t('setting.unSetting');
};
onMounted(() => {

View File

@ -2,7 +2,7 @@
<div>
<el-drawer v-model="drawerVisible" :destroy-on-close="true" :close-on-click-modal="false" size="30%">
<template #header>
<DrawerHeader :header="$t('toolbox.device.localTime')" :back="handleClose" />
<DrawerHeader :header="$t('toolbox.device.syncSite')" :back="handleClose" />
</template>
<el-form ref="formRef" label-position="top" :model="form" @submit.prevent v-loading="loading">
<el-row type="flex" justify="center">
@ -19,10 +19,6 @@
{{ $t('toolbox.device.ntpGoogle') }}
</el-button>
</el-form-item>
<el-form-item :label="$t('toolbox.device.localTime')" prop="localTime">
<el-input v-model="form.localTime" disabled />
</el-form-item>
</el-col>
</el-row>
</el-form>
@ -30,7 +26,7 @@
<span class="dialog-footer">
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button :disabled="loading" type="primary" @click="onSyncTime(formRef)">
{{ $t('commons.button.sync') }}
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
@ -49,21 +45,18 @@ import { updateDevice } from '@/api/modules/toolbox';
const emit = defineEmits<{ (e: 'search'): void }>();
interface DialogProps {
localTime: string;
ntpSite: string;
}
const drawerVisible = ref();
const loading = ref();
const form = reactive({
localTime: '',
ntpSite: '',
});
const formRef = ref<FormInstance>();
const acceptParams = (params: DialogProps): void => {
form.localTime = params.localTime;
form.ntpSite = params.ntpSite;
drawerVisible.value = true;
};
@ -82,7 +75,7 @@ const onSyncTime = async (formEl: FormInstance | undefined) => {
},
).then(async () => {
loading.value = true;
await updateDevice('LocalTime', form.ntpSite)
await updateDevice('Ntp', form.ntpSite)
.then(() => {
loading.value = false;
emit('search');

View File

@ -0,0 +1,201 @@
<template>
<div v-loading="loading">
<el-drawer v-model="drawerVisible" :destroy-on-close="true" :close-on-click-modal="false" size="50%">
<template #header>
<DrawerHeader header="Swap" :back="handleClose" />
</template>
<el-row type="flex" justify="center" v-loading="loading">
<el-col :span="22">
<el-alert class="common-prompt" :closable="false" type="warning">
<template #default>
<ul style="margin-left: -20px">
<li>{{ $t('toolbox.swap.swapHelper1') }}</li>
<li>{{ $t('toolbox.swap.swapHelper2') }}</li>
<li>{{ $t('toolbox.swap.swapHelper3') }}</li>
<li>{{ $t('toolbox.swap.swapHelper4') }}</li>
</ul>
</template>
</el-alert>
<el-card>
<el-form label-position="top" class="ml-3">
<el-row type="flex" justify="center" :gutter="20">
<el-col :xs="8" :sm="8" :md="8" :lg="8" :xl="8">
<el-form-item>
<template #label>
<span class="status-label">Swap {{ $t('home.total') }}</span>
</template>
<span class="status-count">{{ form.swapMemoryTotal }}</span>
</el-form-item>
</el-col>
<el-col :xs="8" :sm="8" :md="8" :lg="8" :xl="8">
<el-form-item>
<template #label>
<span class="status-label">Swap {{ $t('home.used') }}</span>
</template>
<span class="status-count">{{ form.swapMemoryUsed }}</span>
</el-form-item>
</el-col>
<el-col :xs="8" :sm="8" :md="8" :lg="8" :xl="8">
<el-form-item>
<template #label>
<span class="status-label">Swap {{ $t('home.free') }}</span>
</template>
<span class="status-count">{{ form.swapMemoryAvailable }}</span>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<el-table :data="form.swapDetails" class="mt-5">
<el-table-column :label="$t('file.path')" min-width="120" prop="path">
<template #default="{ row }">
<span>{{ row.path }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('file.size')" min-width="150">
<template #default="{ row }">
<el-input placeholder="1024" v-model.number="row.size">
<template #append>
<el-select v-model="row.sizeUnit" style="width: 85px">
<el-option label="KB" value="KB" />
<el-option label="MB" value="MB" />
<el-option label="GB" value="G" />
</el-select>
</template>
</el-input>
</template>
</el-table-column>
<el-table-column :label="$t('home.used')" min-width="70" prop="used">
<template #default="{ row }">
<span v-if="row.used !== '-'">{{ computeSize(row.used * 1024) }}</span>
</template>
</el-table-column>
<el-table-column min-width="70">
<template #default="scope">
<el-button link type="primary" @click="onSave(scope.row)">
{{ $t('commons.button.save') }}
</el-button>
</template>
</el-table-column>
</el-table>
<span class="input-help">{{ $t('toolbox.swap.swapOff') }}</span>
</el-col>
</el-row>
<template #footer>
<span class="dialog-footer">
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
</span>
</template>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import i18n from '@/lang';
import { MsgError, MsgSuccess } from '@/utils/message';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { updateDeviceSwap, getDeviceBase } from '@/api/modules/toolbox';
import { computeSize, splitSize } from '@/utils/util';
import { loadBaseDir } from '@/api/modules/setting';
const form = reactive({
swapItem: '',
swapMemoryTotal: '',
swapMemoryAvailable: '',
swapMemoryUsed: '',
swapDetails: [],
});
const drawerVisible = ref();
const loading = ref();
const acceptParams = (): void => {
search();
drawerVisible.value = true;
};
const search = async () => {
loading.value = true;
const res = await getDeviceBase();
form.swapMemoryTotal = computeSize(res.data.swapMemoryTotal);
form.swapMemoryUsed = computeSize(res.data.swapMemoryUsed);
form.swapMemoryAvailable = computeSize(res.data.swapMemoryAvailable);
form.swapDetails = res.data.swapDetails || [];
await loadBaseDir()
.then((res) => {
loading.value = false;
loadData(res.data.substring(0, res.data.lastIndexOf('/1panel')));
})
.catch(() => {
loading.value = false;
loadData('');
});
};
const loadData = (path: string) => {
let isExist = false;
for (const item of form.swapDetails) {
if (item.path === path + '/.1panel_swap') {
isExist = true;
}
let itemSize = splitSize(item.size * 1024);
item.size = itemSize.size;
item.sizeUnit = itemSize.unit;
}
if (!isExist && path !== '') {
form.swapDetails.push({
path: path + '/.1panel_swap',
size: 0,
used: 0,
isNew: true,
sizeUnit: 'MB',
});
}
};
const onSave = async (row) => {
if (row.sizeUnit === 'KB' && row.size < 40 && row.size !== 0) {
MsgError(i18n.global.t('toolbox.swap.saveSwapHelper'));
return;
}
ElMessageBox.confirm(
i18n.global.t('toolbox.swap.saveSwap', [row.path, row.size + ' ' + row.sizeUnit]),
i18n.global.t('commons.button.save'),
{
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
},
).then(async () => {
let params = {
path: row.path,
size: row.size * 1024,
used: '0',
isNew: row.isNew,
};
loading.value = true;
await updateDeviceSwap(params)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
})
.catch(() => {
loading.value = false;
});
});
};
const handleClose = () => {
drawerVisible.value = false;
};
defineExpose({
acceptParams,
});
</script>

View File

@ -84,7 +84,6 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
outDir: '../cmd/server/web',
minify: 'esbuild',
rollupOptions: {
external: ['codemirror'],
output: {
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',