Merge pull request 'Add feature to add next free IP' (#11) from 5-add-next-free-ip-automatically into main

Reviewed-on: https://codeberg.org/lauralani/ipam/pulls/11
This commit is contained in:
Adora Laura Kalb 2023-04-04 14:45:54 +00:00
commit b0aa4063dd
3 changed files with 106 additions and 35 deletions

View file

@ -6,6 +6,7 @@ package cmd
import ( import (
"errors" "errors"
"math"
"net/netip" "net/netip"
"time" "time"
) )
@ -19,6 +20,21 @@ type Subnet struct {
Addresses []Address `json:"addresses"` 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. // HasIP checks if a Subnet already contains given netip.Addr.
// Returns true if the IP already is present, false otherwise. // Returns true if the IP already is present, false otherwise.
func (s Subnet) HasIP(ip netip.Addr) bool { func (s Subnet) HasIP(ip netip.Addr) bool {
@ -33,6 +49,42 @@ func (s Subnet) HasIP(ip netip.Addr) bool {
return iscontained 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 // RemoveIP removes the Address object for given ip from
// the Address list of the subnet. // the Address list of the subnet.
// //

View file

@ -15,67 +15,80 @@ import (
) )
var ipaddCmd = &cobra.Command{ var ipaddCmd = &cobra.Command{
Use: "add ipaddress [hostname]", Use: "add ipaddress|subnet [hostname]",
Short: "Add new IP address", 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"}, Aliases: []string{"a"},
Args: cobra.RangeArgs(1, 2), Args: cobra.RangeArgs(1, 2),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
var ipaddress, hostname string var iparg, hostname string
if len(args) == 1 { if len(args) == 1 {
ipaddress = args[0] iparg = args[0]
hostname = "" hostname = ""
} else { } else {
ipaddress = args[0] iparg = args[0]
hostname = args[1] hostname = args[1]
} }
ip, parseerr := netip.ParseAddr(ipaddress) var bestsubnet Subnet
var ip netip.Addr
// Exit if parsed value is no valid IP argip, ipparseerr := netip.ParseAddr(iparg)
if parseerr != nil { argsubnet, subparseerr := netip.ParsePrefix(iparg)
fmt.Println("[ERROR]", parseerr)
if ipparseerr != nil && subparseerr != nil {
fmt.Printf("[ERROR] Argument is neither a valid IP address nor a valid Subnet: %v", iparg)
os.Exit(1) os.Exit(1)
} } else if ipparseerr == nil && subparseerr != nil {
// argument was a single IP
// Exit if parsed value is an IPv6 Address var subnetexists bool
// TODO: Implement IPv6 support bestsubnet, subnetexists = FindBestSubnet(argip)
//if !ip.Is4() {
// fmt.Printf("[ERROR] IPv6 is not yet supported!\n")
// os.Exit(1)
//}
subnet, subnetexists := FindBestSubnet(ip)
if !subnetexists { if !subnetexists {
fmt.Printf("[ERROR] Found no suitable subnet for IP %v\n", ipaddress) fmt.Printf("[ERROR] Found no suitable subnet for IP %v\n", iparg)
fmt.Printf("[ERROR] Maybe you need to add it first?\n") fmt.Printf("[ERROR] Maybe you need to add it first?\n")
os.Exit(1) os.Exit(1)
} }
if bestsubnet.HasIP(argip) {
if subnet.HasIP(ip) { fmt.Printf("[ERROR] IP %v already exists in subnet %v\n", argip.String(), bestsubnet.Subnet.String())
fmt.Printf("[ERROR] IP %v already exists in subnet %v\n", ip.String(), subnet.Subnet.String())
os.Exit(1) os.Exit(1)
} }
ip = argip
} 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()
if !ip.IsValid() {
fmt.Printf("[ERROR] Found no free IP in Subnet %v\n", argsubnet.String())
os.Exit(1)
}
}
currentuser, _ := user.Current() currentuser, _ := user.Current()
timestamp := time.Now() timestamp := time.Now()
subnet.Addresses = append(subnet.Addresses, Address{ip, hostname, timestamp, currentuser.Username}) bestsubnet.Addresses = append(bestsubnet.Addresses, Address{ip, hostname, timestamp, currentuser.Username})
subnet.ChangedBy = currentuser.Username bestsubnet.ChangedBy = currentuser.Username
subnet.ChangedAt = timestamp bestsubnet.ChangedAt = timestamp
writeerr := subnet.WriteSubnet() writeerr := bestsubnet.WriteSubnet()
if writeerr != nil { if writeerr != nil {
fmt.Println("[ERROR]", writeerr) fmt.Println("[ERROR]", writeerr)
os.Exit(1) os.Exit(1)
} }
if hostname == "" { if hostname == "" {
fmt.Printf("added ip:\nip: %v\n", ipaddress) fmt.Printf("added ip:\nip: %v\n", ip.String())
} else { } 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) dnserr := AddDNSFqdn(hostname, ip)
if dnserr != nil { if dnserr != nil {
fmt.Println("[ERROR]", dnserr) fmt.Println("[ERROR]", dnserr)

View file

@ -114,7 +114,13 @@ func (s Subnet) WriteSubnet() error {
fmt.Println("[ERROR]", fileerr) fmt.Println("[ERROR]", fileerr)
os.Exit(1) 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) _, writeerr := file.Write(data)
if writeerr != nil { if writeerr != nil {
@ -137,12 +143,12 @@ func GetSubnet(net netip.Prefix) (Subnet, error) {
content, readerr := os.ReadFile(filename) content, readerr := os.ReadFile(filename)
if readerr != nil { 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) marsherr := json.Unmarshal(content, &subnet)
if marsherr != nil { 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 return subnet, nil