Skip to content

Commit

Permalink
Add switch migrate endpoint (#566)
Browse files Browse the repository at this point in the history
  • Loading branch information
iljarotar authored Nov 7, 2024
1 parent b01c59b commit 3d4b264
Show file tree
Hide file tree
Showing 11 changed files with 2,281 additions and 56 deletions.
16 changes: 14 additions & 2 deletions cmd/metal-api/internal/datastore/switch.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,21 @@ func (rs *RethinkStore) ConnectMachineWithSwitches(m *metal.Machine) error {
if err != nil {
return err
}
// e.g. "swp1s0" -> "Ethernet0"
switchPortMapping, err := s1.MapPortNames(s2.OS.Vendor)
if err != nil {
return fmt.Errorf("could not create port mapping %w", err)
}

for _, con := range s1.MachineConnections[m.ID] {
if con2, has := byNicName[con.Nic.Name]; has {
if con.Nic.Name != con2.Nic.Name {
// get the corresponding interface name for s2
name, ok := switchPortMapping[con.Nic.Name]
if !ok {
return fmt.Errorf("could not translate port name %s to equivalent port name of switch os %s", con.Nic.Name, s1.OS.Vendor)
}
// check if s2 contains nic of name corresponding to con.Nic.Name
if con2, has := byNicName[name]; has {
if name != con2.Nic.Name {
return connectionMapError
}
} else {
Expand Down
228 changes: 225 additions & 3 deletions cmd/metal-api/internal/metal/switch.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package metal

import (
"fmt"
"strconv"
"strings"
"time"
)

Expand All @@ -24,11 +26,20 @@ type Switch struct {
type Switches []Switch

type SwitchOS struct {
Vendor string `rethinkdb:"vendor" json:"vendor"`
Version string `rethinkdb:"version" json:"version"`
MetalCoreVersion string `rethinkdb:"metal_core_version" json:"metal_core_version"`
Vendor SwitchOSVendor `rethinkdb:"vendor" json:"vendor"`
Version string `rethinkdb:"version" json:"version"`
MetalCoreVersion string `rethinkdb:"metal_core_version" json:"metal_core_version"`
}

// SwitchOSVendor is an enum denoting the name of a switch OS
type SwitchOSVendor string

// The enums for switch OS vendors
const (
SwitchOSVendorSonic SwitchOSVendor = "SONiC"
SwitchOSVendorCumulus SwitchOSVendor = "Cumulus"
)

// Connection between switch port and machine.
type Connection struct {
Nic Nic `rethinkdb:"nic" json:"nic"`
Expand Down Expand Up @@ -81,6 +92,15 @@ func SwitchModeFrom(name string) SwitchMode {
}
}

func ValidateSwitchOSVendor(os SwitchOSVendor) error {
switch os {
case SwitchOSVendorCumulus, SwitchOSVendorSonic:
return nil
default:
return fmt.Errorf("unknown switch os vendor %s", os)
}
}

// ByNicName builds a map of nic names to machine connection
func (c ConnectionMap) ByNicName() (map[string]Connection, error) {
res := make(map[string]Connection)
Expand Down Expand Up @@ -144,3 +164,205 @@ func (s *Switch) SetVrfOfMachine(m *Machine, vrf string) {
}
s.Nics = nics
}

// TranslateNicMap creates a NicMap where the keys are translated to the naming convention of the target OS
//
// example mapping from cumulus to sonic for one single port:
//
// map[string]Nic {
// "swp1s1": Nic{
// Name: "Ethernet1",
// MacAddress: ""
// }
// }
func (s *Switch) TranslateNicMap(targetOS SwitchOSVendor) (NicMap, error) {
nicMap := s.Nics.ByName()
translatedNicMap := make(NicMap)

if s.OS.Vendor == targetOS {
return nicMap, nil
}

ports := make([]string, 0)
for name := range nicMap {
ports = append(ports, name)
}

lines, err := getLinesFromPortNames(ports, s.OS.Vendor)
if err != nil {
return nil, err
}

for _, p := range ports {
targetPort, err := mapPortName(p, s.OS.Vendor, targetOS, lines)
if err != nil {
return nil, err
}

nic, ok := nicMap[p]
if !ok {
return nil, fmt.Errorf("an unknown error occured during port name translation")
}
translatedNicMap[targetPort] = nic
}

return translatedNicMap, nil
}

// MapPortNames creates a dictionary that maps the naming convention of this switch's OS to that of the target OS
func (s *Switch) MapPortNames(targetOS SwitchOSVendor) (map[string]string, error) {
nics := s.Nics.ByName()
portNamesMap := make(map[string]string, len(s.Nics))

ports := make([]string, 0)
for name := range nics {
ports = append(ports, name)
}

lines, err := getLinesFromPortNames(ports, s.OS.Vendor)
if err != nil {
return nil, err
}

for _, p := range ports {
targetPort, err := mapPortName(p, s.OS.Vendor, targetOS, lines)
if err != nil {
return nil, err
}
portNamesMap[p] = targetPort
}

return portNamesMap, nil
}

func mapPortName(port string, sourceOS, targetOS SwitchOSVendor, allLines []int) (string, error) {
line, err := portNameToLine(port, sourceOS)
if err != nil {
return "", fmt.Errorf("unable to get line number from port name, %w", err)
}

switch targetOS {
case SwitchOSVendorSonic:
return sonicPortByLineNumber(line), nil
case SwitchOSVendorCumulus:
return cumulusPortByLineNumber(line, allLines), nil
default:
return "", fmt.Errorf("unknown target switch os %s", targetOS)
}
}

func getLinesFromPortNames(ports []string, os SwitchOSVendor) ([]int, error) {
lines := make([]int, 0)
for _, p := range ports {
l, err := portNameToLine(p, os)
if err != nil {
return nil, fmt.Errorf("unable to get line number from port name, %w", err)
}

lines = append(lines, l)
}

return lines, nil
}

func portNameToLine(port string, os SwitchOSVendor) (int, error) {
switch os {
case SwitchOSVendorSonic:
return sonicPortNameToLine(port)
case SwitchOSVendorCumulus:
return cumulusPortNameToLine(port)
default:
return 0, fmt.Errorf("unknown switch os %s", os)
}

}

func sonicPortNameToLine(port string) (int, error) {
// to prevent accidentally parsing a substring to a negative number
if strings.Contains(port, "-") {
return 0, fmt.Errorf("invalid token '-' in port name %s", port)
}

prefix, lineString, found := strings.Cut(port, "Ethernet")
if !found {
return 0, fmt.Errorf("invalid port name %s, expected to find prefix 'Ethernet'", port)
}

if prefix != "" {
return 0, fmt.Errorf("invalid port name %s, port name is expected to start with 'Ethernet'", port)
}

line, err := strconv.Atoi(lineString)
if err != nil {
return 0, fmt.Errorf("unable to convert port name to line number: %w", err)
}

return line, nil
}

func cumulusPortNameToLine(port string) (int, error) {
// to prevent accidentally parsing a substring to a negative number
if strings.Contains(port, "-") {
return 0, fmt.Errorf("invalid token '-' in port name %s", port)
}

prefix, suffix, found := strings.Cut(port, "swp")
if !found {
return 0, fmt.Errorf("invalid port name %s, expected to find prefix 'swp'", port)
}

if prefix != "" {
return 0, fmt.Errorf("invalid port name %s, port name is expected to start with 'swp'", port)
}

var line int

countString, indexString, found := strings.Cut(suffix, "s")
if !found {
count, err := strconv.Atoi(suffix)
if err != nil {
return 0, fmt.Errorf("unable to convert port name to line number: %w", err)
}
if count <= 0 {
return 0, fmt.Errorf("invalid port name %s would map to negative number", port)
}
line = (count - 1) * 4
} else {
count, err := strconv.Atoi(countString)
if err != nil {
return 0, fmt.Errorf("unable to convert port name to line number: %w", err)
}
if count <= 0 {
return 0, fmt.Errorf("invalid port name %s would map to negative number", port)
}

index, err := strconv.Atoi(indexString)
if err != nil {
return 0, fmt.Errorf("unable to convert port name to line number: %w", err)
}
line = (count-1)*4 + index
}

return line, nil
}

func sonicPortByLineNumber(line int) string {
return fmt.Sprintf("Ethernet%d", line)
}

func cumulusPortByLineNumber(line int, allLines []int) string {
if line%4 > 0 {
return fmt.Sprintf("swp%ds%d", line/4+1, line%4)
}

for _, l := range allLines {
if l == line {
continue
}
if l/4 == line/4 {
return fmt.Sprintf("swp%ds%d", line/4+1, line%4)
}
}

return fmt.Sprintf("swp%d", line/4+1)
}
Loading

0 comments on commit 3d4b264

Please sign in to comment.