diff --git a/README.md b/README.md index 8f66b7c..62ec125 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ - [CoreDHCP](#coredhcp) - [CoreDNS](#coredns) - [Overview](#overview) + - [Configuration](#configuration) - [Build and Install](#build-and-install) - [Build/Install with GoReleaser](#buildinstall-with-goreleaser) - [Environment Variables](#environment-variables) @@ -18,7 +19,7 @@ - [CoreDHCP](#coredhcp-1) - [CoreDNS](#coredns-1) - [Running](#running) - - [Configuration](#configuration) + - [Configuration](#configuration-1) - [Preparation: SMD and BSS](#preparation-smd-and-bss) - [Preparation: TFTP](#preparation-tftp) - [Running](#running-1) @@ -62,6 +63,10 @@ CoreSMD acts as a pull-through cache of DHCP and DNS information from SMD, ensur --- +## Configuration + +Take a look at [**examples/**](examples/). In there are configuration examples and documentation for both CoreDHCP and CoreDNS. + ## Build and Install The plugins in this repository can be built into CoreDHCP/CoreDNS either using a container-based approach (via the provided Dockerfile) or by statically compiling them into CoreDHCP/CoreDNS on bare metal. Additionally, this project uses [GoReleaser](https://goreleaser.com/) to automate releases and include build metadata. diff --git a/examples/coredhcp/README.md b/examples/coredhcp/README.md index af7c4d7..46f5b33 100644 --- a/examples/coredhcp/README.md +++ b/examples/coredhcp/README.md @@ -2,6 +2,13 @@ This directory contains example CoreDHCP configurations for the CoreSMD plugin. +## Contents + +- [CoreDHCP Configuration Examples for CoreSMD](#coredhcp-configuration-examples-for-coresmd) + - [Contents](#contents) + - [Positional vs. Key-Value Format](#positional-vs-key-value-format) + - [Custom Hostnames](#custom-hostnames) + ## Positional vs. Key-Value Format Prior to CoreSMD v0.5.0, positional arguments were used to configure CoreSMD which made it difficult to match configuration values to configuration keys. An example of such configuration would be: @@ -33,3 +40,7 @@ plugins: ``` See [coredhcp.yaml](coredhcp.yaml) for a full example with documentation comments. + +## Custom Hostnames + +Hostname patterns can be used to specify custom hostnames for nodes and BMCs. See [**hostnames.md**](hostnames.md) for more details. diff --git a/examples/coredhcp/coredhcp.yaml b/examples/coredhcp/coredhcp.yaml index 8cb9edc..7994fe0 100644 --- a/examples/coredhcp/coredhcp.yaml +++ b/examples/coredhcp/coredhcp.yaml @@ -38,6 +38,43 @@ server4: # The format of configuration options below is "key=value", with no spaces # on either side of the equal sign. Quotes around values are optional. # + # CUSTOM HOSTNAMES + # + # For node_pattern and bmc_pattern (see CONFIGURATION KEYS below), custom + # hostname patterns can be specified to dynamically generate custom + # hostnames for BMCs and nodes in SMD. + # + # Hostname Placeholder Syntax: + # + # {Nd} - Zero-padded NID where N is the number of digits + # Examples: {04d} -> 0001, 0042, 1234 + # {02d} -> 01, 42, 99 + # {05d} -> 00001, 00042, 12345 + # {id} - Component xname (e.g., x3000c0s0b0n0 for nodes, x3000c0s0b1 for + # BMCs) + # + # Literal text can be mixed with placeholders: + # + # "compute-{04d}" -> compute-0001, compute-0042 + # "node-{id}" -> node-x3000c0s0b0n0 + # "dev-s{02d}" -> dev-s01, dev-s42 + # + # CUSTOM DOMAINS: + # + # If 'domain' is specified (see CONFIGURATION KEYS below), a custom domain + # will be appended to hostnames generated using the custom hostname + # mechanism above, making generated hostnames fully-qualified. + # + # For instance, for the following configuration: + # + # - coresmd: ... node_pattern="{id}" bmc_pattern="{id}" domain=cluster.local + # + # {id} will be expanded to the node/BMC's xname and the domain cluster.local + # will be appended. For example: + # + # BMC hostname: x3000c0s0b1.cluster.local + # Node hostname: x3000c0s0b1n0.cluster.local + # # CONFIGURATION KEYS # # svc_base_uri (REQUIRED, string) @@ -85,6 +122,19 @@ server4: # tftp_port (OPTIONAL, integer, default=69) # The transport layer network port to bind to. If omitted, the default # value will be used. + # + # node_pattern (OPTIONAL, string, default=id{04d}) + # The hostname pattern to use to generate hostnames for nodes in SMD. See + # CUSTOM HOSTNAMES above for details. + # + # bmc_pattern (OPTIONAL, string, default=bmc{04d}) + # The hostname pattern to use to generate hostnames for BMCs in SMD. See + # CUSTOM HOSTNAMES above for details. + # + # domain (OPTIONAL, string) + # An optional domain to append to hostnames generated with node_pattern + # and bmc_pattern. If omitted, no domain will be appended. See CUSTOM + # DOMAINS above for details. - coresmd: | svc_base_uri=https://foobar.openchami.cluster ipxe_base_uri=http://172.16.0.253:8081 @@ -94,6 +144,9 @@ server4: single_port=false tftp_dir=/tftpboot tftp_port=69 + node_pattern=nid{04d} + bmc_pattern=bmc{04d} + domain=openchami.cluster # Any requests reaching this point are unknown to SMD and it is up to the # administrator to decide how to handle unknown packets. diff --git a/examples/coredhcp/hostnames.md b/examples/coredhcp/hostnames.md new file mode 100644 index 0000000..4ee0bc3 --- /dev/null +++ b/examples/coredhcp/hostnames.md @@ -0,0 +1,282 @@ +# CoreSMD Hostname Pattern Examples + +This document provides detailed examples of hostname pattern configuration for the CoreSMD CoreDHCP plugin. + +## Overview + +The CoreSMD plugin generates DHCP hostnames for compute nodes and BMCs using configurable patterns. This allows you to customize hostname formats to match your site's naming conventions. + +## Pattern Syntax + +### `{Nd}` - Zero-Padded NID + +Generates a zero-padded Node ID where `N` is the number of digits: + +| Pattern | NID | Result | +|---------|-----|--------| +| `{02d}` | 1 | `01` | +| `{02d}` | 42 | `42` | +| `{03d}` | 1 | `001` | +| `{03d}` | 42 | `042` | +| `{04d}` | 1 | `0001` | +| `{04d}` | 42 | `0042` | +| `{05d}` | 1 | `00001`| +| `{05d}` | 123 | `00123`| + +### `{id}` - Component Xname + +Uses the full component identifier from SMD: +- Compute nodes: `x3000c0s0b0n0`, `x3000c0s1b0n1`, etc. +- BMCs: `x3000c0s0b1`, `x3000c0s1b1`, etc. + +## Complete Examples + +### Example 1: Default Configuration + +**Configuration:** +```yaml +- coresmd: | + svc_base_uri=https://smd.cluster.local + ipxe_base_uri=http://192.168.1.1 + cache_valid=30s + lease_time=24h +``` + +**Result:** +- Nodes: `nid0001`, `nid0002`, `nid0123`, `nid1234` +- BMCs: No hostname assigned + +**Use case:** Standard HPC cluster with traditional NID-based naming + +--- + +### Example 2: Data Center with Descriptive Names + +**Configuration:** +```yaml +- coresmd: | + svc_base_uri=https://smd.datacenter.com + ipxe_base_uri=http://10.0.0.1 + cache_valid=30s + lease_time=24h + node_pattern="compute-{05d}" + bmc_pattern="bmc-{05d}" + domain=datacenter.com +``` + +**Result:** +- Nodes: `compute-00001.datacenter.com`, `compute-00042.datacenter.com` +- BMCs: `bmc-00001.datacenter.com`, `bmc-00042.datacenter.com` + +**Use case:** Enterprise data center with descriptive hostnames and FQDN requirements + +--- + +### Example 3: LANL Development Cluster + +**Configuration:** +```yaml +- coresmd: | + svc_base_uri=https://smd.dev-osc.lanl.gov + ipxe_base_uri=http://172.16.0.253:8081 + ca_cert=/etc/ssl/certs/lanl-ca.crt + cache_valid=30s + lease_time=24h + node_pattern="dev-s{02d}" + bmc_pattern="bmc{03d}" + domain=dev-osc.lanl.gov +``` + +**Result:** +- Nodes: `dev-s01.dev-osc.lanl.gov`, `dev-s42.dev-osc.lanl.gov` +- BMCs: `bmc001.dev-osc.lanl.gov`, `bmc042.dev-osc.lanl.gov` + +**Use case:** Site-specific naming convention with short node IDs and institutional domain + +--- + +### Example 4: Research Cluster with Project Prefix + +**Configuration:** +```yaml +- coresmd: | + svc_base_uri=https://smd.research.edu + ipxe_base_uri=http://10.1.0.1 + cache_valid=30s + lease_time=24h + node_pattern="astro-node{04d}" + bmc_pattern="astro-bmc{04d}" + domain=astro.research.edu +``` + +**Result:** +- Nodes: `astro-node0001.astro.research.edu`, `astro-node0042.astro.research.edu` +- BMCs: `astro-bmc0001.astro.research.edu`, `astro-bmc0042.astro.research.edu` + +**Use case:** Multi-tenant research facility with project-specific naming + +--- + +### Example 5: Kubernetes Cluster + +**Configuration:** +```yaml +- coresmd: | + svc_base_uri=https://smd.k8s.local + ipxe_base_uri=http://192.168.100.1 + cache_valid=30s + lease_time=1h + node_pattern="worker{03d}" + domain=k8s.local +``` + +**Result:** +- Nodes: `worker001.k8s.local`, `worker042.k8s.local` +- BMCs: No hostname assigned + +**Use case:** Kubernetes worker nodes with short lease times for dynamic environments + +--- + +### Example 6: Xname-based Tracking + +**Configuration:** +```yaml +- coresmd: | + svc_base_uri=https://smd.cluster.local + ipxe_base_uri=http://192.168.1.1 + cache_valid=30s + lease_time=24h + node_pattern="{id}" + bmc_pattern="{id}" + domain=cluster.local +``` + +**Result:** +- Nodes: `x3000c0s0b0n0.cluster.local`, `x3000c0s1b0n1.cluster.local` +- BMCs: `x3000c0s0b1.cluster.local`, `x3000c0s1b1.cluster.local` + +**Use case:** Precise hardware tracking using SMD's native xname identifiers + +--- + +### Example 7: Mixed Pattern (Advanced) + +**Configuration:** +```yaml +- coresmd: | + svc_base_uri=https://smd.cluster.local + ipxe_base_uri=http://192.168.1.1 + cache_valid=30s + lease_time=24h + node_pattern="rack42-{id}" + bmc_pattern="mgmt-{03d}" + domain=lab.local +``` + +**Result:** +- Nodes: `rack42-x3000c0s0b0n0.lab.local`, `rack42-x3000c0s1b0n1.lab.local` +- BMCs: `mgmt-001.lab.local`, `mgmt-042.lab.local` + +**Use case:** Combining rack information with xnames for nodes, simple numbering for BMCs + +--- + +### Example 8: No Domain Suffix + +**Configuration:** +```yaml +- coresmd: | + svc_base_uri=https://smd.cluster.local + ipxe_base_uri=http://192.168.1.1 + cache_valid=30s + lease_time=24h + node_pattern="cn{04d}" + bmc_pattern="ipmi{04d}" +``` + +**Result:** +- Nodes: `cn0001`, `cn0042` +- BMCs: `ipmi0001`, `ipmi0042` + +**Use case:** Flat namespace without domain suffixes + +--- + +## Common Patterns by Site Type + +### Traditional HPC Center +```yaml +node_pattern="nid{04d}" +bmc_pattern="" +domain="" +``` +Result: `nid0001`, `nid0002`, etc. + +### Modern Data Center +```yaml +node_pattern="compute-{05d}" +bmc_pattern="bmc-{05d}" +domain="dc.example.com" +``` +Result: `compute-00001.dc.example.com`, `bmc-00001.dc.example.com` + +### Cloud/Kubernetes +```yaml +node_pattern="worker{03d}" +bmc_pattern="" +domain="k8s.local" +``` +Result: `worker001.k8s.local`, `worker042.k8s.local` + +### Research Lab +```yaml +node_pattern="{id}" +bmc_pattern="{id}" +domain="lab.university.edu" +``` +Result: `x3000c0s0b0n0.lab.university.edu` + +## Validation and Testing + +After configuration, verify hostname generation: + +1. Start CoreDHCP with your config +2. Check logs for hostname configuration: + ``` + coresmd plugin initialized with ... bmc_pattern=bmc{03d} node_pattern=n{02d} domain=openchami.cluster + ``` +3. Request DHCP lease from a node +4. Check logs for assignment: + ``` + assigning IP 172.16.0.1 and hostname re01 to de:ad:be:ee:ef:01 (Node) with a lease duration of 1h0m0s + ``` +5. Verify assigned hostname in DHCP response (option 12) + +## Troubleshooting + +### No Hostname Assigned + +**Problem:** Nodes/BMCs not receiving hostnames + +**Solution:** +- For nodes: Check that `node_pattern` is set (default: `nid{04d}`) +- For BMCs: Check that `bmc_pattern` is set (default: empty, no hostnames) +- Verify component type in SMD matches expectations + +### Invalid Pattern Format + +**Problem:** Error message about invalid pattern + +**Solution:** +- Ensure patterns use `{Nd}` format where N is a digit (e.g., `{04d}`, not `{4d}`) +- Quote patterns with special characters in YAML: `node_pattern="my-node-{04d}"` + +### Wrong Hostname Format + +**Problem:** Hostnames don't match expected format + +**Solution:** +- Review pattern syntax - ensure `{04d}` has two digits specifying padding +- Check for typos in pattern or domain parameters +- Verify NID values in SMD are correct diff --git a/internal/hostname/hostname.go b/internal/hostname/hostname.go new file mode 100644 index 0000000..40d7e79 --- /dev/null +++ b/internal/hostname/hostname.go @@ -0,0 +1,25 @@ +package hostname + +import ( + "fmt" + "regexp" + "strconv" + "strings" +) + +// ExpandHostnamePattern replaces {Nd} with zero-padded NID and {id} with xname +// Example patterns: +// - "nid{04d}" with NID=1 => "nid0001" +// - "dev-s{02d}" with NID=5 => "dev-s05" +// - "bmc{03d}" with NID=42 => "bmc042" +// - "{id}" with xname="x3000c0s0b1" => "x3000c0s0b1" +func ExpandHostnamePattern(pattern string, nid int64, id string) string { + out := strings.ReplaceAll(pattern, "{id}", id) + re := regexp.MustCompile(`\{0*(\d+)d\}`) + out = re.ReplaceAllStringFunc(out, func(m string) string { + nStr := re.FindStringSubmatch(m)[1] + n, _ := strconv.Atoi(nStr) + return fmt.Sprintf("%0*d", n, nid) + }) + return out +} diff --git a/internal/hostname/hostname_test.go b/internal/hostname/hostname_test.go new file mode 100644 index 0000000..0d082c6 --- /dev/null +++ b/internal/hostname/hostname_test.go @@ -0,0 +1,113 @@ +package hostname + +import "testing" + +func TestExpandHostnamePattern(t *testing.T) { + tests := []struct { + name string + pattern string + nid int64 + id string + want string + }{ + { + name: "simple_nid_4_digits_zero_padded", + pattern: "nid{04d}", + nid: 1, + id: "", + want: "nid0001", + }, + { + name: "simple_nid_2_digits_zero_padded", + pattern: "dev-s{02d}", + nid: 5, + id: "", + want: "dev-s05", + }, + { + name: "simple_nid_3_digits_zero_padded", + pattern: "bmc{03d}", + nid: 42, + id: "", + want: "bmc042", + }, + { + name: "id_only_pattern", + pattern: "{id}", + nid: 0, + id: "x3000c0s0b1", + want: "x3000c0s0b1", + }, + { + name: "id_embedded_in_pattern", + pattern: "node-{id}-svc", + nid: 0, + id: "x3000c0s0b1", + want: "node-x3000c0s0b1-svc", + }, + { + name: "nid_and_id_mixed", + pattern: "nid{03d}-{id}", + nid: 7, + id: "x1000c0s0b0", + want: "nid007-x1000c0s0b0", + }, + { + name: "multiple_nid_patterns_with_same_value", + pattern: "rack{02d}-node{03d}", + nid: 7, + id: "", + want: "rack07-node007", + }, + { + name: "pattern_without_any_placeholders", + pattern: "static-hostname", + nid: 123, + id: "ignored", + want: "static-hostname", + }, + { + name: "numeric_pattern_without_leading_zero_in_format", + pattern: "node{4d}", + nid: 7, + id: "", + want: "node0007", + }, + { + name: "large_nid_with_smaller_width_truncation_not_expected", + pattern: "nid{02d}", + nid: 123, + id: "", + // fmt with %0*d will not truncate, just print width>=2; + // so we expect "123" + want: "nid123", + }, + { + name: "zero_nid_with_padding", + pattern: "nid{03d}", + nid: 0, + id: "", + want: "nid000", + }, + { + name: "negative_nid_with_padding", + pattern: "nid{04d}", + nid: -1, + id: "", + // fmt keeps the sign, width includes '-' + // width 4 => "-001" + want: "nid-001", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + got := ExpandHostnamePattern(tt.pattern, tt.nid, tt.id) + if got != tt.want { + t.Errorf("ExpandHostnamePattern(%q, %d, %q) = %q, want %q", + tt.pattern, tt.nid, tt.id, got, tt.want) + } + }) + } +} diff --git a/plugin/coredhcp/coresmd/main.go b/plugin/coredhcp/coresmd/main.go index 8a394f1..e525874 100644 --- a/plugin/coredhcp/coresmd/main.go +++ b/plugin/coredhcp/coresmd/main.go @@ -15,6 +15,7 @@ import ( "github.com/insomniacslk/dhcp/dhcpv4" "github.com/openchami/coresmd/internal/debug" + "github.com/openchami/coresmd/internal/hostname" "github.com/openchami/coresmd/internal/ipxe" "github.com/openchami/coresmd/internal/version" ) @@ -37,10 +38,13 @@ type Config struct { singlePort bool // single_port tftpDir string // tftp_dir tftpPort int // tftp_port + bmcPattern string // bmc_pattern + nodePattern string // node_pattern + domain string // domain } func (c Config) String() string { - return fmt.Sprintf("svc_base_uri=%s ipxe_base_uri=%s ca_cert=%s cache_valid=%s lease_time=%s single_port=%v tftp_dir=%s tftp_port=%d", + return fmt.Sprintf("svc_base_uri=%s ipxe_base_uri=%s ca_cert=%s cache_valid=%s lease_time=%s single_port=%v tftp_dir=%s tftp_port=%d bmc_pattern=%s node_pattern=%s domain=%s", c.svcBaseURI, c.ipxeBaseURI, c.caCert, @@ -49,6 +53,9 @@ func (c Config) String() string { c.singlePort, c.tftpDir, c.tftpPort, + c.bmcPattern, + c.nodePattern, + c.domain, ) } @@ -57,6 +64,8 @@ const ( defaultTFTPPort = 69 defaultCacheValid = "30s" defaultLeaseTime = "1h0m0s" + defaultBMCPattern = "bmc{04d}" + defaultNodePattern = "nid{04d}" ) var ( @@ -206,6 +215,21 @@ func parseConfig(argv ...string) (cfg Config, errs []error) { cfg.tftpPort = defaultTFTPPort } } + case "bmc_pattern": + bmcPattern := strings.Trim(opt[1], `'"`) + if bmcPattern != "" { + cfg.bmcPattern = bmcPattern + } + case "node_pattern": + nodePattern := strings.Trim(opt[1], `"'`) + if nodePattern != "" { + cfg.nodePattern = nodePattern + } + case "domain": + domain := strings.Trim(opt[1], `"'`) + if domain != "" { + cfg.domain = domain + } default: errs = append(errs, fmt.Errorf("arg %d: unknown config key '%s' (skipping)", idx, opt[0])) continue @@ -257,6 +281,17 @@ func (c *Config) validate() (warns []string, errs []error) { warns = append(warns, fmt.Sprintf("tftp_dir unset, defaulting to %s", defaultTFTPDirectory)) c.tftpDir = defaultTFTPDirectory } + if c.bmcPattern == "" { + warns = append(warns, fmt.Sprintf("bmc_pattern unset, defaulting to %s", defaultBMCPattern)) + c.bmcPattern = defaultBMCPattern + } + if c.nodePattern == "" { + warns = append(warns, fmt.Sprintf("node_pattern unset, defaulting to %s", defaultNodePattern)) + c.nodePattern = defaultNodePattern + } + if c.domain == "" { + warns = append(warns, "domain unset, not configuring") + } return } @@ -284,13 +319,30 @@ func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) { } else { resp.Options.Update(dhcpv4.OptIPAddressLeaseTime(*globalConfig.leaseTime)) } - log.Infof("assigning %s to %s (%s) with a lease duration of %s", assignedIP, ifaceInfo.MAC, ifaceInfo.Type, globalConfig.leaseTime) // Set client hostname + hname := "(none)" if ifaceInfo.Type == "Node" { - resp.Options.Update(dhcpv4.OptHostName(fmt.Sprintf("nid%04d", ifaceInfo.CompNID))) + nodeHostname := hostname.ExpandHostnamePattern(globalConfig.nodePattern, ifaceInfo.CompNID, ifaceInfo.CompID) + if globalConfig.domain != "" { + nodeHostname = nodeHostname + "." + globalConfig.domain + } + hname = nodeHostname + resp.Options.Update(dhcpv4.OptHostName(nodeHostname)) + log.Debugf("setting hostname for node %s to %s", ifaceInfo.CompID, nodeHostname) + } else if ifaceInfo.Type == "NodeBMC" { + bmcHostname := hostname.ExpandHostnamePattern(globalConfig.bmcPattern, ifaceInfo.CompNID, ifaceInfo.CompID) + if globalConfig.domain != "" { + bmcHostname = bmcHostname + "." + globalConfig.domain + } + hname = bmcHostname + resp.Options.Update(dhcpv4.OptHostName(bmcHostname)) + log.Debugf("setting hostname for BMC %s to %s", ifaceInfo.CompID, bmcHostname) } + // Log assignment + log.Infof("assigning IP %s and hostname %s to %s (%s) with a lease duration of %s", assignedIP, hname, ifaceInfo.MAC, ifaceInfo.Type, globalConfig.leaseTime) + // Set root path to this server's IP resp.Options.Update(dhcpv4.OptRootPath(resp.ServerIPAddr.String())) diff --git a/plugin/coredhcp/coresmd/main_test.go b/plugin/coredhcp/coresmd/main_test.go index 6c1d68c..008f994 100644 --- a/plugin/coredhcp/coresmd/main_test.go +++ b/plugin/coredhcp/coresmd/main_test.go @@ -23,6 +23,9 @@ func TestConfigString(t *testing.T) { singlePort: true, tftpDir: "/tftp", tftpPort: 1069, + bmcPattern: "bmc{04d}", + nodePattern: "nid{04d}", + domain: "example.test", } s := cfg.String() @@ -37,6 +40,9 @@ func TestConfigString(t *testing.T) { "single_port=true", "tftp_dir=/tftp", "tftp_port=1069", + "bmc_pattern=bmc{04d}", + "node_pattern=nid{04d}", + "domain=example.test", } for _, sub := range wantSubstrings { @@ -68,6 +74,9 @@ func TestParseConfig_Table(t *testing.T) { "single_port=true", "tftp_dir=/tftp", "tftp_port=1069", + "bmc_pattern=bmc{03d}", + "node_pattern=nid{03d}", + "domain=cluster.local", }, wantCfg: func() Config { svc, _ := url.Parse("https://svc.example.test") @@ -81,6 +90,9 @@ func TestParseConfig_Table(t *testing.T) { singlePort: true, tftpDir: "/tftp", tftpPort: 1069, + bmcPattern: "bmc{03d}", + nodePattern: "nid{03d}", + domain: "cluster.local", } }, wantErrsMin: 0, @@ -165,13 +177,19 @@ func TestParseConfig_Table(t *testing.T) { wantErrsMin: 1, }, { - name: "tftp_dir trims quotes", + name: "tftp_dir_and_patterns_and_domain_trim_quotes", args: []string{ `tftp_dir="/quoted/path"`, + `bmc_pattern="bmc{04d}"`, + `node_pattern='nid{04d}'`, + `domain="example.test"`, }, wantCfg: func() Config { return Config{ - tftpDir: "/quoted/path", + tftpDir: "/quoted/path", + bmcPattern: "bmc{04d}", + nodePattern: "nid{04d}", + domain: "example.test", } }, wantErrsMin: 0, @@ -220,6 +238,15 @@ func TestParseConfig_Table(t *testing.T) { if gotCfg.tftpPort != wantCfg.tftpPort { t.Errorf("tftpPort = %d, want %d", gotCfg.tftpPort, wantCfg.tftpPort) } + if wantCfg.bmcPattern != "" && gotCfg.bmcPattern != wantCfg.bmcPattern { + t.Errorf("bmcPattern = %q, want %q", gotCfg.bmcPattern, wantCfg.bmcPattern) + } + if wantCfg.nodePattern != "" && gotCfg.nodePattern != wantCfg.nodePattern { + t.Errorf("nodePattern = %q, want %q", gotCfg.nodePattern, wantCfg.nodePattern) + } + if wantCfg.domain != "" && gotCfg.domain != wantCfg.domain { + t.Errorf("domain = %q, want %q", gotCfg.domain, wantCfg.domain) + } }) } } @@ -239,7 +266,7 @@ func TestConfigValidate_Table(t *testing.T) { { name: "missing required URIs", cfg: Config{}, - wantWarnMin: 1, // ca_cert / cache_valid / lease_time / tftp_* will warn + wantWarnMin: 1, // many warnings expected; just ensure at least one wantErrMin: 2, // svc_base_uri and ipxe_base_uri required check: func(t *testing.T, cfg Config) {}, }, @@ -251,7 +278,7 @@ func TestConfigValidate_Table(t *testing.T) { }, // Exact number of warnings depends on combinations; we only care that // defaults are applied and there are *some* warnings. - wantWarnMin: 3, + wantWarnMin: 5, wantErrMin: 0, check: func(t *testing.T, cfg Config) { if cfg.cacheValid == nil || cfg.cacheValid.String() != defaultCacheValid { @@ -266,6 +293,15 @@ func TestConfigValidate_Table(t *testing.T) { if cfg.tftpDir != defaultTFTPDirectory { t.Errorf("tftpDir = %q, want %q", cfg.tftpDir, defaultTFTPDirectory) } + if cfg.bmcPattern != defaultBMCPattern { + t.Errorf("bmcPattern = %q, want %q", cfg.bmcPattern, defaultBMCPattern) + } + if cfg.nodePattern != defaultNodePattern { + t.Errorf("nodePattern = %q, want %q", cfg.nodePattern, defaultNodePattern) + } + if cfg.domain != "" { + t.Errorf("domain = %q, want empty (unset)", cfg.domain) + } }, }, { @@ -281,6 +317,35 @@ func TestConfigValidate_Table(t *testing.T) { if cfg.tftpPort != defaultTFTPPort { t.Errorf("tftpPort = %d, want %d", cfg.tftpPort, defaultTFTPPort) } + if cfg.bmcPattern != defaultBMCPattern { + t.Errorf("bmcPattern = %q, want %q", cfg.bmcPattern, defaultBMCPattern) + } + if cfg.nodePattern != defaultNodePattern { + t.Errorf("nodePattern = %q, want %q", cfg.nodePattern, defaultNodePattern) + } + }, + }, + { + name: "patterns_and_domain_already_set_no_pattern_defaults", + cfg: Config{ + svcBaseURI: svc, + ipxeBaseURI: ipxe, + bmcPattern: "bmc{03d}", + nodePattern: "nid{03d}", + domain: "example.test", + }, + wantWarnMin: 3, // ca_cert, cache_valid, lease_time at least + wantErrMin: 0, + check: func(t *testing.T, cfg Config) { + if cfg.bmcPattern != "bmc{03d}" { + t.Errorf("bmcPattern = %q, want %q", cfg.bmcPattern, "bmc{03d}") + } + if cfg.nodePattern != "nid{03d}" { + t.Errorf("nodePattern = %q, want %q", cfg.nodePattern, "nid{03d}") + } + if cfg.domain != "example.test" { + t.Errorf("domain = %q, want %q", cfg.domain, "example.test") + } }, }, }