agent/app/agent_capability_unix.go (227 lines of code) (raw):

//go:build linux // +build linux // 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 app import ( "context" "fmt" "os/exec" "path/filepath" "strings" "time" "github.com/aws/amazon-ecs-agent/agent/config" "github.com/aws/amazon-ecs-agent/agent/dockerclient" "github.com/aws/amazon-ecs-agent/agent/dockerclient/dockerapi" "github.com/aws/amazon-ecs-agent/agent/ecscni" "github.com/aws/amazon-ecs-agent/agent/taskresource/volume" "github.com/aws/amazon-ecs-agent/agent/utils" "github.com/aws/amazon-ecs-agent/ecs-agent/tmds/utils/netconfig" "github.com/aws/amazon-ecs-agent/ecs-agent/utils/execwrapper" "github.com/aws/aws-sdk-go-v2/service/ecs/types" "github.com/aws/aws-sdk-go/aws" "github.com/cihub/seelog" ) const ( AVX = "avx" AVX2 = "avx2" SSE41 = "sse4_1" SSE42 = "sse4_2" CpuInfoPath = "/proc/cpuinfo" capabilityDepsRootDir = "/managed-agents" modInfoCmd = "modinfo" faultInjectionKernelModules = "sch_netem" ctxTimeoutDuration = 60 * time.Second tcShowCmdString = "tc -j q show dev %s parent 1:1" ) var ( certsDir = filepath.Join(capabilityExecRootDir, capabilityExecCertsRelativePath) capabilityExecRequiredCerts = []string{ "tls-ca-bundle.pem", } capabilityExecRequiredBinaries = []string{ "amazon-ssm-agent", "ssm-agent-worker", "ssm-session-worker", } // top-level folders, /bin, /config, /certs dependencies = map[string][]string{ binDir: []string{}, configDir: []string{}, certsDir: capabilityExecRequiredCerts, } ) func (agent *ecsAgent) appendVolumeDriverCapabilities(capabilities []types.Attribute) []types.Attribute { // "local" is default docker driver capabilities = appendNameOnlyAttribute(capabilities, attributePrefix+capabilityDockerPluginInfix+volume.DockerLocalVolumeDriver) // for non-standardized plugins, call docker pkg's plugins.Scan() nonStandardizedPlugins, err := agent.mobyPlugins.Scan() if err != nil { seelog.Warnf("Scanning plugins failed: %v", err) // do not return yet, we need the list of plugins below. range handles nil slice. } for _, pluginName := range nonStandardizedPlugins { // Replace the ':' to '.' in the plugin name for attributes capabilities = appendNameOnlyAttribute(capabilities, attributePrefix+capabilityDockerPluginInfix+strings.Replace(pluginName, config.DockerTagSeparator, attributeSeparator, -1)) } // for standardized plugins, call docker's plugin ls API pluginEnabled := true volumeDriverType := []string{dockerapi.VolumeDriverType} standardizedPlugins, err := agent.dockerClient.ListPluginsWithFilters(agent.ctx, pluginEnabled, volumeDriverType, dockerclient.ListPluginsTimeout) if err != nil { seelog.Warnf("Listing plugins with filters enabled=%t, capabilities=%v failed: %v", pluginEnabled, volumeDriverType, err) return capabilities } // For plugin with default tag latest, register two attributes with and without the latest tag // as the tag is optional and can be added by docker or customer for _, pluginName := range standardizedPlugins { names := strings.Split(pluginName, config.DockerTagSeparator) if len(names) > 1 && names[len(names)-1] == config.DefaultDockerTag { capabilities = appendNameOnlyAttribute(capabilities, attributePrefix+capabilityDockerPluginInfix+strings.Join(names[:len(names)-1], attributeSeparator)) } capabilities = appendNameOnlyAttribute(capabilities, attributePrefix+capabilityDockerPluginInfix+strings.Replace(pluginName, config.DockerTagSeparator, attributeSeparator, -1)) } return capabilities } func (agent *ecsAgent) appendNvidiaDriverVersionAttribute(capabilities []types.Attribute) []types.Attribute { if agent.resourceFields != nil && agent.resourceFields.NvidiaGPUManager != nil { driverVersion := agent.resourceFields.NvidiaGPUManager.GetDriverVersion() if driverVersion != "" { capabilities = appendNameOnlyAttribute(capabilities, attributePrefix+capabilityNvidiaDriverVersionInfix+driverVersion) capabilities = append(capabilities, types.Attribute{ Name: aws.String(attributePrefix + capabilityGpuDriverVersion), Value: aws.String(driverVersion), }) } } return capabilities } func (agent *ecsAgent) appendENITrunkingCapabilities(capabilities []types.Attribute) []types.Attribute { if !agent.cfg.ENITrunkingEnabled.Enabled() { return capabilities } capabilities = appendNameOnlyAttribute(capabilities, attributePrefix+taskENITrunkingAttributeSuffix) return agent.appendBranchENIPluginVersionAttribute(capabilities) } func (agent *ecsAgent) appendBranchENIPluginVersionAttribute(capabilities []types.Attribute) []types.Attribute { version, err := agent.cniClient.Version(ecscni.ECSBranchENIPluginName) if err != nil { seelog.Warnf( "Unable to determine the version of the plugin '%s': %v", ecscni.ECSBranchENIPluginName, err) return capabilities } return append(capabilities, types.Attribute{ Name: aws.String(attributePrefix + branchCNIPluginVersionSuffix), Value: aws.String(version), }) } func (agent *ecsAgent) appendPIDAndIPCNamespaceSharingCapabilities(capabilities []types.Attribute) []types.Attribute { isLoaded, err := agent.pauseLoader.IsLoaded(agent.dockerClient) if !isLoaded || err != nil { seelog.Warnf("Pause container is not loaded, did not append PID and IPC capabilities: %v", err) return capabilities } return appendNameOnlyAttribute(capabilities, attributePrefix+capabiltyPIDAndIPCNamespaceSharing) } func (agent *ecsAgent) appendAppMeshCapabilities(capabilities []types.Attribute) []types.Attribute { return appendNameOnlyAttribute(capabilities, attributePrefix+appMeshAttributeSuffix) } func (agent *ecsAgent) appendTaskEIACapabilities(capabilities []types.Attribute) []types.Attribute { capabilities = appendNameOnlyAttribute(capabilities, attributePrefix+taskEIAAttributeSuffix) eiaRequiredFlags := []string{AVX, AVX2, SSE41, SSE42} cpuInfo, err := utils.ReadCPUInfo(CpuInfoPath) if err != nil { seelog.Warnf("Unable to read cpuinfo: %v", err) return capabilities } flagMap := utils.GetCPUFlags(cpuInfo) missingFlags := []string{} for _, requiredFlag := range eiaRequiredFlags { if _, ok := flagMap[requiredFlag]; !ok { missingFlags = append(missingFlags, requiredFlag) } } if len(missingFlags) > 0 { seelog.Infof("Missing cpu flags for EIA support: %v", strings.Join(missingFlags, ",")) return capabilities } return appendNameOnlyAttribute(capabilities, attributePrefix+taskEIAWithOptimizedCPU) } func (agent *ecsAgent) appendFirelensFluentdCapabilities(capabilities []types.Attribute) []types.Attribute { return appendNameOnlyAttribute(capabilities, attributePrefix+capabilityFirelensFluentd) } func (agent *ecsAgent) appendFirelensFluentbitCapabilities(capabilities []types.Attribute) []types.Attribute { return appendNameOnlyAttribute(capabilities, attributePrefix+capabilityFirelensFluentbit) } func (agent *ecsAgent) appendEFSCapabilities(capabilities []types.Attribute) []types.Attribute { return appendNameOnlyAttribute(capabilities, attributePrefix+capabilityEFS) } func (agent *ecsAgent) appendEFSVolumePluginCapabilities(capabilities []types.Attribute, pluginCapability string) []types.Attribute { return appendNameOnlyAttribute(capabilities, attributePrefix+pluginCapability) } func (agent *ecsAgent) appendFirelensLoggingDriverCapabilities(capabilities []types.Attribute) []types.Attribute { return appendNameOnlyAttribute(capabilities, capabilityPrefix+capabilityFirelensLoggingDriver) } func (agent *ecsAgent) appendFirelensLoggingDriverConfigCapabilities(capabilities []types.Attribute) []types.Attribute { return appendNameOnlyAttribute(capabilities, attributePrefix+capabilityFirelensLoggingDriver+capabilityFireLensLoggingDriverConfigBufferLimitSuffix) } func (agent *ecsAgent) appendFirelensConfigCapabilities(capabilities []types.Attribute) []types.Attribute { capabilities = appendNameOnlyAttribute(capabilities, attributePrefix+capabilityFirelensConfigFile) return appendNameOnlyAttribute(capabilities, attributePrefix+capabilityFirelensConfigS3) } func (agent *ecsAgent) appendIPv6Capability(capabilities []types.Attribute) []types.Attribute { return appendNameOnlyAttribute(capabilities, attributePrefix+taskENIIPv6AttributeSuffix) } func (agent *ecsAgent) appendFSxWindowsFileServerCapabilities(capabilities []types.Attribute) []types.Attribute { return capabilities } // getTaskENIPluginVersionAttribute returns the version information of the ECS // CNI plugins. It just executes the ENI plugin as the assumption is that these // plugins are packaged with the ECS Agent, which means all of the other plugins // should also emit the same version information. Also, the version information // doesn't contribute to placement decisions and just serves as additional // debugging information func (agent *ecsAgent) getTaskENIPluginVersionAttribute() (types.Attribute, error) { version, err := agent.cniClient.Version(ecscni.VPCENIPluginName) if err != nil { seelog.Warnf( "Unable to determine the version of the plugin '%s': %v", ecscni.VPCENIPluginName, err) return types.Attribute{}, err } return types.Attribute{ Name: aws.String(attributePrefix + cniPluginVersionSuffix), Value: aws.String(version), }, nil } func defaultIsPlatformExecSupported() (bool, error) { return true, nil } // var to allow mocking for checkNetworkTooling var isFaultInjectionToolingAvailable = checkFaultInjectionTooling // wrapper around exec.LookPath var lookPathFunc = exec.LookPath var osExecWrapper = execwrapper.NewExec() var networkConfigClient = netconfig.NewNetworkConfigClient() // checkFaultInjectionTooling checks for the required network packages like iptables, tc // to be available on the host before ecs.capability.fault-injection can be advertised func checkFaultInjectionTooling() bool { tools := []string{"iptables", "tc", "nsenter"} for _, tool := range tools { if _, err := lookPathFunc(tool); err != nil { seelog.Warnf( "Failed to find network tool %s that is needed for fault-injection feature: %v", tool, err) return false } } return checkFaultInjectionModules() && checkTCShowTooling() } // checkFaultInjectionModules checks for the required kernel modules such as sch_netem to be installed // and avaialble on the host before ecs.capability.fault-injection can be advertised func checkFaultInjectionModules() bool { ctxWithTimeout, cancel := context.WithTimeout(context.Background(), ctxTimeoutDuration) defer cancel() _, err := osExecWrapper.CommandContext(ctxWithTimeout, modInfoCmd, faultInjectionKernelModules).CombinedOutput() if err != nil { seelog.Warnf("Failed to find kernel module %s that is needed for fault-injection feature: %v", faultInjectionKernelModules, err) return false } return true } func checkTCShowTooling() bool { ctxWithTimeout, cancel := context.WithTimeout(context.Background(), ctxTimeoutDuration) defer cancel() hostDeviceName, netErr := netconfig.DefaultNetInterfaceName(networkConfigClient.NetlinkClient) if netErr != nil { seelog.Warnf("Failed to obtain the network interface device name on the host: %v", netErr) return false } tcShowCmd := fmt.Sprintf(tcShowCmdString, hostDeviceName) cmdList := strings.Split(tcShowCmd, " ") _, err := osExecWrapper.CommandContext(ctxWithTimeout, cmdList[0], cmdList[1:]...).CombinedOutput() if err != nil { seelog.Warnf("Failed to call %s which is needed for fault-injection feature: %v", tcShowCmd, err) return false } return true }