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 (
"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.
//

View file

@ -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)
}
// 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)
//}
subnet, subnetexists := FindBestSubnet(ip)
} 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", ipaddress)
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 subnet.HasIP(ip) {
fmt.Printf("[ERROR] IP %v already exists in subnet %v\n", ip.String(), subnet.Subnet.String())
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
} 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()
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)

View file

@ -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