mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-05 05:36:07 +08:00
FIX #18
This commit is contained in:
@@ -46,6 +46,19 @@ type ipv4Route struct {
|
||||
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 {
|
||||
ContainerID int `json:"container_id"`
|
||||
ContainerName string `json:"container_name"`
|
||||
@@ -60,10 +73,12 @@ type routingResponse struct {
|
||||
NAT4 routeCapacity `json:"nat4"`
|
||||
NAT4PortRange nat4PortRange `json:"nat4_port_range"`
|
||||
IPv4 routeCapacity `json:"ipv4"`
|
||||
LANDHCP routeCapacity `json:"lan_dhcp"`
|
||||
IPv6 routeCapacity `json:"ipv6"`
|
||||
HostPublicIPv4 lxc.PublicIPInfo `json:"host_public_ipv4"`
|
||||
PublicIPv4Addresses []lxc.PublicIPInfo `json:"public_ipv4_addresses"`
|
||||
IPv4Assignments []ipv4Route `json:"ipv4_assignments"`
|
||||
LANDHCPAssignments []lanDHCPRoute `json:"lan_dhcp_assignments"`
|
||||
NAT4Mappings []nat4Route `json:"nat4_mappings"`
|
||||
IPv6Assignments []ipv6Route `json:"ipv6_assignments"`
|
||||
IPv6Prefixes []lxc.IPv6PrefixInfo `json:"ipv6_prefixes"`
|
||||
@@ -125,6 +140,7 @@ func handleRoutingGet(w http.ResponseWriter, r *http.Request) {
|
||||
nat4Mappings := make([]nat4Route, 0)
|
||||
usedPorts := map[int]bool{}
|
||||
ipv4Assignments := make([]ipv4Route, 0)
|
||||
lanDHCPAssignments := make([]lanDHCPRoute, 0)
|
||||
ipv6Assignments := make([]ipv6Route, 0)
|
||||
|
||||
nat4StartPort, nat4EndPort := config.NATPortRange()
|
||||
@@ -164,6 +180,20 @@ func handleRoutingGet(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
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 {
|
||||
if ip.Address == "" {
|
||||
continue
|
||||
@@ -191,6 +221,12 @@ func handleRoutingGet(w http.ResponseWriter, r *http.Request) {
|
||||
sort.SliceStable(ipv4Assignments, func(i, j int) bool {
|
||||
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 {
|
||||
return ipv6Assignments[i].Address < ipv6Assignments[j].Address
|
||||
})
|
||||
@@ -231,6 +267,11 @@ func handleRoutingGet(w http.ResponseWriter, r *http.Request) {
|
||||
Remaining: strconv.Itoa(ipv4Remaining),
|
||||
Total: strconv.Itoa(ipv4Total),
|
||||
},
|
||||
LANDHCP: routeCapacity{
|
||||
Used: len(lanDHCPAssignments),
|
||||
Remaining: "DHCP",
|
||||
Total: "DHCP",
|
||||
},
|
||||
IPv6: routeCapacity{
|
||||
Used: len(ipv6Assignments),
|
||||
Remaining: ipv6Remaining,
|
||||
@@ -239,6 +280,7 @@ func handleRoutingGet(w http.ResponseWriter, r *http.Request) {
|
||||
HostPublicIPv4: hostPublicIPv4,
|
||||
PublicIPv4Addresses: publicIPv4s,
|
||||
IPv4Assignments: ipv4Assignments,
|
||||
LANDHCPAssignments: lanDHCPAssignments,
|
||||
NAT4Mappings: nat4Mappings,
|
||||
IPv6Assignments: ipv6Assignments,
|
||||
IPv6Prefixes: prefixes,
|
||||
|
||||
@@ -20,7 +20,7 @@ func runtimeFromRequest(value string) string {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -650,6 +650,10 @@ func HandleBatchCreate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
req.Containers[i].NormalizeResourceAliases()
|
||||
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 {
|
||||
req.Containers[i].RAMMB = 512
|
||||
}
|
||||
|
||||
@@ -129,6 +129,11 @@ type Container struct {
|
||||
IOWriteMBps int `json:"io_write_mbps"`
|
||||
Status string `json:"status"`
|
||||
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"`
|
||||
IPv6 string `json:"ipv6"`
|
||||
IPv6PrefixLen int `json:"ipv6_prefix_len"`
|
||||
@@ -162,6 +167,9 @@ type Container struct {
|
||||
const (
|
||||
VirtualizationLXC = "lxc"
|
||||
VirtualizationKVM = "kvm"
|
||||
|
||||
LANIPv4ModeDHCP = "dhcp"
|
||||
LANIPv4ModeStatic = "static"
|
||||
)
|
||||
|
||||
func NormalizeVirtualization(value string) string {
|
||||
@@ -181,8 +189,56 @@ func (c *Container) IsKVM() bool {
|
||||
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 {
|
||||
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{}
|
||||
filteredIPv4 := make([]PublicIPv4Assignment, 0, len(c.PublicIPv4s))
|
||||
for _, item := range c.PublicIPv4s {
|
||||
|
||||
@@ -40,6 +40,11 @@ type savedTaskConfig struct {
|
||||
ExtraPorts []int `json:"extra_ports"`
|
||||
PortMappingCount int `json:"port_mapping_count"`
|
||||
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"`
|
||||
AllowedImageIDs []string `json:"allowed_image_ids,omitempty"`
|
||||
ImageLimitConfigured bool `json:"image_limit_configured,omitempty"`
|
||||
@@ -205,6 +210,11 @@ func ensureSchema() error {
|
||||
io_write_mbps INTEGER NOT NULL DEFAULT 0,
|
||||
status 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_prefix_len INTEGER,
|
||||
ipv6_interface TEXT,
|
||||
@@ -344,6 +354,11 @@ func ensureSchema() error {
|
||||
cfg_io_write_mbps INTEGER NOT NULL DEFAULT 0,
|
||||
cfg_port_mapping_count 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_assign_ipv4 INTEGER,
|
||||
cfg_ipv4_count INTEGER,
|
||||
@@ -418,6 +433,11 @@ func ensureSchemaMigrations() error {
|
||||
{"tasks", "cfg_ipv4_count", "INTEGER"},
|
||||
{"tasks", "cfg_public_ipv4s", "TEXT"},
|
||||
{"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_addresses", "TEXT"},
|
||||
{"tasks", "cfg_ssh_auth_mode", "TEXT"},
|
||||
@@ -439,6 +459,11 @@ func ensureSchemaMigrations() error {
|
||||
{"containers", "firewall_rules", "TEXT"},
|
||||
{"containers", "allowed_image_ids", "TEXT"},
|
||||
{"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)
|
||||
if err != nil {
|
||||
@@ -480,6 +505,18 @@ func ensureSchemaMigrations() error {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -698,19 +735,21 @@ func saveContainers(tx *sql.Tx) error {
|
||||
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,
|
||||
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,
|
||||
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, 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.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.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,
|
||||
boolInt(c.SnapshotScheduleEnabled), c.SnapshotScheduleIntervalHours, c.SnapshotScheduleTime,
|
||||
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_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_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_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,
|
||||
cfg.Name, cfg.Virtualization, cfg.TemplateID, cfg.VCPU, cfg.CPUPercent, cfg.RAMMB, cfg.DiskGB,
|
||||
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,
|
||||
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.AssignIPv6), cfg.IPv6Count, encodeStringSlice(cfg.IPv6Addresses),
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
snapshot_schedule_enabled, snapshot_schedule_interval_hours, snapshot_schedule_time,
|
||||
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 firewallDefaultAction string
|
||||
var firewallRulesJSON, allowedImageIDs sql.NullString
|
||||
var lanIPv4Address, lanIPv4Gateway sql.NullString
|
||||
var lanIPv4PrefixLen sql.NullInt64
|
||||
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.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.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,
|
||||
&scheduleEnabled, &c.SnapshotScheduleIntervalHours, &c.SnapshotScheduleTime,
|
||||
&c.SnapshotScheduleLastRun, &c.SnapshotScheduleNextRun, &c.SnapshotScheduleCreatedBy,
|
||||
@@ -948,6 +993,11 @@ func loadContainers() ([]Container, error) {
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.LANIPv4Address = lanIPv4Address.String
|
||||
if lanIPv4PrefixLen.Valid {
|
||||
c.LANIPv4PrefixLen = int(lanIPv4PrefixLen.Int64)
|
||||
}
|
||||
c.LANIPv4Gateway = lanIPv4Gateway.String
|
||||
c.SnapshotScheduleEnabled = scheduleEnabled != 0
|
||||
c.PolicyBlocked = policyBlocked != 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_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_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_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`)
|
||||
@@ -1173,15 +1224,16 @@ func loadTasks() ([]SavedTask, error) {
|
||||
var cfg savedTaskConfig
|
||||
var assignIPv4, assignIPv6, imageLimitConfigured int
|
||||
var ip, userAgent, publicIPv4s, ipv6Addresses sql.NullString
|
||||
var sshAuthMode, sshPassword, sshPublicKey, allowedImageIDs sql.NullString
|
||||
var assignNAT, ipv4Count, ipv6Count sql.NullInt64
|
||||
var lanIPv4Mode, lanInterface, lanIPv4Address, lanIPv4Gateway, sshAuthMode, sshPassword, sshPublicKey, allowedImageIDs sql.NullString
|
||||
var assignNAT, lanIPv4PrefixLen, ipv4Count, ipv6Count sql.NullInt64
|
||||
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.NetworkDownMbps, &cfg.NetworkUpMbps,
|
||||
&cfg.MonthlyTrafficGB, &cfg.TrafficMode, &cfg.TrafficInGB,
|
||||
&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,
|
||||
&sshAuthMode, &sshPassword, &sshPublicKey, &allowedImageIDs, &imageLimitConfigured, &cfg.ExpiresAt,
|
||||
); err != nil {
|
||||
@@ -1193,6 +1245,13 @@ func loadTasks() ([]SavedTask, error) {
|
||||
value := assignNAT.Int64 != 0
|
||||
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
|
||||
if ipv4Count.Valid {
|
||||
cfg.IPv4Count = int(ipv4Count.Int64)
|
||||
|
||||
+370
-8
@@ -8,6 +8,8 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -81,6 +83,13 @@ func (m *Manager) WarmRunningContainersSSH() {
|
||||
continue
|
||||
}
|
||||
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) {
|
||||
continue
|
||||
}
|
||||
@@ -238,6 +247,11 @@ type ContainerConfig struct {
|
||||
ExtraPorts []int `json:"extra_ports"`
|
||||
PortMappingCount int `json:"port_mapping_count"`
|
||||
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"`
|
||||
AllowedImageIDs []string `json:"allowed_image_ids,omitempty"`
|
||||
ImageLimitConfigured bool `json:"image_limit_configured,omitempty"`
|
||||
@@ -289,9 +303,24 @@ func (cfg *ContainerConfig) NormalizeResourceAliases() {
|
||||
}
|
||||
|
||||
func (cfg ContainerConfig) WantsNAT() bool {
|
||||
if cfg.WantsLANIPv4() {
|
||||
return false
|
||||
}
|
||||
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.
|
||||
func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
||||
cfg.NormalizeResourceAliases()
|
||||
@@ -355,6 +384,14 @@ func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
||||
_ = m.cleanupContainerStorage(lxcName)
|
||||
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.
|
||||
if err := m.applyResourceLimits(lxcName, cfg); err != nil {
|
||||
@@ -434,6 +471,7 @@ func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
||||
UUID: config.NewContainerUUID(),
|
||||
Name: cfg.Name,
|
||||
Virtualization: config.VirtualizationLXC,
|
||||
LXCName: lxcName,
|
||||
Template: cfg.TemplateID,
|
||||
VCPU: cfg.VCPU,
|
||||
RAMMB: cfg.RAMMB,
|
||||
@@ -451,6 +489,12 @@ func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
||||
IOWriteMBps: cfg.IOWriteMBps,
|
||||
Status: "stopped",
|
||||
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,
|
||||
IPv6Addresses: ipv6Assignments,
|
||||
VNCPort: 0,
|
||||
@@ -469,7 +513,7 @@ func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
||||
|
||||
// Pre-configure network and SSH in the rootfs before first boot.
|
||||
rootfsPath := filepath.Join(m.LxcPath, lxcName, "rootfs")
|
||||
m.preconfigureNetwork(rootfsPath, cfg.TemplateID)
|
||||
m.preconfigureNetwork(rootfsPath, cfg)
|
||||
if len(ipv6Assignments) > 0 {
|
||||
if err := installContainerIPv6Init(rootfsPath, ipv6AssignmentAddresses(ipv6Assignments)...); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
func (m *Manager) preconfigureNetwork(rootfsPath, templateID string) {
|
||||
func (m *Manager) preconfigureNetwork(rootfsPath string, cfg ContainerConfig) {
|
||||
templateID := cfg.TemplateID
|
||||
osRelease := ""
|
||||
if data, err := os.ReadFile(filepath.Join(rootfsPath, "etc", "os-release")); err == nil {
|
||||
osRelease = strings.ToLower(string(data))
|
||||
@@ -518,7 +563,13 @@ func (m *Manager) preconfigureNetwork(rootfsPath, templateID string) {
|
||||
|
||||
if isAlpine {
|
||||
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.WriteFile(interfaces, []byte(content), 0644)
|
||||
_ = m.runRootfsCommand(rootfsPath, "rc-update", "add", "networking", "boot")
|
||||
@@ -535,8 +586,16 @@ interface-name=eth0
|
||||
autoconnect=true
|
||||
|
||||
[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]
|
||||
method=ignore
|
||||
`
|
||||
@@ -552,9 +611,14 @@ method=ignore
|
||||
Name=eth0
|
||||
|
||||
[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
|
||||
`
|
||||
}
|
||||
_ = os.WriteFile(filepath.Join(networkdDir, "10-eth0.network"), []byte(network), 0644)
|
||||
}
|
||||
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.
|
||||
func (m *Manager) preconfigureSSH(rootfsPath, templateID string, sshAuthMode string) error {
|
||||
_ = templateID
|
||||
@@ -1460,6 +1744,7 @@ func (m *Manager) StartContainer(id int) error {
|
||||
c = config.FindContainer(id)
|
||||
if c != nil {
|
||||
c.IP = ip
|
||||
m.refreshContainerIPv4Details(c)
|
||||
config.SaveConfig()
|
||||
}
|
||||
}
|
||||
@@ -1473,6 +1758,9 @@ func (m *Manager) StartContainer(id int) error {
|
||||
}
|
||||
|
||||
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 {
|
||||
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("Container %d (%s) started, IP: %s\n", id, c.Name, ip)
|
||||
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)
|
||||
}
|
||||
c.IP = ip
|
||||
m.refreshContainerIPv4Details(c)
|
||||
config.SaveConfig()
|
||||
return ip, nil
|
||||
}
|
||||
@@ -1794,6 +2082,10 @@ func (m *Manager) WarmSSH(id int) error {
|
||||
if ip, err := m.GetContainerIP(lxcName); err == nil && ip != "" {
|
||||
if current := config.FindContainer(id); current != nil {
|
||||
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()
|
||||
}
|
||||
break
|
||||
@@ -1803,6 +2095,10 @@ func (m *Manager) WarmSSH(id int) error {
|
||||
if current := config.FindContainer(id); current != nil && current.IP == "" {
|
||||
if ip, err := m.EnsureContainerIPv4(id); err == nil && 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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
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
|
||||
func (m *Manager) ListContainers() ([]config.Container, error) {
|
||||
containers := config.AppConfig.Containers
|
||||
@@ -2565,6 +2926,7 @@ func (m *Manager) ListContainers() ([]config.Container, error) {
|
||||
ip, err := m.GetContainerIP(containers[i].LxcName())
|
||||
if err == nil {
|
||||
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.
|
||||
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 err := installContainerIPv6Init(rootfsPath, c.IPv6AddressStrings()...); err != nil {
|
||||
fmt.Printf("Warning: failed to install IPv6 init in %s after reinstall: %v\n", lxcName, err)
|
||||
|
||||
Reference in New Issue
Block a user