Skip to content

Commit

Permalink
allow the API to toggle switch ports
Browse files Browse the repository at this point in the history
  • Loading branch information
ulrichSchreiner committed Feb 26, 2024
1 parent 9858b21 commit 7c8d310
Show file tree
Hide file tree
Showing 4 changed files with 370 additions and 7 deletions.
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 adresses. 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
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
}

// GetIdentifier returns the identifier of a nic.
Expand Down
104 changes: 104 additions & 0 deletions cmd/metal-api/internal/service/switch-service.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"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").
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{}).
Returns(http.StatusNotModified, "Not modified", nil).
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,100 @@ 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.Errorw("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))
}

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() || !desired.IsValid() {
r.sendError(request, response, httperrors.BadRequest(fmt.Errorf("the status %q must be valid and 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
nicname := strings.ToLower(requestPayload.NicName)
updated := false
found := false
for i, nic := range newSwitch.Nics {
if nic.Name == nicname {
found = true
newstate, changed := nic.State.WantState(desired)
if changed {
newSwitch.Nics[i].State = &newstate
updated = true
break
}
}
}
if !found {
r.sendError(request, response, httperrors.NotFound(fmt.Errorf("the nic %q does not exist in this switch", requestPayload.NicName)))
return
}
if !updated {
r.send(request, response, http.StatusNotModified, nil)
return
}

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 +835,7 @@ func makeSwitchNics(s *metal.Switch, ips metal.IPsMap, machines metal.Machines)
Identifier: n.Identifier,
Vrf: n.Vrf,
BGPFilter: filter,
Actual: v1.SwitchPortStatus(pointer.SafeDerefOrDefault(n.State, metal.NicState{Actual: metal.SwitchPortStatusUnknown}).Actual),
}
nics = append(nics, nic)
}
Expand Down
Loading

0 comments on commit 7c8d310

Please sign in to comment.