Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add switch migrate endpoint #566

Open
wants to merge 35 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
00e9018
add switch migrate endpoint and port name mapping
Sep 4, 2024
f65ee72
adjust machine connection during replacement as well
Sep 4, 2024
4293bc3
Merge branch 'master' into switch-migrate
Sep 4, 2024
49cc34f
fix file name
Sep 4, 2024
3ca9d99
add tests for port mapping
Sep 4, 2024
d723145
integration test for switch migrate
Sep 10, 2024
e157760
switch migrate integration test fixed
Sep 10, 2024
c093837
integration test for switch replacement
Sep 10, 2024
c59efbd
Merge branch 'master' into switch-migrate
Sep 10, 2024
9087887
wording
Sep 17, 2024
d35c36e
Merge branch 'master' into switch-migrate
Sep 17, 2024
0d720da
Merge branch 'master' into switch-migrate
Sep 17, 2024
ca0a8c7
naming
Sep 17, 2024
3f0754b
describe rollback strategy in case of errors
Sep 17, 2024
2c28049
fix off by one port mapping
Sep 17, 2024
74a22c4
fix integration test
Sep 17, 2024
c64e1a7
fix switch service test
Sep 17, 2024
11ed256
fix test
Sep 17, 2024
8abf726
check that cumulus port names cannot map to negative line numbers
Sep 17, 2024
d3e5a05
remove swp0 from all tests
Sep 18, 2024
0abbf35
fix integration test
Sep 18, 2024
bbb1e32
fix find twin switch function if more than two switches are present i…
Sep 30, 2024
f1bae9e
disallow switch migrate to a switch that already has machine connections
Sep 30, 2024
4372425
disallow switch migrate when racks differ
Sep 30, 2024
7dbb446
Merge branch 'master' into switch-migrate
Sep 30, 2024
be2912f
Merge branch 'master' into switch-migrate
majst01 Oct 4, 2024
696ce58
Merge master
majst01 Oct 14, 2024
1ecbb23
remove comment
Oct 14, 2024
3d6301e
add enum for switch os vendor
iljarotar Oct 17, 2024
882eacc
typo
iljarotar Oct 17, 2024
1dc69c4
add missing returns
Oct 17, 2024
e00903b
add switch os vendor validation
Oct 17, 2024
30a347d
fix os vendor checks
Oct 17, 2024
5693be6
spec
Oct 17, 2024
be7314a
add os vendor to switch service tests
Oct 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
217 changes: 214 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"
)

iljarotar marked this conversation as resolved.
Show resolved Hide resolved
// Connection between switch port and machine.
type Connection struct {
Nic Nic `rethinkdb:"nic" json:"nic"`
Expand Down Expand Up @@ -144,3 +155,203 @@ 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)
}

if targetOS == SwitchOSVendorCumulus {
return cumulusPortByLineNumber(line, allLines), nil
}
if targetOS == SwitchOSVendorSonic {
return sonicPortByLineNumber(line), nil
}

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) {
if os == SwitchOSVendorSonic {
return sonicPortNameToLine(port)
}
if os == SwitchOSVendorCumulus {
return cumulusPortNameToLine(port)
}
return 0, fmt.Errorf("unknow switch os %s", os)
iljarotar marked this conversation as resolved.
Show resolved Hide resolved
}

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
Loading