From 868073202d7f4d37a8f883918ab84f1809407e82 Mon Sep 17 00:00:00 2001 From: Harsh Rawat Date: Sat, 15 Oct 2022 01:24:14 +0530 Subject: [PATCH] changed method for retrieving Windows network statistics Currently, we are running a Powershell method (Get-NetAdapterStatistics) to retrieve the network statistics in case of awsvpc network mode. However, this can cause CPU spikes on smaller instance types. Therefore, we are switching the method to use native Win32 API for retrieving the same. As part of this change, we have added two new APIs to existing NetworkUtils interface applicable for Windows. - ConvertInterfaceAliasToLUID: Returns the LUID corresponding to the given interface alias. - GetMIBIfEntryFromLUID: Returns the MIB_IF_ROW for the specified LUID. Windows API reference: https://learn.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-convertinterfacealiastoluid https://learn.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-getifentry2ex --- agent/eni/networkutils/mocks/utils_windows.go | 33 +++- agent/eni/networkutils/types_windows.go | 70 ++++++++ agent/eni/networkutils/utils_windows.go | 55 ++++++- agent/eni/networkutils/utils_windows_test.go | 84 +++++++++- agent/stats/task_windows.go | 153 ++++++------------ agent/stats/task_windows_test.go | 76 ++++----- 6 files changed, 314 insertions(+), 157 deletions(-) create mode 100644 agent/eni/networkutils/types_windows.go diff --git a/agent/eni/networkutils/mocks/utils_windows.go b/agent/eni/networkutils/mocks/utils_windows.go index 20cc4390f6b..19149ccf44f 100644 --- a/agent/eni/networkutils/mocks/utils_windows.go +++ b/agent/eni/networkutils/mocks/utils_windows.go @@ -13,7 +13,7 @@ // // Code generated by MockGen. DO NOT EDIT. -// Source: ./utils.go +// Source: ./utils_windows.go // Package mock_networkutils is a generated GoMock package. package mock_networkutils @@ -24,6 +24,7 @@ import ( reflect "reflect" time "time" + networkutils "github.com/aws/amazon-ecs-agent/agent/eni/networkutils" gomock "github.com/golang/mock/gomock" ) @@ -94,3 +95,33 @@ func (mr *MockNetworkUtilsMockRecorder) GetDNSServerAddressList(macAddress inter mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDNSServerAddressList", reflect.TypeOf((*MockNetworkUtils)(nil).GetDNSServerAddressList), macAddress) } + +// ConvertInterfaceAliasToLUID mocks base method +func (m *MockNetworkUtils) ConvertInterfaceAliasToLUID(interfaceAlias string) (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ConvertInterfaceAliasToLUID", interfaceAlias) + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ConvertInterfaceAliasToLUID indicates an expected call of ConvertInterfaceAliasToLUID +func (mr *MockNetworkUtilsMockRecorder) ConvertInterfaceAliasToLUID(interfaceAlias interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConvertInterfaceAliasToLUID", reflect.TypeOf((*MockNetworkUtils)(nil).ConvertInterfaceAliasToLUID), interfaceAlias) +} + +// GetMIBIfEntryFromLUID mocks base method +func (m *MockNetworkUtils) GetMIBIfEntryFromLUID(ifaceLUID uint64) (*networkutils.MibIfRow2, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMIBIfEntryFromLUID", ifaceLUID) + ret0, _ := ret[0].(*networkutils.MibIfRow2) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMIBIfEntryFromLUID indicates an expected call of GetMIBIfEntryFromLUID +func (mr *MockNetworkUtilsMockRecorder) GetMIBIfEntryFromLUID(ifaceLUID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMIBIfEntryFromLUID", reflect.TypeOf((*MockNetworkUtils)(nil).GetMIBIfEntryFromLUID), ifaceLUID) +} diff --git a/agent/eni/networkutils/types_windows.go b/agent/eni/networkutils/types_windows.go new file mode 100644 index 00000000000..551ab47b2fd --- /dev/null +++ b/agent/eni/networkutils/types_windows.go @@ -0,0 +1,70 @@ +//go:build windows +// +build windows + +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file 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 networkutils + +import "golang.org/x/sys/windows" + +const ( + ifMaxStringSize = 256 + ifMaxPhysAddressLength = 32 +) + +// MibIfRow2 structure stores information about a particular interface. +// https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-mib_if_row2 +type MibIfRow2 struct { + InterfaceLUID uint64 + interfaceIndex uint32 + interfaceGUID windows.GUID + alias [ifMaxStringSize + 1]uint16 + description [ifMaxStringSize + 1]uint16 + physicalAddressLength uint32 + physicalAddress [ifMaxPhysAddressLength]byte + permanentPhysicalAddress [ifMaxPhysAddressLength]byte + mtu uint32 + ifType uint32 + tunnelType uint32 + mediaType uint32 + physicalMediumType uint32 + accessType uint32 + directionType uint32 + interfaceAndOperStatusFlags uint8 + operStatus uint32 + adminStatus uint32 + mediaConnectState uint32 + networkGUID windows.GUID + connectionType uint32 + transmitLinkSpeed uint64 + receiveLinkSpeed uint64 + InOctets uint64 + InUcastPkts uint64 + InNUcastPkts uint64 + InDiscards uint64 + InErrors uint64 + inUnknownProtos uint64 + inUcastOctets uint64 + inMulticastOctets uint64 + inBroadcastOctets uint64 + OutOctets uint64 + OutUcastPkts uint64 + OutNUcastPkts uint64 + OutDiscards uint64 + OutErrors uint64 + outUcastOctets uint64 + outMulticastOctets uint64 + outBroadcastOctets uint64 + outQLen uint64 +} diff --git a/agent/eni/networkutils/utils_windows.go b/agent/eni/networkutils/utils_windows.go index 2f8cf299539..398d91962ed 100644 --- a/agent/eni/networkutils/utils_windows.go +++ b/agent/eni/networkutils/utils_windows.go @@ -39,9 +39,18 @@ import ( // NetworkUtils is the interface used for accessing network related functionality on Windows. // The methods declared in this package may or may not add any additional logic over the actual networking api calls. type NetworkUtils interface { + // GetInterfaceMACByIndex returns the MAC address of the device with given interface index. + // We will retry with the given timeout and context before erroring out. GetInterfaceMACByIndex(int, context.Context, time.Duration) (string, error) + // GetAllNetworkInterfaces returns all the network interfaces in the host namespace. GetAllNetworkInterfaces() ([]net.Interface, error) + // GetDNSServerAddressList returns the DNS Server list associated to the interface with + // the given MAC address. GetDNSServerAddressList(macAddress string) ([]string, error) + // ConvertInterfaceAliasToLUID converts an interface alias to it's LUID. + ConvertInterfaceAliasToLUID(interfaceAlias string) (uint64, error) + // GetMIBIfEntryFromLUID returns the MIB_IF_ROW2 for the interface with the given LUID. + GetMIBIfEntryFromLUID(ifaceLUID uint64) (*MibIfRow2, error) } type networkUtils struct { @@ -54,14 +63,25 @@ type networkUtils struct { ctx context.Context // A wrapper over Golang's net package netWrapper netwrapper.NetWrapper + // funcConvertInterfaceAliasToLuid is the system call to ConvertInterfaceAliasToLuid Win32 API. + funcConvertInterfaceAliasToLuid func(a ...uintptr) (r1 uintptr, r2 uintptr, lastErr error) + // funcGetIfEntry2Ex is the system call to GetIfEntry2Ex Win32 API. + funcGetIfEntry2Ex func(a ...uintptr) (r1 uintptr, r2 uintptr, lastErr error) } var funcGetAdapterAddresses = getAdapterAddresses // New creates a new network utils. func New() NetworkUtils { + // We would be using GetIfTable2Ex, GetIfEntry2Ex, and FreeMibTable Win32 APIs from IP Helper. + moduleIPHelper := windows.NewLazySystemDLL("iphlpapi.dll") + procConvertInterfaceAliasToLuid := moduleIPHelper.NewProc("ConvertInterfaceAliasToLuid") + procGetIfEntry2Ex := moduleIPHelper.NewProc("GetIfEntry2Ex") + return &networkUtils{ - netWrapper: netwrapper.New(), + netWrapper: netwrapper.New(), + funcConvertInterfaceAliasToLuid: procConvertInterfaceAliasToLuid.Call, + funcGetIfEntry2Ex: procGetIfEntry2Ex.Call, } } @@ -144,6 +164,39 @@ func (utils *networkUtils) GetDNSServerAddressList(macAddress string) ([]string, return dnsServerAddressList, nil } +// ConvertInterfaceAliasToLUID returns the LUID of the interface with given interface alias. +// Internally, it would invoke ConvertInterfaceAliasToLuid Win32 API to perform the conversion. +func (utils *networkUtils) ConvertInterfaceAliasToLUID(interfaceAlias string) (uint64, error) { + var luid uint64 + alias := windows.StringToUTF16Ptr(interfaceAlias) + + // ConvertInterfaceAliasToLuid function converts alias into LUID. + // https://learn.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-convertinterfacealiastoluid + retVal, _, _ := utils.funcConvertInterfaceAliasToLuid(uintptr(unsafe.Pointer(alias)), uintptr(unsafe.Pointer(&luid))) + if retVal != 0 { + return 0, errors.Errorf("error occured while calling ConvertInterfaceAliasToLuid: %s", syscall.Errno(retVal)) + } + + return luid, nil +} + +// GetMIBIfEntryFromLUID returns the MIB_IF_ROW2 object for the interface with given LUID. +// Internally, this would invoke GetIfEntry2Ex Win32 API to retrieve the specific row. +func (utils *networkUtils) GetMIBIfEntryFromLUID(ifaceLUID uint64) (*MibIfRow2, error) { + row := &MibIfRow2{ + InterfaceLUID: ifaceLUID, + } + + // GetIfEntry2Ex function retrieves the MIB-II interface for the given interface index.. + // https://learn.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-getifentry2ex + retVal, _, _ := utils.funcGetIfEntry2Ex(uintptr(0), uintptr(unsafe.Pointer(row))) + if retVal != 0 { + return nil, errors.Errorf("error occured while calling GetIfEntry2Ex: %s", syscall.Errno(retVal)) + } + + return row, nil +} + // parseMACAddress parses the physical address of windows.IpAdapterAddresses into net.HardwareAddr. func (utils *networkUtils) parseMACAddress(adapterAddress *windows.IpAdapterAddresses) net.HardwareAddr { hardwareAddr := make(net.HardwareAddr, adapterAddress.PhysicalAddressLength) diff --git a/agent/eni/networkutils/utils_windows_test.go b/agent/eni/networkutils/utils_windows_test.go index 65075ad00cc..5629a6e93ae 100644 --- a/agent/eni/networkutils/utils_windows_test.go +++ b/agent/eni/networkutils/utils_windows_test.go @@ -22,6 +22,7 @@ import ( "net" "syscall" "testing" + "unsafe" "golang.org/x/sys/windows" @@ -32,9 +33,13 @@ import ( ) const ( - interfaceIndex = 9 - macAddress = "02:22:ea:8c:81:dc" - validDnsServer = "10.0.0.2" + deviceName = "Ethernet 2" + ifaceLUID uint64 = 1689399649632256 + RxBytes uint64 = 1000 + TxBytes uint64 = 5000 + interfaceIndex = 9 + macAddress = "02:22:ea:8c:81:dc" + validDnsServer = "10.0.0.2" ) // This is a success test. We receive the appropriate MAC address corresponding to the interface index. @@ -230,3 +235,76 @@ func TestGetDNSServerAddressList(t *testing.T) { assert.Len(t, dnsServerList, 1) assert.EqualValues(t, dnsServerList[0], validDnsServer) } + +// TestGetMIBIfEntryFromLUID tests the GetMIBIfEntryFromLUID method. +func TestGetMIBIfEntryFromLUID(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + netUtils := &networkUtils{} + + // Dummy function which is representative of system call (GetIfEntry2Ex) + netUtils.funcGetIfEntry2Ex = func(a ...uintptr) (uintptr, uintptr, error) { + row := (*MibIfRow2)(unsafe.Pointer(a[1])) + row.interfaceIndex = interfaceIndex + row.OutOctets = TxBytes + row.InOctets = RxBytes + return uintptr(0), uintptr(0), nil + } + + ifRow, err := netUtils.GetMIBIfEntryFromLUID(ifaceLUID) + assert.NoError(t, err) + assert.Equal(t, uint32(interfaceIndex), ifRow.interfaceIndex) + assert.Equal(t, TxBytes, ifRow.OutOctets) + assert.Equal(t, RxBytes, ifRow.InOctets) +} + +// TestGetMIBIfEntryFromLUIDError tests the GetMIBIfEntryFromLUID method in error case. +func TestGetMIBIfEntryFromLUIDError(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + netUtils := &networkUtils{} + + // Dummy function which is representative of system call (GetIfEntry2Ex) + netUtils.funcGetIfEntry2Ex = func(a ...uintptr) (uintptr, uintptr, error) { + // Return an error code. + return uintptr(1), uintptr(0), nil + } + + _, err := netUtils.GetMIBIfEntryFromLUID(ifaceLUID) + assert.Error(t, err) +} + +// TestConvertInterfaceAliasToLUID tests ConvertInterfaceAliasToLUID method. +func TestConvertInterfaceAliasToLUID(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + netUtils := &networkUtils{} + // Dummy function which is representative of system call (GetIfEntry2Ex) + netUtils.funcConvertInterfaceAliasToLuid = func(a ...uintptr) (uintptr, uintptr, error) { + luid := (*uint64)(unsafe.Pointer(a[1])) + *luid = ifaceLUID + return uintptr(0), uintptr(0), nil + } + + luid, err := netUtils.ConvertInterfaceAliasToLUID(deviceName) + assert.NoError(t, err) + assert.Equal(t, ifaceLUID, luid) +} + +// TestConvertInterfaceAliasToLUID tests ConvertInterfaceAliasToLUID method in case of error. +func TestConvertInterfaceAliasToLUIDError(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + netUtils := &networkUtils{} + // Dummy function which is representative of system call (GetIfEntry2Ex) + netUtils.funcConvertInterfaceAliasToLuid = func(a ...uintptr) (uintptr, uintptr, error) { + return uintptr(1), uintptr(0), nil + } + + _, err := netUtils.ConvertInterfaceAliasToLUID(deviceName) + assert.Error(t, err) +} diff --git a/agent/stats/task_windows.go b/agent/stats/task_windows.go index 165cab6ca02..6e9d0744a28 100644 --- a/agent/stats/task_windows.go +++ b/agent/stats/task_windows.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -17,66 +18,43 @@ package stats import ( "context" - "os/exec" - "strconv" - "strings" "time" "github.com/aws/amazon-ecs-agent/agent/api/task" + "github.com/aws/amazon-ecs-agent/agent/eni/networkutils" "github.com/aws/amazon-ecs-agent/agent/stats/resolver" dockerstats "github.com/docker/docker/api/types" "github.com/pkg/errors" ) -const ( - receivedBroadcastPackets = "ReceivedBroadcastPackets" - receivedMulticastPackets = "ReceivedMulticastPackets" - receivedUnicastPackets = "ReceivedUnicastPackets" - sentBroadcastPackets = "SentBroadcastPackets" - sentMulticastPackets = "SentMulticastPackets" - sentUnicastPackets = "SentUnicastPackets" - receivedBytes = "ReceivedBytes" - receivedPacketErrors = "ReceivedPacketErrors" - receivedDiscardedPackets = "ReceivedDiscardedPackets" - sentBytes = "SentBytes" - outboundPacketErrors = "OutboundPacketErrors" - outboundDiscardedPackets = "OutboundDiscardedPackets" -) - -var ( - // Making it visible for unit testing - execCommand = exec.Command - // Fields to be extracted from the stats returned by cmdlet. - networkStatKeys = []string{ - receivedBroadcastPackets, - receivedMulticastPackets, - receivedUnicastPackets, - sentBroadcastPackets, - sentMulticastPackets, - sentUnicastPackets, - receivedBytes, - receivedPacketErrors, - receivedDiscardedPackets, - sentBytes, - outboundDiscardedPackets, - outboundPacketErrors, - } -) - type StatsTask struct { *statsTaskCommon + interfaceLUID []uint64 + netUtils networkutils.NetworkUtils } func newStatsTaskContainer(taskARN, taskId, containerPID string, numberOfContainers int, resolver resolver.ContainerMetadataResolver, publishInterval time.Duration, taskENIs task.TaskENIs) (*StatsTask, error) { - ctx, cancel := context.WithCancel(context.Background()) + + // Instantiate an instance of network utils. + // This interface would be used to invoke Windows networking APIs. + netUtils := networkutils.New() devices := make([]string, len(taskENIs)) + ifaceLUID := make([]uint64, len(taskENIs)) + // Find and store the device name along with the interface LUID. for index, device := range taskENIs { devices[index] = device.LinkName + + interfaceLUID, err := netUtils.ConvertInterfaceAliasToLUID(device.LinkName) + if err != nil { + return nil, errors.Wrapf(err, "failed to initialise stats task container") + } + ifaceLUID[index] = interfaceLUID } + ctx, cancel := context.WithCancel(context.Background()) return &StatsTask{ statsTaskCommon: &statsTaskCommon{ TaskMetadata: &TaskMetadata{ @@ -90,86 +68,53 @@ func newStatsTaskContainer(taskARN, taskId, containerPID string, numberOfContain Resolver: resolver, metricPublishInterval: publishInterval, }, + interfaceLUID: ifaceLUID, + netUtils: netUtils, }, nil } +// retrieveNetworkStatistics retrieves the network statistics for the task devices by querying +// the Windows networking APIs. func (taskStat *StatsTask) retrieveNetworkStatistics() (map[string]dockerstats.NetworkStats, error) { if len(taskStat.TaskMetadata.DeviceName) == 0 { return nil, errors.Errorf("unable to find any device name associated with the task %s", taskStat.TaskMetadata.TaskArn) } networkStats := make(map[string]dockerstats.NetworkStats, len(taskStat.TaskMetadata.DeviceName)) - for _, device := range taskStat.TaskMetadata.DeviceName { - networkAdaptorStatistics, err := taskStat.getNetworkAdaptorStatistics(device) + for index, device := range taskStat.TaskMetadata.DeviceName { + numberOfContainers := taskStat.TaskMetadata.NumberContainers + + // Query the MIB_IF_ROW2 for the given interface LUID which would contain network statistics. + ifaceLUID := taskStat.interfaceLUID[index] + ifRow, err := taskStat.netUtils.GetMIBIfEntryFromLUID(ifaceLUID) if err != nil { - return nil, err + return nil, errors.Wrapf(err, "failed to retrieve network stats") } + + // Parse the retrieved network statistics. + networkAdaptorStatistics := taskStat.parseNetworkStatsPerContainerFromIfRow(ifRow, numberOfContainers) networkStats[device] = *networkAdaptorStatistics } return networkStats, nil } -// getNetworkAdaptorStatistics returns the network statistics per container for the given network interface. -func (taskStat *StatsTask) getNetworkAdaptorStatistics(device string) (*dockerstats.NetworkStats, error) { - // Ref: https://docs.microsoft.com/en-us/powershell/module/netadapter/get-netadapterstatistics?view=windowsserver2019-ps - // The Get-NetAdapterStatistics cmdlet gets networking statistics from a network adapter. - // The statistics include broadcast, multicast, discards, and errors. - cmd := "Get-NetAdapterStatistics -Name \"" + device + "\" | Format-List -Property *" - out, err := execCommand("powershell", "-Command", cmd).CombinedOutput() - - if err != nil { - return nil, errors.Wrapf(err, "failed to run Get-NetAdapterStatistics for %s", device) - } - str := string(out) - - // Extract rawStats from the cmdlet output. - lines := strings.Split(str, "\n") - rawStats := make(map[string]string) - for _, line := range lines { - // populate all the network metrics in a map - kv := strings.Split(line, ":") - if len(kv) != 2 { - continue - } - key := strings.TrimSpace(kv[0]) - value := strings.TrimSpace(kv[1]) - rawStats[key] = value - } - - // Parse the required fields from the generated map. - parsedStats := make(map[string]uint64) - for _, key := range networkStatKeys { - value, err := taskStat.getMapValue(rawStats, key) - if err != nil { - return nil, err - } - parsedStats[key] = value - } - - numberOfContainers := uint64(taskStat.TaskMetadata.NumberContainers) - - return &dockerstats.NetworkStats{ - RxBytes: parsedStats[receivedBytes] / numberOfContainers, - RxPackets: (parsedStats[receivedBroadcastPackets] + parsedStats[receivedMulticastPackets] + parsedStats[receivedUnicastPackets]) / numberOfContainers, - RxErrors: parsedStats[receivedPacketErrors] / numberOfContainers, - RxDropped: parsedStats[receivedDiscardedPackets] / numberOfContainers, - TxBytes: parsedStats[sentBytes] / numberOfContainers, - TxPackets: (parsedStats[sentBroadcastPackets] + parsedStats[sentMulticastPackets] + parsedStats[sentUnicastPackets]) / numberOfContainers, - TxErrors: parsedStats[outboundPacketErrors] / numberOfContainers, - TxDropped: parsedStats[outboundDiscardedPackets] / numberOfContainers, - }, nil -} - -// getMapValue retrieves the value of the key from the given map. -func (taskStat *StatsTask) getMapValue(m map[string]string, key string) (uint64, error) { - v, ok := m[key] - if !ok { - return 0, errors.Errorf("failed to find key: %s in output", key) - } - val, err := strconv.ParseUint(v, 10, 64) - if err != nil { - return 0, errors.Errorf("failed to parse network stats for %s with value: %s", key, v) - } - return val, nil +// parseNetworkStatsPerContainerFromIfRow parses the network statistics from MibIfRow2 row into +// docker network stats. The stats are averaged over all the task containers. +func (taskStat *StatsTask) parseNetworkStatsPerContainerFromIfRow( + iface *networkutils.MibIfRow2, + numberOfContainers int, +) *dockerstats.NetworkStats { + + stats := &dockerstats.NetworkStats{} + stats.RxBytes = iface.InOctets / uint64(numberOfContainers) + stats.RxPackets = (iface.InNUcastPkts + iface.InUcastPkts) / uint64(numberOfContainers) + stats.RxErrors = iface.InErrors / uint64(numberOfContainers) + stats.RxDropped = iface.InDiscards / uint64(numberOfContainers) + stats.TxBytes = iface.OutOctets / uint64(numberOfContainers) + stats.TxPackets = (iface.OutNUcastPkts + iface.OutUcastPkts) / uint64(numberOfContainers) + stats.TxErrors = iface.OutErrors / uint64(numberOfContainers) + stats.TxDropped = iface.OutDiscards / uint64(numberOfContainers) + + return stats } diff --git a/agent/stats/task_windows_test.go b/agent/stats/task_windows_test.go index 874e930efdc..52867d52ece 100644 --- a/agent/stats/task_windows_test.go +++ b/agent/stats/task_windows_test.go @@ -1,4 +1,5 @@ //go:build windows +// +build windows // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // @@ -17,15 +18,14 @@ package stats import ( "context" - "fmt" - "os" - "os/exec" "testing" "time" apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container" apitask "github.com/aws/amazon-ecs-agent/agent/api/task" apitaskstatus "github.com/aws/amazon-ecs-agent/agent/api/task/status" + "github.com/aws/amazon-ecs-agent/agent/eni/networkutils" + mock_networkutils "github.com/aws/amazon-ecs-agent/agent/eni/networkutils/mocks" mock_resolver "github.com/aws/amazon-ecs-agent/agent/stats/resolver/mock" dockerstats "github.com/docker/docker/api/types" @@ -34,33 +34,27 @@ import ( ) const ( - networkAdapterStatisticsResult = `ifAlias : Ethernet 3 -InterfaceAlias : Ethernet 3 -InterfaceDescription : Amazon Elastic Network Adapter #2 -Name : Ethernet 3 -Source : 2 -OutboundDiscardedPackets : 30 -OutboundPacketErrors : 20 -ReceivedBroadcastBytes : 247548 -ReceivedBroadcastPackets : 5894 -ReceivedBytes : 249578 -ReceivedDiscardedPackets : 10 -ReceivedMulticastBytes : 0 -ReceivedMulticastPackets : 0 -ReceivedPacketErrors : 4 -ReceivedUnicastBytes : 2030 -ReceivedUnicastPackets : 8 -SentBroadcastBytes : 2858 -SentBroadcastPackets : 26 -SentBytes : 256478 -SentMulticastBytes : 5995 -SentMulticastPackets : 65 -SentUnicastBytes : 247624 -SentUnicastPackets : 5895 -SupportedStatistics : 4163583` + deviceName = "Ethernet 3" + ifaceLUID uint64 = 1689399649632256 ) -var expectedNetworkStats = dockerstats.NetworkStats{ +// Result from GetIfEntry2Ex Win32 API call. +var ifRowResult = &networkutils.MibIfRow2{ + InterfaceLUID: ifaceLUID, + InOctets: 249578, + InUcastPkts: 8, + InNUcastPkts: 5894, + InErrors: 4, + InDiscards: 10, + OutOctets: 256478, + OutUcastPkts: 5895, + OutNUcastPkts: 91, + OutErrors: 20, + OutDiscards: 30, +} + +// Expected output from the stats collection module. +var expectedNetworkStats = &dockerstats.NetworkStats{ RxBytes: 249578, RxPackets: 5902, RxErrors: 4, @@ -73,31 +67,14 @@ var expectedNetworkStats = dockerstats.NetworkStats{ InstanceID: "", } -// Supporting methods for network stats test. -func fakeExecCommandForNetworkStats(command string, args ...string) *exec.Cmd { - cs := []string{"-test.run=TestNetworkStatsProcess", "--", command} - cs = append(cs, args...) - cmd := exec.Command(os.Args[0], cs...) - cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} - return cmd -} - -// TestNetworkStatsProcess is invoked from fakeExecCommand to return the network stats. -func TestNetworkStatsProcess(t *testing.T) { - if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { - return - } - fmt.Fprintf(os.Stdout, networkAdapterStatisticsResult) - os.Exit(0) -} - +// TestTaskStatsCollection tests the network statistics collection. func TestTaskStatsCollection(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() ctx, cancel := context.WithCancel(context.TODO()) resolver := mock_resolver.NewMockContainerMetadataResolver(ctrl) - execCommand = fakeExecCommandForNetworkStats + mockNetUtils := mock_networkutils.NewMockNetworkUtils(ctrl) containerPID := "23" taskId := "task1" @@ -108,7 +85,7 @@ func TestTaskStatsCollection(t *testing.T) { TaskMetadata: &TaskMetadata{ TaskArn: taskId, ContainerPID: containerPID, - DeviceName: []string{"Ethernet 3"}, + DeviceName: []string{deviceName}, NumberContainers: numberOfContainers, }, Ctx: ctx, @@ -116,6 +93,8 @@ func TestTaskStatsCollection(t *testing.T) { Resolver: resolver, metricPublishInterval: time.Second, }, + netUtils: mockNetUtils, + interfaceLUID: []uint64{ifaceLUID}, } testTask := &apitask.Task{ @@ -126,6 +105,7 @@ func TestTaskStatsCollection(t *testing.T) { KnownStatusUnsafe: apitaskstatus.TaskRunning, } resolver.EXPECT().ResolveTaskByARN(gomock.Any()).Return(testTask, nil).AnyTimes() + mockNetUtils.EXPECT().GetMIBIfEntryFromLUID(ifaceLUID).Return(ifRowResult, nil).AnyTimes() taskStats.StartStatsCollection() time.Sleep(checkPointSleep)