From 057c2a4d29afa0fdeab842de043722005f51de92 Mon Sep 17 00:00:00 2001 From: jiawei Date: Tue, 24 Dec 2024 16:56:15 +0800 Subject: [PATCH] add bandwith limit Signed-off-by: jiawei --- .../github.com/safchain/ethtool/LICENSE | 206 +++++++ docs/images/bandwith/bandwith.png | Bin 0 -> 154315 bytes docs/proposals/bandwidth-limit.md | 101 ++++ edge/cmd/edgecore/app/server.go | 2 +- edge/cmd/edgemark/hollow_edgecore.go | 3 +- edge/pkg/edged/bandwidth/consts/consts.go | 27 + edge/pkg/edged/bandwidth/kube/kube.go | 42 ++ .../edged/bandwidth/tclimit/container_cmd.go | 112 ++++ edge/pkg/edged/bandwidth/tclimit/pod_watch.go | 145 +++++ edge/pkg/edged/bandwidth/tclimit/tc.go | 128 +++++ edge/pkg/edged/bandwidth/tclimit/tc_conf.go | 142 +++++ .../pkg/edged/bandwidth/tclimit/tc_context.go | 57 ++ .../bandwidth/tclinux/ifb_creator_linux.go | 184 ++++++ .../pkg/edged/bandwidth/tclinux/link_linux.go | 260 +++++++++ .../tclinux/netlink_interface_linux.go | 183 ++++++ edge/pkg/edged/bandwidth/tclinux/ns/ns.go | 31 + .../edged/bandwidth/tclinux/ns/ns_linux.go | 184 ++++++ .../edged/bandwidth/tclinux/sysctl/sysctl.go | 65 +++ .../edged/bandwidth/tclinux/tclinux_darwin.go | 37 ++ .../bandwidth/tclinux/tclinux_windows.go | 37 ++ edge/pkg/edged/edged.go | 62 +- go.mod | 1 + go.sum | 1 + .../keadm/app/cmd/util/edgecoreinstaller.go | 1 - .../edgecore/v1alpha2/types.go | 3 + vendor/github.com/safchain/ethtool/.gitignore | 27 + .../github.com/safchain/ethtool/.travis.yml | 1 + vendor/github.com/safchain/ethtool/LICENSE | 202 +++++++ vendor/github.com/safchain/ethtool/Makefile | 4 + vendor/github.com/safchain/ethtool/README.md | 60 ++ vendor/github.com/safchain/ethtool/ethtool.go | 541 ++++++++++++++++++ .../safchain/ethtool/ethtool_cmd.go | 207 +++++++ .../safchain/ethtool/ethtool_msglvl.go | 113 ++++ vendor/modules.txt | 3 + 34 files changed, 3151 insertions(+), 21 deletions(-) create mode 100644 LICENSES/vendor/github.com/safchain/ethtool/LICENSE create mode 100644 docs/images/bandwith/bandwith.png create mode 100644 docs/proposals/bandwidth-limit.md create mode 100644 edge/pkg/edged/bandwidth/consts/consts.go create mode 100644 edge/pkg/edged/bandwidth/kube/kube.go create mode 100644 edge/pkg/edged/bandwidth/tclimit/container_cmd.go create mode 100644 edge/pkg/edged/bandwidth/tclimit/pod_watch.go create mode 100644 edge/pkg/edged/bandwidth/tclimit/tc.go create mode 100644 edge/pkg/edged/bandwidth/tclimit/tc_conf.go create mode 100644 edge/pkg/edged/bandwidth/tclimit/tc_context.go create mode 100644 edge/pkg/edged/bandwidth/tclinux/ifb_creator_linux.go create mode 100644 edge/pkg/edged/bandwidth/tclinux/link_linux.go create mode 100644 edge/pkg/edged/bandwidth/tclinux/netlink_interface_linux.go create mode 100644 edge/pkg/edged/bandwidth/tclinux/ns/ns.go create mode 100644 edge/pkg/edged/bandwidth/tclinux/ns/ns_linux.go create mode 100644 edge/pkg/edged/bandwidth/tclinux/sysctl/sysctl.go create mode 100644 edge/pkg/edged/bandwidth/tclinux/tclinux_darwin.go create mode 100644 edge/pkg/edged/bandwidth/tclinux/tclinux_windows.go create mode 100644 vendor/github.com/safchain/ethtool/.gitignore create mode 100644 vendor/github.com/safchain/ethtool/.travis.yml create mode 100644 vendor/github.com/safchain/ethtool/LICENSE create mode 100644 vendor/github.com/safchain/ethtool/Makefile create mode 100644 vendor/github.com/safchain/ethtool/README.md create mode 100644 vendor/github.com/safchain/ethtool/ethtool.go create mode 100644 vendor/github.com/safchain/ethtool/ethtool_cmd.go create mode 100644 vendor/github.com/safchain/ethtool/ethtool_msglvl.go diff --git a/LICENSES/vendor/github.com/safchain/ethtool/LICENSE b/LICENSES/vendor/github.com/safchain/ethtool/LICENSE new file mode 100644 index 00000000000..c407809fee2 --- /dev/null +++ b/LICENSES/vendor/github.com/safchain/ethtool/LICENSE @@ -0,0 +1,206 @@ += vendor/github.com/safchain/ethtool licensed under: = + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. + + += vendor/github.com/safchain/ethtool/LICENSE fa818a259cbed7ce8bc2a22d35a464fc diff --git a/docs/images/bandwith/bandwith.png b/docs/images/bandwith/bandwith.png new file mode 100644 index 0000000000000000000000000000000000000000..bd52a40c316c2159a04462931ceccab44d4c3db1 GIT binary patch literal 154315 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

~Nj?`q87+@e;4nv>maKj#FZQv3&T1zR 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 + + +## Detail Design + +1)Deployment配置: +在Deployment配置中针对应用的网络资源添加流量配置选项功能,细分为两个维度: +``` sh + ingress(入口流量速率限制) + egress(出口流量速率限制) + kubeedge/ingress-bandwidth: 2M + kubeedge/egress-bandwidth: 3M +``` + +2)edgecore感知到流量限制配置: +- 依赖边端的edgecore的metamanager模块监听边端节点的pod变化(Add、Update、Delete事件) +- tc模块基于metamanager获取pod的annotions配置,如果发现有限流标签配置(key判断,value不能为空),执行限流操作 + +3)获取应用容器的网卡: +当前容器网络模式是bridge桥接,直接通过当前容器获取网卡接口; +``` sh +# 获取 PID +CONTAINER_ID="%s" +PID=$(ctr -n k8s.io tasks ls | grep $CONTAINER_ID | awk '{print $2}' | tr -d '\n') + +# 进入网络命名空间并提取 eth0@if +IFACE_NAME=$(nsenter -t $PID -n ip a | grep 'eth0@if' | awk '{print $2}' | tr -d '\n') + +# 输出接口名称 +echo "$IFACE_NAME" +``` + +4)限流配置 +如果当前容器网络模式是bridge桥接,配置到虚拟网卡上; +``` sh +tc qdisc add dev vethdd8cdba root handle 1: tbf rate 4Mbit burst 5kb latency 5ms +``` + +## Implementation Details +1)在edge/pkg目录下添加bandwidth目录,注册 "带宽限流" 模块, 其中moudleName(tc)、groupName(bandwith)。 + +2)edgecore的配置文件里,添加 "BandwidthManager"配置模块。 +``` sh +...... + serviceBus: + enable: false + port: 9060 + server: 127.0.0.1 + timeout: 60 + bandwidthManager: + enable: true +``` + + diff --git a/edge/cmd/edgecore/app/server.go b/edge/cmd/edgecore/app/server.go index 1776c0d78f3..b59b4763f04 100644 --- a/edge/cmd/edgecore/app/server.go +++ b/edge/cmd/edgecore/app/server.go @@ -195,7 +195,7 @@ func environmentCheck(skipCheck bool) error { // registerModules register all the modules started in edgecore func registerModules(c *v1alpha2.EdgeCoreConfig) { devicetwin.Register(c.Modules.DeviceTwin, c.Modules.Edged.HostnameOverride) - edged.Register(c.Modules.Edged) + edged.Register(c.Modules.Edged, c.Modules.MetaManager.MetaServer.Server) edgehub.Register(c.Modules.EdgeHub, c.Modules.Edged.HostnameOverride) eventbus.Register(c.Modules.EventBus, c.Modules.Edged.HostnameOverride) metamanager.Register(c.Modules.MetaManager) diff --git a/edge/cmd/edgemark/hollow_edgecore.go b/edge/cmd/edgemark/hollow_edgecore.go index 7a20018e4d0..b16d0c426f7 100644 --- a/edge/cmd/edgemark/hollow_edgecore.go +++ b/edge/cmd/edgemark/hollow_edgecore.go @@ -113,8 +113,7 @@ func run(config *hollowEdgeNodeConfig) { kubeDeps *kubelet.Dependencies, featureGate featuregate.FeatureGate) error { return kubeletapp.RunKubelet(s, kubeDeps, false) } - - edged.Register(c.Modules.Edged) + edged.Register(c.Modules.Edged, c.Modules.MetaManager.MetaServer.Server) edgehub.Register(c.Modules.EdgeHub, c.Modules.Edged.HostnameOverride) metamanager.Register(c.Modules.MetaManager) diff --git a/edge/pkg/edged/bandwidth/consts/consts.go b/edge/pkg/edged/bandwidth/consts/consts.go new file mode 100644 index 00000000000..b1682d1130d --- /dev/null +++ b/edge/pkg/edged/bandwidth/consts/consts.go @@ -0,0 +1,27 @@ +/* +Copyright 2024 The KubeEdge 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 consts + +const ( + TCSleepTimes = 30 + + AnnotationPrefix string = "kubeedge/" + + AnnotationIngressBandwidth = AnnotationPrefix + "ingress-bandwidth" + + AnnotationEgressBandwidth = AnnotationPrefix + "egress-bandwidth" +) diff --git a/edge/pkg/edged/bandwidth/kube/kube.go b/edge/pkg/edged/bandwidth/kube/kube.go new file mode 100644 index 00000000000..7488ee33a3e --- /dev/null +++ b/edge/pkg/edged/bandwidth/kube/kube.go @@ -0,0 +1,42 @@ +/* +Copyright 2024 The KubeEdge 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 kube + +import ( + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +var edgeClient kubernetes.Interface + +func InitEdgeClient(host string) error { + var err error + restConfig := &rest.Config{ + Host: host, + TLSClientConfig: rest.TLSClientConfig{ + Insecure: true, + }, + } + if edgeClient, err = kubernetes.NewForConfig(restConfig); err != nil { + return err + } + return nil +} + +func EdgeClient() kubernetes.Interface { + return edgeClient +} diff --git a/edge/pkg/edged/bandwidth/tclimit/container_cmd.go b/edge/pkg/edged/bandwidth/tclimit/container_cmd.go new file mode 100644 index 00000000000..a290dc0bbc6 --- /dev/null +++ b/edge/pkg/edged/bandwidth/tclimit/container_cmd.go @@ -0,0 +1,112 @@ +/* +Copyright 2024 The KubeEdge 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 tclimit + +import ( + "bytes" + "fmt" + "os/exec" + "regexp" + "strings" + + "k8s.io/klog/v2" +) + +const ( + MinMatches = 2 + VethInterfaceName = "ip link show | grep %s: | grep veth | awk '{print $2}' | tr -d ':' | awk -F" + + "'@' '{print $1}'\n" +) + +func GetNetlinkDeviceName(containerID string) (string, error) { + ifaceName, err := getInterfaceName(containerID) + if err != nil { + klog.Warningf("getInterfaceName Error: %v", err) + return "", err + } + + ifaceNumber, err := extractInterfaceNumber(ifaceName) + if err != nil { + klog.Warningf("extractInterfaceNumber Error: %v", err) + return "", err + } + + hostVethName, err := getHostVethInterfaceName(ifaceNumber) + if err != nil { + klog.Warningf("getInterfaceName Error:%v", err) + return "", err + } + klog.Infof("ifaceName:%s", hostVethName) + + return hostVethName, nil +} + +// getInterfaceName executes an inline shell script to get the network interface name. +func getInterfaceName(containerID string) (string, error) { + script := fmt.Sprintf(` +#!/bin/bash + +# 获取 PID +CONTAINER_ID="%s" +PID=$(ctr -n k8s.io tasks ls | grep $CONTAINER_ID | awk '{print $2}' | tr -d '\n') + +# 进入网络命名空间并提取 eth0@if +IFACE_NAME=$(nsenter -t $PID -n ip a | grep 'eth0@if' | awk '{print $2}' | tr -d '\n') + +# 输出接口名称 +echo "$IFACE_NAME" +`, containerID) + + cmd := exec.Command("bash", "-c", script) + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("failed to run script: %s, output: %s", err, out.String()) + } + + return out.String(), nil +} + +// extractInterfaceNumber extracts the number between 'if' and ':' from the input string. +func extractInterfaceNumber(iface string) (string, error) { + // Define a regex pattern to match the desired part of the string + re := regexp.MustCompile(`if(\d+):`) + matches := re.FindStringSubmatch(iface) + + if len(matches) < MinMatches { + return "", fmt.Errorf("no match found") + } + + return matches[1], nil // Return the first capturing group which contains the number +} + +// getInterfaceName retrieves the interface name based on the given identifier. +func getHostVethInterfaceName(identifier string) (string, error) { + // Use the constant command template to format the command + cmd := exec.Command("bash", "-c", fmt.Sprintf(VethInterfaceName, identifier)) + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out // Capture stderr as well + + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("failed to run command: %s, output: %s", err, out.String()) + } + + return strings.TrimSpace(out.String()), nil // Return the resulting interface name +} diff --git a/edge/pkg/edged/bandwidth/tclimit/pod_watch.go b/edge/pkg/edged/bandwidth/tclimit/pod_watch.go new file mode 100644 index 00000000000..f47eeb25ed8 --- /dev/null +++ b/edge/pkg/edged/bandwidth/tclimit/pod_watch.go @@ -0,0 +1,145 @@ +/* +Copyright 2024 The KubeEdge 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 tclimit + +import ( + "context" + "fmt" + "time" + + v1 "k8s.io/api/core/v1" + "k8s.io/client-go/informers" + "k8s.io/client-go/tools/cache" + "k8s.io/klog/v2" + + "github.com/kubeedge/kubeedge/edge/pkg/edged/bandwidth/consts" + "github.com/kubeedge/kubeedge/edge/pkg/edged/bandwidth/kube" +) + +const ( + refreshInterval = 5 +) + +var nodeName string + +var AddBandwidthFunc = func(obj interface{}) { + p, exist := obj.(*v1.Pod) + if !exist { + klog.Error("Pod type assertion failed") + return + } + // check whether there is limited flow annotation + if !filterPod(p) { + return + } + if !isPodRunning(p) { + klog.Warningf("ADD EVENT: pod `%s` is not running, please check reason", p.Name) + return + } + klog.Infof("ADD EVENT: pod `%s` netlink interface bandwidth limit", p.Name) + // pod network limit + podBandwidthLimit(p) + klog.Infof("ADD EVENT END...") +} + +var UpdateBandwidthFunc = func(_, newObj interface{}) { + p, exist := newObj.(*v1.Pod) + if !exist { + klog.Error("Pod type assertion failed") + return + } + // check whether there is limited flow annotation + if !filterPod(p) { + return + } + // check whether the new container is running normally + if !isPodRunning(p) { + // If terminated state, delete the ifb network interface + if isPodTerminated(p) { + klog.Infof("UPDATE EVENT: pod `%s` terminated,delete ifb netlink interface,pod status:%s", p.Name, p.Status.Phase) + deleteNetworkLimit(p) + return + } + klog.Warningf("UPDATE EVENT: pod `%s` is not running,pod status:%s", p.Name, p.Status.Phase) + return + } + klog.Infof("UPDATE EVENT: pod `%s` netlink interface bandwidth limit", p.Name) + // pod network limit + podBandwidthLimit(p) + klog.Infof("UPDATE EVENT END...") +} + +var DeleteBandwidthFunc = func(obj interface{}) { + p, ok := obj.(*v1.Pod) + if !ok { + klog.Errorf("obj is not a *v1.Pod") + } + klog.Infof("DELETE EVENT: pod `%s` deleted", p.Name) + // check whether there is limited flow annotation + if !filterPod(p) { + return + } + klog.Infof("DELETE EVENT: Delete pod `%s` ifb netlink interface...", p.Name) + // delete the pod network interface + deleteNetworkLimit(p) + klog.Infof("DELETE EVENT END...") +} + +func EdgeWatch(ctx context.Context, hostnameOverride string) error { + nodeName = hostnameOverride + client := kube.EdgeClient() + // list sync cache every 5 minutes + factory := informers.NewSharedInformerFactory(client, time.Minute*refreshInterval) + // register event handler + _, err := factory.Core().V1().Pods().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: AddBandwidthFunc, + UpdateFunc: UpdateBandwidthFunc, + DeleteFunc: DeleteBandwidthFunc, + }) + if err != nil { + return fmt.Errorf("failed to add event handler: %v", err) + } + // start informer, List & Watch + go factory.Start(ctx.Done()) + return nil +} + +func filterPod(pod *v1.Pod) bool { + if pod.Spec.NodeName != nodeName { + return false + } + annotions := pod.ObjectMeta.Annotations + if _, ok := annotions[consts.AnnotationIngressBandwidth]; ok { + return true + } + if _, ok := annotions[consts.AnnotationEgressBandwidth]; ok { + return true + } + return false +} + +func isPodRunning(pod *v1.Pod) bool { + // check if the pod is started + status := pod.Status + return status.Phase == v1.PodRunning +} + +func isPodTerminated(pod *v1.Pod) bool { + // check if pod is deleted + status := pod.Status + return status.Phase == v1.PodSucceeded +} diff --git a/edge/pkg/edged/bandwidth/tclimit/tc.go b/edge/pkg/edged/bandwidth/tclimit/tc.go new file mode 100644 index 00000000000..9f9905e05fa --- /dev/null +++ b/edge/pkg/edged/bandwidth/tclimit/tc.go @@ -0,0 +1,128 @@ +/* +Copyright 2024 The KubeEdge 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 tclimit + +import ( + "strings" + + v1 "k8s.io/api/core/v1" + "k8s.io/klog/v2" + + "github.com/kubeedge/kubeedge/edge/pkg/edged/bandwidth/tclinux" +) + +func doIngressBandwidthLimit(pod *v1.Pod) { + klog.Infof("pod `%s` start Ingress bandwidth limit", pod.Name) + // get container ID + containerID, err := getContainerID(&pod.Status.ContainerStatuses[0]) + if err != nil { + klog.Warningf("get container id failed when ingress bandwidth limit,err:%v", err) + return + } + // get container network interface device + netlinkDeviceName, err := getContainerHostNetworkDevice(pod, containerID) + if err != nil { + klog.Warningf("get container network device name failed when ingress bandwidth "+ + "limit,containerId:%s,err:%v", containerID, err) + return + } + // parse current limit configuration + conf, err := parseIngressTrafficControlConf(pod, netlinkDeviceName) + if err != nil { + klog.Warningf("parse ingress bandwidth conf failed,err:%v", err) + return + } + // bandwidth limit (tbf solution) + err = tclinux.CreateIngressQdisc(conf.rate, conf.burst, conf.netlinkDeviceName) + if err != nil { + klog.Warningf("ingress bandwidth limit failed,err:%v", err) + return + } + // Store the current network interface and pod mapping relationship + tclinux.StorePodNetworkDeviceMapping(pod.Name, conf.netlinkDeviceName) + klog.Infof("pod `%s` Ingress bandwidth limit succcess to network interface name:%s", pod.Name, conf.netlinkDeviceName) +} + +func doEgressBandwidthLimit(pod *v1.Pod) { + klog.Infof("pod `%s` start Egress bandwidth limit", pod.Name) + // get container ID + containerID, err := getContainerID(&pod.Status.ContainerStatuses[0]) + if err != nil { + klog.Warningf("get container id failed when egress bandwidth limit,err:%v", err) + return + } + hostNetworkDevice, err := getContainerHostNetworkDevice(pod, containerID) + if err != nil { + klog.Warningf("get container network device name failed when egress bandwidth "+ + "limit,containerId:%s,err:%v", containerID, err) + return + } + mtu, err := tclinux.GetMTU(hostNetworkDevice) + if err != nil { + klog.Warningf("pod `%s` get mtu param value failed when egress bandwidth limit,error:%v", pod.Name, err) + return + } + // check if ifb device already exists + ifbName := tclinux.GetIfbDeviceByPod(pod.Name) + if ifbName == "" { + // create ifb network card device + ifbName = tclinux.CalIfbName(containerID) + err = tclinux.CreateIfb(ifbName, mtu) + if err != nil { + klog.Warningf("pod `%s` create ifb device failed when egress bandwidth limit,error:%v", pod.Name, err) + return + } + } + // parse egress limit configuration parameters + conf, err := parseEgressTrafficControlConf(pod, ifbName) + if err != nil { + klog.Warningf("pod `%s` parse egress bandwidth param failed,error:%v", pod.Name, err) + return + } + // egress queue traffic limit and mirror + err = tclinux.CreateEgressQdisc(conf.rate, conf.burst, hostNetworkDevice, ifbName) + if err != nil { + klog.Warningf("pod `%s` Egress bandwidth limit create qdisc failed,error:%v", pod.Name, err) + return + } + // storage egress limit ifb network interface and pod relationship + tclinux.StorePodNetworkDeviceMapping(pod.Name, ifbName) + klog.Infof("pod `%s` egress bandwidth limit succcess to network interface name:%s", pod.Name, conf.netlinkDeviceName) +} + +func getContainerHostNetworkDevice(pod *v1.Pod, containerID string) (string, error) { + // get the container's network device from the cache + var err error + hostNetworkDevice := tclinux.GetNetworkDeviceByPod(pod.Name) + if hostNetworkDevice == "" { + // get in real time + hostNetworkDevice, err = GetNetlinkDeviceName(containerID) + if err != nil { + return "", err + } + // save relationship between the pod name and the host network interface + tclinux.StorePodNetworkDeviceMapping(pod.Name, hostNetworkDevice) + } + return hostNetworkDevice, err +} + +func getContainerID(cs *v1.ContainerStatus) (string, error) { + podContainerID := cs.ContainerID + // get the pause container id of the current container + index := strings.LastIndex(podContainerID, "/") + return strings.Trim(podContainerID[index+1:], "\""), nil +} diff --git a/edge/pkg/edged/bandwidth/tclimit/tc_conf.go b/edge/pkg/edged/bandwidth/tclimit/tc_conf.go new file mode 100644 index 00000000000..131cc79d78c --- /dev/null +++ b/edge/pkg/edged/bandwidth/tclimit/tc_conf.go @@ -0,0 +1,142 @@ +/* +Copyright 2024 The KubeEdge 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 tclimit + +import ( + "fmt" + "math" + "regexp" + "strconv" + "strings" + + "github.com/pkg/errors" + v1 "k8s.io/api/core/v1" + + "github.com/kubeedge/kubeedge/edge/pkg/edged/bandwidth/consts" +) + +const ( + BandwidthRegexCheck = "^[1-9]\\d*(M|Mi|G|Gi)$" + NumRegex = "[1-9]\\d*" + MB = "M" + GB = "G" + KB = "Kb" // KBytes + B = "b" + burstDivisor = 100 + bytesPerKilobyte = 1000 +) + +type TokenBucketFilterConf struct { + netlinkDeviceName string + // bit + rate uint64 + // bit + burst uint64 +} + +func parseIngressTrafficControlConf(pod *v1.Pod, networkDeviceName string) (*TokenBucketFilterConf, error) { + annotations := pod.Annotations + // MB/GB + value, ok := annotations[consts.AnnotationIngressBandwidth] + if !ok { + return nil, errors.Errorf("pod `%s` ingress bandwidth annotation not found", pod.Name) + } + res, err := parseBandwidthParam(networkDeviceName, value) + if err != nil { + return nil, err + } + return res, nil +} + +func parseEgressTrafficControlConf(pod *v1.Pod, networkDeviceName string) (*TokenBucketFilterConf, error) { + annotations := pod.Annotations + // MB/GB + value, ok := annotations[consts.AnnotationEgressBandwidth] + if !ok { + return nil, errors.Errorf("pod `%s` egress bandwidth annotation not found", pod.Name) + } + res, err := parseBandwidthParam(networkDeviceName, value) + if err != nil { + return nil, err + } + return res, nil +} + +func parseBandwidthParam(networkDeviceName, bandwidthValue string) (*TokenBucketFilterConf, error) { + rate, burst, err := parseRate(bandwidthValue) + if err != nil { + return nil, err + } + res := &TokenBucketFilterConf{ + netlinkDeviceName: networkDeviceName, + rate: rate, + burst: burst, + } + if err := validateRateAndBurst(res.rate, res.burst); err != nil { + return nil, err + } + + return res, nil +} + +func parseRate(value string) (rate, burst uint64, err error) { + compile := regexp.MustCompile(BandwidthRegexCheck) + if !compile.MatchString(value) { + // no value matched + return 0, 0, errors.Errorf("bandwidth limit annotation value regex match failed,value:%s", value) + } + numMatch := regexp.MustCompile(NumRegex) + nums := numMatch.FindAllString(value, -1) + if len(nums) != 1 { + return 0, 0, errors.Errorf("bandwidth limit annotation rate value regex match failed,value:%s", value) + } + rateNum := nums[0] + rateUnit := strings.Split(value, rateNum)[1] + rateValue, err := strconv.ParseUint(rateNum, 10, 32) + // refer to the bandwidth source code to process the remaining parameters. + rate, burst = dealTCParam(rateUnit, rateValue) + return rate, burst, err +} + +// convert processing parameters into bits and calculate burst +func dealTCParam(unit string, rate uint64) (adjustedRate, adjustedBurst uint64) { + switch unit { + case KB: + rate *= burstDivisor + case MB: + rate *= bytesPerKilobyte * bytesPerKilobyte + case GB: + rate *= bytesPerKilobyte * bytesPerKilobyte * bytesPerKilobyte + } + // calculate the optimal burst buffer data area size through rate. The faster the network bandwidth, + // the larger the value (rate is 10Mbit, burst must be >10kb) + burst := rate / burstDivisor + return rate, burst +} + +func validateRateAndBurst(rate, burst uint64) error { + switch { + case burst == 0 && rate != 0: + return fmt.Errorf("if rate is set, burst must also be set") + case rate == 0 && burst != 0: + return fmt.Errorf("if burst is set, rate must also be set") + case burst/8 >= math.MaxUint32: + return fmt.Errorf("burst cannot be more than 4GB") + } + + return nil +} diff --git a/edge/pkg/edged/bandwidth/tclimit/tc_context.go b/edge/pkg/edged/bandwidth/tclimit/tc_context.go new file mode 100644 index 00000000000..2d37bfab824 --- /dev/null +++ b/edge/pkg/edged/bandwidth/tclimit/tc_context.go @@ -0,0 +1,57 @@ +/* +Copyright 2024 The KubeEdge 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 tclimit + +import ( + v1 "k8s.io/api/core/v1" + + "github.com/kubeedge/kubeedge/edge/pkg/edged/bandwidth/consts" + "github.com/kubeedge/kubeedge/edge/pkg/edged/bandwidth/tclinux" +) + +func deleteNetworkLimit(pod *v1.Pod) { + tclinux.DeleteNetworkDevice(pod.Name) +} + +func podBandwidthLimit(pod *v1.Pod) { + ingressBandwidthLimit(pod) + egressBandwidthLimit(pod) +} + +func ingressBandwidthLimit(pod *v1.Pod) { + if !checkIngressEnable(pod.Annotations) { + return + } + doIngressBandwidthLimit(pod) +} + +func egressBandwidthLimit(pod *v1.Pod) { + if !checkEgressEnable(pod.Annotations) { + return + } + doEgressBandwidthLimit(pod) +} + +func checkIngressEnable(annotations map[string]string) bool { + _, ok := annotations[consts.AnnotationIngressBandwidth] + return ok +} + +func checkEgressEnable(annotations map[string]string) bool { + _, ok := annotations[consts.AnnotationEgressBandwidth] + return ok +} diff --git a/edge/pkg/edged/bandwidth/tclinux/ifb_creator_linux.go b/edge/pkg/edged/bandwidth/tclinux/ifb_creator_linux.go new file mode 100644 index 00000000000..4c582df8536 --- /dev/null +++ b/edge/pkg/edged/bandwidth/tclinux/ifb_creator_linux.go @@ -0,0 +1,184 @@ +package tclinux + +import ( + "fmt" + "net" + "syscall" + + "github.com/vishvananda/netlink" + "k8s.io/klog/v2" +) + +const ( + latencyInMillis = 25 + conversionFactor = 1000.0 + bytesNum = 8 +) + +func CreateIfb(ifbDeviceName string, mtu int) error { + // 判断ifb设备是否已存在 + ifblink, err := netlink.LinkByName(ifbDeviceName) + if err == nil && ifblink != nil { + // ifb已存在 + klog.Infof("ifb netlink interface existed,name:%s", ifbDeviceName) + return nil + } + err = netlink.LinkAdd(&netlink.Ifb{ + LinkAttrs: netlink.LinkAttrs{ + Name: ifbDeviceName, + Flags: net.FlagUp, + MTU: mtu, + }, + }) + if err != nil { + return fmt.Errorf("adding link: %s", err) + } + + return nil +} + +func TeardownIfb(deviceName string) error { + _, err := DelLinkByNameAddr(deviceName) + if err != nil && err == ErrLinkNotFound { + return nil + } + return err +} + +func CreateIngressQdisc(rateInBits, burstInBits uint64, hostDeviceName string) error { + hostDevice, err := netlink.LinkByName(hostDeviceName) + if err != nil { + return fmt.Errorf("get host device: %s", err) + } + return createTBF(rateInBits, burstInBits, hostDevice) +} + +func CreateEgressQdisc(rateInBits, burstInBits uint64, hostDeviceName, ifbDeviceName string) error { + ifbDevice, err := netlink.LinkByName(ifbDeviceName) + if err != nil { + return fmt.Errorf("get ifb device: %s", err) + } + hostDevice, err := netlink.LinkByName(hostDeviceName) + if err != nil { + return fmt.Errorf("get host device: %s", err) + } + + // add qdisc ingress on host device + ingress := &netlink.Ingress{ + QdiscAttrs: netlink.QdiscAttrs{ + LinkIndex: hostDevice.Attrs().Index, + Handle: netlink.MakeHandle(maxHandleValue, 0), // ffff: + Parent: netlink.HANDLE_INGRESS, + }, + } + ingressFlag := false + qdiscList, err := netlink.QdiscList(hostDevice) + if err != nil { + klog.Warningf("`%s` list qdisc failed,error:%v", hostDeviceName, err) + } + for _, qdisc := range qdiscList { + if qdisc.Attrs().Parent == ingress.Parent && qdisc.Attrs().Handle == ingress.Handle { + klog.Infof("netlink `%s` ingress existed ", hostDevice.Attrs().Name) + ingressFlag = true + } + } + if !ingressFlag { + err = netlink.QdiscAdd(ingress) + if err != nil { + return fmt.Errorf("create ingress qdisc: %s", err) + } + } + // add filter on host device to mirror traffic to ifb device + filter := &netlink.U32{ + FilterAttrs: netlink.FilterAttrs{ + LinkIndex: hostDevice.Attrs().Index, + Parent: ingress.QdiscAttrs.Handle, + Priority: 1, + Protocol: syscall.ETH_P_ALL, + }, + ClassId: netlink.MakeHandle(1, 1), + RedirIndex: ifbDevice.Attrs().Index, + Actions: []netlink.Action{ + &netlink.MirredAction{ + ActionAttrs: netlink.ActionAttrs{}, + MirredAction: netlink.TCA_EGRESS_REDIR, + Ifindex: ifbDevice.Attrs().Index, + }, + }, + } + err = netlink.FilterReplace(filter) + if err != nil { + return fmt.Errorf("add filter: %s", err) + } + + // throttle traffic on ifb device + err = createTBF(rateInBits, burstInBits, ifbDevice) + if err != nil { + return fmt.Errorf("create ifb qdisc: %s", err) + } + return nil +} + +func createTBF(rateInBits, burstInBits uint64, link netlink.Link) error { + linkIndex := link.Attrs().Index + linkName := link.Attrs().Name + // Equivalent to + // tc qdisc add dev link root tbf + // rate netConf.BandwidthLimits.Rate + // burst netConf.BandwidthLimits.Burst + if rateInBits <= 0 { + return fmt.Errorf("invalid rate: %d", rateInBits) + } + if burstInBits <= 0 { + return fmt.Errorf("invalid burst: %d", burstInBits) + } + rateInBytes := rateInBits / bytesNum + burstInBytes := burstInBits / bytesNum + bufferInBytes := buffer(rateInBytes, uint32(burstInBytes)) + latency := latencyInUsec(latencyInMillis) + limitInBytes := limit(rateInBytes, latency, uint32(burstInBytes)) + + qdisc := &netlink.Tbf{ + QdiscAttrs: netlink.QdiscAttrs{ + LinkIndex: linkIndex, + Handle: netlink.MakeHandle(1, 0), + Parent: netlink.HANDLE_ROOT, + }, + Limit: limitInBytes, + Rate: rateInBytes, + Buffer: bufferInBytes, + } + // check network card queue qdisc exists + list, err := netlink.QdiscList(link) + if err != nil { + klog.Warningf("netlink %s list qdisc failed,error:%v", linkName, err) + } + + if len(list) == 0 { + klog.Infof("netlink %s add qdisc", linkName) + err = netlink.QdiscAdd(qdisc) + } else { + klog.Infof("netlink %s replace qdisc", linkName) + err = netlink.QdiscReplace(qdisc) + } + if err != nil { + return fmt.Errorf("create qdisc: %s", err) + } + return nil +} + +func time2Tick(time uint32) uint32 { + return uint32(float64(time) * float64(netlink.TickInUsec())) +} + +func buffer(rate uint64, burst uint32) uint32 { + return time2Tick(uint32(float64(burst) * float64(netlink.TIME_UNITS_PER_SEC) / float64(rate))) +} + +func limit(rate uint64, latency float64, buffer uint32) uint32 { + return uint32(float64(rate)*latency/float64(netlink.TIME_UNITS_PER_SEC)) + buffer +} + +func latencyInUsec(latencyInMillis float64) float64 { + return float64(netlink.TIME_UNITS_PER_SEC) * (latencyInMillis / conversionFactor) +} diff --git a/edge/pkg/edged/bandwidth/tclinux/link_linux.go b/edge/pkg/edged/bandwidth/tclinux/link_linux.go new file mode 100644 index 00000000000..8c143a9d24d --- /dev/null +++ b/edge/pkg/edged/bandwidth/tclinux/link_linux.go @@ -0,0 +1,260 @@ +package tclinux + +import ( + "crypto/rand" + "errors" + "fmt" + "net" + "os" + + "github.com/safchain/ethtool" + "github.com/vishvananda/netlink" + + "github.com/kubeedge/kubeedge/edge/pkg/edged/bandwidth/tclinux/ns" + "github.com/kubeedge/kubeedge/edge/pkg/edged/bandwidth/tclinux/sysctl" +) + +const ( + entropySize = 4 + expectedDeviceCount = 2 + maxHandleValue = 0xffff +) + +var ErrLinkNotFound = errors.New("link not found") + +// makeVethPair is called from within the container's network namespace +func makeVethPair(name, peer string, mtu int, mac string, hostNS ns.NetNS) (netlink.Link, error) { + veth := &netlink.Veth{ + LinkAttrs: netlink.LinkAttrs{ + Name: name, + MTU: mtu, + }, + PeerName: peer, + PeerNamespace: netlink.NsFd(int(hostNS.Fd())), + } + if mac != "" { + m, err := net.ParseMAC(mac) + if err != nil { + return nil, err + } + veth.LinkAttrs.HardwareAddr = m + } + if err := netlink.LinkAdd(veth); err != nil { + return nil, err + } + // Re-fetch the container link to get its creation-time parameters, e.g. index and mac + veth2, err := netlink.LinkByName(name) + if err != nil { + err = netlink.LinkDel(veth) // try and clean up the link if possible. + if err != nil { + return nil, err + } + return nil, err + } + + return veth2, nil +} + +func peerExists(name string) bool { + if _, err := netlink.LinkByName(name); err != nil { + return false + } + return true +} + +func makeVeth(name, vethPeerName string, mtu int, mac string, hostNS ns.NetNS) (peerName string, + veth netlink.Link, err error, +) { + for i := 0; i < 10; i++ { + if vethPeerName != "" { + peerName = vethPeerName + } else { + peerName, err = RandomVethName() + if err != nil { + return peerName, veth, err + } + } + + veth, err = makeVethPair(name, peerName, mtu, mac, hostNS) + switch { + case err == nil: + return peerName, veth, nil + + case os.IsExist(err): + if peerExists(peerName) && vethPeerName == "" { + continue + } + return peerName, veth, fmt.Errorf("container veth name provided (%v) already exists", name) + + default: + return peerName, veth, fmt.Errorf("failed to make veth pair: %v", err) + } + } + + // should really never be hit + return "", nil, fmt.Errorf("failed to find a unique veth name") +} + +// RandomVethName returns string "veth" with random prefix (hashed from entropy) +func RandomVethName() (string, error) { + entropy := make([]byte, entropySize) + _, err := rand.Read(entropy) + if err != nil { + return "", fmt.Errorf("failed to generate random veth name: %v", err) + } + + // NetworkManager (recent versions) will ignore veth devices that start with "veth" + return fmt.Sprintf("veth%x", entropy), nil +} + +func RenameLink(curName, newName string) error { + link, err := netlink.LinkByName(curName) + if err == nil { + err = netlink.LinkSetName(link, newName) + } + return err +} + +func ifaceFromNetlinkLink(l netlink.Link) net.Interface { + a := l.Attrs() + return net.Interface{ + Index: a.Index, + MTU: a.MTU, + Name: a.Name, + HardwareAddr: a.HardwareAddr, + Flags: a.Flags, + } +} + +// SetupVethWithName sets up a pair of virtual ethernet devices. +// Call SetupVethWithName from inside the container netns. It will create both veth +// devices and move the host-side veth into the provided hostNS namespace. +// hostVethName: If hostVethName is not specified, the host-side veth name will use a random string. +// On success, SetupVethWithName returns (hostVeth, containerVeth, nil) +func SetupVethWithName(contVethName, hostVethName string, mtu int, contVethMac string, hostNS ns.NetNS) (contIface, + hostIface net.Interface, err error, +) { + hostVethName, contVeth, err := makeVeth(contVethName, hostVethName, mtu, contVethMac, hostNS) + if err != nil { + return net.Interface{}, net.Interface{}, err + } + + var hostVeth netlink.Link + err = hostNS.Do(func(_ ns.NetNS) error { + hostVeth, err = netlink.LinkByName(hostVethName) + if err != nil { + return fmt.Errorf("failed to lookup %q in %q: %v", hostVethName, hostNS.Path(), err) + } + + if err = netlink.LinkSetUp(hostVeth); err != nil { + return fmt.Errorf("failed to set %q up: %v", hostVethName, err) + } + + // we want to own the routes for this interface + _, err = sysctl.Sysctl(fmt.Sprintf("net/ipv6/conf/%s/accept_ra", hostVethName), "0") + if err != nil { + return fmt.Errorf("sysctl error, hostVethName %s: %v", hostVethName, err) + } + return nil + }) + if err != nil { + return net.Interface{}, net.Interface{}, err + } + return ifaceFromNetlinkLink(hostVeth), ifaceFromNetlinkLink(contVeth), nil +} + +// SetupVeth sets up a pair of virtual ethernet devices. +// Call SetupVeth from inside the container netns. It will create both veth +// devices and move the host-side veth into the provided hostNS namespace. +// On success, SetupVeth returns (hostVeth, containerVeth, nil) +func SetupVeth(contVethName string, mtu int, contVethMac string, hostNS ns.NetNS) (contIface, hostIface net.Interface, + err error, +) { + return SetupVethWithName(contVethName, "", mtu, contVethMac, hostNS) +} + +// DelLinkByName removes an interface link. +func DelLinkByName(ifName string) error { + iface, err := netlink.LinkByName(ifName) + if err != nil { + if _, ok := err.(netlink.LinkNotFoundError); ok { + return ErrLinkNotFound + } + return fmt.Errorf("failed to lookup %q: %v", ifName, err) + } + + if err = netlink.LinkDel(iface); err != nil { + return fmt.Errorf("failed to delete %q: %v", ifName, err) + } + + return nil +} + +// DelLinkByNameAddr remove an interface and returns its addresses +func DelLinkByNameAddr(ifName string) ([]*net.IPNet, error) { + iface, err := netlink.LinkByName(ifName) + if err != nil { + if _, ok := err.(netlink.LinkNotFoundError); ok { + return nil, ErrLinkNotFound + } + return nil, fmt.Errorf("failed to lookup %q: %v", ifName, err) + } + + addrs, err := netlink.AddrList(iface, netlink.FAMILY_ALL) + if err != nil { + return nil, fmt.Errorf("failed to get IP addresses for %q: %v", ifName, err) + } + + if err = netlink.LinkDel(iface); err != nil { + return nil, fmt.Errorf("failed to delete %q: %v", ifName, err) + } + + out := []*net.IPNet{} + for _, addr := range addrs { + if addr.IP.IsGlobalUnicast() { + out = append(out, addr.IPNet) + } + } + + return out, nil +} + +// GetVethPeerIfindex returns the veth link object, the peer ifindex of the +// veth, or an error. This peer ifindex will only be valid in the peer's +// network namespace. +func GetVethPeerIfindex(ifName string) (netlink.Link, int, error) { + link, err := netlink.LinkByName(ifName) + if err != nil { + return nil, -1, fmt.Errorf("could not look up %q: %v", ifName, err) + } + if _, ok := link.(*netlink.Veth); !ok { + return nil, -1, fmt.Errorf("interface %q was not a veth interface", ifName) + } + + // veth supports IFLA_LINK (what vishvananda/netlink calls ParentIndex) + // on 4.1 and higher kernels + peerIndex := link.Attrs().ParentIndex + if peerIndex <= 0 { + // Fall back to ethtool for 4.0 and earlier kernels + e, err := ethtool.NewEthtool() + if err != nil { + return nil, -1, fmt.Errorf("failed to initialize ethtool: %v", err) + } + defer e.Close() + + stats, err := e.Stats(link.Attrs().Name) + if err != nil { + return nil, -1, fmt.Errorf("failed to request ethtool stats: %v", err) + } + n, ok := stats["peer_ifindex"] + if !ok { + return nil, -1, fmt.Errorf("failed to find 'peer_ifindex' in ethtool stats") + } + if n > 32767 || n == 0 { + return nil, -1, fmt.Errorf("invalid 'peer_ifindex' %d", n) + } + peerIndex = int(n) + } + + return link, peerIndex, nil +} diff --git a/edge/pkg/edged/bandwidth/tclinux/netlink_interface_linux.go b/edge/pkg/edged/bandwidth/tclinux/netlink_interface_linux.go new file mode 100644 index 00000000000..3596d4e38ae --- /dev/null +++ b/edge/pkg/edged/bandwidth/tclinux/netlink_interface_linux.go @@ -0,0 +1,183 @@ +package tclinux + +import ( + "bytes" + "crypto/sha512" + "fmt" + "os/exec" + "strconv" + "strings" + + "k8s.io/klog/v2" +) + +const ( + Prefix = "bwp" + Length = 15 + commandTemplate = "ip a |grep %s | awk '/mtu/ {print $5}'" +) + +// key:podName,val:deviceName array +var podNetworkDeviceMapping = make(map[string][]string) + +func StorePodNetworkDeviceMapping(podName, deviceName string) { + // check the device exists in the current pod + if _, ok := podNetworkDeviceMapping[podName]; ok { + devices := podNetworkDeviceMapping[podName] + if len(devices) == expectedDeviceCount { + // If it is already included, there is no need to replace it. + if containsNetlink(devices, deviceName) { + klog.Infof("pod `%s` %s netlink has existed", podName, deviceName) + return + } + klog.Warningf("pod `%s` has existed two netlink:%v,now replace as %s", podName, devices, deviceName) + // replace network device + index := getNetlinkIndex(deviceName, devices) + podNetworkDeviceMapping[podName][index] = deviceName + return + } + // check the current device is included + if containsNetlink(devices, deviceName) { + return + } + podNetworkDeviceMapping[podName] = append(podNetworkDeviceMapping[podName], deviceName) + } else { + // add device + podNetworkDeviceMapping[podName] = []string{deviceName} + } +} + +func containsNetlink(devices []string, deviceName string) bool { + if deviceName == "" || len(devices) == 0 { + return false + } + for _, v := range devices { + if strings.EqualFold(deviceName, v) { + return true + } + } + return false +} + +func getNetlinkIndex(deviceName string, devices []string) int { + if deviceName == "" || len(devices) == 0 { + return 0 + } + if strings.HasPrefix(deviceName, Prefix) { + // get the subscript of ifb network interface + for index, v := range devices { + if strings.HasPrefix(v, Prefix) { + return index + } + } + return 1 + } + return 0 +} + +// get pod host network interface vethxxx +func GetNetworkDeviceByPod(podName string) string { + if v, ok := podNetworkDeviceMapping[podName]; ok { + // check is a container host network interface + if v, ok := isHostNetworkDeviceExist(v); ok { + return v + } + return "" + } + return "" +} + +func GetIfbDeviceByPod(podName string) string { + if v, ok := podNetworkDeviceMapping[podName]; ok { + // check is a container host network interface + if name, flag := isIfbDeviceExist(v); flag { + return name + } + return "" + } + return "" +} + +func DeleteNetworkDevice(podName string) { + if v, ok := podNetworkDeviceMapping[podName]; ok { + for _, ifblink := range v { + if strings.HasPrefix(ifblink, Prefix) { + klog.Infof("delete pod `%s` ifb netlink interface:%s", podName, ifblink) + // delete ifb interface + err := DelLinkByName(ifblink) + if err != nil { + klog.Warningf("delete ifb netlink interface device failed,please delete it manual,ip "+ + "link del dev %s,err:%v", ifblink, err) + } + } + } + // delete the key->podName (cache)) + delete(podNetworkDeviceMapping, podName) + return + } + klog.Warningf("pod `%s` netlink interface device does not exist", podName) +} + +// getMTU retrieves the MTU value based on the given interface name. +func GetMTU(interfaceName string) (int, error) { + // Use the constant command template to format the command + cmd := exec.Command("bash", "-c", fmt.Sprintf(commandTemplate, interfaceName)) + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out // Capture stderr as well + + if err := cmd.Run(); err != nil { + return 0, fmt.Errorf("failed to run command: %s, output: %s", err, out.String()) + } + + // Trim whitespace and convert output to integer + mtuStr := strings.TrimSpace(out.String()) + mtu, err := strconv.Atoi(mtuStr) + if err != nil { + return 0, fmt.Errorf("failed to convert MTU to int: %s", err) + } + + return mtu, nil // Return the resulting MTU value as an integer +} + +// calculate ifb device name to ensure standard and unique +func CalIfbName(containerIDOrName string) string { + output := sha512.Sum512([]byte(containerIDOrName)) + return fmt.Sprintf("%s%x", Prefix, output)[:Length] +} + +func isIfbDeviceExist(devices []string) (string, bool) { + for _, v := range devices { + if strings.HasPrefix(v, Prefix) { + return v, true + } + } + return "", false +} + +func isHostNetworkDeviceExist(devices []string) (string, bool) { + flag := false + hostDevice := "" + if len(devices) == 0 { + return hostDevice, false + } + for _, v := range devices { + if !strings.HasPrefix(v, Prefix) { + flag = true + } + } + if len(devices) == 1 && flag { + return devices[0], true + } + if len(devices) == expectedDeviceCount && flag { + // get ifb interface + for _, v := range devices { + if !strings.HasPrefix(v, Prefix) { + hostDevice = v + return hostDevice, flag + } + } + return hostDevice, true + } + return hostDevice, flag +} diff --git a/edge/pkg/edged/bandwidth/tclinux/ns/ns.go b/edge/pkg/edged/bandwidth/tclinux/ns/ns.go new file mode 100644 index 00000000000..8dd2e20d6de --- /dev/null +++ b/edge/pkg/edged/bandwidth/tclinux/ns/ns.go @@ -0,0 +1,31 @@ +package ns + +type NetNS interface { + // Executes the passed closure in this object's network namespace, + // attempting to restore the original namespace before returning. + // However, since each OS thread can have a different network namespace, + // and Go's thread scheduling is highly variable, callers cannot + // guarantee any specific namespace is set unless operations that + // require that namespace are wrapped with Do(). Also, no code called + // from Do() should call runtime.UnlockOSThread(), or the risk + // of executing code in an incorrect namespace will be greater. See + // https://github.com/golang/go/wiki/LockOSThread for further details. + Do(toRun func(NetNS) error) error + + // Sets the current network namespace to this object's network namespace. + // Note that since Go's thread scheduling is highly variable, callers + // cannot guarantee the requested namespace will be the current namespace + // after this function is called; to ensure this wrap operations that + // require the namespace with Do() instead. + Set() error + + // Returns the filesystem path representing this object's network namespace + Path() string + + // Returns a file descriptor representing this object's network namespace + Fd() uintptr + + // Cleans up this instance of the network namespace; if this instance + // is the last user the namespace will be destroyed + Close() error +} diff --git a/edge/pkg/edged/bandwidth/tclinux/ns/ns_linux.go b/edge/pkg/edged/bandwidth/tclinux/ns/ns_linux.go new file mode 100644 index 00000000000..7cdfe8029a9 --- /dev/null +++ b/edge/pkg/edged/bandwidth/tclinux/ns/ns_linux.go @@ -0,0 +1,184 @@ +package ns + +import ( + "fmt" + "os" + "runtime" + "sync" + "syscall" + + "golang.org/x/sys/unix" +) + +type netNS struct { + file *os.File + closed bool +} + +// Returns an object representing the current OS thread's network namespace +func GetCurrentNS() (NetNS, error) { + // Lock the thread in case other goroutine executes in it and changes its + // network namespace after getCurrentThreadNetNSPath(), otherwise it might + // return an unexpected network namespace. + runtime.LockOSThread() + defer runtime.UnlockOSThread() + return GetNS(getCurrentThreadNetNSPath()) +} + +func getCurrentThreadNetNSPath() string { + // /proc/self/ns/net returns the namespace of the main thread, not + // of whatever thread this goroutine is running on. Make sure we + // use the thread's net namespace since the thread is switching around + return fmt.Sprintf("/proc/%d/task/%d/ns/net", os.Getpid(), unix.Gettid()) +} + +func (ns *netNS) Close() error { + if err := ns.errorIfClosed(); err != nil { + return err + } + + if err := ns.file.Close(); err != nil { + return fmt.Errorf("failed to close %q: %v", ns.file.Name(), err) + } + ns.closed = true + + return nil +} + +func (ns *netNS) Set() error { + if err := ns.errorIfClosed(); err != nil { + return err + } + + if err := unix.Setns(int(ns.Fd()), unix.CLONE_NEWNET); err != nil { + return fmt.Errorf("Error switching to ns %v: %v", ns.file.Name(), err) + } + + return nil +} + +// netNS implements the NetNS interface +var _ NetNS = &netNS{} + +type PathNotExistErr struct{ msg string } + +func (e PathNotExistErr) Error() string { return e.msg } + +type PathNotNSErr struct{ msg string } + +func (e PathNotNSErr) Error() string { return e.msg } + +func IsNSorErr(nspath string) error { + stat := syscall.Statfs_t{} + if err := syscall.Statfs(nspath, &stat); err != nil { + if os.IsNotExist(err) { + err = PathNotExistErr{msg: fmt.Sprintf("failed to Statfs %q: %v", nspath, err)} + } else { + err = fmt.Errorf("failed to Statfs %q: %v", nspath, err) + } + return err + } + + switch stat.Type { + case unix.NSFS_MAGIC, unix.PROC_SUPER_MAGIC: + return nil + default: + return PathNotNSErr{msg: fmt.Sprintf("unknown FS magic on %q: %x", nspath, stat.Type)} + } +} + +// Returns an object representing the namespace referred to by @path +func GetNS(nspath string) (NetNS, error) { + err := IsNSorErr(nspath) + if err != nil { + return nil, err + } + + fd, err := os.Open(nspath) + if err != nil { + return nil, err + } + + return &netNS{file: fd}, nil +} + +func (ns *netNS) Path() string { + return ns.file.Name() +} + +func (ns *netNS) Fd() uintptr { + return ns.file.Fd() +} + +func (ns *netNS) errorIfClosed() error { + if ns.closed { + return fmt.Errorf("%q has already been closed", ns.file.Name()) + } + return nil +} + +func (ns *netNS) Do(toRun func(NetNS) error) error { + if err := ns.errorIfClosed(); err != nil { + return err + } + + containedCall := func(hostNS NetNS) error { + threadNS, err := GetCurrentNS() + if err != nil { + return fmt.Errorf("failed to open current netns: %v", err) + } + defer threadNS.Close() + + // switch to target namespace + if err = ns.Set(); err != nil { + return fmt.Errorf("error switching to ns %v: %v", ns.file.Name(), err) + } + defer func() { + err := threadNS.Set() // switch back + if err == nil { + // Unlock the current thread only when we successfully switched back + // to the original namespace; otherwise leave the thread locked which + // will force the runtime to scrap the current thread, that is maybe + // not as optimal but at least always safe to do. + runtime.UnlockOSThread() + } + }() + + return toRun(hostNS) + } + + // save a handle to current network namespace + hostNS, err := GetCurrentNS() + if err != nil { + return fmt.Errorf("failed to open current namespace: %v", err) + } + defer hostNS.Close() + + var wg sync.WaitGroup + wg.Add(1) + + // Start the callback in a new green thread so that if we later fail + // to switch the namespace back to the original one, we can safely + // leave the thread locked to die without a risk of the current thread + // left lingering with incorrect namespace. + var innerError error + go func() { + defer wg.Done() + runtime.LockOSThread() + innerError = containedCall(hostNS) + }() + wg.Wait() + + return innerError +} + +// WithNetNSPath executes the passed closure under the given network +// namespace, restoring the original namespace afterwards. +func WithNetNSPath(nspath string, toRun func(NetNS) error) error { + ns, err := GetNS(nspath) + if err != nil { + return err + } + defer ns.Close() + return ns.Do(toRun) +} diff --git a/edge/pkg/edged/bandwidth/tclinux/sysctl/sysctl.go b/edge/pkg/edged/bandwidth/tclinux/sysctl/sysctl.go new file mode 100644 index 00000000000..a2cce667282 --- /dev/null +++ b/edge/pkg/edged/bandwidth/tclinux/sysctl/sysctl.go @@ -0,0 +1,65 @@ +package sysctl + +import ( + "fmt" + "os" + "strings" +) + +const filePermissions = 0o600 + +// Sysctl provides a method to set/get values from /proc/sys - in linux systems +// new interface to set/get values of variables formerly handled by sysctl syscall +// If optional `params` have only one string value - this function will +// set this value into corresponding sysctl variable +func Sysctl(name string, params ...string) (string, error) { + if len(params) > 1 { + return "", fmt.Errorf("unexcepted additional parameters") + } else if len(params) == 1 { + return setSysctl(name, params[0]) + } + return getSysctl(name) +} + +func getSysctl(name string) (string, error) { + fullName := "/proc/sys/" + toNormalName(name) + data, err := os.ReadFile(fullName) + if err != nil { + return "", err + } + + return string(data[:len(data)-1]), nil +} + +func setSysctl(name, value string) (string, error) { + fullName := "/proc/sys/" + toNormalName(name) + if err := os.WriteFile(fullName, []byte(value), filePermissions); err != nil { + return "", err + } + + return getSysctl(name) +} + +// Normalize names by using slash as separator +// Sysctl names can use dots or slashes as separator: +// - if dots are used, dots and slashes are interchanged. +// - if slashes are used, slashes and dots are left intact. +// Separator in use is determined by first occurrence. +func toNormalName(name string) string { + interchange := false + for _, c := range name { + if c == '.' { + interchange = true + break + } + if c == '/' { + break + } + } + + if interchange { + r := strings.NewReplacer(".", "/", "/", ".") + return r.Replace(name) + } + return name +} diff --git a/edge/pkg/edged/bandwidth/tclinux/tclinux_darwin.go b/edge/pkg/edged/bandwidth/tclinux/tclinux_darwin.go new file mode 100644 index 00000000000..1f91b61be95 --- /dev/null +++ b/edge/pkg/edged/bandwidth/tclinux/tclinux_darwin.go @@ -0,0 +1,37 @@ +package tclinux + +// tc related functions only take effect under the linux system, and tc related functions are disguised under the mac system + +func DeleteNetworkDevice(_ string) { +} + +func CreateIngressQdisc(_, _ uint64, _ string) error { + return nil +} + +func StorePodNetworkDeviceMapping(_, _ string) { +} + +func GetMTU(_ string) (int, error) { + return -1, nil +} + +func GetIfbDeviceByPod(_ string) string { + return "" +} + +func CalIfbName(_ string) string { + return "" +} + +func CreateIfb(_ string, _ int) error { + return nil +} + +func CreateEgressQdisc(_, _ uint64, _, _ string) error { + return nil +} + +func GetNetworkDeviceByPod(_ string) string { + return "" +} diff --git a/edge/pkg/edged/bandwidth/tclinux/tclinux_windows.go b/edge/pkg/edged/bandwidth/tclinux/tclinux_windows.go new file mode 100644 index 00000000000..10f83d6ed38 --- /dev/null +++ b/edge/pkg/edged/bandwidth/tclinux/tclinux_windows.go @@ -0,0 +1,37 @@ +package tclinux + +// tc related functions only take effect under the linux system, and tc related functions are disguised under the windows system + +func DeleteNetworkDevice(podName string) { +} + +func CreateIngressQdisc(rateInBits, burstInBits uint64, hostDeviceName string) error { + return nil +} + +func StorePodNetworkDeviceMapping(podName, deviceName string) { +} + +func GetMTU(deviceName string) (int, error) { + return -1, nil +} + +func GetIfbDeviceByPod(podName string) string { + return "" +} + +func CalIfbName(containerIDOrName string) string { + return "" +} + +func CreateIfb(ifbDeviceName string, mtu int) error { + return nil +} + +func CreateEgressQdisc(rateInBits, burstInBits uint64, hostDeviceName, ifbDeviceName string) error { + return nil +} + +func GetNetworkDeviceByPod(podName string) string { + return "" +} diff --git a/edge/pkg/edged/edged.go b/edge/pkg/edged/edged.go index c8ccaf3a86e..b1d0b5a74d0 100644 --- a/edge/pkg/edged/edged.go +++ b/edge/pkg/edged/edged.go @@ -56,6 +56,9 @@ import ( "github.com/kubeedge/kubeedge/common/constants" "github.com/kubeedge/kubeedge/edge/pkg/common/modules" "github.com/kubeedge/kubeedge/edge/pkg/common/util" + "github.com/kubeedge/kubeedge/edge/pkg/edged/bandwidth/consts" + "github.com/kubeedge/kubeedge/edge/pkg/edged/bandwidth/kube" + "github.com/kubeedge/kubeedge/edge/pkg/edged/bandwidth/tclimit" edgedconfig "github.com/kubeedge/kubeedge/edge/pkg/edged/config" kubebridge "github.com/kubeedge/kubeedge/edge/pkg/edged/kubeclientbridge" "github.com/kubeedge/kubeedge/edge/pkg/metamanager" @@ -85,21 +88,23 @@ var DefaultRunLiteKubelet RunLiteKubelet = kubeletserver.Run // edged is the main edged implementation. type edged struct { - enable bool - KubeletServer *kubeletoptions.KubeletServer - KubeletDeps *kubelet.Dependencies - FeatureGate featuregate.FeatureGate - context context.Context - nodeName string - namespace string + enable bool + KubeletServer *kubeletoptions.KubeletServer + KubeletDeps *kubelet.Dependencies + FeatureGate featuregate.FeatureGate + context context.Context + nodeName string + namespace string + metaServerURL string + bandwidthManager bool } var _ core.Module = (*edged)(nil) // Register register edged -func Register(e *v1alpha2.Edged) { +func Register(e *v1alpha2.Edged, metaServerURL string) { edgedconfig.InitConfigure(e) - edged, err := newEdged(e.Enable, e.HostnameOverride, e.RegisterNodeNamespace) + edged, err := newEdged(e.Enable, e.HostnameOverride, e.RegisterNodeNamespace, metaServerURL, e.BandwidthManager) if err != nil { klog.Errorf("init new edged error, %v", err) os.Exit(1) @@ -163,11 +168,15 @@ func (e *edged) Start() { klog.Info("Start sync pod") } + if e.bandwidthManager { + go e.bandwidthLimit() + } + e.syncPod(e.KubeletDeps.PodConfig) } // newEdged creates new edged object and initialises it -func newEdged(enable bool, nodeName, namespace string) (*edged, error) { +func newEdged(enable bool, nodeName, namespace, metaServerURL string, bandwidthManager bool) (*edged, error) { var ed *edged var err error if !enable { @@ -223,13 +232,15 @@ func newEdged(enable bool, nodeName, namespace string) (*edged, error) { kubeletDeps.PodConfig = config.NewPodConfig(config.PodConfigNotificationIncremental, kubeletDeps.Recorder, kubeletDeps.PodStartupLatencyTracker) ed = &edged{ - enable: true, - context: context.Background(), - KubeletServer: &kubeletServer, - KubeletDeps: kubeletDeps, - FeatureGate: utilfeature.DefaultFeatureGate, - nodeName: nodeName, - namespace: namespace, + enable: true, + context: context.Background(), + KubeletServer: &kubeletServer, + KubeletDeps: kubeletDeps, + FeatureGate: utilfeature.DefaultFeatureGate, + nodeName: nodeName, + namespace: namespace, + bandwidthManager: bandwidthManager, + metaServerURL: metaServerURL, } return ed, nil @@ -521,3 +532,20 @@ func kubeletHealthCheck(port int32, kubeletReadyChan chan struct{}) { time.Sleep(50 * time.Millisecond) } } + +func (e *edged) bandwidthLimit() { + // sleep 30s, wait edgecore is started + time.Sleep(consts.TCSleepTimes * time.Second) + // todo listen in syncPod and abandon client + // Initialize metaServer connection + err := kube.InitEdgeClient(fmt.Sprintf("http://%s", e.metaServerURL)) + if err != nil { + klog.Errorf("failed to start bandwidth limit, err: %v", err) + return + } + if err := tclimit.EdgeWatch(beehiveContext.GetContext(), e.nodeName); err != nil { + klog.Errorf("failed to watch pod by informer, err: %v", err) + return + } + klog.Infof("enable edge pod watch...") +} diff --git a/go.mod b/go.mod index c219ffc72ee..009cc07b5ff 100644 --- a/go.mod +++ b/go.mod @@ -61,6 +61,7 @@ require ( github.com/onsi/ginkgo/v2 v2.17.1 github.com/opencontainers/selinux v1.11.0 github.com/pkg/errors v0.9.1 + github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8 github.com/stretchr/testify v1.9.0 github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852 go.opentelemetry.io/otel/trace v1.22.0 diff --git a/go.sum b/go.sum index 69d0ff7ade8..be8e3a348f9 100644 --- a/go.sum +++ b/go.sum @@ -1733,6 +1733,7 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8 h1:2c1EFnZHIPCW8qKWgHMH/fX2PkSabFc5mrVzfUNdg5U= github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= diff --git a/keadm/cmd/keadm/app/cmd/util/edgecoreinstaller.go b/keadm/cmd/keadm/app/cmd/util/edgecoreinstaller.go index 5919fd1491e..b56b7060f2b 100755 --- a/keadm/cmd/keadm/app/cmd/util/edgecoreinstaller.go +++ b/keadm/cmd/keadm/app/cmd/util/edgecoreinstaller.go @@ -27,7 +27,6 @@ import ( "github.com/kubeedge/api/apis/common/constants" "github.com/kubeedge/api/apis/componentconfig/edgecore/v1alpha2" "github.com/kubeedge/api/apis/componentconfig/edgecore/v1alpha2/validation" - types "github.com/kubeedge/kubeedge/keadm/cmd/keadm/app/cmd/common" "github.com/kubeedge/kubeedge/pkg/util" "github.com/kubeedge/kubeedge/pkg/viaduct/pkg/api" diff --git a/staging/src/github.com/kubeedge/api/apis/componentconfig/edgecore/v1alpha2/types.go b/staging/src/github.com/kubeedge/api/apis/componentconfig/edgecore/v1alpha2/types.go index 7fdd64f9692..500ad707c5b 100644 --- a/staging/src/github.com/kubeedge/api/apis/componentconfig/edgecore/v1alpha2/types.go +++ b/staging/src/github.com/kubeedge/api/apis/componentconfig/edgecore/v1alpha2/types.go @@ -115,6 +115,9 @@ type Edged struct { // RegisterNodeNamespace indicates register node namespace // default "default" RegisterNodeNamespace string `json:"registerNodeNamespace,omitempty"` + // BandwidthManager indicates whether bandwidth manager is enabled, + // default false + BandwidthManager bool `json:"bandwidthManager"` } // TailoredKubeletConfiguration indicates the tailored kubelet configuration. diff --git a/vendor/github.com/safchain/ethtool/.gitignore b/vendor/github.com/safchain/ethtool/.gitignore new file mode 100644 index 00000000000..db6cadffd1e --- /dev/null +++ b/vendor/github.com/safchain/ethtool/.gitignore @@ -0,0 +1,27 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +# Skip compiled example binary file +/example/example diff --git a/vendor/github.com/safchain/ethtool/.travis.yml b/vendor/github.com/safchain/ethtool/.travis.yml new file mode 100644 index 00000000000..4f2ee4d9733 --- /dev/null +++ b/vendor/github.com/safchain/ethtool/.travis.yml @@ -0,0 +1 @@ +language: go diff --git a/vendor/github.com/safchain/ethtool/LICENSE b/vendor/github.com/safchain/ethtool/LICENSE new file mode 100644 index 00000000000..8f71f43fee3 --- /dev/null +++ b/vendor/github.com/safchain/ethtool/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. + diff --git a/vendor/github.com/safchain/ethtool/Makefile b/vendor/github.com/safchain/ethtool/Makefile new file mode 100644 index 00000000000..67d2da395f9 --- /dev/null +++ b/vendor/github.com/safchain/ethtool/Makefile @@ -0,0 +1,4 @@ +all: build + +build: + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build diff --git a/vendor/github.com/safchain/ethtool/README.md b/vendor/github.com/safchain/ethtool/README.md new file mode 100644 index 00000000000..1f146229cb4 --- /dev/null +++ b/vendor/github.com/safchain/ethtool/README.md @@ -0,0 +1,60 @@ +# ethtool go package # + +[![Build Status](https://travis-ci.org/safchain/ethtool.png?branch=master)](https://travis-ci.org/safchain/ethtool) +[![GoDoc](https://godoc.org/github.com/safchain/ethtool?status.svg)](https://godoc.org/github.com/safchain/ethtool) + +The ethtool package aims to provide a library giving a simple access to the Linux SIOCETHTOOL ioctl operations. It can be used to retrieve informations from a network device like statistics, driver related informations or even the peer of a VETH interface. + +## Build and Test ## + +go get command: + + go get github.com/safchain/ethtool + +Testing + +In order to run te + + go test github.com/safchain/ethtool + +## Examples ## + +```go +package main + +import ( + "fmt" + + "github.com/safchain/ethtool" +) + +func main() { + ethHandle, err := ethtool.NewEthtool() + if err != nil { + panic(err.Error()) + } + defer ethHandle.Close() + + // Retrieve tx from eth0 + stats, err := ethHandle.Stats("eth0") + if err != nil { + panic(err.Error()) + } + fmt.Printf("TX: %d\n", stats["tx_bytes"]) + + // Retrieve peer index of a veth interface + stats, err = ethHandle.Stats("veth0") + if err != nil { + panic(err.Error()) + } + fmt.Printf("Peer Index: %d\n", stats["peer_ifindex"]) +} +``` + +## LICENSE ## + +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. diff --git a/vendor/github.com/safchain/ethtool/ethtool.go b/vendor/github.com/safchain/ethtool/ethtool.go new file mode 100644 index 00000000000..8dcc78c057b --- /dev/null +++ b/vendor/github.com/safchain/ethtool/ethtool.go @@ -0,0 +1,541 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 ethtool aims to provide a library giving a simple access to the +// Linux SIOCETHTOOL ioctl operations. It can be used to retrieve informations +// from a network device like statistics, driver related informations or +// even the peer of a VETH interface. +package ethtool + +import ( + "bytes" + "encoding/hex" + "fmt" + "strings" + "syscall" + "unsafe" +) + +// Maximum size of an interface name +const ( + IFNAMSIZ = 16 +) + +// ioctl ethtool request +const ( + SIOCETHTOOL = 0x8946 +) + +// ethtool stats related constants. +const ( + ETH_GSTRING_LEN = 32 + ETH_SS_STATS = 1 + ETH_SS_FEATURES = 4 + ETHTOOL_GDRVINFO = 0x00000003 + ETHTOOL_GSTRINGS = 0x0000001b + ETHTOOL_GSTATS = 0x0000001d + // other CMDs from ethtool-copy.h of ethtool-3.5 package + ETHTOOL_GSET = 0x00000001 /* Get settings. */ + ETHTOOL_SSET = 0x00000002 /* Set settings. */ + ETHTOOL_GMSGLVL = 0x00000007 /* Get driver message level */ + ETHTOOL_SMSGLVL = 0x00000008 /* Set driver msg level. */ + /* Get link status for host, i.e. whether the interface *and* the + * physical port (if there is one) are up (ethtool_value). */ + ETHTOOL_GLINK = 0x0000000a + ETHTOOL_GMODULEINFO = 0x00000042 /* Get plug-in module information */ + ETHTOOL_GMODULEEEPROM = 0x00000043 /* Get plug-in module eeprom */ + ETHTOOL_GPERMADDR = 0x00000020 + ETHTOOL_GFEATURES = 0x0000003a /* Get device offload settings */ + ETHTOOL_SFEATURES = 0x0000003b /* Change device offload settings */ + ETHTOOL_GFLAGS = 0x00000025 /* Get flags bitmap(ethtool_value) */ + ETHTOOL_GSSET_INFO = 0x00000037 /* Get string set info */ +) + +// MAX_GSTRINGS maximum number of stats entries that ethtool can +// retrieve currently. +const ( + MAX_GSTRINGS = 1000 + MAX_FEATURE_BLOCKS = (MAX_GSTRINGS + 32 - 1) / 32 + EEPROM_LEN = 640 + PERMADDR_LEN = 32 +) + +type ifreq struct { + ifr_name [IFNAMSIZ]byte + ifr_data uintptr +} + +// following structures comes from uapi/linux/ethtool.h +type ethtoolSsetInfo struct { + cmd uint32 + reserved uint32 + sset_mask uint32 + data uintptr +} + +type ethtoolGetFeaturesBlock struct { + available uint32 + requested uint32 + active uint32 + never_changed uint32 +} + +type ethtoolGfeatures struct { + cmd uint32 + size uint32 + blocks [MAX_FEATURE_BLOCKS]ethtoolGetFeaturesBlock +} + +type ethtoolSetFeaturesBlock struct { + valid uint32 + requested uint32 +} + +type ethtoolSfeatures struct { + cmd uint32 + size uint32 + blocks [MAX_FEATURE_BLOCKS]ethtoolSetFeaturesBlock +} + +type ethtoolDrvInfo struct { + cmd uint32 + driver [32]byte + version [32]byte + fw_version [32]byte + bus_info [32]byte + erom_version [32]byte + reserved2 [12]byte + n_priv_flags uint32 + n_stats uint32 + testinfo_len uint32 + eedump_len uint32 + regdump_len uint32 +} + +type ethtoolGStrings struct { + cmd uint32 + string_set uint32 + len uint32 + data [MAX_GSTRINGS * ETH_GSTRING_LEN]byte +} + +type ethtoolStats struct { + cmd uint32 + n_stats uint32 + data [MAX_GSTRINGS]uint64 +} + +type ethtoolEeprom struct { + cmd uint32 + magic uint32 + offset uint32 + len uint32 + data [EEPROM_LEN]byte +} + +type ethtoolModInfo struct { + cmd uint32 + tpe uint32 + eeprom_len uint32 + reserved [8]uint32 +} + +type ethtoolLink struct { + cmd uint32 + data uint32 +} + +type ethtoolPermAddr struct { + cmd uint32 + size uint32 + data [PERMADDR_LEN]byte +} + +type Ethtool struct { + fd int +} + +// DriverName returns the driver name of the given interface name. +func (e *Ethtool) DriverName(intf string) (string, error) { + info, err := e.getDriverInfo(intf) + if err != nil { + return "", err + } + return string(bytes.Trim(info.driver[:], "\x00")), nil +} + +// BusInfo returns the bus information of the given interface name. +func (e *Ethtool) BusInfo(intf string) (string, error) { + info, err := e.getDriverInfo(intf) + if err != nil { + return "", err + } + return string(bytes.Trim(info.bus_info[:], "\x00")), nil +} + +// ModuleEeprom returns Eeprom information of the given interface name. +func (e *Ethtool) ModuleEeprom(intf string) ([]byte, error) { + eeprom, _, err := e.getModuleEeprom(intf) + if err != nil { + return nil, err + } + + return eeprom.data[:eeprom.len], nil +} + +// ModuleEeprom returns Eeprom information of the given interface name. +func (e *Ethtool) ModuleEepromHex(intf string) (string, error) { + eeprom, _, err := e.getModuleEeprom(intf) + if err != nil { + return "", err + } + + return hex.EncodeToString(eeprom.data[:eeprom.len]), nil +} + +// DriverInfo returns driver information of the given interface name. +func (e *Ethtool) DriverInfo(intf string) (ethtoolDrvInfo, error) { + drvInfo, err := e.getDriverInfo(intf) + if err != nil { + return ethtoolDrvInfo{}, err + } + + return drvInfo, nil +} + +// PermAddr returns permanent address of the given interface name. +func (e *Ethtool) PermAddr(intf string) (string, error) { + permAddr, err := e.getPermAddr(intf) + if err != nil { + return "", err + } + + if permAddr.data[0] == 0 && permAddr.data[1] == 0 && + permAddr.data[2] == 0 && permAddr.data[3] == 0 && + permAddr.data[4] == 0 && permAddr.data[5] == 0 { + return "", nil + } + + return fmt.Sprintf("%x:%x:%x:%x:%x:%x", + permAddr.data[0:1], + permAddr.data[1:2], + permAddr.data[2:3], + permAddr.data[3:4], + permAddr.data[4:5], + permAddr.data[5:6], + ), nil +} + +func (e *Ethtool) ioctl(intf string, data uintptr) error { + var name [IFNAMSIZ]byte + copy(name[:], []byte(intf)) + + ifr := ifreq{ + ifr_name: name, + ifr_data: data, + } + + _, _, ep := syscall.Syscall(syscall.SYS_IOCTL, uintptr(e.fd), SIOCETHTOOL, uintptr(unsafe.Pointer(&ifr))) + if ep != 0 { + return syscall.Errno(ep) + } + + return nil +} + +func (e *Ethtool) getDriverInfo(intf string) (ethtoolDrvInfo, error) { + drvinfo := ethtoolDrvInfo{ + cmd: ETHTOOL_GDRVINFO, + } + + if err := e.ioctl(intf, uintptr(unsafe.Pointer(&drvinfo))); err != nil { + return ethtoolDrvInfo{}, err + } + + return drvinfo, nil +} + +func (e *Ethtool) getPermAddr(intf string) (ethtoolPermAddr, error) { + permAddr := ethtoolPermAddr{ + cmd: ETHTOOL_GPERMADDR, + size: PERMADDR_LEN, + } + + if err := e.ioctl(intf, uintptr(unsafe.Pointer(&permAddr))); err != nil { + return ethtoolPermAddr{}, err + } + + return permAddr, nil +} + +func (e *Ethtool) getModuleEeprom(intf string) (ethtoolEeprom, ethtoolModInfo, error) { + modInfo := ethtoolModInfo{ + cmd: ETHTOOL_GMODULEINFO, + } + + if err := e.ioctl(intf, uintptr(unsafe.Pointer(&modInfo))); err != nil { + return ethtoolEeprom{}, ethtoolModInfo{}, err + } + + eeprom := ethtoolEeprom{ + cmd: ETHTOOL_GMODULEEEPROM, + len: modInfo.eeprom_len, + offset: 0, + } + + if modInfo.eeprom_len > EEPROM_LEN { + return ethtoolEeprom{}, ethtoolModInfo{}, fmt.Errorf("eeprom size: %d is larger than buffer size: %d", modInfo.eeprom_len, EEPROM_LEN) + } + + if err := e.ioctl(intf, uintptr(unsafe.Pointer(&eeprom))); err != nil { + return ethtoolEeprom{}, ethtoolModInfo{}, err + } + + return eeprom, modInfo, nil +} + +func isFeatureBitSet(blocks [MAX_FEATURE_BLOCKS]ethtoolGetFeaturesBlock, index uint) bool { + return (blocks)[index/32].active&(1<<(index%32)) != 0 +} + +func setFeatureBit(blocks *[MAX_FEATURE_BLOCKS]ethtoolSetFeaturesBlock, index uint, value bool) { + blockIndex, bitIndex := index/32, index%32 + + blocks[blockIndex].valid |= 1 << bitIndex + + if value { + blocks[blockIndex].requested |= 1 << bitIndex + } else { + blocks[blockIndex].requested &= ^(1 << bitIndex) + } +} + +// FeatureNames shows supported features by their name. +func (e *Ethtool) FeatureNames(intf string) (map[string]uint, error) { + ssetInfo := ethtoolSsetInfo{ + cmd: ETHTOOL_GSSET_INFO, + sset_mask: 1 << ETH_SS_FEATURES, + } + + if err := e.ioctl(intf, uintptr(unsafe.Pointer(&ssetInfo))); err != nil { + return nil, err + } + + length := uint32(ssetInfo.data) + if length == 0 { + return map[string]uint{}, nil + } else if length > MAX_GSTRINGS { + return nil, fmt.Errorf("ethtool currently doesn't support more than %d entries, received %d", MAX_GSTRINGS, length) + } + + gstrings := ethtoolGStrings{ + cmd: ETHTOOL_GSTRINGS, + string_set: ETH_SS_FEATURES, + len: length, + data: [MAX_GSTRINGS * ETH_GSTRING_LEN]byte{}, + } + + if err := e.ioctl(intf, uintptr(unsafe.Pointer(&gstrings))); err != nil { + return nil, err + } + + var result = make(map[string]uint) + for i := 0; i != int(length); i++ { + b := gstrings.data[i*ETH_GSTRING_LEN : i*ETH_GSTRING_LEN+ETH_GSTRING_LEN] + key := string(bytes.Trim(b, "\x00")) + if key != "" { + result[key] = uint(i) + } + } + + return result, nil +} + +// Features retrieves features of the given interface name. +func (e *Ethtool) Features(intf string) (map[string]bool, error) { + names, err := e.FeatureNames(intf) + if err != nil { + return nil, err + } + + length := uint32(len(names)) + if length == 0 { + return map[string]bool{}, nil + } + + features := ethtoolGfeatures{ + cmd: ETHTOOL_GFEATURES, + size: (length + 32 - 1) / 32, + } + + if err := e.ioctl(intf, uintptr(unsafe.Pointer(&features))); err != nil { + return nil, err + } + + var result = make(map[string]bool, length) + for key, index := range names { + result[key] = isFeatureBitSet(features.blocks, index) + } + + return result, nil +} + +// Change requests a change in the given device's features. +func (e *Ethtool) Change(intf string, config map[string]bool) error { + names, err := e.FeatureNames(intf) + if err != nil { + return err + } + + length := uint32(len(names)) + + features := ethtoolSfeatures{ + cmd: ETHTOOL_SFEATURES, + size: (length + 32 - 1) / 32, + } + + for key, value := range config { + if index, ok := names[key]; ok { + setFeatureBit(&features.blocks, index, value) + } else { + return fmt.Errorf("unsupported feature %q", key) + } + } + + return e.ioctl(intf, uintptr(unsafe.Pointer(&features))) +} + +// Get state of a link. +func (e *Ethtool) LinkState(intf string) (uint32, error) { + x := ethtoolLink{ + cmd: ETHTOOL_GLINK, + } + + if err := e.ioctl(intf, uintptr(unsafe.Pointer(&x))); err != nil { + return 0, err + } + + return x.data, nil +} + +// Stats retrieves stats of the given interface name. +func (e *Ethtool) Stats(intf string) (map[string]uint64, error) { + drvinfo := ethtoolDrvInfo{ + cmd: ETHTOOL_GDRVINFO, + } + + if err := e.ioctl(intf, uintptr(unsafe.Pointer(&drvinfo))); err != nil { + return nil, err + } + + if drvinfo.n_stats*ETH_GSTRING_LEN > MAX_GSTRINGS*ETH_GSTRING_LEN { + return nil, fmt.Errorf("ethtool currently doesn't support more than %d entries, received %d", MAX_GSTRINGS, drvinfo.n_stats) + } + + gstrings := ethtoolGStrings{ + cmd: ETHTOOL_GSTRINGS, + string_set: ETH_SS_STATS, + len: drvinfo.n_stats, + data: [MAX_GSTRINGS * ETH_GSTRING_LEN]byte{}, + } + + if err := e.ioctl(intf, uintptr(unsafe.Pointer(&gstrings))); err != nil { + return nil, err + } + + stats := ethtoolStats{ + cmd: ETHTOOL_GSTATS, + n_stats: drvinfo.n_stats, + data: [MAX_GSTRINGS]uint64{}, + } + + if err := e.ioctl(intf, uintptr(unsafe.Pointer(&stats))); err != nil { + return nil, err + } + + var result = make(map[string]uint64) + for i := 0; i != int(drvinfo.n_stats); i++ { + b := gstrings.data[i*ETH_GSTRING_LEN : i*ETH_GSTRING_LEN+ETH_GSTRING_LEN] + key := string(b[:strings.Index(string(b), "\x00")]) + if len(key) != 0 { + result[key] = stats.data[i] + } + } + + return result, nil +} + +// Close closes the ethool handler +func (e *Ethtool) Close() { + syscall.Close(e.fd) +} + +// NewEthtool returns a new ethtool handler +func NewEthtool() (*Ethtool, error) { + fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_IP) + if err != nil { + return nil, err + } + + return &Ethtool{ + fd: int(fd), + }, nil +} + +// BusInfo returns bus information of the given interface name. +func BusInfo(intf string) (string, error) { + e, err := NewEthtool() + if err != nil { + return "", err + } + defer e.Close() + return e.BusInfo(intf) +} + +// DriverName returns the driver name of the given interface name. +func DriverName(intf string) (string, error) { + e, err := NewEthtool() + if err != nil { + return "", err + } + defer e.Close() + return e.DriverName(intf) +} + +// Stats retrieves stats of the given interface name. +func Stats(intf string) (map[string]uint64, error) { + e, err := NewEthtool() + if err != nil { + return nil, err + } + defer e.Close() + return e.Stats(intf) +} + +// PermAddr returns permanent address of the given interface name. +func PermAddr(intf string) (string, error) { + e, err := NewEthtool() + if err != nil { + return "", err + } + defer e.Close() + return e.PermAddr(intf) +} diff --git a/vendor/github.com/safchain/ethtool/ethtool_cmd.go b/vendor/github.com/safchain/ethtool/ethtool_cmd.go new file mode 100644 index 00000000000..d0c35e47639 --- /dev/null +++ b/vendor/github.com/safchain/ethtool/ethtool_cmd.go @@ -0,0 +1,207 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 ethtool aims to provide a library giving a simple access to the +// Linux SIOCETHTOOL ioctl operations. It can be used to retrieve informations +// from a network device like statistics, driver related informations or +// even the peer of a VETH interface. +package ethtool + +import ( + "math" + "reflect" + "syscall" + "unsafe" +) + +type EthtoolCmd struct { /* ethtool.c: struct ethtool_cmd */ + Cmd uint32 + Supported uint32 + Advertising uint32 + Speed uint16 + Duplex uint8 + Port uint8 + Phy_address uint8 + Transceiver uint8 + Autoneg uint8 + Mdio_support uint8 + Maxtxpkt uint32 + Maxrxpkt uint32 + Speed_hi uint16 + Eth_tp_mdix uint8 + Reserved2 uint8 + Lp_advertising uint32 + Reserved [2]uint32 +} + +// CmdGet returns the interface settings in the receiver struct +// and returns speed +func (ecmd *EthtoolCmd) CmdGet(intf string) (uint32, error) { + e, err := NewEthtool() + if err != nil { + return 0, err + } + defer e.Close() + return e.CmdGet(ecmd, intf) +} + +// CmdSet sets and returns the settings in the receiver struct +// and returns speed +func (ecmd *EthtoolCmd) CmdSet(intf string) (uint32, error) { + e, err := NewEthtool() + if err != nil { + return 0, err + } + defer e.Close() + return e.CmdSet(ecmd, intf) +} + +func (f *EthtoolCmd) reflect(retv *map[string]uint64) { + val := reflect.ValueOf(f).Elem() + + for i := 0; i < val.NumField(); i++ { + valueField := val.Field(i) + typeField := val.Type().Field(i) + + t := valueField.Interface() + //tt := reflect.TypeOf(t) + //fmt.Printf(" t %T %v tt %T %v\n", t, t, tt, tt) + switch t.(type) { + case uint32: + //fmt.Printf(" t is uint32\n") + (*retv)[typeField.Name] = uint64(t.(uint32)) + case uint16: + (*retv)[typeField.Name] = uint64(t.(uint16)) + case uint8: + (*retv)[typeField.Name] = uint64(t.(uint8)) + case int32: + (*retv)[typeField.Name] = uint64(t.(int32)) + case int16: + (*retv)[typeField.Name] = uint64(t.(int16)) + case int8: + (*retv)[typeField.Name] = uint64(t.(int8)) + default: + (*retv)[typeField.Name+"_unknown_type"] = 0 + } + + //tag := typeField.Tag + //fmt.Printf("Field Name: %s,\t Field Value: %v,\t Tag Value: %s\n", + // typeField.Name, valueField.Interface(), tag.Get("tag_name")) + } +} + +// CmdGet returns the interface settings in the receiver struct +// and returns speed +func (e *Ethtool) CmdGet(ecmd *EthtoolCmd, intf string) (uint32, error) { + ecmd.Cmd = ETHTOOL_GSET + + var name [IFNAMSIZ]byte + copy(name[:], []byte(intf)) + + ifr := ifreq{ + ifr_name: name, + ifr_data: uintptr(unsafe.Pointer(ecmd)), + } + + _, _, ep := syscall.Syscall(syscall.SYS_IOCTL, uintptr(e.fd), + SIOCETHTOOL, uintptr(unsafe.Pointer(&ifr))) + if ep != 0 { + return 0, syscall.Errno(ep) + } + + var speedval uint32 = (uint32(ecmd.Speed_hi) << 16) | + (uint32(ecmd.Speed) & 0xffff) + if speedval == math.MaxUint16 { + speedval = math.MaxUint32 + } + + return speedval, nil +} + +// CmdSet sets and returns the settings in the receiver struct +// and returns speed +func (e *Ethtool) CmdSet(ecmd *EthtoolCmd, intf string) (uint32, error) { + ecmd.Cmd = ETHTOOL_SSET + + var name [IFNAMSIZ]byte + copy(name[:], []byte(intf)) + + ifr := ifreq{ + ifr_name: name, + ifr_data: uintptr(unsafe.Pointer(ecmd)), + } + + _, _, ep := syscall.Syscall(syscall.SYS_IOCTL, uintptr(e.fd), + SIOCETHTOOL, uintptr(unsafe.Pointer(&ifr))) + if ep != 0 { + return 0, syscall.Errno(ep) + } + + var speedval uint32 = (uint32(ecmd.Speed_hi) << 16) | + (uint32(ecmd.Speed) & 0xffff) + if speedval == math.MaxUint16 { + speedval = math.MaxUint32 + } + + return speedval, nil +} + +// CmdGetMapped returns the interface settings in a map +func (e *Ethtool) CmdGetMapped(intf string) (map[string]uint64, error) { + ecmd := EthtoolCmd{ + Cmd: ETHTOOL_GSET, + } + + var name [IFNAMSIZ]byte + copy(name[:], []byte(intf)) + + ifr := ifreq{ + ifr_name: name, + ifr_data: uintptr(unsafe.Pointer(&ecmd)), + } + + _, _, ep := syscall.Syscall(syscall.SYS_IOCTL, uintptr(e.fd), + SIOCETHTOOL, uintptr(unsafe.Pointer(&ifr))) + if ep != 0 { + return nil, syscall.Errno(ep) + } + + var result = make(map[string]uint64) + + // ref https://gist.github.com/drewolson/4771479 + // Golang Reflection Example + ecmd.reflect(&result) + + var speedval uint32 = (uint32(ecmd.Speed_hi) << 16) | + (uint32(ecmd.Speed) & 0xffff) + result["speed"] = uint64(speedval) + + return result, nil +} + +func CmdGetMapped(intf string) (map[string]uint64, error) { + e, err := NewEthtool() + if err != nil { + return nil, err + } + defer e.Close() + return e.CmdGetMapped(intf) +} diff --git a/vendor/github.com/safchain/ethtool/ethtool_msglvl.go b/vendor/github.com/safchain/ethtool/ethtool_msglvl.go new file mode 100644 index 00000000000..91836f01905 --- /dev/null +++ b/vendor/github.com/safchain/ethtool/ethtool_msglvl.go @@ -0,0 +1,113 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 ethtool aims to provide a library giving a simple access to the +// Linux SIOCETHTOOL ioctl operations. It can be used to retrieve informations +// from a network device like statistics, driver related informations or +// even the peer of a VETH interface. +package ethtool + +import ( + "syscall" + "unsafe" +) + +type ethtoolValue struct { /* ethtool.c: struct ethtool_value */ + cmd uint32 + data uint32 +} + +// MsglvlGet returns the msglvl of the given interface. +func (e *Ethtool) MsglvlGet(intf string) (uint32, error) { + edata := ethtoolValue{ + cmd: ETHTOOL_GMSGLVL, + } + + var name [IFNAMSIZ]byte + copy(name[:], []byte(intf)) + + ifr := ifreq{ + ifr_name: name, + ifr_data: uintptr(unsafe.Pointer(&edata)), + } + + _, _, ep := syscall.Syscall(syscall.SYS_IOCTL, uintptr(e.fd), + SIOCETHTOOL, uintptr(unsafe.Pointer(&ifr))) + if ep != 0 { + return 0, syscall.Errno(ep) + } + + return edata.data, nil +} + +// MsglvlSet returns the read-msglvl, post-set-msglvl of the given interface. +func (e *Ethtool) MsglvlSet(intf string, valset uint32) (uint32, uint32, error) { + edata := ethtoolValue{ + cmd: ETHTOOL_GMSGLVL, + } + + var name [IFNAMSIZ]byte + copy(name[:], []byte(intf)) + + ifr := ifreq{ + ifr_name: name, + ifr_data: uintptr(unsafe.Pointer(&edata)), + } + + _, _, ep := syscall.Syscall(syscall.SYS_IOCTL, uintptr(e.fd), + SIOCETHTOOL, uintptr(unsafe.Pointer(&ifr))) + if ep != 0 { + return 0, 0, syscall.Errno(ep) + } + + readval := edata.data + + edata.cmd = ETHTOOL_SMSGLVL + edata.data = valset + + _, _, ep = syscall.Syscall(syscall.SYS_IOCTL, uintptr(e.fd), + SIOCETHTOOL, uintptr(unsafe.Pointer(&ifr))) + if ep != 0 { + return 0, 0, syscall.Errno(ep) + } + + return readval, edata.data, nil +} + +// MsglvlGet returns the msglvl of the given interface. +func MsglvlGet(intf string) (uint32, error) { + e, err := NewEthtool() + if err != nil { + return 0, err + } + defer e.Close() + return e.MsglvlGet(intf) +} + +// MsglvlSet returns the read-msglvl, post-set-msglvl of the given interface. +func MsglvlSet(intf string, valset uint32) (uint32, uint32, error) { + e, err := NewEthtool() + if err != nil { + return 0, 0, err + } + defer e.Close() + return e.MsglvlSet(intf, valset) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 64cbe11df45..11b7a3d699a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -856,6 +856,9 @@ github.com/rubenv/sql-migrate/sqlparse # github.com/russross/blackfriday/v2 v2.1.0 ## explicit github.com/russross/blackfriday/v2 +# github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8 +## explicit +github.com/safchain/ethtool # github.com/seccomp/libseccomp-golang v0.10.0 ## explicit; go 1.14 github.com/seccomp/libseccomp-golang