diff --git a/cmd/classes.go b/cmd/classes.go index 7abd575..35a8357 100644 --- a/cmd/classes.go +++ b/cmd/classes.go @@ -6,6 +6,7 @@ package cmd import ( "errors" + "math" "net/netip" "time" ) @@ -19,6 +20,21 @@ type Subnet struct { Addresses []Address `json:"addresses"` } +// GetIPCount gets the IP count for the Subnet +// +// Returns the IP count or -1 if it's a IPv6 prefix +func (s Subnet) GetIPCount() int { + if !s.Subnet.Addr().Is4() { + return -1 + } + hostbits := float64(32 - s.Subnet.Bits()) + if s.Subnet.Bits() == 31 { + return 2 + } else { + return int(math.Pow(2, hostbits)) - 2 + } +} + // HasIP checks if a Subnet already contains given netip.Addr. // Returns true if the IP already is present, false otherwise. func (s Subnet) HasIP(ip netip.Addr) bool { @@ -33,6 +49,42 @@ func (s Subnet) HasIP(ip netip.Addr) bool { return iscontained } +// FindFirstFreeIP finds and returns the next free netip.Addr +// or an invalid netip.Addr if no free IP was found +func (s Subnet) FindFirstFreeIP() netip.Addr { + var ip netip.Addr + + if s.Subnet.Addr().Is4() { + subnetips := s.GetIPCount() + + // handling /31 prefixes + if subnetips == 2 { + ip = s.Subnet.Addr() + } else { + ip = s.Subnet.Addr().Next() + } + + // start at first free IP + for count := 0; count < subnetips; count++ { + + if s.HasIP(ip) { + ip = ip.Next() + } else { + return ip + } + } + } else { + ip = s.Subnet.Addr().Next() + for ; s.Subnet.Contains(ip); ip = ip.Next() { + if !s.HasIP(ip) { + return ip + } + } + } + + return netip.Addr{} +} + // RemoveIP removes the Address object for given ip from // the Address list of the subnet. // diff --git a/cmd/ip-add.go b/cmd/ip-add.go index 2a1ed31..26d0739 100644 --- a/cmd/ip-add.go +++ b/cmd/ip-add.go @@ -15,67 +15,80 @@ import ( ) var ipaddCmd = &cobra.Command{ - Use: "add ipaddress [hostname]", + Use: "add ipaddress|subnet [hostname]", Short: "Add new IP address", - Long: `Add new IP address`, + Long: `Adds a new IP address OR the next free IP address from a subnet`, Aliases: []string{"a"}, Args: cobra.RangeArgs(1, 2), Run: func(cmd *cobra.Command, args []string) { - var ipaddress, hostname string + var iparg, hostname string if len(args) == 1 { - ipaddress = args[0] + iparg = args[0] hostname = "" } else { - ipaddress = args[0] + iparg = args[0] hostname = args[1] } - ip, parseerr := netip.ParseAddr(ipaddress) + var bestsubnet Subnet + var ip netip.Addr - // Exit if parsed value is no valid IP - if parseerr != nil { - fmt.Println("[ERROR]", parseerr) + argip, ipparseerr := netip.ParseAddr(iparg) + argsubnet, subparseerr := netip.ParsePrefix(iparg) + + if ipparseerr != nil && subparseerr != nil { + fmt.Printf("[ERROR] Argument is neither a valid IP address nor a valid Subnet: %v", iparg) os.Exit(1) - } + } else if ipparseerr == nil && subparseerr != nil { + // argument was a single IP + var subnetexists bool + bestsubnet, subnetexists = FindBestSubnet(argip) + if !subnetexists { + fmt.Printf("[ERROR] Found no suitable subnet for IP %v\n", iparg) + fmt.Printf("[ERROR] Maybe you need to add it first?\n") + os.Exit(1) + } + if bestsubnet.HasIP(argip) { + fmt.Printf("[ERROR] IP %v already exists in subnet %v\n", argip.String(), bestsubnet.Subnet.String()) + os.Exit(1) + } + ip = argip - // Exit if parsed value is an IPv6 Address - // TODO: Implement IPv6 support - //if !ip.Is4() { - // fmt.Printf("[ERROR] IPv6 is not yet supported!\n") - // os.Exit(1) - //} + } else if subparseerr == nil && ipparseerr != nil { + // argument was a subnet + var subneterr error + bestsubnet, subneterr = GetSubnet(argsubnet) + if subneterr != nil { + fmt.Println("[ERROR]", subneterr) + os.Exit(1) + } + ip = bestsubnet.FindFirstFreeIP() - subnet, subnetexists := FindBestSubnet(ip) + if !ip.IsValid() { + fmt.Printf("[ERROR] Found no free IP in Subnet %v\n", argsubnet.String()) + os.Exit(1) + } - 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) - } - - if subnet.HasIP(ip) { - fmt.Printf("[ERROR] IP %v already exists in subnet %v\n", ip.String(), subnet.Subnet.String()) - os.Exit(1) } currentuser, _ := user.Current() timestamp := time.Now() - subnet.Addresses = append(subnet.Addresses, Address{ip, hostname, timestamp, currentuser.Username}) - subnet.ChangedBy = currentuser.Username - subnet.ChangedAt = timestamp + bestsubnet.Addresses = append(bestsubnet.Addresses, Address{ip, hostname, timestamp, currentuser.Username}) + bestsubnet.ChangedBy = currentuser.Username + bestsubnet.ChangedAt = timestamp - writeerr := subnet.WriteSubnet() + writeerr := bestsubnet.WriteSubnet() if writeerr != nil { fmt.Println("[ERROR]", writeerr) os.Exit(1) } if hostname == "" { - fmt.Printf("added ip:\nip: %v\n", ipaddress) + fmt.Printf("added ip:\nip: %v\n", ip.String()) } else { - fmt.Printf("added ip:\nip: %v\nhostname: %v\n", ipaddress, hostname) + fmt.Printf("added ip:\nip: %v\nhostname: %v\n", ip.String(), hostname) dnserr := AddDNSFqdn(hostname, ip) if dnserr != nil { fmt.Println("[ERROR]", dnserr) diff --git a/cmd/storage.go b/cmd/storage.go index 9240997..a9c9782 100644 --- a/cmd/storage.go +++ b/cmd/storage.go @@ -114,7 +114,13 @@ func (s Subnet) WriteSubnet() error { fmt.Println("[ERROR]", fileerr) os.Exit(1) } - defer file.Close() + defer func(file *os.File) { + err := file.Close() + if err != nil { + fmt.Println("[ERROR]", err) + os.Exit(1) + } + }(file) _, writeerr := file.Write(data) if writeerr != nil { @@ -137,12 +143,12 @@ func GetSubnet(net netip.Prefix) (Subnet, error) { content, readerr := os.ReadFile(filename) if readerr != nil { - return Subnet{}, readerr + return Subnet{}, fmt.Errorf("can't open file for subnet %v for reading", net.String()) } marsherr := json.Unmarshal(content, &subnet) if marsherr != nil { - return Subnet{}, marsherr + return Subnet{}, fmt.Errorf("can't unmarshal file contents of file %v\n%v", filename, marsherr) } return subnet, nil