diff --git a/cmd/apply.go b/cmd/apply.go index d8e9cf2..2954205 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -22,21 +22,27 @@ SOFTWARE. package cmd import ( + "fmt" "log/slog" "netnsplan/config" + "netnsplan/iproute2" + "slices" "github.com/spf13/cobra" ) -// applyCmd represents the apply command +var alwaysRunPostScript bool + var applyCmd = &cobra.Command{ Use: "apply", Short: "Apply netns networks configuration to running system", Long: "Apply netns networks configuration to running system", RunE: func(cmd *cobra.Command, args []string) error { for netns, values := range cfg.Netns { + needPostScript := true if ip.ExistsNetns(netns) { slog.Warn("netns is already exists", "name", netns) + needPostScript = false } else { slog.Info("create netns", "name", netns) err := ip.AddNetns(netns) @@ -65,18 +71,28 @@ var applyCmd = &cobra.Command{ return err } - err = RunPostScript(netns, values.PostScript) - if err != nil { - return err + if needPostScript || alwaysRunPostScript { + err = RunPostScript(netns, values.PostScript) + if err != nil { + return err + } } } return nil }, } +func init() { + rootCmd.AddCommand(applyCmd) + applyCmd.Flags().BoolVarP(&alwaysRunPostScript, "always-run-post-script", "R", false, "always run post-script. by default, runs only when a netns is created.") +} + type IpCommand interface { SetLinkUp(name string) error + ShowLink(name string) (*iproute2.Link, error) + ShowInterface(name string) (*iproute2.InterfaceInfo, error) AddAddress(name, address string) error + ShowRoutes(name string) (iproute2.Routes, error) AddRoute(name, to, via string) error InNetns() bool Netns() string @@ -88,19 +104,48 @@ func SetupDevice(ip IpCommand, name string, addresses []string, routes []config. return err } - if ip.InNetns() { - slog.Info("add addresses", "name", name, "addresses", addresses, "netns", ip.Netns()) - } else { - slog.Info("add addresses", "name", name, "addresses", addresses) + iface, err := ip.ShowInterface(name) + if err != nil { + return err + } + + var intAddrs []string + for _, i := range iface.AddrInfo { + intAddrs = append(intAddrs, fmt.Sprintf("%s/%d", i.Local, i.Prefixlen)) } + for _, address := range addresses { - err := ip.AddAddress(name, address) + if slices.Contains(intAddrs, address) { + slog.Debug("address is already exists", "name", name, "address", address) + continue + } + + if ip.InNetns() { + slog.Info("add addresses", "name", name, "address", address, "netns", ip.Netns()) + } else { + slog.Info("add addresses", "name", name, "address", address) + } + + err = ip.AddAddress(name, address) if err != nil { return err } } + rt, err := ip.ShowRoutes(name) + if err != nil { + return err + } + for _, route := range routes { + slog.Debug("route", "name", name, "to", route.To, "via", route.Via, "rt", rt) + if slices.ContainsFunc(rt, func(r iproute2.Route) bool { + return r.Dst == route.To && r.Gateway == route.Via + }) { + slog.Debug("route is already exists", "name", name, "to", route.To, "via", route.Via) + continue + } + if ip.InNetns() { slog.Info("add route", "name", name, "to", route.To, "via", route.Via, "netns", ip.Netns()) } else { @@ -115,6 +160,16 @@ func SetupDevice(ip IpCommand, name string, addresses []string, routes []config. } func SetLinkUp(ip IpCommand, name string) error { + link, err := ip.ShowLink(name) + if err != nil { + return err + } + + if slices.Contains(link.Flags, "UP") { + slog.Debug("link is already up", "name", name) + return nil + } + if ip.InNetns() { slog.Info("link up", "name", name, "netns", ip.Netns()) } else { @@ -124,23 +179,33 @@ func SetLinkUp(ip IpCommand, name string) error { return ip.SetLinkUp(name) } -func SetupLoopback(netns string) error { - return SetLinkUp(ip.IntoNetns(netns), "lo") -} - func SetNetns(name string, netns string) error { slog.Info("set netns", "name", name, "netns", netns) return ip.SetNetns(name, netns) } +func SetupLoopback(netns string) error { + return SetLinkUp(ip.IntoNetns(netns), "lo") +} + func SetupEthernets(netns string, ethernets map[string]config.Ethernet) error { + n := ip.IntoNetns(netns) for name, values := range ethernets { - err := SetNetns(name, netns) - if err != nil { - return err + _, err := n.ShowLink(name) + if err == nil { + slog.Debug("device is already exists in netns", "name", name, "netns", netns) + } else { + if _, ok := err.(*iproute2.NotExistError); ok { + err := SetNetns(name, netns) + if err != nil { + return err + } + } else { + return err + } } - err = SetupDevice(ip.IntoNetns(netns), name, values.Addresses, values.Routes) + err = SetupDevice(n, name, values.Addresses, values.Routes) if err != nil { return err } @@ -149,13 +214,21 @@ func SetupEthernets(netns string, ethernets map[string]config.Ethernet) error { } func SetupDummyDevices(netns string, devices map[string]config.Ethernet) error { + n := ip.IntoNetns(netns) for name, values := range devices { - n := ip.IntoNetns(netns) - - slog.Info("add dummy device", "name", name, "netns", netns) - err := n.AddDummyDevice(name) - if err != nil { - return err + _, err := n.ShowLink(name) + if err == nil { + slog.Debug("device is already exists in netns", "name", name, "netns", netns) + } else { + if _, ok := err.(*iproute2.NotExistError); !ok { + return err + } else { + slog.Info("add dummy device", "name", name, "netns", netns) + err := n.AddDummyDevice(name) + if err != nil { + return err + } + } } err = SetupDevice(n, name, values.Addresses, values.Routes) @@ -167,33 +240,63 @@ func SetupDummyDevices(netns string, devices map[string]config.Ethernet) error { } func SetupVethDevices(netns string, devices map[string]config.VethDevice) error { + n := ip.IntoNetns(netns) for name, values := range devices { peerName := values.Peer.Name peerNetns := values.Peer.Netns - slog.Info("add veth device", "name", name, "peer name", peerName) - err := ip.AddVethDevice(name, peerName) - if err != nil { - return err - } + // check if device is already exists in netns + _, err := n.ShowLink(name) + if err == nil { + slog.Debug("device is already exists in netns", "name", name, "netns", netns) + } else { + if _, ok := err.(*iproute2.NotExistError); !ok { + return err + } else { + // check if device is already exists in "default" netns + _, e := ip.ShowLink(name) + if e == nil { + slog.Debug("device is already exists", "name", name) + } else { + if _, ok := err.(*iproute2.NotExistError); !ok { + return err + } else { + slog.Info("add veth device", "name", name, "peer name", peerName) + err := ip.AddVethDevice(name, peerName) + if err != nil { + return err + } + } + } - err = SetNetns(name, netns) - if err != nil { - return err + err = SetNetns(name, netns) + if err != nil { + return err + } + } } - n := ip.IntoNetns(netns) err = SetupDevice(n, name, values.Addresses, values.Routes) if err != nil { return err } if peerNetns != "" { - err = SetNetns(peerName, peerNetns) - if err != nil { - return err - } n := ip.IntoNetns(peerNetns) + + _, err := n.ShowLink(peerName) + if err == nil { + slog.Debug("device is already exists in netns", "name", peerName, "netns", peerNetns) + } else { + if _, ok := err.(*iproute2.NotExistError); !ok { + return err + } else { + err = SetNetns(peerName, peerNetns) + if err != nil { + return err + } + } + } err = SetupDevice(n, peerName, values.Peer.Addresses, values.Peer.Routes) if err != nil { return err @@ -208,10 +311,6 @@ func SetupVethDevices(netns string, devices map[string]config.VethDevice) error return nil } -func init() { - rootCmd.AddCommand(applyCmd) -} - func RunPostScript(netns string, script string) error { if script == "" { return nil diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..8edfeb3 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,111 @@ +/* +Copyright © 2024 buty4649 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +package config + +import ( + "os" + "reflect" + "testing" +) + +const testYAML = ` +netns: + netns1: + ethernets: + eth0: + addresses: + - "192.168.1.1/24" + routes: + - to: "0.0.0.0/0" + via: "192.168.1.254" + dummy-devices: + dummy0: + addresses: + - "10.0.0.1/8" + veth-devices: + veth0: + addresses: + - "10.1.0.1/24" + peer: + name: "veth0-peer" + netns: "netns2" + addresses: + - "10.1.0.2/24" + post-script: | + echo 'Hello' + echo 'World!' +` + +func TestLoadConfig(t *testing.T) { + tmpfile, err := os.CreateTemp("", "test*.yaml") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tmpfile.Name()) + + if _, err := tmpfile.Write([]byte(testYAML)); err != nil { + t.Fatalf("Failed to write to temp file: %v", err) + } + if err := tmpfile.Close(); err != nil { + t.Fatalf("Failed to close temp file: %v", err) + } + + config, err := LoadConfig(tmpfile.Name()) + if err != nil { + t.Fatalf("LoadConfig returned an error: %v", err) + } + + expected := &Config{ + Netns: map[string]Netns{ + "netns1": { + Ethernets: map[string]Ethernet{ + "eth0": { + Addresses: []string{"192.168.1.1/24"}, + Routes: []Route{ + {To: "0.0.0.0/0", Via: "192.168.1.254"}, + }, + }, + }, + DummyDevices: map[string]Ethernet{ + "dummy0": { + Addresses: []string{"10.0.0.1/8"}, + }, + }, + VethDevices: map[string]VethDevice{ + "veth0": { + Addresses: []string{"10.1.0.1/24"}, + Peer: Peer{ + Name: "veth0-peer", + Netns: "netns2", + Addresses: []string{"10.1.0.2/24"}, + }, + }, + }, + PostScript: "echo 'Hello'\necho 'World!'\n", + }, + }, + } + + if !reflect.DeepEqual(config, expected) { + t.Errorf("Config does not match expected\nGot: %#v\nWant: %#v", config, expected) + } +} diff --git a/iproute2/base.go b/iproute2/base.go index 56bc01e..1a38617 100644 --- a/iproute2/base.go +++ b/iproute2/base.go @@ -23,7 +23,6 @@ package iproute2 import ( "encoding/json" - "fmt" "io" "os/exec" "strings" @@ -42,17 +41,21 @@ func (b *BaseCommand) run(args ...string) error { func (b *BaseCommand) runIpCommand(args ...string) (string, error) { cmd := append([]string{b.path}, args...) - return b.runCommand(cmd, nil) -} + out, err := b.runCommand(cmd, nil) + if err == nil { + return out, nil + } -type Error struct { - ExitStatus int - Message string -} + msg := err.Error() + if strings.Contains(msg, "Operation not permitted") { + return "", &OperationNotPermittedError{Msg: msg} + } -func (e *Error) Error() string { - msg := strings.TrimRight(e.Message, "\n") - return fmt.Sprintf("%s (exit status: %d)", msg, e.ExitStatus) + if strings.Contains(msg, "does not exist") { + return "", &NotExistError{Msg: msg} + } + + return "", &UnknownError{Msg: err.Error()} } func (b *BaseCommand) runCommand(cmd []string, input *string) (string, error) { @@ -83,9 +86,9 @@ func (b *BaseCommand) runCommand(cmd []string, input *string) (string, error) { exitErr, _ := err.(*exec.ExitError) status, _ := exitErr.Sys().(syscall.WaitStatus) stderr := string(exitErr.Stderr) - return "", &Error{ + return "", &CommandError{ ExitStatus: status.ExitStatus(), - Message: stderr, + Msg: stderr, } } return string(stdout), nil @@ -146,13 +149,21 @@ type AddressInfo struct { PreferredLifeTime uint64 `json:"preferred_life_time"` } +type OperState string + +const ( + OperStateUp OperState = "UP" + OperStateDown OperState = "DOWN" + OperStateUnkwon OperState = "UNKNOWN" +) + type InterfaceInfo struct { Ifindex int `json:"ifindex"` Ifname string `json:"ifname"` Flags []string `json:"flags"` Mtu int `json:"mtu"` Qdisc string `json:"qdisc"` - Operstate string `json:"operstate"` + Operstate OperState `json:"operstate"` Group string `json:"group"` Txqlen int `json:"txqlen"` LinkType string `json:"link_type"` @@ -161,51 +172,127 @@ type InterfaceInfo struct { AddrInfo []AddressInfo `json:"addr_info"` } -type Addresses []InterfaceInfo +type Interfaces []InterfaceInfo -func (b *BaseCommand) ListAddresses() (*Addresses, error) { +func (b *BaseCommand) ListInterfaces() (Interfaces, error) { data, err := b.runIpCommand("-json", "address", "show") if err != nil { return nil, err } - var addresses Addresses - err = json.Unmarshal([]byte(data), &addresses) + return unmarshalInterfacesData(data) +} + +func (b *BaseCommand) ShowInterface(name string) (*InterfaceInfo, error) { + data, err := b.runIpCommand("-json", "address", "show", "dev", name) if err != nil { return nil, err } - return &addresses, nil + i, err := unmarshalInterfacesData(data) + if err != nil { + return nil, err + } + + return &i[0], err +} + +func unmarshalInterfacesData(data string) (Interfaces, error) { + var addresses Interfaces + err := json.Unmarshal([]byte(data), &addresses) + if err != nil { + return nil, err + } + + return addresses, nil } type Link struct { - Ifindex int `json:"ifindex"` - Ifname string `json:"ifname"` - Flags []string `json:"flags"` - Mtu int `json:"mtu"` - Qdisc string `json:"qdisc"` - Operstate string `json:"operstate"` - Linkmode string `json:"linkmode"` - Group string `json:"group"` - Txqlen int `json:"txqlen"` - LinkType string `json:"link_type"` - Address string `json:"address"` - Broadcast string `json:"broadcast"` + Ifindex int `json:"ifindex"` + Ifname string `json:"ifname"` + Flags []string `json:"flags"` + Mtu int `json:"mtu"` + Qdisc string `json:"qdisc"` + Operstate OperState `json:"operstate"` + Linkmode string `json:"linkmode"` + Group string `json:"group"` + Txqlen int `json:"txqlen"` + LinkType string `json:"link_type"` + Address string `json:"address"` + Broadcast string `json:"broadcast"` } type Links []Link -func (b *BaseCommand) ListLinks() (*Links, error) { +func (b *BaseCommand) ListLinks() (Links, error) { data, err := b.runIpCommand("-json", "link", "show") if err != nil { return nil, err } + return unmarshalLinksData(data) +} +func (b *BaseCommand) ShowLink(name string) (*Link, error) { + data, err := b.runIpCommand("-json", "link", "show", "dev", name) + if err != nil { + return nil, err + } + + links, err := unmarshalLinksData(data) + if err != nil { + return nil, err + } + + return &links[0], nil +} + +func unmarshalLinksData(data string) (Links, error) { var links Links - err = json.Unmarshal([]byte(data), &links) + err := json.Unmarshal([]byte(data), &links) + if err != nil { + return nil, err + } + + return links, nil +} + +type Route struct { + Dst string `json:"dst,omitempty"` + Gateway string `json:"gateway,omitempty"` + Dev string `json:"dev,omitempty"` + Type string `json:"type,omitempty"` + Protocol string `json:"protocol,omitempty"` + Scope string `json:"scope,omitempty"` + PrefSrc string `json:"prefsrc,omitempty"` + Flags []string `json:"flags,omitempty"` +} + +type Routes []Route + +func (b *BaseCommand) ListRoutes() (Routes, error) { + data, err := b.runIpCommand("-json", "route", "show") + if err != nil { + return nil, err + } + + return unmarshalRoutesData(data) +} + +func (b *BaseCommand) ShowRoutes(name string) (Routes, error) { + data, err := b.runIpCommand("-json", "route", "show", "dev", name) + if err != nil { + return nil, err + } + + return unmarshalRoutesData(data) +} + +func unmarshalRoutesData(data string) (Routes, error) { + var routes []Route + err := json.Unmarshal([]byte(data), &routes) if err != nil { return nil, err } - return &links, nil + return routes, nil } diff --git a/iproute2/base_test.go b/iproute2/base_test.go new file mode 100644 index 0000000..e2c2bdb --- /dev/null +++ b/iproute2/base_test.go @@ -0,0 +1,130 @@ +/* +Copyright © 2024 buty4649 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +package iproute2 + +import ( + "reflect" + "testing" +) + +func TestUnmarshalInterfacesData(t *testing.T) { + testCases := []struct { + desc string + input string + expected Interfaces + expectingErr bool + }{ + { + desc: "Valid input", + input: `[{"ifindex":1,"ifname":"lo","flags":["LOOPBACK","UP","LOWER_UP"],"mtu":65536,"qdisc":"noqueue","operstate":"UNKNOWN","group":"default","txqlen":1000,"link_type":"loopback","address":"00:00:00:00:00:00","broadcast":"00:00:00:00:00:00","addr_info":[{"family":"inet","local":"127.0.0.1","prefixlen":8,"scope":"host","label":"lo","valid_life_time":4294967295,"preferred_life_time":4294967295},{"family":"inet6","local":"::1","prefixlen":128,"scope":"host","valid_life_time":4294967295,"preferred_life_time":4294967295}]}]`, + expected: Interfaces{ + { + Ifindex: 1, + Ifname: "lo", + Flags: []string{"LOOPBACK", "UP", "LOWER_UP"}, + Mtu: 65536, + Qdisc: "noqueue", + Operstate: OperStateUnkwon, + Group: "default", + Txqlen: 1000, + LinkType: "loopback", + Address: "00:00:00:00:00:00", + Broadcast: "00:00:00:00:00:00", + AddrInfo: []AddressInfo{ + {Family: "inet", Local: "127.0.0.1", Prefixlen: 8, Scope: "host", Label: "lo", ValidLifeTime: 4294967295, PreferredLifeTime: 4294967295}, + {Family: "inet6", Local: "::1", Prefixlen: 128, Scope: "host", Label: "", ValidLifeTime: 4294967295, PreferredLifeTime: 4294967295}, + }, + }, + }, + expectingErr: false, + }, + { + desc: "Invalid input", + input: `invalid JSON`, + expected: nil, + expectingErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + got, err := unmarshalInterfacesData(tc.input) + if (err != nil) != tc.expectingErr { + t.Errorf("unmarshalInterfacesData() error = %v, expectingErr %v", err, tc.expectingErr) + return + } + if !reflect.DeepEqual(got, tc.expected) { + t.Errorf("unmarshalInterfacesData() = %v, want %v", got, tc.expected) + } + }) + } +} + +func TestUnmarshalLinksData(t *testing.T) { + testCases := []struct { + desc string + input string + expected Links + expectingErr bool + }{ + { + desc: "Valid input", + input: `[{"ifindex":1,"ifname":"lo","flags":["LOOPBACK"],"mtu":65536,"qdisc":"noop","operstate":"DOWN","linkmode":"DEFAULT","group":"default","txqlen":1000,"link_type":"loopback","address":"00:00:00:00:00:00","broadcast":"00:00:00:00:00:00"}]`, + expected: Links{ + { + Ifindex: 1, + Ifname: "lo", + Flags: []string{"LOOPBACK"}, + Mtu: 65536, + Qdisc: "noop", + Operstate: OperStateDown, + Linkmode: "DEFAULT", + Group: "default", + Txqlen: 1000, + LinkType: "loopback", + Address: "00:00:00:00:00:00", + Broadcast: "00:00:00:00:00:00", + }, + }, + expectingErr: false, + }, + { + desc: "Invalid input", + input: `invalid JSON`, + expected: nil, + expectingErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + got, err := unmarshalLinksData(tc.input) + if (err != nil) != tc.expectingErr { + t.Errorf("unmarshalLinksData() error = %v, expectingErr %v", err, tc.expectingErr) + return + } + if !reflect.DeepEqual(got, tc.expected) { + t.Errorf("unmarshalLinksData() = %v, want %v", got, tc.expected) + } + }) + } +} diff --git a/iproute2/error.go b/iproute2/error.go new file mode 100644 index 0000000..87ea93e --- /dev/null +++ b/iproute2/error.go @@ -0,0 +1,62 @@ +/* +Copyright © 2024 buty4649 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +package iproute2 + +import ( + "fmt" + "strings" +) + +type NotExistError struct { + Msg string +} + +func (e *NotExistError) Error() string { + return e.Msg +} + +type OperationNotPermittedError struct { + Msg string +} + +func (e *OperationNotPermittedError) Error() string { + return e.Msg +} + +type UnknownError struct { + Msg string +} + +func (e *UnknownError) Error() string { + return e.Msg +} + +type CommandError struct { + ExitStatus int + Msg string +} + +func (e *CommandError) Error() string { + msg := strings.TrimRight(e.Msg, "\n") + msg = strings.TrimPrefix(msg, "Error: ") + return fmt.Sprintf("%s (exit status: %d)", msg, e.ExitStatus) +} diff --git a/iproute2/error_test.go b/iproute2/error_test.go new file mode 100644 index 0000000..faf1a53 --- /dev/null +++ b/iproute2/error_test.go @@ -0,0 +1,62 @@ +/* +Copyright © 2024 buty4649 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +package iproute2 + +import "testing" + +func TestNotExistError(t *testing.T) { + msg := "Device \"test\" does not exist" + err := &NotExistError{Msg: msg} + + if got := err.Error(); got != msg { + t.Errorf("NotExistError.Error() = %v, want %v", got, msg) + } +} + +func TestOperationNotPermittedError(t *testing.T) { + msg := "mount --make-shared /run/testnetns failed: Operation not permitted" + err := &OperationNotPermittedError{Msg: msg} + + if got := err.Error(); got != msg { + t.Errorf("OperationNotPermittedError.Error() = %v, want %v", got, msg) + } +} + +func TestCommandError(t *testing.T) { + exitStatus := 1 + msg := "Command failed\n" + want := "Command failed (exit status: 1)" + err := &CommandError{ExitStatus: exitStatus, Msg: msg} + + if got := err.Error(); got != want { + t.Errorf("CommandError.Error() = %v, want %v", got, want) + } + + exitStatus = 2 + msg = "Error: Nexthop has invalid gateway.\n" + want = "Nexthop has invalid gateway. (exit status: 2)" + err = &CommandError{ExitStatus: exitStatus, Msg: msg} + + if got := err.Error(); got != want { + t.Errorf("CommandError.Error() = %v, want %v", got, want) + } +}