cni/network/network.go (1,167 lines of code) (raw):
// Copyright 2017 Microsoft. All rights reserved.
// MIT License
package network
import (
"context"
"encoding/json"
"fmt"
"net"
"os"
"regexp"
"strconv"
"time"
"github.com/Azure/azure-container-networking/aitelemetry"
"github.com/Azure/azure-container-networking/cni"
"github.com/Azure/azure-container-networking/cni/api"
"github.com/Azure/azure-container-networking/cni/log"
"github.com/Azure/azure-container-networking/cni/util"
"github.com/Azure/azure-container-networking/cns"
cnscli "github.com/Azure/azure-container-networking/cns/client"
"github.com/Azure/azure-container-networking/cns/fsnotify"
"github.com/Azure/azure-container-networking/common"
"github.com/Azure/azure-container-networking/dhcp"
"github.com/Azure/azure-container-networking/iptables"
"github.com/Azure/azure-container-networking/netio"
"github.com/Azure/azure-container-networking/netlink"
"github.com/Azure/azure-container-networking/network"
"github.com/Azure/azure-container-networking/network/policy"
"github.com/Azure/azure-container-networking/platform"
nnscontracts "github.com/Azure/azure-container-networking/proto/nodenetworkservice/3.302.0.744"
"github.com/Azure/azure-container-networking/store"
"github.com/Azure/azure-container-networking/telemetry"
cniSkel "github.com/containernetworking/cni/pkg/skel"
cniTypes "github.com/containernetworking/cni/pkg/types"
cniTypesCurr "github.com/containernetworking/cni/pkg/types/100"
"github.com/pkg/errors"
"go.uber.org/zap"
)
// matches if the string fully consists of zero or more alphanumeric, dots, dashes, parentheses, spaces, or underscores
var allowedInput = regexp.MustCompile(`^[a-zA-Z0-9._\-\(\) ]*$`)
const (
dockerNetworkOption = "com.docker.network.generic"
OpModeTransparent = "transparent"
// Supported IP version. Currently support only IPv4
ipamV6 = "azure-vnet-ipamv6"
defaultRequestTimeout = 15 * time.Second
ipv4FullMask = 32
ipv6FullMask = 128
ibInterfacePrefix = "ib"
)
// CNI Operation Types
const (
CNI_ADD = "ADD"
CNI_DEL = "DEL"
CNI_UPDATE = "UPDATE"
)
const (
// URL to query NMAgent version and determine whether we snat on host
nmAgentSupportedApisURL = "http://168.63.129.16/machine/plugins/?comp=nmagent&type=GetSupportedApis"
// Only SNAT support (no DNS support)
nmAgentSnatSupportAPI = "NetworkManagementSnatSupport"
// SNAT and DNS are both supported
nmAgentSnatAndDnsSupportAPI = "NetworkManagementDNSSupport"
)
// temporary consts related func determineSnat() which is to be deleted after
// a baking period with newest NMAgent changes
const (
jsonFileExtension = ".json"
)
// NetPlugin represents the CNI network plugin.
type NetPlugin struct {
*cni.Plugin
nm network.NetworkManager
ipamInvoker IPAMInvoker
report *telemetry.CNIReport
tb *telemetry.TelemetryBuffer
nnsClient NnsClient
multitenancyClient MultitenancyClient
netClient InterfaceGetter
}
type PolicyArgs struct {
subnetInfos []network.SubnetInfo
nwCfg *cni.NetworkConfig
ipconfigs []*network.IPConfig
}
// client for node network service
type NnsClient interface {
// Do network port programming for the pod via node network service.
// podName - name of the pod as received from containerD
// nwNamesapce - network namespace name as received from containerD
AddContainerNetworking(ctx context.Context, podName, nwNamespace string) (*nnscontracts.ConfigureContainerNetworkingResponse, error)
// Undo or delete network port programming for the pod via node network service.
// podName - name of the pod as received from containerD
// nwNamesapce - network namespace name as received from containerD
DeleteContainerNetworking(ctx context.Context, podName, nwNamespace string) (*nnscontracts.ConfigureContainerNetworkingResponse, error)
}
// client for getting interface
type InterfaceGetter interface {
GetNetworkInterfaces() ([]net.Interface, error)
GetNetworkInterfaceAddrs(iface *net.Interface) ([]net.Addr, error)
}
// snatConfiguration contains a bool that determines whether CNI enables snat on host and snat for dns
type snatConfiguration struct {
EnableSnatOnHost bool
EnableSnatForDns bool
}
// NewPlugin creates a new NetPlugin object.
func NewPlugin(name string,
config *common.PluginConfig,
client NnsClient,
multitenancyClient MultitenancyClient,
) (*NetPlugin, error) {
// Setup base plugin.
plugin, err := cni.NewPlugin(name, config.Version)
if err != nil {
return nil, err
}
nl := netlink.NewNetlink()
// Setup network manager.
nm, err := network.NewNetworkManager(nl, platform.NewExecClient(logger), &netio.NetIO{}, network.NewNamespaceClient(), iptables.NewClient(), dhcp.New(logger))
if err != nil {
return nil, err
}
config.NetApi = nm
return &NetPlugin{
Plugin: plugin,
nm: nm,
nnsClient: client,
multitenancyClient: multitenancyClient,
netClient: &netio.NetIO{},
}, nil
}
func (plugin *NetPlugin) SetCNIReport(report *telemetry.CNIReport, tb *telemetry.TelemetryBuffer) {
plugin.report = report
plugin.tb = tb
}
// Starts the plugin.
func (plugin *NetPlugin) Start(config *common.PluginConfig) error {
// Initialize base plugin.
err := plugin.Initialize(config)
if err != nil {
logger.Error("Failed to initialize base plugin", zap.Error(err))
return err
}
// Log platform information.
logger.Info("Plugin Info",
zap.String("name", plugin.Name),
zap.String("version", plugin.Version))
// Initialize network manager. rehyrdration not required on reboot for cni plugin
err = plugin.nm.Initialize(config, false)
if err != nil {
logger.Error("Failed to initialize network manager", zap.Error(err))
return err
}
logger.Info("Plugin started")
return nil
}
func sendEvent(plugin *NetPlugin, msg string) {
eventMsg := fmt.Sprintf("[%d] %s", os.Getpid(), msg)
plugin.report.Version = plugin.Version
plugin.report.EventMessage = eventMsg
telemetry.SendCNIEvent(plugin.tb, plugin.report)
}
func (plugin *NetPlugin) GetAllEndpointState(networkid string) (*api.AzureCNIState, error) {
st := api.AzureCNIState{
ContainerInterfaces: make(map[string]api.PodNetworkInterfaceInfo),
}
eps, err := plugin.nm.GetAllEndpoints(networkid)
if err == store.ErrStoreEmpty {
logger.Error("failed to retrieve endpoint state", zap.Error(err))
} else if err != nil {
return nil, err
}
for _, ep := range eps {
id := ep.EndpointID
info := api.PodNetworkInterfaceInfo{
PodName: ep.PODName,
PodNamespace: ep.PODNameSpace,
PodEndpointId: ep.EndpointID,
ContainerID: ep.ContainerID,
IPAddresses: ep.IPAddresses,
}
st.ContainerInterfaces[id] = info
}
return &st, nil
}
// Stops the plugin.
func (plugin *NetPlugin) Stop() {
plugin.nm.Uninitialize()
plugin.Uninitialize()
logger.Info("Plugin stopped")
}
// findInterfaceByMAC returns the name of the master interface
func (plugin *NetPlugin) findInterfaceByMAC(macAddress string) string {
interfaces, err := plugin.netClient.GetNetworkInterfaces()
if err != nil {
logger.Error("failed to get interfaces", zap.Error(err))
return ""
}
macs := make([]string, 0, len(interfaces))
for _, iface := range interfaces {
// find master interface by macAddress for Swiftv2
macs = append(macs, iface.HardwareAddr.String())
if iface.HardwareAddr.String() == macAddress {
return iface.Name
}
}
// Failed to find a suitable interface.
logger.Error("Failed to find interface by MAC", zap.String("macAddress", macAddress), zap.Strings("interfaces", macs))
return ""
}
// findMasterInterfaceBySubnet returns the name of the master interface.
func (plugin *NetPlugin) findMasterInterfaceBySubnet(nwCfg *cni.NetworkConfig, subnetPrefix *net.IPNet) string {
// An explicit master configuration wins. Explicitly specifying a master is
// useful if host has multiple interfaces with addresses in the same subnet.
if nwCfg.Master != "" {
return nwCfg.Master
}
// Otherwise, pick the first interface with an IP address in the given subnet.
subnetPrefixString := subnetPrefix.String()
interfaces, err := plugin.netClient.GetNetworkInterfaces()
if err != nil {
logger.Error("failed to get interfaces", zap.Error(err))
return ""
}
var ipnets []string
for _, iface := range interfaces {
addrs, _ := plugin.netClient.GetNetworkInterfaceAddrs(&iface) //nolint
for _, addr := range addrs {
_, ipnet, err := net.ParseCIDR(addr.String())
if err != nil {
continue
}
ipnets = append(ipnets, ipnet.String())
if subnetPrefixString == ipnet.String() {
return iface.Name
}
}
}
// Failed to find a suitable interface.
logger.Error("Failed to find interface by subnet prefix", zap.String("subnetPrefix", subnetPrefixString), zap.Strings("interfaces", ipnets))
return ""
}
// GetEndpointID returns a unique endpoint ID based on the CNI args.
func GetEndpointID(args *cniSkel.CmdArgs) string {
infraEpId, _ := network.ConstructEndpointID(args.ContainerID, args.Netns, args.IfName)
return infraEpId
}
// getPodInfo returns POD info by parsing the CNI args.
func (plugin *NetPlugin) getPodInfo(args string) (name, ns string, err error) {
podCfg, err := cni.ParseCniArgs(args)
if err != nil {
logger.Error("Error while parsing CNI Args", zap.Error(err))
return "", "", err
}
k8sNamespace := string(podCfg.K8S_POD_NAMESPACE)
if len(k8sNamespace) == 0 {
errMsg := "Pod Namespace not specified in CNI Args"
logger.Error(errMsg)
return "", "", plugin.Errorf(errMsg)
}
k8sPodName := string(podCfg.K8S_POD_NAME)
if len(k8sPodName) == 0 {
errMsg := "Pod Name not specified in CNI Args"
logger.Error(errMsg)
return "", "", plugin.Errorf(errMsg)
}
return k8sPodName, k8sNamespace, nil
}
func SetCustomDimensions(cniMetric *telemetry.AIMetric, nwCfg *cni.NetworkConfig, err error) {
if cniMetric == nil {
logger.Error("Unable to set custom dimension. Report is nil")
return
}
if err != nil {
cniMetric.Metric.CustomDimensions[telemetry.StatusStr] = telemetry.FailedStr
} else {
cniMetric.Metric.CustomDimensions[telemetry.StatusStr] = telemetry.SucceededStr
}
if nwCfg != nil {
if nwCfg.MultiTenancy {
cniMetric.Metric.CustomDimensions[telemetry.CNIModeStr] = telemetry.MultiTenancyStr
} else {
cniMetric.Metric.CustomDimensions[telemetry.CNIModeStr] = telemetry.SingleTenancyStr
}
cniMetric.Metric.CustomDimensions[telemetry.CNINetworkModeStr] = nwCfg.Mode
}
}
func (plugin *NetPlugin) setCNIReportDetails(nwCfg *cni.NetworkConfig, opType, msg string) {
plugin.report.OperationType = opType
plugin.report.SubContext = fmt.Sprintf("%+v", nwCfg)
plugin.report.EventMessage = msg
plugin.report.BridgeDetails.NetworkMode = nwCfg.Mode
plugin.report.InterfaceDetails.SecondaryCAUsedCount = plugin.nm.GetNumberOfEndpoints("", nwCfg.Name)
}
func addNatIPV6SubnetInfo(nwCfg *cni.NetworkConfig,
resultV6 *cniTypesCurr.Result,
nwInfo *network.NetworkInfo,
) {
if nwCfg.IPV6Mode == network.IPV6Nat {
ipv6Subnet := resultV6.IPs[0].Address
ipv6Subnet.IP = ipv6Subnet.IP.Mask(ipv6Subnet.Mask)
ipv6SubnetInfo := network.SubnetInfo{
Family: platform.AfINET6,
Prefix: ipv6Subnet,
Gateway: resultV6.IPs[0].Gateway,
}
logger.Info("ipv6 subnet info",
zap.Any("ipv6SubnetInfo", ipv6SubnetInfo))
nwInfo.Subnets = append(nwInfo.Subnets, ipv6SubnetInfo)
}
}
func (plugin *NetPlugin) addIpamInvoker(ipamAddConfig IPAMAddConfig) (IPAMAddResult, error) {
ipamAddResult, err := plugin.ipamInvoker.Add(ipamAddConfig)
if err != nil {
return IPAMAddResult{}, errors.Wrap(err, "failed to add ipam invoker")
}
sendEvent(plugin, fmt.Sprintf("Allocated IPAddress from ipam interface: %+v", ipamAddResult.PrettyString()))
return ipamAddResult, nil
}
// get network
func (plugin *NetPlugin) getNetworkID(netNs string, interfaceInfo *network.InterfaceInfo, nwCfg *cni.NetworkConfig) (string, error) {
networkID, err := plugin.getNetworkName(netNs, interfaceInfo, nwCfg)
if err != nil {
return "", err
}
return networkID, nil
}
// get network info for legacy
func (plugin *NetPlugin) getNetworkInfo(netNs string, interfaceInfo *network.InterfaceInfo, nwCfg *cni.NetworkConfig) network.EndpointInfo {
networkID, _ := plugin.getNetworkID(netNs, interfaceInfo, nwCfg)
nwInfo, _ := plugin.nm.GetNetworkInfo(networkID)
return nwInfo
}
// CNI implementation
// https://github.com/containernetworking/cni/blob/master/SPEC.md
// Add handles CNI add commands.
func (plugin *NetPlugin) Add(args *cniSkel.CmdArgs) error {
var (
ipamAddResult IPAMAddResult
azIpamResult *cniTypesCurr.Result
enableInfraVnet bool
enableSnatForDNS bool
k8sPodName string
cniMetric telemetry.AIMetric
epInfos []*network.EndpointInfo
)
startTime := time.Now()
logger.Info("Processing ADD command",
zap.String("containerId", args.ContainerID),
zap.String("netNS", args.Netns),
zap.String("ifName", args.IfName),
zap.Any("args", args.Args),
zap.String("path", args.Path),
zap.ByteString("stdinData", args.StdinData))
sendEvent(plugin, fmt.Sprintf("[cni-net] Processing ADD command with args {ContainerID:%v Netns:%v IfName:%v Args:%v Path:%v StdinData:%s}.",
args.ContainerID, args.Netns, args.IfName, args.Args, args.Path, args.StdinData))
// Parse network configuration from stdin.
nwCfg, err := cni.ParseNetworkConfig(args.StdinData)
if err != nil {
err = plugin.Errorf("Failed to parse network configuration: %v.", err)
return err
}
if argErr := plugin.validateArgs(args, nwCfg); argErr != nil {
err = argErr
return err
}
iptables.DisableIPTableLock = nwCfg.DisableIPTableLock
plugin.setCNIReportDetails(nwCfg, CNI_ADD, "")
defer func() {
operationTimeMs := time.Since(startTime).Milliseconds()
cniMetric.Metric = aitelemetry.Metric{
Name: telemetry.CNIAddTimeMetricStr,
Value: float64(operationTimeMs),
AppVersion: plugin.Version,
CustomDimensions: make(map[string]string),
}
SetCustomDimensions(&cniMetric, nwCfg, err)
telemetry.SendCNIMetric(&cniMetric, plugin.tb)
// Add Interfaces to result.
// previously we had a default interface info to select which interface info was the one to be returned from cni add
cniResult := &cniTypesCurr.Result{}
for key := range ipamAddResult.interfaceInfo {
// now we have to infer which interface info should be returned
// we assume that we want to return the infra nic always, and if that is not found, return any one of the secondary interfaces
// if there is an infra nic + secondary, we will always return the infra nic (linux swift v2)
cniResult = convertInterfaceInfoToCniResult(ipamAddResult.interfaceInfo[key], args.IfName)
if ipamAddResult.interfaceInfo[key].NICType == cns.InfraNIC {
break
}
}
// stdout multiple cniResults for containerd to create multiple pods
// containerd receives each cniResult as the stdout and create pod
addSnatInterface(nwCfg, cniResult) //nolint TODO: check whether Linux supports adding secondary snatinterface
// add IB NIC interfaceInfo to cniResult
for _, epInfo := range epInfos {
if epInfo.NICType == cns.BackendNIC {
cniResult.Interfaces = append(cniResult.Interfaces, &cniTypesCurr.Interface{
Name: epInfo.MasterIfName,
Mac: epInfo.MacAddress.String(),
PciID: epInfo.PnPID,
})
}
}
// Convert result to the requested CNI version.
res, vererr := cniResult.GetAsVersion(nwCfg.CNIVersion)
if vererr != nil {
logger.Error("GetAsVersion failed", zap.Error(vererr))
plugin.Error(vererr) //nolint
}
if err == nil && res != nil {
// Output the result to stdout.
res.Print()
}
logger.Info("ADD command completed for",
zap.String("pod", k8sPodName),
zap.Any("IPs", cniResult.IPs),
zap.Error(log.NewErrorWithoutStackTrace(err)))
}()
ipamAddResult = IPAMAddResult{interfaceInfo: make(map[string]network.InterfaceInfo)}
// Parse Pod arguments.
k8sPodName, k8sNamespace, err := plugin.getPodInfo(args.Args)
if err != nil {
return err
}
plugin.report.ContainerName = k8sPodName + ":" + k8sNamespace
k8sContainerID := args.ContainerID
if len(k8sContainerID) == 0 {
errMsg := "Container ID not specified in CNI Args"
logger.Error(errMsg)
return plugin.Errorf(errMsg)
}
k8sIfName := args.IfName
if len(k8sIfName) == 0 {
errMsg := "Interfacename not specified in CNI Args"
logger.Error(errMsg)
return plugin.Errorf(errMsg)
}
platformInit(nwCfg)
if nwCfg.ExecutionMode == string(util.Baremetal) {
var res *nnscontracts.ConfigureContainerNetworkingResponse
logger.Info("Baremetal mode. Calling vnet agent for ADD")
res, err = plugin.nnsClient.AddContainerNetworking(context.Background(), k8sPodName, args.Netns)
if err == nil {
ipamAddResult.interfaceInfo[string(cns.InfraNIC)] = network.InterfaceInfo{
IPConfigs: convertNnsToIPConfigs(res, args.IfName, k8sPodName, "AddContainerNetworking"),
NICType: cns.InfraNIC,
}
}
return err
}
for _, ns := range nwCfg.PodNamespaceForDualNetwork {
if k8sNamespace == ns {
logger.Info("Enable infravnet for pod",
zap.String("pod", k8sPodName),
zap.String("namespace", k8sNamespace))
enableInfraVnet = true
break
}
}
cnsClient, err := cnscli.New(nwCfg.CNSUrl, defaultRequestTimeout)
if err != nil {
return fmt.Errorf("failed to create cns client with error: %w", err)
}
options := make(map[string]any)
ipamAddConfig := IPAMAddConfig{nwCfg: nwCfg, args: args, options: options}
if nwCfg.MultiTenancy {
// triggered only in swift v1 multitenancy
// dual nic multitenancy -> two interface infos
// multitenancy (swift v1) -> one interface info
plugin.report.Context = "AzureCNIMultitenancy"
plugin.multitenancyClient.Init(cnsClient, AzureNetIOShim{})
// Temporary if block to determining whether we disable SNAT on host (for multi-tenant scenario only)
if enableSnatForDNS, nwCfg.EnableSnatOnHost, err = plugin.multitenancyClient.DetermineSnatFeatureOnHost(
snatConfigFileName, nmAgentSupportedApisURL); err != nil {
return fmt.Errorf("%w", err)
}
ipamAddResult, err = plugin.multitenancyClient.GetAllNetworkContainers(context.TODO(), nwCfg, k8sPodName, k8sNamespace, args.IfName)
if err != nil {
err = fmt.Errorf("GetAllNetworkContainers failed for podname %s namespace %s. error: %w", k8sPodName, k8sNamespace, err)
logger.Error("GetAllNetworkContainers failed",
zap.String("pod", k8sPodName),
zap.String("namespace", k8sNamespace),
zap.Error(err))
return err
}
// dual nic when we get multiple interface infos back (multitenancy does not necessarily have multiple if infos)
if len(ipamAddResult.interfaceInfo) > 1 && !plugin.isDualNicFeatureSupported(args.Netns) {
errMsg := fmt.Sprintf("received multiple NC results %+v from CNS while dualnic feature is not supported", ipamAddResult.interfaceInfo)
logger.Error("received multiple NC results from CNS while dualnic feature is not supported",
zap.Any("results", ipamAddResult.interfaceInfo))
return plugin.Errorf(errMsg)
}
} else {
// when nwcfg.multitenancy (use multitenancy flag for swift v1 only) is false
if plugin.ipamInvoker == nil {
switch nwCfg.IPAM.Type {
case network.AzureCNS:
plugin.ipamInvoker = NewCNSInvoker(k8sPodName, k8sNamespace, cnsClient, util.ExecutionMode(nwCfg.ExecutionMode), util.IpamMode(nwCfg.IPAM.Mode))
default:
// legacy
nwInfo := plugin.getNetworkInfo(args.Netns, nil, nwCfg)
plugin.ipamInvoker = NewAzureIpamInvoker(plugin, &nwInfo)
}
}
ipamAddResult, err = plugin.addIpamInvoker(ipamAddConfig)
if err != nil {
return fmt.Errorf("IPAM Invoker Add failed with error: %w", err)
}
// TODO: This proably needs to be changed as we return all interfaces...
// sendEvent(plugin, fmt.Sprintf("Allocated IPAddress from ipam DefaultInterface: %+v, SecondaryInterfaces: %+v", ipamAddResult.interfaceInfo[ifIndex], ipamAddResult.interfaceInfo))
}
policies := cni.GetPoliciesFromNwCfg(nwCfg.AdditionalArgs)
// moved to addIpamInvoker
// sendEvent(plugin, fmt.Sprintf("Allocated IPAddress from ipam interface: %+v", ipamAddResult.PrettyString()))
defer func() { //nolint:gocritic
if err != nil {
// for swift v1 multi-tenancies scenario, CNI is not supposed to invoke CNS for cleaning Ips
if !nwCfg.MultiTenancy {
for _, ifInfo := range ipamAddResult.interfaceInfo {
// This used to only be called for infraNIC, test if this breaks scenarios
// If it does then will have to search for infraNIC
if ifInfo.NICType == cns.InfraNIC {
plugin.cleanupAllocationOnError(ifInfo.IPConfigs, nwCfg, args, options)
}
}
}
}
}()
infraSeen := false
endpointIndex := 1
for key := range ipamAddResult.interfaceInfo {
ifInfo := ipamAddResult.interfaceInfo[key]
logger.Info("Processing interfaceInfo:", zap.Any("ifInfo", ifInfo))
natInfo := getNATInfo(nwCfg, options[network.SNATIPKey], enableSnatForDNS)
networkID, _ := plugin.getNetworkID(args.Netns, &ifInfo, nwCfg)
createEpInfoOpt := createEpInfoOpt{
nwCfg: nwCfg,
cnsNetworkConfig: ifInfo.NCResponse,
ipamAddResult: ipamAddResult,
azIpamResult: azIpamResult,
args: args,
policies: policies,
k8sPodName: k8sPodName,
k8sNamespace: k8sNamespace,
enableInfraVnet: enableInfraVnet,
enableSnatForDNS: enableSnatForDNS,
natInfo: natInfo,
networkID: networkID,
ifInfo: &ifInfo,
ipamAddConfig: &ipamAddConfig,
ipv6Enabled: ipamAddResult.ipv6Enabled,
infraSeen: &infraSeen,
endpointIndex: endpointIndex,
}
var epInfo *network.EndpointInfo
epInfo, err = plugin.createEpInfo(&createEpInfoOpt)
if err != nil {
return err
}
epInfos = append(epInfos, epInfo)
// TODO: should this statement be based on the current iteration instead of the constant ifIndex?
// TODO figure out where to put telemetry: sendEvent(plugin, fmt.Sprintf("CNI ADD succeeded: IP:%+v, VlanID: %v, podname %v, namespace %v numendpoints:%d",
// ipamAddResult.interfaceInfo[ifIndex].IPConfigs, epInfo.Data[network.VlanIDKey], k8sPodName, k8sNamespace, plugin.nm.GetNumberOfEndpoints("", nwCfg.Name)))
endpointIndex++
}
cnsclient, err := cnscli.New(nwCfg.CNSUrl, defaultRequestTimeout)
if err != nil {
return errors.Wrap(err, "failed to create cns client")
}
defer func() {
if err != nil {
// Delete all endpoints
for _, epInfo := range epInfos {
deleteErr := plugin.nm.DeleteEndpoint(epInfo.NetworkID, epInfo.EndpointID, epInfo)
if deleteErr != nil {
// we already do not return an error when the endpoint is not found, so deleteErr is a real error
logger.Error("Could not delete endpoint after detecting add failure", zap.String("epInfo", epInfo.PrettyString()), zap.Error(deleteErr))
return
}
}
// Rely on cleanupAllocationOnError declared above to delete ips
// Delete state in disk here
delErr := plugin.nm.DeleteState(epInfos)
if delErr != nil {
logger.Error("Could not delete state after detecting add failure", zap.Error(delErr))
return
}
}
}()
err = plugin.nm.EndpointCreate(cnsclient, epInfos)
if err != nil {
return errors.Wrap(err, "failed to create endpoint") // behavior can change if you don't assign to err prior to returning
}
// telemetry added
sendEvent(plugin, fmt.Sprintf("CNI ADD Process succeeded for interfaces: %v", ipamAddResult.PrettyString()))
return nil
}
func (plugin *NetPlugin) findMasterInterface(opt *createEpInfoOpt) string {
switch opt.ifInfo.NICType {
case cns.InfraNIC:
return plugin.findMasterInterfaceBySubnet(opt.ipamAddConfig.nwCfg, &opt.ifInfo.HostSubnetPrefix)
case cns.NodeNetworkInterfaceFrontendNIC:
return plugin.findInterfaceByMAC(opt.ifInfo.MacAddress.String())
case cns.BackendNIC:
// if windows swiftv2 has right network drivers, there will be an NDIS interface while the VFs are mounted
// when the VF is dismounted, this interface will go away
// return an unique interface name to containerd
return ibInterfacePrefix + strconv.Itoa(opt.endpointIndex)
default:
return ""
}
}
type createEpInfoOpt struct {
nwCfg *cni.NetworkConfig
cnsNetworkConfig *cns.GetNetworkContainerResponse
ipamAddResult IPAMAddResult
azIpamResult *cniTypesCurr.Result
args *cniSkel.CmdArgs
policies []policy.Policy
k8sPodName string
k8sNamespace string
enableInfraVnet bool
enableSnatForDNS bool
natInfo []policy.NATInfo
networkID string
ifInfo *network.InterfaceInfo
ipamAddConfig *IPAMAddConfig
ipv6Enabled bool
infraSeen *bool // Only the first infra gets args.ifName, even if the second infra is on a different network
endpointIndex int
}
func (plugin *NetPlugin) createEpInfo(opt *createEpInfoOpt) (*network.EndpointInfo, error) { // you can modify to pass in whatever else you need
// ensure we can find the master interface
opt.ifInfo.HostSubnetPrefix.IP = opt.ifInfo.HostSubnetPrefix.IP.Mask(opt.ifInfo.HostSubnetPrefix.Mask)
opt.ipamAddConfig.nwCfg.IPAM.Subnet = opt.ifInfo.HostSubnetPrefix.String()
// populate endpoint info section
masterIfName := plugin.findMasterInterface(opt)
if masterIfName == "" {
err := plugin.Errorf("Failed to find the master interface")
return nil, err
}
networkPolicies := opt.policies // save network policies before we modify the slice pointer for ep policies
// populate endpoint info
epDNSInfo, err := getEndpointDNSSettings(opt.nwCfg, opt.ifInfo.DNS, opt.k8sNamespace) // Probably won't panic if given bad values
if err != nil {
err = plugin.Errorf("Failed to getEndpointDNSSettings: %v", err)
return nil, err
}
vethName := fmt.Sprintf("%s.%s", opt.k8sNamespace, opt.k8sPodName)
if opt.nwCfg.Mode != OpModeTransparent {
// this mechanism of using only namespace and name is not unique for different incarnations of POD/container.
// IT will result in unpredictable behavior if API server decides to
// reorder DELETE and ADD call for new incarnation of same POD.
vethName = fmt.Sprintf("%s%s%s", opt.networkID, opt.args.ContainerID, opt.args.IfName)
}
// for secondary (Populate addresses)
// initially only for infra nic but now applied to all nic types
addresses := make([]net.IPNet, len(opt.ifInfo.IPConfigs))
for i, ipconfig := range opt.ifInfo.IPConfigs {
addresses[i] = ipconfig.Address
}
// generate endpoint info
var endpointID, ifName string
if opt.ifInfo.NICType == cns.InfraNIC && !*opt.infraSeen {
// so we do not break existing scenarios, only the first infra gets the original endpoint id generation
ifName = opt.args.IfName
endpointID = plugin.nm.GetEndpointID(opt.args.ContainerID, ifName)
*opt.infraSeen = true
} else {
ifName = "eth" + strconv.Itoa(opt.endpointIndex)
endpointID = plugin.nm.GetEndpointID(opt.args.ContainerID, ifName)
}
endpointInfo := network.EndpointInfo{
NetworkID: opt.networkID,
Mode: opt.ipamAddConfig.nwCfg.Mode,
MasterIfName: masterIfName,
AdapterName: opt.ipamAddConfig.nwCfg.AdapterName,
BridgeName: opt.ipamAddConfig.nwCfg.Bridge,
NetworkPolicies: networkPolicies, // nw and ep policies separated to avoid possible conflicts
NetNs: opt.ipamAddConfig.args.Netns,
Options: opt.ipamAddConfig.shallowCopyIpamAddConfigOptions(),
DisableHairpinOnHostInterface: opt.ipamAddConfig.nwCfg.DisableHairpinOnHostInterface,
IsIPv6Enabled: opt.ipv6Enabled, // present infra only
EndpointID: endpointID,
ContainerID: opt.args.ContainerID,
NetNsPath: opt.args.Netns, // probably same value as epInfo.NetNs
IfName: ifName,
Data: make(map[string]interface{}),
EndpointDNS: epDNSInfo,
// endpoint policies are populated later
IPsToRouteViaHost: opt.nwCfg.IPsToRouteViaHost,
EnableSnatOnHost: opt.nwCfg.EnableSnatOnHost,
EnableMultiTenancy: opt.nwCfg.MultiTenancy,
EnableInfraVnet: opt.enableInfraVnet,
EnableSnatForDns: opt.enableSnatForDNS,
PODName: opt.k8sPodName,
PODNameSpace: opt.k8sNamespace,
SkipHotAttachEp: false, // Hot attach at the time of endpoint creation
IPV6Mode: opt.nwCfg.IPV6Mode,
VnetCidrs: opt.nwCfg.VnetCidrs,
ServiceCidrs: opt.nwCfg.ServiceCidrs,
NATInfo: opt.natInfo,
NICType: opt.ifInfo.NICType,
SkipDefaultRoutes: opt.ifInfo.SkipDefaultRoutes,
Routes: opt.ifInfo.Routes,
// added the following for delegated vm nic
IPAddresses: addresses,
MacAddress: opt.ifInfo.MacAddress,
// the following is used for creating an external interface if we can't find an existing network
HostSubnetPrefix: opt.ifInfo.HostSubnetPrefix.String(),
PnPID: opt.ifInfo.PnPID,
}
if err = addSubnetToEndpointInfo(*opt.ifInfo, &endpointInfo); err != nil {
logger.Info("Failed to add subnets to endpointInfo", zap.Error(err))
return nil, err
}
setNetworkOptions(opt.ifInfo.NCResponse, &endpointInfo)
// update endpoint policies
policyArgs := PolicyArgs{
subnetInfos: endpointInfo.Subnets, // getEndpointPolicies requires nwInfo.Subnets only (checked)
nwCfg: opt.nwCfg,
ipconfigs: opt.ifInfo.IPConfigs,
}
endpointPolicies, err := getEndpointPolicies(policyArgs)
if err != nil {
logger.Error("Failed to get endpoint policies", zap.Error(err))
return nil, err
}
// create endpoint policies by appending to network policies
// the value passed into NetworkPolicies should be unaffected since we reassign here
opt.policies = append(opt.policies, endpointPolicies...)
// appends endpoint policies specific to this interface
opt.policies = append(opt.policies, opt.ifInfo.EndpointPolicies...)
endpointInfo.EndpointPolicies = opt.policies
// add even more endpoint policies
epPolicies, err := getPoliciesFromRuntimeCfg(opt.nwCfg, opt.ipamAddResult.ipv6Enabled) // not specific to delegated or infra
if err != nil {
logger.Error("failed to get policies from runtime configurations", zap.Error(err))
return nil, plugin.Errorf(err.Error())
}
endpointInfo.EndpointPolicies = append(endpointInfo.EndpointPolicies, epPolicies...)
if opt.ipamAddResult.ipv6Enabled { // not specific to this particular interface
endpointInfo.IPV6Mode = string(util.IpamMode(opt.nwCfg.IPAM.Mode)) // TODO: check IPV6Mode field can be deprecated and can we add IsIPv6Enabled flag for generic working
}
if opt.azIpamResult != nil && opt.azIpamResult.IPs != nil {
endpointInfo.InfraVnetIP = opt.azIpamResult.IPs[0].Address
}
if opt.nwCfg.MultiTenancy {
// previously only infra nic was passed into this function but now all nics are passed in (possibly breaks swift v2)
plugin.multitenancyClient.SetupRoutingForMultitenancy(opt.nwCfg, opt.cnsNetworkConfig, opt.azIpamResult, &endpointInfo, opt.ifInfo)
}
setEndpointOptions(opt.cnsNetworkConfig, &endpointInfo, vethName)
logger.Info("Generated endpoint info from fields", zap.String("epInfo", endpointInfo.PrettyString()))
// now our ep info should have the full combined information from both the network and endpoint structs
return &endpointInfo, nil
}
// cleanup allocated ipv4 and ipv6 addresses if they exist
func (plugin *NetPlugin) cleanupAllocationOnError(
result []*network.IPConfig,
nwCfg *cni.NetworkConfig,
args *cniSkel.CmdArgs,
options map[string]interface{},
) {
if result != nil {
for i := 0; i < len(result); i++ {
if er := plugin.ipamInvoker.Delete(&result[i].Address, nwCfg, args, options); er != nil {
logger.Error("Failed to cleanup ip allocation on failure", zap.Error(er))
}
}
}
}
// construct network info with ipv4/ipv6 subnets (updates subnets field)
func addSubnetToEndpointInfo(interfaceInfo network.InterfaceInfo, nwInfo *network.EndpointInfo) error {
for _, ipConfig := range interfaceInfo.IPConfigs {
ip, podSubnetPrefix, err := net.ParseCIDR(ipConfig.Address.String())
if err != nil {
return fmt.Errorf("Failed to ParseCIDR for pod subnet prefix: %w", err)
}
subnet := network.SubnetInfo{
Family: platform.AfINET,
Prefix: *podSubnetPrefix,
Gateway: ipConfig.Gateway,
}
if ip.To4() == nil {
subnet.Family = platform.AfINET6
}
nwInfo.Subnets = append(nwInfo.Subnets, subnet)
}
return nil
}
// Get handles CNI Get commands.
func (plugin *NetPlugin) Get(args *cniSkel.CmdArgs) error {
var (
result cniTypesCurr.Result
err error
nwCfg *cni.NetworkConfig
epInfo *network.EndpointInfo
iface *cniTypesCurr.Interface
networkID string
)
logger.Info("Processing GET command",
zap.String("container", args.ContainerID),
zap.String("netns", args.Netns),
zap.String("ifname", args.IfName),
zap.String("args", args.Args),
zap.String("path", args.Path))
defer func() {
// Add Interfaces to result.
iface = &cniTypesCurr.Interface{
Name: args.IfName,
}
result.Interfaces = append(result.Interfaces, iface)
// Convert result to the requested CNI version.
res, vererr := result.GetAsVersion(nwCfg.CNIVersion)
if vererr != nil {
logger.Error("GetAsVersion failed", zap.Error(vererr))
plugin.Error(vererr)
}
if err == nil && res != nil {
// Output the result to stdout.
res.Print()
}
logger.Info("GET command completed", zap.Any("result", result),
zap.Error(log.NewErrorWithoutStackTrace(err)))
}()
// Parse network configuration from stdin.
if nwCfg, err = cni.ParseNetworkConfig(args.StdinData); err != nil {
err = plugin.Errorf("Failed to parse network configuration: %v.", err)
return err
}
logger.Info("Read network configuration", zap.Any("config", nwCfg))
if argErr := plugin.validateArgs(args, nwCfg); argErr != nil {
err = argErr
return err
}
iptables.DisableIPTableLock = nwCfg.DisableIPTableLock
// Initialize values from network config.
if networkID, err = plugin.getNetworkName(args.Netns, nil, nwCfg); err != nil {
// TODO: Ideally we should return from here only.
logger.Error("Failed to extract network name from network config",
zap.Error(err))
}
endpointID := GetEndpointID(args)
// Query the network.
if _, err = plugin.nm.GetNetworkInfo(networkID); err != nil {
logger.Error("Failed to query network", zap.Error(err))
return err
}
// Query the endpoint.
if epInfo, err = plugin.nm.GetEndpointInfo(networkID, endpointID); err != nil {
logger.Error("Failed to query endpoint", zap.Error(err))
return err
}
for _, ipAddresses := range epInfo.IPAddresses {
ipConfig := &cniTypesCurr.IPConfig{
Interface: &epInfo.IfIndex,
Address: ipAddresses,
}
if epInfo.Gateways != nil {
ipConfig.Gateway = epInfo.Gateways[0]
}
result.IPs = append(result.IPs, ipConfig)
}
for _, route := range epInfo.Routes {
result.Routes = append(result.Routes, &cniTypes.Route{Dst: route.Dst, GW: route.Gw})
}
result.DNS.Nameservers = epInfo.EndpointDNS.Servers
result.DNS.Domain = epInfo.EndpointDNS.Suffix
return nil
}
// Delete handles CNI delete commands.
func (plugin *NetPlugin) Delete(args *cniSkel.CmdArgs) error {
var (
err error
nwCfg *cni.NetworkConfig
k8sPodName string
k8sNamespace string
networkID string
nwInfo network.EndpointInfo
cniMetric telemetry.AIMetric
)
startTime := time.Now()
logger.Info("Processing DEL command",
zap.String("containerId", args.ContainerID),
zap.String("netNS", args.Netns),
zap.String("ifName", args.IfName),
zap.Any("args", args.Args),
zap.String("path", args.Path),
zap.ByteString("stdinData", args.StdinData))
sendEvent(plugin, fmt.Sprintf("[cni-net] Processing DEL command with args {ContainerID:%v Netns:%v IfName:%v Args:%v Path:%v, StdinData:%s}.",
args.ContainerID, args.Netns, args.IfName, args.Args, args.Path, args.StdinData))
defer func() {
logger.Info("DEL command completed",
zap.String("pod", k8sPodName),
zap.Error(log.NewErrorWithoutStackTrace(err)))
}()
// Parse network configuration from stdin.
if nwCfg, err = cni.ParseNetworkConfig(args.StdinData); err != nil {
err = plugin.Errorf("[cni-net] Failed to parse network configuration: %v", err)
return err
}
if argErr := plugin.validateArgs(args, nwCfg); argErr != nil {
err = argErr
return err
}
// Parse Pod arguments.
if k8sPodName, k8sNamespace, err = plugin.getPodInfo(args.Args); err != nil {
logger.Error("Failed to get POD info", zap.Error(err))
}
plugin.setCNIReportDetails(nwCfg, CNI_DEL, "")
plugin.report.ContainerName = k8sPodName + ":" + k8sNamespace
iptables.DisableIPTableLock = nwCfg.DisableIPTableLock
sendMetricFunc := func() {
operationTimeMs := time.Since(startTime).Milliseconds()
cniMetric.Metric = aitelemetry.Metric{
Name: telemetry.CNIDelTimeMetricStr,
Value: float64(operationTimeMs),
AppVersion: plugin.Version,
CustomDimensions: make(map[string]string),
}
SetCustomDimensions(&cniMetric, nwCfg, err)
telemetry.SendCNIMetric(&cniMetric, plugin.tb)
}
platformInit(nwCfg)
logger.Info("Execution mode", zap.String("mode", nwCfg.ExecutionMode))
if nwCfg.ExecutionMode == string(util.Baremetal) {
// schedule send metric before attempting delete
defer sendMetricFunc()
_, err = plugin.nnsClient.DeleteContainerNetworking(context.Background(), k8sPodName, args.Netns)
if err != nil {
return fmt.Errorf("nnsClient.DeleteContainerNetworking failed with err %w", err)
}
}
if plugin.ipamInvoker == nil {
switch nwCfg.IPAM.Type {
case network.AzureCNS:
cnsClient, cnsErr := cnscli.New("", defaultRequestTimeout)
if cnsErr != nil {
logger.Error("failed to create cns client", zap.Error(cnsErr))
return errors.Wrap(cnsErr, "failed to create cns client")
}
plugin.ipamInvoker = NewCNSInvoker(k8sPodName, k8sNamespace, cnsClient, util.ExecutionMode(nwCfg.ExecutionMode), util.IpamMode(nwCfg.IPAM.Mode))
default:
// nwInfo gets populated later in the function
plugin.ipamInvoker = NewAzureIpamInvoker(plugin, &nwInfo)
}
}
// Loop through all the networks that are created for the given Netns. In case of multi-nic scenario ( currently supported
// scenario is dual-nic ), single container may have endpoints created in multiple networks. As all the endpoints are
// deleted, getNetworkName will return error of the type NetworkNotFoundError which will result in nil error as compliance
// with CNI SPEC as mentioned below.
// We get the network id and nw info here to preserve existing behavior
networkID, err = plugin.getNetworkID(args.Netns, nil, nwCfg)
if nwInfo, err = plugin.nm.GetNetworkInfo(networkID); err != nil {
if !nwCfg.MultiTenancy {
logger.Error("Failed to query network",
zap.String("network", networkID),
zap.Error(err))
// Log the error if the network is not found.
// if cni hits this, mostly state file would be missing and it can be reboot scenario where
// container runtime tries to delete and create pods which existed before reboot.
// this condition will not apply to stateless CNI since the network struct will be crated on each call
err = nil
}
}
// Initialize values from network config.
if err != nil {
// if swift v1 multitenancy and we got an error retrieving the nwInfo
// If error is not found error, then we ignore it, to comply with CNI SPEC.
if network.IsNetworkNotFoundError(err) {
err = nil
return err
}
logger.Error("Failed to extract network name from network config", zap.Error(err))
err = plugin.Errorf("Failed to extract network name from network config. error: %v", err)
return err
}
logger.Info("Retrieved network info, populating endpoint infos with container id", zap.String("containerID", args.ContainerID))
var epInfos []*network.EndpointInfo
if plugin.nm.IsStatelessCNIMode() {
// network ID is passed in and used only for migration
// otherwise, in stateless, we don't need the network id for deletion
epInfos, err = plugin.nm.GetEndpointState(networkID, args.ContainerID)
// if stateless CNI fail to get the endpoint from CNS for any reason other than Endpoint Not found
if err != nil {
if errors.Is(err, network.ErrConnectionFailure) {
logger.Info("failed to connect to CNS", zap.String("containerID", args.ContainerID), zap.Error(err))
addErr := fsnotify.AddFile(args.ContainerID, args.ContainerID, watcherPath)
logger.Info("add containerid file for Asynch delete", zap.String("containerID", args.ContainerID), zap.Error(addErr))
if addErr != nil {
logger.Error("failed to add file to watcher", zap.String("containerID", args.ContainerID), zap.Error(addErr))
return errors.Wrap(addErr, fmt.Sprintf("failed to add file to watcher with containerID %s", args.ContainerID))
}
return nil
}
if errors.Is(err, network.ErrEndpointStateNotFound) {
logger.Info("Endpoint Not found", zap.String("containerID", args.ContainerID), zap.Error(err))
return nil
}
logger.Error("Get Endpoint State API returned error", zap.String("containerID", args.ContainerID), zap.Error(err))
return plugin.RetriableError(fmt.Errorf("failed to delete endpoint: %w", err))
}
} else {
epInfos = plugin.nm.GetEndpointInfosFromContainerID(args.ContainerID)
}
// for when the endpoint is not created, but the ips are already allocated (only works if single network, single infra)
// this block is not applied to stateless CNI
if len(epInfos) == 0 {
endpointID := plugin.nm.GetEndpointID(args.ContainerID, args.IfName)
if !nwCfg.MultiTenancy {
logger.Error("Failed to query endpoint",
zap.String("endpoint", endpointID),
zap.Error(err))
logger.Error("Release ip by ContainerID (endpoint not found)",
zap.String("containerID", args.ContainerID))
sendEvent(plugin, fmt.Sprintf("Release ip by ContainerID (endpoint not found):%v", args.ContainerID))
if err = plugin.ipamInvoker.Delete(nil, nwCfg, args, nwInfo.Options); err != nil {
return plugin.RetriableError(fmt.Errorf("failed to release address(no endpoint): %w", err))
}
}
// Log the error but return success if the endpoint being deleted is not found.
err = nil
return err
}
logger.Info("Deleting the endpoints", zap.Any("endpointInfos", epInfos))
// populate ep infos here in loop if necessary
// delete endpoints
for _, epInfo := range epInfos {
// in stateless, network id is not populated in epInfo, but in stateful cni, it is (nw id is used in stateful)
if err = plugin.nm.DeleteEndpoint(epInfo.NetworkID, epInfo.EndpointID, epInfo); err != nil {
// An error will not be returned if the endpoint is not found
// return a retriable error so the container runtime will retry this DEL later
// the implementation of this function returns nil if the endpoint doens't exist, so
// we don't have to check that here
return plugin.RetriableError(fmt.Errorf("failed to delete endpoint: %w", err))
}
}
logger.Info("Deleting the endpoints from the ipam")
// delete endpoint state in cns and in statefile
for _, epInfo := range epInfos {
// schedule send metric before attempting delete
defer sendMetricFunc() //nolint:gocritic
logger.Info("Deleting endpoint",
zap.String("endpointID", epInfo.EndpointID))
sendEvent(plugin, fmt.Sprintf("Deleting endpoint:%v", epInfo.EndpointID))
if !nwCfg.MultiTenancy && (epInfo.NICType == cns.InfraNIC || epInfo.NICType == "") {
// Delegated/secondary nic ips are statically allocated so we don't need to release
// Call into IPAM plugin to release the endpoint's addresses.
for i := range epInfo.IPAddresses {
logger.Info("Release ip", zap.String("ip", epInfo.IPAddresses[i].IP.String()))
sendEvent(plugin, fmt.Sprintf("Release ip:%s", epInfo.IPAddresses[i].IP.String()))
err = plugin.ipamInvoker.Delete(&epInfo.IPAddresses[i], nwCfg, args, nwInfo.Options)
if err != nil {
return plugin.RetriableError(fmt.Errorf("failed to release address: %w", err))
}
}
} else if epInfo.EnableInfraVnet { // remove in future PR
nwCfg.IPAM.Subnet = nwInfo.Subnets[0].Prefix.String()
nwCfg.IPAM.Address = epInfo.InfraVnetIP.IP.String()
err = plugin.ipamInvoker.Delete(nil, nwCfg, args, nwInfo.Options)
if err != nil {
return plugin.RetriableError(fmt.Errorf("failed to release address: %w", err))
}
}
}
logger.Info("Deleting the state from the cni statefile")
err = plugin.nm.DeleteState(epInfos)
if err != nil {
return plugin.RetriableError(fmt.Errorf("failed to save state: %w", err))
}
sendEvent(plugin, fmt.Sprintf("CNI DEL succeeded : Released ip %+v podname %v namespace %v", nwCfg.IPAM.Address, k8sPodName, k8sNamespace))
return err
}
// Update handles CNI update commands.
// Update is only supported for multitenancy and to update routes.
func (plugin *NetPlugin) Update(args *cniSkel.CmdArgs) error {
var (
result *cniTypesCurr.Result
err error
nwCfg *cni.NetworkConfig
existingEpInfo *network.EndpointInfo
podCfg *cni.K8SPodEnvArgs
orchestratorContext []byte
targetNetworkConfig *cns.GetNetworkContainerResponse
cniMetric telemetry.AIMetric
)
startTime := time.Now()
logger.Info("Processing UPDATE command",
zap.String("netns", args.Netns),
zap.String("args", args.Args),
zap.String("path", args.Path))
// Parse network configuration from stdin.
if nwCfg, err = cni.ParseNetworkConfig(args.StdinData); err != nil {
err = plugin.Errorf("Failed to parse network configuration: %v.", err)
return err
}
if argErr := plugin.validateArgs(args, nwCfg); argErr != nil {
err = argErr
return err
}
logger.Info("Read network configuration", zap.Any("config", nwCfg))
iptables.DisableIPTableLock = nwCfg.DisableIPTableLock
plugin.setCNIReportDetails(nwCfg, CNI_UPDATE, "")
defer func() {
operationTimeMs := time.Since(startTime).Milliseconds()
cniMetric.Metric = aitelemetry.Metric{
Name: telemetry.CNIUpdateTimeMetricStr,
Value: float64(operationTimeMs),
AppVersion: plugin.Version,
CustomDimensions: make(map[string]string),
}
SetCustomDimensions(&cniMetric, nwCfg, err)
telemetry.SendCNIMetric(&cniMetric, plugin.tb)
if result == nil {
result = &cniTypesCurr.Result{}
}
// Convert result to the requested CNI version.
res, vererr := result.GetAsVersion(nwCfg.CNIVersion)
if vererr != nil {
logger.Error("GetAsVersion failed", zap.Error(vererr))
plugin.Error(vererr) //nolint
}
if err == nil && res != nil {
// Output the result to stdout.
res.Print()
}
logger.Info("UPDATE command completed",
zap.Any("result", result),
zap.Error(log.NewErrorWithoutStackTrace(err)))
}()
// Parse Pod arguments.
if podCfg, err = cni.ParseCniArgs(args.Args); err != nil {
logger.Error("Error while parsing CNI Args during UPDATE",
zap.Error(err))
return err
}
k8sNamespace := string(podCfg.K8S_POD_NAMESPACE)
if len(k8sNamespace) == 0 {
errMsg := "Required parameter Pod Namespace not specified in CNI Args during UPDATE"
logger.Error(errMsg)
return plugin.Errorf(errMsg)
}
k8sPodName := string(podCfg.K8S_POD_NAME)
if len(k8sPodName) == 0 {
errMsg := "Required parameter Pod Name not specified in CNI Args during UPDATE"
logger.Error(errMsg)
return plugin.Errorf(errMsg)
}
// Initialize values from network config.
networkID := nwCfg.Name
// Query the network.
if _, err = plugin.nm.GetNetworkInfo(networkID); err != nil {
errMsg := fmt.Sprintf("Failed to query network during CNI UPDATE: %v", err)
logger.Error(errMsg)
return plugin.Errorf(errMsg)
}
// Query the existing endpoint since this is an update.
// Right now, we do not support updating pods that have multiple endpoints.
existingEpInfo, err = plugin.nm.GetEndpointInfoBasedOnPODDetails(networkID, k8sPodName, k8sNamespace, nwCfg.EnableExactMatchForPodName)
if err != nil {
plugin.Errorf("Failed to retrieve target endpoint for CNI UPDATE [name=%v, namespace=%v]: %v", k8sPodName, k8sNamespace, err)
return err
}
logger.Info("Retrieved existing endpoint from state that may get update",
zap.Any("info", existingEpInfo))
// now query CNS to get the target routes that should be there in the networknamespace (as a result of update)
logger.Info("Going to collect target routes from CNS",
zap.String("pod", k8sPodName),
zap.String("namespace", k8sNamespace))
// create struct with info for target POD
podInfo := cns.KubernetesPodInfo{
PodName: k8sPodName,
PodNamespace: k8sNamespace,
}
if orchestratorContext, err = json.Marshal(podInfo); err != nil {
logger.Error("Marshalling KubernetesPodInfo failed",
zap.Error(err))
return plugin.Errorf(err.Error())
}
cnsclient, err := cnscli.New(nwCfg.CNSUrl, defaultRequestTimeout)
if err != nil {
logger.Error("failed to initialized cns client",
zap.String("url", nwCfg.CNSUrl),
zap.String("error", err.Error()))
return plugin.Errorf(err.Error())
}
if targetNetworkConfig, err = cnsclient.GetNetworkContainer(context.TODO(), orchestratorContext); err != nil {
logger.Info("GetNetworkContainer failed",
zap.Error(err))
return plugin.Errorf(err.Error())
}
logger.Info("Network config received from cns",
zap.String("pod", k8sPodName),
zap.String("namespace", k8sNamespace),
zap.Any("config", targetNetworkConfig))
targetEpInfo := &network.EndpointInfo{}
// get the target routes that should replace existingEpInfo.Routes inside the network namespace
if targetNetworkConfig.Routes != nil && len(targetNetworkConfig.Routes) > 0 {
for _, route := range targetNetworkConfig.Routes {
logger.Info("Adding route from routes from targetNetworkConfig to targetEpInfo", zap.Any("route", route))
_, dstIPNet, _ := net.ParseCIDR(route.IPAddress)
gwIP := net.ParseIP(route.GatewayIPAddress)
targetEpInfo.Routes = append(targetEpInfo.Routes, network.RouteInfo{Dst: *dstIPNet, Gw: gwIP, DevName: existingEpInfo.IfName})
}
}
logger.Info("Going to collect target routes based on Cnetaddressspace from targetNetworkConfig",
zap.String("pod", k8sPodName),
zap.String("namespace", k8sNamespace))
ipconfig := targetNetworkConfig.IPConfiguration
for _, ipRouteSubnet := range targetNetworkConfig.CnetAddressSpace {
logger.Info("Adding route from cnetAddressspace to targetEpInfo", zap.Any("subnet", ipRouteSubnet))
dstIPNet := net.IPNet{IP: net.ParseIP(ipRouteSubnet.IPAddress), Mask: net.CIDRMask(int(ipRouteSubnet.PrefixLength), 32)}
gwIP := net.ParseIP(ipconfig.GatewayIPAddress)
route := network.RouteInfo{Dst: dstIPNet, Gw: gwIP, DevName: existingEpInfo.IfName}
targetEpInfo.Routes = append(targetEpInfo.Routes, route)
}
logger.Info("Finished collecting new routes in targetEpInfo", zap.Any("route", targetEpInfo.Routes))
logger.Info("Now saving existing infravnetaddress space if needed.")
for _, ns := range nwCfg.PodNamespaceForDualNetwork {
if k8sNamespace == ns {
targetEpInfo.EnableInfraVnet = true
targetEpInfo.InfraVnetAddressSpace = nwCfg.InfraVnetAddressSpace
logger.Info("Saving infravnet address space",
zap.String("space", targetEpInfo.InfraVnetAddressSpace),
zap.String("namespace", existingEpInfo.PODNameSpace),
zap.String("pod", existingEpInfo.PODName))
break
}
}
// Update the endpoint.
logger.Info("Now updating existing endpoint with targetNetworkConfig",
zap.String("endpoint", existingEpInfo.EndpointID),
zap.Any("config", targetNetworkConfig))
if err = plugin.nm.UpdateEndpoint(networkID, existingEpInfo, targetEpInfo); err != nil {
err = plugin.Errorf("Failed to update endpoint: %v", err)
return err
}
msg := fmt.Sprintf("CNI UPDATE succeeded : Updated %+v podname %v namespace %v", targetNetworkConfig, k8sPodName, k8sNamespace)
plugin.setCNIReportDetails(nwCfg, CNI_UPDATE, msg)
return nil
}
func convertNnsToIPConfigs(
netRes *nnscontracts.ConfigureContainerNetworkingResponse,
ifName string,
podName string,
operationName string,
) []*network.IPConfig {
// This function does not add interfaces to CNI result. Reason being CRI (containerD in baremetal case)
// only looks for default interface named "eth0" and this default interface is added in the defer
// method of ADD method
var ipConfigs []*network.IPConfig
if netRes.Interfaces != nil {
for _, ni := range netRes.Interfaces {
for _, ip := range ni.Ipaddresses {
ipAddr := net.ParseIP(ip.Ip)
prefixLength, err := strconv.Atoi(ip.PrefixLength)
if err != nil {
logger.Error("Error parsing prefix length while converting to cni result",
zap.String("prefixLength", ip.PrefixLength),
zap.String("operation", operationName),
zap.String("pod", podName),
zap.Error(err))
continue
}
address := net.IPNet{
IP: ipAddr,
Mask: net.CIDRMask(prefixLength, ipv6FullMask),
}
if ipAddr.To4() != nil {
address.Mask = net.CIDRMask(prefixLength, ipv4FullMask)
}
gateway := net.ParseIP(ip.DefaultGateway)
ipConfigs = append(ipConfigs, &network.IPConfig{
Address: address,
Gateway: gateway,
})
}
}
}
return ipConfigs
}
func convertInterfaceInfoToCniResult(info network.InterfaceInfo, ifName string) *cniTypesCurr.Result {
result := &cniTypesCurr.Result{
Interfaces: []*cniTypesCurr.Interface{
{
Name: ifName,
Mac: info.MacAddress.String(),
},
},
DNS: cniTypes.DNS{
Domain: info.DNS.Suffix,
Nameservers: info.DNS.Servers,
},
}
if len(info.IPConfigs) > 0 {
for _, ipconfig := range info.IPConfigs {
result.IPs = append(result.IPs, &cniTypesCurr.IPConfig{Address: ipconfig.Address, Gateway: ipconfig.Gateway})
}
for i := range info.Routes {
result.Routes = append(result.Routes, &cniTypes.Route{Dst: info.Routes[i].Dst, GW: info.Routes[i].Gw})
}
}
return result
}
func convertCniResultToInterfaceInfo(result *cniTypesCurr.Result) network.InterfaceInfo {
interfaceInfo := network.InterfaceInfo{}
if result != nil {
for _, ipconfig := range result.IPs {
interfaceInfo.IPConfigs = append(interfaceInfo.IPConfigs, &network.IPConfig{Address: ipconfig.Address, Gateway: ipconfig.Gateway})
}
for _, route := range result.Routes {
interfaceInfo.Routes = append(interfaceInfo.Routes, network.RouteInfo{Dst: route.Dst, Gw: route.GW})
}
interfaceInfo.DNS = network.DNSInfo{
Suffix: result.DNS.Domain,
Servers: result.DNS.Nameservers,
}
}
return interfaceInfo
}
func (plugin *NetPlugin) validateArgs(args *cniSkel.CmdArgs, nwCfg *cni.NetworkConfig) error {
if !allowedInput.MatchString(args.ContainerID) || !allowedInput.MatchString(args.IfName) {
return errors.New("invalid args value")
}
if !allowedInput.MatchString(nwCfg.Bridge) {
return errors.New("invalid network config value")
}
return nil
}