mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-04 21:31:23 +08:00
FIX #18
This commit is contained in:
@@ -46,6 +46,19 @@ type ipv4Route struct {
|
|||||||
Gateway string `json:"gateway,omitempty"`
|
Gateway string `json:"gateway,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type lanDHCPRoute struct {
|
||||||
|
ContainerID int `json:"container_id"`
|
||||||
|
ContainerName string `json:"container_name"`
|
||||||
|
LXCName string `json:"lxc_name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
Interface string `json:"interface"`
|
||||||
|
PrefixLen int `json:"prefix_len,omitempty"`
|
||||||
|
Gateway string `json:"gateway,omitempty"`
|
||||||
|
MACAddress string `json:"mac_address,omitempty"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
}
|
||||||
|
|
||||||
type ipv6Route struct {
|
type ipv6Route struct {
|
||||||
ContainerID int `json:"container_id"`
|
ContainerID int `json:"container_id"`
|
||||||
ContainerName string `json:"container_name"`
|
ContainerName string `json:"container_name"`
|
||||||
@@ -60,10 +73,12 @@ type routingResponse struct {
|
|||||||
NAT4 routeCapacity `json:"nat4"`
|
NAT4 routeCapacity `json:"nat4"`
|
||||||
NAT4PortRange nat4PortRange `json:"nat4_port_range"`
|
NAT4PortRange nat4PortRange `json:"nat4_port_range"`
|
||||||
IPv4 routeCapacity `json:"ipv4"`
|
IPv4 routeCapacity `json:"ipv4"`
|
||||||
|
LANDHCP routeCapacity `json:"lan_dhcp"`
|
||||||
IPv6 routeCapacity `json:"ipv6"`
|
IPv6 routeCapacity `json:"ipv6"`
|
||||||
HostPublicIPv4 lxc.PublicIPInfo `json:"host_public_ipv4"`
|
HostPublicIPv4 lxc.PublicIPInfo `json:"host_public_ipv4"`
|
||||||
PublicIPv4Addresses []lxc.PublicIPInfo `json:"public_ipv4_addresses"`
|
PublicIPv4Addresses []lxc.PublicIPInfo `json:"public_ipv4_addresses"`
|
||||||
IPv4Assignments []ipv4Route `json:"ipv4_assignments"`
|
IPv4Assignments []ipv4Route `json:"ipv4_assignments"`
|
||||||
|
LANDHCPAssignments []lanDHCPRoute `json:"lan_dhcp_assignments"`
|
||||||
NAT4Mappings []nat4Route `json:"nat4_mappings"`
|
NAT4Mappings []nat4Route `json:"nat4_mappings"`
|
||||||
IPv6Assignments []ipv6Route `json:"ipv6_assignments"`
|
IPv6Assignments []ipv6Route `json:"ipv6_assignments"`
|
||||||
IPv6Prefixes []lxc.IPv6PrefixInfo `json:"ipv6_prefixes"`
|
IPv6Prefixes []lxc.IPv6PrefixInfo `json:"ipv6_prefixes"`
|
||||||
@@ -125,6 +140,7 @@ func handleRoutingGet(w http.ResponseWriter, r *http.Request) {
|
|||||||
nat4Mappings := make([]nat4Route, 0)
|
nat4Mappings := make([]nat4Route, 0)
|
||||||
usedPorts := map[int]bool{}
|
usedPorts := map[int]bool{}
|
||||||
ipv4Assignments := make([]ipv4Route, 0)
|
ipv4Assignments := make([]ipv4Route, 0)
|
||||||
|
lanDHCPAssignments := make([]lanDHCPRoute, 0)
|
||||||
ipv6Assignments := make([]ipv6Route, 0)
|
ipv6Assignments := make([]ipv6Route, 0)
|
||||||
|
|
||||||
nat4StartPort, nat4EndPort := config.NATPortRange()
|
nat4StartPort, nat4EndPort := config.NATPortRange()
|
||||||
@@ -164,6 +180,20 @@ func handleRoutingGet(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
c.NormalizeNetworkAssignments()
|
c.NormalizeNetworkAssignments()
|
||||||
|
if c.UsesLANIPv4() {
|
||||||
|
lanDHCPAssignments = append(lanDHCPAssignments, lanDHCPRoute{
|
||||||
|
ContainerID: c.ID,
|
||||||
|
ContainerName: c.Name,
|
||||||
|
LXCName: c.LxcName(),
|
||||||
|
Status: c.Status,
|
||||||
|
Address: c.IP,
|
||||||
|
Interface: c.LANInterface,
|
||||||
|
PrefixLen: c.LANIPv4PrefixLen,
|
||||||
|
Gateway: c.LANIPv4Gateway,
|
||||||
|
MACAddress: c.MACAddress,
|
||||||
|
Mode: c.LANIPv4Mode,
|
||||||
|
})
|
||||||
|
}
|
||||||
for _, ip := range c.IPv6Addresses {
|
for _, ip := range c.IPv6Addresses {
|
||||||
if ip.Address == "" {
|
if ip.Address == "" {
|
||||||
continue
|
continue
|
||||||
@@ -191,6 +221,12 @@ func handleRoutingGet(w http.ResponseWriter, r *http.Request) {
|
|||||||
sort.SliceStable(ipv4Assignments, func(i, j int) bool {
|
sort.SliceStable(ipv4Assignments, func(i, j int) bool {
|
||||||
return ipv4Assignments[i].Address < ipv4Assignments[j].Address
|
return ipv4Assignments[i].Address < ipv4Assignments[j].Address
|
||||||
})
|
})
|
||||||
|
sort.SliceStable(lanDHCPAssignments, func(i, j int) bool {
|
||||||
|
if lanDHCPAssignments[i].Interface == lanDHCPAssignments[j].Interface {
|
||||||
|
return lanDHCPAssignments[i].ContainerName < lanDHCPAssignments[j].ContainerName
|
||||||
|
}
|
||||||
|
return lanDHCPAssignments[i].Interface < lanDHCPAssignments[j].Interface
|
||||||
|
})
|
||||||
sort.SliceStable(ipv6Assignments, func(i, j int) bool {
|
sort.SliceStable(ipv6Assignments, func(i, j int) bool {
|
||||||
return ipv6Assignments[i].Address < ipv6Assignments[j].Address
|
return ipv6Assignments[i].Address < ipv6Assignments[j].Address
|
||||||
})
|
})
|
||||||
@@ -231,6 +267,11 @@ func handleRoutingGet(w http.ResponseWriter, r *http.Request) {
|
|||||||
Remaining: strconv.Itoa(ipv4Remaining),
|
Remaining: strconv.Itoa(ipv4Remaining),
|
||||||
Total: strconv.Itoa(ipv4Total),
|
Total: strconv.Itoa(ipv4Total),
|
||||||
},
|
},
|
||||||
|
LANDHCP: routeCapacity{
|
||||||
|
Used: len(lanDHCPAssignments),
|
||||||
|
Remaining: "DHCP",
|
||||||
|
Total: "DHCP",
|
||||||
|
},
|
||||||
IPv6: routeCapacity{
|
IPv6: routeCapacity{
|
||||||
Used: len(ipv6Assignments),
|
Used: len(ipv6Assignments),
|
||||||
Remaining: ipv6Remaining,
|
Remaining: ipv6Remaining,
|
||||||
@@ -239,6 +280,7 @@ func handleRoutingGet(w http.ResponseWriter, r *http.Request) {
|
|||||||
HostPublicIPv4: hostPublicIPv4,
|
HostPublicIPv4: hostPublicIPv4,
|
||||||
PublicIPv4Addresses: publicIPv4s,
|
PublicIPv4Addresses: publicIPv4s,
|
||||||
IPv4Assignments: ipv4Assignments,
|
IPv4Assignments: ipv4Assignments,
|
||||||
|
LANDHCPAssignments: lanDHCPAssignments,
|
||||||
NAT4Mappings: nat4Mappings,
|
NAT4Mappings: nat4Mappings,
|
||||||
IPv6Assignments: ipv6Assignments,
|
IPv6Assignments: ipv6Assignments,
|
||||||
IPv6Prefixes: prefixes,
|
IPv6Prefixes: prefixes,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func runtimeFromRequest(value string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func hasRequestedNetwork(cfg lxc.ContainerConfig) bool {
|
func hasRequestedNetwork(cfg lxc.ContainerConfig) bool {
|
||||||
return cfg.WantsNAT() || cfg.AssignIPv4 || len(cfg.PublicIPv4s) > 0 || cfg.AssignIPv6 || len(cfg.IPv6Addresses) > 0
|
return cfg.WantsNAT() || cfg.WantsLANIPv4() || cfg.AssignIPv4 || len(cfg.PublicIPv4s) > 0 || cfg.AssignIPv6 || len(cfg.IPv6Addresses) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func runtimeFromTemplateID(templateID string) string {
|
func runtimeFromTemplateID(templateID string) string {
|
||||||
|
|||||||
@@ -650,6 +650,10 @@ func HandleBatchCreate(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
req.Containers[i].NormalizeResourceAliases()
|
req.Containers[i].NormalizeResourceAliases()
|
||||||
req.Containers[i].Virtualization = runtimeFromRequest(req.Containers[i].Virtualization)
|
req.Containers[i].Virtualization = runtimeFromRequest(req.Containers[i].Virtualization)
|
||||||
|
if req.Containers[i].WantsLANIPv4() && req.Containers[i].Virtualization != config.VirtualizationLXC {
|
||||||
|
jsonResponse(w, http.StatusBadRequest, APIResponse{Success: false, Message: name + ": LAN IPv4 is only supported for LXC containers"})
|
||||||
|
return
|
||||||
|
}
|
||||||
if req.Containers[i].RAMMB < 128 {
|
if req.Containers[i].RAMMB < 128 {
|
||||||
req.Containers[i].RAMMB = 512
|
req.Containers[i].RAMMB = 512
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,11 @@ type Container struct {
|
|||||||
IOWriteMBps int `json:"io_write_mbps"`
|
IOWriteMBps int `json:"io_write_mbps"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
IP string `json:"ip"`
|
IP string `json:"ip"`
|
||||||
|
LANIPv4Mode string `json:"lan_ipv4_mode,omitempty"`
|
||||||
|
LANInterface string `json:"lan_interface,omitempty"`
|
||||||
|
LANIPv4Address string `json:"lan_ipv4_address,omitempty"`
|
||||||
|
LANIPv4PrefixLen int `json:"lan_ipv4_prefix_len,omitempty"`
|
||||||
|
LANIPv4Gateway string `json:"lan_ipv4_gateway,omitempty"`
|
||||||
PublicIPv4s []PublicIPv4Assignment `json:"public_ipv4s,omitempty"`
|
PublicIPv4s []PublicIPv4Assignment `json:"public_ipv4s,omitempty"`
|
||||||
IPv6 string `json:"ipv6"`
|
IPv6 string `json:"ipv6"`
|
||||||
IPv6PrefixLen int `json:"ipv6_prefix_len"`
|
IPv6PrefixLen int `json:"ipv6_prefix_len"`
|
||||||
@@ -162,6 +167,9 @@ type Container struct {
|
|||||||
const (
|
const (
|
||||||
VirtualizationLXC = "lxc"
|
VirtualizationLXC = "lxc"
|
||||||
VirtualizationKVM = "kvm"
|
VirtualizationKVM = "kvm"
|
||||||
|
|
||||||
|
LANIPv4ModeDHCP = "dhcp"
|
||||||
|
LANIPv4ModeStatic = "static"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NormalizeVirtualization(value string) string {
|
func NormalizeVirtualization(value string) string {
|
||||||
@@ -181,8 +189,56 @@ func (c *Container) IsKVM() bool {
|
|||||||
return c.Runtime() == VirtualizationKVM
|
return c.Runtime() == VirtualizationKVM
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Container) UsesLANDHCP() bool {
|
||||||
|
return strings.EqualFold(strings.TrimSpace(c.LANIPv4Mode), LANIPv4ModeDHCP)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) UsesLANStaticIPv4() bool {
|
||||||
|
return strings.EqualFold(strings.TrimSpace(c.LANIPv4Mode), LANIPv4ModeStatic)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Container) UsesLANIPv4() bool {
|
||||||
|
return c.UsesLANDHCP() || c.UsesLANStaticIPv4()
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Container) NormalizeNetworkAssignments() bool {
|
func (c *Container) NormalizeNetworkAssignments() bool {
|
||||||
changed := false
|
changed := false
|
||||||
|
lanMode := strings.ToLower(strings.TrimSpace(c.LANIPv4Mode))
|
||||||
|
if lanMode != "" && lanMode != LANIPv4ModeDHCP && lanMode != LANIPv4ModeStatic {
|
||||||
|
lanMode = ""
|
||||||
|
}
|
||||||
|
if c.LANIPv4Mode != lanMode {
|
||||||
|
c.LANIPv4Mode = lanMode
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
lanInterface := strings.TrimSpace(c.LANInterface)
|
||||||
|
if c.LANInterface != lanInterface {
|
||||||
|
c.LANInterface = lanInterface
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
lanAddress := strings.TrimSpace(c.LANIPv4Address)
|
||||||
|
if c.LANIPv4Address != lanAddress {
|
||||||
|
c.LANIPv4Address = lanAddress
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
lanGateway := strings.TrimSpace(c.LANIPv4Gateway)
|
||||||
|
if c.LANIPv4Gateway != lanGateway {
|
||||||
|
c.LANIPv4Gateway = lanGateway
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if c.LANIPv4Mode == LANIPv4ModeDHCP {
|
||||||
|
if c.LANIPv4Address != "" {
|
||||||
|
c.LANIPv4Address = ""
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
} else if c.LANIPv4Mode != LANIPv4ModeStatic {
|
||||||
|
if c.LANIPv4Address != "" || c.LANIPv4PrefixLen != 0 || c.LANIPv4Gateway != "" {
|
||||||
|
c.LANIPv4Address = ""
|
||||||
|
c.LANIPv4PrefixLen = 0
|
||||||
|
c.LANIPv4Gateway = ""
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
seenIPv4 := map[string]bool{}
|
seenIPv4 := map[string]bool{}
|
||||||
filteredIPv4 := make([]PublicIPv4Assignment, 0, len(c.PublicIPv4s))
|
filteredIPv4 := make([]PublicIPv4Assignment, 0, len(c.PublicIPv4s))
|
||||||
for _, item := range c.PublicIPv4s {
|
for _, item := range c.PublicIPv4s {
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ type savedTaskConfig struct {
|
|||||||
ExtraPorts []int `json:"extra_ports"`
|
ExtraPorts []int `json:"extra_ports"`
|
||||||
PortMappingCount int `json:"port_mapping_count"`
|
PortMappingCount int `json:"port_mapping_count"`
|
||||||
AssignNAT *bool `json:"assign_nat,omitempty"`
|
AssignNAT *bool `json:"assign_nat,omitempty"`
|
||||||
|
LANIPv4Mode string `json:"lan_ipv4_mode,omitempty"`
|
||||||
|
LANInterface string `json:"lan_interface,omitempty"`
|
||||||
|
LANIPv4Address string `json:"lan_ipv4_address,omitempty"`
|
||||||
|
LANIPv4PrefixLen int `json:"lan_ipv4_prefix_len,omitempty"`
|
||||||
|
LANIPv4Gateway string `json:"lan_ipv4_gateway,omitempty"`
|
||||||
SnapshotLimit int `json:"snapshot_limit"`
|
SnapshotLimit int `json:"snapshot_limit"`
|
||||||
AllowedImageIDs []string `json:"allowed_image_ids,omitempty"`
|
AllowedImageIDs []string `json:"allowed_image_ids,omitempty"`
|
||||||
ImageLimitConfigured bool `json:"image_limit_configured,omitempty"`
|
ImageLimitConfigured bool `json:"image_limit_configured,omitempty"`
|
||||||
@@ -205,6 +210,11 @@ func ensureSchema() error {
|
|||||||
io_write_mbps INTEGER NOT NULL DEFAULT 0,
|
io_write_mbps INTEGER NOT NULL DEFAULT 0,
|
||||||
status TEXT,
|
status TEXT,
|
||||||
ip TEXT,
|
ip TEXT,
|
||||||
|
lan_ipv4_mode TEXT,
|
||||||
|
lan_interface TEXT,
|
||||||
|
lan_ipv4_address TEXT,
|
||||||
|
lan_ipv4_prefix_len INTEGER,
|
||||||
|
lan_ipv4_gateway TEXT,
|
||||||
ipv6 TEXT,
|
ipv6 TEXT,
|
||||||
ipv6_prefix_len INTEGER,
|
ipv6_prefix_len INTEGER,
|
||||||
ipv6_interface TEXT,
|
ipv6_interface TEXT,
|
||||||
@@ -344,6 +354,11 @@ func ensureSchema() error {
|
|||||||
cfg_io_write_mbps INTEGER NOT NULL DEFAULT 0,
|
cfg_io_write_mbps INTEGER NOT NULL DEFAULT 0,
|
||||||
cfg_port_mapping_count INTEGER,
|
cfg_port_mapping_count INTEGER,
|
||||||
cfg_assign_nat INTEGER,
|
cfg_assign_nat INTEGER,
|
||||||
|
cfg_lan_ipv4_mode TEXT,
|
||||||
|
cfg_lan_interface TEXT,
|
||||||
|
cfg_lan_ipv4_address TEXT,
|
||||||
|
cfg_lan_ipv4_prefix_len INTEGER,
|
||||||
|
cfg_lan_ipv4_gateway TEXT,
|
||||||
cfg_snapshot_limit INTEGER,
|
cfg_snapshot_limit INTEGER,
|
||||||
cfg_assign_ipv4 INTEGER,
|
cfg_assign_ipv4 INTEGER,
|
||||||
cfg_ipv4_count INTEGER,
|
cfg_ipv4_count INTEGER,
|
||||||
@@ -418,6 +433,11 @@ func ensureSchemaMigrations() error {
|
|||||||
{"tasks", "cfg_ipv4_count", "INTEGER"},
|
{"tasks", "cfg_ipv4_count", "INTEGER"},
|
||||||
{"tasks", "cfg_public_ipv4s", "TEXT"},
|
{"tasks", "cfg_public_ipv4s", "TEXT"},
|
||||||
{"tasks", "cfg_assign_nat", "INTEGER"},
|
{"tasks", "cfg_assign_nat", "INTEGER"},
|
||||||
|
{"tasks", "cfg_lan_ipv4_mode", "TEXT"},
|
||||||
|
{"tasks", "cfg_lan_interface", "TEXT"},
|
||||||
|
{"tasks", "cfg_lan_ipv4_address", "TEXT NOT NULL DEFAULT ''"},
|
||||||
|
{"tasks", "cfg_lan_ipv4_prefix_len", "INTEGER NOT NULL DEFAULT 0"},
|
||||||
|
{"tasks", "cfg_lan_ipv4_gateway", "TEXT NOT NULL DEFAULT ''"},
|
||||||
{"tasks", "cfg_ipv6_count", "INTEGER"},
|
{"tasks", "cfg_ipv6_count", "INTEGER"},
|
||||||
{"tasks", "cfg_ipv6_addresses", "TEXT"},
|
{"tasks", "cfg_ipv6_addresses", "TEXT"},
|
||||||
{"tasks", "cfg_ssh_auth_mode", "TEXT"},
|
{"tasks", "cfg_ssh_auth_mode", "TEXT"},
|
||||||
@@ -439,6 +459,11 @@ func ensureSchemaMigrations() error {
|
|||||||
{"containers", "firewall_rules", "TEXT"},
|
{"containers", "firewall_rules", "TEXT"},
|
||||||
{"containers", "allowed_image_ids", "TEXT"},
|
{"containers", "allowed_image_ids", "TEXT"},
|
||||||
{"containers", "image_limit_configured", "INTEGER NOT NULL DEFAULT 0"},
|
{"containers", "image_limit_configured", "INTEGER NOT NULL DEFAULT 0"},
|
||||||
|
{"containers", "lan_ipv4_mode", "TEXT"},
|
||||||
|
{"containers", "lan_interface", "TEXT"},
|
||||||
|
{"containers", "lan_ipv4_address", "TEXT NOT NULL DEFAULT ''"},
|
||||||
|
{"containers", "lan_ipv4_prefix_len", "INTEGER NOT NULL DEFAULT 0"},
|
||||||
|
{"containers", "lan_ipv4_gateway", "TEXT NOT NULL DEFAULT ''"},
|
||||||
} {
|
} {
|
||||||
wasAdded, err := ensureColumn(column.table, column.name, column.def)
|
wasAdded, err := ensureColumn(column.table, column.name, column.def)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -480,6 +505,18 @@ func ensureSchemaMigrations() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if _, err := db.Exec(`UPDATE containers
|
||||||
|
SET lan_ipv4_address = COALESCE(lan_ipv4_address, ''),
|
||||||
|
lan_ipv4_prefix_len = COALESCE(lan_ipv4_prefix_len, 0),
|
||||||
|
lan_ipv4_gateway = COALESCE(lan_ipv4_gateway, '')`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := db.Exec(`UPDATE tasks
|
||||||
|
SET cfg_lan_ipv4_address = COALESCE(cfg_lan_ipv4_address, ''),
|
||||||
|
cfg_lan_ipv4_prefix_len = COALESCE(cfg_lan_ipv4_prefix_len, 0),
|
||||||
|
cfg_lan_ipv4_gateway = COALESCE(cfg_lan_ipv4_gateway, '')`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -698,19 +735,21 @@ func saveContainers(tx *sql.Tx) error {
|
|||||||
monthly_traffic_gb, traffic_mode, traffic_in_gb,
|
monthly_traffic_gb, traffic_mode, traffic_in_gb,
|
||||||
traffic_out_gb, traffic_used_rx, traffic_used_tx, traffic_reset_date,
|
traffic_out_gb, traffic_used_rx, traffic_used_tx, traffic_reset_date,
|
||||||
io_speed_mbps, io_read_mbps, io_write_mbps,
|
io_speed_mbps, io_read_mbps, io_write_mbps,
|
||||||
status, ip, ipv6, ipv6_prefix_len, ipv6_interface, vnc_port, ssh_port, ssh_password,
|
status, ip, lan_ipv4_mode, lan_interface, lan_ipv4_address, lan_ipv4_prefix_len, lan_ipv4_gateway,
|
||||||
|
ipv6, ipv6_prefix_len, ipv6_interface, vnc_port, ssh_port, ssh_password,
|
||||||
ssh_host_key, port_mapping_limit, snapshot_limit, created_at, expires_at,
|
ssh_host_key, port_mapping_limit, snapshot_limit, created_at, expires_at,
|
||||||
snapshot_schedule_enabled, snapshot_schedule_interval_hours, snapshot_schedule_time,
|
snapshot_schedule_enabled, snapshot_schedule_interval_hours, snapshot_schedule_time,
|
||||||
snapshot_schedule_last_run, snapshot_schedule_next_run, snapshot_schedule_created_by,
|
snapshot_schedule_last_run, snapshot_schedule_next_run, snapshot_schedule_created_by,
|
||||||
policy_blocked, policy_blocked_reason, policy_blocked_at,
|
policy_blocked, policy_blocked_reason, policy_blocked_at,
|
||||||
firewall_enabled, firewall_default_action, firewall_rules, allowed_image_ids, image_limit_configured
|
firewall_enabled, firewall_default_action, firewall_rules, allowed_image_ids, image_limit_configured
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
c.ID, c.UUID, c.Name, c.Virtualization, c.LXCName, c.KVMName, c.DiskImage, c.MACAddress, c.Template,
|
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.NetworkDownMbps, c.NetworkUpMbps,
|
c.VCPU, c.RAMMB, c.DiskGB, c.NetworkBWMbps, c.NetworkDownMbps, c.NetworkUpMbps,
|
||||||
c.MonthlyTrafficGB, c.TrafficMode, c.TrafficInGB,
|
c.MonthlyTrafficGB, c.TrafficMode, c.TrafficInGB,
|
||||||
c.TrafficOutGB, c.TrafficUsedRX, c.TrafficUsedTX, c.TrafficResetDate,
|
c.TrafficOutGB, c.TrafficUsedRX, c.TrafficUsedTX, c.TrafficResetDate,
|
||||||
c.IOSpeedMBps, c.IOReadMBps, c.IOWriteMBps,
|
c.IOSpeedMBps, c.IOReadMBps, c.IOWriteMBps,
|
||||||
c.Status, c.IP, c.IPv6, c.IPv6PrefixLen, c.IPv6Interface, c.VNCPort, c.SSHPort, c.SSHPassword,
|
c.Status, c.IP, c.LANIPv4Mode, c.LANInterface, c.LANIPv4Address, c.LANIPv4PrefixLen, c.LANIPv4Gateway,
|
||||||
|
c.IPv6, c.IPv6PrefixLen, c.IPv6Interface, c.VNCPort, c.SSHPort, c.SSHPassword,
|
||||||
c.SSHHostKey, c.PortMappingLimit, c.SnapshotLimit, c.CreatedAt, c.ExpiresAt,
|
c.SSHHostKey, c.PortMappingLimit, c.SnapshotLimit, c.CreatedAt, c.ExpiresAt,
|
||||||
boolInt(c.SnapshotScheduleEnabled), c.SnapshotScheduleIntervalHours, c.SnapshotScheduleTime,
|
boolInt(c.SnapshotScheduleEnabled), c.SnapshotScheduleIntervalHours, c.SnapshotScheduleTime,
|
||||||
c.SnapshotScheduleLastRun, c.SnapshotScheduleNextRun, c.SnapshotScheduleCreatedBy,
|
c.SnapshotScheduleLastRun, c.SnapshotScheduleNextRun, c.SnapshotScheduleCreatedBy,
|
||||||
@@ -854,16 +893,18 @@ func saveTasksDB(tx *sql.Tx) error {
|
|||||||
cfg_network_bw_mbps, cfg_network_down_mbps, cfg_network_up_mbps,
|
cfg_network_bw_mbps, cfg_network_down_mbps, cfg_network_up_mbps,
|
||||||
cfg_monthly_traffic_gb, cfg_traffic_mode, cfg_traffic_in_gb,
|
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_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_port_mapping_count, cfg_assign_nat, cfg_lan_ipv4_mode, cfg_lan_interface,
|
||||||
|
cfg_lan_ipv4_address, cfg_lan_ipv4_prefix_len, cfg_lan_ipv4_gateway, cfg_snapshot_limit,
|
||||||
cfg_assign_ipv4, cfg_ipv4_count, cfg_public_ipv4s, cfg_assign_ipv6, cfg_ipv6_count, cfg_ipv6_addresses,
|
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_allowed_image_ids, cfg_image_limit_configured, cfg_expires_at
|
cfg_ssh_auth_mode, cfg_ssh_password, cfg_ssh_public_key, cfg_allowed_image_ids, cfg_image_limit_configured, 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,
|
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.Name, cfg.Virtualization, cfg.TemplateID, cfg.VCPU, cfg.CPUPercent, cfg.RAMMB, cfg.DiskGB,
|
||||||
cfg.NetworkBWMbps, cfg.NetworkDownMbps, cfg.NetworkUpMbps,
|
cfg.NetworkBWMbps, cfg.NetworkDownMbps, cfg.NetworkUpMbps,
|
||||||
cfg.MonthlyTrafficGB, cfg.TrafficMode, cfg.TrafficInGB,
|
cfg.MonthlyTrafficGB, cfg.TrafficMode, cfg.TrafficInGB,
|
||||||
cfg.TrafficOutGB, cfg.IOSpeedMBps, cfg.IOReadMBps, cfg.IOWriteMBps,
|
cfg.TrafficOutGB, cfg.IOSpeedMBps, cfg.IOReadMBps, cfg.IOWriteMBps,
|
||||||
cfg.PortMappingCount, boolPtrInt(cfg.AssignNAT), cfg.SnapshotLimit,
|
cfg.PortMappingCount, boolPtrInt(cfg.AssignNAT), cfg.LANIPv4Mode, cfg.LANInterface,
|
||||||
|
cfg.LANIPv4Address, cfg.LANIPv4PrefixLen, cfg.LANIPv4Gateway, cfg.SnapshotLimit,
|
||||||
boolInt(cfg.AssignIPv4), cfg.IPv4Count, encodeStringSlice(cfg.PublicIPv4s),
|
boolInt(cfg.AssignIPv4), cfg.IPv4Count, encodeStringSlice(cfg.PublicIPv4s),
|
||||||
boolInt(cfg.AssignIPv6), cfg.IPv6Count, encodeStringSlice(cfg.IPv6Addresses),
|
boolInt(cfg.AssignIPv6), cfg.IPv6Count, encodeStringSlice(cfg.IPv6Addresses),
|
||||||
cfg.SSHAuthMode, cfg.SSHPassword, cfg.SSHPublicKey, encodeStringSlice(cfg.AllowedImageIDs), boolInt(cfg.ImageLimitConfigured), cfg.ExpiresAt,
|
cfg.SSHAuthMode, cfg.SSHPassword, cfg.SSHPublicKey, encodeStringSlice(cfg.AllowedImageIDs), boolInt(cfg.ImageLimitConfigured), cfg.ExpiresAt,
|
||||||
@@ -915,7 +956,8 @@ func loadContainers() ([]Container, error) {
|
|||||||
monthly_traffic_gb, traffic_mode, traffic_in_gb,
|
monthly_traffic_gb, traffic_mode, traffic_in_gb,
|
||||||
traffic_out_gb, traffic_used_rx, traffic_used_tx, traffic_reset_date,
|
traffic_out_gb, traffic_used_rx, traffic_used_tx, traffic_reset_date,
|
||||||
io_speed_mbps, io_read_mbps, io_write_mbps,
|
io_speed_mbps, io_read_mbps, io_write_mbps,
|
||||||
status, ip, ipv6, ipv6_prefix_len, ipv6_interface, vnc_port, ssh_port, ssh_password,
|
status, ip, lan_ipv4_mode, lan_interface, lan_ipv4_address, lan_ipv4_prefix_len, lan_ipv4_gateway,
|
||||||
|
ipv6, ipv6_prefix_len, ipv6_interface, vnc_port, ssh_port, ssh_password,
|
||||||
ssh_host_key, port_mapping_limit, snapshot_limit, created_at, expires_at,
|
ssh_host_key, port_mapping_limit, snapshot_limit, created_at, expires_at,
|
||||||
snapshot_schedule_enabled, snapshot_schedule_interval_hours, snapshot_schedule_time,
|
snapshot_schedule_enabled, snapshot_schedule_interval_hours, snapshot_schedule_time,
|
||||||
snapshot_schedule_last_run, snapshot_schedule_next_run, snapshot_schedule_created_by,
|
snapshot_schedule_last_run, snapshot_schedule_next_run, snapshot_schedule_created_by,
|
||||||
@@ -933,13 +975,16 @@ func loadContainers() ([]Container, error) {
|
|||||||
var scheduleEnabled, policyBlocked, firewallEnabled, imageLimitConfigured int
|
var scheduleEnabled, policyBlocked, firewallEnabled, imageLimitConfigured int
|
||||||
var firewallDefaultAction string
|
var firewallDefaultAction string
|
||||||
var firewallRulesJSON, allowedImageIDs sql.NullString
|
var firewallRulesJSON, allowedImageIDs sql.NullString
|
||||||
|
var lanIPv4Address, lanIPv4Gateway sql.NullString
|
||||||
|
var lanIPv4PrefixLen sql.NullInt64
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&c.ID, &c.UUID, &c.Name, &c.Virtualization, &c.LXCName, &c.KVMName, &c.DiskImage, &c.MACAddress, &c.Template,
|
&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.NetworkDownMbps, &c.NetworkUpMbps,
|
&c.VCPU, &c.RAMMB, &c.DiskGB, &c.NetworkBWMbps, &c.NetworkDownMbps, &c.NetworkUpMbps,
|
||||||
&c.MonthlyTrafficGB, &c.TrafficMode, &c.TrafficInGB,
|
&c.MonthlyTrafficGB, &c.TrafficMode, &c.TrafficInGB,
|
||||||
&c.TrafficOutGB, &c.TrafficUsedRX, &c.TrafficUsedTX, &c.TrafficResetDate,
|
&c.TrafficOutGB, &c.TrafficUsedRX, &c.TrafficUsedTX, &c.TrafficResetDate,
|
||||||
&c.IOSpeedMBps, &c.IOReadMBps, &c.IOWriteMBps,
|
&c.IOSpeedMBps, &c.IOReadMBps, &c.IOWriteMBps,
|
||||||
&c.Status, &c.IP, &c.IPv6, &c.IPv6PrefixLen, &c.IPv6Interface, &c.VNCPort, &c.SSHPort, &c.SSHPassword,
|
&c.Status, &c.IP, &c.LANIPv4Mode, &c.LANInterface, &lanIPv4Address, &lanIPv4PrefixLen, &lanIPv4Gateway,
|
||||||
|
&c.IPv6, &c.IPv6PrefixLen, &c.IPv6Interface, &c.VNCPort, &c.SSHPort, &c.SSHPassword,
|
||||||
&c.SSHHostKey, &c.PortMappingLimit, &c.SnapshotLimit, &c.CreatedAt, &c.ExpiresAt,
|
&c.SSHHostKey, &c.PortMappingLimit, &c.SnapshotLimit, &c.CreatedAt, &c.ExpiresAt,
|
||||||
&scheduleEnabled, &c.SnapshotScheduleIntervalHours, &c.SnapshotScheduleTime,
|
&scheduleEnabled, &c.SnapshotScheduleIntervalHours, &c.SnapshotScheduleTime,
|
||||||
&c.SnapshotScheduleLastRun, &c.SnapshotScheduleNextRun, &c.SnapshotScheduleCreatedBy,
|
&c.SnapshotScheduleLastRun, &c.SnapshotScheduleNextRun, &c.SnapshotScheduleCreatedBy,
|
||||||
@@ -948,6 +993,11 @@ func loadContainers() ([]Container, error) {
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
c.LANIPv4Address = lanIPv4Address.String
|
||||||
|
if lanIPv4PrefixLen.Valid {
|
||||||
|
c.LANIPv4PrefixLen = int(lanIPv4PrefixLen.Int64)
|
||||||
|
}
|
||||||
|
c.LANIPv4Gateway = lanIPv4Gateway.String
|
||||||
c.SnapshotScheduleEnabled = scheduleEnabled != 0
|
c.SnapshotScheduleEnabled = scheduleEnabled != 0
|
||||||
c.PolicyBlocked = policyBlocked != 0
|
c.PolicyBlocked = policyBlocked != 0
|
||||||
c.FirewallEnabled = firewallEnabled != 0
|
c.FirewallEnabled = firewallEnabled != 0
|
||||||
@@ -1158,7 +1208,8 @@ func loadTasks() ([]SavedTask, error) {
|
|||||||
cfg_network_bw_mbps, cfg_network_down_mbps, cfg_network_up_mbps,
|
cfg_network_bw_mbps, cfg_network_down_mbps, cfg_network_up_mbps,
|
||||||
cfg_monthly_traffic_gb, cfg_traffic_mode, cfg_traffic_in_gb,
|
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_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_port_mapping_count, cfg_assign_nat, cfg_lan_ipv4_mode, cfg_lan_interface,
|
||||||
|
cfg_lan_ipv4_address, cfg_lan_ipv4_prefix_len, cfg_lan_ipv4_gateway, cfg_snapshot_limit,
|
||||||
cfg_assign_ipv4, cfg_ipv4_count, cfg_public_ipv4s, cfg_assign_ipv6, cfg_ipv6_count, cfg_ipv6_addresses,
|
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_allowed_image_ids, cfg_image_limit_configured, cfg_expires_at
|
cfg_ssh_auth_mode, cfg_ssh_password, cfg_ssh_public_key, cfg_allowed_image_ids, cfg_image_limit_configured, cfg_expires_at
|
||||||
FROM tasks ORDER BY created_at, id`)
|
FROM tasks ORDER BY created_at, id`)
|
||||||
@@ -1173,15 +1224,16 @@ func loadTasks() ([]SavedTask, error) {
|
|||||||
var cfg savedTaskConfig
|
var cfg savedTaskConfig
|
||||||
var assignIPv4, assignIPv6, imageLimitConfigured int
|
var assignIPv4, assignIPv6, imageLimitConfigured int
|
||||||
var ip, userAgent, publicIPv4s, ipv6Addresses sql.NullString
|
var ip, userAgent, publicIPv4s, ipv6Addresses sql.NullString
|
||||||
var sshAuthMode, sshPassword, sshPublicKey, allowedImageIDs sql.NullString
|
var lanIPv4Mode, lanInterface, lanIPv4Address, lanIPv4Gateway, sshAuthMode, sshPassword, sshPublicKey, allowedImageIDs sql.NullString
|
||||||
var assignNAT, ipv4Count, ipv6Count sql.NullInt64
|
var assignNAT, lanIPv4PrefixLen, ipv4Count, ipv6Count sql.NullInt64
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&t.ID, &t.Type, &t.ContainerID, &t.ContainerName, &t.Status, &t.Error, &t.CreatedAt, &t.TemplateID, &t.User, &ip, &userAgent,
|
&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.Name, &cfg.Virtualization, &cfg.TemplateID, &cfg.VCPU, &cfg.CPUPercent, &cfg.RAMMB, &cfg.DiskGB,
|
||||||
&cfg.NetworkBWMbps, &cfg.NetworkDownMbps, &cfg.NetworkUpMbps,
|
&cfg.NetworkBWMbps, &cfg.NetworkDownMbps, &cfg.NetworkUpMbps,
|
||||||
&cfg.MonthlyTrafficGB, &cfg.TrafficMode, &cfg.TrafficInGB,
|
&cfg.MonthlyTrafficGB, &cfg.TrafficMode, &cfg.TrafficInGB,
|
||||||
&cfg.TrafficOutGB, &cfg.IOSpeedMBps, &cfg.IOReadMBps, &cfg.IOWriteMBps,
|
&cfg.TrafficOutGB, &cfg.IOSpeedMBps, &cfg.IOReadMBps, &cfg.IOWriteMBps,
|
||||||
&cfg.PortMappingCount, &assignNAT, &cfg.SnapshotLimit,
|
&cfg.PortMappingCount, &assignNAT, &lanIPv4Mode, &lanInterface,
|
||||||
|
&lanIPv4Address, &lanIPv4PrefixLen, &lanIPv4Gateway, &cfg.SnapshotLimit,
|
||||||
&assignIPv4, &ipv4Count, &publicIPv4s, &assignIPv6, &ipv6Count, &ipv6Addresses,
|
&assignIPv4, &ipv4Count, &publicIPv4s, &assignIPv6, &ipv6Count, &ipv6Addresses,
|
||||||
&sshAuthMode, &sshPassword, &sshPublicKey, &allowedImageIDs, &imageLimitConfigured, &cfg.ExpiresAt,
|
&sshAuthMode, &sshPassword, &sshPublicKey, &allowedImageIDs, &imageLimitConfigured, &cfg.ExpiresAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
@@ -1193,6 +1245,13 @@ func loadTasks() ([]SavedTask, error) {
|
|||||||
value := assignNAT.Int64 != 0
|
value := assignNAT.Int64 != 0
|
||||||
cfg.AssignNAT = &value
|
cfg.AssignNAT = &value
|
||||||
}
|
}
|
||||||
|
cfg.LANIPv4Mode = lanIPv4Mode.String
|
||||||
|
cfg.LANInterface = lanInterface.String
|
||||||
|
cfg.LANIPv4Address = lanIPv4Address.String
|
||||||
|
if lanIPv4PrefixLen.Valid {
|
||||||
|
cfg.LANIPv4PrefixLen = int(lanIPv4PrefixLen.Int64)
|
||||||
|
}
|
||||||
|
cfg.LANIPv4Gateway = lanIPv4Gateway.String
|
||||||
cfg.AssignIPv4 = assignIPv4 != 0
|
cfg.AssignIPv4 = assignIPv4 != 0
|
||||||
if ipv4Count.Valid {
|
if ipv4Count.Valid {
|
||||||
cfg.IPv4Count = int(ipv4Count.Int64)
|
cfg.IPv4Count = int(ipv4Count.Int64)
|
||||||
|
|||||||
+370
-8
@@ -8,6 +8,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hash/fnv"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -81,6 +83,13 @@ func (m *Manager) WarmRunningContainersSSH() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
config.UpdateContainerStatus(c.ID, "running")
|
config.UpdateContainerStatus(c.ID, "running")
|
||||||
|
if current := config.FindContainer(c.ID); current != nil {
|
||||||
|
m.refreshContainerIPv4Details(current)
|
||||||
|
c = *current
|
||||||
|
}
|
||||||
|
if err := m.ensureLANHostAccess(&c); err != nil {
|
||||||
|
fmt.Printf("Warning: failed to prepare LAN IPv4 host access for %s: %v\n", c.LxcName(), err)
|
||||||
|
}
|
||||||
if c.IP != "" && m.containerPortListening(c.LxcName(), 22) {
|
if c.IP != "" && m.containerPortListening(c.LxcName(), 22) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -238,6 +247,11 @@ type ContainerConfig struct {
|
|||||||
ExtraPorts []int `json:"extra_ports"`
|
ExtraPorts []int `json:"extra_ports"`
|
||||||
PortMappingCount int `json:"port_mapping_count"`
|
PortMappingCount int `json:"port_mapping_count"`
|
||||||
AssignNAT *bool `json:"assign_nat,omitempty"`
|
AssignNAT *bool `json:"assign_nat,omitempty"`
|
||||||
|
LANIPv4Mode string `json:"lan_ipv4_mode,omitempty"`
|
||||||
|
LANInterface string `json:"lan_interface,omitempty"`
|
||||||
|
LANIPv4Address string `json:"lan_ipv4_address,omitempty"`
|
||||||
|
LANIPv4PrefixLen int `json:"lan_ipv4_prefix_len,omitempty"`
|
||||||
|
LANIPv4Gateway string `json:"lan_ipv4_gateway,omitempty"`
|
||||||
SnapshotLimit int `json:"snapshot_limit"`
|
SnapshotLimit int `json:"snapshot_limit"`
|
||||||
AllowedImageIDs []string `json:"allowed_image_ids,omitempty"`
|
AllowedImageIDs []string `json:"allowed_image_ids,omitempty"`
|
||||||
ImageLimitConfigured bool `json:"image_limit_configured,omitempty"`
|
ImageLimitConfigured bool `json:"image_limit_configured,omitempty"`
|
||||||
@@ -289,9 +303,24 @@ func (cfg *ContainerConfig) NormalizeResourceAliases() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cfg ContainerConfig) WantsNAT() bool {
|
func (cfg ContainerConfig) WantsNAT() bool {
|
||||||
|
if cfg.WantsLANIPv4() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return cfg.AssignNAT == nil || *cfg.AssignNAT
|
return cfg.AssignNAT == nil || *cfg.AssignNAT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cfg ContainerConfig) WantsLANDHCP() bool {
|
||||||
|
return strings.EqualFold(strings.TrimSpace(cfg.LANIPv4Mode), config.LANIPv4ModeDHCP)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg ContainerConfig) WantsLANStaticIPv4() bool {
|
||||||
|
return strings.EqualFold(strings.TrimSpace(cfg.LANIPv4Mode), config.LANIPv4ModeStatic)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg ContainerConfig) WantsLANIPv4() bool {
|
||||||
|
return cfg.WantsLANDHCP() || cfg.WantsLANStaticIPv4()
|
||||||
|
}
|
||||||
|
|
||||||
// CreateContainer creates a new LXC container. Uses ct-{id} as LXC name internally.
|
// CreateContainer creates a new LXC container. Uses ct-{id} as LXC name internally.
|
||||||
func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
||||||
cfg.NormalizeResourceAliases()
|
cfg.NormalizeResourceAliases()
|
||||||
@@ -355,6 +384,14 @@ func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
|||||||
_ = m.cleanupContainerStorage(lxcName)
|
_ = m.cleanupContainerStorage(lxcName)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if cfg.WantsLANIPv4() {
|
||||||
|
iface, err := m.applyLANIPv4Config(lxcName, cfg)
|
||||||
|
if err != nil {
|
||||||
|
_ = m.cleanupContainerStorage(lxcName)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cfg.LANInterface = iface
|
||||||
|
}
|
||||||
|
|
||||||
// Apply resource limits and mandatory security hardening.
|
// Apply resource limits and mandatory security hardening.
|
||||||
if err := m.applyResourceLimits(lxcName, cfg); err != nil {
|
if err := m.applyResourceLimits(lxcName, cfg); err != nil {
|
||||||
@@ -434,6 +471,7 @@ func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
|||||||
UUID: config.NewContainerUUID(),
|
UUID: config.NewContainerUUID(),
|
||||||
Name: cfg.Name,
|
Name: cfg.Name,
|
||||||
Virtualization: config.VirtualizationLXC,
|
Virtualization: config.VirtualizationLXC,
|
||||||
|
LXCName: lxcName,
|
||||||
Template: cfg.TemplateID,
|
Template: cfg.TemplateID,
|
||||||
VCPU: cfg.VCPU,
|
VCPU: cfg.VCPU,
|
||||||
RAMMB: cfg.RAMMB,
|
RAMMB: cfg.RAMMB,
|
||||||
@@ -451,6 +489,12 @@ func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
|||||||
IOWriteMBps: cfg.IOWriteMBps,
|
IOWriteMBps: cfg.IOWriteMBps,
|
||||||
Status: "stopped",
|
Status: "stopped",
|
||||||
IP: "",
|
IP: "",
|
||||||
|
LANIPv4Mode: normalizedLANIPv4Mode(cfg.LANIPv4Mode),
|
||||||
|
LANInterface: strings.TrimSpace(cfg.LANInterface),
|
||||||
|
LANIPv4Address: strings.TrimSpace(cfg.LANIPv4Address),
|
||||||
|
LANIPv4PrefixLen: cfg.LANIPv4PrefixLen,
|
||||||
|
LANIPv4Gateway: strings.TrimSpace(cfg.LANIPv4Gateway),
|
||||||
|
MACAddress: readLXCConfigValue(lxcName, "lxc.net.0.hwaddr"),
|
||||||
PublicIPv4s: publicIPv4s,
|
PublicIPv4s: publicIPv4s,
|
||||||
IPv6Addresses: ipv6Assignments,
|
IPv6Addresses: ipv6Assignments,
|
||||||
VNCPort: 0,
|
VNCPort: 0,
|
||||||
@@ -469,7 +513,7 @@ func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
|||||||
|
|
||||||
// Pre-configure network and SSH in the rootfs before first boot.
|
// Pre-configure network and SSH in the rootfs before first boot.
|
||||||
rootfsPath := filepath.Join(m.LxcPath, lxcName, "rootfs")
|
rootfsPath := filepath.Join(m.LxcPath, lxcName, "rootfs")
|
||||||
m.preconfigureNetwork(rootfsPath, cfg.TemplateID)
|
m.preconfigureNetwork(rootfsPath, cfg)
|
||||||
if len(ipv6Assignments) > 0 {
|
if len(ipv6Assignments) > 0 {
|
||||||
if err := installContainerIPv6Init(rootfsPath, ipv6AssignmentAddresses(ipv6Assignments)...); err != nil {
|
if err := installContainerIPv6Init(rootfsPath, ipv6AssignmentAddresses(ipv6Assignments)...); err != nil {
|
||||||
fmt.Printf("Warning: failed to install IPv6 init in %s: %v\n", lxcName, err)
|
fmt.Printf("Warning: failed to install IPv6 init in %s: %v\n", lxcName, err)
|
||||||
@@ -502,7 +546,8 @@ func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) preconfigureNetwork(rootfsPath, templateID string) {
|
func (m *Manager) preconfigureNetwork(rootfsPath string, cfg ContainerConfig) {
|
||||||
|
templateID := cfg.TemplateID
|
||||||
osRelease := ""
|
osRelease := ""
|
||||||
if data, err := os.ReadFile(filepath.Join(rootfsPath, "etc", "os-release")); err == nil {
|
if data, err := os.ReadFile(filepath.Join(rootfsPath, "etc", "os-release")); err == nil {
|
||||||
osRelease = strings.ToLower(string(data))
|
osRelease = strings.ToLower(string(data))
|
||||||
@@ -518,7 +563,13 @@ func (m *Manager) preconfigureNetwork(rootfsPath, templateID string) {
|
|||||||
|
|
||||||
if isAlpine {
|
if isAlpine {
|
||||||
interfaces := filepath.Join(rootfsPath, "etc", "network", "interfaces")
|
interfaces := filepath.Join(rootfsPath, "etc", "network", "interfaces")
|
||||||
content := "auto lo\niface lo inet loopback\n\nauto eth0\niface eth0 inet dhcp\n"
|
content := "auto lo\niface lo inet loopback\n\nauto eth0\n"
|
||||||
|
if cfg.WantsLANStaticIPv4() {
|
||||||
|
content += fmt.Sprintf("iface eth0 inet static\n address %s\n netmask %s\n gateway %s\n",
|
||||||
|
cfg.LANIPv4Address, subnetMaskFromPrefixLen(cfg.LANIPv4PrefixLen), cfg.LANIPv4Gateway)
|
||||||
|
} else {
|
||||||
|
content += "iface eth0 inet dhcp\n"
|
||||||
|
}
|
||||||
_ = os.MkdirAll(filepath.Dir(interfaces), 0755)
|
_ = os.MkdirAll(filepath.Dir(interfaces), 0755)
|
||||||
_ = os.WriteFile(interfaces, []byte(content), 0644)
|
_ = os.WriteFile(interfaces, []byte(content), 0644)
|
||||||
_ = m.runRootfsCommand(rootfsPath, "rc-update", "add", "networking", "boot")
|
_ = m.runRootfsCommand(rootfsPath, "rc-update", "add", "networking", "boot")
|
||||||
@@ -535,8 +586,16 @@ interface-name=eth0
|
|||||||
autoconnect=true
|
autoconnect=true
|
||||||
|
|
||||||
[ipv4]
|
[ipv4]
|
||||||
method=auto
|
`
|
||||||
|
if cfg.WantsLANStaticIPv4() {
|
||||||
|
keyfile += fmt.Sprintf(`method=manual
|
||||||
|
address1=%s/%d,%s
|
||||||
|
`, cfg.LANIPv4Address, cfg.LANIPv4PrefixLen, cfg.LANIPv4Gateway)
|
||||||
|
} else {
|
||||||
|
keyfile += `method=auto
|
||||||
|
`
|
||||||
|
}
|
||||||
|
keyfile += `
|
||||||
[ipv6]
|
[ipv6]
|
||||||
method=ignore
|
method=ignore
|
||||||
`
|
`
|
||||||
@@ -552,9 +611,14 @@ method=ignore
|
|||||||
Name=eth0
|
Name=eth0
|
||||||
|
|
||||||
[Network]
|
[Network]
|
||||||
DHCP=ipv4
|
`
|
||||||
|
if cfg.WantsLANStaticIPv4() {
|
||||||
|
network += fmt.Sprintf("Address=%s/%d\nGateway=%s\nIPv6AcceptRA=no\n", cfg.LANIPv4Address, cfg.LANIPv4PrefixLen, cfg.LANIPv4Gateway)
|
||||||
|
} else {
|
||||||
|
network += `DHCP=ipv4
|
||||||
IPv6AcceptRA=no
|
IPv6AcceptRA=no
|
||||||
`
|
`
|
||||||
|
}
|
||||||
_ = os.WriteFile(filepath.Join(networkdDir, "10-eth0.network"), []byte(network), 0644)
|
_ = os.WriteFile(filepath.Join(networkdDir, "10-eth0.network"), []byte(network), 0644)
|
||||||
}
|
}
|
||||||
if !isRHELFamily {
|
if !isRHELFamily {
|
||||||
@@ -562,6 +626,226 @@ IPv6AcceptRA=no
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizedLANIPv4Mode(mode string) string {
|
||||||
|
if strings.EqualFold(strings.TrimSpace(mode), config.LANIPv4ModeDHCP) {
|
||||||
|
return config.LANIPv4ModeDHCP
|
||||||
|
}
|
||||||
|
if strings.EqualFold(strings.TrimSpace(mode), config.LANIPv4ModeStatic) {
|
||||||
|
return config.LANIPv4ModeStatic
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) applyLANIPv4Config(lxcName string, cfg ContainerConfig) (string, error) {
|
||||||
|
if !cfg.WantsLANIPv4() {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
if cfg.WantsLANStaticIPv4() {
|
||||||
|
if err := validateLANStaticIPv4(cfg); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iface := cfg.LANInterface
|
||||||
|
iface = strings.TrimSpace(iface)
|
||||||
|
if iface == "" || isInvalidLANUplinkInterface(iface) {
|
||||||
|
iface = defaultLANInterface()
|
||||||
|
}
|
||||||
|
if iface == "" {
|
||||||
|
return "", fmt.Errorf("LAN IPv4 requires an uplink interface")
|
||||||
|
}
|
||||||
|
if isInvalidLANUplinkInterface(iface) {
|
||||||
|
return "", fmt.Errorf("invalid LAN IPv4 uplink interface: %s", iface)
|
||||||
|
}
|
||||||
|
if out, err := exec.Command("ip", "link", "show", "dev", iface).CombinedOutput(); err != nil {
|
||||||
|
return "", fmt.Errorf("LAN IPv4 uplink interface %s not found: %v, output: %s", iface, err, string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := filepath.Join(m.LxcPath, lxcName, "config")
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read LXC config for LAN DHCP: %v", err)
|
||||||
|
}
|
||||||
|
lines := strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n")
|
||||||
|
values := map[string]string{
|
||||||
|
"lxc.net.0.type": "macvlan",
|
||||||
|
"lxc.net.0.link": iface,
|
||||||
|
"lxc.net.0.flags": "up",
|
||||||
|
"lxc.net.0.macvlan.mode": "bridge",
|
||||||
|
}
|
||||||
|
if cfg.WantsLANStaticIPv4() {
|
||||||
|
values["lxc.net.0.ipv4.address"] = fmt.Sprintf("%s/%d", strings.TrimSpace(cfg.LANIPv4Address), cfg.LANIPv4PrefixLen)
|
||||||
|
values["lxc.net.0.ipv4.gateway"] = strings.TrimSpace(cfg.LANIPv4Gateway)
|
||||||
|
}
|
||||||
|
seen := map[string]bool{}
|
||||||
|
next := make([]string, 0, len(lines)+len(values))
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if !cfg.WantsLANStaticIPv4() && (strings.HasPrefix(trimmed, "lxc.net.0.ipv4.address") || strings.HasPrefix(trimmed, "lxc.net.0.ipv4.gateway")) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
replaced := false
|
||||||
|
for key, value := range values {
|
||||||
|
if strings.HasPrefix(trimmed, key+" ") || strings.HasPrefix(trimmed, key+"=") {
|
||||||
|
next = append(next, fmt.Sprintf("%s = %s", key, value))
|
||||||
|
seen[key] = true
|
||||||
|
replaced = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !replaced {
|
||||||
|
next = append(next, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, key := range []string{"lxc.net.0.type", "lxc.net.0.link", "lxc.net.0.flags", "lxc.net.0.macvlan.mode", "lxc.net.0.ipv4.address", "lxc.net.0.ipv4.gateway"} {
|
||||||
|
if !seen[key] {
|
||||||
|
if value, ok := values[key]; ok {
|
||||||
|
next = append(next, fmt.Sprintf("%s = %s", key, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(configPath, []byte(strings.Join(next, "\n")), 0644); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to write LXC LAN IPv4 config: %v", err)
|
||||||
|
}
|
||||||
|
return iface, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateLANStaticIPv4(cfg ContainerConfig) error {
|
||||||
|
addr, err := netip.ParseAddr(strings.TrimSpace(cfg.LANIPv4Address))
|
||||||
|
if err != nil || !addr.Is4() {
|
||||||
|
return fmt.Errorf("LAN static IPv4 address is invalid")
|
||||||
|
}
|
||||||
|
gateway, err := netip.ParseAddr(strings.TrimSpace(cfg.LANIPv4Gateway))
|
||||||
|
if err != nil || !gateway.Is4() {
|
||||||
|
return fmt.Errorf("LAN static IPv4 gateway is invalid")
|
||||||
|
}
|
||||||
|
if cfg.LANIPv4PrefixLen < 1 || cfg.LANIPv4PrefixLen > 32 {
|
||||||
|
return fmt.Errorf("LAN static IPv4 prefix length must be 1-32")
|
||||||
|
}
|
||||||
|
prefix := netip.PrefixFrom(addr, cfg.LANIPv4PrefixLen).Masked()
|
||||||
|
if !prefix.Contains(gateway) && cfg.LANIPv4PrefixLen < 32 {
|
||||||
|
return fmt.Errorf("LAN static IPv4 gateway must be in the same subnet")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func subnetMaskFromPrefixLen(prefixLen int) string {
|
||||||
|
if prefixLen < 0 || prefixLen > 32 {
|
||||||
|
return "255.255.255.0"
|
||||||
|
}
|
||||||
|
mask := uint32(0)
|
||||||
|
if prefixLen > 0 {
|
||||||
|
mask = ^uint32(0) << (32 - prefixLen)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d.%d.%d.%d", byte(mask>>24), byte(mask>>16), byte(mask>>8), byte(mask))
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultLANInterface() string {
|
||||||
|
out, err := exec.Command("ip", "-4", "route", "show", "default").Output()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(string(out), "\n") {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
for i := 0; i+1 < len(fields); i++ {
|
||||||
|
if fields[i] == "dev" && !isInvalidLANUplinkInterface(fields[i+1]) {
|
||||||
|
return fields[i+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func isInvalidLANUplinkInterface(name string) bool {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
return name == "" ||
|
||||||
|
name == "lo" ||
|
||||||
|
strings.HasPrefix(name, "lxc") ||
|
||||||
|
strings.HasPrefix(name, "docker") ||
|
||||||
|
strings.HasPrefix(name, "br-") ||
|
||||||
|
strings.HasPrefix(name, "veth") ||
|
||||||
|
strings.HasPrefix(name, "virbr") ||
|
||||||
|
strings.HasPrefix(name, "clmv-")
|
||||||
|
}
|
||||||
|
|
||||||
|
func readLXCConfigValue(lxcName string, key string) string {
|
||||||
|
data, err := os.ReadFile(filepath.Join("/var/lib/lxc", lxcName, "config"))
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
prefix := key + " "
|
||||||
|
for _, line := range strings.Split(string(data), "\n") {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(trimmed, prefix) || strings.HasPrefix(trimmed, key+"=") {
|
||||||
|
parts := strings.SplitN(trimmed, "=", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
return strings.TrimSpace(parts[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) ensureLANHostAccess(c *config.Container) error {
|
||||||
|
if c == nil || !c.UsesLANIPv4() || strings.TrimSpace(c.IP) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
uplink := strings.TrimSpace(c.LANInterface)
|
||||||
|
if uplink == "" {
|
||||||
|
uplink = defaultLANInterface()
|
||||||
|
}
|
||||||
|
if uplink == "" {
|
||||||
|
return fmt.Errorf("missing LAN IPv4 uplink interface")
|
||||||
|
}
|
||||||
|
shim := lanHostShimName(uplink)
|
||||||
|
if _, err := exec.Command("ip", "link", "show", "dev", shim).Output(); err != nil {
|
||||||
|
if out, addErr := exec.Command("ip", "link", "add", shim, "link", uplink, "type", "macvlan", "mode", "bridge").CombinedOutput(); addErr != nil {
|
||||||
|
return fmt.Errorf("failed to create host macvlan shim %s on %s: %v, output: %s", shim, uplink, addErr, string(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runQuiet("ip", "link", "set", shim, "up")
|
||||||
|
if out, err := exec.Command("ip", "route", "replace", c.IP+"/32", "dev", shim).CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("failed to route %s through %s: %v, output: %s", c.IP, shim, err, string(out))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) removeLANHostRoute(c *config.Container) {
|
||||||
|
if c == nil || !c.UsesLANIPv4() || strings.TrimSpace(c.IP) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uplink := strings.TrimSpace(c.LANInterface)
|
||||||
|
if uplink == "" {
|
||||||
|
uplink = defaultLANInterface()
|
||||||
|
}
|
||||||
|
if uplink == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
runQuiet("ip", "route", "del", c.IP+"/32", "dev", lanHostShimName(uplink))
|
||||||
|
}
|
||||||
|
|
||||||
|
func lanHostShimName(uplink string) string {
|
||||||
|
cleaned := make([]rune, 0, len(uplink))
|
||||||
|
for _, r := range uplink {
|
||||||
|
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
|
||||||
|
cleaned = append(cleaned, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
base := strings.ToLower(string(cleaned))
|
||||||
|
if base == "" {
|
||||||
|
base = "if"
|
||||||
|
}
|
||||||
|
if len(base) <= 10 {
|
||||||
|
return "clmv-" + base
|
||||||
|
}
|
||||||
|
h := fnv.New32a()
|
||||||
|
_, _ = h.Write([]byte(uplink))
|
||||||
|
suffix := fmt.Sprintf("%04x", h.Sum32()&0xffff)
|
||||||
|
if len(base) > 6 {
|
||||||
|
base = base[:6]
|
||||||
|
}
|
||||||
|
return "clmv-" + base + suffix
|
||||||
|
}
|
||||||
|
|
||||||
// preconfigureSSH installs and configures SSH directly in the rootfs before first boot.
|
// preconfigureSSH installs and configures SSH directly in the rootfs before first boot.
|
||||||
func (m *Manager) preconfigureSSH(rootfsPath, templateID string, sshAuthMode string) error {
|
func (m *Manager) preconfigureSSH(rootfsPath, templateID string, sshAuthMode string) error {
|
||||||
_ = templateID
|
_ = templateID
|
||||||
@@ -1460,6 +1744,7 @@ func (m *Manager) StartContainer(id int) error {
|
|||||||
c = config.FindContainer(id)
|
c = config.FindContainer(id)
|
||||||
if c != nil {
|
if c != nil {
|
||||||
c.IP = ip
|
c.IP = ip
|
||||||
|
m.refreshContainerIPv4Details(c)
|
||||||
config.SaveConfig()
|
config.SaveConfig()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1473,6 +1758,9 @@ func (m *Manager) StartContainer(id int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ip != "" {
|
if ip != "" {
|
||||||
|
if err := m.ensureLANHostAccess(c); err != nil {
|
||||||
|
fmt.Printf("Warning: failed to prepare LAN IPv4 host access for %s: %v\n", lxcName, err)
|
||||||
|
}
|
||||||
if err := m.EnsureSSH(id); err != nil {
|
if err := m.EnsureSSH(id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1496,7 +1784,6 @@ func (m *Manager) StartContainer(id int) error {
|
|||||||
fmt.Printf("Warning: failed to apply IPv6 routing for %s: %v\n", lxcName, err)
|
fmt.Printf("Warning: failed to apply IPv6 routing for %s: %v\n", lxcName, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Container %d (%s) started, IP: %s\n", id, c.Name, ip)
|
fmt.Printf("Container %d (%s) started, IP: %s\n", id, c.Name, ip)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1773,6 +2060,7 @@ ip -4 addr show eth0 2>/dev/null | awk '/inet / {sub(/\/.*/, "", $2); print $2;
|
|||||||
return "", fmt.Errorf("no IPv4 address after DHCP repair in %s", lxcName)
|
return "", fmt.Errorf("no IPv4 address after DHCP repair in %s", lxcName)
|
||||||
}
|
}
|
||||||
c.IP = ip
|
c.IP = ip
|
||||||
|
m.refreshContainerIPv4Details(c)
|
||||||
config.SaveConfig()
|
config.SaveConfig()
|
||||||
return ip, nil
|
return ip, nil
|
||||||
}
|
}
|
||||||
@@ -1794,6 +2082,10 @@ func (m *Manager) WarmSSH(id int) error {
|
|||||||
if ip, err := m.GetContainerIP(lxcName); err == nil && ip != "" {
|
if ip, err := m.GetContainerIP(lxcName); err == nil && ip != "" {
|
||||||
if current := config.FindContainer(id); current != nil {
|
if current := config.FindContainer(id); current != nil {
|
||||||
current.IP = ip
|
current.IP = ip
|
||||||
|
m.refreshContainerIPv4Details(current)
|
||||||
|
if err := m.ensureLANHostAccess(current); err != nil {
|
||||||
|
fmt.Printf("Warning: failed to prepare LAN IPv4 host access for %s: %v\n", lxcName, err)
|
||||||
|
}
|
||||||
config.SaveConfig()
|
config.SaveConfig()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -1803,6 +2095,10 @@ func (m *Manager) WarmSSH(id int) error {
|
|||||||
if current := config.FindContainer(id); current != nil && current.IP == "" {
|
if current := config.FindContainer(id); current != nil && current.IP == "" {
|
||||||
if ip, err := m.EnsureContainerIPv4(id); err == nil && ip != "" {
|
if ip, err := m.EnsureContainerIPv4(id); err == nil && ip != "" {
|
||||||
current.IP = ip
|
current.IP = ip
|
||||||
|
m.refreshContainerIPv4Details(current)
|
||||||
|
if err := m.ensureLANHostAccess(current); err != nil {
|
||||||
|
fmt.Printf("Warning: failed to prepare LAN IPv4 host access for %s: %v\n", lxcName, err)
|
||||||
|
}
|
||||||
config.SaveConfig()
|
config.SaveConfig()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2550,6 +2846,71 @@ func (m *Manager) GetContainerIP(lxcName string) (string, error) {
|
|||||||
return "", fmt.Errorf("no IPv4 address found for %s (IPv6 is disabled for containers)", lxcName)
|
return "", fmt.Errorf("no IPv4 address found for %s (IPv6 is disabled for containers)", lxcName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) GetContainerIPv4Details(lxcName string) (string, int, string, error) {
|
||||||
|
script := `
|
||||||
|
addr="$(ip -4 -o addr show dev eth0 scope global 2>/dev/null | awk '{print $4; exit}')"
|
||||||
|
gateway="$(ip route show default 0.0.0.0/0 dev eth0 2>/dev/null | awk '{for (i=1; i<=NF; i++) if ($i=="via") {print $(i+1); exit}}')"
|
||||||
|
printf '%s\n%s\n' "$addr" "$gateway"
|
||||||
|
`
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
cmd := exec.CommandContext(ctx, "lxc-attach", "-n", lxcName, "--", "sh", "-c", script)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if ctx.Err() == context.DeadlineExceeded {
|
||||||
|
return "", 0, "", fmt.Errorf("timed out reading IPv4 details for %s", lxcName)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, "", fmt.Errorf("failed to read IPv4 details for %s: %v, output: %s", lxcName, err, string(output))
|
||||||
|
}
|
||||||
|
lines := strings.Split(strings.TrimRight(string(output), "\n"), "\n")
|
||||||
|
if len(lines) == 0 || strings.TrimSpace(lines[0]) == "" {
|
||||||
|
return "", 0, "", fmt.Errorf("no IPv4 address details found for %s", lxcName)
|
||||||
|
}
|
||||||
|
prefix, err := netip.ParsePrefix(strings.TrimSpace(lines[0]))
|
||||||
|
if err != nil || !prefix.Addr().Is4() {
|
||||||
|
return "", 0, "", fmt.Errorf("invalid IPv4 address details for %s: %s", lxcName, strings.TrimSpace(lines[0]))
|
||||||
|
}
|
||||||
|
gateway := ""
|
||||||
|
if len(lines) > 1 {
|
||||||
|
candidate := strings.TrimSpace(lines[1])
|
||||||
|
if addr, err := netip.ParseAddr(candidate); err == nil && addr.Is4() {
|
||||||
|
gateway = candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prefix.Addr().String(), prefix.Bits(), gateway, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) refreshContainerIPv4Details(c *config.Container) {
|
||||||
|
if c == nil || c.IsKVM() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ip, prefixLen, gateway, err := m.GetContainerIPv4Details(c.LxcName())
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
changed := false
|
||||||
|
if ip != "" && c.IP != ip {
|
||||||
|
c.IP = ip
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if c.UsesLANDHCP() {
|
||||||
|
if prefixLen > 0 && c.LANIPv4PrefixLen != prefixLen {
|
||||||
|
c.LANIPv4PrefixLen = prefixLen
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if gateway != "" && c.LANIPv4Gateway != gateway {
|
||||||
|
c.LANIPv4Gateway = gateway
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.NormalizeNetworkAssignments() {
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
config.SaveConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ListContainers lists all LXC containers and updates statuses
|
// ListContainers lists all LXC containers and updates statuses
|
||||||
func (m *Manager) ListContainers() ([]config.Container, error) {
|
func (m *Manager) ListContainers() ([]config.Container, error) {
|
||||||
containers := config.AppConfig.Containers
|
containers := config.AppConfig.Containers
|
||||||
@@ -2565,6 +2926,7 @@ func (m *Manager) ListContainers() ([]config.Container, error) {
|
|||||||
ip, err := m.GetContainerIP(containers[i].LxcName())
|
ip, err := m.GetContainerIP(containers[i].LxcName())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
containers[i].IP = ip
|
containers[i].IP = ip
|
||||||
|
m.refreshContainerIPv4Details(&containers[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2824,7 +3186,7 @@ func (m *Manager) ReinstallContainer(id int, templateID string, authConfig ...Co
|
|||||||
|
|
||||||
// Set root password and pre-configure network/SSH via chroot.
|
// Set root password and pre-configure network/SSH via chroot.
|
||||||
rootfsPath := filepath.Join(m.LxcPath, lxcName, "rootfs")
|
rootfsPath := filepath.Join(m.LxcPath, lxcName, "rootfs")
|
||||||
m.preconfigureNetwork(rootfsPath, templateID)
|
m.preconfigureNetwork(rootfsPath, ContainerConfig{TemplateID: templateID})
|
||||||
if c.IPv6 != "" || len(c.IPv6Addresses) > 0 {
|
if c.IPv6 != "" || len(c.IPv6Addresses) > 0 {
|
||||||
if err := installContainerIPv6Init(rootfsPath, c.IPv6AddressStrings()...); err != nil {
|
if err := installContainerIPv6Init(rootfsPath, c.IPv6AddressStrings()...); err != nil {
|
||||||
fmt.Printf("Warning: failed to install IPv6 init in %s after reinstall: %v\n", lxcName, err)
|
fmt.Printf("Warning: failed to install IPv6 init in %s after reinstall: %v\n", lxcName, err)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState, type ReactNode } from 'react'
|
import { useEffect, useMemo, useState, type ReactNode } from 'react'
|
||||||
import { CalendarClock, RefreshCw, X } from 'lucide-react'
|
import { CalendarClock, RefreshCw, X } from 'lucide-react'
|
||||||
import { batchCreate, getIPv6Status, getEnabledImages, getHostInfo, CreateContainerRequest, HostInfo, IPv6Status, Template } from '../services/api'
|
import { batchCreate, getIPv6Status, getEnabledImages, getHostInfo, getHostReport, CreateContainerRequest, HostInfo, HostProbeReport, IPv6Status, Template } from '../services/api'
|
||||||
import { useDialog } from './Dialog'
|
import { useDialog } from './Dialog'
|
||||||
import { useLanguage, type Language } from '../contexts/LanguageContext'
|
import { useLanguage, type Language } from '../contexts/LanguageContext'
|
||||||
import { generateSSHPassword, sshPasswordError, sshPublicKeyError, type SSHAuthMode } from '../utils/sshAuth'
|
import { generateSSHPassword, sshPasswordError, sshPublicKeyError, type SSHAuthMode } from '../utils/sshAuth'
|
||||||
@@ -33,6 +33,11 @@ const defaultForm: CreateContainerRequest = {
|
|||||||
extra_ports: [],
|
extra_ports: [],
|
||||||
port_mapping_count: 2,
|
port_mapping_count: 2,
|
||||||
assign_nat: true,
|
assign_nat: true,
|
||||||
|
lan_ipv4_mode: '',
|
||||||
|
lan_interface: '',
|
||||||
|
lan_ipv4_address: '',
|
||||||
|
lan_ipv4_prefix_len: 24,
|
||||||
|
lan_ipv4_gateway: '',
|
||||||
snapshot_limit: 1,
|
snapshot_limit: 1,
|
||||||
assign_ipv4: false,
|
assign_ipv4: false,
|
||||||
ipv4_count: 1,
|
ipv4_count: 1,
|
||||||
@@ -57,6 +62,7 @@ export default function CreateContainerModal({ isOpen, onClose, onSuccess, exist
|
|||||||
const [batchCount, setBatchCount] = useState(1)
|
const [batchCount, setBatchCount] = useState(1)
|
||||||
const [form, setForm] = useState<CreateContainerRequest>(defaultForm)
|
const [form, setForm] = useState<CreateContainerRequest>(defaultForm)
|
||||||
const [hostInfo, setHostInfo] = useState<HostInfo | null>(null)
|
const [hostInfo, setHostInfo] = useState<HostInfo | null>(null)
|
||||||
|
const [hostReport, setHostReport] = useState<HostProbeReport | null>(null)
|
||||||
const [ipv6Status, setIPv6Status] = useState<IPv6Status | null>(null)
|
const [ipv6Status, setIPv6Status] = useState<IPv6Status | null>(null)
|
||||||
const [nameError, setNameError] = useState('')
|
const [nameError, setNameError] = useState('')
|
||||||
|
|
||||||
@@ -97,6 +103,10 @@ export default function CreateContainerModal({ isOpen, onClose, onSuccess, exist
|
|||||||
getHostInfo()
|
getHostInfo()
|
||||||
.then((res) => setHostInfo(res.data.data || null))
|
.then((res) => setHostInfo(res.data.data || null))
|
||||||
.catch(() => setHostInfo(null))
|
.catch(() => setHostInfo(null))
|
||||||
|
|
||||||
|
getHostReport()
|
||||||
|
.then((res) => setHostReport(res.data.data || null))
|
||||||
|
.catch(() => setHostReport(null))
|
||||||
}, [isOpen, form.virtualization])
|
}, [isOpen, form.virtualization])
|
||||||
|
|
||||||
const ipv6Available = !!ipv6Status?.available
|
const ipv6Available = !!ipv6Status?.available
|
||||||
@@ -116,7 +126,11 @@ export default function CreateContainerModal({ isOpen, onClose, onSuccess, exist
|
|||||||
}, [hostInfo, kvmAvailable, form.virtualization])
|
}, [hostInfo, kvmAvailable, form.virtualization])
|
||||||
const maxDiskGB = hostInfo?.disk.total_gb ? Math.max(1, Math.floor(hostInfo.disk.total_gb)) : undefined
|
const maxDiskGB = hostInfo?.disk.total_gb ? Math.max(1, Math.floor(hostInfo.disk.total_gb)) : undefined
|
||||||
const resourceErrors = validateResourceInputs(form, maxVCPU, maxRAMMB, maxDiskGB)
|
const resourceErrors = validateResourceInputs(form, maxVCPU, maxRAMMB, maxDiskGB)
|
||||||
const natEnabled = form.assign_nat !== false
|
const lanIPv4Enabled = form.lan_ipv4_mode === 'dhcp' || form.lan_ipv4_mode === 'static'
|
||||||
|
const lanStaticEnabled = form.lan_ipv4_mode === 'static'
|
||||||
|
const natEnabled = form.assign_nat !== false && !lanIPv4Enabled
|
||||||
|
const lanInterfaces = useMemo(() => getLANDHCPInterfaces(hostReport), [hostReport])
|
||||||
|
const defaultLANInterface = lanInterfaces[0]?.name || ''
|
||||||
const natPortCount = natEnabled ? Math.max(2, form.port_mapping_count || 2) : 0
|
const natPortCount = natEnabled ? Math.max(2, form.port_mapping_count || 2) : 0
|
||||||
const linuxTemplate = !isWindowsTemplate(form.template_id)
|
const linuxTemplate = !isWindowsTemplate(form.template_id)
|
||||||
const sshAuthMode = (form.ssh_auth_mode || 'auto_password') as SSHAuthMode
|
const sshAuthMode = (form.ssh_auth_mode || 'auto_password') as SSHAuthMode
|
||||||
@@ -169,11 +183,18 @@ export default function CreateContainerModal({ isOpen, onClose, onSuccess, exist
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!form.assign_ipv4 && !form.assign_ipv6 && form.assign_nat === false) {
|
if (!form.assign_ipv4 && !form.assign_ipv6 && form.assign_nat === false && form.lan_ipv4_mode !== 'dhcp' && form.lan_ipv4_mode !== 'static') {
|
||||||
dialog.alert('提示', '请勾选任意一个可用网络')
|
dialog.alert('提示', '请勾选任意一个可用网络')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (form.lan_ipv4_mode === 'static') {
|
||||||
|
if (!isIPv4Address(form.lan_ipv4_address || '') || !isIPv4Address(form.lan_ipv4_gateway || '') || !form.lan_ipv4_prefix_len) {
|
||||||
|
dialog.alert('局域网 IPv4 配置有误', '请填写有效的 IPv4 地址、子网掩码和网关')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const authError = validateSSHAuthInputs(form)
|
const authError = validateSSHAuthInputs(form)
|
||||||
if (authError) {
|
if (authError) {
|
||||||
dialog.alert('登录方式有误', authError)
|
dialog.alert('登录方式有误', authError)
|
||||||
@@ -388,7 +409,7 @@ export default function CreateContainerModal({ isOpen, onClose, onSuccess, exist
|
|||||||
...form,
|
...form,
|
||||||
assign_ipv4: event.target.checked,
|
assign_ipv4: event.target.checked,
|
||||||
public_ipv4s: event.target.checked ? form.public_ipv4s : [],
|
public_ipv4s: event.target.checked ? form.public_ipv4s : [],
|
||||||
...(event.target.checked ? { assign_nat: false, port_mapping_count: 0, extra_ports: [] } : {}),
|
...(event.target.checked ? { assign_nat: false, port_mapping_count: 0, extra_ports: [], lan_ipv4_mode: '', lan_interface: '' } : {}),
|
||||||
})}
|
})}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
@@ -454,6 +475,98 @@ export default function CreateContainerModal({ isOpen, onClose, onSuccess, exist
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={`rounded-md border px-3 py-2 text-sm ${form.virtualization === 'lxc' && lanInterfaces.length > 0 ? 'border-gray-200 bg-white' : 'border-gray-200 bg-gray-50 text-gray-400'}`}>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<label className="flex min-w-0 flex-1 items-start gap-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={lanIPv4Enabled}
|
||||||
|
disabled={form.virtualization !== 'lxc' || lanInterfaces.length === 0}
|
||||||
|
onChange={(event) => {
|
||||||
|
const checked = event.target.checked
|
||||||
|
setForm({
|
||||||
|
...form,
|
||||||
|
lan_ipv4_mode: checked ? 'dhcp' : '',
|
||||||
|
lan_interface: checked ? (form.lan_interface || defaultLANInterface) : '',
|
||||||
|
assign_nat: checked ? false : form.assign_nat,
|
||||||
|
port_mapping_count: checked ? 0 : form.port_mapping_count,
|
||||||
|
extra_ports: checked ? [] : form.extra_ports,
|
||||||
|
assign_ipv4: checked ? false : form.assign_ipv4,
|
||||||
|
public_ipv4s: checked ? [] : form.public_ipv4s,
|
||||||
|
ipv4_count: checked ? 0 : form.ipv4_count,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
<span className="min-w-0">
|
||||||
|
<span className="block font-medium text-gray-800">局域网 DHCP</span>
|
||||||
|
<span className="block text-xs text-gray-500">
|
||||||
|
{lanInterfaces.length > 0 ? 'macvlan 独立局域网 IP' : '未检测到可用上联网卡'}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{lanIPv4Enabled && (
|
||||||
|
<select
|
||||||
|
value={form.lan_interface || defaultLANInterface}
|
||||||
|
onChange={(event) => setForm({ ...form, lan_interface: event.target.value })}
|
||||||
|
className="h-9 w-32 shrink-0 rounded-md border border-gray-300 bg-white px-2 text-xs text-gray-700 focus:outline-none focus:ring-1 focus:ring-black"
|
||||||
|
>
|
||||||
|
{lanInterfaces.map((item) => (
|
||||||
|
<option key={item.name} value={item.name}>{item.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{lanIPv4Enabled && (
|
||||||
|
<div className="mt-3 space-y-3 pl-6">
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setForm({ ...form, lan_ipv4_mode: 'dhcp' })}
|
||||||
|
className={`rounded-md border px-3 py-2 text-xs font-medium ${form.lan_ipv4_mode === 'dhcp' ? 'border-black bg-black text-white' : 'border-gray-300 text-gray-700 hover:bg-gray-50'}`}
|
||||||
|
>
|
||||||
|
DHCP 自动获取
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setForm({ ...form, lan_ipv4_mode: 'static' })}
|
||||||
|
className={`rounded-md border px-3 py-2 text-xs font-medium ${lanStaticEnabled ? 'border-black bg-black text-white' : 'border-gray-300 text-gray-700 hover:bg-gray-50'}`}
|
||||||
|
>
|
||||||
|
手动配置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{lanStaticEnabled && (
|
||||||
|
<div className="grid gap-3 sm:grid-cols-3">
|
||||||
|
<Field label="IPv4 地址">
|
||||||
|
<input
|
||||||
|
value={form.lan_ipv4_address || ''}
|
||||||
|
onChange={(event) => setForm({ ...form, lan_ipv4_address: event.target.value })}
|
||||||
|
className={inputClass}
|
||||||
|
placeholder="192.168.2.250"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="子网掩码">
|
||||||
|
<input
|
||||||
|
value={subnetMaskFromPrefixLen(form.lan_ipv4_prefix_len || 24)}
|
||||||
|
onChange={(event) => setForm({ ...form, lan_ipv4_prefix_len: prefixLenFromSubnetMask(event.target.value) || 24 })}
|
||||||
|
className={inputClass}
|
||||||
|
placeholder="255.255.255.0"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field label="网关">
|
||||||
|
<input
|
||||||
|
value={form.lan_ipv4_gateway || ''}
|
||||||
|
onChange={(event) => setForm({ ...form, lan_ipv4_gateway: event.target.value })}
|
||||||
|
className={inputClass}
|
||||||
|
placeholder="192.168.2.202"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={`rounded-md border px-3 py-2 text-sm ${ipv6Available ? 'border-gray-200 bg-white' : 'border-gray-200 bg-gray-50 text-gray-400'}`}>
|
<div className={`rounded-md border px-3 py-2 text-sm ${ipv6Available ? 'border-gray-200 bg-white' : 'border-gray-200 bg-gray-50 text-gray-400'}`}>
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<label className="flex min-w-0 flex-1 items-start gap-3">
|
<label className="flex min-w-0 flex-1 items-start gap-3">
|
||||||
@@ -497,7 +610,7 @@ export default function CreateContainerModal({ isOpen, onClose, onSuccess, exist
|
|||||||
assign_nat: checked,
|
assign_nat: checked,
|
||||||
port_mapping_count: checked ? Math.max(2, form.port_mapping_count || 2) : 0,
|
port_mapping_count: checked ? Math.max(2, form.port_mapping_count || 2) : 0,
|
||||||
extra_ports: [],
|
extra_ports: [],
|
||||||
...(checked ? { assign_ipv4: false, public_ipv4s: [], ipv4_count: 0 } : {}),
|
...(checked ? { assign_ipv4: false, public_ipv4s: [], ipv4_count: 0, lan_ipv4_mode: '', lan_interface: '' } : {}),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
@@ -757,10 +870,13 @@ function validateResourceInputs(form: CreateContainerRequest, maxVCPU: number, m
|
|||||||
|
|
||||||
function normalizeCreateForm(form: CreateContainerRequest): CreateContainerRequest {
|
function normalizeCreateForm(form: CreateContainerRequest): CreateContainerRequest {
|
||||||
const normalized = applyTemplateDefaults(form)
|
const normalized = applyTemplateDefaults(form)
|
||||||
|
const wantsLANDHCP = normalized.virtualization === 'lxc' && normalized.lan_ipv4_mode === 'dhcp'
|
||||||
|
const wantsLANStatic = normalized.virtualization === 'lxc' && normalized.lan_ipv4_mode === 'static'
|
||||||
|
const wantsLANIPv4 = wantsLANDHCP || wantsLANStatic
|
||||||
const wantsIPv4 = !!normalized.assign_ipv4
|
const wantsIPv4 = !!normalized.assign_ipv4
|
||||||
const wantsIPv6 = !!normalized.assign_ipv6
|
const wantsIPv6 = !!normalized.assign_ipv6
|
||||||
// IPv4 and NAT are mutually exclusive
|
// IPv4 and NAT are mutually exclusive
|
||||||
const wantsNAT = wantsIPv4 ? false : normalized.assign_nat !== false
|
const wantsNAT = wantsLANIPv4 || wantsIPv4 ? false : normalized.assign_nat !== false
|
||||||
const linuxTemplate = !isWindowsTemplate(normalized.template_id)
|
const linuxTemplate = !isWindowsTemplate(normalized.template_id)
|
||||||
const sshAuthMode = linuxTemplate ? (normalized.ssh_auth_mode || 'auto_password') : 'auto_password'
|
const sshAuthMode = linuxTemplate ? (normalized.ssh_auth_mode || 'auto_password') : 'auto_password'
|
||||||
return {
|
return {
|
||||||
@@ -770,6 +886,11 @@ function normalizeCreateForm(form: CreateContainerRequest): CreateContainerReque
|
|||||||
disk_gb: Math.round(normalized.disk_gb),
|
disk_gb: Math.round(normalized.disk_gb),
|
||||||
assign_nat: wantsNAT,
|
assign_nat: wantsNAT,
|
||||||
port_mapping_count: wantsNAT ? clampInt(normalized.port_mapping_count, 2, 64, 2) : 0,
|
port_mapping_count: wantsNAT ? clampInt(normalized.port_mapping_count, 2, 64, 2) : 0,
|
||||||
|
lan_ipv4_mode: wantsLANDHCP ? 'dhcp' : (wantsLANStatic ? 'static' : ''),
|
||||||
|
lan_interface: wantsLANIPv4 ? (normalized.lan_interface || '').trim() : '',
|
||||||
|
lan_ipv4_address: wantsLANStatic ? (normalized.lan_ipv4_address || '').trim() : '',
|
||||||
|
lan_ipv4_prefix_len: wantsLANStatic ? clampInt(normalized.lan_ipv4_prefix_len || 24, 1, 32, 24) : 0,
|
||||||
|
lan_ipv4_gateway: wantsLANStatic ? (normalized.lan_ipv4_gateway || '').trim() : '',
|
||||||
assign_ipv4: wantsIPv4,
|
assign_ipv4: wantsIPv4,
|
||||||
ipv4_count: wantsIPv4 ? clampInt(normalized.ipv4_count || 1, 1, 64, 1) : 0,
|
ipv4_count: wantsIPv4 ? clampInt(normalized.ipv4_count || 1, 1, 64, 1) : 0,
|
||||||
public_ipv4s: wantsIPv4 ? (normalized.public_ipv4s || []) : [],
|
public_ipv4s: wantsIPv4 ? (normalized.public_ipv4s || []) : [],
|
||||||
@@ -792,6 +913,16 @@ function validateSSHAuthInputs(form: CreateContainerRequest) {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLANDHCPInterfaces(report: HostProbeReport | null) {
|
||||||
|
const interfaces = report?.network_interfaces || []
|
||||||
|
return interfaces.filter((item) => {
|
||||||
|
const name = item.name || ''
|
||||||
|
if (!name || name === 'lo') return false
|
||||||
|
if (name.startsWith('lxc') || name.startsWith('docker') || name.startsWith('br-') || name.startsWith('veth') || name.startsWith('virbr') || name.startsWith('clmv-')) return false
|
||||||
|
return (item.state || '').toLowerCase() === 'up'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function applyTemplateDefaults(form: CreateContainerRequest): CreateContainerRequest {
|
function applyTemplateDefaults(form: CreateContainerRequest): CreateContainerRequest {
|
||||||
if (!isWindowsTemplate(form.template_id)) return form
|
if (!isWindowsTemplate(form.template_id)) return form
|
||||||
return {
|
return {
|
||||||
@@ -817,6 +948,28 @@ function clampInt(value: number, min: number, max?: number, fallback = min) {
|
|||||||
return Math.min(Math.max(next, min), max ?? next)
|
return Math.min(Math.max(next, min), max ?? next)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isIPv4Address(value: string) {
|
||||||
|
const parts = value.trim().split('.')
|
||||||
|
return parts.length === 4 && parts.every((part) => {
|
||||||
|
if (!/^\d+$/.test(part)) return false
|
||||||
|
const n = Number(part)
|
||||||
|
return n >= 0 && n <= 255
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function subnetMaskFromPrefixLen(prefixLen: number) {
|
||||||
|
if (!Number.isFinite(prefixLen) || prefixLen < 0 || prefixLen > 32) return '255.255.255.0'
|
||||||
|
const mask = prefixLen === 0 ? 0 : (0xffffffff << (32 - prefixLen)) >>> 0
|
||||||
|
return [24, 16, 8, 0].map((shift) => (mask >>> shift) & 255).join('.')
|
||||||
|
}
|
||||||
|
|
||||||
|
function prefixLenFromSubnetMask(mask: string) {
|
||||||
|
if (!isIPv4Address(mask)) return 0
|
||||||
|
const bits = mask.split('.').map((part) => Number(part).toString(2).padStart(8, '0')).join('')
|
||||||
|
if (!/^1*0*$/.test(bits)) return 0
|
||||||
|
return bits.indexOf('0') === -1 ? 32 : bits.indexOf('0')
|
||||||
|
}
|
||||||
|
|
||||||
const createNetworkText = {
|
const createNetworkText = {
|
||||||
zh: {
|
zh: {
|
||||||
publicIPv4: '公网 IPv4',
|
publicIPv4: '公网 IPv4',
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
updateRoutingPools,
|
updateRoutingPools,
|
||||||
type IPv4Route,
|
type IPv4Route,
|
||||||
type IPv6Route,
|
type IPv6Route,
|
||||||
|
type LANDHCPRoute,
|
||||||
type IPv6PrefixInfo,
|
type IPv6PrefixInfo,
|
||||||
type NAT4PortRange,
|
type NAT4PortRange,
|
||||||
type NAT4Route,
|
type NAT4Route,
|
||||||
@@ -56,6 +57,7 @@ export default function Routing() {
|
|||||||
|
|
||||||
const publicIPv4s = routing?.public_ipv4_addresses || []
|
const publicIPv4s = routing?.public_ipv4_addresses || []
|
||||||
const ipv4Assignments = routing?.ipv4_assignments || []
|
const ipv4Assignments = routing?.ipv4_assignments || []
|
||||||
|
const lanDHCPAssignments = routing?.lan_dhcp_assignments || []
|
||||||
const nat4Mappings = routing?.nat4_mappings || []
|
const nat4Mappings = routing?.nat4_mappings || []
|
||||||
const ipv6Prefixes = routing?.ipv6_prefixes || []
|
const ipv6Prefixes = routing?.ipv6_prefixes || []
|
||||||
const ipv6Assignments = routing?.ipv6_assignments || []
|
const ipv6Assignments = routing?.ipv6_assignments || []
|
||||||
@@ -276,7 +278,7 @@ export default function Routing() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-4">
|
||||||
<CapacityCard
|
<CapacityCard
|
||||||
title={text.nat4Ports}
|
title={text.nat4Ports}
|
||||||
watermark="NAT4"
|
watermark="NAT4"
|
||||||
@@ -293,6 +295,7 @@ export default function Routing() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<CapacityCard title={text.publicIPv4} watermark="IPv4" remaining={routing?.ipv4.remaining || '0'} total={routing?.ipv4.total || '0'} used={routing?.ipv4.used || 0} label={formatPoolCount(publicIPv4s.length, language)} usedLabel={text.used} />
|
<CapacityCard title={text.publicIPv4} watermark="IPv4" remaining={routing?.ipv4.remaining || '0'} total={routing?.ipv4.total || '0'} used={routing?.ipv4.used || 0} label={formatPoolCount(publicIPv4s.length, language)} usedLabel={text.used} />
|
||||||
|
<CapacityCard title={text.lanDHCP} watermark="LAN" remaining={String(routing?.lan_dhcp.used || 0)} total={routing?.lan_dhcp.total || 'DHCP'} used={routing?.lan_dhcp.used || 0} label={text.dhcpManagedByLAN} usedLabel={text.used} />
|
||||||
<CapacityCard title="IPv6" watermark="IPv6" remaining={formatCapacity(routing?.ipv6.remaining || '0', language)} total={formatCapacity(routing?.ipv6.total || '0', language)} used={routing?.ipv6.used || 0} label={formatDetectedPrefixCount(ipv6Prefixes.length, language)} usedLabel={text.used} />
|
<CapacityCard title="IPv6" watermark="IPv6" remaining={formatCapacity(routing?.ipv6.remaining || '0', language)} total={formatCapacity(routing?.ipv6.total || '0', language)} used={routing?.ipv6.used || 0} label={formatDetectedPrefixCount(ipv6Prefixes.length, language)} usedLabel={text.used} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -541,6 +544,48 @@ export default function Routing() {
|
|||||||
</RouteModal>
|
</RouteModal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Panel title={text.lanDHCPAssignments} subtitle={formatAddressSubtitle(lanDHCPAssignments.length, lanDHCPAssignments.length, language)}>
|
||||||
|
{lanDHCPAssignments.length === 0 ? (
|
||||||
|
<EmptyState text={text.noLANDHCPAssignments} icon={<Network className="h-7 w-7" />} />
|
||||||
|
) : (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full min-w-[980px] text-sm">
|
||||||
|
<thead className="border-b border-gray-200 bg-gray-50 text-xs text-gray-500">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left font-medium">{text.container}</th>
|
||||||
|
<th className="px-4 py-3 text-left font-medium">{text.runtimeName}</th>
|
||||||
|
<th className="px-4 py-3 text-left font-medium">{text.guestIPv4}</th>
|
||||||
|
<th className="px-4 py-3 text-left font-medium">模式</th>
|
||||||
|
<th className="px-4 py-3 text-left font-medium">{text.gateway}</th>
|
||||||
|
<th className="px-4 py-3 text-left font-medium">MAC</th>
|
||||||
|
<th className="px-4 py-3 text-left font-medium">{text.interface}</th>
|
||||||
|
<th className="px-4 py-3 text-left font-medium">{text.status}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-100">
|
||||||
|
{lanDHCPAssignments.map((item: LANDHCPRoute) => (
|
||||||
|
<tr key={`${item.container_id}-${item.interface}-${item.mac_address || item.address}`} className="hover:bg-gray-50">
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<button onClick={() => navigate(`/container/${item.container_id}`)} className="inline-flex items-center gap-2 text-left font-medium text-black hover:underline">
|
||||||
|
<Server className="h-4 w-4 text-gray-400" />
|
||||||
|
{item.container_name}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 font-mono text-xs text-gray-600">{item.lxc_name}</td>
|
||||||
|
<td className="px-4 py-3 font-mono text-xs text-gray-700">{item.address ? `${item.address}${item.prefix_len ? `/${item.prefix_len}` : ''}` : '-'}</td>
|
||||||
|
<td className="px-4 py-3 text-xs text-gray-600">{item.mode === 'static' ? '手动' : 'DHCP'}</td>
|
||||||
|
<td className="px-4 py-3 font-mono text-xs text-gray-600">{item.gateway || '-'}</td>
|
||||||
|
<td className="px-4 py-3 font-mono text-xs text-gray-600">{item.mac_address || '-'}</td>
|
||||||
|
<td className="px-4 py-3 font-mono text-xs text-gray-600">{item.interface || '-'}</td>
|
||||||
|
<td className="px-4 py-3"><StatusBadge status={item.status} language={language} /></td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Panel>
|
||||||
|
|
||||||
<Panel title={text.ipv4NAT} subtitle={formatMappingSubtitle(filteredNat4.length, nat4Mappings.length, language)} action={<SearchBox value={nat4Search} onChange={setNat4Search} placeholder={text.searchNAT} />}>
|
<Panel title={text.ipv4NAT} subtitle={formatMappingSubtitle(filteredNat4.length, nat4Mappings.length, language)} action={<SearchBox value={nat4Search} onChange={setNat4Search} placeholder={text.searchNAT} />}>
|
||||||
{nat4Mappings.length === 0 ? (
|
{nat4Mappings.length === 0 ? (
|
||||||
<EmptyState text={text.noIPv4NATMappings} icon={<Network className="h-7 w-7" />} />
|
<EmptyState text={text.noIPv4NATMappings} icon={<Network className="h-7 w-7" />} />
|
||||||
@@ -851,6 +896,10 @@ const routingText = {
|
|||||||
saveNAT4RangeFailed: '保存 NAT4 范围失败',
|
saveNAT4RangeFailed: '保存 NAT4 范围失败',
|
||||||
remainingTotal: '剩余 / 总数',
|
remainingTotal: '剩余 / 总数',
|
||||||
publicIPv4: '公网 IPv4',
|
publicIPv4: '公网 IPv4',
|
||||||
|
lanDHCP: '局域网 DHCP',
|
||||||
|
dhcpManagedByLAN: '由局域网 DHCP 分配',
|
||||||
|
lanDHCPAssignments: '局域网 DHCP 分配',
|
||||||
|
noLANDHCPAssignments: '暂无局域网 DHCP 分配',
|
||||||
publicIPv4Pool: '公网 IPv4 池',
|
publicIPv4Pool: '公网 IPv4 池',
|
||||||
editPool: '编辑 IP 池',
|
editPool: '编辑 IP 池',
|
||||||
noPublicIPv4Pool: '暂未配置公网 IPv4 池',
|
noPublicIPv4Pool: '暂未配置公网 IPv4 池',
|
||||||
@@ -922,6 +971,10 @@ const routingText = {
|
|||||||
saveNAT4RangeFailed: 'Save NAT4 range failed',
|
saveNAT4RangeFailed: 'Save NAT4 range failed',
|
||||||
remainingTotal: 'remaining / total',
|
remainingTotal: 'remaining / total',
|
||||||
publicIPv4: 'Public IPv4',
|
publicIPv4: 'Public IPv4',
|
||||||
|
lanDHCP: 'LAN DHCP',
|
||||||
|
dhcpManagedByLAN: 'Managed by LAN DHCP',
|
||||||
|
lanDHCPAssignments: 'LAN DHCP assignments',
|
||||||
|
noLANDHCPAssignments: 'No LAN DHCP assignments',
|
||||||
publicIPv4Pool: 'Public IPv4 pool',
|
publicIPv4Pool: 'Public IPv4 pool',
|
||||||
editPool: 'Edit pool',
|
editPool: 'Edit pool',
|
||||||
noPublicIPv4Pool: 'No public IPv4 pool configured',
|
noPublicIPv4Pool: 'No public IPv4 pool configured',
|
||||||
|
|||||||
@@ -94,6 +94,12 @@ export interface Container {
|
|||||||
io_write_mbps: number
|
io_write_mbps: number
|
||||||
status: string
|
status: string
|
||||||
ip: string
|
ip: string
|
||||||
|
lan_ipv4_mode?: string
|
||||||
|
lan_interface?: string
|
||||||
|
lan_ipv4_address?: string
|
||||||
|
lan_ipv4_prefix_len?: number
|
||||||
|
lan_ipv4_gateway?: string
|
||||||
|
mac_address?: string
|
||||||
public_ipv4s?: PublicIPv4Assignment[]
|
public_ipv4s?: PublicIPv4Assignment[]
|
||||||
ipv6: string
|
ipv6: string
|
||||||
ipv6_prefix_len: number
|
ipv6_prefix_len: number
|
||||||
@@ -154,6 +160,11 @@ export interface CreateContainerRequest {
|
|||||||
extra_ports: number[]
|
extra_ports: number[]
|
||||||
port_mapping_count: number
|
port_mapping_count: number
|
||||||
assign_nat?: boolean
|
assign_nat?: boolean
|
||||||
|
lan_ipv4_mode?: string
|
||||||
|
lan_interface?: string
|
||||||
|
lan_ipv4_address?: string
|
||||||
|
lan_ipv4_prefix_len?: number
|
||||||
|
lan_ipv4_gateway?: string
|
||||||
snapshot_limit: number
|
snapshot_limit: number
|
||||||
assign_ipv4?: boolean
|
assign_ipv4?: boolean
|
||||||
ipv4_count?: number
|
ipv4_count?: number
|
||||||
@@ -602,6 +613,19 @@ export interface IPv4Route {
|
|||||||
gateway?: string
|
gateway?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LANDHCPRoute {
|
||||||
|
container_id: number
|
||||||
|
container_name: string
|
||||||
|
lxc_name: string
|
||||||
|
status: string
|
||||||
|
address: string
|
||||||
|
interface: string
|
||||||
|
prefix_len?: number
|
||||||
|
gateway?: string
|
||||||
|
mac_address?: string
|
||||||
|
mode: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface IPv6Route {
|
export interface IPv6Route {
|
||||||
container_id: number
|
container_id: number
|
||||||
container_name: string
|
container_name: string
|
||||||
@@ -616,10 +640,12 @@ export interface RoutingInfo {
|
|||||||
nat4: RouteCapacity
|
nat4: RouteCapacity
|
||||||
nat4_port_range: NAT4PortRange
|
nat4_port_range: NAT4PortRange
|
||||||
ipv4: RouteCapacity
|
ipv4: RouteCapacity
|
||||||
|
lan_dhcp: RouteCapacity
|
||||||
ipv6: RouteCapacity
|
ipv6: RouteCapacity
|
||||||
host_public_ipv4?: PublicIPv4Info
|
host_public_ipv4?: PublicIPv4Info
|
||||||
public_ipv4_addresses: PublicIPv4Info[]
|
public_ipv4_addresses: PublicIPv4Info[]
|
||||||
ipv4_assignments: IPv4Route[]
|
ipv4_assignments: IPv4Route[]
|
||||||
|
lan_dhcp_assignments: LANDHCPRoute[]
|
||||||
nat4_mappings: NAT4Route[]
|
nat4_mappings: NAT4Route[]
|
||||||
ipv6_assignments: IPv6Route[]
|
ipv6_assignments: IPv6Route[]
|
||||||
ipv6_prefixes: IPv6PrefixInfo[]
|
ipv6_prefixes: IPv6PrefixInfo[]
|
||||||
|
|||||||
Reference in New Issue
Block a user