diff --git a/cmd/ip-add.go b/cmd/ip-add.go index 82ddddb..1c9f2aa 100644 --- a/cmd/ip-add.go +++ b/cmd/ip-add.go @@ -6,7 +6,7 @@ package cmd import ( "fmt" - "net" + "net/netip" "os" "github.com/spf13/cobra" @@ -29,30 +29,42 @@ var ipaddCmd = &cobra.Command{ hostname = args[1] } - ip := net.ParseIP(ipaddress) + ip, parseerr := netip.ParseAddr(ipaddress) // Exit if parsed value is no valid IP - if ip == nil { - fmt.Printf("[ERROR] not a valid IP: %v\n", ipaddress) + if parseerr != nil { + fmt.Println("[ERROR]", parseerr) os.Exit(1) } // Exit if parsed value is an IPv6 Address // TODO: Implement IPv6 support - if ip.To4() == nil { + if !ip.Is4() { fmt.Printf("[ERROR] IPv6 is not yet supported!\n") os.Exit(1) } - // TODO: Check if there is already a subnet that can contain this IP, err if not + subnet, subnetexists := SearchBestSubnet(ip) - if hostname == "" { - fmt.Printf("Adding IP %v\n", ipaddress) - } else { - fmt.Printf("Adding IP %v with hostname %v\n", ipaddress, hostname) + if !subnetexists { + fmt.Printf("[ERROR] Found no suitable subnet for IP %v\n", ipaddress) + fmt.Printf("[ERROR] Maybe you need to add it first?\n") + os.Exit(1) } - // TODO: Save to file + subnet.Addresses = append(subnet.Addresses, Address{ip.String(), hostname}) + + writeerr := WriteSubnet(subnet) + if writeerr != nil { + fmt.Println("[ERROR]", writeerr) + os.Exit(1) + } + + if hostname == "" { + fmt.Printf("added ip:\nip: %v\n", ipaddress) + } else { + fmt.Printf("added ip:\nip: %v\nhostname: %v\n", ipaddress, hostname) + } }, } diff --git a/cmd/root.go b/cmd/root.go index f1b0963..0343be2 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -84,7 +84,4 @@ func initConfig() { println("[ERROR] Can't read config file!", err) } } - - fmt.Println(viper.AllKeys()) - } diff --git a/cmd/storage.go b/cmd/storage.go index cb2fa2c..9b4d9b8 100644 --- a/cmd/storage.go +++ b/cmd/storage.go @@ -5,11 +5,17 @@ Copyright © 2023 Laura Kalb package cmd import ( - "net" + "bufio" + "fmt" + "net/netip" + "os" + "strings" + + "github.com/spf13/viper" ) type Subnet struct { - Subnet net.IPNet + Subnet netip.Prefix Name string Vlan string Addresses []Address @@ -20,15 +26,76 @@ type Address struct { FQDN string } -// GetSubnet tries to load the most fitting IP subnet file on disk. -// It takes an IP object and tries to get the best subnet (meaning -// the subnet with the smallest subnet size). +// SearchBestSubnet tries to load the most fitting IP subnet file +// on disk. It takes an IP object and tries to get the best subnet +// (meaning the subnet with the smallest subnet size). // // Returns the best subnet as Subnet object and true if a suitable // subnet was found, otherwise an empty Subnet object and false. -func GetSubnet(ip net.IP) (Subnet, bool) { +func SearchBestSubnet(ip netip.Addr) (Subnet, bool) { + subnets := ListSubnets() + var smallestprefix int = 0 + bestmatch, _ := netip.ParsePrefix("0.0.0.0/32") + var isipv4 bool = ip.Is4() + var subnet Subnet - return Subnet{}, false + for _, net := range subnets { + prefix, _ := netip.ParsePrefix(net) + if prefix.Addr().Is4() == isipv4 { + if prefix.Contains(ip) { + if prefix.Bits() > smallestprefix { + bestmatch = prefix + } + } + } + } + if !bestmatch.Addr().IsUnspecified() { + var geterr error + subnet, geterr = GetSubnet(bestmatch) + if geterr != nil { + fmt.Println("[ERROR]", geterr) + os.Exit(1) + } + return subnet, true + } else { + return Subnet{}, false + } +} + +// SubnetExists will return true if the given subnet already exists +// on file, false otherwise. +func SubnetExists(net netip.Prefix) bool { + subnets := ListSubnets() + + for _, b := range subnets { + if b == net.String() { + return true + } + } + return false +} + +// ListSubnets returns a list of strings containing the current +// subnets configured. +func ListSubnets() []string { + subnets := make([]string, 0) + var datadir string = viper.GetString("DataPath") + + subnetfiles, readerr := os.ReadDir(datadir) + + if len(subnetfiles) == 0 { + return subnets + } + + if readerr != nil { + fmt.Println("[ERROR]", readerr) + os.Exit(1) + } + for _, element := range subnetfiles { + subnets = append(subnets, strings.Replace(element.Name(), "_", "/", 1)) + } + + return subnets } // WriteSubnet takes a given Subnet object and tries to write it to @@ -36,7 +103,101 @@ func GetSubnet(ip net.IP) (Subnet, bool) { // // Returns nil on success or the error that happened. func WriteSubnet(subnet Subnet) error { - //if subnet.Subnet + var datadir string = viper.GetString("DataPath") + + _, direrr := os.Stat(datadir) + if direrr != nil { + mkerr := os.MkdirAll(datadir, 0755) + if mkerr != nil { + println("[ERROR] Can't create ipam config directory!", mkerr) + os.Exit(1) + } + } + + filename := datadir + strings.Replace(subnet.Subnet.String(), "/", "_", 1) + + file, fileerr := os.Create(filename) + if fileerr != nil { + fmt.Println("[ERROR]", fileerr) + os.Exit(1) + } + defer file.Close() + + _, suberr := file.WriteString(subnet.Subnet.String() + "\n") + if suberr != nil { + fmt.Println("[ERROR]", suberr) + os.Exit(1) + } + + _, nameerr := file.WriteString(subnet.Name + "\n") + if nameerr != nil { + fmt.Println("[ERROR]", nameerr) + os.Exit(1) + } + + _, vlanerr := file.WriteString(subnet.Vlan + "\n") + if vlanerr != nil { + fmt.Println("[ERROR]", vlanerr) + os.Exit(1) + } + + if len(subnet.Addresses) != 0 { + for _, element := range subnet.Addresses { + _, err := file.WriteString(element.IP + ":" + element.FQDN + "\n") + if err != nil { + fmt.Println("[ERROR]", err) + os.Exit(1) + } + } + } return nil } + +// GetSubnet reads the corresponding file for the given +// netip.Prefix net and constructs a Subnet object. +// +// Returns the Subnet object and nil if the file read was +// successful, an empty Subnet object and the error otherwise. +func GetSubnet(net netip.Prefix) (Subnet, error) { + var datadir string = viper.GetString("DataPath") + filename := datadir + strings.Replace(net.String(), "/", "_", 1) + var subnet Subnet = Subnet{} + + // open file + file, openerr := os.Open(filename) + if openerr != nil { + return Subnet{}, openerr + } + // remember to close the file at the end of the program + defer file.Close() + + // read the file line by line using scanner + scanner := bufio.NewScanner(file) + + var counter int = 0 + for scanner.Scan() { + switch counter { + case 0: + subnet.Subnet, _ = netip.ParsePrefix(scanner.Text()) + + case 1: + subnet.Name = scanner.Text() + + case 2: + subnet.Vlan = scanner.Text() + + default: + s := strings.Split(scanner.Text(), ":") + a := Address{s[0], s[1]} + subnet.Addresses = append(subnet.Addresses, a) + } + counter = counter + 1 + } + + if scanerr := scanner.Err(); scanerr != nil { + return Subnet{}, openerr + } + + return subnet, nil +} diff --git a/cmd/subnet-add.go b/cmd/subnet-add.go index e682fed..5c1f0f2 100644 --- a/cmd/subnet-add.go +++ b/cmd/subnet-add.go @@ -6,7 +6,7 @@ package cmd import ( "fmt" - "net" + "net/netip" "os" "github.com/spf13/cobra" @@ -14,29 +14,37 @@ import ( // addCmd represents the add command var subnetaddCmd = &cobra.Command{ - Use: "add subnet [vlan]", + Use: "add subnet subnet-name [vlan]", Short: "Add a new subnet", Long: `Add a new subnet`, - Args: cobra.RangeArgs(1, 2), + Args: cobra.RangeArgs(2, 3), Aliases: []string{"a"}, Run: func(cmd *cobra.Command, args []string) { var subnet string var vlanid string var netname string - if len(args) == 1 { + if len(args) == 2 { subnet = args[0] - + netname = args[1] vlanid = "-" } - if len(args) == 2 { + if len(args) == 3 { subnet = args[0] - vlanid = args[1] + netname = args[1] + vlanid = args[2] } // Parse subnet into ParseCIDR to test if it's a valid subnet - _, ipnet, err := net.ParseCIDR(subnet) + // _, ipnet, err := net.ParseCIDR(subnet) + ipnet, err := netip.ParsePrefix(subnet) + + // Exit if subnet already exists, no need to add it then + if SubnetExists(ipnet) { + fmt.Printf("[ERROR] Subnet already exists: %v\n", subnet) + os.Exit(1) + } // Exit if parsed value is no valid IP if err != nil { @@ -46,23 +54,28 @@ var subnetaddCmd = &cobra.Command{ // Exit if parsed value is an IPv6 Address // TODO: Implement IPv6 support - if ipnet.IP.To4() == nil { + if !ipnet.Addr().Is4() { fmt.Printf("[ERROR] IPv6 is not yet supported!\n") os.Exit(1) } - // Ask for Subnet Name - // TODO: Check if net name only contains letters, numbers and hyphens - fmt.Printf("Subnet name: ") - fmt.Scan(&netname) + subnetobject := Subnet{} + subnetobject.Subnet = ipnet + subnetobject.Name = netname + subnetobject.Vlan = vlanid - if vlanid == "-" { - fmt.Printf("Adding Subnet %v.\n", subnet) - } else { - fmt.Printf("Adding Subnet %v with VLAN Tag %v.\n", subnet, vlanid) + writeerr := WriteSubnet(subnetobject) + + if writeerr != nil { + fmt.Println("[ERROR]", err) + os.Exit(1) } - // TODO: Save subnet to file + if vlanid == "-" { + fmt.Printf("added subnet:\nnet: %v\nname: %v\n", subnet, netname) + } else { + fmt.Printf("added subnet:\nnet: %v\nname: %v\nvlan: %v\n", subnet, netname, vlanid) + } }, }