add the swap function in WebUI

This commit is contained in:
LingkongSky
2025-03-27 20:00:46 +08:00
parent 087dbaded9
commit e6565fbe4b
8 changed files with 234 additions and 0 deletions

View File

@@ -84,6 +84,10 @@ type GetSSHStateRsp struct {
Enabled bool `json:"enabled"`
}
type GetSwapStateRsp struct {
Enabled bool `json:"enabled"`
}
type GetMdnsStateRsp struct {
Enabled bool `json:"enabled"`
}

View File

@@ -40,6 +40,10 @@ func vmRouter(r *gin.Engine) {
api.POST("/vm/ssh/enable", service.EnableSSH) // enable SSH
api.POST("/vm/ssh/disable", service.DisableSSH) // disable SSH
api.GET("/vm/swap", service.GetSwapState) // get Swap state
api.POST("/vm/swap/enable", service.EnableSwap) // enable Swap
api.POST("/vm/swap/disable", service.DisableSwap) // disable Swap
api.GET("/vm/mdns", service.GetMdnsState) // get mDNS state
api.POST("/vm/mdns/enable", service.EnableMdns) // enable mDNS
api.POST("/vm/mdns/disable", service.DisableMdns) // disable mDNS

136
server/service/vm/swap.go Normal file
View File

@@ -0,0 +1,136 @@
package vm
import (
"NanoKVM-Server/proto"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
const (
SwapFile = "/swapfile"
SwapSize = "128M"
InittabPath = "/etc/inittab"
TempInittab = "/etc/inittab.tmp"
)
func (s *Service) GetSwapState(c *gin.Context) {
var rsp proto.Response
enabled := isSwapEnabled()
rsp.OkRspWithData(c, &proto.GetSwapStateRsp{
Enabled: enabled,
})
}
func (s *Service) EnableSwap(c *gin.Context) {
var rsp proto.Response
if err := exec.Command("fallocate", "-l", SwapSize, SwapFile).Run(); err != nil {
log.Errorf("create swap partition failed: %s", err)
rsp.ErrRsp(c, -1, "operation failed")
return
}
if err := exec.Command("chmod", "600", SwapFile).Run(); err != nil {
log.Errorf("chmod failed: %s", err)
rsp.ErrRsp(c, -1, "operation failed")
return
}
if err := exec.Command("mkswap", SwapFile).Run(); err != nil {
log.Errorf("formatting failed: %s", err)
rsp.ErrRsp(c, -1, "operation failed")
return
}
if err := exec.Command("swapon", SwapFile).Run(); err != nil {
log.Errorf("enable swap partition failed: %s", err)
rsp.ErrRsp(c, -1, "operation failed")
return
}
fstabEntry := fmt.Sprintf("\nsi11::sysinit:/sbin/swapon %s", SwapFile)
f, err := os.OpenFile(InittabPath, os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
log.Errorf("read fstab failed: %s", err)
rsp.ErrRsp(c, -1, "operation failed")
return
}
defer f.Close()
_, err = f.WriteString(fstabEntry)
if err != nil {
log.Errorf("write fstab failed: %s", err)
rsp.ErrRsp(c, -1, "operation failed")
return
}
rsp.OkRsp(c)
log.Debugf("Swap enabled")
}
func (s *Service) DisableSwap(c *gin.Context) {
var rsp proto.Response
input, err := os.ReadFile(InittabPath)
if err != nil {
log.Errorf("read fstab failed: %s", err)
rsp.ErrRsp(c, -1, "operation failed")
return
}
lines := strings.Split(string(input), "\n")
output := make([]string, 0)
for _, line := range lines {
if !strings.Contains(line, SwapFile) {
output = append(output, line)
}
}
content := strings.Join(output, "\n")
content = strings.TrimSuffix(content, "\n")
if err := os.WriteFile(TempInittab, []byte(content), 0644); err != nil {
log.Errorf("write temp fstab failed: %s", err)
rsp.ErrRsp(c, -1, "operation failed")
return
}
if err := os.Rename(TempInittab, InittabPath); err != nil {
log.Errorf("replace fstab failed: %s", err)
rsp.ErrRsp(c, -1, "operation failed")
return
}
if err := exec.Command("swapoff", "-a").Run(); err != nil {
log.Errorf("close swap partition failed: %s", err)
rsp.ErrRsp(c, -1, "operation failed")
return
}
if err := os.Remove(SwapFile); err != nil && !os.IsNotExist(err) {
log.Warnf("delete swap file failed: %s (ignored)", err)
}
rsp.OkRsp(c)
log.Debugf("Swap disabled")
}
func isSwapEnabled() bool {
_, err := os.Stat(SwapFile)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return false
}
}
return true
}

View File

@@ -77,6 +77,21 @@ export function disableSSH() {
return http.post('/api/vm/ssh/disable');
}
// get Swap state
export function getSwapState() {
return http.get('/api/vm/swap');
}
// enable Swap
export function enableSwap() {
return http.post('/api/vm/swap/enable');
}
// disable Swap
export function disableSwap() {
return http.post('/api/vm/swap/disable');
}
// get mDNS state
export function getMdnsState() {
return http.get('/api/vm/mdns');

View File

@@ -220,6 +220,10 @@ const en = {
description: 'Enable SSH remote access',
tip: 'Set a strong password before enabling (Account - Change Password)'
},
swap: {
description: 'Enable the swap partition',
tip: 'Enable the default swap partition(128M)'
},
mdns: {
description: 'Enable mDNS discovery service',
tip: "Turning it off if it's not needed"

View File

@@ -204,6 +204,10 @@ const zh = {
description: '启用 SSH 远程访问',
tip: '启用前请务必设置强密码(帐号 - 修改密码)'
},
swap: {
description: '启用交换分区',
tip: '启用默认的交换分区128M'
},
mdns: {
description: '启用 mDNS 发现服务',
tip: '如果您未使用此功能,建议将其关闭'

View File

@@ -13,6 +13,7 @@ import { Reboot } from './reboot.tsx';
import { Ssh } from './ssh.tsx';
import { VirtualDevices } from './virtual-devices.tsx';
import { Wifi } from './wifi.tsx';
import { Swap } from './swap.tsx';
export const Device = () => {
const { t } = useTranslation();
@@ -37,6 +38,7 @@ export const Device = () => {
<Wifi />
<Ssh />
<Mdns />
<Swap />
{hidMode === 'normal' ? <VirtualDevices /> : <HidMode />}
</div>
<Divider />

View File

@@ -0,0 +1,65 @@
import { useEffect, useState } from 'react';
import { Switch, Tooltip } from 'antd';
import { CircleAlertIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import * as api from '@/api/vm.ts';
export const Swap = () => {
const { t } = useTranslation();
const [isEnabled, setIsEnabled] = useState(false);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(true);
api
.getSwapState()
.then((rsp) => {
if (rsp.data?.enabled) {
setIsEnabled(true);
}
})
.finally(() => {
setIsLoading(false);
});
}, []);
async function update() {
if (isLoading) return;
setIsLoading(true);
const rsp = isEnabled ? await api.disableSwap() : await api.enableSwap();
setIsLoading(false);
if (rsp.code !== 0) {
console.log(rsp.msg);
return;
}
setIsEnabled(!isEnabled);
}
return (
<div className="flex items-center justify-between">
<div className="flex flex-col">
<div className="flex items-center space-x-2">
<span>Swap</span>
<Tooltip
title={t('settings.device.swap.tip')}
className="cursor-pointer"
placement="bottom"
overlayStyle={{ maxWidth: '300px' }}
>
<CircleAlertIcon size={15} />
</Tooltip>
</div>
<span className="text-xs text-neutral-500">{t('settings.device.swap.description')}</span>
</div>
<Switch checked={isEnabled} loading={isLoading} onChange={update} />
</div>
);
};