From 2bc40f66e163faf4767b096c17cc1b3671cbb64d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graber?= Date: Sun, 5 Nov 2023 01:53:04 -0400 Subject: [PATCH] lxd-to-incus: Add support for OVN database mangling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #216 Signed-off-by: Stéphane Graber --- cmd/lxd-to-incus/main.go | 60 +++++++++---- cmd/lxd-to-incus/ovn.go | 181 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 226 insertions(+), 15 deletions(-) create mode 100644 cmd/lxd-to-incus/ovn.go diff --git a/cmd/lxd-to-incus/main.go b/cmd/lxd-to-incus/main.go index 8b927624cb9..5f0e18470d8 100644 --- a/cmd/lxd-to-incus/main.go +++ b/cmd/lxd-to-incus/main.go @@ -237,6 +237,38 @@ func (c *cmdMigrate) Run(app *cobra.Command, args []string) error { } } + // Mangle OVS/OVN. + srcServerInfo, _, err := srcClient.GetServer() + if err != nil { + return fmt.Errorf("Failed to get source server info: %w", err) + } + + ovnNB, ok := srcServerInfo.Config["network.ovn.northbound_connection"].(string) + if ok && ovnNB != "" { + if !c.flagClusterMember { + out, err := subprocess.RunCommand("ovs-vsctl", "get", "open_vswitch", ".", "external_ids:ovn-remote") + if err != nil { + return fmt.Errorf("Failed to get OVN southbound database address: %w", err) + } + + ovnSB := strings.TrimSpace(strings.Replace(out, "\"", "", -1)) + + commands, err := ovnConvert(ovnNB, ovnSB) + if err != nil { + return fmt.Errorf("Failed to prepare OVN conversion: %v", err) + } + + rewriteCommands = append(rewriteCommands, commands...) + } + + commands, err := ovsConvert() + if err != nil { + return fmt.Errorf("Failed to prepare OVS conversion: %v", err) + } + + rewriteCommands = append(rewriteCommands, commands...) + } + // Confirm migration. if !c.flagClusterMember && !c.flagYes { if !clustered { @@ -392,23 +424,21 @@ Instead this tool will be providing specific commands for each of the servers. return fmt.Errorf("Failed to migrate database in %q: %w", filepath.Join(targetPaths.Daemon, "database"), err) } - // Apply custom SQL statements. - if !c.flagClusterMember { - if len(rewriteStatements) > 0 { - fmt.Println("=> Writing database patch") - err = os.WriteFile(filepath.Join(targetPaths.Daemon, "database", "patch.global.sql"), []byte(strings.Join(rewriteStatements, "\n")+"\n"), 0600) - if err != nil { - return fmt.Errorf("Failed to write database path: %w", err) - } + // Apply custom migration statements. + if len(rewriteStatements) > 0 { + fmt.Println("=> Writing database patch") + err = os.WriteFile(filepath.Join(targetPaths.Daemon, "database", "patch.global.sql"), []byte(strings.Join(rewriteStatements, "\n")+"\n"), 0600) + if err != nil { + return fmt.Errorf("Failed to write database path: %w", err) } + } - if len(rewriteCommands) > 0 { - fmt.Println("=> Running data migration commands") - for _, cmd := range rewriteCommands { - _, err := subprocess.RunCommand(cmd[0], cmd[1:]...) - if err != nil { - return err - } + if len(rewriteCommands) > 0 { + fmt.Println("=> Running data migration commands") + for _, cmd := range rewriteCommands { + _, err := subprocess.RunCommand(cmd[0], cmd[1:]...) + if err != nil { + return err } } } diff --git a/cmd/lxd-to-incus/ovn.go b/cmd/lxd-to-incus/ovn.go new file mode 100644 index 00000000000..0d442f1e157 --- /dev/null +++ b/cmd/lxd-to-incus/ovn.go @@ -0,0 +1,181 @@ +package main + +import ( + "encoding/csv" + "fmt" + "strings" + + "github.com/lxc/incus/shared/subprocess" +) + +func ovsConvert() ([][]string, error) { + commands := [][]string{} + + output, err := subprocess.RunCommand("ovs-vsctl", "get", "open_vswitch", ".", "external-ids:ovn-bridge-mappings") + if err != nil { + return nil, err + } + + oldValue := strings.TrimSpace(strings.Replace(output, "\"", "", -1)) + + values := strings.Split(oldValue, ",") + for i, value := range values { + fields := strings.Split(value, ":") + fields[1] = strings.Replace(fields[1], "lxdovn", "incusovn", -1) + values[i] = strings.Join(fields, ":") + } + + newValue := strings.Join(values, ",") + + if oldValue != newValue { + commands = append(commands, []string{"ovs-vsctl", "set", "openv_vswitch", ".", fmt.Sprintf("external-ids:ovn-bridge-mappings=%s", newValue)}) + } + + return commands, nil +} + +func ovnConvert(nbDB string, sbDB string) ([][]string, error) { + commands := [][]string{} + + // Patch the Northbound records. + output, err := subprocess.RunCommand("ovsdb-client", "dump", "-f", "csv", nbDB, "OVN_Northbound") + if err != nil { + return nil, err + } + + data, err := ovnParseDump(output) + if err != nil { + return nil, err + } + + for table, records := range data { + for _, record := range records { + for k, v := range record { + needsFixing, newValue, err := ovnCheckValue(table, k, v) + if err != nil { + return nil, err + } + + if needsFixing { + commands = append(commands, []string{"ovn-nbctl", "--db", nbDB, "set", table, record["_uuid"], fmt.Sprintf("%s=%s", k, newValue)}) + } + } + } + } + + // Patch the Southbound records. + output, err = subprocess.RunCommand("ovsdb-client", "dump", "-f", "csv", sbDB, "OVN_Southbound") + if err != nil { + return nil, err + } + + data, err = ovnParseDump(output) + if err != nil { + return nil, err + } + + for table, records := range data { + for _, record := range records { + for k, v := range record { + needsFixing, newValue, err := ovnCheckValue(table, k, v) + if err != nil { + return nil, err + } + + if needsFixing { + commands = append(commands, []string{"ovn-sbctl", "--db", sbDB, "set", table, record["_uuid"], fmt.Sprintf("%s=%s", k, newValue)}) + } + } + } + } + + return commands, nil +} + +func ovnCheckValue(table string, k string, v string) (bool, string, error) { + if !strings.Contains(v, "lxd") { + return false, "", nil + } + + if table == "DNS" && k == "records" { + return false, "", nil + } + + if table == "Chassis" && k == "other_config" { + return false, "", nil + } + + if table == "Logical_Flow" && k == "actions" { + return false, "", nil + } + + if table == "DHCP_Options" && k == "options" { + return false, "", nil + } + + if table == "Logical_Router_Port" && k == "ipv6_ra_configs" { + return false, "", nil + } + + newValue := strings.Replace(v, "lxd-net", "incus-net", -1) + newValue = strings.Replace(newValue, "lxd_acl", "incus_acl", -1) + newValue = strings.Replace(newValue, "lxd_location", "incus_location", -1) + newValue = strings.Replace(newValue, "lxd_net", "incus_net", -1) + newValue = strings.Replace(newValue, "lxd_port_group", "incus_port_group", -1) + newValue = strings.Replace(newValue, "lxd_project_id", "incus_project_id", -1) + newValue = strings.Replace(newValue, "lxd_switch", "incus_switch", -1) + newValue = strings.Replace(newValue, "lxd_switch_port", "incus_switch_port", -1) + + if v == newValue { + return true, "", fmt.Errorf("Couldn't convert value %q for key %q in table %q", v, k, table) + } + + return true, newValue, nil +} + +func ovnParseDump(data string) (map[string][]map[string]string, error) { + output := map[string][]map[string]string{} + + tableName := "" + fields := []string{} + newTable := false + for _, line := range strings.Split(data, "\n") { + if line == "" { + continue + } + + if !strings.Contains(line, ",") && strings.HasSuffix(line, " table") { + newTable = true + tableName = strings.Split(line, " ")[0] + output[tableName] = []map[string]string{} + continue + } + + if newTable { + newTable = false + + var err error + fields, err = csv.NewReader(strings.NewReader(line)).Read() + if err != nil { + return nil, err + } + + continue + } + + record := map[string]string{} + + entry, err := csv.NewReader(strings.NewReader(line)).Read() + if err != nil { + return nil, err + } + + for k, v := range entry { + record[fields[k]] = v + } + + output[tableName] = append(output[tableName], record) + } + + return output, nil +}