diff --git a/.gitignore b/.gitignore index 564f528a0..482cf0eac 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ vendor generate coverage.out __debug_bin +.mirrord diff --git a/.golangci.yaml b/.golangci.yaml index 9449c1ce5..a9dd7d49f 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -3,4 +3,12 @@ run: deadline: 10m linters: disable: - - musttag \ No newline at end of file + - musttag + - protogetter + enable: + - testifylint + - unused + presets: + - bugs + - unused +fast: true diff --git a/Dockerfile.dev b/Dockerfile.dev deleted file mode 100644 index d62669243..000000000 --- a/Dockerfile.dev +++ /dev/null @@ -1,4 +0,0 @@ -FROM alpine:3.19 -RUN apk add ca-certificates -COPY bin/metal-api /metal-api -ENTRYPOINT [ "/metal-api" ] \ No newline at end of file diff --git a/Makefile b/Makefile index fc121de73..b25332452 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/cmd/metal-api/internal/metal/network.go b/cmd/metal-api/internal/metal/network.go index 5349aef35..d1488d501 100644 --- a/cmd/metal-api/internal/metal/network.go +++ b/cmd/metal-api/internal/metal/network.go @@ -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 @@ -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. diff --git a/cmd/metal-api/internal/metal/network_test.go b/cmd/metal-api/internal/metal/network_test.go index fa9c96950..2ef535bef 100644 --- a/cmd/metal-api/internal/metal/network_test.go +++ b/cmd/metal-api/internal/metal/network_test.go @@ -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) + } + }) + } +} diff --git a/cmd/metal-api/internal/service/switch-service.go b/cmd/metal-api/internal/service/switch-service.go index feaff2265..d021d66f0 100644 --- a/cmd/metal-api/internal/service/switch-service.go +++ b/cmd/metal-api/internal/service/switch-service.go @@ -6,6 +6,7 @@ import ( "log/slog" "net/http" "sort" + "strings" "time" "github.com/avast/retry-go/v4" @@ -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"). @@ -244,9 +256,111 @@ 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 + + // old versions of metal-core do not send this field, so make sure we do not crash here + if requestPayload.PortStates != nil { + for i, nic := range newSwitch.Nics { + state, has := requestPayload.PortStates[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; the switch does not know the given NIC. + // 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 { + // compare nic-names case-insensitive + 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 + } + } + 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) @@ -732,6 +846,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) } @@ -746,13 +868,28 @@ func makeSwitchNics(s *metal.Switch, ips metal.IPsMap, machines metal.Machines) func makeSwitchCons(s *metal.Switch) []v1.SwitchConnection { cons := []v1.SwitchConnection{} + nicMap := s.Nics.ByName() + for _, metalConnections := range s.MachineConnections { for _, mc := range metalConnections { + // The connection state is set to the state of the NIC in the database. + // This state is not necessarily the actual state of the port on the switch. + // When the port is toggled, the connection state in the DB is updated after + // the real switch port changed state. + // So if a client queries the current switch state, it will see the desired + // state in the global NIC state, but the actual state of the port in the + // connection map. + n := nicMap[mc.Nic.Name] + state := metal.SwitchPortStatusUnknown + if n != nil && n.State != nil { + state = n.State.Actual + } nic := v1.SwitchNic{ MacAddress: string(mc.Nic.MacAddress), Name: mc.Nic.Name, Identifier: mc.Nic.Identifier, Vrf: mc.Nic.Vrf, + Actual: v1.SwitchPortStatus(state), } con := v1.SwitchConnection{ Nic: nic, diff --git a/cmd/metal-api/internal/service/switch-service_test.go b/cmd/metal-api/internal/service/switch-service_test.go index 49c1d5cd2..32325beff 100644 --- a/cmd/metal-api/internal/service/switch-service_test.go +++ b/cmd/metal-api/internal/service/switch-service_test.go @@ -3,6 +3,7 @@ package service import ( "bytes" "encoding/json" + "fmt" "log/slog" "net/http" "net/http/httptest" @@ -19,6 +20,7 @@ import ( "github.com/metal-stack/metal-api/cmd/metal-api/internal/metal" v1 "github.com/metal-stack/metal-api/cmd/metal-api/internal/service/v1" "github.com/metal-stack/metal-api/cmd/metal-api/internal/testdata" + "github.com/metal-stack/metal-lib/httperrors" ) func TestRegisterSwitch(t *testing.T) { @@ -521,6 +523,9 @@ func TestMakeSwitchNics(t *testing.T) { metal.Nic{ Name: "swp1", Vrf: "vrf1", + State: &metal.NicState{ + Actual: metal.SwitchPortStatusUp, + }, }, metal.Nic{ Name: "swp2", @@ -563,6 +568,7 @@ func TestMakeSwitchNics(t *testing.T) { CIDRs: []string{"212.89.1.1/32"}, VNIs: []string{}, }, + Actual: v1.SwitchPortStatusUp, }, v1.SwitchNic{ Name: "swp2", @@ -571,6 +577,7 @@ func TestMakeSwitchNics(t *testing.T) { CIDRs: []string{}, VNIs: []string{"1", "2"}, }, + Actual: v1.SwitchPortStatusUnknown, }, }, }, @@ -1274,3 +1281,108 @@ func TestNotifyErrorSwitch(t *testing.T) { require.Equal(t, d, result.LastSyncError.Duration) require.Equal(t, e, *result.LastSyncError.Error) } + +func TestToggleSwitchWrongNic(t *testing.T) { + ds, mock := datastore.InitMockDB(t) + testdata.InitMockDBData(mock) + log := slog.Default() + + switchservice := NewSwitch(log, ds) + container := restful.NewContainer().Add(switchservice) + + updateRequest := v1.SwitchPortToggleRequest{ + NicName: "wrongname", + Status: v1.SwitchPortStatusDown, + } + js, err := json.Marshal(updateRequest) + require.NoError(t, err) + body := bytes.NewBuffer(js) + req := httptest.NewRequest("POST", "/v1/switch/"+testdata.Switch1.ID+"/port", body) + container = injectAdmin(log, container, req) + req.Header.Add("Content-Type", "application/json") + w := httptest.NewRecorder() + container.ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + require.Equal(t, http.StatusNotFound, resp.StatusCode, w.Body.String()) + var result httperrors.HTTPErrorResponse + err = json.NewDecoder(resp.Body).Decode(&result) + + require.NoError(t, err) + require.Equal(t, "the nic \"wrongname\" does not exist in this switch", result.Message) +} + +func TestToggleSwitchWrongState(t *testing.T) { + ds, mock := datastore.InitMockDB(t) + testdata.InitMockDBData(mock) + log := slog.Default() + + switchservice := NewSwitch(log, ds) + container := restful.NewContainer().Add(switchservice) + + states := []v1.SwitchPortStatus{ + v1.SwitchPortStatusUnknown, + v1.SwitchPortStatus("illegal"), + } + + for _, s := range states { + + updateRequest := v1.SwitchPortToggleRequest{ + NicName: testdata.Switch1.Nics[0].Name, + Status: s, + } + js, err := json.Marshal(updateRequest) + require.NoError(t, err) + body := bytes.NewBuffer(js) + req := httptest.NewRequest("POST", "/v1/switch/"+testdata.Switch1.ID+"/port", body) + container = injectAdmin(log, container, req) + req.Header.Add("Content-Type", "application/json") + w := httptest.NewRecorder() + container.ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + require.Equal(t, http.StatusBadRequest, resp.StatusCode, w.Body.String()) + var result httperrors.HTTPErrorResponse + err = json.NewDecoder(resp.Body).Decode(&result) + + require.NoError(t, err) + require.Equal(t, result.Message, fmt.Sprintf("the status %q must be concrete", s)) + } +} + +func TestToggleSwitch(t *testing.T) { + ds, mock := datastore.InitMockDB(t) + testdata.InitMockDBData(mock) + log := slog.Default() + + switchservice := NewSwitch(log, ds) + container := restful.NewContainer().Add(switchservice) + + updateRequest := v1.SwitchPortToggleRequest{ + NicName: testdata.Switch1.Nics[0].Name, + Status: v1.SwitchPortStatusDown, + } + + js, err := json.Marshal(updateRequest) + require.NoError(t, err) + body := bytes.NewBuffer(js) + req := httptest.NewRequest("POST", "/v1/switch/"+testdata.Switch1.ID+"/port", body) + container = injectAdmin(log, container, req) + req.Header.Add("Content-Type", "application/json") + w := httptest.NewRecorder() + container.ServeHTTP(w, req) + + resp := w.Result() + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode, w.Body.String()) + var result v1.SwitchResponse + err = json.NewDecoder(resp.Body).Decode(&result) + + require.NoError(t, err) + require.Equal(t, testdata.Switch1.ID, result.ID) + require.Equal(t, testdata.Switch1.Name, *result.Name) + require.Equal(t, v1.SwitchPortStatusDown, result.Nics[0].Actual) + require.Equal(t, v1.SwitchPortStatusUnknown, result.Connections[0].Nic.Actual) +} diff --git a/cmd/metal-api/internal/service/v1/switch.go b/cmd/metal-api/internal/service/v1/switch.go index 7fad81462..99eaf221d 100644 --- a/cmd/metal-api/internal/service/v1/switch.go +++ b/cmd/metal-api/internal/service/v1/switch.go @@ -8,6 +8,20 @@ import ( "github.com/metal-stack/metal-api/cmd/metal-api/internal/metal" ) +// 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" +) + type SwitchBase struct { RackID string `json:"rack_id" modelDescription:"A switch that can register at the api." description:"the id of the rack in which this switch is located"` Mode string `json:"mode" description:"the mode the switch currently has" optional:"true"` @@ -26,11 +40,12 @@ type SwitchOS struct { type SwitchNics []SwitchNic type SwitchNic struct { - MacAddress string `json:"mac" description:"the mac address of this network interface"` - Name string `json:"name" description:"the name of this network interface"` - Identifier string `json:"identifier" description:"the identifier of this network interface"` - Vrf string `json:"vrf" description:"the vrf this network interface is part of" optional:"true"` - BGPFilter *BGPFilter `json:"filter" description:"configures the bgp filter applied at the switch port" optional:"true"` + MacAddress string `json:"mac" description:"the mac address of this network interface"` + Name string `json:"name" description:"the name of this network interface"` + Identifier string `json:"identifier" description:"the identifier of this network interface"` + Vrf string `json:"vrf" description:"the vrf this network interface is part of" optional:"true"` + BGPFilter *BGPFilter `json:"filter" description:"configures the bgp filter applied at the switch port" optional:"true"` + Actual SwitchPortStatus `json:"actual" description:"the current state of the nic" enum:"UP|DOWN|UNKNOWN"` } type BGPFilter struct { @@ -70,9 +85,18 @@ type SwitchUpdateRequest struct { SwitchBase } +type SwitchPortToggleRequest struct { + NicName string `json:"nic" description:"the nic of the switch you want to change"` + Status SwitchPortStatus `json:"status" description:"sets the port status" enum:"UP|DOWN"` +} + +// SwitchNotifyRequest represents the notification sent from the switch +// to the metal-api after a sync operation. It contains the duration of +// the sync, any error that occurred, and the updated switch port states. type SwitchNotifyRequest struct { - Duration time.Duration `json:"sync_duration" description:"the duration of the switch synchronization"` - Error *string `json:"error"` + Duration time.Duration `json:"sync_duration" description:"the duration of the switch synchronization"` + Error *string `json:"error"` + PortStates map[string]SwitchPortStatus `json:"port_states" description:"the current switch port states"` } type SwitchNotifyResponse struct { @@ -84,9 +108,9 @@ type SwitchNotifyResponse struct { type SwitchResponse struct { Common SwitchBase - Nics SwitchNics `json:"nics" description:"the list of network interfaces on the switch"` + Nics SwitchNics `json:"nics" description:"the list of network interfaces on the switch with the desired nic states"` Partition PartitionResponse `json:"partition" description:"the partition in which this switch is located"` - Connections []SwitchConnection `json:"connections" description:"a connection between a switch port and a machine"` + Connections []SwitchConnection `json:"connections" description:"a connection between a switch port and a machine with the real nic states"` LastSync *SwitchSync `json:"last_sync" description:"last successful synchronization to the switch" optional:"true"` LastSyncError *SwitchSync `json:"last_sync_error" description:"last synchronization to the switch that was erroneous" optional:"true"` Timestamps diff --git a/spec/metal-api.json b/spec/metal-api.json index b29faa11e..cde2ba203 100644 --- a/spec/metal-api.json +++ b/spec/metal-api.json @@ -4867,6 +4867,15 @@ }, "v1.SwitchNic": { "properties": { + "actual": { + "description": "the current state of the nic", + "enum": [ + "DOWN", + "UNKNOWN", + "UP" + ], + "type": "string" + }, "filter": { "$ref": "#/definitions/v1.BGPFilter", "description": "configures the bgp filter applied at the switch port" @@ -4889,6 +4898,7 @@ } }, "required": [ + "actual", "identifier", "mac", "name" @@ -4899,6 +4909,13 @@ "error": { "type": "string" }, + "port_states": { + "additionalProperties": { + "type": "string" + }, + "description": "the current switch port states", + "type": "object" + }, "sync_duration": { "description": "the duration of the switch synchronization", "format": "int64", @@ -4907,6 +4924,7 @@ }, "required": [ "error", + "port_states", "sync_duration" ] }, @@ -4953,6 +4971,26 @@ } } }, + "v1.SwitchPortToggleRequest": { + "properties": { + "nic": { + "description": "the nic of the switch you want to change", + "type": "string" + }, + "status": { + "description": "sets the port status", + "enum": [ + "DOWN", + "UP" + ], + "type": "string" + } + }, + "required": [ + "nic", + "status" + ] + }, "v1.SwitchRegisterRequest": { "properties": { "console_command": { @@ -5019,7 +5057,7 @@ "type": "string" }, "connections": { - "description": "a connection between a switch port and a machine", + "description": "a connection between a switch port and a machine with the real nic states", "items": { "$ref": "#/definitions/v1.SwitchConnection" }, @@ -5068,7 +5106,7 @@ "type": "string" }, "nics": { - "description": "the list of network interfaces on the switch", + "description": "the list of network interfaces on the switch with the desired nic states", "items": { "$ref": "#/definitions/v1.SwitchNic" }, @@ -9443,6 +9481,58 @@ ] } }, + "/v1/switch/{id}/port": { + "post": { + "consumes": [ + "application/json" + ], + "operationId": "toggleSwitchPort", + "parameters": [ + { + "description": "identifier of the switch", + "in": "path", + "name": "id", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.SwitchPortToggleRequest" + } + } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.SwitchResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/httperrors.HTTPErrorResponse" + } + }, + "default": { + "description": "Error", + "schema": { + "$ref": "#/definitions/httperrors.HTTPErrorResponse" + } + } + }, + "summary": "toggles the port of the switch with a nicname to the given state", + "tags": [ + "switch" + ] + } + }, "/v1/tenant": { "get": { "consumes": [