diff --git a/.gitignore b/.gitignore index eb8635c6..b5124321 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ gopath/ *.sw[ponm] .vagrant release-* +cnitool/cnitool diff --git a/cnitool/LICENSE b/cnitool/LICENSE new file mode 100644 index 00000000..e69de29b diff --git a/cnitool/README.md b/cnitool/README.md index dc48d92a..dbf15197 100644 --- a/cnitool/README.md +++ b/cnitool/README.md @@ -75,3 +75,11 @@ And clean up: sudo CNI_PATH=./bin cnitool del myptp /var/run/netns/testing sudo ip netns del testing ``` + +cnitool also supports the GC command, which takes a list of still-valid attachments +and removes all others. Since cnitool generates the container ID based on the +path to the network namespace, this is the parameter + +```bash +sudo CNI_PATH=./bin cnitool gc myptp /var/run/netns/keep-1 /var/run/netns/keep-2 +``` \ No newline at end of file diff --git a/cnitool/cmd/add.go b/cnitool/cmd/add.go new file mode 100644 index 00000000..297a5465 --- /dev/null +++ b/cnitool/cmd/add.go @@ -0,0 +1,40 @@ +/* +Copyright © 2024 NAME HERE + +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// addCmd represents the add command +var addCmd = &cobra.Command{ + Use: "add", + Short: "A brief description of your command", + Long: `A longer description that spans multiple lines and likely contains examples +and usage of using your command. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("add called") + }, +} + +func init() { + rootCmd.AddCommand(addCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // addCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // addCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cnitool/cmd/root.go b/cnitool/cmd/root.go new file mode 100644 index 00000000..ed18c86c --- /dev/null +++ b/cnitool/cmd/root.go @@ -0,0 +1,51 @@ +/* +Copyright © 2024 NAME HERE + +*/ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" +) + + + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "cnitool", + Short: "A brief description of your application", + Long: `A longer description that spans multiple lines and likely contains +examples and usage of using your application. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + // Uncomment the following line if your bare application + // has an action associated with it: + // Run: func(cmd *cobra.Command, args []string) { }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. + + // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cnitool.yaml)") + + // Cobra also supports local flags, which will only run + // when this action is called directly. + rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} + + diff --git a/cnitool/cnitool.go b/cnitool/cnitool.go deleted file mode 100644 index 0ba91838..00000000 --- a/cnitool/cnitool.go +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright 2015 CNI 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 main - -import ( - "context" - "crypto/sha512" - "encoding/json" - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/containernetworking/cni/libcni" -) - -// Protocol parameters are passed to the plugins via OS environment variables. -const ( - EnvCNIPath = "CNI_PATH" - EnvNetDir = "NETCONFPATH" - EnvCapabilityArgs = "CAP_ARGS" - EnvCNIArgs = "CNI_ARGS" - EnvCNIIfname = "CNI_IFNAME" - - DefaultNetDir = "/etc/cni/net.d" - - CmdAdd = "add" - CmdCheck = "check" - CmdDel = "del" -) - -func parseArgs(args string) ([][2]string, error) { - var result [][2]string - - pairs := strings.Split(args, ";") - for _, pair := range pairs { - kv := strings.Split(pair, "=") - if len(kv) != 2 || kv[0] == "" || kv[1] == "" { - return nil, fmt.Errorf("invalid CNI_ARGS pair %q", pair) - } - - result = append(result, [2]string{kv[0], kv[1]}) - } - - return result, nil -} - -func main() { - if len(os.Args) < 4 { - usage() - } - - netdir := os.Getenv(EnvNetDir) - if netdir == "" { - netdir = DefaultNetDir - } - netconf, err := libcni.LoadConfList(netdir, os.Args[2]) - if err != nil { - exit(err) - } - - var capabilityArgs map[string]interface{} - capabilityArgsValue := os.Getenv(EnvCapabilityArgs) - if len(capabilityArgsValue) > 0 { - if err = json.Unmarshal([]byte(capabilityArgsValue), &capabilityArgs); err != nil { - exit(err) - } - } - - var cniArgs [][2]string - args := os.Getenv(EnvCNIArgs) - if len(args) > 0 { - cniArgs, err = parseArgs(args) - if err != nil { - exit(err) - } - } - - ifName, ok := os.LookupEnv(EnvCNIIfname) - if !ok { - ifName = "eth0" - } - - netns := os.Args[3] - netns, err = filepath.Abs(netns) - if err != nil { - exit(err) - } - - // Generate the containerid by hashing the netns path - s := sha512.Sum512([]byte(netns)) - containerID := fmt.Sprintf("cnitool-%x", s[:10]) - - cninet := libcni.NewCNIConfig(filepath.SplitList(os.Getenv(EnvCNIPath)), nil) - - rt := &libcni.RuntimeConf{ - ContainerID: containerID, - NetNS: netns, - IfName: ifName, - Args: cniArgs, - CapabilityArgs: capabilityArgs, - } - - switch os.Args[1] { - case CmdAdd: - result, err := cninet.AddNetworkList(context.TODO(), netconf, rt) - if result != nil { - _ = result.Print() - } - exit(err) - case CmdCheck: - err := cninet.CheckNetworkList(context.TODO(), netconf, rt) - exit(err) - case CmdDel: - exit(cninet.DelNetworkList(context.TODO(), netconf, rt)) - } -} - -func usage() { - exe := filepath.Base(os.Args[0]) - - fmt.Fprintf(os.Stderr, "%s: Add, check, or remove network interfaces from a network namespace\n", exe) - fmt.Fprintf(os.Stderr, " %s add \n", exe) - fmt.Fprintf(os.Stderr, " %s check \n", exe) - fmt.Fprintf(os.Stderr, " %s del \n", exe) - os.Exit(1) -} - -func exit(err error) { - if err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err) - os.Exit(1) - } - os.Exit(0) -} diff --git a/cnitool/go.mod b/cnitool/go.mod new file mode 100644 index 00000000..f9356f52 --- /dev/null +++ b/cnitool/go.mod @@ -0,0 +1,16 @@ +module github.com/containernetworking/cni/cnitool + +go 1.20 + +require ( + github.com/containernetworking/cni v0.0.0-00010101000000-000000000000 + github.com/spf13/cobra v1.8.0 +) + +require ( + github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect +) + +replace github.com/containernetworking/cni => ../ diff --git a/cnitool/go.sum b/cnitool/go.sum new file mode 100644 index 00000000..732fc7b3 --- /dev/null +++ b/cnitool/go.sum @@ -0,0 +1,24 @@ +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cnitool/main.go b/cnitool/main.go new file mode 100644 index 00000000..41902263 --- /dev/null +++ b/cnitool/main.go @@ -0,0 +1,267 @@ +/* +Copyright © 2024 NAME HERE +*/ +package main + +import ( + "context" + "crypto/sha512" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/containernetworking/cni/libcni" + "github.com/containernetworking/cni/pkg/types" + "github.com/spf13/cobra" +) + +func main() { + initFlags() + + rootCmd.AddCommand(addCmd) + rootCmd.AddCommand(delCmd) + rootCmd.AddCommand(checkCmd) + rootCmd.AddCommand(statusCmd) + rootCmd.AddCommand(gcCmd) + + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +const ( + EnvCNIPath = "CNI_PATH" + EnvNetDir = "NETCONFPATH" + EnvCapabilityArgs = "CAP_ARGS" + EnvCNIArgs = "CNI_ARGS" + EnvCNIIfname = "CNI_IFNAME" +) + +var ( + cniBinDir string + cniConfDir string + + ifName string + capabilityArgs string + cniArgs string + + cniBinDirs []string + capabilityArgsParsed map[string]interface{} + cniArgsParsed [][2]string +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "cnitool", + Short: "execute CNI operations manually", + SilenceUsage: true, +} + +var addCmd = &cobra.Command{ + Use: "add ", + Short: "attach a container to a CNI network", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + return doAttach(cmd.Context(), "add", args) + }, +} + +var delCmd = &cobra.Command{ + Use: "del ", + Short: "delete a container's CNI attachment", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + return doAttach(cmd.Context(), "del", args) + }, +} + +var checkCmd = &cobra.Command{ + Use: "check ", + Short: "check a container's CNI attachment", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + return doAttach(cmd.Context(), "check", args) + }, +} + +var statusCmd = &cobra.Command{ + Use: "status ", + Short: "check a CNI network's status", + Args: cobra.ExactArgs(1), + RunE: doStatus, +} + +var gcCmd = &cobra.Command{ + Use: "gc [[:]]...", + Short: "garbage collect any stale resources", + Long: "GR removes any stale resources, keeping any belonging to the set of network namespaces provided", + Args: cobra.MinimumNArgs(1), + RunE: doGC, +} + +// some args are by-environment; pull them in +func loadArgs() error { + if cniBinDir == "" { + cniBinDir = os.Getenv(EnvCNIPath) + } + if cniBinDir == "" { + return fmt.Errorf("--cni-bin-dir is required") + } + cniBinDirs = filepath.SplitList(cniBinDir) + + if cniConfDir == "" { + cniConfDir = os.Getenv(EnvNetDir) + } + if cniConfDir == "" { + return fmt.Errorf("--cni-conf-dir is required") + } + + if capabilityArgs == "" { + capabilityArgs = os.Getenv(EnvCapabilityArgs) + } + if cniArgs == "" { + cniArgs = os.Getenv(EnvCNIArgs) + } + if i := os.Getenv(EnvCNIIfname); i != "" { + ifName = i + } + + if len(capabilityArgs) > 0 { + if err := json.Unmarshal([]byte(capabilityArgs), &capabilityArgs); err != nil { + return fmt.Errorf("failed to parse capability args: %w", err) + } + } + + if len(cniArgs) > 0 { + for _, pair := range strings.Split(cniArgs, ";") { + kv := strings.Split(pair, "=") + if len(kv) != 2 || kv[0] == "" || kv[1] == "" { + return fmt.Errorf("invalid cni-args pair %q", pair) + } + cniArgsParsed = append(cniArgsParsed, [2]string{kv[0], kv[1]}) + } + } + + return nil +} + +// doAttach executes either add, del, or check +func doAttach(ctx context.Context, op string, args []string) error { + if err := loadArgs(); err != nil { + return err + } + + if len(args) != 2 { + return fmt.Errorf("2 arguments required") + } + + name := args[0] + netns := args[1] + + cninet := libcni.NewCNIConfig(cniBinDirs, nil) + + rt := &libcni.RuntimeConf{ + ContainerID: containerID(netns), + NetNS: netns, + IfName: ifName, + Args: cniArgsParsed, + CapabilityArgs: capabilityArgsParsed, + } + + netconf, err := libcni.LoadConfList(cniConfDir, name) + if err != nil { + return err + } + + switch op { + case "add": + result, err := cninet.AddNetworkList(ctx, netconf, rt) + if result != nil { + _ = result.Print() + } + return err + case "del": + return cninet.DelNetworkList(ctx, netconf, rt) + case "check": + return cninet.CheckNetworkList(ctx, netconf, rt) + } + + return nil +} + +func doStatus(cmd *cobra.Command, args []string) error { + if err := loadArgs(); err != nil { + return err + } + + if len(args) != 1 { + return fmt.Errorf("1 argument required") + } + name := args[0] + + cninet := libcni.NewCNIConfig(cniBinDirs, nil) + netconf, err := libcni.LoadConfList(cniConfDir, name) + if err != nil { + return err + } + + err = cninet.GetStatusNetworkList(cmd.Context(), netconf) + if err != nil { + return fmt.Errorf("network %s is not ready: %w", name, err) + } + cmd.Printf("Network %s is ready for ADD requests\n", name) + return nil +} + +func doGC(cmd *cobra.Command, args []string) error { + if err := loadArgs(); err != nil { + return err + } + + if len(args) < 1 { + return fmt.Errorf("1 argument required") + } + + validAttachments := []types.GCAttachment{} + for _, netns := range args[1:] { + pair := strings.Split(netns, ":") + ifname := "eth0" + if len(pair) > 1 { + ifname = pair[1] + } + + validAttachments = append(validAttachments, types.GCAttachment{ + IfName: ifname, + ContainerID: containerID(netns), + }) + } + + name := args[0] + netconf, err := libcni.LoadConfList(cniConfDir, name) + if err != nil { + return err + } + + cninet := libcni.NewCNIConfig(cniBinDirs, nil) + return cninet.GCNetworkList(cmd.Context(), netconf, &libcni.GCArgs{ValidAttachments: validAttachments}) +} + +func containerID(netns string) string { + s := sha512.Sum512([]byte(netns)) + return fmt.Sprintf("cnitool-%x", s[:10]) +} + +func initFlags() { + rootCmd.PersistentFlags().StringVar(&cniBinDir, "cni-bin-dir", "", "The folder(s) in which to look for CNI plugins, colon-separated.") + rootCmd.PersistentFlags().StringVar(&cniConfDir, "cni-conf-dir", "/etc/cni/net.d", "The folder in which to look for CNI network configurations.") + + // common args between ADD, DEL, and CHECK + for _, cmd := range []*cobra.Command{addCmd, delCmd, checkCmd} { + cmd.Flags().StringVar(&ifName, "ifname", "eth0", "The value to pass to CNI_IFNAME") + cmd.Flags().StringVar(&capabilityArgs, "capability-args", "", "Capability args, json-formatted, to pass to the plugins.") + cmd.Flags().StringVar(&cniArgs, "cni-args", "", "Plugin arguments, in =;... format") + } +} diff --git a/go.work b/go.work new file mode 100644 index 00000000..d6fdf96a --- /dev/null +++ b/go.work @@ -0,0 +1,6 @@ +go 1.21.6 + +use ( + . + ./cnitool +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 00000000..923dfbce --- /dev/null +++ b/go.work.sum @@ -0,0 +1,7 @@ +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=