From 49fff18d7df0871512419c98eec352a4f6b42282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AD=A6=E7=A7=AF=E8=B6=85?= Date: Thu, 29 Aug 2024 09:17:59 +0800 Subject: [PATCH] syscontainer-tools: support-Vnic-specifying-network-namespace --- ...rt-Vnic-specifying-network-namespace.patch | 1682 +++++++++++++++++ syscontainer-tools.spec | 9 +- 2 files changed, 1690 insertions(+), 1 deletion(-) create mode 100644 0012-support-Vnic-specifying-network-namespace.patch diff --git a/0012-support-Vnic-specifying-network-namespace.patch b/0012-support-Vnic-specifying-network-namespace.patch new file mode 100644 index 0000000..dee7672 --- /dev/null +++ b/0012-support-Vnic-specifying-network-namespace.patch @@ -0,0 +1,1682 @@ +From c779e4b97de5eed63184c8183b39a7d1727105dd Mon Sep 17 00:00:00 2001 +From: wujichao +Date: Thu, 29 Aug 2024 12:00:46 +0800 +Subject: [PATCH] support Vnic specifying network namespace + +--- + integration_cover.sh | 94 +++++++ + libnetwork/drivers/common/driver.go | 31 ++- + libnetwork/drivers/common/driver_test.go | 21 +- + libnetwork/drivers/driver.go | 8 + + libnetwork/drivers/veth/driver.go | 191 +++++++------- + libnetwork/drivers/veth/driver_test.go | 245 ++++++++++++++++++ + libnetwork/interfaces.go | 10 +- + libnetwork/nsutils/ns_utils.go | 33 +++ + libnetwork/nsutils/ns_utils_test.go | 121 +++++++++ + network.go | 23 +- + test/netns_test_cover.sh | 302 +++++++++++++++++++++++ + types/network.go | 56 ++++- + types/network_test.go | 262 ++++++++++++++++++++ + 13 files changed, 1272 insertions(+), 125 deletions(-) + create mode 100644 integration_cover.sh + create mode 100644 libnetwork/drivers/veth/driver_test.go + create mode 100644 libnetwork/nsutils/ns_utils_test.go + create mode 100644 test/netns_test_cover.sh + +diff --git a/integration_cover.sh b/integration_cover.sh +new file mode 100644 +index 0000000..ae1af79 +--- /dev/null ++++ b/integration_cover.sh +@@ -0,0 +1,94 @@ ++### ++ # Copyright (c) Huawei Technologies Co., Ltd. 2021-2024. All rights reserved. ++ # syscontainer-tools licensed under the Mulan PSL v2. ++ # You can use this software according to the terms and conditions of the Mulan PSL v2. ++ # You may obtain a copy of Mulan PSL v2 at: ++ # http://license.coscl.org.cn/MulanPSL2 ++ # THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR ++ # IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR ++ # PURPOSE. ++ # See the Mulan PSL v2 for more details. ++ # Author: syscontainer-tools Team ++ # Date: 2024-07-25 ++ # Description: This file is used for coverage ++### ++ ++# 获取当前git仓库的路径 ++project_root=$(git rev-parse --show-toplevel 2>/dev/null) ++if [ -z "$git_dir" ]; then ++ project_root=$(cd "$(dirname "$0")" && pwd) ++fi ++echo "project directory is ${project_root}" ++ ++## 项目所使用的目录 ++cover_pkg=${project_root}/coverage/ ++build_pkg=${project_root}/build/ ++test_pkg=${project_root}/test/ ++log_pkg=${project_root}/log/ ++ ++## 集成测试覆盖率数据目录和文件 ++integration_coveragePkg=${cover_pkg}/integration ++integration_coverage_file=${integration_coveragePkg}/cover_integration_test_all.out ++integration_coverage_html=${integration_coveragePkg}/cover_integration_test_all.html ++integration_coverage_func=${integration_coveragePkg}/cover_integration_test_all_func_cover ++integration_coverage_pkg=${integration_coveragePkg}/cover_integration_test_all_pkg_cover ++integration_coverage_data="${integration_coveragePkg}"/cover_data ++ ++## 单元测试覆盖率数据目录和文件 ++uint_coveragePkg=${cover_pkg}/uint ++uint_coverage_data="${uint_coveragePkg}"/cover_data ++ ++## 集成测试和单元测试的合并覆盖率数据目录和文件 ++merge_coverage_file=${cover_pkg}/cover_integration_and_unit_test_all.out ++merge_coverage_html=${cover_pkg}/cover_integration_and_unit_test_all.html ++ ++ ++function generate_integration_coverage() { ++ go tool covdata percent -i="${integration_coverage_data}" > "${integration_coverage_pkg}" ++ go tool covdata textfmt -i="${integration_coverage_data}" -o "${integration_coverage_file}" ++ go tool cover -html="${integration_coverage_file}" -o="${integration_coverage_html}" ++ go tool cover -func "${integration_coverage_file}" > "${integration_coverage_func}" ++} ++ ++function merge_coverage() { ++ generate_integration_coverage ++ go tool covdata textfmt -i="${integration_coverage_data}","${uint_coverage_data}" -o "${merge_coverage_file}" ++ go tool cover -html="${merge_coverage_file}" -o="${merge_coverage_html}" ++} ++ ++function cover_integration_build() { ++ sed -i 's/go build -buildmode=pie/go build -cover -buildmode=pie/g' ${project_root}/Makefile ++ make ++} ++ ++function cover_uint_tests() { ++ mkdir -p ${uint_coverage_data} ++ go test -mod=vendor -cover -v ./... -args -test.gocoverdir=${uint_coverage_data} ++} ++ ++function cover_integration_tests() { ++ cover_integration_build ++ mkdir -p "${log_pkg}" ++ mkdir -p "${integration_coverage_data}" ++ # 定义字符串列表,包含用例脚本名 ++ scripts=("netns_test_cover") ++ ++ # 遍历列表,依次执行脚本 ++ for script in "${scripts[@]}" ++ do ++ # 拼接project_root和脚本名 ++ script_path="${test_pkg}/${script}.sh" ++ ls ${script_path} ++ if [ $? -ne 0 ]; then ++ echo "$script_path is not existed" ++ else ++ # 执行脚本,将日志保存到${project_root}/log目录中,日志以用例名.log命名 ++ bash -x "${script_path}" "${build_pkg}" "${integration_coverage_data}" > "${log_pkg}/${script%}.log" 2>&1 ++ fi ++ done ++} ++ ++#cover_uint_tests ++#cover_integration_build ++#cover_integration_tests ++merge_coverage +diff --git a/libnetwork/drivers/common/driver.go b/libnetwork/drivers/common/driver.go +index a2158a1..c8280b9 100644 +--- a/libnetwork/drivers/common/driver.go ++++ b/libnetwork/drivers/common/driver.go +@@ -22,16 +22,17 @@ import ( + + // Driver implement the network driver common options + type Driver struct { +- nsPath string +- ctrName string +- hostName string +- mac *net.HardwareAddr +- ip *net.IPNet +- ip6 *net.IPNet +- bridge string +- bridgeDriver api.BridgeDriver +- mtu int +- qlen int ++ nsPath string ++ ctrName string ++ hostName string ++ mac *net.HardwareAddr ++ ip *net.IPNet ++ ip6 *net.IPNet ++ bridge string ++ bridgeDriver api.BridgeDriver ++ mtu int ++ qlen int ++ vethHostNSPath string + } + + // SetCtrNicName will set the network interface name in container +@@ -114,6 +115,16 @@ func (d *Driver) GetQlen() int { + return d.qlen + } + ++// SetVethHostNSPath will set the network interface vethHostNSPath ++func (d *Driver) SetVethHostNSPath(vethHostNSPath string) { ++ d.vethHostNSPath = vethHostNSPath ++} ++ ++// GetVethHostNSPath will return the network interface vethHostNSPath ++func (d *Driver) GetVethHostNSPath() string { ++ return d.vethHostNSPath ++} ++ + // SetBridge will set the bridge name which the nic connected to + func (d *Driver) SetBridge(bridgeName string) { + d.bridge = bridgeName +diff --git a/libnetwork/drivers/common/driver_test.go b/libnetwork/drivers/common/driver_test.go +index 7247232..2ee070d 100644 +--- a/libnetwork/drivers/common/driver_test.go ++++ b/libnetwork/drivers/common/driver_test.go +@@ -22,15 +22,16 @@ import ( + // TestDriver_GetAndSet tests setter & getter methods + func TestDriver_GetAndSet(t *testing.T) { + type fields struct { +- nsPath string +- ctrName string +- hostName string +- mac *net.HardwareAddr +- ip *net.IPNet +- ip6 *net.IPNet +- bridge string +- mtu int +- qlen int ++ nsPath string ++ ctrName string ++ hostName string ++ mac *net.HardwareAddr ++ ip *net.IPNet ++ ip6 *net.IPNet ++ bridge string ++ mtu int ++ qlen int ++ vethHostNSPath string + } + tests := []struct { + name string +@@ -52,6 +53,7 @@ func TestDriver_GetAndSet(t *testing.T) { + d.SetMtu(tt.fields.mtu) + d.SetQlen(tt.fields.qlen) + d.SetNsPath(tt.fields.nsPath) ++ d.SetVethHostNSPath(tt.fields.vethHostNSPath) + + d.GetBridge() + d.GetCtrNicName() +@@ -63,6 +65,7 @@ func TestDriver_GetAndSet(t *testing.T) { + d.GetQlen() + d.GetNsPath() + d.GetBridgeDriver() ++ d.GetVethHostNSPath() + }) + } + } +diff --git a/libnetwork/drivers/driver.go b/libnetwork/drivers/driver.go +index 86cac7a..58ac632 100644 +--- a/libnetwork/drivers/driver.go ++++ b/libnetwork/drivers/driver.go +@@ -188,6 +188,14 @@ func NicOptionQlen(qlen int) DriverOptions { + } + } + ++//NicOptionVethHostNSPath handles interface VethHostNSPath option ++func NicOptionVethHostNSPath(vethHostNSPath string) DriverOptions { ++ return func (d *common.Driver) error { ++ d.SetVethHostNSPath(strings.TrimSpace(vethHostNSPath)) ++ return nil ++ } ++} ++ + // NicOptionBridge handles brigde name option + func NicOptionBridge(bridge string) DriverOptions { + return func(d *common.Driver) error { +diff --git a/libnetwork/drivers/veth/driver.go b/libnetwork/drivers/veth/driver.go +index a485505..d1b86ad 100644 +--- a/libnetwork/drivers/veth/driver.go ++++ b/libnetwork/drivers/veth/driver.go +@@ -21,6 +21,7 @@ import ( + "github.com/docker/libnetwork/netutils" + "github.com/sirupsen/logrus" + "github.com/vishvananda/netlink" ++ "github.com/vishvananda/netns" + + "isula.org/syscontainer-tools/libnetwork/drivers/common" + "isula.org/syscontainer-tools/libnetwork/nsutils" +@@ -79,13 +80,19 @@ func (d *vethDriver) CreateIf() error { + PeerName: guestIfName, + } + d.mutex.Unlock() +- if err = netlink.LinkAdd(d.veth); err != nil { +- return fmt.Errorf("failed to create veth pairs: %v", err) +- } +- if err := d.setDefaultVethFeature(guestIfName); err != nil { +- return err +- } +- if err := d.setDefaultVethFeature(hostIfName); err != nil { ++ // switch to the specified namespace to create a network interface ++ if err := nsutils.SwitchAndExecute(d.GetVethHostNSPath(), func() error { ++ if err := netlink.LinkAdd(d.veth); err != nil { ++ return fmt.Errorf("failed to create veth pairs: %v", err) ++ } ++ if err := d.setDefaultVethFeature(guestIfName); err != nil { ++ return fmt.Errorf("failed to set default features for guest interface: %v", err) ++ } ++ if err := d.setDefaultVethFeature(hostIfName); err != nil { ++ return fmt.Errorf("failed to set default features for host interface: %v", err) ++ } ++ return nil ++ }); err != nil { + return err + } + logrus.Debugf("veth pair (%s, %s) created", hostIfName, guestIfName) +@@ -93,18 +100,21 @@ func (d *vethDriver) CreateIf() error { + } + + func (d *vethDriver) DeleteIf() error { +- veth, err := netlink.LinkByName(d.GetHostNicName()) +- if err != nil { +- // As add-nic supports 'update-config-only' option, +- // With this flag, syscontainer-tools will update config only, don't add device to container. +- // So if device dose not exist on host, ignore it. +- if strings.Contains(err.Error(), "Link not found") { +- return nil ++ return nsutils.SwitchAndExecute(d.GetVethHostNSPath(), func() error { ++ veth, err := netlink.LinkByName(d.GetHostNicName()) ++ if err != nil { ++ // As add-nic supports 'update-config-only' option, ++ // With this flag, syscontainer-tools will update config only, don't add device to container. ++ // So if device dose not exist on host, ignore it. ++ if strings.Contains(err.Error(), "Link not found") { ++ return nil ++ } ++ return err + } +- return err +- } + +- return netlink.LinkDel(veth) ++ return netlink.LinkDel(veth) ++ }) ++ + } + + func (d *vethDriver) setNicConfigure(nic netlink.Link) (rErr error) { +@@ -151,88 +161,99 @@ func (d *vethDriver) JoinAndConfigure() (rErr error) { + defer func() { + if rErr != nil { + logrus.Infof("Recover on failure: delete veth(%s)", hostNicName) +- nic, err := netlink.LinkByName(hostNicName) +- if err != nil { +- logrus.Errorf("Recover on failure: failed to get link by name(%q): %v", hostNicName, err) +- return +- } +- if err := netlink.LinkDel(nic); err != nil { +- logrus.Errorf("Recover on failure: failed to remove nic(%s): %v", hostNicName, err) ++ // restore the NIC environment in the specified namespace ++ if err := nsutils.SwitchAndExecute(d.GetVethHostNSPath(), func() error { ++ nic, err := netlink.LinkByName(hostNicName) ++ if err != nil { ++ return fmt.Errorf("failed to get link by name(%q): %v", hostNicName, err) ++ } ++ if err := netlink.LinkDel(nic); err != nil { ++ return fmt.Errorf("failed to remove nic(%s): %v", hostNicName, err) ++ } ++ return nil ++ }); err != nil { ++ logrus.Errorf("Recover on failure: %v", err) + } + } + }() +- err := nsutils.NsInvoke( +- d.GetNsPath(), func(nsFD int) error { +- // pre function is executed in host +- hostNic, err := netlink.LinkByName(d.veth.Attrs().Name) +- if err != nil { +- return fmt.Errorf("failed to get link by name %q: %v", d.veth.Attrs().Name, err) +- } +- ctrNic, err := netlink.LinkByName(d.veth.PeerName) +- if err != nil { +- return fmt.Errorf("failed to get link by name %q: %v", d.veth.PeerName, err) +- } + +- // down the interface before configuring +- if err := netlink.LinkSetDown(hostNic); err != nil { +- return fmt.Errorf("failed to set link down: %v", err) +- } +- if err := netlink.LinkSetDown(ctrNic); err != nil { +- return fmt.Errorf("failed to set link down: %v", err) +- } ++ // Move one end of the virtual NIC pair to the network namespace of the container ++ if err := nsutils.SwitchAndExecute(d.GetVethHostNSPath(), func() error { ++ hostNic, err := netlink.LinkByName(d.veth.Attrs().Name) ++ if err != nil { ++ return fmt.Errorf("failed to get link by name %q: %v", d.veth.Attrs().Name, err) ++ } ++ ctrNic, err := netlink.LinkByName(d.veth.PeerName) ++ if err != nil { ++ return fmt.Errorf("failed to get link by name %q: %v", d.veth.PeerName, err) ++ } ++ // down the interface before configuring ++ if err := netlink.LinkSetDown(hostNic); err != nil { ++ return fmt.Errorf("failed to set hostnic down: %v", err) ++ } ++ if err := netlink.LinkSetDown(ctrNic); err != nil { ++ return fmt.Errorf("failed to set link down: %v", err) ++ } + +- // move the network interface to the destination +- if err := netlink.LinkSetNsFd(ctrNic, nsFD); err != nil { +- return fmt.Errorf("failed to set namespace on link %q: %v", d.veth.PeerName, err) +- } ++ // move the network interface to the destination ++ ctrNs, err := netns.GetFromPath(d.GetNsPath()) ++ if err != nil { ++ return fmt.Errorf("failed get network namespace %s: %v", d.GetNsPath(), err) ++ } ++ defer ctrNs.Close() ++ if err := netlink.LinkSetNsFd(ctrNic, int(ctrNs)); err != nil { ++ return fmt.Errorf("failed to set namespace on link %q: %v", d.veth.PeerName, err) ++ } + +- // attach host nic to bridge and configure mtu +- if err = netlink.LinkSetMTU(hostNic, d.GetMtu()); err != nil { +- return fmt.Errorf("failed to set mtu: %v", err) +- } ++ // attach host nic to bridge and configure mtu ++ if err = netlink.LinkSetMTU(hostNic, d.GetMtu()); err != nil { ++ return fmt.Errorf("failed to set mtu: %v", err) ++ } + +- if d.GetHostNicName() != "" { +- // set iface to user desired name +- if err := netlink.LinkSetName(hostNic, d.GetHostNicName()); err != nil { +- return fmt.Errorf("failed to rename link %s -> %s: %v", d.veth.Attrs().Name, d.GetHostNicName(), err) +- } +- hostNicName = d.GetHostNicName() +- logrus.Debugf("Rename host link %s -> %s", d.veth.Attrs().Name, d.GetHostNicName()) ++ if d.GetHostNicName() != "" { ++ // set iface to user desired name ++ if err := netlink.LinkSetName(hostNic, d.GetHostNicName()); err != nil { ++ return fmt.Errorf("failed to rename link %s -> %s: %v", d.veth.Attrs().Name, d.GetHostNicName(), err) + } ++ hostNicName = d.GetHostNicName() ++ logrus.Debugf("Rename host link %s -> %s", d.veth.Attrs().Name, d.GetHostNicName()) ++ } + +- if err = d.AddToBridge(); err != nil { +- return fmt.Errorf("failed to add to bridge: %v", err) +- } ++ if err := d.AddToBridge(); err != nil { ++ return fmt.Errorf("failed to add to bridge: %v", err) ++ } + +- if err := netlink.LinkSetUp(hostNic); err != nil { +- return fmt.Errorf("failed to set link up: %v", err) +- } +- return nil +- }, func(nsFD int) error { +- // post function is executed in container +- ctrNic, err := netlink.LinkByName(d.veth.PeerName) +- if err != nil { +- return fmt.Errorf("failed to get link by name %q: %v", d.veth.PeerName, err) +- } ++ if err := netlink.LinkSetUp(hostNic); err != nil { ++ return fmt.Errorf("failed to set link up: %v", err) ++ } ++ return nil ++ }); err != nil { ++ return err ++ } + +- // set iface to user desired name +- if err := netlink.LinkSetName(ctrNic, d.GetCtrNicName()); err != nil { +- return fmt.Errorf("failed to rename link: %v", err) +- } +- logrus.Debugf("Rename container link %s -> %s", d.veth.PeerName, d.GetCtrNicName()) ++ // Set vNIC properties in the container's network namespace ++ return nsutils.SwitchAndExecute(d.GetNsPath(), func() error { ++ ctrNic, err := netlink.LinkByName(d.veth.PeerName) ++ if err != nil { ++ return fmt.Errorf("failed to get link by name %q: %v", d.veth.PeerName, err) ++ } + +- if err := d.setNicConfigure(ctrNic); err != nil { +- return err +- } ++ // set iface to user desired name ++ if err := netlink.LinkSetName(ctrNic, d.GetCtrNicName()); err != nil { ++ return fmt.Errorf("failed to rename link: %v", err) ++ } ++ logrus.Debugf("Rename container link %s -> %s", d.veth.PeerName, d.GetCtrNicName()) + +- // Up the interface. +- if err := netlink.LinkSetUp(ctrNic); err != nil { +- return fmt.Errorf("failed to set link up: %v", err) +- } +- return nil +- }) ++ if err := d.setNicConfigure(ctrNic); err != nil { ++ return err ++ } + +- return err ++ // Up the interface. ++ if err := netlink.LinkSetUp(ctrNic); err != nil { ++ return fmt.Errorf("failed to set link up: %v", err) ++ } ++ return nil ++ }) + } + + func (d *vethDriver) Configure() (rErr error) { +diff --git a/libnetwork/drivers/veth/driver_test.go b/libnetwork/drivers/veth/driver_test.go +new file mode 100644 +index 0000000..facb7af +--- /dev/null ++++ b/libnetwork/drivers/veth/driver_test.go +@@ -0,0 +1,245 @@ ++// Copyright (c) Huawei Technologies Co., Ltd. 2018-2024. All rights reserved. ++// syscontainer-tools is licensed under the Mulan PSL v2. ++// You can use this software according to the terms and conditions of the Mulan PSL v2. ++// You may obtain a copy of Mulan PSL v2 at: ++// http://license.coscl.org.cn/MulanPSL2 ++// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR ++// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR ++// PURPOSE. ++// See the Mulan PSL v2 for more details. ++// Description: virtual ethetic network driver ++// Author: Jichao Wu ++// Create: 2018-01-18 ++ ++package veth ++ ++import ( ++ "fmt" ++ "os/exec" ++ "testing" ++ ++ "github.com/docker/libnetwork/netutils" ++ "github.com/sirupsen/logrus" ++ "github.com/vishvananda/netlink" ++ ++ "isula.org/syscontainer-tools/libnetwork/drivers/common" ++ "isula.org/syscontainer-tools/types" ++) ++ ++func deleteNs(name string) { ++ if err := exec.Command("sudo", "ip", "netns", "del", name).Run(); err != nil { ++ logrus.Errorf("failed to delete the network namespace:%v", err) ++ } ++} ++func createNs(name string) error { ++ if err := exec.Command("sudo", "ip", "netns", "add", name).Run(); err != nil { ++ return fmt.Errorf("failed to create a new network namespace: %v", err) ++ } ++ return nil ++} ++ ++func createBridge(name string) error { ++ if err := netlink.LinkAdd(&netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: name, TxQLen: -1}}); err != nil { ++ return fmt.Errorf("failed to create foo bridge: %v", err) ++ } ++ return nil ++} ++ ++func deleteBridge(name string) { ++ l, err := netlink.LinkByName(name) ++ if err != nil { ++ logrus.Errorf("failed to find the link %v: %v", name, err) ++ return ++ } ++ if err := netlink.LinkDel(l); err != nil { ++ logrus.Errorf("failed to delete bridge %v: %v", name, err) ++ } ++} ++ ++// Test_vethDriver_CreateIf tests CreateIf of vethDriver ++func Test_vethDriver_CreateIf(t *testing.T) { ++ const ( ++ bridgeName = "foo" ++ containerNs = "testNameSpase" ++ containerNsPath = "/var/run/netns/" + containerNs ++ qlen = 1500 ++ mtu = 1000 ++ ) ++ ipNet, err := netlink.ParseIPNet(types.CIDRIpExample1) ++ if err != nil { ++ logrus.Errorf("skip this testcase because it failed topoarse ipnet: %v", err) ++ return ++ } ++ ++ if err := createNs(containerNs); err != nil { ++ logrus.Errorf("skip this testcase: %v", err) ++ return ++ } ++ defer deleteNs(containerNs) ++ ++ if err := createBridge(bridgeName); err != nil { ++ logrus.Errorf("skip this testcase: %v", err) ++ return ++ } ++ defer deleteBridge(bridgeName) ++ ++ tests := []struct { ++ name string ++ wantErr bool ++ wantCreateErr bool ++ wantJoin bool ++ wantJoinErr bool ++ wantDelete bool ++ wantDeleteErr bool ++ pre func(d *vethDriver) error ++ post func(d *vethDriver) ++ }{ ++ { ++ name: "TC1-empty veth namespace & add nic successfully", ++ pre: func(d *vethDriver) error { ++ d.SetQlen(qlen) ++ d.SetMtu(mtu) ++ d.SetIP(ipNet) ++ d.SetHostNicName("H") ++ d.SetCtrNicName("C") ++ d.SetBridge(bridgeName) ++ d.SetNsPath(containerNsPath) ++ return nil ++ }, ++ post: func(d *vethDriver) { ++ if err := netlink.LinkDel(d.veth); err != nil { ++ logrus.Errorf("failed to delete test veth: %v", err) ++ } ++ }, ++ wantJoin: true, ++ wantDelete: true, ++ }, ++ } ++ for _, tt := range tests { ++ t.Run(tt.name, func(t *testing.T) { ++ d := &vethDriver{ ++ Driver: &common.Driver{}, ++ } ++ if tt.pre != nil { ++ if err := tt.pre(d); err != nil { ++ logrus.Errorf("skip this tc because it failed to execute pre func: %v", err) ++ return ++ } ++ } ++ defer func() { ++ if tt.post != nil { ++ tt.post(d) ++ } ++ }() ++ ++ if err := d.CreateIf(); (err != nil) != tt.wantCreateErr { ++ t.Errorf("vethDriver.CreateIf() error = %v, wantErr %v", err, tt.wantCreateErr) ++ return ++ } ++ if tt.wantJoin { ++ // The NIC is set only after the creation is successful. ++ if err := d.JoinAndConfigure(); (err != nil) != tt.wantJoinErr { ++ t.Errorf("vethDriver.JoinAndConfigure() error = %v, wantErr %v", err, tt.wantJoinErr) ++ return ++ } ++ } ++ if tt.wantDelete { ++ // The NIC is deleted only after it is successfully configured. ++ if err := d.DeleteIf(); (err != nil) != tt.wantDeleteErr { ++ t.Errorf("vethDriver.DeleteIf() error = %v, wantErr %v", err, tt.wantDeleteErr) ++ } ++ } ++ }) ++ } ++} ++ ++// Test_vethDriver_DeleteIf tests DeleteIf of vethDriver ++func Test_vethDriver_DeleteIf(t *testing.T) { ++ randomHostNic, err := netutils.GenerateIfaceName("test", 10) ++ if err != nil { ++ logrus.Infof("skip this TC because it failed to get ifname: %v", err) ++ return ++ } ++ tests := []struct { ++ name string ++ wantErr bool ++ pre func(d *vethDriver) ++ }{ ++ { ++ name: "TC1-non existed nic", ++ pre: func(d *vethDriver) { ++ d.SetHostNicName(randomHostNic) ++ }, ++ }, ++ } ++ for _, tt := range tests { ++ t.Run(tt.name, func(t *testing.T) { ++ d := &vethDriver{ ++ Driver: &common.Driver{}, ++ } ++ if tt.pre != nil { ++ tt.pre(d) ++ } ++ if err := d.DeleteIf(); (err != nil) != tt.wantErr { ++ t.Errorf("vethDriver.DeleteIf() error = %v, wantErr %v", err, tt.wantErr) ++ } ++ }) ++ } ++} ++ ++// Test_vethDriver_JoinAndConfigure tests JoinAndConfigure of vethDriver ++func Test_vethDriver_JoinAndConfigure(t *testing.T) { ++ const ( ++ hostNicName = "testHostNic" ++ ctrNicName = "testCtrNic" ++ nonExistctrNicName = "testCtrNic1" ++ ) ++ tests := []struct { ++ name string ++ wantErr bool ++ pre func(d *vethDriver) ++ }{ ++ { ++ name: "TC1-empty veth", ++ wantErr: true, ++ }, ++ { ++ name: "TC2-non existed nic", ++ pre: func(d *vethDriver) { ++ d.veth = &netlink.Veth{ ++ LinkAttrs: netlink.LinkAttrs{Name: hostNicName, TxQLen: d.GetQlen()}, ++ PeerName: ctrNicName, ++ } ++ }, ++ wantErr: true, ++ }, ++ { ++ name: "TC3-host nic existed but ctr nic is not existed", ++ pre: func(d *vethDriver) { ++ d.veth = &netlink.Veth{ ++ LinkAttrs: netlink.LinkAttrs{Name: hostNicName, TxQLen: d.GetQlen()}, ++ PeerName: ctrNicName, ++ } ++ if err := netlink.LinkAdd(d.veth); err != nil { ++ logrus.Errorf("failed to create veth pairs: %v", err) ++ return ++ } ++ d.veth.PeerName = nonExistctrNicName ++ }, ++ wantErr: true, ++ }, ++ } ++ for _, tt := range tests { ++ t.Run(tt.name, func(t *testing.T) { ++ d := &vethDriver{ ++ Driver: &common.Driver{}, ++ } ++ if tt.pre != nil { ++ tt.pre(d) ++ } ++ if err := d.JoinAndConfigure(); (err != nil) != tt.wantErr { ++ t.Errorf("vethDriver.JoinAndConfigure() error = %v, wantErr %v", err, tt.wantErr) ++ } ++ }) ++ } ++} +diff --git a/libnetwork/interfaces.go b/libnetwork/interfaces.go +index 121243a..0c255c2 100644 +--- a/libnetwork/interfaces.go ++++ b/libnetwork/interfaces.go +@@ -75,7 +75,8 @@ func AddNicToContainer(nsPath string, config *types.InterfaceConf) (rErr error) + drivers.NicOptionMac(config.Mac), + drivers.NicOptionMtu(config.Mtu), + drivers.NicOptionQlen(config.Qlen), +- drivers.NicOptionBridge(config.Bridge)) ++ drivers.NicOptionBridge(config.Bridge), ++ drivers.NicOptionVethHostNSPath(config.VethHostNSPath)) + if err != nil { + return err + } +@@ -156,8 +157,8 @@ func DelNicFromContainer(nsPath string, config *types.InterfaceConf) error { + drivers.NicOptionIP6(config.IP6), + drivers.NicOptionMac(config.Mac), + drivers.NicOptionMtu(config.Mtu), +- drivers.NicOptionBridge(config.Bridge)) +- ++ drivers.NicOptionBridge(config.Bridge), ++ drivers.NicOptionVethHostNSPath(config.VethHostNSPath)) + if err != nil { + return err + } +@@ -184,6 +185,9 @@ func UpdateNic(ctr *container.Container, config *types.InterfaceConf, updateConf + return fmt.Errorf("Network interface %s,%s with type %s not exist in container %s", config.HostNicName, config.CtrNicName, config.Type, ctr.Name()) + } + ++ if oldConfig.VethHostNSPath != "" { ++ return fmt.Errorf("failed to update the virtual NIC pair configuration for the specified namespace") ++ } + if config.IP == "" { + tmpConfig.IP = oldConfig.IP + } else { +diff --git a/libnetwork/nsutils/ns_utils.go b/libnetwork/nsutils/ns_utils.go +index 2f7ce5d..da42157 100644 +--- a/libnetwork/nsutils/ns_utils.go ++++ b/libnetwork/nsutils/ns_utils.go +@@ -20,6 +20,39 @@ import ( + "github.com/vishvananda/netns" + ) + ++// SwitchAndExecute executes the handler in the specified network namespace ++func SwitchAndExecute(nsPath string, handler func() error) error { ++ // If nsPath is empty, execute handler function directly ++ if nsPath == "" { ++ return handler() ++ } ++ initns, err := netns.Get() ++ if err != nil { ++ return fmt.Errorf("failed to get current namespace %v", err) ++ } ++ defer initns.Close() ++ ++ ns, err := netns.GetFromPath(nsPath) ++ if err != nil { ++ return fmt.Errorf("failed to get namespace %s: %v", nsPath, err) ++ } ++ defer ns.Close() ++ ++ runtime.LockOSThread() ++ defer runtime.UnlockOSThread() ++ ++ if err := netns.Set(ns); err != nil { ++ return err ++ } ++ ++ // Invoked after the namespace switch. ++ err = handler() ++ if setErr := netns.Set(initns); setErr != nil { ++ return fmt.Errorf("failed to set to initial namespace: %v, and handler err is: %v", setErr, err) ++ } ++ return err ++} ++ + // NsInvoke function is used for setting network outside/inside the container/netns + // prefunc is called in the host, and postfunc is used in container + func NsInvoke(path string, prefunc func(nsFD int) error, postfunc func(callerFD int) error) error { +diff --git a/libnetwork/nsutils/ns_utils_test.go b/libnetwork/nsutils/ns_utils_test.go +new file mode 100644 +index 0000000..071e277 +--- /dev/null ++++ b/libnetwork/nsutils/ns_utils_test.go +@@ -0,0 +1,121 @@ ++// Copyright (c) Huawei Technologies Co., Ltd. 2018-2023. All rights reserved. ++// syscontainer-tools is licensed under the Mulan PSL v2. ++// You can use this software according to the terms and conditions of the Mulan PSL v2. ++// You may obtain a copy of Mulan PSL v2 at: ++// http://license.coscl.org.cn/MulanPSL2 ++// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR ++// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR ++// PURPOSE. ++// See the Mulan PSL v2 for more details. ++// Description: network interface type ++// Author: Jichao Wu ++// Create: 2023-07-22 ++ ++package nsutils ++ ++import ( ++ "fmt" ++ "os" ++ "os/exec" ++ "testing" ++ ++ "github.com/sirupsen/logrus" ++) ++ ++// TestSwitchAndExecute tests SwitchAndExecute ++func TestSwitchAndExecute(t *testing.T) { ++ const ( ++ normalFilePath = "/root/test_net" ++ ) ++ type args struct { ++ nsPath string ++ handler func() error ++ } ++ tests := []struct { ++ name string ++ args args ++ pre func(t *testing.T) error ++ post func(t *testing.T) ++ wantErr bool ++ }{ ++ { ++ name: "TC1-correct namespace path", ++ args: args{ ++ nsPath: "/var/run/netns/myNs", ++ handler: func() error { ++ fmt.Println("when nsPath is not empty, handler called") ++ return nil ++ }, ++ }, ++ pre: func(t *testing.T) error { ++ cmd := exec.Command("ip", "netns", "add", "myNs") ++ return cmd.Run() ++ }, ++ post: func(t *testing.T) { ++ cmd := exec.Command("ip", "netns", "delete", "myNs") ++ err := cmd.Run() ++ if err != nil { ++ t.Errorf("failed to delete the network namespace:%v", err) ++ } ++ }, ++ wantErr: false, ++ }, ++ { ++ name: "TC2-empty namespace path", ++ args: args{ ++ nsPath: "", ++ handler: func() error { ++ fmt.Println("when nsPath is empty, handler called") ++ return nil ++ }, ++ }, ++ wantErr: false, ++ }, ++ { ++ name: "TC3-non-existed namespace path", ++ args: args{ ++ nsPath: "xxx", ++ handler: func() error { ++ return nil ++ }, ++ }, ++ wantErr: true, ++ }, ++ { ++ name: "TC4-normal file path", ++ args: args{ ++ nsPath: normalFilePath, ++ handler: func() error { ++ return nil ++ }, ++ }, ++ pre: func(t *testing.T) error { ++ file, err := os.Create(normalFilePath) ++ file.Close() ++ return err ++ }, ++ post: func(t *testing.T) { ++ if err := os.Remove(normalFilePath); err != nil { ++ t.Errorf("failed to remove the file %v: %v", normalFilePath, err) ++ } ++ }, ++ wantErr: true, ++ }, ++ } ++ for _, tt := range tests { ++ t.Run(tt.name, func(t *testing.T) { ++ if tt.pre != nil { ++ if err := tt.pre(t); err != nil { ++ logrus.Infof("skip TC because it failed to create the network namespace:%v", err) ++ return ++ } ++ } ++ if err := SwitchAndExecute(tt.args.nsPath, tt.args.handler); (err != nil) != tt.wantErr { ++ t.Errorf("SwitchAndExecute() error = %v, wantErr %v", err, tt.wantErr) ++ } ++ if tt.post != nil { ++ tt.post(t) ++ } ++ }) ++ } ++} +diff --git a/network.go b/network.go +index 6e32dc4..dea982d 100644 +--- a/network.go ++++ b/network.go +@@ -37,6 +37,10 @@ var addNicCommand = cli.Command{ + and configure it as you wanted, then attach to specified bridge. + `, + Flags: []cli.Flag{ ++ cli.StringFlag{ ++ Name: "namespace", ++ Usage: "set network namespace of the virtual NIC on the host", ++ }, + cli.StringFlag{ + Name: "type", + Usage: "set network interface type (veth/eth)", +@@ -104,15 +108,16 @@ and configure it as you wanted, then attach to specified bridge. + } + + nicConf := &types.InterfaceConf{ +- IP: context.String("ip"), +- IP6: context.String("ip6"), +- Mac: context.String("mac"), +- Mtu: context.Int("mtu"), +- Type: context.String("type"), +- Bridge: context.String("bridge"), +- Qlen: context.Int("qlen"), +- CtrNicName: ctrNicName, +- HostNicName: hostNicName, ++ IP: context.String("ip"), ++ IP6: context.String("ip6"), ++ Mac: context.String("mac"), ++ Mtu: context.Int("mtu"), ++ Type: context.String("type"), ++ Bridge: context.String("bridge"), ++ Qlen: context.Int("qlen"), ++ VethHostNSPath: context.String("namespace"), ++ CtrNicName: ctrNicName, ++ HostNicName: hostNicName, + } + + if err := types.ValidNetworkConfig(nicConf); err != nil { +diff --git a/test/netns_test_cover.sh b/test/netns_test_cover.sh +new file mode 100644 +index 0000000..d3fbe4a +--- /dev/null ++++ b/test/netns_test_cover.sh +@@ -0,0 +1,302 @@ ++# Copyright (c) Huawei Technologies Co., Ltd. 2018-2019. All rights reserved. ++# syscontainer-tools is licensed under the Mulan PSL v2. ++# You can use this software according to the terms and conditions of the Mulan PSL v2. ++# You may obtain a copy of Mulan PSL v2 at: ++# http://license.coscl.org.cn/MulanPSL2 ++# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR ++# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR ++# PURPOSE. ++# See the Mulan PSL v2 for more details. ++# Description: network test ++# Author: syscontainer-tools Team ++# Create: 2024-07-31 ++ ++current_dir=$(cd $(dirname "$0") && pwd) ++TOOL_DIR=${current_dir} ++if [ -n "$1" ]; then ++ TOOL_DIR=$1 ++fi ++TOOL_CLI="${TOOL_DIR}/syscontainer-tools" ++HOOK_CLI="${TOOL_DIR}/syscontainer-hooks" ++ ++ ++COVER_DIR="${current_dir}/cover_data" ++if [ -n "$2" ]; then ++ COVER_DIR="$2" ++fi ++ ++testcase_name=$0 ++rename_hooks=0 ++ns_name="userSpecifiedNS" ++another_ns_name="userSpecifiedNS1" ++bridge_name="br0" ++ctrNic="ctrNic" ++hotsNic="hotsNic" ++first="172" ++sec="17" ++third="28" ++fourth="5" ++mask="24" ++ip=$first"."$sec"."$third"."$fourth"/"$mask ++mac="00:ff:48:13:90:01" ++expect_result="[{\"Ip\":\"$ip\",\"Ip6\":\"\",\"Mac\":\"$mac\",\"Mtu\":1500,\"Qlen\":1000,\"Type\":\"veth\",\"Bridge\":\"$bridge_name\",\"HostNicName\":\"$hotsNic\",\"CtrNicName\":\"$ctrNic\",\"Namespace\":\"/var/run/netns/$ns_name\"}]" ++#color code ++color_red=$(tput setaf 1 || tput AF 1) ++color_green=$(tput setaf 2 || tput AF 2) ++color_yellow=$(tput setaf 3 || tput AF 3) ++color_reset=$(tput sgr0 || tput me) ++ ++exit_flag=0 ++ ++function echoTxt() { ++ TXT="[$(date "+%Y-%m-%d-%H-%M-%S")]$1" ++ COLOR=$2 ++ if [ "${COLOR}" = "red" ]; then ++ echo -e "${color_red}${TXT}${color_reset}" ++ elif [ "${COLOR}" = "green" ]; then ++ echo -e "${color_green}${TXT}${color_reset}" ++ elif [ "${COLOR}" = "yellow" ]; then ++ echo -e "${color_yellow}${TXT}${color_reset}" ++ else ++ echo "${TXT}" ++ fi ++} ++ ++function tsm_error() { ++ txt_str=$1 ++ echoTxt "$txt_str" red ++} ++ ++function tsm_info() { ++ txt_str=$1 ++ echoTxt "$txt_str" green ++} ++ ++function WaitInspect() { ++ container=$1 ++ expect_state=$2 ++ result="FAIL" ++ if [ $# -lt 2 ]; then ++ tsm_error "FAILED: take at lease 2 input parameters" ++ return 1 ++ fi ++ for ((i = 0; i < 30; i++)); do ++ current_state=$(isula inspect -f '{{.State.Status}}' "$container") ++ if [ "$current_state" == "$expect_state" ]; then ++ result="PASS" ++ break ++ else ++ sleep 1 ++ fi ++ done ++ if [ "$result" == "PASS" ]; then ++ tsm_info "PASS:return $current_state as expected!($3)" ++ return 0 ++ else ++ tsm_error "FAILED:return $current_state not as expected!($3)" ++ ((exit_flag++)) ++ return 1 ++ fi ++} ++ ++function skip_test() { ++ echo "skip this testcase ${testcase_name}" ++ exit 1 ++} ++ ++function check_tools_existed() { ++ if [ ! -f "${TOOL_CLI}" ]; then ++ echo "Error: ${TOOL_CLI} not found" ++ skip_test ++ fi ++ if [ ! -f "${HOOK_CLI}" ]; then ++ echo "Error: ${HOOK_CLI} not found" ++ skip_test ++ fi ++} ++ ++function fn_check_result_noeq() { ++ if [ "$1" != "$2" ]; then ++ tsm_info "PASS:return $1 as expected,not equal $2!($3)" ++ else ++ tsm_error "FAILED:return $1 not as expected,equal $2!($3)" ++ ((exit_flag++)) ++ fi ++} ++ ++function fn_check_result() { ++ if [ "$1" = "$2" ]; then ++ tsm_info "PASS:return $1 as expected!($3)" ++ else ++ tsm_error "FAILED:return $1 not as expected $2!($3)" ++ ((exit_flag++)) ++ fi ++} ++ ++function pre() { ++ check_tools_existed ++ if [ -f "/var/lib/isulad/hooks/syscontainer-hooks" ]; then ++ # 存在则备份为syscontainer-hooks.bak ++ mv "/var/lib/isulad/hooks/syscontainer-hooks" "/var/lib/isulad/hooks/syscontainer-hooks.bak" ++ rename_hooks=1 ++ fi ++ if [ -f "${HOOK_CLI}" ]; then ++ # 存在则拷贝到/var/lib/isulad/hooks/目录下 ++ cp "${HOOK_CLI}" "/var/lib/isulad/hooks/" ++ fi ++ delete_network_namespace $ns_name ++ delete_network_namespace $another_ns_name ++ create_network_namespace ${ns_name} ++ create_network_namespace ${another_ns_name} ++ mkdir -p "${COVER_DIR}" ++ echo "COVER_DIR is ${COVER_DIR}" ++ echo "TOOL_DIR is ${TOOL_DIR}" ++} ++ ++function post() { ++ if [ -f "/var/lib/isulad/hooks/syscontainer-hooks.bak" ] && [ "$rename_hooks" == "1" ]; then ++ mv "/var/lib/isulad/hooks/syscontainer-hooks.bak" "/var/lib/isulad/hooks/syscontainer-hooks" ++ fi ++ isula rm -f $(isula ps -aq) ++ delete_network_namespace ${ns_name} ++ delete_network_namespace ${another_ns_name} ++} ++ ++#check wether a nic in the specified container ++function check_nic_in_container() { ++ local nic_name="$1" ++ local container_id="$2" ++ ++ isula exec "$container_id" ip link show | grep "$nic_name" > /dev/null ++ return $? ++} ++ ++#check whether a specified NIC in the specified network namespace. ++function check_nic_in_namespace() { ++ local nic_name="$1" ++ local ns_name="$2" ++ ++ ip netns | grep -q "^$ns_name$" ++ fn_check_result $? 0 "Network namespace ${ns_name} should exist." ++ ++ ip netns exec "$ns_name" ip addr show "$nic_name" > /dev/null 2>&1 ++ return $? ++} ++ ++#create network namespace ++function create_network_namespace() { ++ local NS=$1 ++ ip netns add "$NS" ++ ++ ip netns | grep -q "^$NS$" ++ if [[ $? -eq 0 ]]; then ++ echo "Network namespace $NS created successfully." ++ return 0 ++ else ++ echo "Failed to create network namespace $NS." ++ return 1 ++ fi ++} ++ ++#delete network namespace ++function delete_network_namespace() { ++ local ns_name=$1 ++ ++ if [ -z "$ns_name" ]; then ++ return 1 ++ fi ++ ++ ip netns | grep -q "^$ns_name$" ++ if [ $? -ne 0 ]; then ++ return 0 ++ fi ++ ++ ip netns delete "$ns_name" ++ return $? ++} ++ ++function main() { ++ #start syscontainer ++ container_id=$(isula run -tid --hook-spec /etc/syscontainer-tools/hookspec.json --system-container --external-rootfs /opt/images/rootfs/root-fs/ none init) ++ WaitInspect "$container_id" "running" ++ fn_check_result $? 0 "container start" ++ ++ #add bridge to user specifed namespace ++ ip netns exec "${ns_name}" brctl addbr $bridge_name ++ fn_check_result $? 0 "add bridge to user specifed namespace" ++ ++ #add veth nic ++ GOCOVERDIR=${COVER_DIR} ${TOOL_CLI} add-nic --type "veth" --namespace /var/run/netns/"${ns_name}" --name ${hotsNic}:${ctrNic} --ip $ip --mac $mac --bridge "${bridge_name}" "${container_id}" ++ fn_check_result $? 0 "1. add veth nic" ++ ++ #check nicName in container ++ check_nic_in_container ${ctrNic} "$container_id" ++ fn_check_result $? 0 "2. ${ctrNic} should in the container ${container_id}" ++ ++ #check is nicName in specified namespace ++ check_nic_in_namespace ${hotsNic} "${ns_name}" ++ fn_check_result $? 0 "3. ${hotsNic} should in the specified namespace ${ns_name}" ++ ++ #stop container ++ isula stop "$container_id" > /dev/null ++ WaitInspect "${container_id}" "exited" ++ fn_check_result $? 0 "4. stop container" ++ ++ #check is nicName in specified namespace ++ check_nic_in_namespace ${hotsNic} "${ns_name}" ++ fn_check_result_noeq $? 0 "5. ${hotsNic} should not in the specified namespace ${ns_name}" ++ ++ #restart container ++ isula start "$container_id" ++ WaitInspect "${container_id}" "running" ++ fn_check_result $? 0 "6. container ${container_id} should be running." ++ ++ #check is nicName in specified namespace ++ check_nic_in_namespace ${hotsNic} "${ns_name}" ++ fn_check_result $? 0 "7. ${hotsNic} should in the specified namespace ${ns_name}" ++ ++ #check nicName in container ++ check_nic_in_container ${ctrNic} "$container_id" ++ fn_check_result $? 0 "8. ${ctrNic} should in the container ${container_id}" ++ ++ #add vethNic with same name ++ GOCOVERDIR=${COVER_DIR} ${TOOL_CLI} add-nic --type "veth" --namespace /var/run/netns/"${ns_name}" --name ${hotsNic}:${ctrNic} --ip $ip --mac $mac --bridge $bridge_name "$container_id" > /dev/null ++ fn_check_result_noeq $? 0 "9. can not add vethNic with same name" ++ ++ #add bridge to user specifed namespace ${another_ns_name} ++ ip netns exec ${another_ns_name} brctl addbr $bridge_name ++ fn_check_result $? 0 "10. add bridge to user specifed namespace ${another_ns_name}" ++ ++ #add vethNic with same name but different namespace ++ GOCOVERDIR=${COVER_DIR} ${TOOL_CLI} add-nic --type "veth" --namespace /var/run/netns/${another_ns_name} --name ${hotsNic}:${ctrNic} --ip $ip --mac $mac --bridge $bridge_name "$container_id" > /dev/null ++ fn_check_result_noeq $? 0 "11. add vethNic with same name but different namespace" ++ ++ #list nic ++ res=$(GOCOVERDIR=${COVER_DIR} ${TOOL_CLI} list-nic "$container_id") ++ fn_check_result "$res" "$expect_result" "12. list nic successfully" ++ ++ #update nic ++ GOCOVERDIR=${COVER_DIR} ${TOOL_CLI} update-nic --name ${ctrNic} --mac "00:ff:10:43:13:13" "$container_id" > /dev/null ++ fn_check_result_noeq $? 0 "13. update nic" ++ ++ #remove nic ++ GOCOVERDIR=${COVER_DIR} ${TOOL_CLI} remove-nic --type veth --name ${hotsNic}:${ctrNic} "$container_id" > /dev/null ++ fn_check_result $? 0 "14. remove nic" ++ ++ #check is nicName in specified namespace ++ check_nic_in_namespace ${hotsNic} "${ns_name}" ++ fn_check_result_noeq $? 0 "15. ${hotsNic} should not in the specified namespace ${ns_name}" ++ ++ #check nicName in container ++ check_nic_in_container ${ctrNic} "$container_id" ++ fn_check_result_noeq $? 0 "16. ${ctrNic} should not in the container ${container_id}" ++} ++ ++pre ++main ++post ++if [[ ${exit_flag} -ne 0 ]]; then ++ tsm_error "FAILED" ++else ++ tsm_info "DONE" ++fi +diff --git a/types/network.go b/types/network.go +index 524e1d5..87bbc5c 100644 +--- a/types/network.go ++++ b/types/network.go +@@ -16,9 +16,12 @@ package types + import ( + "fmt" + "net" ++ "os" ++ "path/filepath" + "strings" + + "github.com/vishvananda/netlink" ++ "isula.org/syscontainer-tools/libnetwork/nsutils" + ) + + const ( +@@ -53,15 +56,16 @@ type NamespacePath struct { + + // InterfaceConf is the network interface config + type InterfaceConf struct { +- IP string `json:"Ip"` +- IP6 string `json:"Ip6"` +- Mac string `json:"Mac"` +- Mtu int `json:"Mtu"` +- Qlen int `json:"Qlen"` +- Type string `json:"Type"` +- Bridge string `json:"Bridge"` +- HostNicName string `json:"HostNicName"` +- CtrNicName string `json:"CtrNicName"` ++ IP string `json:"Ip"` ++ IP6 string `json:"Ip6"` ++ Mac string `json:"Mac"` ++ Mtu int `json:"Mtu"` ++ Qlen int `json:"Qlen"` ++ Type string `json:"Type"` ++ Bridge string `json:"Bridge"` ++ HostNicName string `json:"HostNicName"` ++ CtrNicName string `json:"CtrNicName"` ++ VethHostNSPath string `json:"Namespace,omitempty"` + } + + func (nic *InterfaceConf) String() string { +@@ -86,6 +90,10 @@ func IsConflictNic(nic1, nic2 *InterfaceConf) error { + if nic1.CtrNicName == nic2.CtrNicName { + return fmt.Errorf("interface name conflict: %s", nic1.CtrNicName) + } ++ // The same container cannot have a host network card with the same name ++ // For example: when the user adds a virtual network card pair and specifies a namespace for ++ // the host-side network card, the host-side network card name cannot have the same name as ++ // other host-side network cards added to the container. + if nic1.HostNicName == nic2.HostNicName { + return fmt.Errorf("interface name conflict: %s", nic1.HostNicName) + } +@@ -113,6 +121,9 @@ func IsSameNic(obj, src *InterfaceConf) bool { + if obj.Mac != src.Mac && obj.Mac != "" { + return false + } ++ if obj.VethHostNSPath != src.VethHostNSPath && obj.VethHostNSPath != "" { ++ return false ++ } + if obj.Mtu != src.Mtu && obj.Mtu != 0 { + return false + } +@@ -198,7 +209,14 @@ func ValidNetworkConfig(conf *InterfaceConf) error { + if conf.Bridge == "" { + return fmt.Errorf("bridge must be specified") + } ++ conf.VethHostNSPath = strings.TrimSpace(conf.VethHostNSPath) ++ if err := validNamespace(conf.VethHostNSPath); err != nil { ++ return err ++ } + case "eth": ++ if conf.VethHostNSPath != "" { ++ return fmt.Errorf("for eth type, namespace should be empty") ++ } + if conf.HostNicName == "" { + return fmt.Errorf("host nic name input error") + } +@@ -214,3 +232,23 @@ func ValidNetworkConfig(conf *InterfaceConf) error { + } + return nil + } ++ ++// validNamespace will check if the namespace is valid, existing and accessible ++func validNamespace(ns string) error { ++ if ns == "" { ++ return nil ++ } ++ // check whether the namespace path is an absolute path ++ if !filepath.IsAbs(ns) { ++ return fmt.Errorf("the specified namespace %v must be an absolute path", ns) ++ } ++ // check whether the namespace path exists ++ if _, err := os.Stat(ns); os.IsNotExist(err) { ++ return fmt.Errorf("the specified namespace %v does not exist", ns) ++ } ++ // use SwitchAndExecute to check whether the namespace is correct ++ if err := nsutils.SwitchAndExecute(ns, func() error { return nil }); err != nil { ++ return fmt.Errorf("invalid network namespace path %v: %v", ns, err) ++ } ++ return nil ++} +diff --git a/types/network_test.go b/types/network_test.go +index fecf8b6..479f70f 100644 +--- a/types/network_test.go ++++ b/types/network_test.go +@@ -15,7 +15,11 @@ + package types + + import ( ++ "os" ++ "os/exec" + "testing" ++ ++ "github.com/sirupsen/logrus" + ) + + // TestIsConflictNic tests IsConflictNic +@@ -214,6 +218,69 @@ func TestIsConflictNic(t *testing.T) { + } + } + ++// TestIsConflictNic tests IsConflictNic with namespace added ++func TestIsConflictNicNamespace(t *testing.T) { ++ const ( ++ testCtrNicName1 = "ctr1" ++ testCtrNicName2 = "ctr2" ++ testHostNicName1 = "host1" ++ testHostNicName2 = "host2" ++ testMac1 = "aa:bb:cc:dd:ee:ff" ++ testIP41 = CIDRIpExample1 ++ testIP42 = CIDRIpExample2 ++ testIP61 = CIDRIp6Example1 ++ testIP62 = CIDRIp6Example2 ++ testMTU = 1500 ++ validNsPath = "/var/run/netns/myNs" ++ validNsPath1 = "/var/run/netns/myNs1" ++ ) ++ type args struct { ++ nic1 *InterfaceConf ++ nic2 *InterfaceConf ++ } ++ tests := []struct { ++ name string ++ args args ++ wantErr bool ++ }{ ++ { ++ name: "TC1-same HostNicName but different VethHostNSPath", ++ args: args{ ++ nic1: &InterfaceConf{ ++ HostNicName: testHostNicName1, ++ VethHostNSPath: validNsPath, ++ }, ++ nic2: &InterfaceConf{ ++ HostNicName: testHostNicName1, ++ VethHostNSPath: validNsPath1, ++ }, ++ }, ++ wantErr: true, ++ }, ++ { ++ name: "TC2-same HostNicName and same VethHostNSPath", ++ args: args{ ++ nic1: &InterfaceConf{ ++ HostNicName: testHostNicName1, ++ VethHostNSPath: validNsPath, ++ }, ++ nic2: &InterfaceConf{ ++ HostNicName: testHostNicName1, ++ VethHostNSPath: validNsPath, ++ }, ++ }, ++ wantErr: true, ++ }, ++ } ++ for _, tt := range tests { ++ t.Run(tt.name, func(t *testing.T) { ++ if err := IsConflictNic(tt.args.nic1, tt.args.nic2); (err != nil) != tt.wantErr { ++ t.Errorf("IsConflictNic() error = %v, wantErr %v", err, tt.wantErr) ++ } ++ }) ++ } ++} ++ + // TestIsSameNic tests IsSameNic + func TestIsSameNic(t *testing.T) { + const ( +@@ -371,6 +438,61 @@ func TestIsSameNic(t *testing.T) { + } + } + ++// TestIsSameNic tests IsSameNic with namespace added ++func TestIsSameNicNamespace(t *testing.T) { ++ const ( ++ testCtrNicName1 = "ctr1" ++ testCtrNicName2 = "ctr2" ++ testHostNicName1 = "host1" ++ testHostNicName2 = "host2" ++ testMac1 = "aa:bb:cc:dd:ee:ff" ++ testMac2 = "ff:ee:dd:cc:bb:aa" ++ testIP41 = CIDRIpExample1 ++ testIP42 = CIDRIpExample2 ++ testIP61 = CIDRIp6Example1 ++ testIP62 = CIDRIp6Example2 ++ testMTU1 = 1500 ++ testMTU2 = 1200 ++ testQlen1 = 500 ++ testQlen2 = 1000 ++ testBridge1 = "test1" ++ testBridge2 = "test2" ++ testType1 = "eth" ++ testType2 = "veth" ++ notEmptyNsPath = "/var/run/netns" ++ notEmptyNsPath1 = "/var/run/netns1" ++ ) ++ type args struct { ++ obj *InterfaceConf ++ src *InterfaceConf ++ } ++ tests := []struct { ++ name string ++ args args ++ want bool ++ }{ ++ { ++ name: "TC1-obj-notEmptyNsPath && obj-notEmptyNsPath != src-notEmptyNsPath1", ++ args: args{ ++ obj: &InterfaceConf{ ++ VethHostNSPath: notEmptyNsPath, ++ }, ++ src: &InterfaceConf{ ++ VethHostNSPath: notEmptyNsPath1, ++ }, ++ }, ++ want: false, ++ }, ++ } ++ for _, tt := range tests { ++ t.Run(tt.name, func(t *testing.T) { ++ if got := IsSameNic(tt.args.obj, tt.args.src); got != tt.want { ++ t.Errorf("IsSameNic() = %v, want %v", got, tt.want) ++ } ++ }) ++ } ++} ++ + // TestValidNetworkConfig tests ValidNetworkConfig + func TestValidNetworkConfig(t *testing.T) { + const ( +@@ -512,3 +634,143 @@ func TestValidNetworkConfig(t *testing.T) { + }) + } + } ++ ++// TestValidNetworkConfigNamespace tests ValidNetworkConfig with namespace added ++func TestValidNetworkConfigNamespace(t *testing.T) { ++ const ( ++ testHostNicName = "host1" ++ testIP4 = CIDRIpExample1 ++ testBridge = "test1" ++ ethType = "eth" ++ vethType = "veth" ++ relativeNsPath = "./net" ++ noExistedNsPath = "/xxx/net" ++ normalFilePath = "/root/test_net" ++ validNsPath = "/var/run/netns/myNs" ++ ) ++ type args struct { ++ conf *InterfaceConf ++ } ++ tests := []struct { ++ name string ++ args args ++ wantErr bool ++ pre func(t *testing.T) error ++ post func(t *testing.T) ++ }{ ++ { ++ name: "TC1-relative namespace path with veth", ++ args: args{ ++ conf: &InterfaceConf{ ++ IP: testIP4, ++ Type: vethType, ++ HostNicName: testHostNicName, ++ Bridge: testBridge, ++ VethHostNSPath: relativeNsPath, ++ }, ++ }, ++ wantErr: true, ++ }, ++ { ++ name: "TC2-non-existed namespace path with veth", ++ args: args{ ++ conf: &InterfaceConf{ ++ IP: testIP4, ++ Type: vethType, ++ HostNicName: testHostNicName, ++ Bridge: testBridge, ++ VethHostNSPath: noExistedNsPath, ++ }, ++ }, ++ wantErr: true, ++ }, ++ { ++ name: "TC3-normal file namespace path with veth", ++ args: args{ ++ conf: &InterfaceConf{ ++ IP: testIP4, ++ Type: vethType, ++ HostNicName: testHostNicName, ++ Bridge: testBridge, ++ VethHostNSPath: normalFilePath, ++ }, ++ }, ++ pre: func(t *testing.T) error { ++ file, err := os.Create(normalFilePath) ++ file.Close() ++ return err ++ }, ++ post: func(t *testing.T) { ++ if err := os.Remove(normalFilePath); err != nil { ++ t.Errorf("failed to delete a incorrectNsPath: %v", err) ++ } ++ }, ++ wantErr: true, ++ }, ++ { ++ name: "TC4-valid namespace path with veth", ++ args: args{ ++ conf: &InterfaceConf{ ++ IP: testIP4, ++ Type: vethType, ++ HostNicName: testHostNicName, ++ Bridge: testBridge, ++ VethHostNSPath: validNsPath, ++ }, ++ }, ++ pre: func(t *testing.T) error { ++ cmd := exec.Command("ip", "netns", "add", "myNs") ++ return cmd.Run() ++ }, ++ post: func(t *testing.T) { ++ cmd := exec.Command("ip", "netns", "delete", "myNs") ++ err := cmd.Run() ++ if err != nil { ++ t.Errorf("failed to delete the network namespace:%v", err) ++ } ++ }, ++ wantErr: false, ++ }, ++ { ++ name: "TC5-empty namespace path with veth", ++ args: args{ ++ conf: &InterfaceConf{ ++ IP: testIP4, ++ Type: vethType, ++ Bridge: testBridge, ++ HostNicName: testHostNicName, ++ VethHostNSPath: "", ++ }, ++ }, ++ wantErr: false, ++ }, ++ { ++ name: "TC6-not empty namespace path with eth", ++ args: args{ ++ conf: &InterfaceConf{ ++ IP: testIP4, ++ Type: ethType, ++ VethHostNSPath: validNsPath, ++ }, ++ }, ++ wantErr: true, ++ }, ++ } ++ for _, tt := range tests { ++ t.Run(tt.name, func(t *testing.T) { ++ if tt.pre != nil { ++ if err := tt.pre(t); err != nil { ++ logrus.Infof("skip TC because it failed to create the network namespace:%v", err) ++ return ++ } ++ } ++ if err := ValidNetworkConfig(tt.args.conf); (err != nil) != tt.wantErr { ++ ++ t.Errorf("ValidNetworkConfig() error = %v, wantErr %v", err, tt.wantErr) ++ } ++ if tt.post != nil { ++ tt.post(t) ++ } ++ }) ++ } ++} +-- +2.33.0 + diff --git a/syscontainer-tools.spec b/syscontainer-tools.spec index fd15798..a1ab4dd 100644 --- a/syscontainer-tools.spec +++ b/syscontainer-tools.spec @@ -1,7 +1,7 @@ #Basic Information Name: syscontainer-tools Version: 0.9 -Release: 65 +Release: 66 Summary: syscontainer tools for IT, work with iSulad License: Mulan PSL v2 URL: https://gitee.com/openeuler/syscontainer-tools @@ -19,6 +19,7 @@ Patch8: 0008-clean-up-run-syscontainer-tools-netns-containerid-di.patch Patch9: 0009-drop-useless-function-error-and-info.patch Patch10: 0010-fix-log-of-removeUdevRule.patch Patch11: 0011-static-compilation-for-cross-version-use.patch +Patch12: 0012-support-Vnic-specifying-network-namespace.patch %ifarch sw_64 Patch1000: 1000-add-sw_64-support-not-upstream-modified-files.patch @@ -127,6 +128,12 @@ chmod 0640 ${HOOK_SPEC}/hookspec.json rm -rfv %{buildroot} %changelog +* Thu Aug 29 2024 wujichao - 0.9-66 +- Type:patch +- CVE:NA +- SUG:NA +- DESC:support-Vnic-specifying-network-namespace + * Fri May 10 2024 yangjiaqi - 0.9-65 - Type:bugfix - CVE:NA -- Gitee