mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-06 13:54:44 +08:00
修复了一些已知问题
This commit is contained in:
+83
-19
@@ -429,6 +429,7 @@ func (m *Manager) defineContainer(id int, vmName string, cfg lxc.ContainerConfig
|
||||
ipv6Assignments = assigned
|
||||
}
|
||||
ipv6List := configIPv6AssignmentAddresses(ipv6Assignments)
|
||||
ipv4List := configIPv4AssignmentAddresses(publicIPv4s)
|
||||
defaultHostIP := lxc.DefaultPortMappingHostIP(publicIPv4s)
|
||||
|
||||
var xml string
|
||||
@@ -448,7 +449,7 @@ func (m *Manager) defineContainer(id int, vmName string, cfg lxc.ContainerConfig
|
||||
}
|
||||
winAdminPassword = generateWindowsPassword()
|
||||
unattendPath := filepath.Join(m.instanceDir(vmName), "unattend.iso")
|
||||
if err := createWindowsUnattendISO(unattendPath, cfg.Name, winAdminPassword, ipv6List); err != nil {
|
||||
if err := createWindowsUnattendISO(unattendPath, cfg.Name, winAdminPassword, ipv6List, ipv4List); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
xml = windowsDomainXML(vmName, int(cfg.VCPU), cfg.RAMMB, diskPath, ImagePath(image.ID), unattendPath, mac, cfg.IOSpeedMBps, cfg.NetworkBWMbps)
|
||||
@@ -464,7 +465,7 @@ func (m *Manager) defineContainer(id int, vmName string, cfg lxc.ContainerConfig
|
||||
if err := createOverlayDisk(ImagePath(image.ID), diskPath, cfg.DiskGB); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := createSeedISO(seedPath, vmName, cfg.Name, sshPassword, sshPublicKey, mac, ipv6List, *image); err != nil {
|
||||
if err := createSeedISO(seedPath, vmName, cfg.Name, sshPassword, sshPublicKey, mac, ipv6List, ipv4List, *image); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
xml = domainXML(vmName, int(cfg.VCPU), cfg.RAMMB, diskPath, seedPath, mac, cfg.IOSpeedMBps, cfg.NetworkBWMbps, image.Desktop != "")
|
||||
@@ -1389,7 +1390,7 @@ func (m *Manager) RefreshNetwork(id int) (string, error) {
|
||||
if changed {
|
||||
config.SaveConfig()
|
||||
}
|
||||
if c.Status == "running" && len(c.PortMappings) > 0 && shouldApplyPortMappings(id, changed) {
|
||||
if c.Status == "running" && shouldApplyPortMappings(id, changed) {
|
||||
if err := lxc.NewManager().ApplyPortMappings(id); err != nil {
|
||||
return ip, err
|
||||
}
|
||||
@@ -1596,7 +1597,7 @@ func createEmptyDisk(target string, diskGB int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func createWindowsUnattendISO(target, hostname, adminPassword string, ipv6s []string) error {
|
||||
func createWindowsUnattendISO(target, hostname, adminPassword string, ipv6s []string, ipv4s []string) error {
|
||||
tool := firstAvailableCommand("genisoimage", "mkisofs", "xorriso")
|
||||
if tool == "" {
|
||||
return fmt.Errorf("one of genisoimage, mkisofs, xorriso is required for Windows unattended setup")
|
||||
@@ -1622,13 +1623,13 @@ func createWindowsUnattendISO(target, hostname, adminPassword string, ipv6s []st
|
||||
if err := os.WriteFile(filepath.Join(setupScriptsDir, "SetupComplete.cmd"), []byte(windowsSetupCompleteCMD()), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(clicdDir, "FirstLogon.ps1"), []byte(windowsFirstLogonPowerShell(adminPassword, ipv6s)), 0600); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(clicdDir, "FirstLogon.ps1"), []byte(windowsFirstLogonPowerShell(adminPassword, ipv6s, ipv4s)), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "SetupComplete.cmd"), []byte(windowsSetupCompleteCMD()), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "FirstLogon.ps1"), []byte(windowsFirstLogonPowerShell(adminPassword, ipv6s)), 0600); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(dir, "FirstLogon.ps1"), []byte(windowsFirstLogonPowerShell(adminPassword, ipv6s, ipv4s)), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = os.Remove(target)
|
||||
@@ -1738,7 +1739,7 @@ exit /b 0
|
||||
`
|
||||
}
|
||||
|
||||
func windowsFirstLogonPowerShell(adminPassword string, ipv6s []string) string {
|
||||
func windowsFirstLogonPowerShell(adminPassword string, ipv6s []string, ipv4s []string) string {
|
||||
commands := []string{
|
||||
"$ErrorActionPreference='Continue'",
|
||||
"$ProgressPreference='SilentlyContinue'",
|
||||
@@ -1774,6 +1775,12 @@ func windowsFirstLogonPowerShell(adminPassword string, ipv6s []string) string {
|
||||
windowsIPv6PowerShell(ipv6s),
|
||||
)
|
||||
}
|
||||
ipv4s = normalizeKVMIPv4List(ipv4s)
|
||||
if len(ipv4s) > 0 {
|
||||
commands = append(commands,
|
||||
windowsIPv4PowerShell(ipv4s),
|
||||
)
|
||||
}
|
||||
commands = append(commands,
|
||||
"New-Item -ItemType File -Force -Path 'C:\\CLICD\\init.done' | Out-Null",
|
||||
"} finally { Stop-Transcript | Out-Null }",
|
||||
@@ -1806,11 +1813,47 @@ func windowsIPv6PowerShell(ipv6s []string) string {
|
||||
}, "\r\n")
|
||||
}
|
||||
|
||||
func windowsIPv4PowerShell(ipv4s []string) string {
|
||||
ipv4s = normalizeKVMIPv4List(ipv4s)
|
||||
if len(ipv4s) == 0 {
|
||||
return ""
|
||||
}
|
||||
quoted := make([]string, 0, len(ipv4s))
|
||||
for _, ipv4 := range ipv4s {
|
||||
quoted = append(quoted, "'"+strings.ReplaceAll(ipv4, "'", "''")+"'")
|
||||
}
|
||||
return strings.Join([]string{
|
||||
"$clicdIPv4=@(" + strings.Join(quoted, ",") + ")",
|
||||
"$iface=$null",
|
||||
"for ($i=0; $i -lt 60 -and -not $iface; $i++) { $iface=Get-NetAdapter | Where-Object { $_.Status -eq 'Up' -and $_.HardwareInterface } | Sort-Object ifIndex | Select-Object -First 1; if (-not $iface) { Start-Sleep -Seconds 5 } }",
|
||||
"if ($iface) {",
|
||||
" foreach ($ip in $clicdIPv4) {",
|
||||
" Get-NetIPAddress -InterfaceIndex $iface.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object { $_.IPAddress -eq $ip } | Remove-NetIPAddress -Confirm:$false -ErrorAction SilentlyContinue",
|
||||
" New-NetIPAddress -IPAddress $ip -PrefixLength 32 -InterfaceIndex $iface.ifIndex -SkipAsSource:$false -ErrorAction SilentlyContinue | Out-Null",
|
||||
" }",
|
||||
"}",
|
||||
}, "\r\n")
|
||||
}
|
||||
|
||||
func normalizeKVMIPv4List(values []string) []string {
|
||||
seen := map[string]bool{}
|
||||
result := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" || seen[value] {
|
||||
continue
|
||||
}
|
||||
seen[value] = true
|
||||
result = append(result, value)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func shellQuoteWindows(value string) string {
|
||||
return `"` + strings.ReplaceAll(value, `"`, `\"`) + `"`
|
||||
}
|
||||
|
||||
func createSeedISO(seedPath, instanceID, hostname, password, publicKey, mac string, ipv6s []string, image Image) error {
|
||||
func createSeedISO(seedPath, instanceID, hostname, password, publicKey, mac string, ipv6s []string, ipv4s []string, image Image) error {
|
||||
guestSetup := kvmSSHSetupScript(password, publicKey)
|
||||
if desktopSetup := kvmDesktopSetupScript(image); desktopSetup != "" {
|
||||
guestSetup += "\n" + desktopSetup
|
||||
@@ -1846,29 +1889,40 @@ runcmd:
|
||||
%s
|
||||
`, hostname, password, authorizedKeys, setupScript)
|
||||
metaData := fmt.Sprintf("instance-id: %s\nlocal-hostname: %s\n", instanceID, hostname)
|
||||
ipv6Block := ""
|
||||
|
||||
// Build static address block (IPv4 + IPv6)
|
||||
ipv4s = normalizeKVMIPv4List(ipv4s)
|
||||
addressBlock := ""
|
||||
addressLines := make([]string, 0, len(ipv4s)+len(ipv6s))
|
||||
for _, ipv4 := range ipv4s {
|
||||
addressLines = append(addressLines, fmt.Sprintf(" - %s/32", ipv4))
|
||||
}
|
||||
for _, ipv6 := range ipv6s {
|
||||
addressLines = append(addressLines, fmt.Sprintf(" - %s/128", ipv6))
|
||||
}
|
||||
if len(addressLines) > 0 {
|
||||
addressBlock = fmt.Sprintf("\n addresses:\n%s", strings.Join(addressLines, "\n"))
|
||||
}
|
||||
|
||||
// IPv6 routes (only needed when IPv6 addresses are configured)
|
||||
ipv6RouteBlock := ""
|
||||
if len(ipv6s) > 0 {
|
||||
addressLines := make([]string, 0, len(ipv6s))
|
||||
for _, ipv6 := range ipv6s {
|
||||
addressLines = append(addressLines, fmt.Sprintf(" - %s/128", ipv6))
|
||||
}
|
||||
ipv6Block = fmt.Sprintf(`
|
||||
addresses:
|
||||
%s
|
||||
ipv6RouteBlock = fmt.Sprintf(`
|
||||
routes:
|
||||
- to: default
|
||||
via: %s
|
||||
on-link: true
|
||||
metric: 100`, strings.Join(addressLines, "\n"), ipv6GatewayLinkLocal)
|
||||
metric: 100`, ipv6GatewayLinkLocal)
|
||||
}
|
||||
|
||||
networkConfig := fmt.Sprintf(`version: 2
|
||||
ethernets:
|
||||
nic0:
|
||||
match:
|
||||
macaddress: "%s"
|
||||
dhcp4: true
|
||||
dhcp6: false%s
|
||||
`, strings.ToLower(mac), ipv6Block)
|
||||
dhcp6: false%s%s
|
||||
`, strings.ToLower(mac), addressBlock, ipv6RouteBlock)
|
||||
dir := filepath.Dir(seedPath)
|
||||
userPath := filepath.Join(dir, "user-data")
|
||||
metaPath := filepath.Join(dir, "meta-data")
|
||||
@@ -1900,6 +1954,16 @@ func configIPv6AssignmentAddresses(assignments []config.IPv6Assignment) []string
|
||||
return values
|
||||
}
|
||||
|
||||
func configIPv4AssignmentAddresses(assignments []config.PublicIPv4Assignment) []string {
|
||||
values := make([]string, 0, len(assignments))
|
||||
for _, item := range assignments {
|
||||
if strings.TrimSpace(item.Address) != "" {
|
||||
values = append(values, strings.TrimSpace(item.Address))
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func normalizeKVMIPv6List(values []string) []string {
|
||||
seen := map[string]bool{}
|
||||
result := make([]string, 0, len(values))
|
||||
|
||||
@@ -59,11 +59,60 @@ func (m *Manager) ApplyPortMappings(id int) error {
|
||||
}
|
||||
}
|
||||
|
||||
// When container has public IPv4 but no port mappings (independent IP mode),
|
||||
// ensure inbound DNAT for standard service ports (SSH / RDP).
|
||||
if len(c.PortMappings) == 0 && len(c.PublicIPv4s) > 0 {
|
||||
ensureIndependentIPv4Ingress(c, tag)
|
||||
}
|
||||
|
||||
applyIPv4EgressPolicy(c, bridge, subnet, tag)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureIndependentIPv4Ingress(c *config.Container, tag string) {
|
||||
if c == nil || c.IP == "" || len(c.PublicIPv4s) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
type svcPort struct {
|
||||
port int
|
||||
protocol string
|
||||
desc string
|
||||
}
|
||||
servicePorts := []svcPort{{port: 22, protocol: "tcp", desc: "SSH"}}
|
||||
if strings.Contains(strings.ToLower(c.Template), "windows") {
|
||||
servicePorts = []svcPort{{port: 3389, protocol: "tcp", desc: "RDP"}}
|
||||
}
|
||||
|
||||
for _, assignment := range c.PublicIPv4s {
|
||||
hostIP := strings.TrimSpace(assignment.Address)
|
||||
if hostIP == "" {
|
||||
continue
|
||||
}
|
||||
for _, svc := range servicePorts {
|
||||
args := []string{
|
||||
"-t", "nat",
|
||||
"-I", "PREROUTING", "1",
|
||||
"-d", hostIP,
|
||||
"-p", svc.protocol,
|
||||
"--dport", fmt.Sprintf("%d", svc.port),
|
||||
"-j", "DNAT",
|
||||
"--to-destination", fmt.Sprintf("%s:%d", c.IP, svc.port),
|
||||
"-m", "comment", "--comment", fmt.Sprintf("clicd-%s-%s-%d", tag, natRuleIPTag(hostIP), svc.port),
|
||||
}
|
||||
cmd := exec.Command("iptables", args...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: failed to apply %s ingress %s:%d->%s:%d: %v, output: %s\n",
|
||||
svc.desc, hostIP, svc.port, c.IP, svc.port, err, string(output))
|
||||
continue
|
||||
}
|
||||
fmt.Printf("%s ingress: %s:%d -> %s:%d\n", svc.desc, hostIP, svc.port, c.IP, svc.port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applyIPv4EgressPolicy(c *config.Container, bridge, subnet, tag string) {
|
||||
if c == nil || strings.TrimSpace(c.IP) == "" {
|
||||
return
|
||||
|
||||
@@ -770,8 +770,26 @@ export default function ContainerDetail() {
|
||||
if (ipv6List.length === 0 && container.ipv6) ipv6List.push(container.ipv6)
|
||||
const maxVCPU = hostInfo?.cpu.cores || 64
|
||||
const maxRAMMB = hostInfo?.ram.total_mb ? Number(hostInfo.ram.total_mb) : undefined
|
||||
const publicEndpoint = container.ssh_port > 0 ? `${publicHost}:${container.ssh_port}` : '-'
|
||||
const sshCommand = container.ssh_port > 0 ? `ssh -p ${container.ssh_port} root@${publicHost}` : ''
|
||||
const hasIndependentIPv4 = assignedIPv4List.length > 0
|
||||
const hasIndependentIPv6 = ipv6List.length > 0
|
||||
const hasIndependentIP = hasIndependentIPv4 || hasIndependentIPv6
|
||||
const defaultConnPort = isWindows ? 3389 : 22
|
||||
|
||||
let publicEndpoint = '-'
|
||||
let sshCommand = ''
|
||||
|
||||
if (container.ssh_port > 0) {
|
||||
// NAT port mapping mode
|
||||
publicEndpoint = `${publicHost}:${container.ssh_port}`
|
||||
sshCommand = `ssh -p ${container.ssh_port} root@${publicHost}`
|
||||
} else if (hasIndependentIP) {
|
||||
// Direct connection via independent IPv4 or IPv6
|
||||
const connIP = hasIndependentIPv4 ? assignedIPv4List[0] : `[${ipv6List[0]}]`
|
||||
publicEndpoint = `${connIP}:${defaultConnPort}`
|
||||
if (!isWindows) {
|
||||
sshCommand = `ssh root@${connIP}`
|
||||
}
|
||||
}
|
||||
const editingSSH = draft.index !== null && !!container.port_mappings?.[draft.index] && (
|
||||
container.port_mappings[draft.index].description === 'SSH' || container.port_mappings[draft.index].container_port === 22 ||
|
||||
container.port_mappings[draft.index].description === 'RDP' || container.port_mappings[draft.index].container_port === 3389
|
||||
|
||||
Reference in New Issue
Block a user