From c7742319b22437135c2bab408d4b5873e2a501bf Mon Sep 17 00:00:00 2001
From: MengMengCode <227010654+MengMengCode@users.noreply.github.com>
Date: Fri, 12 Jun 2026 15:53:31 +0800
Subject: [PATCH] feat: split rate limits with configurable per-endpoint
controls
---
README.md | 2 +-
backend/internal/api/handlers.go | 161 +++++++++++++++--
backend/internal/api/runtime.go | 1 +
backend/internal/api/taskqueue.go | 9 +
backend/internal/cli/cli.go | 1 +
backend/internal/config/config.go | 93 ++++++++++
backend/internal/config/store_sqlite.go | 163 +++++++++++++++---
backend/internal/kvm/kvm.go | 85 ++++++---
backend/internal/lxc/lxc.go | 137 +++++++++++----
backend/internal/server/web/.gitkeep | 2 +-
frontend/src/components/ContainerCard.tsx | 9 +-
.../src/components/CreateContainerModal.tsx | 35 +++-
frontend/src/pages/ApiIntegration.tsx | 46 +++--
frontend/src/pages/ContainerDetail.tsx | 87 ++++++++--
frontend/src/pages/Containers.tsx | 4 +
frontend/src/services/api.ts | 16 +-
frontend/src/utils/i18n.ts | 36 +++-
17 files changed, 746 insertions(+), 141 deletions(-)
diff --git a/README.md b/README.md
index 9a87f91..3f7c08d 100644
--- a/README.md
+++ b/README.md
@@ -73,7 +73,7 @@ curl -fsSL https://raw.githubusercontent.com/MengMengCode/CLICD/main/install.sh
| 虚拟化管理 | 在同一个面板里管理 LXC 容器和 KVM 虚拟机,支持创建、重装、开机、关机、重启、删除、重置密码、到期时间和批量操作。 |
| 镜像与模板 | 内置模板和镜像管理,支持 Ubuntu、Debian、Alpine、CentOS、Arch Linux、Fedora、Rocky Linux 等常见发行版,镜像可按需下载、取消、启用、禁用和清理缓存。 |
| 网络能力 | 支持 NAT4 端口配额、随机可用端口、TCP/UDP 端口映射、公网 IPv4 池管理、IPv6 前缀检测、IPv6 状态检查和容器级 IPv6 分配。 |
-| 资源限制 | 支持 CPU、内存、磁盘、Swap、带宽用量、流量重置、流量限制和资源限制管理;容器到期或超额后可自动关机,避免资源和流量失控。 |
+| 资源限制 | 支持 CPU、内存、磁盘、Swap、独立上行/下行带宽、读/写 I/O 限速、流量重置、流量限制和资源限制管理;容器到期或超额后可自动关机,避免资源和流量失控。 |
| 远程控制 | 内置 WebSSH 和 WebVNC 票据访问,用户可以直接在浏览器打开终端或控制台,不需要手动复制连接信息。 |
| 快照能力 | 支持快照总览、容器快照、创建快照、删除快照、恢复快照、计划快照和快照配额。 |
| 安全告警 | 基于 conntrack 做轻量安全检测,可识别端口扫描、横向扫描、爆破倾向、SMTP 滥用、UDP 反射、挖矿端口、代理/VPN/Tor 等风险,并提供安全日志、汇总和设置项。 |
diff --git a/backend/internal/api/handlers.go b/backend/internal/api/handlers.go
index c64808a..8ff60e7 100644
--- a/backend/internal/api/handlers.go
+++ b/backend/internal/api/handlers.go
@@ -2,6 +2,8 @@ package api
import (
"encoding/json"
+ "fmt"
+ "io"
"net/http"
"strconv"
"strings"
@@ -210,10 +212,21 @@ func listContainers(w http.ResponseWriter, r *http.Request) {
func createContainer(w http.ResponseWriter, r *http.Request) {
var cfg lxc.ContainerConfig
- if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
+ body, err := io.ReadAll(r.Body)
+ if err != nil {
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid request body"})
return
}
+ var fields map[string]json.RawMessage
+ if err := json.Unmarshal(body, &cfg); err != nil {
+ jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid request body"})
+ return
+ }
+ _ = json.Unmarshal(body, &fields)
+ if err := normalizeCreateResourceLimits(&cfg, fields); err != nil {
+ jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: err.Error()})
+ return
+ }
if cfg.Name == "" {
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Container name is required"})
return
@@ -386,10 +399,14 @@ func updateTrafficLimit(w http.ResponseWriter, r *http.Request, id int) {
func updateResourceLimit(w http.ResponseWriter, r *http.Request, id int) {
var req struct {
- VCPU float64 `json:"vcpu"`
- RAMMB int `json:"ram_mb"`
- IOMBps int `json:"io_speed_mbps"`
- BWMbps int `json:"network_bw_mbps"`
+ VCPU *float64 `json:"vcpu"`
+ RAMMB *int `json:"ram_mb"`
+ IOMBps *int `json:"io_speed_mbps"`
+ IOReadMBps *int `json:"io_read_mbps"`
+ IOWriteMBps *int `json:"io_write_mbps"`
+ BWMbps *int `json:"network_bw_mbps"`
+ NetworkDownMbps *int `json:"network_down_mbps"`
+ NetworkUpMbps *int `json:"network_up_mbps"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: "Invalid request"})
@@ -404,21 +421,35 @@ func updateResourceLimit(w http.ResponseWriter, r *http.Request, id int) {
// Update config
nextVCPU := c.VCPU
nextRAMMB := c.RAMMB
- if req.VCPU > 0 {
- nextVCPU = req.VCPU
+ if req.VCPU != nil {
+ nextVCPU = *req.VCPU
}
- if req.RAMMB > 0 {
- nextRAMMB = req.RAMMB
+ if req.RAMMB != nil {
+ nextRAMMB = *req.RAMMB
}
if err := validateRuntimeResourceRequest(c.Runtime(), nextVCPU, nextRAMMB, c.DiskGB); err != nil {
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: err.Error()})
return
}
+ for name, value := range map[string]*int{
+ "network_bw_mbps": req.BWMbps,
+ "network_down_mbps": req.NetworkDownMbps,
+ "network_up_mbps": req.NetworkUpMbps,
+ "io_speed_mbps": req.IOMBps,
+ "io_read_mbps": req.IOReadMBps,
+ "io_write_mbps": req.IOWriteMBps,
+ } {
+ if err := rejectNegativeLimit(name, value); err != nil {
+ jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: err.Error()})
+ return
+ }
+ }
c.VCPU = nextVCPU
c.RAMMB = nextRAMMB
- c.IOSpeedMBps = req.IOMBps
- c.NetworkBWMbps = req.BWMbps
+ applyNetworkLimitPatch(c, req.BWMbps, req.NetworkDownMbps, req.NetworkUpMbps)
+ applyIOLimitPatch(c, req.IOMBps, req.IOReadMBps, req.IOWriteMBps)
+ config.NormalizeContainerResourceAliases(c)
config.SaveConfig()
// Re-apply resource limits to running container
@@ -436,6 +467,114 @@ func updateResourceLimit(w http.ResponseWriter, r *http.Request, id int) {
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: msg})
}
+func normalizeCreateResourceLimits(cfg *lxc.ContainerConfig, fields map[string]json.RawMessage) error {
+ if cfg == nil {
+ return nil
+ }
+ if err := rejectNegativeCreateLimits(*cfg); err != nil {
+ return err
+ }
+ bwSet := hasJSONField(fields, "network_bw_mbps")
+ downSet := hasJSONField(fields, "network_down_mbps")
+ upSet := hasJSONField(fields, "network_up_mbps")
+ if bwSet {
+ if !downSet {
+ cfg.NetworkDownMbps = cfg.NetworkBWMbps
+ }
+ if !upSet {
+ cfg.NetworkUpMbps = cfg.NetworkBWMbps
+ }
+ }
+ ioSet := hasJSONField(fields, "io_speed_mbps")
+ readSet := hasJSONField(fields, "io_read_mbps")
+ writeSet := hasJSONField(fields, "io_write_mbps")
+ if ioSet {
+ if !readSet {
+ cfg.IOReadMBps = cfg.IOSpeedMBps
+ }
+ if !writeSet {
+ cfg.IOWriteMBps = cfg.IOSpeedMBps
+ }
+ }
+ cfg.NormalizeResourceAliases()
+ return nil
+}
+
+func rejectNegativeCreateLimits(cfg lxc.ContainerConfig) error {
+ for name, value := range map[string]int{
+ "network_bw_mbps": cfg.NetworkBWMbps,
+ "network_down_mbps": cfg.NetworkDownMbps,
+ "network_up_mbps": cfg.NetworkUpMbps,
+ "io_speed_mbps": cfg.IOSpeedMBps,
+ "io_read_mbps": cfg.IOReadMBps,
+ "io_write_mbps": cfg.IOWriteMBps,
+ } {
+ if value < 0 {
+ return fmt.Errorf("%s cannot be negative", name)
+ }
+ }
+ return nil
+}
+
+func hasJSONField(fields map[string]json.RawMessage, name string) bool {
+ if fields == nil {
+ return false
+ }
+ _, ok := fields[name]
+ return ok
+}
+
+func rejectNegativeLimit(name string, value *int) error {
+ if value != nil && *value < 0 {
+ return fmt.Errorf("%s cannot be negative", name)
+ }
+ return nil
+}
+
+func applyNetworkLimitPatch(c *config.Container, legacy *int, down *int, up *int) {
+ if c == nil {
+ return
+ }
+ config.NormalizeContainerResourceAliases(c)
+ nextDown := c.NetworkDownMbps
+ nextUp := c.NetworkUpMbps
+ if legacy != nil {
+ nextDown = *legacy
+ nextUp = *legacy
+ }
+ if down != nil {
+ nextDown = *down
+ }
+ if up != nil {
+ nextUp = *up
+ }
+ c.NetworkDownMbps = nextDown
+ c.NetworkUpMbps = nextUp
+ c.NetworkBWMbps = config.LegacySymmetricLimit(nextDown, nextUp)
+}
+
+func applyIOLimitPatch(c *config.Container, legacy *int, read *int, write *int) {
+ if c == nil {
+ return
+ }
+ config.NormalizeContainerResourceAliases(c)
+ nextRead := c.IOReadMBps
+ nextWrite := c.IOWriteMBps
+ if legacy != nil {
+ nextRead = *legacy
+ nextWrite = *legacy
+ }
+ if read != nil {
+ nextRead = *read
+ }
+ if write != nil {
+ nextWrite = *write
+ }
+ c.IOReadMBps = nextRead
+ c.IOWriteMBps = nextWrite
+ c.IOSpeedMBps = config.LegacySymmetricLimit(nextRead, nextWrite)
+}
+
func getRandomPort(w http.ResponseWriter, r *http.Request, id int) {
c := config.FindContainer(id)
if c == nil {
diff --git a/backend/internal/api/runtime.go b/backend/internal/api/runtime.go
index 55c186c..289967b 100644
--- a/backend/internal/api/runtime.go
+++ b/backend/internal/api/runtime.go
@@ -32,6 +32,7 @@ func runtimeFromTemplateID(templateID string) string {
func createByRuntime(cfg lxc.ContainerConfig) error {
cfg.Virtualization = runtimeFromRequest(cfg.Virtualization)
+ cfg.NormalizeResourceAliases()
if cfg.Virtualization == config.VirtualizationKVM {
return kvmManager.CreateContainer(cfg)
}
diff --git a/backend/internal/api/taskqueue.go b/backend/internal/api/taskqueue.go
index 512f6f3..52dfdba 100644
--- a/backend/internal/api/taskqueue.go
+++ b/backend/internal/api/taskqueue.go
@@ -98,6 +98,7 @@ func (q *TaskQueue) EnqueueWithAudit(containerID int, containerName string, task
}
if cfg != nil {
task.Config = *cfg
+ task.Config.NormalizeResourceAliases()
}
q.enqueueTask(task)
q.persistTasks()
@@ -162,6 +163,7 @@ func (q *TaskQueue) enqueueBatchCreateList(configs []lxc.ContainerConfig, user s
var result []string
for _, cfg := range configs {
cfgCopy := cfg
+ cfgCopy.NormalizeResourceAliases()
id := q.nextID
q.nextID++
task := &Task{
@@ -246,6 +248,7 @@ func (q *TaskQueue) createWorker() {
if task.Config.Name == "" {
task.Config.Name = task.ContainerName
}
+ task.Config.NormalizeResourceAliases()
if task.Config.Name == "" {
task.Status = "failed"
task.Error = "container name is required"
@@ -592,6 +595,11 @@ func HandleBatchCreate(w http.ResponseWriter, r *http.Request) {
if req.Containers[i].VCPU <= 0 {
req.Containers[i].VCPU = 1
}
+ if err := rejectNegativeCreateLimits(req.Containers[i]); err != nil {
+ jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: name + ": " + err.Error()})
+ return
+ }
+ req.Containers[i].NormalizeResourceAliases()
req.Containers[i].Virtualization = runtimeFromRequest(req.Containers[i].Virtualization)
if req.Containers[i].RAMMB < 128 {
req.Containers[i].RAMMB = 512
@@ -820,6 +828,7 @@ func RestoreTasks() {
if cfg.Name == "" {
cfg.Name = containerName
}
+ cfg.NormalizeResourceAliases()
containerID := st.ContainerID
if containerID <= 0 && containerName != "" {
if c := config.FindContainerByName(containerName); c != nil {
diff --git a/backend/internal/cli/cli.go b/backend/internal/cli/cli.go
index 3d26060..5bead1e 100644
--- a/backend/internal/cli/cli.go
+++ b/backend/internal/cli/cli.go
@@ -388,6 +388,7 @@ func cliCreateContainer(reader *bufio.Reader) {
IOSpeedMBps: promptInt(reader, "IO 速度 (MB/s)", 500),
ExtraPorts: promptPortList(reader, "额外 NAT 端口,多个用逗号分隔"),
}
+ cfg.NormalizeResourceAliases()
cliPrintf("\n正在创建容器 %s ...\n", name)
if err := manager.CreateContainer(cfg); err != nil {
diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go
index bb24edd..3e3d9ce 100644
--- a/backend/internal/config/config.go
+++ b/backend/internal/config/config.go
@@ -115,6 +115,8 @@ type Container struct {
RAMMB int `json:"ram_mb"`
DiskGB int `json:"disk_gb"`
NetworkBWMbps int `json:"network_bw_mbps"`
+ NetworkDownMbps int `json:"network_down_mbps"`
+ NetworkUpMbps int `json:"network_up_mbps"`
MonthlyTrafficGB int `json:"monthly_traffic_gb"`
TrafficMode string `json:"traffic_mode"` // "total" or "in_out"
TrafficInGB int `json:"traffic_in_gb"` // 0 = unlimited
@@ -123,6 +125,8 @@ type Container struct {
TrafficUsedTX int64 `json:"traffic_used_tx"`
TrafficResetDate string `json:"traffic_reset_date"`
IOSpeedMBps int `json:"io_speed_mbps"`
+ IOReadMBps int `json:"io_read_mbps"`
+ IOWriteMBps int `json:"io_write_mbps"`
Status string `json:"status"`
IP string `json:"ip"`
PublicIPv4s []PublicIPv4Assignment `json:"public_ipv4s,omitempty"`
@@ -704,6 +708,9 @@ func migrateLoadedConfig() bool {
if ensureContainerNetworkAssignments() {
changed = true
}
+ if ensureContainerResourceAliases() {
+ changed = true
+ }
if ensureContainerSnapshotScheduleDefaults() {
changed = true
}
@@ -802,6 +809,91 @@ func ensureContainerNetworkAssignments() bool {
return changed
}
+func ensureContainerResourceAliases() bool {
+ changed := false
+ for i := range AppConfig.Containers {
+ if NormalizeContainerResourceAliases(&AppConfig.Containers[i]) {
+ changed = true
+ }
+ }
+ return changed
+}
+
+func NormalizeContainerResourceAliases(c *Container) bool {
+ if c == nil {
+ return false
+ }
+ changed := false
+ if c.NetworkBWMbps < 0 {
+ c.NetworkBWMbps = 0
+ changed = true
+ }
+ if c.NetworkDownMbps < 0 {
+ c.NetworkDownMbps = 0
+ changed = true
+ }
+ if c.NetworkUpMbps < 0 {
+ c.NetworkUpMbps = 0
+ changed = true
+ }
+ if c.NetworkDownMbps == 0 && c.NetworkUpMbps == 0 && c.NetworkBWMbps > 0 {
+ c.NetworkDownMbps = c.NetworkBWMbps
+ c.NetworkUpMbps = c.NetworkBWMbps
+ changed = true
+ }
+ nextNetworkBW := LegacySymmetricLimit(c.NetworkDownMbps, c.NetworkUpMbps)
+ if c.NetworkBWMbps != nextNetworkBW {
+ c.NetworkBWMbps = nextNetworkBW
+ changed = true
+ }
+
+ if c.IOSpeedMBps < 0 {
+ c.IOSpeedMBps = 0
+ changed = true
+ }
+ if c.IOReadMBps < 0 {
+ c.IOReadMBps = 0
+ changed = true
+ }
+ if c.IOWriteMBps < 0 {
+ c.IOWriteMBps = 0
+ changed = true
+ }
+ if c.IOReadMBps == 0 && c.IOWriteMBps == 0 && c.IOSpeedMBps > 0 {
+ c.IOReadMBps = c.IOSpeedMBps
+ c.IOWriteMBps = c.IOSpeedMBps
+ changed = true
+ }
+ nextIO := LegacySymmetricLimit(c.IOReadMBps, c.IOWriteMBps)
+ if c.IOSpeedMBps != nextIO {
+ c.IOSpeedMBps = nextIO
+ changed = true
+ }
+ return changed
+}
+
+func LegacySymmetricLimit(a, b int) int {
+ if a < 0 {
+ a = 0
+ }
+ if b < 0 {
+ b = 0
+ }
+ if a == b {
+ return a
+ }
+ if a == 0 {
+ return b
+ }
+ if b == 0 {
+ return a
+ }
+ if a < b {
+ return a
+ }
+ return b
+}
+
func migrateSubUsers() bool {
changed := false
for i := range AppConfig.SubUsers {
@@ -886,6 +978,7 @@ func AddContainer(c Container) {
c.UUID = NewContainerUUID()
}
c.Virtualization = NormalizeVirtualization(c.Virtualization)
+ NormalizeContainerResourceAliases(&c)
AppConfig.Containers = append(AppConfig.Containers, c)
SaveConfig()
}
diff --git a/backend/internal/config/store_sqlite.go b/backend/internal/config/store_sqlite.go
index 3a0f098..658d5a7 100644
--- a/backend/internal/config/store_sqlite.go
+++ b/backend/internal/config/store_sqlite.go
@@ -28,11 +28,15 @@ type savedTaskConfig struct {
RAMMB int `json:"ram_mb"`
DiskGB int `json:"disk_gb"`
NetworkBWMbps int `json:"network_bw_mbps"`
+ NetworkDownMbps int `json:"network_down_mbps"`
+ NetworkUpMbps int `json:"network_up_mbps"`
MonthlyTrafficGB int `json:"monthly_traffic_gb"`
TrafficMode string `json:"traffic_mode"`
TrafficInGB int `json:"traffic_in_gb"`
TrafficOutGB int `json:"traffic_out_gb"`
IOSpeedMBps int `json:"io_speed_mbps"`
+ IOReadMBps int `json:"io_read_mbps"`
+ IOWriteMBps int `json:"io_write_mbps"`
ExtraPorts []int `json:"extra_ports"`
PortMappingCount int `json:"port_mapping_count"`
AssignNAT *bool `json:"assign_nat,omitempty"`
@@ -55,10 +59,12 @@ func parseSavedTaskConfig(raw string) savedTaskConfig {
}
var cfg savedTaskConfig
_ = json.Unmarshal([]byte(raw), &cfg)
+ normalizeSavedTaskConfigLimits(&cfg)
return cfg
}
func encodeSavedTaskConfig(cfg savedTaskConfig) string {
+ normalizeSavedTaskConfigLimits(&cfg)
data, err := json.Marshal(cfg)
if err != nil {
return ""
@@ -66,6 +72,41 @@ func encodeSavedTaskConfig(cfg savedTaskConfig) string {
return string(data)
}
+func normalizeSavedTaskConfigLimits(cfg *savedTaskConfig) {
+ if cfg == nil {
+ return
+ }
+ if cfg.NetworkBWMbps < 0 {
+ cfg.NetworkBWMbps = 0
+ }
+ if cfg.NetworkDownMbps < 0 {
+ cfg.NetworkDownMbps = 0
+ }
+ if cfg.NetworkUpMbps < 0 {
+ cfg.NetworkUpMbps = 0
+ }
+ if cfg.NetworkDownMbps == 0 && cfg.NetworkUpMbps == 0 && cfg.NetworkBWMbps > 0 {
+ cfg.NetworkDownMbps = cfg.NetworkBWMbps
+ cfg.NetworkUpMbps = cfg.NetworkBWMbps
+ }
+ cfg.NetworkBWMbps = LegacySymmetricLimit(cfg.NetworkDownMbps, cfg.NetworkUpMbps)
+
+ if cfg.IOSpeedMBps < 0 {
+ cfg.IOSpeedMBps = 0
+ }
+ if cfg.IOReadMBps < 0 {
+ cfg.IOReadMBps = 0
+ }
+ if cfg.IOWriteMBps < 0 {
+ cfg.IOWriteMBps = 0
+ }
+ if cfg.IOReadMBps == 0 && cfg.IOWriteMBps == 0 && cfg.IOSpeedMBps > 0 {
+ cfg.IOReadMBps = cfg.IOSpeedMBps
+ cfg.IOWriteMBps = cfg.IOSpeedMBps
+ }
+ cfg.IOSpeedMBps = LegacySymmetricLimit(cfg.IOReadMBps, cfg.IOWriteMBps)
+}
+
func encodeStringSlice(values []string) string {
if len(values) == 0 {
return ""
@@ -148,6 +189,8 @@ func ensureSchema() error {
ram_mb INTEGER,
disk_gb INTEGER,
network_bw_mbps INTEGER,
+ network_down_mbps INTEGER NOT NULL DEFAULT 0,
+ network_up_mbps INTEGER NOT NULL DEFAULT 0,
monthly_traffic_gb INTEGER,
traffic_mode TEXT,
traffic_in_gb INTEGER,
@@ -156,6 +199,8 @@ func ensureSchema() error {
traffic_used_tx INTEGER,
traffic_reset_date TEXT,
io_speed_mbps INTEGER,
+ io_read_mbps INTEGER NOT NULL DEFAULT 0,
+ io_write_mbps INTEGER NOT NULL DEFAULT 0,
status TEXT,
ip TEXT,
ipv6 TEXT,
@@ -282,11 +327,15 @@ func ensureSchema() error {
cfg_ram_mb INTEGER,
cfg_disk_gb INTEGER,
cfg_network_bw_mbps INTEGER,
+ cfg_network_down_mbps INTEGER NOT NULL DEFAULT 0,
+ cfg_network_up_mbps INTEGER NOT NULL DEFAULT 0,
cfg_monthly_traffic_gb INTEGER,
cfg_traffic_mode TEXT,
cfg_traffic_in_gb INTEGER,
cfg_traffic_out_gb INTEGER,
cfg_io_speed_mbps INTEGER,
+ cfg_io_read_mbps INTEGER NOT NULL DEFAULT 0,
+ cfg_io_write_mbps INTEGER NOT NULL DEFAULT 0,
cfg_port_mapping_count INTEGER,
cfg_assign_nat INTEGER,
cfg_snapshot_limit INTEGER,
@@ -340,6 +389,7 @@ func ensureSchema() error {
}
func ensureSchemaMigrations() error {
+ added := map[string]bool{}
for _, column := range []struct {
table string
name string
@@ -352,6 +402,10 @@ func ensureSchemaMigrations() error {
{"api_keys", "last_used_ip", "TEXT"},
{"tasks", "ip", "TEXT"},
{"tasks", "user_agent", "TEXT"},
+ {"tasks", "cfg_network_down_mbps", "INTEGER NOT NULL DEFAULT 0"},
+ {"tasks", "cfg_network_up_mbps", "INTEGER NOT NULL DEFAULT 0"},
+ {"tasks", "cfg_io_read_mbps", "INTEGER NOT NULL DEFAULT 0"},
+ {"tasks", "cfg_io_write_mbps", "INTEGER NOT NULL DEFAULT 0"},
{"tasks", "cfg_assign_ipv4", "INTEGER"},
{"tasks", "cfg_ipv4_count", "INTEGER"},
{"tasks", "cfg_public_ipv4s", "TEXT"},
@@ -364,21 +418,61 @@ func ensureSchemaMigrations() error {
{"port_mappings", "host_ip", "TEXT"},
{"container_public_ipv4s", "prefix_len", "INTEGER"},
{"container_public_ipv4s", "gateway", "TEXT"},
+ {"containers", "network_down_mbps", "INTEGER NOT NULL DEFAULT 0"},
+ {"containers", "network_up_mbps", "INTEGER NOT NULL DEFAULT 0"},
+ {"containers", "io_read_mbps", "INTEGER NOT NULL DEFAULT 0"},
+ {"containers", "io_write_mbps", "INTEGER NOT NULL DEFAULT 0"},
{"containers", "firewall_enabled", "INTEGER NOT NULL DEFAULT 0"},
{"containers", "firewall_default_action", "TEXT NOT NULL DEFAULT 'DROP'"},
{"containers", "firewall_rules", "TEXT"},
} {
- if err := ensureColumn(column.table, column.name, column.def); err != nil {
+ wasAdded, err := ensureColumn(column.table, column.name, column.def)
+ if err != nil {
+ return err
+ }
+ if wasAdded {
+ added[column.table+"."+column.name] = true
+ }
+ }
+ if added["containers.network_down_mbps"] || added["containers.network_up_mbps"] {
+ if _, err := db.Exec(`UPDATE containers
+ SET network_down_mbps = COALESCE(NULLIF(network_down_mbps, 0), COALESCE(network_bw_mbps, 0)),
+ network_up_mbps = COALESCE(NULLIF(network_up_mbps, 0), COALESCE(network_bw_mbps, 0))
+ WHERE COALESCE(network_bw_mbps, 0) > 0`); err != nil {
+ return err
+ }
+ }
+ if added["containers.io_read_mbps"] || added["containers.io_write_mbps"] {
+ if _, err := db.Exec(`UPDATE containers
+ SET io_read_mbps = COALESCE(NULLIF(io_read_mbps, 0), COALESCE(io_speed_mbps, 0)),
+ io_write_mbps = COALESCE(NULLIF(io_write_mbps, 0), COALESCE(io_speed_mbps, 0))
+ WHERE COALESCE(io_speed_mbps, 0) > 0`); err != nil {
+ return err
+ }
+ }
+ if added["tasks.cfg_network_down_mbps"] || added["tasks.cfg_network_up_mbps"] {
+ if _, err := db.Exec(`UPDATE tasks
+ SET cfg_network_down_mbps = COALESCE(NULLIF(cfg_network_down_mbps, 0), COALESCE(cfg_network_bw_mbps, 0)),
+ cfg_network_up_mbps = COALESCE(NULLIF(cfg_network_up_mbps, 0), COALESCE(cfg_network_bw_mbps, 0))
+ WHERE COALESCE(cfg_network_bw_mbps, 0) > 0`); err != nil {
+ return err
+ }
+ }
+ if added["tasks.cfg_io_read_mbps"] || added["tasks.cfg_io_write_mbps"] {
+ if _, err := db.Exec(`UPDATE tasks
+ SET cfg_io_read_mbps = COALESCE(NULLIF(cfg_io_read_mbps, 0), COALESCE(cfg_io_speed_mbps, 0)),
+ cfg_io_write_mbps = COALESCE(NULLIF(cfg_io_write_mbps, 0), COALESCE(cfg_io_speed_mbps, 0))
+ WHERE COALESCE(cfg_io_speed_mbps, 0) > 0`); err != nil {
return err
}
}
return nil
}
-func ensureColumn(table, name, def string) error {
+func ensureColumn(table, name, def string) (bool, error) {
rows, err := db.Query("PRAGMA table_info(" + table + ")")
if err != nil {
- return err
+ return false, err
}
defer rows.Close()
for rows.Next() {
@@ -387,17 +481,17 @@ func ensureColumn(table, name, def string) error {
var notNull, pk int
var defaultValue interface{}
if err := rows.Scan(&cid, &columnName, &columnType, ¬Null, &defaultValue, &pk); err != nil {
- return err
+ return false, err
}
if columnName == name {
- return nil
+ return false, nil
}
}
if err := rows.Err(); err != nil {
- return err
+ return false, err
}
_, err = db.Exec("ALTER TABLE " + table + " ADD COLUMN " + name + " " + def)
- return err
+ return err == nil, err
}
func loadConfigFromDB() (*ClicdConfig, bool, error) {
@@ -578,20 +672,25 @@ func saveMeta(tx *sql.Tx) error {
func saveContainers(tx *sql.Tx) error {
for _, c := range AppConfig.Containers {
+ NormalizeContainerResourceAliases(&c)
if _, err := tx.Exec(`INSERT INTO containers (
id, uuid, name, virtualization, lxc_name, kvm_name, disk_image, mac_address, template,
- vcpu, ram_mb, disk_gb, network_bw_mbps, monthly_traffic_gb, traffic_mode, traffic_in_gb,
- traffic_out_gb, traffic_used_rx, traffic_used_tx, traffic_reset_date, io_speed_mbps,
+ vcpu, ram_mb, disk_gb, network_bw_mbps, network_down_mbps, network_up_mbps,
+ monthly_traffic_gb, traffic_mode, traffic_in_gb,
+ traffic_out_gb, traffic_used_rx, traffic_used_tx, traffic_reset_date,
+ io_speed_mbps, io_read_mbps, io_write_mbps,
status, ip, ipv6, ipv6_prefix_len, ipv6_interface, vnc_port, ssh_port, ssh_password,
ssh_host_key, port_mapping_limit, snapshot_limit, created_at, expires_at,
snapshot_schedule_enabled, snapshot_schedule_interval_hours, snapshot_schedule_time,
snapshot_schedule_last_run, snapshot_schedule_next_run, snapshot_schedule_created_by,
policy_blocked, policy_blocked_reason, policy_blocked_at,
firewall_enabled, firewall_default_action, firewall_rules
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
c.ID, c.UUID, c.Name, c.Virtualization, c.LXCName, c.KVMName, c.DiskImage, c.MACAddress, c.Template,
- c.VCPU, c.RAMMB, c.DiskGB, c.NetworkBWMbps, c.MonthlyTrafficGB, c.TrafficMode, c.TrafficInGB,
- c.TrafficOutGB, c.TrafficUsedRX, c.TrafficUsedTX, c.TrafficResetDate, c.IOSpeedMBps,
+ c.VCPU, c.RAMMB, c.DiskGB, c.NetworkBWMbps, c.NetworkDownMbps, c.NetworkUpMbps,
+ c.MonthlyTrafficGB, c.TrafficMode, c.TrafficInGB,
+ c.TrafficOutGB, c.TrafficUsedRX, c.TrafficUsedTX, c.TrafficResetDate,
+ c.IOSpeedMBps, c.IOReadMBps, c.IOWriteMBps,
c.Status, c.IP, c.IPv6, c.IPv6PrefixLen, c.IPv6Interface, c.VNCPort, c.SSHPort, c.SSHPassword,
c.SSHHostKey, c.PortMappingLimit, c.SnapshotLimit, c.CreatedAt, c.ExpiresAt,
boolInt(c.SnapshotScheduleEnabled), c.SnapshotScheduleIntervalHours, c.SnapshotScheduleTime,
@@ -732,15 +831,19 @@ func saveTasksDB(tx *sql.Tx) error {
if _, err := tx.Exec(`INSERT INTO tasks(
id, type, container_id, container_name, status, error, created_at, template_id, user, ip, user_agent,
cfg_name, cfg_virtualization, cfg_template_id, cfg_vcpu, cfg_cpu_percent, cfg_ram_mb, cfg_disk_gb,
- cfg_network_bw_mbps, cfg_monthly_traffic_gb, cfg_traffic_mode, cfg_traffic_in_gb,
- cfg_traffic_out_gb, cfg_io_speed_mbps, cfg_port_mapping_count, cfg_assign_nat, cfg_snapshot_limit,
+ cfg_network_bw_mbps, cfg_network_down_mbps, cfg_network_up_mbps,
+ cfg_monthly_traffic_gb, cfg_traffic_mode, cfg_traffic_in_gb,
+ cfg_traffic_out_gb, cfg_io_speed_mbps, cfg_io_read_mbps, cfg_io_write_mbps,
+ cfg_port_mapping_count, cfg_assign_nat, cfg_snapshot_limit,
cfg_assign_ipv4, cfg_ipv4_count, cfg_public_ipv4s, cfg_assign_ipv6, cfg_ipv6_count, cfg_ipv6_addresses,
cfg_ssh_auth_mode, cfg_ssh_password, cfg_ssh_public_key, cfg_expires_at
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
task.ID, task.Type, task.ContainerID, task.ContainerName, task.Status, task.Error, task.CreatedAt, task.TemplateID, task.User, task.IP, task.UserAgent,
cfg.Name, cfg.Virtualization, cfg.TemplateID, cfg.VCPU, cfg.CPUPercent, cfg.RAMMB, cfg.DiskGB,
- cfg.NetworkBWMbps, cfg.MonthlyTrafficGB, cfg.TrafficMode, cfg.TrafficInGB,
- cfg.TrafficOutGB, cfg.IOSpeedMBps, cfg.PortMappingCount, boolPtrInt(cfg.AssignNAT), cfg.SnapshotLimit,
+ cfg.NetworkBWMbps, cfg.NetworkDownMbps, cfg.NetworkUpMbps,
+ cfg.MonthlyTrafficGB, cfg.TrafficMode, cfg.TrafficInGB,
+ cfg.TrafficOutGB, cfg.IOSpeedMBps, cfg.IOReadMBps, cfg.IOWriteMBps,
+ cfg.PortMappingCount, boolPtrInt(cfg.AssignNAT), cfg.SnapshotLimit,
boolInt(cfg.AssignIPv4), cfg.IPv4Count, encodeStringSlice(cfg.PublicIPv4s),
boolInt(cfg.AssignIPv6), cfg.IPv6Count, encodeStringSlice(cfg.IPv6Addresses),
cfg.SSHAuthMode, cfg.SSHPassword, cfg.SSHPublicKey, cfg.ExpiresAt,
@@ -788,8 +891,10 @@ func saveSnapshots(tx *sql.Tx) error {
func loadContainers() ([]Container, error) {
rows, err := db.Query(`SELECT
id, uuid, name, virtualization, lxc_name, kvm_name, disk_image, mac_address, template,
- vcpu, ram_mb, disk_gb, network_bw_mbps, monthly_traffic_gb, traffic_mode, traffic_in_gb,
- traffic_out_gb, traffic_used_rx, traffic_used_tx, traffic_reset_date, io_speed_mbps,
+ vcpu, ram_mb, disk_gb, network_bw_mbps, network_down_mbps, network_up_mbps,
+ monthly_traffic_gb, traffic_mode, traffic_in_gb,
+ traffic_out_gb, traffic_used_rx, traffic_used_tx, traffic_reset_date,
+ io_speed_mbps, io_read_mbps, io_write_mbps,
status, ip, ipv6, ipv6_prefix_len, ipv6_interface, vnc_port, ssh_port, ssh_password,
ssh_host_key, port_mapping_limit, snapshot_limit, created_at, expires_at,
snapshot_schedule_enabled, snapshot_schedule_interval_hours, snapshot_schedule_time,
@@ -810,8 +915,10 @@ func loadContainers() ([]Container, error) {
var firewallRulesJSON sql.NullString
if err := rows.Scan(
&c.ID, &c.UUID, &c.Name, &c.Virtualization, &c.LXCName, &c.KVMName, &c.DiskImage, &c.MACAddress, &c.Template,
- &c.VCPU, &c.RAMMB, &c.DiskGB, &c.NetworkBWMbps, &c.MonthlyTrafficGB, &c.TrafficMode, &c.TrafficInGB,
- &c.TrafficOutGB, &c.TrafficUsedRX, &c.TrafficUsedTX, &c.TrafficResetDate, &c.IOSpeedMBps,
+ &c.VCPU, &c.RAMMB, &c.DiskGB, &c.NetworkBWMbps, &c.NetworkDownMbps, &c.NetworkUpMbps,
+ &c.MonthlyTrafficGB, &c.TrafficMode, &c.TrafficInGB,
+ &c.TrafficOutGB, &c.TrafficUsedRX, &c.TrafficUsedTX, &c.TrafficResetDate,
+ &c.IOSpeedMBps, &c.IOReadMBps, &c.IOWriteMBps,
&c.Status, &c.IP, &c.IPv6, &c.IPv6PrefixLen, &c.IPv6Interface, &c.VNCPort, &c.SSHPort, &c.SSHPassword,
&c.SSHHostKey, &c.PortMappingLimit, &c.SnapshotLimit, &c.CreatedAt, &c.ExpiresAt,
&scheduleEnabled, &c.SnapshotScheduleIntervalHours, &c.SnapshotScheduleTime,
@@ -828,6 +935,7 @@ func loadContainers() ([]Container, error) {
if firewallRulesJSON.Valid && strings.TrimSpace(firewallRulesJSON.String) != "" {
_ = json.Unmarshal([]byte(firewallRulesJSON.String), &c.FirewallRules)
}
+ NormalizeContainerResourceAliases(&c)
result = append(result, c)
}
if err := rows.Err(); err != nil {
@@ -1021,8 +1129,10 @@ func loadTasks() ([]SavedTask, error) {
rows, err := db.Query(`SELECT
id, type, container_id, container_name, status, error, created_at, template_id, user, ip, user_agent,
cfg_name, cfg_virtualization, cfg_template_id, cfg_vcpu, cfg_cpu_percent, cfg_ram_mb, cfg_disk_gb,
- cfg_network_bw_mbps, cfg_monthly_traffic_gb, cfg_traffic_mode, cfg_traffic_in_gb,
- cfg_traffic_out_gb, cfg_io_speed_mbps, cfg_port_mapping_count, cfg_assign_nat, cfg_snapshot_limit,
+ cfg_network_bw_mbps, cfg_network_down_mbps, cfg_network_up_mbps,
+ cfg_monthly_traffic_gb, cfg_traffic_mode, cfg_traffic_in_gb,
+ cfg_traffic_out_gb, cfg_io_speed_mbps, cfg_io_read_mbps, cfg_io_write_mbps,
+ cfg_port_mapping_count, cfg_assign_nat, cfg_snapshot_limit,
cfg_assign_ipv4, cfg_ipv4_count, cfg_public_ipv4s, cfg_assign_ipv6, cfg_ipv6_count, cfg_ipv6_addresses,
cfg_ssh_auth_mode, cfg_ssh_password, cfg_ssh_public_key, cfg_expires_at
FROM tasks ORDER BY created_at, id`)
@@ -1042,8 +1152,10 @@ func loadTasks() ([]SavedTask, error) {
if err := rows.Scan(
&t.ID, &t.Type, &t.ContainerID, &t.ContainerName, &t.Status, &t.Error, &t.CreatedAt, &t.TemplateID, &t.User, &ip, &userAgent,
&cfg.Name, &cfg.Virtualization, &cfg.TemplateID, &cfg.VCPU, &cfg.CPUPercent, &cfg.RAMMB, &cfg.DiskGB,
- &cfg.NetworkBWMbps, &cfg.MonthlyTrafficGB, &cfg.TrafficMode, &cfg.TrafficInGB,
- &cfg.TrafficOutGB, &cfg.IOSpeedMBps, &cfg.PortMappingCount, &assignNAT, &cfg.SnapshotLimit,
+ &cfg.NetworkBWMbps, &cfg.NetworkDownMbps, &cfg.NetworkUpMbps,
+ &cfg.MonthlyTrafficGB, &cfg.TrafficMode, &cfg.TrafficInGB,
+ &cfg.TrafficOutGB, &cfg.IOSpeedMBps, &cfg.IOReadMBps, &cfg.IOWriteMBps,
+ &cfg.PortMappingCount, &assignNAT, &cfg.SnapshotLimit,
&assignIPv4, &ipv4Count, &publicIPv4s, &assignIPv6, &ipv6Count, &ipv6Addresses,
&sshAuthMode, &sshPassword, &sshPublicKey, &cfg.ExpiresAt,
); err != nil {
@@ -1068,6 +1180,7 @@ func loadTasks() ([]SavedTask, error) {
cfg.SSHAuthMode = sshAuthMode.String
cfg.SSHPassword = sshPassword.String
cfg.SSHPublicKey = sshPublicKey.String
+ normalizeSavedTaskConfigLimits(&cfg)
result = append(result, t)
configs = append(configs, cfg)
}
diff --git a/backend/internal/kvm/kvm.go b/backend/internal/kvm/kvm.go
index fd10d04..9515089 100644
--- a/backend/internal/kvm/kvm.go
+++ b/backend/internal/kvm/kvm.go
@@ -346,6 +346,7 @@ func normalizeQCOW2(ctx context.Context, src, target string) error {
}
func (m *Manager) CreateContainer(cfg lxc.ContainerConfig) error {
+ cfg.NormalizeResourceAliases()
image := FindImage(cfg.TemplateID)
if image == nil {
return fmt.Errorf("KVM image not found: %s", cfg.TemplateID)
@@ -456,7 +457,7 @@ func (m *Manager) defineContainer(id int, vmName string, cfg lxc.ContainerConfig
if err := createWindowsUnattendISO(unattendPath, cfg.Name, winAdminPassword, ipv6List, ipv4List); err != nil {
return nil, err
}
- xml = windowsDomainXML(vmName, int(cfg.VCPU), cfg.RAMMB, diskPath, ImagePath(image.ID), unattendPath, mac, cfg.IOSpeedMBps, cfg.NetworkBWMbps)
+ xml = windowsDomainXML(vmName, int(cfg.VCPU), cfg.RAMMB, diskPath, ImagePath(image.ID), unattendPath, mac, cfg.IOReadMBps, cfg.IOWriteMBps, cfg.NetworkDownMbps, cfg.NetworkUpMbps)
} else {
if image.Desktop != "" {
if cfg.RAMMB < 2048 {
@@ -472,7 +473,7 @@ func (m *Manager) defineContainer(id int, vmName string, cfg lxc.ContainerConfig
if err := createSeedISO(seedPath, vmName, cfg.Name, sshPassword, sshPublicKey, mac, ipv6List, ipv4List, *image, sshAuthMode); err != nil {
return nil, err
}
- xml = domainXML(vmName, int(cfg.VCPU), cfg.RAMMB, diskPath, seedPath, mac, cfg.IOSpeedMBps, cfg.NetworkBWMbps, image.Desktop != "")
+ xml = domainXML(vmName, int(cfg.VCPU), cfg.RAMMB, diskPath, seedPath, mac, cfg.IOReadMBps, cfg.IOWriteMBps, cfg.NetworkDownMbps, cfg.NetworkUpMbps, image.Desktop != "")
}
xmlPath := filepath.Join(m.instanceDir(vmName), "domain.xml")
if err := os.WriteFile(xmlPath, []byte(xml), 0644); err != nil {
@@ -542,12 +543,16 @@ func (m *Manager) defineContainer(id int, vmName string, cfg lxc.ContainerConfig
RAMMB: cfg.RAMMB,
DiskGB: cfg.DiskGB,
NetworkBWMbps: cfg.NetworkBWMbps,
+ NetworkDownMbps: cfg.NetworkDownMbps,
+ NetworkUpMbps: cfg.NetworkUpMbps,
MonthlyTrafficGB: cfg.MonthlyTrafficGB,
TrafficMode: trafficMode,
TrafficInGB: cfg.TrafficInGB,
TrafficOutGB: cfg.TrafficOutGB,
TrafficResetDate: now[:7],
IOSpeedMBps: cfg.IOSpeedMBps,
+ IOReadMBps: cfg.IOReadMBps,
+ IOWriteMBps: cfg.IOWriteMBps,
PublicIPv4s: publicIPv4s,
IPv6Addresses: ipv6Assignments,
Status: "stopped",
@@ -781,11 +786,15 @@ func (m *Manager) ReinstallContainer(id int, templateID string, authConfig ...lx
RAMMB: c.RAMMB,
DiskGB: c.DiskGB,
NetworkBWMbps: c.NetworkBWMbps,
+ NetworkDownMbps: c.NetworkDownMbps,
+ NetworkUpMbps: c.NetworkUpMbps,
MonthlyTrafficGB: c.MonthlyTrafficGB,
TrafficMode: c.TrafficMode,
TrafficInGB: c.TrafficInGB,
TrafficOutGB: c.TrafficOutGB,
IOSpeedMBps: c.IOSpeedMBps,
+ IOReadMBps: c.IOReadMBps,
+ IOWriteMBps: c.IOWriteMBps,
PortMappingCount: c.PortMappingLimit,
SnapshotLimit: c.SnapshotLimit,
ExpiresAt: c.ExpiresAt,
@@ -882,6 +891,7 @@ func (m *Manager) ApplyContainerLimits(c *config.Container) error {
if c == nil || !c.IsKVM() {
return nil
}
+ config.NormalizeContainerResourceAliases(c)
if c.Status == "running" {
// Config already saved; domain definition will be refreshed on next start
return nil
@@ -893,10 +903,10 @@ func (m *Manager) ApplyContainerLimits(c *config.Container) error {
if IsWindowsImage(c.Template) {
winISO := ImagePath(c.Template)
unattendISO := existingWindowsUnattendISO(m.instanceDir(c.VirshName()))
- xml = windowsDomainXML(c.VirshName(), int(c.VCPU), c.RAMMB, c.DiskImage, winISO, unattendISO, c.MACAddress, c.IOSpeedMBps, c.NetworkBWMbps)
+ xml = windowsDomainXML(c.VirshName(), int(c.VCPU), c.RAMMB, c.DiskImage, winISO, unattendISO, c.MACAddress, c.IOReadMBps, c.IOWriteMBps, c.NetworkDownMbps, c.NetworkUpMbps)
} else {
seedPath := filepath.Join(m.instanceDir(c.VirshName()), "seed.iso")
- xml = domainXML(c.VirshName(), int(c.VCPU), c.RAMMB, c.DiskImage, seedPath, c.MACAddress, c.IOSpeedMBps, c.NetworkBWMbps, isKVMDesktopTemplate(c.Template))
+ xml = domainXML(c.VirshName(), int(c.VCPU), c.RAMMB, c.DiskImage, seedPath, c.MACAddress, c.IOReadMBps, c.IOWriteMBps, c.NetworkDownMbps, c.NetworkUpMbps, isKVMDesktopTemplate(c.Template))
}
xmlPath := filepath.Join(m.instanceDir(c.VirshName()), "domain.xml")
if err := os.WriteFile(xmlPath, []byte(xml), 0644); err != nil {
@@ -913,15 +923,16 @@ func (m *Manager) ensureDomainDefinition(c *config.Container) error {
if c == nil || !c.IsKVM() || c.DiskImage == "" || c.MACAddress == "" {
return nil
}
+ config.NormalizeContainerResourceAliases(c)
var xml string
xmlPath := filepath.Join(m.instanceDir(c.VirshName()), "domain.xml")
if IsWindowsImage(c.Template) {
winISO := ImagePath(c.Template)
unattendISO := existingWindowsUnattendISO(m.instanceDir(c.VirshName()))
- xml = windowsDomainXML(c.VirshName(), int(c.VCPU), c.RAMMB, c.DiskImage, winISO, unattendISO, c.MACAddress, c.IOSpeedMBps, c.NetworkBWMbps)
+ xml = windowsDomainXML(c.VirshName(), int(c.VCPU), c.RAMMB, c.DiskImage, winISO, unattendISO, c.MACAddress, c.IOReadMBps, c.IOWriteMBps, c.NetworkDownMbps, c.NetworkUpMbps)
} else {
seedPath := filepath.Join(m.instanceDir(c.VirshName()), "seed.iso")
- xml = domainXML(c.VirshName(), int(c.VCPU), c.RAMMB, c.DiskImage, seedPath, c.MACAddress, c.IOSpeedMBps, c.NetworkBWMbps, isKVMDesktopTemplate(c.Template))
+ xml = domainXML(c.VirshName(), int(c.VCPU), c.RAMMB, c.DiskImage, seedPath, c.MACAddress, c.IOReadMBps, c.IOWriteMBps, c.NetworkDownMbps, c.NetworkUpMbps, isKVMDesktopTemplate(c.Template))
}
if err := os.WriteFile(xmlPath, []byte(xml), 0644); err != nil {
return err
@@ -2079,7 +2090,7 @@ func isKVMDesktopTemplate(templateID string) bool {
return image != nil && image.Desktop != ""
}
-func domainXML(name string, vcpu int, ramMB int, diskPath, seedPath, mac string, ioSpeedMBps int, networkBWMbps int, desktop bool) string {
+func domainXML(name string, vcpu int, ramMB int, diskPath, seedPath, mac string, ioReadMBps int, ioWriteMBps int, networkDownMbps int, networkUpMbps int, desktop bool) string {
if vcpu < 1 {
vcpu = 1
}
@@ -2087,21 +2098,32 @@ func domainXML(name string, vcpu int, ramMB int, diskPath, seedPath, mac string,
ramMB = 512
}
iotune := ""
- if ioSpeedMBps > 0 {
- bytesPerSecond := int64(ioSpeedMBps) * 1024 * 1024
+ if ioReadMBps > 0 || ioWriteMBps > 0 {
+ var parts []string
+ if ioReadMBps > 0 {
+ parts = append(parts, fmt.Sprintf("
{resourceErrors.disk_gb}
}