diff --git a/CHANGELOG.md b/CHANGELOG.md index 29459bb9..b9456a6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Add completion for profile list. #1695 - Add OPTIONS argument for `warewulfd.service`. #1707 - Document `warewulf.conf:dhcp.template`. #1701 +- New template field `IpCIDR`. #1700 +- `wwctl configure` persists auto-detected server network settings to `warewulf.conf`. #1700 ### Changed @@ -22,6 +24,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Only calculate image chroot size when requested. #1504 - Create temporary files in overlay directory during `wwctl overlay edit`. #1473 - Re-order SSH key types to make ed25519 default. #981 +- Don't assume default values for `warewulf.conf` network settings. #1700 +- Omit DHCP pool from `dhcpd.conf` if any required fields are missing. #1700 +- `warewulf.conf:ipaddr6` is no longer required to be a `/64` or smaller. #1700 ### Fixed @@ -46,6 +51,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Removed - Remove `warewulf.conf:syslog`. #1606 +- Properly handle parsing of server network and netmask from CIDR `warewulf.conf:ipaddr`. #1541, #1594 +- Populate template field `NetworkCIDR`. #1700 ## v4.6.0rc1, 2025-01-29 diff --git a/Variables.mk b/Variables.mk index 8926e230..f6394dd1 100644 --- a/Variables.mk +++ b/Variables.mk @@ -1,7 +1,7 @@ -include Defaults.mk # Linux distro (try and set to /etc/os-release ID) -OS_REL := $(shell sed -n "s/^ID\s*=\s*['"\""]\(.*\)['"\""]/\1/p" /etc/os-release) +OS_REL := $(shell sh -c '. /etc/os-release; echo $ID') OS ?= $(OS_REL) ARCH_REL := $(shell uname -p) diff --git a/internal/app/wwclient/root.go b/internal/app/wwclient/root.go index 8034deb2..c1c75b79 100644 --- a/internal/app/wwclient/root.go +++ b/internal/app/wwclient/root.go @@ -54,11 +54,11 @@ func GetRootCommand() *cobra.Command { func CobraRunE(cmd *cobra.Command, args []string) (err error) { conf := warewulfconf.Get() if WarewulfConfArg != "" { - err = conf.Read(WarewulfConfArg) + err = conf.Read(WarewulfConfArg, false) } else if os.Getenv("WAREWULFCONF") != "" { - err = conf.Read(os.Getenv("WAREWULFCONF")) + err = conf.Read(os.Getenv("WAREWULFCONF"), false) } else { - err = conf.Read(warewulfconf.ConfigFile) + err = conf.Read(warewulfconf.ConfigFile, false) } if err != nil { return diff --git a/internal/app/wwctl/configure/main.go b/internal/app/wwctl/configure/main.go index fad8c4af..090a2353 100644 --- a/internal/app/wwctl/configure/main.go +++ b/internal/app/wwctl/configure/main.go @@ -11,6 +11,14 @@ import ( func CobraRunE(cmd *cobra.Command, args []string) error { var err error + + conf := warewulfconf.Get() + if conf.Autodetected() && conf.InitializedFromFile() { + if err = conf.PersistToFile(conf.GetWarewulfConf()); err != nil { + wwlog.Warn("error when persisting auto-detected settings: %s", err) + } + } + if allFunctions { err = configure.DHCP() if err != nil { diff --git a/internal/app/wwctl/overlay/imprt/main_test.go b/internal/app/wwctl/overlay/imprt/main_test.go index b78ea764..2003ce93 100644 --- a/internal/app/wwctl/overlay/imprt/main_test.go +++ b/internal/app/wwctl/overlay/imprt/main_test.go @@ -52,7 +52,7 @@ nodes: {} conf_yml := `` conf := warewulfconf.New() - err = conf.Parse([]byte(conf_yml)) + err = conf.Parse([]byte(conf_yml), false) assert.NoError(t, err) warewulfd.SetNoDaemon() conf.Paths.WWOverlaydir = overlayDir diff --git a/internal/app/wwctl/root.go b/internal/app/wwctl/root.go index 6941309e..1f9c05bc 100644 --- a/internal/app/wwctl/root.go +++ b/internal/app/wwctl/root.go @@ -87,13 +87,10 @@ func rootPersistentPreRunE(cmd *cobra.Command, args []string) (err error) { conf := warewulfconf.Get() if !AllowEmptyConf && !conf.InitializedFromFile() { - if err = conf.Read(warewulfconf.ConfigFile); err != nil { + if err = conf.Read(warewulfconf.ConfigFile, true); err != nil { wwlog.Error("error reading config file: %s", err) return } } - if err = conf.SetDynamicDefaults(); err != nil { - wwlog.Error("error setting default configuration: %s", err) - } return } diff --git a/internal/pkg/config/root.go b/internal/pkg/config/root.go index 50796e8b..a79a0b57 100644 --- a/internal/pkg/config/root.go +++ b/internal/pkg/config/root.go @@ -10,12 +10,9 @@ import ( "bytes" "fmt" "net" - "net/netip" "os" "reflect" - "github.com/pkg/errors" - "github.com/creasty/defaults" "github.com/warewulf/warewulf/internal/pkg/wwlog" "gopkg.in/yaml.v3" @@ -44,6 +41,7 @@ type WarewulfYaml struct { WWClient *WWClientConf `yaml:"wwclient,omitempty"` warewulfconf string + autodetected bool } // New caches and returns a new [WarewulfYaml] initialized with empty @@ -76,12 +74,12 @@ func Get() *WarewulfYaml { // Read populates [WarewulfYaml] with the values from a configuration // file. -func (conf *WarewulfYaml) Read(confFileName string) error { +func (conf *WarewulfYaml) Read(confFileName string, autodetect bool) error { wwlog.Debug("Reading warewulf.conf from: %s", confFileName) conf.warewulfconf = confFileName if data, err := os.ReadFile(confFileName); err != nil { return err - } else if err := conf.Parse(data); err != nil { + } else if err := conf.Parse(data, autodetect); err != nil { return err } else { return nil @@ -89,7 +87,7 @@ func (conf *WarewulfYaml) Read(confFileName string) error { } // Parse populates [WarewulfYaml] with the values from a yaml document. -func (conf *WarewulfYaml) Parse(data []byte) error { +func (conf *WarewulfYaml) Parse(data []byte, autodetect bool) error { // ipxe binaries are merged not overwritten, store defaults separate defIpxe := make(map[string]string) for k, v := range conf.TFTP.IpxeBinaries { @@ -103,98 +101,82 @@ func (conf *WarewulfYaml) Parse(data []byte) error { conf.TFTP.IpxeBinaries = defIpxe } - // check whether ip addr is CIDR type and configure related fields as required if ip, network, err := net.ParseCIDR(conf.Ipaddr); err == nil { conf.Ipaddr = ip.String() if conf.Network == "" { conf.Network = network.IP.String() } if conf.Netmask == "" { - mask := network.Mask - conf.Netmask = fmt.Sprintf("%d.%d.%d.%d", mask[0], mask[1], mask[2], mask[3]) + conf.Netmask = net.IP(network.Mask).String() } } + + if autodetect { + if conf.Ipaddr == "" { + if ip := GetOutboundIP(); ip != nil { + conf.Ipaddr = ip.String() + conf.autodetected = true + } + } + + if conf.Netmask == "" { + if ip := net.ParseIP(conf.Ipaddr); ip != nil { + if network, err := GetIPNetForIP(ip); err == nil { + conf.Netmask = net.IP(network.Mask).String() + conf.autodetected = true + } + } + } + + if conf.Network == "" { + if ip := net.ParseIP(conf.Ipaddr); ip != nil { + if mask := net.IPMask(net.ParseIP(conf.Netmask)); mask != nil { + conf.Network = ip.Mask(mask).String() + conf.autodetected = true + } + } + } + } + + if conf.Ipaddr6 != "" { + if _, network, err := net.ParseCIDR(conf.Ipaddr6); err == nil { + if conf.Ipv6net == "" { + conf.Ipv6net = network.IP.String() + } + } else { + return fmt.Errorf("invalid ipv6 address: must use CIDR notation: %s", conf.Ipaddr6) + } + } + return nil } -// SetDynamicDefaults populates [WarewulfYaml] with plausible defaults for -// the runtime environment. -func (conf *WarewulfYaml) SetDynamicDefaults() (err error) { - if conf.Ipaddr == "" || conf.Netmask == "" || conf.Network == "" { - var mask net.IPMask - var network *net.IPNet - var ip net.IP - cidr := conf.Ipaddr - - if conf.Ipaddr == "" { - wwlog.Verbose("Configuration has no valid network, going to dynamic values") - conn, err := net.Dial("udp", "8.8.8.8:80") - if err == nil { - defer conn.Close() - ipaddr := conn.LocalAddr().(*net.UDPAddr).IP - mask = ipaddr.DefaultMask() - sz, _ := mask.Size() - cidr = ipaddr.String() + fmt.Sprintf("/%d", sz) - } else { - cidr = "192.168.1.1/24" - } - } else if addr, err := netip.ParseAddr(conf.Ipaddr); err == nil { - // if the ipaddr does not have mask appended, update it with default generated mask - ipaddr := net.IP(addr.AsSlice()) - sz, _ := ipaddr.DefaultMask().Size() - cidr = fmt.Sprintf("%s/%d", conf.Ipaddr, sz) - // otherwise, the following code will handle the ipaddr format: invalid or xxx.xxx.xxx.xxx/xx - } - - ip, network, err = net.ParseCIDR(cidr) - if err == nil { - mask = network.Mask - } else { - return fmt.Errorf("Couldn't parse IP address: %w", err) - } - if conf.Netmask == "" { - conf.Netmask = fmt.Sprintf("%d.%d.%d.%d", mask[0], mask[1], mask[2], mask[3]) - wwlog.Verbose("Netmask address is not configured in warewulf.conf, using %s", conf.Netmask) - } - if conf.Network == "" { - conf.Network = network.IP.String() - wwlog.Verbose("Network is not configured in warewulf.conf, using %s", conf.Network) - } - // always update the ipaddr to valid ip - conf.Ipaddr = ip.String() +func (config *WarewulfYaml) NetworkCIDR() string { + if config.Network == "" || config.Netmask == "" { + return "" } - if conf.DHCP.RangeStart == "" && conf.DHCP.RangeEnd == "" { - start := net.ParseIP(conf.Network).To4() - start[3] += 1 - if start.Equal(net.ParseIP(conf.Ipaddr)) { - start[3] += 1 - } - conf.DHCP.RangeStart = start.String() - wwlog.Verbose("dhpd start is not configured in warewulf.conf, using %s", conf.DHCP.RangeStart) - sz, _ := net.IPMask(net.ParseIP(conf.Netmask).To4()).Size() - range_end := (1 << (32 - sz)) / 8 - if range_end > 127 { - range_end = 127 - } - end := net.ParseIP(conf.Network).To4() - end[3] += byte(range_end) - conf.DHCP.RangeEnd = end.String() - wwlog.Verbose("dhpd end is not configured in warewulf.conf, using %s", conf.DHCP.RangeEnd) + cidr := net.IPNet{ + IP: net.ParseIP(config.Network), + Mask: net.IPMask(net.ParseIP(config.Netmask)), + } + if cidr.IP == nil || cidr.Mask == nil { + return "" + } + return cidr.String() +} +func (config *WarewulfYaml) IpCIDR() string { + if config.Ipaddr == "" || config.Netmask == "" { + return "" } - // check validity of ipv6 net - if conf.Ipaddr6 != "" { - _, ipv6net, err := net.ParseCIDR(conf.Ipaddr6) - if err != nil { - wwlog.Error("Invalid ipv6 address specified, mut be CIDR notation: %s", conf.Ipaddr6) - return errors.New("invalid ipv6 network") - } - if msize, _ := ipv6net.Mask.Size(); msize > 64 { - wwlog.Error("ipv6 mask size must be smaller than 64") - return errors.New("invalid ipv6 network size") - } + cidr := net.IPNet{ + IP: net.ParseIP(config.Ipaddr), + Mask: net.IPMask(net.ParseIP(config.Netmask)), } - return + if cidr.IP == nil || cidr.Mask == nil { + return "" + } + return cidr.String() } // InitializedFromFile returns true if [WarewulfYaml] memory was read from @@ -207,6 +189,10 @@ func (conf *WarewulfYaml) GetWarewulfConf() string { return conf.warewulfconf } +func (conf *WarewulfYaml) Autodetected() bool { + return conf.autodetected +} + func (config *WarewulfYaml) Dump() ([]byte, error) { var buf bytes.Buffer yamlEncoder := yaml.NewEncoder(&buf) diff --git a/internal/pkg/config/root_test.go b/internal/pkg/config/root_test.go index 9e75b92d..9794af2c 100644 --- a/internal/pkg/config/root_test.go +++ b/internal/pkg/config/root_test.go @@ -7,69 +7,289 @@ import ( "github.com/stretchr/testify/assert" ) -func TestDefaultWarewulfYaml(t *testing.T) { - conf := New() - - assert.Equal(t, 9873, conf.Warewulf.Port) - assert.True(t, conf.Warewulf.Secure()) - assert.Equal(t, 60, conf.Warewulf.UpdateInterval) - assert.True(t, conf.Warewulf.AutobuildOverlays()) - assert.True(t, conf.Warewulf.EnableHostOverlay()) - - assert.True(t, conf.DHCP.Enabled()) - assert.Equal(t, "default", conf.DHCP.Template) - assert.Empty(t, conf.DHCP.RangeStart) - assert.Empty(t, conf.DHCP.RangeEnd) - assert.Equal(t, "dhcpd", conf.DHCP.SystemdName) - - assert.True(t, conf.TFTP.Enabled()) - assert.NotEmpty(t, conf.TFTP.TftpRoot) - assert.Equal(t, "tftp", conf.TFTP.SystemdName) - assert.NotEmpty(t, conf.TFTP.IpxeBinaries["00:00"]) - assert.NotEmpty(t, conf.TFTP.IpxeBinaries["00:07"]) - assert.NotEmpty(t, conf.TFTP.IpxeBinaries["00:09"]) - assert.NotEmpty(t, conf.TFTP.IpxeBinaries["00:0B"]) - - assert.True(t, conf.NFS.Enabled()) - assert.Empty(t, conf.NFS.ExportsExtended) - assert.Equal(t, "nfsd", conf.NFS.SystemdName) - - assert.Equal(t, "/etc/resolv.conf", conf.MountsImage[0].Source) - assert.Equal(t, "/etc/resolv.conf", conf.MountsImage[0].Dest) - assert.False(t, conf.MountsImage[0].ReadOnly()) - assert.Empty(t, conf.MountsImage[0].Options) - - assert.NotEmpty(t, conf.Paths.Bindir) - assert.NotEmpty(t, conf.Paths.Sysconfdir) - assert.NotEmpty(t, conf.Paths.Localstatedir) - assert.NotEmpty(t, conf.Paths.Srvdir) - assert.NotEmpty(t, conf.Paths.Firewallddir) - assert.NotEmpty(t, conf.Paths.Systemddir) - assert.NotEmpty(t, conf.Paths.WWOverlaydir) - assert.NotEmpty(t, conf.Paths.WWChrootdir) - assert.NotEmpty(t, conf.Paths.WWProvisiondir) - assert.NotEmpty(t, conf.Paths.WWClientdir) - assert.NotEmpty(t, conf.Paths.Cachedir) -} - -func TestInitializedFromFile(t *testing.T) { - example_warewulf_conf := "" - tempWarewulfConf, warewulfConfErr := os.CreateTemp("", "warewulf.conf-") - assert.NoError(t, warewulfConfErr) - defer os.Remove(tempWarewulfConf.Name()) - _, warewulfConfErr = tempWarewulfConf.Write([]byte(example_warewulf_conf)) - assert.NoError(t, warewulfConfErr) - assert.NoError(t, tempWarewulfConf.Sync()) - - conf := New() - assert.False(t, conf.InitializedFromFile()) - assert.NoError(t, conf.Read(tempWarewulfConf.Name())) - assert.True(t, conf.InitializedFromFile()) - assert.Equal(t, conf.GetWarewulfConf(), tempWarewulfConf.Name()) -} - -func TestExampleWarewulfYaml(t *testing.T) { - example_warewulf_conf := ` +func TestParse(t *testing.T) { + tests := map[string]struct { + input string + result string + }{ + "default": { + input: ``, + result: ` +warewulf: + autobuild overlays: true + grubboot: false + host overlay: true + port: 9873 + secure: true + update interval: 60 +nfs: + enabled: true + systemd name: nfsd +dhcp: + enabled: true + systemd name: dhcpd + template: default +image mounts: +- dest: /etc/resolv.conf + source: /etc/resolv.conf +paths: + bindir: /usr/local/bin + cachedir: /var/local/cache + datadir: /usr/local/share + firewallddir: /usr/lib/firewalld/services + ipxesource: /usr/local/share/ipxe + localstatedir: /var/local + srvdir: /srv + sysconfdir: /usr/local/etc + systemddir: /usr/lib/systemd/system + wwchrootdir: /var/local/warewulf/chroots + wwclientdir: /warewulf + wwoverlaydir: /var/local/warewulf/overlays + wwprovisiondir: /var/local/warewulf/provision +ssh: + key types: + - ed25519 + - ecdsa + - rsa + - dsa +tftp: + enabled: true + ipxe: + "00:00": undionly.kpxe + "00:07": ipxe-snponly-x86_64.efi + "00:09": ipxe-snponly-x86_64.efi + "00:0B": arm64-efi/snponly.efi + systemd name: tftp + tftproot: /var/lib/tftpboot +`, + }, + "cidr": { + input: ` +ipaddr: 192.168.0.1/24 +`, + result: ` +ipaddr: 192.168.0.1 +network: 192.168.0.0 +netmask: 255.255.255.0 +warewulf: + autobuild overlays: true + grubboot: false + host overlay: true + port: 9873 + secure: true + update interval: 60 +nfs: + enabled: true + systemd name: nfsd +dhcp: + enabled: true + systemd name: dhcpd + template: default +image mounts: +- dest: /etc/resolv.conf + source: /etc/resolv.conf +paths: + bindir: /usr/local/bin + cachedir: /var/local/cache + datadir: /usr/local/share + firewallddir: /usr/lib/firewalld/services + ipxesource: /usr/local/share/ipxe + localstatedir: /var/local + srvdir: /srv + sysconfdir: /usr/local/etc + systemddir: /usr/lib/systemd/system + wwchrootdir: /var/local/warewulf/chroots + wwclientdir: /warewulf + wwoverlaydir: /var/local/warewulf/overlays + wwprovisiondir: /var/local/warewulf/provision +ssh: + key types: + - ed25519 + - ecdsa + - rsa + - dsa +tftp: + enabled: true + ipxe: + "00:00": undionly.kpxe + "00:07": ipxe-snponly-x86_64.efi + "00:09": ipxe-snponly-x86_64.efi + "00:0B": arm64-efi/snponly.efi + systemd name: tftp + tftproot: /var/lib/tftpboot +`, + }, + "cidr with conflicts": { + input: ` +ipaddr: 192.168.1.1/24 +network: 192.168.0.0 +netmask: 255.255.0.0 +`, + result: ` +ipaddr: 192.168.1.1 +network: 192.168.0.0 +netmask: 255.255.0.0 +warewulf: + autobuild overlays: true + grubboot: false + host overlay: true + port: 9873 + secure: true + update interval: 60 +nfs: + enabled: true + systemd name: nfsd +dhcp: + enabled: true + systemd name: dhcpd + template: default +image mounts: +- dest: /etc/resolv.conf + source: /etc/resolv.conf +paths: + bindir: /usr/local/bin + cachedir: /var/local/cache + datadir: /usr/local/share + firewallddir: /usr/lib/firewalld/services + ipxesource: /usr/local/share/ipxe + localstatedir: /var/local + srvdir: /srv + sysconfdir: /usr/local/etc + systemddir: /usr/lib/systemd/system + wwchrootdir: /var/local/warewulf/chroots + wwclientdir: /warewulf + wwoverlaydir: /var/local/warewulf/overlays + wwprovisiondir: /var/local/warewulf/provision +ssh: + key types: + - ed25519 + - ecdsa + - rsa + - dsa +tftp: + enabled: true + ipxe: + "00:00": undionly.kpxe + "00:07": ipxe-snponly-x86_64.efi + "00:09": ipxe-snponly-x86_64.efi + "00:0B": arm64-efi/snponly.efi + systemd name: tftp + tftproot: /var/lib/tftpboot +`, + }, + "ipv6 cidr": { + input: ` +ipaddr6: "2001:db8::1/64" +`, + result: ` +ipaddr6: "2001:db8::1/64" +ipv6net: "2001:db8::" +warewulf: + autobuild overlays: true + grubboot: false + host overlay: true + port: 9873 + secure: true + update interval: 60 +nfs: + enabled: true + systemd name: nfsd +dhcp: + enabled: true + systemd name: dhcpd + template: default +image mounts: +- dest: /etc/resolv.conf + source: /etc/resolv.conf +paths: + bindir: /usr/local/bin + cachedir: /var/local/cache + datadir: /usr/local/share + firewallddir: /usr/lib/firewalld/services + ipxesource: /usr/local/share/ipxe + localstatedir: /var/local + srvdir: /srv + sysconfdir: /usr/local/etc + systemddir: /usr/lib/systemd/system + wwchrootdir: /var/local/warewulf/chroots + wwclientdir: /warewulf + wwoverlaydir: /var/local/warewulf/overlays + wwprovisiondir: /var/local/warewulf/provision +ssh: + key types: + - ed25519 + - ecdsa + - rsa + - dsa +tftp: + enabled: true + ipxe: + "00:00": undionly.kpxe + "00:07": ipxe-snponly-x86_64.efi + "00:09": ipxe-snponly-x86_64.efi + "00:0B": arm64-efi/snponly.efi + systemd name: tftp + tftproot: /var/lib/tftpboot +`, + }, + "ipv6 cidr conflict": { + input: ` +ipaddr6: "2001:db8:1::1/64" +ipv6net: "2001:db8:2::" +`, + result: ` +ipaddr6: "2001:db8:1::1/64" +ipv6net: "2001:db8:2::" +warewulf: + autobuild overlays: true + grubboot: false + host overlay: true + port: 9873 + secure: true + update interval: 60 +nfs: + enabled: true + systemd name: nfsd +dhcp: + enabled: true + systemd name: dhcpd + template: default +image mounts: +- dest: /etc/resolv.conf + source: /etc/resolv.conf +paths: + bindir: /usr/local/bin + cachedir: /var/local/cache + datadir: /usr/local/share + firewallddir: /usr/lib/firewalld/services + ipxesource: /usr/local/share/ipxe + localstatedir: /var/local + srvdir: /srv + sysconfdir: /usr/local/etc + systemddir: /usr/lib/systemd/system + wwchrootdir: /var/local/warewulf/chroots + wwclientdir: /warewulf + wwoverlaydir: /var/local/warewulf/overlays + wwprovisiondir: /var/local/warewulf/provision +ssh: + key types: + - ed25519 + - ecdsa + - rsa + - dsa +tftp: + enabled: true + ipxe: + "00:00": undionly.kpxe + "00:07": ipxe-snponly-x86_64.efi + "00:09": ipxe-snponly-x86_64.efi + "00:0B": arm64-efi/snponly.efi + systemd name: tftp + tftproot: /var/lib/tftpboot +`, + }, + "example": { + input: ` ipaddr: 192.168.200.1 netmask: 255.255.255.0 network: 192.168.200.0 @@ -98,8 +318,84 @@ nfs: image mounts: - source: /etc/resolv.conf dest: /etc/resolv.conf - readonly: true` + readonly: true +`, + result: ` +ipaddr: 192.168.200.1 +netmask: 255.255.255.0 +network: 192.168.200.0 +warewulf: + autobuild overlays: true + grubboot: false + host overlay: true + port: 9873 + secure: false + update interval: 60 +nfs: + enabled: true + systemd name: nfs-server + export paths: + - path: /home + export options: rw,sync + - path: /opt + export options: ro,sync,no_root_squash +dhcp: + enabled: true + systemd name: dhcpd + template: default + range end: 192.168.200.99 + range start: 192.168.200.50 +image mounts: +- dest: /etc/resolv.conf + readonly: true + source: /etc/resolv.conf +paths: + bindir: /usr/local/bin + cachedir: /var/local/cache + datadir: /usr/local/share + firewallddir: /usr/lib/firewalld/services + ipxesource: /usr/local/share/ipxe + localstatedir: /var/local + srvdir: /srv + sysconfdir: /usr/local/etc + systemddir: /usr/lib/systemd/system + wwchrootdir: /var/local/warewulf/chroots + wwclientdir: /warewulf + wwoverlaydir: /var/local/warewulf/overlays + wwprovisiondir: /var/local/warewulf/provision +ssh: + key types: + - ed25519 + - ecdsa + - rsa + - dsa +tftp: + enabled: true + ipxe: + "00:00": undionly.kpxe + "00:07": ipxe-snponly-x86_64.efi + "00:09": ipxe-snponly-x86_64.efi + "00:0B": arm64-efi/snponly.efi + systemd name: tftp + tftproot: /var/lib/tftpboot +`, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + conf := New() + err := conf.Parse([]byte(tt.input), false) + assert.NoError(t, err) + result, err := conf.Dump() + assert.NoError(t, err) + assert.YAMLEq(t, tt.result, string(result)) + }) + } +} + +func TestInitializedFromFile(t *testing.T) { + example_warewulf_conf := "" tempWarewulfConf, warewulfConfErr := os.CreateTemp("", "warewulf.conf-") assert.NoError(t, warewulfConfErr) defer os.Remove(tempWarewulfConf.Name()) @@ -108,36 +404,10 @@ image mounts: assert.NoError(t, tempWarewulfConf.Sync()) conf := New() - assert.NoError(t, conf.Read(tempWarewulfConf.Name())) - - assert.Equal(t, "192.168.200.1", conf.Ipaddr) - assert.Equal(t, "255.255.255.0", conf.Netmask) - assert.Equal(t, "192.168.200.0", conf.Network) - - assert.Equal(t, 9873, conf.Warewulf.Port) - assert.False(t, conf.Warewulf.Secure()) - assert.Equal(t, 60, conf.Warewulf.UpdateInterval) - assert.True(t, conf.Warewulf.AutobuildOverlays()) - assert.True(t, conf.Warewulf.EnableHostOverlay()) - - assert.True(t, conf.DHCP.Enabled()) - assert.Equal(t, "192.168.200.50", conf.DHCP.RangeStart) - assert.Equal(t, "192.168.200.99", conf.DHCP.RangeEnd) - assert.Equal(t, "dhcpd", conf.DHCP.SystemdName) - - assert.True(t, conf.TFTP.Enabled()) - assert.Equal(t, "tftp", conf.TFTP.SystemdName) - - assert.True(t, conf.NFS.Enabled()) - assert.Equal(t, "/home", conf.NFS.ExportsExtended[0].Path) - assert.Equal(t, "rw,sync", conf.NFS.ExportsExtended[0].ExportOptions) - assert.Equal(t, "/opt", conf.NFS.ExportsExtended[1].Path) - assert.Equal(t, "ro,sync,no_root_squash", conf.NFS.ExportsExtended[1].ExportOptions) - assert.Equal(t, "nfs-server", conf.NFS.SystemdName) - - assert.Equal(t, "/etc/resolv.conf", conf.MountsImage[0].Source) - assert.Equal(t, "/etc/resolv.conf", conf.MountsImage[0].Dest) - assert.True(t, conf.MountsImage[0].ReadOnly()) + assert.False(t, conf.InitializedFromFile()) + assert.NoError(t, conf.Read(tempWarewulfConf.Name(), false)) + assert.True(t, conf.InitializedFromFile()) + assert.Equal(t, conf.GetWarewulfConf(), tempWarewulfConf.Name()) } func TestCache(t *testing.T) { @@ -154,3 +424,99 @@ func TestCache(t *testing.T) { New() assert.NotEqual(t, 9999, Get().Warewulf.Port) } + +func TestIpCIDR(t *testing.T) { + tests := map[string]struct { + ipaddr string + netmask string + cidr string + }{ + "blank": { + ipaddr: "", + netmask: "", + cidr: "", + }, + "ip only": { + ipaddr: "192.168.0.1", + netmask: "", + cidr: "", + }, + "netmask only": { + ipaddr: "", + netmask: "255.255.255.0", + cidr: "", + }, + "full": { + ipaddr: "192.168.0.1", + netmask: "255.255.255.0", + cidr: "192.168.0.1/24", + }, + "invalid ip": { + ipaddr: "asdf", + netmask: "255.255.255.0", + cidr: "", + }, + "invalid netmask": { + ipaddr: "192.168.0.1", + netmask: "asdf", + cidr: "", + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + conf := New() + conf.Ipaddr = tt.ipaddr + conf.Netmask = tt.netmask + assert.Equal(t, tt.cidr, conf.IpCIDR()) + }) + } +} + +func TestNetworkCIDR(t *testing.T) { + tests := map[string]struct { + network string + netmask string + cidr string + }{ + "blank": { + network: "", + netmask: "", + cidr: "", + }, + "network only": { + network: "192.168.0.0", + netmask: "", + cidr: "", + }, + "netmask only": { + network: "", + netmask: "255.255.255.0", + cidr: "", + }, + "full": { + network: "192.168.0.0", + netmask: "255.255.255.0", + cidr: "192.168.0.0/24", + }, + "invalid network": { + network: "asdf", + netmask: "255.255.255.0", + cidr: "", + }, + "invalid netmask": { + network: "192.168.0.0", + netmask: "asdf", + cidr: "", + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + conf := New() + conf.Network = tt.network + conf.Netmask = tt.netmask + assert.Equal(t, tt.cidr, conf.NetworkCIDR()) + }) + } +} diff --git a/internal/pkg/config/util.go b/internal/pkg/config/util.go index 1198d46b..9e74d9cd 100644 --- a/internal/pkg/config/util.go +++ b/internal/pkg/config/util.go @@ -1,5 +1,62 @@ package config +import ( + "fmt" + "net" +) + func BoolP(p *bool) bool { return p != nil && *p } + +func GetOutboundIP() net.IP { + conn, err := net.Dial("udp", "192.0.2.1:80") + if err != nil { + return nil + } + defer conn.Close() + localAddr := conn.LocalAddr().(*net.UDPAddr) + return localAddr.IP +} + +func GetIPNetForIP(ip net.IP) (*net.IPNet, error) { + interfaces, err := net.Interfaces() + if err != nil { + return nil, fmt.Errorf("failed to get network interfaces: %v", err) + } + + for _, iface := range interfaces { + // skip interfaces that are down or not relevant + if iface.Flags&net.FlagUp == 0 { + continue + } + + addrs, err := iface.Addrs() + if err != nil { + continue // try next interface + } + + for _, addr := range addrs { + // We expect addr to be of type *net.IPNet. + var ipNet *net.IPNet + switch v := addr.(type) { + case *net.IPNet: + ipNet = v + case *net.IPAddr: + // Wrap net.IPAddr in an IPNet with its default mask. + ipNet = &net.IPNet{IP: v.IP, Mask: v.IP.DefaultMask()} + } + + if ipNet == nil { + continue + } + + // Check if the IP matches (for IPv4, Equal works well) + if ipNet.IP.Equal(ip) { + return ipNet, nil + } + } + } + + return nil, fmt.Errorf("could not find IPNet for IP %v", ip) +} diff --git a/internal/pkg/overlay/datastructure.go b/internal/pkg/overlay/datastructure.go index 00e0a0db..25779c25 100644 --- a/internal/pkg/overlay/datastructure.go +++ b/internal/pkg/overlay/datastructure.go @@ -24,6 +24,7 @@ type TemplateStruct struct { BuildTimeUnix string BuildSource string Ipaddr string + IpCIDR string Ipaddr6 string Netmask string Network string @@ -67,9 +68,11 @@ func InitStruct(overlayName string, nodeData node.Node, allNodes []node.Node) (T tstruct.Paths = *controller.Paths tstruct.Warewulf = *controller.Warewulf tstruct.Ipaddr = controller.Ipaddr + tstruct.IpCIDR = controller.IpCIDR() tstruct.Ipaddr6 = controller.Ipaddr6 tstruct.Netmask = controller.Netmask tstruct.Network = controller.Network + tstruct.NetworkCIDR = controller.NetworkCIDR() // init some convenience vars tstruct.Id = nodeData.Id() tstruct.Hostname = nodeData.Id() diff --git a/internal/pkg/testenv/testenv.go b/internal/pkg/testenv/testenv.go index 8a612c11..d6dca4c2 100644 --- a/internal/pkg/testenv/testenv.go +++ b/internal/pkg/testenv/testenv.go @@ -92,7 +92,7 @@ func (env *TestEnv) init() { func (env *TestEnv) Configure() *config.WarewulfYaml { conf := config.New() - err := conf.Read(env.GetPath(path.Join(Sysconfdir, "warewulf/warewulf.conf"))) + err := conf.Read(env.GetPath(path.Join(Sysconfdir, "warewulf/warewulf.conf")), false) env.assertNoError(err) conf.Paths.Sysconfdir = env.GetPath(Sysconfdir) conf.Paths.Bindir = env.GetPath(Bindir) diff --git a/overlays/debug/internal/debug_test.go b/overlays/debug/internal/debug_test.go index 504b9a9a..16dab229 100644 --- a/overlays/debug/internal/debug_test.go +++ b/overlays/debug/internal/debug_test.go @@ -19,8 +19,10 @@ func Test_debugOverlay(t *testing.T) { env := testenv.New(t) defer env.RemoveAll() + env.ImportFile("etc/warewulf/warewulf.conf", "warewulf.conf") env.ImportFile("etc/warewulf/nodes.conf", "nodes.conf") env.ImportFile("var/lib/warewulf/overlays/debug/rootfs/tstruct.md.ww", "../rootfs/tstruct.md.ww") + env.Configure() tests := []struct { name string @@ -86,11 +88,12 @@ data from other structures. ### Network -- Ipaddr: +- Ipaddr: 192.168.0.1 +- IpCIDR: 192.168.0.1/24 - Ipaddr6: -- Netmask: -- Network: -- NetworkCIDR: +- Netmask: 255.255.255.0 +- Network: 192.168.0.0 +- NetworkCIDR: 192.168.0.0/24 - Ipv6: false ### DHCP diff --git a/overlays/debug/internal/warewulf.conf b/overlays/debug/internal/warewulf.conf new file mode 100644 index 00000000..89c6ce11 --- /dev/null +++ b/overlays/debug/internal/warewulf.conf @@ -0,0 +1 @@ +ipaddr: 192.168.0.1/24 diff --git a/overlays/debug/rootfs/tstruct.md.ww b/overlays/debug/rootfs/tstruct.md.ww index 3684dde1..b70bd838 100644 --- a/overlays/debug/rootfs/tstruct.md.ww +++ b/overlays/debug/rootfs/tstruct.md.ww @@ -29,6 +29,7 @@ data from other structures. ### Network - Ipaddr: {{ .Ipaddr }} +- IpCIDR: {{ .IpCIDR }} - Ipaddr6: {{ .Ipaddr6 }} - Netmask: {{ .Netmask }} - Network: {{ .Network }} diff --git a/overlays/fstab/internal/fstab_test.go b/overlays/fstab/internal/fstab_test.go index 35050e62..dfbaa0b1 100644 --- a/overlays/fstab/internal/fstab_test.go +++ b/overlays/fstab/internal/fstab_test.go @@ -16,7 +16,7 @@ func Test_fstabOverlay(t *testing.T) { defer env.RemoveAll() env.ImportFile("etc/warewulf/nodes.conf", "nodes.conf") env.ImportFile("etc/warewulf/warewulf.conf", "warewulf.conf") - assert.NoError(t, config.Get().Read(env.GetPath("etc/warewulf/warewulf.conf"))) + assert.NoError(t, config.Get().Read(env.GetPath("etc/warewulf/warewulf.conf"), false)) env.ImportFile("var/lib/warewulf/overlays/fstab/rootfs/etc/fstab.ww", "../rootfs/etc/fstab.ww") tests := []struct { diff --git a/overlays/host/internal/host_test.go b/overlays/host/internal/host_test.go index c1c13a83..6556a176 100644 --- a/overlays/host/internal/host_test.go +++ b/overlays/host/internal/host_test.go @@ -86,7 +86,7 @@ func Test_hostOverlay(t *testing.T) { t.Run(tt.name, func(t *testing.T) { if tt.conf != "" { env.ImportFile("etc/warewulf/warewulf.conf", tt.conf) - assert.NoError(t, config.Get().Read(env.GetPath("etc/warewulf/warewulf.conf"))) + assert.NoError(t, config.Get().Read(env.GetPath("etc/warewulf/warewulf.conf"), false)) } cmd := show.GetCommand() cmd.SetArgs(tt.args) @@ -152,7 +152,6 @@ option architecture-type code 93 = unsigned integer 16; if exists user-class and option user-class = "iPXE" { filename "http://192.168.0.1:9873/ipxe/${mac:hexhyp}?assetkey=${asset}&uuid=${uuid}"; } else { - if option architecture-type = 00:00 { filename "/warewulf/undionly.kpxe"; } @@ -166,7 +165,6 @@ if exists user-class and option user-class = "iPXE" { filename "/warewulf/snponly.efi"; } } - subnet 192.168.0.0 netmask 255.255.255.0 { max-lease-time 120; range 192.168.0.100 192.168.0.199; @@ -201,7 +199,6 @@ option architecture-type code 93 = unsigned integer 16; if exists user-class and option user-class = "iPXE" { filename "http://192.168.0.1:9873/ipxe/${mac:hexhyp}?assetkey=${asset}&uuid=${uuid}"; } else { - if option architecture-type = 00:00 { filename "/warewulf/undionly.kpxe"; } @@ -215,7 +212,6 @@ if exists user-class and option user-class = "iPXE" { filename "/warewulf/snponly.efi"; } } - subnet 192.168.0.0 netmask 255.255.255.0 { max-lease-time 120; range 192.168.0.100 192.168.0.199; @@ -278,7 +274,6 @@ dhcp-host=e6:92:39:49:7b:04,set:warewulf,node2,192.168.3.23,infinite const host_exports string = `backupFile: true writeFile: true Filename: etc/exports - # This file is autogenerated by warewulf /home 192.168.0.0/255.255.255.0(rw,sync) /opt 192.168.0.0/255.255.255.0(ro,sync,no_root_squash) diff --git a/overlays/host/rootfs/etc/dhcp/dhcpd.conf.ww b/overlays/host/rootfs/etc/dhcp/dhcpd.conf.ww index 8c611416..d9cd43b2 100644 --- a/overlays/host/rootfs/etc/dhcp/dhcpd.conf.ww +++ b/overlays/host/rootfs/etc/dhcp/dhcpd.conf.ww @@ -1,4 +1,4 @@ -{{if $.Dhcp.Enabled -}} +{{ if $.Dhcp.Enabled -}} # This file is autogenerated by warewulf allow booting; @@ -38,19 +38,21 @@ if substring (option vendor-class-identifier, 0, 9) = "PXEClient" { if exists user-class and option user-class = "iPXE" { filename "http://{{$.Ipaddr}}:{{$.Warewulf.Port}}/ipxe/${mac:hexhyp}?assetkey=${asset}&uuid=${uuid}"; } else { -{{range $type,$name := $.Tftp.IpxeBinaries }} +{{- range $type, $name := $.Tftp.IpxeBinaries }} if option architecture-type = {{ $type }} { filename "/warewulf/{{ basename $name }}"; } -{{- end }}{{/* range IpxeBinaries */}} +{{- end }} } -{{- end }}{{/* BootMethod */}} +{{- end }} -subnet {{$.Network}} netmask {{$.Netmask}} { +{{- if and .Network .Netmask .Dhcp.RangeStart .Dhcp.RangeEnd }} +subnet {{.Network}} netmask {{.Netmask}} { max-lease-time 120; - range {{$.Dhcp.RangeStart}} {{$.Dhcp.RangeEnd}}; + range {{.Dhcp.RangeStart}} {{.Dhcp.RangeEnd}}; next-server {{.Ipaddr}}; } +{{- end }} {{- if eq .Dhcp.Template "static" }} {{- range $nodes := $.AllNodes}} diff --git a/overlays/host/rootfs/etc/exports.ww b/overlays/host/rootfs/etc/exports.ww index 7507c37f..b8c44fff 100644 --- a/overlays/host/rootfs/etc/exports.ww +++ b/overlays/host/rootfs/etc/exports.ww @@ -1,4 +1,4 @@ -{{- if .Nfs.Enabled }} +{{ if .Nfs.Enabled -}} # This file is autogenerated by warewulf {{- $network := .Network }} {{- $netmask := .Netmask }} diff --git a/overlays/hosts/internal/hosts_test.go b/overlays/hosts/internal/hosts_test.go index bf0eb0d2..da26cd68 100644 --- a/overlays/hosts/internal/hosts_test.go +++ b/overlays/hosts/internal/hosts_test.go @@ -19,7 +19,7 @@ func Test_hostsOverlay(t *testing.T) { env := testenv.New(t) defer env.RemoveAll() env.ImportFile("etc/warewulf/warewulf.conf", "warewulf.conf") - assert.NoError(t, config.Get().Read(env.GetPath("etc/warewulf/warewulf.conf"))) + assert.NoError(t, config.Get().Read(env.GetPath("etc/warewulf/warewulf.conf"), false)) env.ImportFile("etc/warewulf/nodes.conf", "nodes.conf") env.ImportFile("var/lib/warewulf/overlays/hosts/rootfs/etc/hosts.ww", "../rootfs/etc/hosts.ww") diff --git a/overlays/localtime/internal/localtime_test.go b/overlays/localtime/internal/localtime_test.go index 30407248..28c247c2 100644 --- a/overlays/localtime/internal/localtime_test.go +++ b/overlays/localtime/internal/localtime_test.go @@ -15,7 +15,7 @@ func Test_localtimeOverlay(t *testing.T) { env := testenv.New(t) defer env.RemoveAll() env.ImportFile("etc/warewulf/nodes.conf", "nodes.conf") - assert.NoError(t, config.Get().Read(env.GetPath("etc/warewulf/warewulf.conf"))) + assert.NoError(t, config.Get().Read(env.GetPath("etc/warewulf/warewulf.conf"), false)) env.ImportFile("var/lib/warewulf/overlays/localtime/rootfs/etc/localtime.ww", "../rootfs/etc/localtime.ww") tests := []struct {