220 lines
5.4 KiB
Go
220 lines
5.4 KiB
Go
package netconfig
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"os/exec"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const nodeworxBin = "/usr/bin/nodeworx"
|
|
|
|
type interworx struct {
|
|
}
|
|
|
|
// Move sites off the old IP to a new IP.
|
|
func (*interworx) moveSites(ctx context.Context, oldAddr, newAddr net.IP) error {
|
|
cmd := exec.CommandContext(ctx, "/usr/local/interworx/bin/php", "/usr/local/interworx/bin/migrate-ip.php")
|
|
|
|
// Get output pipes.
|
|
stdout, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
stderr, err := cmd.StderrPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
stdin, err := cmd.StdinPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Send the IP addresses.
|
|
|
|
// Start the command.
|
|
err = cmd.Start()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Setup wait group and buffer.
|
|
var wg sync.WaitGroup
|
|
wg.Add(2)
|
|
|
|
// Setup expected line matching.
|
|
oldMatch := regexp.MustCompile(`(?i)OLD IP Address`)
|
|
newMatch := regexp.MustCompile(`(?i)NEW IP Address`)
|
|
revokeMatch := regexp.MustCompile(`(?i)\[y/N\]`)
|
|
|
|
// Read stdout, and provide answers to expected lines.
|
|
var line strings.Builder
|
|
go func() {
|
|
for {
|
|
// Read one byte to avoid lock up when the migrator asks for info.
|
|
buf := make([]byte, 1)
|
|
n, err := stdout.Read(buf)
|
|
if err != nil || n != 1 {
|
|
break
|
|
}
|
|
|
|
// If this is a new line, write the log and reset the line.
|
|
if buf[0] == '\n' || buf[0] == '\r' {
|
|
logger.Printf("Interworx migrate <stdout>: %s\n", line.String())
|
|
line.Reset()
|
|
continue
|
|
}
|
|
|
|
// Write the read byte to the buffer.
|
|
line.Write(buf)
|
|
|
|
// If the byte matches expected questions, answer them.
|
|
if buf[0] == ']' {
|
|
if revokeMatch.MatchString(line.String()) {
|
|
fmt.Fprintln(stdin, "y")
|
|
}
|
|
} else if buf[0] == ':' {
|
|
if oldMatch.MatchString(line.String()) {
|
|
fmt.Fprintln(stdin, oldAddr.String())
|
|
} else if newMatch.MatchString(line.String()) {
|
|
fmt.Fprintln(stdin, newAddr.String())
|
|
}
|
|
}
|
|
}
|
|
wg.Done()
|
|
}()
|
|
|
|
// Read stderr.
|
|
stderrScanner := bufio.NewScanner(stderr)
|
|
go func() {
|
|
for stderrScanner.Scan() {
|
|
line := stderrScanner.Text()
|
|
logger.Printf("Interworx migrate <stderr>: %s\n", line)
|
|
}
|
|
wg.Done()
|
|
}()
|
|
|
|
// Wait for file reads to finish before.
|
|
wg.Wait()
|
|
|
|
// Wait for the command to finish.
|
|
err = cmd.Wait()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Remove an IP address from interworx.
|
|
func (p *interworx) removeIP(ctx context.Context, addr net.IP) error {
|
|
// We need the IP type.
|
|
isIPv6 := addr.To4() == nil
|
|
|
|
// Get list of IP addresses in nodeworx.
|
|
out, err := runCommand(ctx, nodeworxBin, "-u", "-n", "-c", "Ip", "-a", "listIpAddresses")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Data to capture from IP list.
|
|
var sharedIP net.IP
|
|
var publicIPs []net.IP
|
|
var privateIPs []net.IP
|
|
|
|
// Parse the IP list.
|
|
for _, line := range out {
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 4 {
|
|
continue
|
|
}
|
|
|
|
// Parse the IP field.
|
|
ip := net.ParseIP(fields[0])
|
|
|
|
// Skip IP addresses of a different type.
|
|
if ip.To4() == nil && !isIPv6 {
|
|
continue
|
|
} else if ip.To4() != nil && isIPv6 {
|
|
continue
|
|
}
|
|
|
|
// Depending on IP match, apply to fields. When we find the address we are removing,
|
|
// we should determine how may sites are hosted by the IP. If the type is an shared iP,
|
|
// set the shared IP to it. Otherwise, associate other IP types to public/private IP
|
|
// lists to allow us to select an IP where a shared IP doesn't exist.
|
|
if ip.Equal(addr) {
|
|
continue
|
|
} else if fields[3] == "shared" && !ip.IsPrivate() {
|
|
sharedIP = ip
|
|
} else if !ip.IsPrivate() {
|
|
publicIPs = append(publicIPs, ip)
|
|
} else {
|
|
pubIP := net.ParseIP(fields[1])
|
|
if pubIP != nil && !pubIP.IsPrivate() {
|
|
publicIPs = append(publicIPs, ip)
|
|
} else {
|
|
privateIPs = append(privateIPs, ip)
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no shared IP, try public IP and private IP association instead.
|
|
if sharedIP == nil {
|
|
if len(publicIPs) != 0 {
|
|
sharedIP = publicIPs[0]
|
|
} else if len(privateIPs) != 0 {
|
|
sharedIP = privateIPs[0]
|
|
} else {
|
|
return fmt.Errorf("unable to find a shared IP to move ip addresses to")
|
|
}
|
|
}
|
|
|
|
// Migrate sites to new shared IP.
|
|
err = p.moveSites(ctx, addr, sharedIP)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Wait briefly for the migration to propagate before removing the address.
|
|
// The wait honours ctx cancellation so the caller can abort a stuck removal.
|
|
select {
|
|
case <-time.After(30 * time.Second):
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
|
|
// Tell nodeworx to remove the IP address.
|
|
out, err = runCommand(ctx, nodeworxBin, "-u", "-n", "-c", "Ip", "-a", "delete", "--confirm_action", "all", "--ip", addr.String())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(out) != 0 {
|
|
logger.Println("Interworx output:", strings.Join(out, "\n"))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// setMainIP is a no-op for InterWorx. InterWorx changes the primary IP with the
|
|
// migrate-ip tool, which moves sites between IPs rather than repointing a stored
|
|
// main IP. That migration is performed by removeIP (via moveSites) when the old
|
|
// primary is removed, so promoting an address needs no separate action here.
|
|
func (*interworx) setMainIP(ctx context.Context, addr net.IP) error {
|
|
return nil
|
|
}
|
|
|
|
// Tell interworx to reload the available IP addresses.
|
|
func (*interworx) reload(ctx context.Context) error {
|
|
out, err := runCommand(ctx, nodeworxBin, "-u", "-n", "-c", "Ip", "-a", "import", "--ip", "all")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(out) != 0 {
|
|
logger.Println("Interworx output:", strings.Join(out, "\n"))
|
|
}
|
|
return nil
|
|
}
|