diff --git a/docs/antctl.md b/docs/antctl.md index 93eb29d5741..82f4722e33b 100644 --- a/docs/antctl.md +++ b/docs/antctl.md @@ -769,6 +769,18 @@ NAME ROUTER-ID LOCAL-ASN LISTEN-PORT example-bgp-policy 172.18.0.2 64512 179 ``` +`antctl` agent command `get bgppeers` print the current status of all BGP peers +of effective BGP policy applied on the local Node. It includes Peer IP address with port, +ASN, and State of the BGP Peers. + +```bash +$ antctl get bgppeers + +PEER ASN STATE +192.168.77.200:179 65001 Established +192.168.77.201:179 65002 Active +``` + ### Upgrade existing objects of CRDs antctl supports upgrading existing objects of Antrea CRDs to the storage version. diff --git a/pkg/agent/apis/types.go b/pkg/agent/apis/types.go index e20abe996a5..3aebbe0f663 100644 --- a/pkg/agent/apis/types.go +++ b/pkg/agent/apis/types.go @@ -210,3 +210,22 @@ func (r BGPPolicyResponse) GetTableRow(_ int) []string { func (r BGPPolicyResponse) SortRows() bool { return true } + +// BGPPeerResponse describes the response struct of bgppeers command. +type BGPPeerResponse struct { + Peer string `json:"peer,omitempty"` + ASN int32 `json:"asn,omitempty"` + State string `json:"state,omitempty"` +} + +func (r BGPPeerResponse) GetTableHeader() []string { + return []string{"PEER", "ASN", "STATE"} +} + +func (r BGPPeerResponse) GetTableRow(_ int) []string { + return []string{r.Peer, strconv.Itoa(int(r.ASN)), r.State} +} + +func (r BGPPeerResponse) SortRows() bool { + return true +} diff --git a/pkg/agent/apiserver/apiserver.go b/pkg/agent/apiserver/apiserver.go index 819f5d99daa..71c78cf6ee1 100644 --- a/pkg/agent/apiserver/apiserver.go +++ b/pkg/agent/apiserver/apiserver.go @@ -36,6 +36,7 @@ import ( "antrea.io/antrea/pkg/agent/apiserver/handlers/addressgroup" "antrea.io/antrea/pkg/agent/apiserver/handlers/agentinfo" "antrea.io/antrea/pkg/agent/apiserver/handlers/appliedtogroup" + "antrea.io/antrea/pkg/agent/apiserver/handlers/bgppeer" "antrea.io/antrea/pkg/agent/apiserver/handlers/bgppolicy" "antrea.io/antrea/pkg/agent/apiserver/handlers/featuregates" "antrea.io/antrea/pkg/agent/apiserver/handlers/memberlist" @@ -100,6 +101,7 @@ func installHandlers(aq agentquerier.AgentQuerier, npq querier.AgentNetworkPolic s.Handler.NonGoRestfulMux.HandleFunc("/serviceexternalip", serviceexternalip.HandleFunc(seipq)) s.Handler.NonGoRestfulMux.HandleFunc("/memberlist", memberlist.HandleFunc(aq)) s.Handler.NonGoRestfulMux.HandleFunc("/bgppolicy", bgppolicy.HandleFunc(bgpq)) + s.Handler.NonGoRestfulMux.HandleFunc("/bgppeers", bgppeer.HandleFunc(bgpq)) } func installAPIGroup(s *genericapiserver.GenericAPIServer, aq agentquerier.AgentQuerier, npq querier.AgentNetworkPolicyInfoQuerier, v4Enabled, v6Enabled bool) error { diff --git a/pkg/agent/apiserver/handlers/bgppeer/handler.go b/pkg/agent/apiserver/handlers/bgppeer/handler.go new file mode 100644 index 00000000000..521ea206965 --- /dev/null +++ b/pkg/agent/apiserver/handlers/bgppeer/handler.go @@ -0,0 +1,66 @@ +// Copyright 2024 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bgppeer + +import ( + "encoding/json" + "errors" + "net" + "net/http" + "reflect" + "strconv" + + "k8s.io/klog/v2" + + "antrea.io/antrea/pkg/agent/apis" + "antrea.io/antrea/pkg/agent/controller/bgp" + "antrea.io/antrea/pkg/querier" +) + +// HandleFunc returns the function which can handle queries issued by the bgppeers command. +func HandleFunc(bq querier.AgentBGPPolicyInfoQuerier) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if bq == nil || reflect.ValueOf(bq).IsNil() { + // The error message must match the "FOO is not enabled" pattern to pass antctl e2e tests. + http.Error(w, "bgp is not enabled", http.StatusServiceUnavailable) + return + } + + peers, err := bq.GetBGPPeerStatus(r.Context()) + if err != nil { + if errors.Is(err, bgp.ErrBGPPolicyNotFound) { + http.Error(w, "there is no effective bgp policy applied to the Node", http.StatusNotFound) + return + } else { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + + var bgpPeersResp []apis.BGPPeerResponse + for _, peer := range peers { + bgpPeersResp = append(bgpPeersResp, apis.BGPPeerResponse{ + Peer: net.JoinHostPort(peer.Address, strconv.Itoa(int(peer.Port))), + ASN: peer.ASN, + State: string(peer.SessionState), + }) + } + + if err := json.NewEncoder(w).Encode(bgpPeersResp); err != nil { + w.WriteHeader(http.StatusInternalServerError) + klog.ErrorS(err, "Error when encoding BGPPeersResp to json") + } + } +} diff --git a/pkg/agent/apiserver/handlers/bgppeer/handler_test.go b/pkg/agent/apiserver/handlers/bgppeer/handler_test.go new file mode 100644 index 00000000000..d9c9ca4153e --- /dev/null +++ b/pkg/agent/apiserver/handlers/bgppeer/handler_test.go @@ -0,0 +1,102 @@ +// Copyright 2024 Antrea Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bgppeer + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "antrea.io/antrea/pkg/agent/apis" + "antrea.io/antrea/pkg/agent/bgp" + bgpcontroller "antrea.io/antrea/pkg/agent/controller/bgp" + queriertest "antrea.io/antrea/pkg/querier/testing" +) + +func TestBGPPeerQuery(t *testing.T) { + tests := []struct { + name string + fakeBGPPeerStatus []bgp.PeerStatus + expectedStatus int + expectedResponse []apis.BGPPeerResponse + fakeErr error + }{ + { + name: "bgpPolicyState exists", + fakeBGPPeerStatus: []bgp.PeerStatus{ + { + Address: "192.168.77.200", + Port: 179, + ASN: 65001, + SessionState: bgp.SessionEstablished, + }, + { + Address: "192.168.77.201", + Port: 179, + ASN: 65002, + SessionState: bgp.SessionActive, + }, + }, + expectedStatus: http.StatusOK, + expectedResponse: []apis.BGPPeerResponse{ + { + Peer: "192.168.77.200:179", + ASN: 65001, + State: "Established", + }, + { + Peer: "192.168.77.201:179", + ASN: 65002, + State: "Active", + }, + }, + }, + { + name: "bgpPolicyState does not exist", + fakeBGPPeerStatus: nil, + expectedStatus: http.StatusNotFound, + fakeErr: bgpcontroller.ErrBGPPolicyNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + q := queriertest.NewMockAgentBGPPolicyInfoQuerier(ctrl) + q.EXPECT().GetBGPPeerStatus(context.Background()).Return(tt.fakeBGPPeerStatus, tt.fakeErr) + handler := HandleFunc(q) + + req, err := http.NewRequest(http.MethodGet, "", nil) + require.NoError(t, err) + + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, req) + assert.Equal(t, tt.expectedStatus, recorder.Code) + + if tt.expectedStatus == http.StatusOK { + var received []apis.BGPPeerResponse + err = json.Unmarshal(recorder.Body.Bytes(), &received) + require.NoError(t, err) + assert.ElementsMatch(t, tt.expectedResponse, received) + } + }) + } +} diff --git a/pkg/agent/controller/bgp/controller.go b/pkg/agent/controller/bgp/controller.go index 8f1e3d4b6d3..888cb4a77ba 100644 --- a/pkg/agent/controller/bgp/controller.go +++ b/pkg/agent/controller/bgp/controller.go @@ -17,6 +17,7 @@ package bgp import ( "context" "encoding/json" + "errors" "fmt" "hash/fnv" "net" @@ -73,6 +74,10 @@ const ( const dummyKey = "dummyKey" +var ( + ErrBGPPolicyNotFound = errors.New("BGPPolicy not found") +) + type bgpPolicyState struct { // The local BGP server. bgpServer bgp.Interface @@ -964,3 +969,25 @@ func (c *Controller) GetBGPPolicyInfo() (string, string, int32, int32) { } return name, routerID, localASN, listenPort } + +// GetBGPPeerStatus returns current status of all BGP Peers of effective BGP Policy applied on the Node. +func (c *Controller) GetBGPPeerStatus(ctx context.Context) ([]bgp.PeerStatus, error) { + getBgpServer := func() bgp.Interface { + c.bgpPolicyStateMutex.RLock() + defer c.bgpPolicyStateMutex.RUnlock() + if c.bgpPolicyState == nil { + return nil + } + return c.bgpPolicyState.bgpServer + } + + bgpServer := getBgpServer() + if bgpServer == nil { + return nil, ErrBGPPolicyNotFound + } + peers, err := bgpServer.GetPeers(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get bgp peers: %w", err) + } + return peers, nil +} diff --git a/pkg/agent/controller/bgp/controller_test.go b/pkg/agent/controller/bgp/controller_test.go index 3a9b6026f58..60a9eb91d37 100644 --- a/pkg/agent/controller/bgp/controller_test.go +++ b/pkg/agent/controller/bgp/controller_test.go @@ -2161,3 +2161,72 @@ func TestGetBGPPolicyInfo(t *testing.T) { }) } } + +func TestGetBGPPeerStatus(t *testing.T) { + ctx := context.Background() + testCases := []struct { + name string + existingState *bgpPolicyState + expectedCalls func(mockBGPServer *bgptest.MockInterfaceMockRecorder) + expectedBgpPeerStatus []bgp.PeerStatus + expectedErr error + }{ + { + name: "bgpPolicyState exists", + existingState: &bgpPolicyState{}, + expectedCalls: func(mockBGPServer *bgptest.MockInterfaceMockRecorder) { + mockBGPServer.GetPeers(ctx).Return([]bgp.PeerStatus{ + { + Address: "192.168.77.200", + ASN: 65001, + SessionState: bgp.SessionEstablished, + }, + { + Address: "192.168.77.201", + ASN: 65002, + SessionState: bgp.SessionActive, + }, + }, nil) + }, + expectedBgpPeerStatus: []bgp.PeerStatus{ + { + Address: "192.168.77.200", + ASN: 65001, + SessionState: bgp.SessionEstablished, + }, + { + Address: "192.168.77.201", + ASN: 65002, + SessionState: bgp.SessionActive, + }, + }, + }, + { + name: "bgpPolicyState does not exist", + expectedErr: ErrBGPPolicyNotFound, + }, + } + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + c := newFakeController(t, nil, nil, true, false) + + // Fake the BGPPolicy state. + c.bgpPolicyState = tt.existingState + if c.bgpPolicyState != nil { + c.bgpPolicyState.bgpServer = c.mockBGPServer + } + + if tt.expectedCalls != nil { + tt.expectedCalls(c.mockBGPServer.EXPECT()) + } + + actualBgpPeerStatus, err := c.GetBGPPeerStatus(ctx) + if tt.expectedErr != nil { + assert.ErrorIs(t, err, tt.expectedErr) + } else { + require.NoError(t, err) + assert.ElementsMatch(t, actualBgpPeerStatus, tt.expectedBgpPeerStatus) + } + }) + } +} diff --git a/pkg/antctl/antctl.go b/pkg/antctl/antctl.go index 65a412893b7..33aee26b692 100644 --- a/pkg/antctl/antctl.go +++ b/pkg/antctl/antctl.go @@ -652,6 +652,23 @@ $ antctl get podmulticaststats pod -n namespace`, commandGroup: get, transformedResponse: reflect.TypeOf(agentapis.BGPPolicyResponse{}), }, + { + use: "bgppeers", + aliases: []string{"bgppeer"}, + short: "Print the current status of all bgp peers of effective bgppolicy", + long: "Print the current status of all bgp peers of effective bgppolicy which includes peer IP address with port, asn and state", + example: ` Get the list of bgppeers with their current status + $ antctl get bgppeers +`, + agentEndpoint: &endpoint{ + nonResourceEndpoint: &nonResourceEndpoint{ + path: "/bgppeers", + outputType: multiple, + }, + }, + commandGroup: get, + transformedResponse: reflect.TypeOf(agentapis.BGPPeerResponse{}), + }, }, rawCommands: []rawCommand{ { diff --git a/pkg/antctl/command_list_test.go b/pkg/antctl/command_list_test.go index 42f88e658f7..4e875055ec2 100644 --- a/pkg/antctl/command_list_test.go +++ b/pkg/antctl/command_list_test.go @@ -70,7 +70,7 @@ func TestGetDebugCommands(t *testing.T) { { name: "Antctl running against agent mode", mode: "agent", - expected: [][]string{{"version"}, {"get", "podmulticaststats"}, {"log-level"}, {"get", "networkpolicy"}, {"get", "appliedtogroup"}, {"get", "addressgroup"}, {"get", "agentinfo"}, {"get", "podinterface"}, {"get", "ovsflows"}, {"trace-packet"}, {"get", "serviceexternalip"}, {"get", "memberlist"}, {"get", "bgppolicy"}, {"supportbundle"}, {"traceflow"}, {"get", "featuregates"}}, + expected: [][]string{{"version"}, {"get", "podmulticaststats"}, {"log-level"}, {"get", "networkpolicy"}, {"get", "appliedtogroup"}, {"get", "addressgroup"}, {"get", "agentinfo"}, {"get", "podinterface"}, {"get", "ovsflows"}, {"trace-packet"}, {"get", "serviceexternalip"}, {"get", "memberlist"}, {"get", "bgppolicy"}, {"get", "bgppeers"}, {"supportbundle"}, {"traceflow"}, {"get", "featuregates"}}, }, { name: "Antctl running against flow-aggregator mode", diff --git a/pkg/querier/querier.go b/pkg/querier/querier.go index c8ea3725431..f64fdc924fb 100644 --- a/pkg/querier/querier.go +++ b/pkg/querier/querier.go @@ -15,11 +15,14 @@ package querier import ( + "context" + v1 "k8s.io/api/core/v1" apitypes "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" "antrea.io/antrea/pkg/agent/apis" + "antrea.io/antrea/pkg/agent/bgp" "antrea.io/antrea/pkg/agent/interfacestore" "antrea.io/antrea/pkg/agent/multicast" "antrea.io/antrea/pkg/agent/types" @@ -142,4 +145,6 @@ type ServiceExternalIPStatusQuerier interface { type AgentBGPPolicyInfoQuerier interface { // GetBGPPolicyInfo returns Name, RouterID, LocalASN and ListenPort of effective BGP Policy applied on the Node. GetBGPPolicyInfo() (string, string, int32, int32) + // GetBGPPeerStatus returns current status of all BGP Peers of effective BGP Policy applied on the Node. + GetBGPPeerStatus(ctx context.Context) ([]bgp.PeerStatus, error) } diff --git a/pkg/querier/testing/mock_querier.go b/pkg/querier/testing/mock_querier.go index 6442a85fab7..71f44342bfa 100644 --- a/pkg/querier/testing/mock_querier.go +++ b/pkg/querier/testing/mock_querier.go @@ -25,8 +25,10 @@ package testing import ( + context "context" reflect "reflect" + bgp "antrea.io/antrea/pkg/agent/bgp" interfacestore "antrea.io/antrea/pkg/agent/interfacestore" multicast "antrea.io/antrea/pkg/agent/multicast" types "antrea.io/antrea/pkg/agent/types" @@ -357,6 +359,21 @@ func (m *MockAgentBGPPolicyInfoQuerier) EXPECT() *MockAgentBGPPolicyInfoQuerierM return m.recorder } +// GetBGPPeerStatus mocks base method. +func (m *MockAgentBGPPolicyInfoQuerier) GetBGPPeerStatus(arg0 context.Context) ([]bgp.PeerStatus, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetBGPPeerStatus", arg0) + ret0, _ := ret[0].([]bgp.PeerStatus) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBGPPeerStatus indicates an expected call of GetBGPPeerStatus. +func (mr *MockAgentBGPPolicyInfoQuerierMockRecorder) GetBGPPeerStatus(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBGPPeerStatus", reflect.TypeOf((*MockAgentBGPPolicyInfoQuerier)(nil).GetBGPPeerStatus), arg0) +} + // GetBGPPolicyInfo mocks base method. func (m *MockAgentBGPPolicyInfoQuerier) GetBGPPolicyInfo() (string, string, int32, int32) { m.ctrl.T.Helper()