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

allow the API to toggle switch ports #506

Merged
merged 26 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
dd6d7f5
allow the API to toggle switch ports
ulrichSchreiner Feb 26, 2024
bae5b4b
add path parameter
ulrichSchreiner Feb 29, 2024
b559143
repair test to make linter happy
ulrichSchreiner Feb 29, 2024
ef31bab
see linter errors on local machine before pushing to gh
ulrichSchreiner Feb 29, 2024
de9aef0
Merge branch 'master' into toggle-switch-port
majst01 Mar 4, 2024
aa588f0
not needed any more
ulrichSchreiner Mar 18, 2024
20f329d
send desired state to switch if it is set
ulrichSchreiner Mar 18, 2024
e3e8d5d
add enum types for port state
ulrichSchreiner Mar 19, 2024
ea27662
merge master
ulrichSchreiner Mar 19, 2024
2d0093a
set the enum values in the endpoint
ulrichSchreiner Mar 19, 2024
2bee462
Merge branch 'master' into toggle-switch-port
ulrichSchreiner Apr 2, 2024
0a629c9
merge master
ulrichSchreiner Apr 2, 2024
c8c8c5c
merge master
ulrichSchreiner Apr 4, 2024
d5e6e9f
comment code
ulrichSchreiner Apr 4, 2024
b118ddc
fix typo
ulrichSchreiner Apr 4, 2024
5276d55
fix typo
ulrichSchreiner Apr 4, 2024
d3d32ed
add check for bugs and unused
ulrichSchreiner Apr 8, 2024
cb1fac4
set connection nic state to the REAL state not the desired state
ulrichSchreiner Apr 17, 2024
e2f3552
nic state could be nil, so check state too
ulrichSchreiner Apr 22, 2024
5b62d99
make sure we do not crash with old versions of metal-core
ulrichSchreiner Apr 22, 2024
3a3c6d6
resolve review conversation
ulrichSchreiner Apr 22, 2024
4dfae64
FIX: wrong comment
ulrichSchreiner Apr 22, 2024
2345b8b
document the different nic-states in the different fields of the resp…
ulrichSchreiner Apr 22, 2024
2af9749
resolve review comments
ulrichSchreiner Apr 23, 2024
8cd2f0d
add tests for state methods
ulrichSchreiner Apr 23, 2024
109a56a
the NIC names of sonic are case sensitive
ulrichSchreiner Apr 25, 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
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
6 changes: 5 additions & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@ run:
deadline: 10m
linters:
disable:
- musttag
- musttag
enable:
- testifylint
- unused
ulrichSchreiner marked this conversation as resolved.
Show resolved Hide resolved
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
124 changes: 124 additions & 0 deletions cmd/metal-api/internal/metal/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,33 @@ import (
"strings"
)

// 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 +45,103 @@ 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 actuale 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 {
// we already have the reported state, but the desired one is different
// so nothing changed
ulrichSchreiner marked this conversation as resolved.
Show resolved Hide resolved
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 NicState{
Actual: ns.Actual,
Desired: &s,
}, true
ulrichSchreiner marked this conversation as resolved.
Show resolved Hide resolved
}

// GetIdentifier returns the identifier of a nic.
Expand Down
118 changes: 118 additions & 0 deletions cmd/metal-api/internal/service/switch-service.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"log/slog"
"net/http"
"sort"
"strings"
"time"

"github.com/avast/retry-go/v4"
Expand Down Expand Up @@ -104,6 +105,17 @@ func (r *switchResource) webService() *restful.WebService {
Returns(http.StatusConflict, "Conflict", httperrors.HTTPErrorResponse{}).
DefaultReturns("Error", httperrors.HTTPErrorResponse{}))

ws.Route(ws.POST("/{id}/port").
To(admin(r.toggleSwitchPort)).
Operation("toggleSwitchPort").
Param(ws.PathParameter("id", "identifier of the switch").DataType("string")).
Doc("toggles the port of the switch with a nicname to the given state").
Metadata(restfulspec.KeyOpenAPITags, tags).
Reads(v1.SwitchPortToggleRequest{}).
Returns(http.StatusOK, "OK", v1.SwitchResponse{}).
Returns(http.StatusConflict, "Conflict", httperrors.HTTPErrorResponse{}).
DefaultReturns("Error", httperrors.HTTPErrorResponse{}))

ws.Route(ws.POST("/{id}/notify").
To(editor(r.notifySwitch)).
Doc("notify the metal-api about a configuration change of a switch").
Expand Down Expand Up @@ -244,9 +256,107 @@ func (r *switchResource) notifySwitch(request *restful.Request, response *restfu
return
}

oldSwitch, err := r.ds.FindSwitch(id)
if err != nil {
r.sendError(request, response, defaultError(err))
return
}

newSwitch := *oldSwitch
switchUpdated := false
for i, nic := range newSwitch.Nics {
state, has := requestPayload.PortStates[strings.ToLower(nic.Name)]
if has {
reported := metal.SwitchPortStatus(state)
newstate, changed := nic.State.SetState(reported)
if changed {
newSwitch.Nics[i].State = &newstate
switchUpdated = true
}
} else {
// this should NEVER happen; if the switch reports the state of an unknown port
// we log this and ignore it, but something is REALLY wrong in this case
r.log.Error("unknown switch port", "id", id, "nic", nic.Name)
}
}

if switchUpdated {
if err := r.ds.UpdateSwitch(oldSwitch, &newSwitch); err != nil {
r.sendError(request, response, defaultError(err))
return
}
}

r.send(request, response, http.StatusOK, v1.NewSwitchNotifyResponse(&newSS))
}

// toggleSwitchPort handles a request to toggle the state of a port on a switch. It reads the request body, validates the requested status is concrete, finds the switch, updates its NIC state if needed, and returns the updated switch on success.
// toggleSwitchPort handles a request to toggle the state of a port on a switch. It reads the request body to get the switch ID, NIC name and desired state. It finds the switch, updates the state of the matching NIC if needed, and returns the updated switch on success.
func (r *switchResource) toggleSwitchPort(request *restful.Request, response *restful.Response) {
var requestPayload v1.SwitchPortToggleRequest
err := request.ReadEntity(&requestPayload)
if err != nil {
r.sendError(request, response, httperrors.BadRequest(err))
return
}

desired := metal.SwitchPortStatus(requestPayload.Status)

if !desired.IsConcrete() {
r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("the status %q must be concrete", requestPayload.Status)))
return
}

id := request.PathParameter("id")
oldSwitch, err := r.ds.FindSwitch(id)
if err != nil {
r.sendError(request, response, defaultError(err))
return
}

newSwitch := *oldSwitch
updated := false
found := false

// Updates the state of a NIC on the switch if the requested state change is valid
//
// Loops through each NIC on the switch and checks if the name matches the
// requested NIC. If a match is found, it tries to update the NIC's state to
// the requested state. If the state change is valid, it sets the new state on
// the NIC and marks that an update was made.
for i, nic := range newSwitch.Nics {
// compre nic-names case-insensitive
ulrichSchreiner marked this conversation as resolved.
Show resolved Hide resolved
if strings.EqualFold(nic.Name, requestPayload.NicName) {
found = true
newstate, changed := nic.State.WantState(desired)
if changed {
newSwitch.Nics[i].State = &newstate
updated = true
break
ulrichSchreiner marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
if !found {
r.sendError(request, response, httperrors.NotFound(fmt.Errorf("the nic %q does not exist in this switch", requestPayload.NicName)))
return
}

if updated {
if err := r.ds.UpdateSwitch(oldSwitch, &newSwitch); err != nil {
r.sendError(request, response, defaultError(err))
return
}
}

resp, err := makeSwitchResponse(&newSwitch, r.ds)
if err != nil {
r.sendError(request, response, defaultError(err))
return
}

r.send(request, response, http.StatusOK, resp)
}

func (r *switchResource) updateSwitch(request *restful.Request, response *restful.Response) {
var requestPayload v1.SwitchUpdateRequest
err := request.ReadEntity(&requestPayload)
Expand Down Expand Up @@ -732,6 +842,14 @@ func makeSwitchNics(s *metal.Switch, ips metal.IPsMap, machines metal.Machines)
Identifier: n.Identifier,
Vrf: n.Vrf,
BGPFilter: filter,
Actual: v1.SwitchPortStatusUnknown,
}
if n.State != nil {
if n.State.Desired != nil {
nic.Actual = v1.SwitchPortStatus(*n.State.Desired)
} else {
nic.Actual = v1.SwitchPortStatus(n.State.Actual)
}
}
nics = append(nics, nic)
}
Expand Down
Loading