Merge pull request #2014 from mslacken/showTags
show used Tags for overlays
This commit is contained in:
@@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
- `wwclient.aarch64` overlay always provides an aarch64 wwclient executable.
|
- `wwclient.aarch64` overlay always provides an aarch64 wwclient executable.
|
||||||
- `wwclient.x86_64` overlay always provides an x86_64 wwclient executable.
|
- `wwclient.x86_64` overlay always provides an x86_64 wwclient executable.
|
||||||
- systemd-networkd overlay with IPv6 support
|
- systemd-networkd overlay with IPv6 support
|
||||||
|
- `wwctl overlay info` lists the variables used by an overlay template
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -36,6 +36,7 @@ require (
|
|||||||
github.com/swaggest/usecase v1.3.1
|
github.com/swaggest/usecase v1.3.1
|
||||||
github.com/talos-systems/go-smbios v0.1.1
|
github.com/talos-systems/go-smbios v0.1.1
|
||||||
golang.org/x/crypto v0.32.0
|
golang.org/x/crypto v0.32.0
|
||||||
|
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67
|
||||||
golang.org/x/sys v0.29.0
|
golang.org/x/sys v0.29.0
|
||||||
golang.org/x/term v0.28.0
|
golang.org/x/term v0.28.0
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489
|
google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489
|
||||||
@@ -149,7 +150,6 @@ require (
|
|||||||
go.opentelemetry.io/otel v1.32.0 // indirect
|
go.opentelemetry.io/otel v1.32.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.32.0 // indirect
|
go.opentelemetry.io/otel/metric v1.32.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect
|
|
||||||
golang.org/x/net v0.33.0 // indirect
|
golang.org/x/net v0.33.0 // indirect
|
||||||
golang.org/x/sync v0.11.0 // indirect
|
golang.org/x/sync v0.11.0 // indirect
|
||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func CobraRunE(cmd *cobra.Command, args []string) error {
|
|||||||
defer os.Remove(tempFile.Name())
|
defer os.Remove(tempFile.Name())
|
||||||
|
|
||||||
if !NoHeader {
|
if !NoHeader {
|
||||||
yamlTemplate := node.UnmarshalConf(node.Node{}, nil)
|
yamlTemplate := node.ConfToYaml(node.Node{}, nil)
|
||||||
if _, err := tempFile.WriteString("#nodename:\n# " + strings.Join(yamlTemplate, "\n# ") + "\n"); err != nil {
|
if _, err := tempFile.WriteString("#nodename:\n# " + strings.Join(yamlTemplate, "\n# ") + "\n"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
99
internal/app/wwctl/overlay/info/main.go
Normal file
99
internal/app/wwctl/overlay/info/main.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package info
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/warewulf/warewulf/internal/app/wwctl/table"
|
||||||
|
"github.com/warewulf/warewulf/internal/pkg/overlay"
|
||||||
|
"github.com/warewulf/warewulf/internal/pkg/wwlog"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CobraRunE(cmd *cobra.Command, args []string) error {
|
||||||
|
overlayName := args[0]
|
||||||
|
filePath := args[1]
|
||||||
|
|
||||||
|
ov, err := overlay.Get(overlayName)
|
||||||
|
if err != nil {
|
||||||
|
wwlog.Error("Failed to get overlay %s: %s", overlayName, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use new type-based variable resolution
|
||||||
|
varFields := ov.ParseVarFields(filePath)
|
||||||
|
if varFields == nil {
|
||||||
|
return fmt.Errorf("could not parse variables for %s in overlay %s", filePath, overlayName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Still parse comment vars for wwdoc and inline documentation
|
||||||
|
commentMap := ov.ParseCommentVars(filePath)
|
||||||
|
commentKeys := maps.Keys(commentMap)
|
||||||
|
sort.Strings(commentKeys)
|
||||||
|
hasWwdoc := false
|
||||||
|
for _, docLn := range commentKeys {
|
||||||
|
if strings.Contains(docLn, "wwdoc") {
|
||||||
|
wwlog.Info(commentMap[docLn])
|
||||||
|
hasWwdoc = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add newline after wwdoc lines if they exist
|
||||||
|
if hasWwdoc {
|
||||||
|
fmt.Fprintln(cmd.OutOrStdout())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort variables by name for consistent output
|
||||||
|
varNames := maps.Keys(varFields)
|
||||||
|
sort.Strings(varNames)
|
||||||
|
|
||||||
|
t := table.New(cmd.OutOrStdout())
|
||||||
|
t.AddHeader("VARIABLE", "OPTION", "TYPE", "HELP")
|
||||||
|
|
||||||
|
for _, varName := range varNames {
|
||||||
|
fieldInfo := varFields[varName]
|
||||||
|
helpText, hasCommentHelp := commentMap[varName]
|
||||||
|
|
||||||
|
// Extract metadata from the resolved field
|
||||||
|
opt := ""
|
||||||
|
typ := ""
|
||||||
|
help := ""
|
||||||
|
|
||||||
|
// Check if we have valid field information (field name is not empty)
|
||||||
|
hasValidField := fieldInfo.Field.Name != ""
|
||||||
|
|
||||||
|
if hasValidField {
|
||||||
|
// Get option from lopt tag
|
||||||
|
if lopt := fieldInfo.Field.Tag.Get("lopt"); lopt != "" {
|
||||||
|
opt = "--" + lopt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get type from type tag, or use field type string
|
||||||
|
typ = fieldInfo.Field.Tag.Get("type")
|
||||||
|
if typ == "" {
|
||||||
|
// Use String() instead of Name() to handle composite types (slices, maps, pointers)
|
||||||
|
typ = fieldInfo.Field.Type.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get help from comment tag
|
||||||
|
help = fieldInfo.Field.Tag.Get("comment")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer inline comment documentation if available
|
||||||
|
if hasCommentHelp {
|
||||||
|
help = helpText
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special handling for Tags fields
|
||||||
|
if strings.Contains(varName, "Tags") {
|
||||||
|
t.AddLine(varName, opt, typ, help)
|
||||||
|
} else if hasValidField || hasCommentHelp {
|
||||||
|
t.AddLine(varName, opt, typ, help)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Print()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
366
internal/app/wwctl/overlay/info/main_test.go
Normal file
366
internal/app/wwctl/overlay/info/main_test.go
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
package info
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/warewulf/warewulf/internal/pkg/testenv"
|
||||||
|
"github.com/warewulf/warewulf/internal/pkg/warewulfd"
|
||||||
|
"github.com/warewulf/warewulf/internal/pkg/wwlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_Overlay_Variables(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
writeFiles map[string]string
|
||||||
|
args []string
|
||||||
|
expectError bool
|
||||||
|
expectedOutput string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "overlay variables",
|
||||||
|
writeFiles: map[string]string{
|
||||||
|
"var/lib/warewulf/overlays/test-overlay/test.ww": `
|
||||||
|
{{/* .Kernel.Tags.foo: "some help text" */}}
|
||||||
|
{{/* wwdoc1: First Line */}}
|
||||||
|
{{ .Node.Tags.bar }}
|
||||||
|
{{/* wwdoc2: Second Line */}}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
args: []string{"test-overlay", "test.ww"},
|
||||||
|
expectError: false,
|
||||||
|
expectedOutput: "First Line\n" +
|
||||||
|
"Second Line\n" +
|
||||||
|
"\n" +
|
||||||
|
"VARIABLE OPTION TYPE HELP\n" +
|
||||||
|
"-------- ------ ---- ----\n" +
|
||||||
|
".Node.Tags.bar string \n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "overlay variables no file",
|
||||||
|
writeFiles: map[string]string{
|
||||||
|
"var/lib/warewulf/overlays/test-overlay/test.ww": ``,
|
||||||
|
},
|
||||||
|
args: []string{"test-overlay", "no-file.ww"},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "overlay variables no overlay",
|
||||||
|
args: []string{"no-overlay", "test.ww"},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nested fields multiple levels",
|
||||||
|
writeFiles: map[string]string{
|
||||||
|
"var/lib/warewulf/overlays/test-overlay/nested.ww": `
|
||||||
|
{{ .Kernel.Args }}
|
||||||
|
{{ .Ipmi.UserName }}
|
||||||
|
{{ .Ipmi.Ipaddr }}
|
||||||
|
{{ .Warewulf.Port }}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
args: []string{"test-overlay", "nested.ww"},
|
||||||
|
expectError: false,
|
||||||
|
expectedOutput: "VARIABLE OPTION TYPE HELP\n" +
|
||||||
|
"-------- ------ ---- ----\n" +
|
||||||
|
".Ipmi.Ipaddr --ipmiaddr IP Set the IPMI IP address\n" +
|
||||||
|
".Ipmi.UserName --ipmiuser string Set the IPMI username\n" +
|
||||||
|
".Kernel.Args --kernelargs []string Set kernel arguments\n" +
|
||||||
|
".Warewulf.Port int \n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "template without variables",
|
||||||
|
writeFiles: map[string]string{
|
||||||
|
"var/lib/warewulf/overlays/test-overlay/no-vars.ww": `
|
||||||
|
# Static configuration file
|
||||||
|
# This file has no template variables
|
||||||
|
static_value=true
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
args: []string{"test-overlay", "no-vars.ww"},
|
||||||
|
expectError: false,
|
||||||
|
expectedOutput: `VARIABLE OPTION TYPE HELP
|
||||||
|
-------- ------ ---- ----
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only documentation no variables",
|
||||||
|
writeFiles: map[string]string{
|
||||||
|
"var/lib/warewulf/overlays/test-overlay/doc-only.ww": `
|
||||||
|
{{/* wwdoc: This is just documentation */}}
|
||||||
|
{{/* wwdoc2: No actual template variables used */}}
|
||||||
|
Static content here
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
args: []string{"test-overlay", "doc-only.ww"},
|
||||||
|
expectError: false,
|
||||||
|
expectedOutput: `This is just documentation
|
||||||
|
No actual template variables used
|
||||||
|
|
||||||
|
VARIABLE OPTION TYPE HELP
|
||||||
|
-------- ------ ---- ----
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "template parse error",
|
||||||
|
writeFiles: map[string]string{
|
||||||
|
"var/lib/warewulf/overlays/test-overlay/invalid.ww": `
|
||||||
|
{{ .Id }
|
||||||
|
{{ unclosed range .NetDevs }}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
args: []string{"test-overlay", "invalid.ww"},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "inline comment documentation",
|
||||||
|
writeFiles: map[string]string{
|
||||||
|
"var/lib/warewulf/overlays/test-overlay/inline-doc.ww": `
|
||||||
|
{{/* .Id: The unique node identifier */}}
|
||||||
|
{{/* .Hostname: The node's hostname in DNS */}}
|
||||||
|
{{/* .Tags.env: Environment tag (prod, dev, test) */}}
|
||||||
|
{{ .Id }}
|
||||||
|
{{ .Hostname }}
|
||||||
|
{{ .Tags.env }}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
args: []string{"test-overlay", "inline-doc.ww"},
|
||||||
|
expectError: false,
|
||||||
|
expectedOutput: `VARIABLE OPTION TYPE HELP
|
||||||
|
-------- ------ ---- ----
|
||||||
|
.Hostname string The node's hostname in DNS
|
||||||
|
.Id string The unique node identifier
|
||||||
|
.Tags.env string Environment tag (prod, dev, test)
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "complex conditional branches",
|
||||||
|
writeFiles: map[string]string{
|
||||||
|
"var/lib/warewulf/overlays/test-overlay/conditionals.ww": `
|
||||||
|
{{ if eq .Kernel.Version "5.15" }}
|
||||||
|
Old kernel
|
||||||
|
{{ else if gt (len .NetDevs) 0 }}
|
||||||
|
Has network: {{ .Id }}
|
||||||
|
{{ else }}
|
||||||
|
Default: {{ .Hostname }}
|
||||||
|
{{ end }}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
args: []string{"test-overlay", "conditionals.ww"},
|
||||||
|
expectError: false,
|
||||||
|
expectedOutput: "VARIABLE OPTION TYPE HELP\n" +
|
||||||
|
"-------- ------ ---- ----\n" +
|
||||||
|
".Hostname string \n" +
|
||||||
|
".Id string \n" +
|
||||||
|
".Kernel.Version --kernelversion string Set kernel version\n" +
|
||||||
|
".NetDevs map[string]*node.NetDev \n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sprig function pipelines",
|
||||||
|
writeFiles: map[string]string{
|
||||||
|
"var/lib/warewulf/overlays/test-overlay/sprig.ww": `
|
||||||
|
{{ .Id | upper }}
|
||||||
|
{{ .Hostname | lower }}
|
||||||
|
{{ .Kernel.Args | join " " }}
|
||||||
|
{{ .Tags.foo | default "bar" }}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
args: []string{"test-overlay", "sprig.ww"},
|
||||||
|
expectError: false,
|
||||||
|
expectedOutput: "VARIABLE OPTION TYPE HELP\n" +
|
||||||
|
"-------- ------ ---- ----\n" +
|
||||||
|
".Hostname string \n" +
|
||||||
|
".Id string \n" +
|
||||||
|
".Kernel.Args --kernelargs []string Set kernel arguments\n" +
|
||||||
|
".Tags.foo string \n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "include function usage",
|
||||||
|
writeFiles: map[string]string{
|
||||||
|
"var/lib/warewulf/overlays/test-overlay/include.ww": `
|
||||||
|
{{ Include "/etc/passwd" }}
|
||||||
|
{{ IncludeFrom .ImageName "/etc/group" }}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
args: []string{"test-overlay", "include.ww"},
|
||||||
|
expectError: false,
|
||||||
|
expectedOutput: "VARIABLE OPTION TYPE HELP\n" +
|
||||||
|
"-------- ------ ---- ----\n" +
|
||||||
|
".ImageName --image string Set image name\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mixed documentation types",
|
||||||
|
writeFiles: map[string]string{
|
||||||
|
"var/lib/warewulf/overlays/test-overlay/mixed-doc.ww": `
|
||||||
|
{{/* wwdoc: Configuration for network interfaces */}}
|
||||||
|
{{/* wwdoc-details: This template generates network configs */}}
|
||||||
|
{{/* .NetDevs: Map of network devices by name */}}
|
||||||
|
{{/* .Id: Node identifier */}}
|
||||||
|
{{ .Id }}
|
||||||
|
{{ .NetDevs }}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
args: []string{"test-overlay", "mixed-doc.ww"},
|
||||||
|
expectError: false,
|
||||||
|
expectedOutput: "Configuration for network interfaces\n" +
|
||||||
|
"This template generates network configs\n" +
|
||||||
|
"\n" +
|
||||||
|
"VARIABLE OPTION TYPE HELP\n" +
|
||||||
|
"-------- ------ ---- ----\n" +
|
||||||
|
".Id string Node identifier\n" +
|
||||||
|
".NetDevs map[string]*node.NetDev Map of network devices by name\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tag field access",
|
||||||
|
writeFiles: map[string]string{
|
||||||
|
"var/lib/warewulf/overlays/test-overlay/tags.ww": `
|
||||||
|
{{ .Tags.foo }}
|
||||||
|
{{ .Ipmi.Tags.vlan }}
|
||||||
|
{{ .NetDevs }}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
args: []string{"test-overlay", "tags.ww"},
|
||||||
|
expectError: false,
|
||||||
|
expectedOutput: "VARIABLE OPTION TYPE HELP\n" +
|
||||||
|
"-------- ------ ---- ----\n" +
|
||||||
|
".Ipmi.Tags.vlan string \n" +
|
||||||
|
".NetDevs map[string]*node.NetDev \n" +
|
||||||
|
".Tags.foo string \n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dollar sign root context",
|
||||||
|
writeFiles: map[string]string{
|
||||||
|
"var/lib/warewulf/overlays/test-overlay/dollar.ww": `
|
||||||
|
{{ $.Id }}
|
||||||
|
{{ $.BuildHost }}
|
||||||
|
{{ $.Ipaddr }}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
args: []string{"test-overlay", "dollar.ww"},
|
||||||
|
expectError: false,
|
||||||
|
expectedOutput: "VARIABLE OPTION TYPE HELP\n" +
|
||||||
|
"-------- ------ ---- ----\n" +
|
||||||
|
"$.BuildHost string \n" +
|
||||||
|
"$.Id string \n" +
|
||||||
|
"$.Ipaddr string \n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "range over map basic",
|
||||||
|
writeFiles: map[string]string{
|
||||||
|
"var/lib/warewulf/overlays/test-overlay/range-map.ww": `
|
||||||
|
{{/* wwdoc: Network device iteration */}}
|
||||||
|
{{- range $devname, $netdev := .NetDevs }}
|
||||||
|
Device: {{ $netdev.Device }}
|
||||||
|
Type: {{ $netdev.Type }}
|
||||||
|
IP: {{ $netdev.Ipaddr }}
|
||||||
|
{{- end }}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
args: []string{"test-overlay", "range-map.ww"},
|
||||||
|
expectError: false,
|
||||||
|
expectedOutput: "Network device iteration\n" +
|
||||||
|
"\n" +
|
||||||
|
"VARIABLE OPTION TYPE HELP\n" +
|
||||||
|
"-------- ------ ---- ----\n" +
|
||||||
|
"$netdev.Device --netdev string Set the device for given network\n" +
|
||||||
|
"$netdev.Ipaddr --ipaddr IP IPv4 address in given network\n" +
|
||||||
|
"$netdev.Type --type string Set device type of given network\n" +
|
||||||
|
".NetDevs map[string]*node.NetDev \n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "range over map with output verification",
|
||||||
|
writeFiles: map[string]string{
|
||||||
|
"var/lib/warewulf/overlays/test-overlay/range-verify.ww": `
|
||||||
|
{{- range $devname, $netdev := .NetDevs }}
|
||||||
|
{{ $devname }}
|
||||||
|
{{ $netdev.Device }}
|
||||||
|
{{ $netdev.Type }}
|
||||||
|
{{- end }}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
args: []string{"test-overlay", "range-verify.ww"},
|
||||||
|
expectError: false,
|
||||||
|
expectedOutput: "VARIABLE OPTION TYPE HELP\n" +
|
||||||
|
"-------- ------ ---- ----\n" +
|
||||||
|
"$netdev.Device --netdev string Set the device for given network\n" +
|
||||||
|
"$netdev.Type --type string Set device type of given network\n" +
|
||||||
|
".NetDevs map[string]*node.NetDev \n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty collection checks",
|
||||||
|
writeFiles: map[string]string{
|
||||||
|
"var/lib/warewulf/overlays/test-overlay/empty.ww": `
|
||||||
|
{{ if gt (len .NetDevs) 0 }}
|
||||||
|
Has devices
|
||||||
|
{{ end }}
|
||||||
|
{{ if .FileSystems }}
|
||||||
|
Has filesystems
|
||||||
|
{{ end }}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
args: []string{"test-overlay", "empty.ww"},
|
||||||
|
expectError: false,
|
||||||
|
expectedOutput: "VARIABLE OPTION TYPE HELP\n" +
|
||||||
|
"-------- ------ ---- ----\n" +
|
||||||
|
".FileSystems map[string]*node.FileSystem \n" +
|
||||||
|
".NetDevs map[string]*node.NetDev \n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "file and abort directives",
|
||||||
|
writeFiles: map[string]string{
|
||||||
|
"var/lib/warewulf/overlays/test-overlay/control.ww": `
|
||||||
|
{{ file "output.conf" }}
|
||||||
|
{{ if .Tags.enabled }}
|
||||||
|
Config: {{ .Tags.value }}
|
||||||
|
{{ else }}
|
||||||
|
{{ abort }}
|
||||||
|
{{ end }}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
args: []string{"test-overlay", "control.ww"},
|
||||||
|
expectError: false,
|
||||||
|
expectedOutput: "VARIABLE OPTION TYPE HELP\n" +
|
||||||
|
"-------- ------ ---- ----\n" +
|
||||||
|
".Tags.enabled string \n" +
|
||||||
|
".Tags.value string \n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
env := testenv.New(t)
|
||||||
|
defer env.RemoveAll()
|
||||||
|
warewulfd.SetNoDaemon()
|
||||||
|
|
||||||
|
for path, content := range tt.writeFiles {
|
||||||
|
env.WriteFile(path, content)
|
||||||
|
}
|
||||||
|
baseCmd := GetCommand()
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
baseCmd.SetOut(buf)
|
||||||
|
baseCmd.SetErr(buf)
|
||||||
|
wwlog.SetLogWriter(buf)
|
||||||
|
|
||||||
|
baseCmd.SetArgs(tt.args)
|
||||||
|
err := baseCmd.Execute()
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.expectedOutput != "" {
|
||||||
|
output := buf.String()
|
||||||
|
assert.Equal(t, tt.expectedOutput, output)
|
||||||
|
} else {
|
||||||
|
// For tests without expected output, print actual output for debugging
|
||||||
|
// This helps understand what variables are being detected
|
||||||
|
if testing.Verbose() {
|
||||||
|
t.Logf("Output:\n%s", buf.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
38
internal/app/wwctl/overlay/info/root.go
Normal file
38
internal/app/wwctl/overlay/info/root.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package info
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/warewulf/warewulf/internal/pkg/overlay"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
baseCmd = &cobra.Command{
|
||||||
|
Use: "info [flags] OVERLAY_NAME FILE_PATH",
|
||||||
|
Short: "Show variables for a template file in an overlay",
|
||||||
|
Long: "This command will show the variables for a given template file in a given\n" + "overlay.",
|
||||||
|
Aliases: []string{"variables", "vars"},
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
RunE: CobraRunE,
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return overlay.FindOverlays(), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
} else if len(args) == 1 {
|
||||||
|
ov, err := overlay.Get(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, cobra.ShellCompDirectiveError
|
||||||
|
}
|
||||||
|
files, err := ov.GetFiles()
|
||||||
|
if err != nil {
|
||||||
|
return nil, cobra.ShellCompDirectiveError
|
||||||
|
}
|
||||||
|
return files, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetCommand returns the root cobra.Command for this application.
|
||||||
|
func GetCommand() *cobra.Command {
|
||||||
|
return baseCmd
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/warewulf/warewulf/internal/app/wwctl/overlay/delete"
|
"github.com/warewulf/warewulf/internal/app/wwctl/overlay/delete"
|
||||||
"github.com/warewulf/warewulf/internal/app/wwctl/overlay/edit"
|
"github.com/warewulf/warewulf/internal/app/wwctl/overlay/edit"
|
||||||
"github.com/warewulf/warewulf/internal/app/wwctl/overlay/imprt"
|
"github.com/warewulf/warewulf/internal/app/wwctl/overlay/imprt"
|
||||||
|
"github.com/warewulf/warewulf/internal/app/wwctl/overlay/info"
|
||||||
"github.com/warewulf/warewulf/internal/app/wwctl/overlay/list"
|
"github.com/warewulf/warewulf/internal/app/wwctl/overlay/list"
|
||||||
"github.com/warewulf/warewulf/internal/app/wwctl/overlay/mkdir"
|
"github.com/warewulf/warewulf/internal/app/wwctl/overlay/mkdir"
|
||||||
"github.com/warewulf/warewulf/internal/app/wwctl/overlay/show"
|
"github.com/warewulf/warewulf/internal/app/wwctl/overlay/show"
|
||||||
@@ -35,6 +36,7 @@ func init() {
|
|||||||
baseCmd.AddCommand(imprt.GetCommand())
|
baseCmd.AddCommand(imprt.GetCommand())
|
||||||
baseCmd.AddCommand(chmod.GetCommand())
|
baseCmd.AddCommand(chmod.GetCommand())
|
||||||
baseCmd.AddCommand(chown.GetCommand())
|
baseCmd.AddCommand(chown.GetCommand())
|
||||||
|
baseCmd.AddCommand(info.GetCommand())
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRootCommand returns the root cobra.Command for the application.
|
// GetRootCommand returns the root cobra.Command for the application.
|
||||||
|
|||||||
@@ -91,8 +91,8 @@ func CobraRunE(cmd *cobra.Command, args []string) error {
|
|||||||
wwlog.Debug("Found multifile comment, new filename %s", filenameFromTemplate[0][1])
|
wwlog.Debug("Found multifile comment, new filename %s", filenameFromTemplate[0][1])
|
||||||
if foundFileComment {
|
if foundFileComment {
|
||||||
if !Quiet {
|
if !Quiet {
|
||||||
wwlog.Info("backupFile: %v", backupFile)
|
wwlog.Info("backupFile: %v", *backupFile)
|
||||||
wwlog.Info("writeFile: %v", writeFile)
|
wwlog.Info("writeFile: %v", *writeFile)
|
||||||
wwlog.Info("Filename: %s", destFileName)
|
wwlog.Info("Filename: %s", destFileName)
|
||||||
}
|
}
|
||||||
wwlog.Output("%s", outBuffer.String())
|
wwlog.Output("%s", outBuffer.String())
|
||||||
@@ -105,8 +105,8 @@ func CobraRunE(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !Quiet {
|
if !Quiet {
|
||||||
wwlog.Info("backupFile: %v", backupFile)
|
wwlog.Info("backupFile: %v", *backupFile)
|
||||||
wwlog.Info("writeFile: %v", writeFile)
|
wwlog.Info("writeFile: %v", *writeFile)
|
||||||
wwlog.Info("Filename: %s", destFileName)
|
wwlog.Info("Filename: %s", destFileName)
|
||||||
}
|
}
|
||||||
wwlog.Output("%s", outBuffer.String())
|
wwlog.Output("%s", outBuffer.String())
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func CobraRunE(cmd *cobra.Command, args []string) error {
|
|||||||
defer os.Remove(tempFile.Name())
|
defer os.Remove(tempFile.Name())
|
||||||
|
|
||||||
if !NoHeader {
|
if !NoHeader {
|
||||||
yamlTemplate := node.UnmarshalConf(node.Profile{}, nil)
|
yamlTemplate := node.ConfToYaml(node.Profile{}, nil)
|
||||||
if _, err := tempFile.WriteString("#profilename:\n# " + strings.Join(yamlTemplate, "\n# ") + "\n"); err != nil {
|
if _, err := tempFile.WriteString("#profilename:\n# " + strings.Join(yamlTemplate, "\n# ") + "\n"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ func Hostfile() (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if writeFile {
|
if *writeFile {
|
||||||
err = overlay.CarefulWriteBuffer("/etc/hosts", buffer, backupFile, info.Mode())
|
err = overlay.CarefulWriteBuffer("/etc/hosts", buffer, *backupFile, info.Mode())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not write file from template: %w", err)
|
return fmt.Errorf("could not write file from template: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ func recursiveFlatten(obj interface{}) (hasContent bool) {
|
|||||||
Create a string slice, where every element represents a yaml entry, used for node/profile edit
|
Create a string slice, where every element represents a yaml entry, used for node/profile edit
|
||||||
in order to get a summary of all available elements
|
in order to get a summary of all available elements
|
||||||
*/
|
*/
|
||||||
func UnmarshalConf(obj interface{}, excludeList []string) (lines []string) {
|
func ConfToYaml(obj interface{}, excludeList []string) (lines []string) {
|
||||||
objType := reflect.TypeOf(obj)
|
objType := reflect.TypeOf(obj)
|
||||||
// now iterate of every field
|
// now iterate of every field
|
||||||
for i := 0; i < objType.NumField(); i++ {
|
for i := 0; i < objType.NumField(); i++ {
|
||||||
@@ -264,7 +264,7 @@ func UnmarshalConf(obj interface{}, excludeList []string) (lines []string) {
|
|||||||
typeLine = strings.Split(typeLine, ",")[0] + ":"
|
typeLine = strings.Split(typeLine, ",")[0] + ":"
|
||||||
}
|
}
|
||||||
lines = append(lines, typeLine)
|
lines = append(lines, typeLine)
|
||||||
nestedLine := UnmarshalConf(reflect.New(field.Type.Elem()).Elem().Interface(), excludeList)
|
nestedLine := ConfToYaml(reflect.New(field.Type.Elem()).Elem().Interface(), excludeList)
|
||||||
for _, ln := range nestedLine {
|
for _, ln := range nestedLine {
|
||||||
lines = append(lines, " "+ln)
|
lines = append(lines, " "+ln)
|
||||||
}
|
}
|
||||||
@@ -274,12 +274,12 @@ func UnmarshalConf(obj interface{}, excludeList []string) (lines []string) {
|
|||||||
typeLine = strings.Split(typeLine, ",")[0] + ":"
|
typeLine = strings.Split(typeLine, ",")[0] + ":"
|
||||||
}
|
}
|
||||||
lines = append(lines, typeLine, " element:")
|
lines = append(lines, typeLine, " element:")
|
||||||
nestedLine := UnmarshalConf(reflect.New(field.Type.Elem().Elem()).Elem().Interface(), excludeList)
|
nestedLine := ConfToYaml(reflect.New(field.Type.Elem().Elem()).Elem().Interface(), excludeList)
|
||||||
for _, ln := range nestedLine {
|
for _, ln := range nestedLine {
|
||||||
lines = append(lines, " "+ln)
|
lines = append(lines, " "+ln)
|
||||||
}
|
}
|
||||||
} else if field.Type.Kind() == reflect.Struct && field.Anonymous {
|
} else if field.Type.Kind() == reflect.Struct && field.Anonymous {
|
||||||
nestedLine := UnmarshalConf(reflect.New(field.Type).Elem().Interface(), excludeList)
|
nestedLine := ConfToYaml(reflect.New(field.Type).Elem().Interface(), excludeList)
|
||||||
lines = append(lines, nestedLine...)
|
lines = append(lines, nestedLine...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -316,9 +316,13 @@ func getYamlString(myType reflect.StructField, excludeList []string) ([]string,
|
|||||||
return []string{ymlStr}, true
|
return []string{ymlStr}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
// struectred type for variable
|
||||||
Getters for unexported fields
|
type TemplateVarDetails struct {
|
||||||
*/
|
Name string
|
||||||
|
Comment string
|
||||||
|
Type string
|
||||||
|
LongOpt string
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Returns the id of the node
|
Returns the id of the node
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
"text/template/parse"
|
||||||
|
|
||||||
"github.com/Masterminds/sprig/v3"
|
"github.com/Masterminds/sprig/v3"
|
||||||
"github.com/coreos/go-systemd/v22/unit"
|
"github.com/coreos/go-systemd/v22/unit"
|
||||||
@@ -272,6 +274,481 @@ func (overlay Overlay) Mkdir(path string, mode int32) (err error) {
|
|||||||
return os.MkdirAll(fullPath, os.FileMode(mode))
|
return os.MkdirAll(fullPath, os.FileMode(mode))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FieldInfo contains detailed type information about a template variable
|
||||||
|
type FieldInfo struct {
|
||||||
|
Field reflect.StructField // Complete field metadata including tags
|
||||||
|
ParentType reflect.Type // The containing struct type
|
||||||
|
FullPath string // Full path like ".NetDevs.Ipaddr6"
|
||||||
|
VarName string // Original variable name from template
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseVarFields returns detailed type information for each variable in the template
|
||||||
|
// by using reflection to resolve the actual struct fields being referenced.
|
||||||
|
func (overlay Overlay) ParseVarFields(file string) map[string]FieldInfo {
|
||||||
|
if !strings.HasSuffix(file, ".ww") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fullPath := overlay.File(file)
|
||||||
|
if !util.IsFile(fullPath) {
|
||||||
|
wwlog.Error("Template file does not exist in overlay %s: %s", overlay.Name(), file)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
funcMap, _, _ := getTemplateFuncMap(fullPath, TemplateStruct{})
|
||||||
|
tmpl, err := template.New(path.Base(fullPath)).Option("missingkey=default").Funcs(funcMap).ParseFiles(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
wwlog.Error("Could not parse template file %s: %s", fullPath, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[string]FieldInfo)
|
||||||
|
rootType := reflect.TypeOf(TemplateStruct{})
|
||||||
|
|
||||||
|
// Track range variables and their types
|
||||||
|
rangeVars := make(map[string]reflect.Type)
|
||||||
|
// Initialize $ to refer to the root template context
|
||||||
|
rangeVars["$"] = rootType
|
||||||
|
|
||||||
|
if tmpl.Tree != nil && tmpl.Tree.Root != nil {
|
||||||
|
walkParseTree(tmpl.Tree.Root, rootType, "", rangeVars, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseCommentVars parses a template file for comments that contain variable documentations.
|
||||||
|
// The comments must be in the format `{{/* key: value */}}`. The content is parsed as YAML.
|
||||||
|
func (overlay Overlay) ParseCommentVars(file string) (retMap map[string]string) {
|
||||||
|
retMap = make(map[string]string)
|
||||||
|
if !strings.HasSuffix(file, ".ww") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fullPath := overlay.File(file)
|
||||||
|
if !util.IsFile(fullPath) {
|
||||||
|
wwlog.Error("Template file does not exist in overlay %s: %s", overlay.Name(), file)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := os.ReadFile(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
wwlog.Error("Could not read template file %s: %s", fullPath, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
re := regexp.MustCompile(`{{\s*/\*\s*(.*?):\s*(.*?)\s*\*/\s*}}`)
|
||||||
|
matches := re.FindAllStringSubmatch(string(content), -1)
|
||||||
|
if len(matches) > 0 {
|
||||||
|
wwlog.Debug("matches: %v len(%d:%d)", matches, len(matches), len(matches[0]))
|
||||||
|
} else {
|
||||||
|
wwlog.Debug("matches: [] len(0)")
|
||||||
|
}
|
||||||
|
for i := range matches {
|
||||||
|
if len(matches[i]) > 2 {
|
||||||
|
retMap[matches[i][1]] = matches[i][2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// walkParseTree recursively traverses the template's parse tree and resolves
|
||||||
|
// variable references to actual struct fields using reflection.
|
||||||
|
func walkParseTree(node parse.Node, currentType reflect.Type, currentPath string, rangeVars map[string]reflect.Type, result map[string]FieldInfo) {
|
||||||
|
if node == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch n := node.(type) {
|
||||||
|
case *parse.ActionNode:
|
||||||
|
// Handle variable assignments like $var := expr
|
||||||
|
if n.Pipe != nil && len(n.Pipe.Decl) > 0 && len(n.Pipe.Cmds) > 0 {
|
||||||
|
// Try to resolve the type of the expression being assigned
|
||||||
|
cmd := n.Pipe.Cmds[0]
|
||||||
|
if len(cmd.Args) > 0 {
|
||||||
|
// Check for field access (e.g., $NetDevs := .NetDevs)
|
||||||
|
if field, ok := cmd.Args[0].(*parse.FieldNode); ok {
|
||||||
|
fieldInfo := resolveFieldChain(currentType, field.Ident, currentPath)
|
||||||
|
if fieldInfo != nil {
|
||||||
|
// Record all declared variables with this type
|
||||||
|
for _, decl := range n.Pipe.Decl {
|
||||||
|
rangeVars[decl.Ident[0]] = fieldInfo.Field.Type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if varNode, ok := cmd.Args[0].(*parse.VariableNode); ok {
|
||||||
|
// Check for variable-to-variable assignment (e.g., $b := $a)
|
||||||
|
if len(varNode.Ident) == 1 {
|
||||||
|
// Simple variable reference
|
||||||
|
varBaseName := varNode.Ident[0]
|
||||||
|
if varType, exists := rangeVars[varBaseName]; exists {
|
||||||
|
for _, decl := range n.Pipe.Decl {
|
||||||
|
rangeVars[decl.Ident[0]] = varType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walkParseTree(n.Pipe, currentType, currentPath, rangeVars, result)
|
||||||
|
|
||||||
|
case *parse.IfNode:
|
||||||
|
walkParseTree(n.Pipe, currentType, currentPath, rangeVars, result)
|
||||||
|
walkParseTree(n.List, currentType, currentPath, rangeVars, result)
|
||||||
|
walkParseTree(n.ElseList, currentType, currentPath, rangeVars, result)
|
||||||
|
|
||||||
|
case *parse.ListNode:
|
||||||
|
if n != nil {
|
||||||
|
for _, child := range n.Nodes {
|
||||||
|
walkParseTree(child, currentType, currentPath, rangeVars, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case *parse.RangeNode:
|
||||||
|
// Handle range statements like {{range $key, $val := .NetDevs}}
|
||||||
|
// First, walk the pipe to record the variable being ranged over
|
||||||
|
walkParseTree(n.Pipe, currentType, currentPath, rangeVars, result)
|
||||||
|
|
||||||
|
if n.Pipe != nil && len(n.Pipe.Cmds) > 0 {
|
||||||
|
// Get what's being ranged over to determine element type
|
||||||
|
var fieldInfo *FieldInfo
|
||||||
|
|
||||||
|
// Check for both FieldNode (e.g., .NetDevs) and VariableNode (e.g., $disk.PartitionList)
|
||||||
|
rangeField := extractFieldFromPipe(n.Pipe)
|
||||||
|
if rangeField != nil {
|
||||||
|
// Resolve the type of the field being ranged over
|
||||||
|
fieldInfo = resolveFieldChain(currentType, rangeField.Ident, currentPath)
|
||||||
|
} else {
|
||||||
|
// Check if it's a variable access
|
||||||
|
for _, cmd := range n.Pipe.Cmds {
|
||||||
|
for _, arg := range cmd.Args {
|
||||||
|
if varNode, ok := arg.(*parse.VariableNode); ok {
|
||||||
|
varBaseName := varNode.Ident[0]
|
||||||
|
if varType, exists := rangeVars[varBaseName]; exists {
|
||||||
|
if len(varNode.Ident) > 1 {
|
||||||
|
// Multi-part variable like $disk.PartitionList
|
||||||
|
fieldIdents := varNode.Ident[1:] // Skip the variable name
|
||||||
|
fieldInfo = resolveFieldChain(varType, fieldIdents, currentPath)
|
||||||
|
} else {
|
||||||
|
// Simple variable like $NetDevs
|
||||||
|
fieldInfo = &FieldInfo{
|
||||||
|
Field: reflect.StructField{
|
||||||
|
Name: varBaseName,
|
||||||
|
Type: varType,
|
||||||
|
},
|
||||||
|
ParentType: currentType,
|
||||||
|
FullPath: currentPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fieldInfo != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fieldInfo != nil {
|
||||||
|
rangeType := fieldInfo.Field.Type
|
||||||
|
|
||||||
|
// For slices and arrays, get the element type
|
||||||
|
if rangeType.Kind() == reflect.Slice || rangeType.Kind() == reflect.Array {
|
||||||
|
rangeType = rangeType.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// For maps, get the value type
|
||||||
|
if rangeType.Kind() == reflect.Map {
|
||||||
|
rangeType = rangeType.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dereference pointers
|
||||||
|
if rangeType.Kind() == reflect.Ptr {
|
||||||
|
rangeType = rangeType.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track the range variable assignments
|
||||||
|
if len(n.Pipe.Decl) > 0 {
|
||||||
|
if len(n.Pipe.Decl) == 2 {
|
||||||
|
// Two-variable range: $key, $val := .Map or $index, $val := .Slice
|
||||||
|
keyType := reflect.TypeOf(0) // Default to int for slice indices
|
||||||
|
if fieldInfo.Field.Type.Kind() == reflect.Map {
|
||||||
|
keyType = fieldInfo.Field.Type.Key()
|
||||||
|
}
|
||||||
|
rangeVars[n.Pipe.Decl[0].Ident[0]] = keyType // First variable is key/index
|
||||||
|
rangeVars[n.Pipe.Decl[1].Ident[0]] = rangeType // Second variable is value
|
||||||
|
} else {
|
||||||
|
// Single-variable range: $val := .Slice
|
||||||
|
rangeVars[n.Pipe.Decl[0].Ident[0]] = rangeType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk the range body with the element type
|
||||||
|
walkParseTree(n.List, rangeType, fieldInfo.FullPath, rangeVars, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walkParseTree(n.ElseList, currentType, currentPath, rangeVars, result)
|
||||||
|
|
||||||
|
case *parse.WithNode:
|
||||||
|
walkParseTree(n.Pipe, currentType, currentPath, rangeVars, result)
|
||||||
|
walkParseTree(n.List, currentType, currentPath, rangeVars, result)
|
||||||
|
walkParseTree(n.ElseList, currentType, currentPath, rangeVars, result)
|
||||||
|
|
||||||
|
case *parse.TemplateNode:
|
||||||
|
walkParseTree(n.Pipe, currentType, currentPath, rangeVars, result)
|
||||||
|
|
||||||
|
case *parse.PipeNode:
|
||||||
|
if n != nil {
|
||||||
|
for _, cmd := range n.Cmds {
|
||||||
|
for _, arg := range cmd.Args {
|
||||||
|
walkParseTree(arg, currentType, currentPath, rangeVars, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case *parse.FieldNode:
|
||||||
|
// Field access like .Ipmi.Ipaddr or $netdev.Ipaddr
|
||||||
|
varName := n.String()
|
||||||
|
|
||||||
|
// Determine the base type for field resolution
|
||||||
|
baseType := currentType
|
||||||
|
basePath := currentPath
|
||||||
|
|
||||||
|
// Check if this is a variable access (starts with $)
|
||||||
|
if strings.HasPrefix(varName, "$") {
|
||||||
|
// Extract variable name (e.g., "$netdev.Device" -> "netdev")
|
||||||
|
parts := strings.SplitN(varName[1:], ".", 2)
|
||||||
|
if len(parts) > 0 {
|
||||||
|
varBaseName := parts[0]
|
||||||
|
if varType, exists := rangeVars[varBaseName]; exists {
|
||||||
|
baseType = varType
|
||||||
|
basePath = currentPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldInfo := resolveFieldChain(baseType, n.Ident, basePath)
|
||||||
|
|
||||||
|
// If resolution failed, try adding "P" suffix to last identifier
|
||||||
|
// This handles methods like Primary() backed by PrimaryP field
|
||||||
|
if fieldInfo == nil && len(n.Ident) >= 1 {
|
||||||
|
identWithP := make([]string, len(n.Ident))
|
||||||
|
copy(identWithP, n.Ident)
|
||||||
|
identWithP[len(identWithP)-1] += "P"
|
||||||
|
fieldInfo = resolveFieldChain(baseType, identWithP, basePath)
|
||||||
|
// Use the original variable name (without P)
|
||||||
|
if fieldInfo != nil {
|
||||||
|
fieldInfo.VarName = varName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fieldInfo != nil {
|
||||||
|
if fieldInfo.VarName == "" {
|
||||||
|
fieldInfo.VarName = varName
|
||||||
|
}
|
||||||
|
result[varName] = *fieldInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
case *parse.VariableNode:
|
||||||
|
// Variable reference like $netdev or possibly $netdev.Field
|
||||||
|
varName := n.String()
|
||||||
|
|
||||||
|
// Check if this is a simple variable or a field access on a variable
|
||||||
|
if len(n.Ident) > 1 {
|
||||||
|
// This is $var.Field or $var.Field.Method - handle like a FieldNode
|
||||||
|
varBaseName := n.Ident[0]
|
||||||
|
if varType, exists := rangeVars[varBaseName]; exists {
|
||||||
|
// Resolve the field chain starting from the variable's type
|
||||||
|
fieldIdents := n.Ident[1:] // Skip the variable name, keep the fields
|
||||||
|
fieldInfo := resolveFieldChain(varType, fieldIdents, currentPath)
|
||||||
|
|
||||||
|
// If resolution failed and we have multiple parts, the last part might be a method
|
||||||
|
// Try resolving without the last identifier (e.g., OnBoot.BoolDefaultTrue -> OnBoot)
|
||||||
|
if fieldInfo == nil && len(fieldIdents) > 1 {
|
||||||
|
fieldIdents = fieldIdents[:len(fieldIdents)-1]
|
||||||
|
fieldInfo = resolveFieldChain(varType, fieldIdents, currentPath)
|
||||||
|
// Use a simplified variable name without the method
|
||||||
|
if fieldInfo != nil {
|
||||||
|
// Build simplified name: $varBaseName.field1.field2 (without method)
|
||||||
|
simplifiedName := varBaseName
|
||||||
|
for _, ident := range fieldIdents {
|
||||||
|
simplifiedName += "." + ident
|
||||||
|
}
|
||||||
|
fieldInfo.VarName = simplifiedName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If resolution still failed, try adding "P" suffix to last identifier
|
||||||
|
// This handles methods like Primary() backed by PrimaryP field
|
||||||
|
if fieldInfo == nil && len(fieldIdents) >= 1 {
|
||||||
|
// Try with "P" suffix on the last identifier
|
||||||
|
fieldIdentsWithP := make([]string, len(fieldIdents))
|
||||||
|
copy(fieldIdentsWithP, fieldIdents)
|
||||||
|
fieldIdentsWithP[len(fieldIdentsWithP)-1] += "P"
|
||||||
|
fieldInfo = resolveFieldChain(varType, fieldIdentsWithP, currentPath)
|
||||||
|
// Use the original variable name (without P)
|
||||||
|
if fieldInfo != nil {
|
||||||
|
fieldInfo.VarName = varName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fieldInfo != nil {
|
||||||
|
if fieldInfo.VarName == "" {
|
||||||
|
fieldInfo.VarName = varName
|
||||||
|
}
|
||||||
|
result[fieldInfo.VarName] = *fieldInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if len(n.Ident) == 1 {
|
||||||
|
// Simple variable reference
|
||||||
|
varBaseName := n.Ident[0]
|
||||||
|
if varType, exists := rangeVars[varBaseName]; exists {
|
||||||
|
result[varName] = FieldInfo{
|
||||||
|
VarName: varName,
|
||||||
|
ParentType: varType,
|
||||||
|
FullPath: currentPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractFieldFromPipe extracts the FieldNode from a pipe (used in range statements)
|
||||||
|
func extractFieldFromPipe(pipe *parse.PipeNode) *parse.FieldNode {
|
||||||
|
if pipe == nil || len(pipe.Cmds) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, cmd := range pipe.Cmds {
|
||||||
|
for _, arg := range cmd.Args {
|
||||||
|
if field, ok := arg.(*parse.FieldNode); ok {
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveFieldChain walks a chain of field identifiers (like ["Ipmi", "Ipaddr"])
|
||||||
|
// and returns the final field's information using reflection.
|
||||||
|
// Methods are resolved and reported. If a method has a backing field with "P" suffix,
|
||||||
|
// the backing field's metadata is used; otherwise, the method's return type is used.
|
||||||
|
func resolveFieldChain(rootType reflect.Type, idents []string, basePath string) *FieldInfo {
|
||||||
|
if rootType.Kind() == reflect.Ptr {
|
||||||
|
rootType = rootType.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(idents) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
currentType := rootType
|
||||||
|
fullPath := basePath
|
||||||
|
var finalField reflect.StructField
|
||||||
|
var parentType reflect.Type
|
||||||
|
|
||||||
|
for i, fieldName := range idents {
|
||||||
|
if currentType.Kind() != reflect.Struct {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
field, found := currentType.FieldByName(fieldName)
|
||||||
|
if !found {
|
||||||
|
// Field not found - try to find a method with this name
|
||||||
|
// This handles methods like DiskList(), PartitionList(), Id(), ShouldExist()
|
||||||
|
ptrType := reflect.PointerTo(currentType)
|
||||||
|
method, methodFound := ptrType.MethodByName(fieldName)
|
||||||
|
if !methodFound {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the method's return type (first return value)
|
||||||
|
methodType := method.Type
|
||||||
|
if methodType.NumOut() == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
returnType := methodType.Out(0)
|
||||||
|
|
||||||
|
// Check if there's a backing field with "P" suffix
|
||||||
|
// Only methods with backing fields should be reported as user-facing variables
|
||||||
|
backingFieldName := fieldName + "P"
|
||||||
|
backingField, backingFound := currentType.FieldByName(backingFieldName)
|
||||||
|
|
||||||
|
// For the last identifier in the chain
|
||||||
|
if i == len(idents)-1 {
|
||||||
|
if backingFound {
|
||||||
|
// Use the backing field's metadata (tags) but the method's return type
|
||||||
|
// This gives us the documentation from ShouldExistP but the type from ShouldExist()
|
||||||
|
finalField = reflect.StructField{
|
||||||
|
Name: fieldName,
|
||||||
|
Type: returnType, // Use method's return type, not backing field's type
|
||||||
|
Tag: backingField.Tag,
|
||||||
|
}
|
||||||
|
parentType = currentType
|
||||||
|
fullPath += "." + fieldName // Use method name in path, not field name
|
||||||
|
currentType = returnType
|
||||||
|
} else {
|
||||||
|
// No backing field - create a field with the method's return type
|
||||||
|
// This allows documenting methods via comments
|
||||||
|
finalField = reflect.StructField{
|
||||||
|
Name: fieldName,
|
||||||
|
Type: returnType,
|
||||||
|
}
|
||||||
|
parentType = currentType
|
||||||
|
fullPath += "." + fieldName
|
||||||
|
currentType = returnType
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not the last identifier - we're in the middle of a chain (e.g., DiskList in node.DiskList.PartitionList)
|
||||||
|
// Continue walking with the method's return type for type resolution
|
||||||
|
parentType = currentType
|
||||||
|
fullPath += "." + fieldName
|
||||||
|
currentType = returnType
|
||||||
|
|
||||||
|
// We don't have a real field yet, just continue to next identifier
|
||||||
|
// Don't set finalField here as we need to keep walking
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Field found
|
||||||
|
finalField = field
|
||||||
|
parentType = currentType
|
||||||
|
fullPath += "." + fieldName
|
||||||
|
currentType = field.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dereference pointer types for next iteration
|
||||||
|
if currentType.Kind() == reflect.Ptr {
|
||||||
|
currentType = currentType.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// For map types, remaining identifiers are map keys, not fields
|
||||||
|
if currentType.Kind() == reflect.Map && i < len(idents)-1 {
|
||||||
|
// Append remaining path as map keys
|
||||||
|
mapKeyPath := ""
|
||||||
|
for j := i + 1; j < len(idents); j++ {
|
||||||
|
mapKeyPath += "." + idents[j]
|
||||||
|
}
|
||||||
|
fullPath += mapKeyPath
|
||||||
|
|
||||||
|
// Create a synthetic field with the map's value type
|
||||||
|
// For Tags (map[string]string), accessing Tags.key should return string, not map[string]string
|
||||||
|
valueType := currentType.Elem()
|
||||||
|
return &FieldInfo{
|
||||||
|
Field: reflect.StructField{
|
||||||
|
Name: idents[len(idents)-1], // Use the last key as the field name
|
||||||
|
Type: valueType, // Use the map's value type
|
||||||
|
},
|
||||||
|
ParentType: parentType,
|
||||||
|
FullPath: fullPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &FieldInfo{
|
||||||
|
Field: finalField,
|
||||||
|
ParentType: parentType,
|
||||||
|
FullPath: fullPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func BuildAllOverlays(nodes []node.Node, allNodes []node.Node, workerCount int) error {
|
func BuildAllOverlays(nodes []node.Node, allNodes []node.Node, workerCount int) error {
|
||||||
nodeChan := make(chan node.Node, len(nodes))
|
nodeChan := make(chan node.Node, len(nodes))
|
||||||
errChan := make(chan error, len(nodes)*2)
|
errChan := make(chan error, len(nodes)*2)
|
||||||
@@ -536,7 +1013,7 @@ func BuildOverlayIndir(nodeData node.Node, allNodes []node.Node, overlayNames []
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to render template %s: %w", walkPath, err)
|
return fmt.Errorf("failed to render template %s: %w", walkPath, err)
|
||||||
}
|
}
|
||||||
if !writeFile {
|
if !*writeFile {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var fileBuffer bytes.Buffer
|
var fileBuffer bytes.Buffer
|
||||||
@@ -561,7 +1038,7 @@ func BuildOverlayIndir(nodeData node.Node, allNodes []node.Node, overlayNames []
|
|||||||
} else if len(filenameFromTemplate) != 0 {
|
} else if len(filenameFromTemplate) != 0 {
|
||||||
wwlog.Debug("Writing file %s", filenameFromTemplate[0][1])
|
wwlog.Debug("Writing file %s", filenameFromTemplate[0][1])
|
||||||
if writingToNamedFile && !isLink {
|
if writingToNamedFile && !isLink {
|
||||||
err = CarefulWriteBuffer(outputPath, fileBuffer, backupFile, info.Mode())
|
err = CarefulWriteBuffer(outputPath, fileBuffer, *backupFile, info.Mode())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not write file from template: %w", err)
|
return fmt.Errorf("could not write file from template: %w", err)
|
||||||
}
|
}
|
||||||
@@ -594,7 +1071,7 @@ func BuildOverlayIndir(nodeData node.Node, allNodes []node.Node, overlayNames []
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !isLink {
|
if !isLink {
|
||||||
err = CarefulWriteBuffer(outputPath, fileBuffer, backupFile, info.Mode())
|
err = CarefulWriteBuffer(outputPath, fileBuffer, *backupFile, info.Mode())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not write file from template: %w", err)
|
return fmt.Errorf("could not write file from template: %w", err)
|
||||||
}
|
}
|
||||||
@@ -668,21 +1145,15 @@ func CarefulWriteBuffer(destFile string, buffer bytes.Buffer, backupFile bool, p
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
// getTemplateFuncMap returns a template.FuncMap with all the functions
|
||||||
Parses the template with the given filename, variables must be in data. Returns the
|
// for warewulf templates.
|
||||||
parsed template as bytes.Buffer, and the bool variables for backupFile and writeFile.
|
func getTemplateFuncMap(fileName string, data TemplateStruct) (funcMap template.FuncMap, writeFile, backupFile *bool) {
|
||||||
If something goes wrong an error is returned.
|
|
||||||
*/
|
|
||||||
func RenderTemplateFile(fileName string, data TemplateStruct) (
|
|
||||||
buffer bytes.Buffer,
|
|
||||||
backupFile bool,
|
|
||||||
writeFile bool,
|
|
||||||
err error,
|
|
||||||
) {
|
|
||||||
backupFile = true
|
|
||||||
writeFile = true
|
|
||||||
// Build our FuncMap
|
// Build our FuncMap
|
||||||
funcMap := template.FuncMap{
|
_writeFile := true
|
||||||
|
_backupFile := true
|
||||||
|
writeFile = &_writeFile
|
||||||
|
backupFile = &_backupFile
|
||||||
|
funcMap = template.FuncMap{
|
||||||
"Include": templateFileInclude,
|
"Include": templateFileInclude,
|
||||||
"IncludeFrom": templateImageFileInclude,
|
"IncludeFrom": templateImageFileInclude,
|
||||||
"IncludeBlock": templateFileBlock,
|
"IncludeBlock": templateFileBlock,
|
||||||
@@ -698,12 +1169,12 @@ func RenderTemplateFile(fileName string, data TemplateStruct) (
|
|||||||
},
|
},
|
||||||
"abort": func() string {
|
"abort": func() string {
|
||||||
wwlog.Debug("abort file called in %s", fileName)
|
wwlog.Debug("abort file called in %s", fileName)
|
||||||
writeFile = false
|
*writeFile = false
|
||||||
return ""
|
return ""
|
||||||
},
|
},
|
||||||
"nobackup": func() string {
|
"nobackup": func() string {
|
||||||
wwlog.Debug("not backup for %s", fileName)
|
wwlog.Debug("not backup for %s", fileName)
|
||||||
backupFile = false
|
*backupFile = false
|
||||||
return ""
|
return ""
|
||||||
},
|
},
|
||||||
"UniqueField": UniqueField,
|
"UniqueField": UniqueField,
|
||||||
@@ -715,6 +1186,20 @@ func RenderTemplateFile(fileName string, data TemplateStruct) (
|
|||||||
for key, value := range sprig.TxtFuncMap() {
|
for key, value := range sprig.TxtFuncMap() {
|
||||||
funcMap[key] = value
|
funcMap[key] = value
|
||||||
}
|
}
|
||||||
|
return funcMap, writeFile, backupFile
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Parses the template with the given filename, variables must be in data. Returns the
|
||||||
|
parsed template as bytes.Buffer, and the bool variables for backupFile and writeFile.
|
||||||
|
If something goes wrong an error is returned.
|
||||||
|
*/
|
||||||
|
func RenderTemplateFile(fileName string, data TemplateStruct) (
|
||||||
|
buffer bytes.Buffer, backupFile, writeFile *bool,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
|
||||||
|
funcMap, writeFile, backupFile := getTemplateFuncMap(fileName, data)
|
||||||
|
|
||||||
// Create the template with the merged FuncMap
|
// Create the template with the merged FuncMap
|
||||||
tmpl, err := template.New(path.Base(fileName)).Option("missingkey=default").Funcs(funcMap).ParseGlob(fileName)
|
tmpl, err := template.New(path.Base(fileName)).Option("missingkey=default").Funcs(funcMap).ParseGlob(fileName)
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
network:
|
network:
|
||||||
version: 2
|
version: 2
|
||||||
renderer: networkd
|
renderer: networkd
|
||||||
|
{{- $NetDevs := .NetDevs }}
|
||||||
{{- $ethernets := list }}
|
{{- $ethernets := list }}
|
||||||
{{- $bonds := list }}
|
{{- $bonds := list }}
|
||||||
{{- $NetDevs := .NetDevs }}
|
{{- range $devname, $netdev := $NetDevs }}
|
||||||
{{- range $devname, $netdev := .NetDevs }}
|
|
||||||
{{- if $netdev.Device }}
|
{{- if $netdev.Device }}
|
||||||
{{- if eq (default "ethernet" (lower $netdev.Type)) "ethernet" }}
|
{{- if eq (default "ethernet" (lower $netdev.Type)) "ethernet" }}
|
||||||
{{- $ethernets = append $ethernets $devname }}
|
{{- $ethernets = append $ethernets $devname }}
|
||||||
@@ -16,8 +16,8 @@ network:
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if $ethernets }}
|
{{- if $ethernets }}
|
||||||
ethernets:
|
ethernets:
|
||||||
{{- range $devname := $ethernets }}
|
{{- range $netdev := $NetDevs }}
|
||||||
{{- $netdev := (index $NetDevs $devname) }}
|
{{- if eq (default "ethernet" (lower $netdev.Type)) "ethernet" }}
|
||||||
{{$netdev.Device}}:
|
{{$netdev.Device}}:
|
||||||
dhcp4: no
|
dhcp4: no
|
||||||
optional: true
|
optional: true
|
||||||
@@ -30,10 +30,11 @@ network:
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
{{- if $bonds }}
|
{{- if $bonds }}
|
||||||
bonds:
|
bonds:
|
||||||
{{- range $devname := $bonds }}
|
{{- range $netdev := $NetDevs }}
|
||||||
{{- $netdev := (index $NetDevs $devname) }}
|
{{- if eq (lower $netdev.Type) "bond" }}
|
||||||
{{$netdev.Device}}:
|
{{$netdev.Device}}:
|
||||||
dhcp4: no
|
dhcp4: no
|
||||||
optional: true
|
optional: true
|
||||||
@@ -61,3 +62,4 @@ network:
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|||||||
@@ -25,6 +25,37 @@ within the overlays with ``wwctl overlay list --all``.
|
|||||||
fstab etc/ false
|
fstab etc/ false
|
||||||
fstab etc/fstab.ww false
|
fstab etc/fstab.ww false
|
||||||
|
|
||||||
|
Overlay Variables
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
The command ``wwctl overlay info`` shows the variables used in an overlay
|
||||||
|
template, along with the help text for each variable.
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
# wwctl overlay info NetworkManager etc/NetworkManager/system-connections/ww4-managed.ww
|
||||||
|
VARIABLE HELP TYPE OPTION
|
||||||
|
-------- ---- ---- ------
|
||||||
|
$netdev.Device Set the device for given network string --netdev
|
||||||
|
$netdev.Gateway Set the node's network device gateway IP --gateway
|
||||||
|
$netdev.Hwaddr Set the device's HW address for given network string --hwaddr
|
||||||
|
$netdev.Ipaddr IPv4 address in given network IP --ipaddr
|
||||||
|
$netdev.Ipaddr6 IPv4 address in given network IP --ipaddr
|
||||||
|
$netdev.Ipaddr6 IPv6 address IP --ipaddr6
|
||||||
|
$netdev.MTU Set the mtu string --mtu
|
||||||
|
$netdev.OnBoot.BoolDefaultTrue Enable/disable network device (true/false) WWbool --onboot
|
||||||
|
$netdev.Tags
|
||||||
|
$netdev.Tags.DNSSEARCH
|
||||||
|
$netdev.Tags.downdelay
|
||||||
|
$netdev.Tags.master
|
||||||
|
$netdev.Tags.miimon
|
||||||
|
$netdev.Tags.mode
|
||||||
|
$netdev.Tags.parent_device
|
||||||
|
$netdev.Tags.updelay
|
||||||
|
$netdev.Tags.vlan_id
|
||||||
|
$netdev.Tags.xmit_hash_policy
|
||||||
|
$netdev.Type Set device type of given network string --type
|
||||||
|
|
||||||
Structure
|
Structure
|
||||||
=========
|
=========
|
||||||
|
|
||||||
@@ -163,6 +194,7 @@ mode that they have on the Warewulf server. Use ``wwctl overlay chown`` and
|
|||||||
wwctl overlay chown issue /etc/issue.ww root root
|
wwctl overlay chown issue /etc/issue.ww root root
|
||||||
wwctl overlay chmod issue /etc/issue.ww 0644
|
wwctl overlay chmod issue /etc/issue.ww 0644
|
||||||
|
|
||||||
|
|
||||||
Distribution Overlays
|
Distribution Overlays
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
@@ -373,7 +405,7 @@ provided as an example. In particular, the provided `tstruct.md.ww` demonstrates
|
|||||||
the use of most available template metadata.
|
the use of most available template metadata.
|
||||||
|
|
||||||
.. code-block:: shell
|
.. code-block:: shell
|
||||||
|
|
||||||
wwctl overlay show --render=<nodename> debug tstruct.md.ww
|
wwctl overlay show --render=<nodename> debug tstruct.md.ww
|
||||||
|
|
||||||
.. _localtime:
|
.. _localtime:
|
||||||
|
|||||||
@@ -33,6 +33,28 @@ non-overlay templates as well.
|
|||||||
commands for the ``wwctl power``, ``wwctl sensor``, and ``wwctl console``
|
commands for the ``wwctl power``, ``wwctl sensor``, and ``wwctl console``
|
||||||
commands.
|
commands.
|
||||||
|
|
||||||
|
Template documentation
|
||||||
|
======================
|
||||||
|
|
||||||
|
Templates can include documentation to be included in the output of ``wwctl overlay info``.
|
||||||
|
|
||||||
|
.. code::
|
||||||
|
|
||||||
|
{{/* wwdoc: Your documentation text */}}
|
||||||
|
|
||||||
|
Template variables
|
||||||
|
==================
|
||||||
|
|
||||||
|
Overlay templates have access to a number of variables that provide information
|
||||||
|
about the server configuration, the node being provisioned, and all nodes in the
|
||||||
|
cluster. An example of the variables available, and their use, is included with
|
||||||
|
Warewulf in the ``tstruct.ww`` template of the ``debug`` overlay.
|
||||||
|
|
||||||
|
Variables used in an overlay template can be documented by adding a comment to
|
||||||
|
the template with the form ``{{/* .My.Var: Your help text */}}``. Variable help
|
||||||
|
text defined in a comment replaces that variable's default help text in the
|
||||||
|
output of ``wwctl overlay info``.
|
||||||
|
|
||||||
Template functions
|
Template functions
|
||||||
==================
|
==================
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user