Skip to content

Commit

Permalink
allow the API to toggle switch ports (#506)
Browse files Browse the repository at this point in the history
  • Loading branch information
ulrichSchreiner committed May 6, 2024
1 parent 98eb923 commit d81ecca
Show file tree
Hide file tree
Showing 10 changed files with 729 additions and 17 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ vendor
generate
coverage.out
__debug_bin
.mirrord
10 changes: 9 additions & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,12 @@ run:
deadline: 10m
linters:
disable:
- musttag
- musttag
- protogetter
enable:
- testifylint
- unused
presets:
- bugs
- unused
fast: true
4 changes: 0 additions & 4 deletions Dockerfile.dev

This file was deleted.

2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ protoc:
.PHONY: mini-lab-push
mini-lab-push:
make
docker build -f Dockerfile.dev -t metalstack/metal-api:latest .
docker build -f Dockerfile -t metalstack/metal-api:latest .
kind --name metal-control-plane load docker-image metalstack/metal-api:latest
kubectl --kubeconfig=$(MINI_LAB_KUBECONFIG) patch deployments.apps -n metal-control-plane metal-api --patch='{"spec":{"template":{"spec":{"containers":[{"name": "metal-api","imagePullPolicy":"IfNotPresent","image":"metalstack/metal-api:latest"}]}}}}'
kubectl --kubeconfig=$(MINI_LAB_KUBECONFIG) delete pod -n metal-control-plane -l app=metal-api
Expand Down
128 changes: 128 additions & 0 deletions cmd/metal-api/internal/metal/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,37 @@ import (
"fmt"
"net"
"strings"

"github.com/samber/lo"
)

// SwitchPortStatus is a type alias for a string that represents the status of a switch port.
// Valid values are defined as constants in this package.
type SwitchPortStatus string

// SwitchPortStatus defines the possible statuses for a switch port.
// UNKNOWN indicates the status is not known.
// UP indicates the port is up and operational.
// DOWN indicates the port is down and not operational.
const (
SwitchPortStatusUnknown SwitchPortStatus = "UNKNOWN"
SwitchPortStatusUp SwitchPortStatus = "UP"
SwitchPortStatusDown SwitchPortStatus = "DOWN"
)

// IsConcrete returns true if the SwitchPortStatus is UP or DOWN,
// which are concrete, known statuses. It returns false if the status
// is UNKNOWN, which indicates the status is not known.
func (s SwitchPortStatus) IsConcrete() bool {
return s == SwitchPortStatusUp || s == SwitchPortStatusDown
}

// IsValid returns true if the SwitchPortStatus is a known valid value
// (UP, DOWN, UNKNOWN).
func (s SwitchPortStatus) IsValid() bool {
return s == SwitchPortStatusUp || s == SwitchPortStatusDown || s == SwitchPortStatusUnknown
}

// A MacAddress is the type for mac addresses. When using a
// custom type, we cannot use strings directly.
type MacAddress string
Expand All @@ -18,6 +47,105 @@ type Nic struct {
Vrf string `rethinkdb:"vrf" json:"vrf"`
Neighbors Nics `rethinkdb:"neighbors" json:"neighbors"`
Hostname string `rethinkdb:"hostname" json:"hostname"`
State *NicState `rethinkdb:"state" json:"state"`
}

// NicState represents the desired and actual state of a network interface
// controller (NIC). The Desired field indicates the intended state of the
// NIC, while Actual indicates its current operational state. The Desired
// state will be removed when the actual state is equal to the desired state.
type NicState struct {
Desired *SwitchPortStatus `rethinkdb:"desired" json:"desired"`
Actual SwitchPortStatus `rethinkdb:"actual" json:"actual"`
}

// SetState updates the NicState with the given SwitchPortStatus. It returns
// a new NicState and a bool indicating if the state was changed.
//
// If the given status matches the current Actual state, it checks if Desired
// is set and matches too. If so, Desired is set to nil since the desired
// state has been reached.
//
// If the given status differs from the current Actual state, Desired is left
// unchanged if it differes from the new state so the desired state is still tracked.
// The Actual state is updated to the given status.
//
// This allows tracking both the desired and actual states, while clearing
// Desired once the desired state is achieved.
func (ns *NicState) SetState(s SwitchPortStatus) (NicState, bool) {
if ns == nil {
return NicState{
Actual: s,
Desired: nil,
}, true
}
if ns.Actual == s {
if ns.Desired != nil {
if *ns.Desired == s {
// we now have the desired state, so set the desired state to nil
return NicState{
Actual: s,
Desired: nil,
}, true
} else {
// we already have the reported state, but the desired one is different
// so nothing changed
return *ns, false
}
}
// nothing changed
return *ns, false
}
// we got another state as we had before
if ns.Desired != nil {
if *ns.Desired == s {
// we now have the desired state, so set the desired state to nil
return NicState{
Actual: s,
Desired: nil,
}, true
} else {
// a new state was reported, but the desired one is different
// so we have to update the state but keep the desired state
return NicState{
Actual: s,
Desired: ns.Desired,
}, true
}
}
return NicState{
Actual: s,
Desired: nil,
}, true
}

// WantState sets the desired state for the NIC. It returns a new NicState
// struct with the desired state set and a bool indicating if the state changed.
// If the current state already matches the desired state, it returns a state
// with a cleared desired field.
func (ns *NicState) WantState(s SwitchPortStatus) (NicState, bool) {
if ns == nil {
return NicState{
Actual: SwitchPortStatusUnknown,
Desired: &s,
}, true
}
if ns.Actual == s {
// we want a state we already have
if ns.Desired != nil {
return NicState{
Actual: s,
Desired: nil,
}, true
}
return *ns, false
}
// return a new state with the desired state set and a bool indicating a state change
// only if the desired state is different from the current one
return NicState{
Actual: ns.Actual,
Desired: &s,
}, lo.FromPtr(ns.Desired) != s
}

// GetIdentifier returns the identifier of a nic.
Expand Down
216 changes: 216 additions & 0 deletions cmd/metal-api/internal/metal/network_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,219 @@ func TestPrefix_Equals(t *testing.T) {
})
}
}

func TestNicState_WantState(t *testing.T) {
up := SwitchPortStatusUp
down := SwitchPortStatusDown
unknown := SwitchPortStatusUnknown

tests := []struct {
name string
nic *NicState
arg SwitchPortStatus
want NicState
changed bool
}{
{
name: "up to desired down",
nic: &NicState{
Desired: nil,
Actual: down,
},
arg: up,
want: NicState{
Desired: &up,
Actual: down,
},
changed: true,
},
{
name: "up to up with empty desired",
nic: &NicState{
Desired: nil,
Actual: up,
},
arg: up,
want: NicState{
Desired: nil,
Actual: up,
},
changed: false,
},
{
name: "up to up with other desired",
nic: &NicState{
Desired: &down,
Actual: up,
},
arg: up,
want: NicState{
Desired: nil,
Actual: up,
},
changed: true,
},
{
name: "nil to up",
nic: nil,
arg: up,
want: NicState{
Desired: &up,
Actual: unknown,
},
changed: true,
},
{
name: "different actual with same desired",
nic: &NicState{
Desired: &down,
Actual: up,
},
arg: down,
want: NicState{
Desired: &down,
Actual: up,
},
changed: false,
},
{
name: "different actual with other desired",
nic: &NicState{
Desired: &up,
Actual: up,
},
arg: down,
want: NicState{
Desired: &down,
Actual: up,
},
changed: true,
},
{
name: "different actual with empty desired",
nic: &NicState{
Desired: nil,
Actual: up,
},
arg: down,
want: NicState{
Desired: &down,
Actual: up,
},
changed: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

got, got1 := tt.nic.WantState(tt.arg)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("NicState.WantState() got = %+v, want %+v", got, tt.want)
}
if got1 != tt.changed {
t.Errorf("NicState.WantState() got1 = %v, want %v", got1, tt.changed)
}
})
}
}

func TestNicState_SetState(t *testing.T) {
up := SwitchPortStatusUp
down := SwitchPortStatusDown
unknown := SwitchPortStatusUnknown

tests := []struct {
name string
nic *NicState
arg SwitchPortStatus
want NicState
changed bool
}{
{
name: "different actual with empty desired",
nic: &NicState{
Desired: nil,
Actual: up,
},
arg: down,
want: NicState{
Desired: nil,
Actual: down,
},
changed: true,
},
{
name: "different actual with same state in desired",
nic: &NicState{
Desired: &down,
Actual: up,
},
arg: down,
want: NicState{
Desired: nil,
Actual: down,
},
changed: true,
},
{
name: "different actual with other state in desired",
nic: &NicState{
Desired: &unknown,
Actual: up,
},
arg: down,
want: NicState{
Desired: &unknown,
Actual: down,
},
changed: true,
},
{
name: "nil nic",
nic: nil,
arg: down,
want: NicState{
Desired: nil,
Actual: down,
},
changed: true,
},
{
name: "same state with same desired",
nic: &NicState{
Desired: &down,
Actual: down,
},
arg: down,
want: NicState{
Desired: nil,
Actual: down,
},
changed: true,
},
{
name: "same state with other desired",
nic: &NicState{
Desired: &up,
Actual: down,
},
arg: down,
want: NicState{
Desired: &up,
Actual: down,
},
changed: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1 := tt.nic.SetState(tt.arg)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("NicState.SetState() got = %+v, want %+v", got, tt.want)
}
if got1 != tt.changed {
t.Errorf("NicState.SetState() got1 = %v, want %v", got1, tt.changed)
}
})
}
}

0 comments on commit d81ecca

Please sign in to comment.