diff --git a/CHANGELOG.md b/CHANGELOG.md index 34419edd..ccec5f09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - New "localtime" overlay to define the system time zone. #1303 - Add support for nested profiles. #1572, #1598 - Adds `wwctl container --build=false` to prevent automatically (re)building the container. #1490, #1489 -- Added resource as global variable for templates +- Added resources as generic, arbitrary YAML data for nodes and profiles. #1568 +- New `fstab` resource configures mounts in fstab overlay, including NFS mounts. #515 ### Changed @@ -102,6 +103,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Remove `wwctl kernel` #1556 - Remove `wwctl --kerneloverride` #1556 - Remove `wwctl container --setdefault` #1335 +- Remove NFS mount options from warewulf.conf. #515 ### Fixed diff --git a/docs/man/man5/warewulf.conf.5 b/docs/man/man5/warewulf.conf.5 index bb22172d..e3b5e918 100644 --- a/docs/man/man5/warewulf.conf.5 +++ b/docs/man/man5/warewulf.conf.5 @@ -209,9 +209,8 @@ Default: true .TP \fBexport paths\fP -A list of paths to be exported by the NFS service and, optionally, to -be automatically mounted on compute nodes. Each item in the list is, -itself, a map of parameters for the mount. (See the \fBEXAMPLE\fP +A list of paths to be exported by the NFS service. Each item in the list is, +itself, a map of parameters for the export. (See the \fBEXAMPLE\fP section for overall structure.) .RS @@ -234,27 +233,6 @@ generating /etc/exports or similar. Default: rw,sync,no_subtree_check .IP -.TP -\fBmount options\fP - -The NFS mount options to use when mounting the given \fBpath\fP on -compute nodes via the NFS service. Provided to the compute node's -overlays as the variable \fB.Nfs.ExportsExtended[].MountOptions\fP, -typically for use in generating /etc/fstab or similar. - -Default: defaults -.IP - -.TP -\fBmount\fP - -If true, mount the NFS share automatically on compute nodes. Provided -to the compute node's overlays as the variable -\fB.Nfs.ExportsExtended[].Mount\fP, typically for use in generating -/etc/fstab or similar. - -Default: true -.IP .RE .IP @@ -297,12 +275,8 @@ nfs: export paths: - path: /home export options: rw,sync - mount options: defaults - mount: true - path: /opt export options: ro,sync,no_root_squash - mount options: defaults - mount: true systemd name: nfs-server .EE diff --git a/etc/warewulf.conf b/etc/warewulf.conf index b0589a91..39e52f23 100644 --- a/etc/warewulf.conf +++ b/etc/warewulf.conf @@ -24,12 +24,8 @@ nfs: export paths: - path: /home export options: rw,sync - mount options: defaults - mount: true - path: /opt export options: ro,sync,no_root_squash - mount options: defaults - mount: false systemd name: nfs-server container mounts: - source: /etc/resolv.conf diff --git a/go.mod b/go.mod index a4f400fd..ba486e98 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 github.com/hashicorp/go-version v1.7.0 github.com/manifoldco/promptui v0.9.0 + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/opencontainers/image-spec v1.1.0 github.com/opencontainers/umoci v0.4.7 github.com/pkg/errors v0.9.1 diff --git a/internal/app/wwctl/container/exec/child/main.go b/internal/app/wwctl/container/exec/child/main.go index 195458a0..7847616d 100644 --- a/internal/app/wwctl/container/exec/child/main.go +++ b/internal/app/wwctl/container/exec/child/main.go @@ -103,7 +103,7 @@ func CobraRunE(cmd *cobra.Command, args []string) (err error) { } overlays := filteredNodes[0].SystemOverlay overlays = append(overlays, filteredNodes[0].RuntimeOverlay...) - err = overlay.BuildOverlayIndir(filteredNodes[0], allNodes, nodeDB.Resources, overlays, path.Join(runDir, "nodeoverlay")) + err = overlay.BuildOverlayIndir(filteredNodes[0], allNodes, overlays, path.Join(runDir, "nodeoverlay")) if err != nil { return fmt.Errorf("could not build overlay: %s", err) } diff --git a/internal/app/wwctl/overlay/build/main.go b/internal/app/wwctl/overlay/build/main.go index 1f898243..2e987de7 100644 --- a/internal/app/wwctl/overlay/build/main.go +++ b/internal/app/wwctl/overlay/build/main.go @@ -57,7 +57,7 @@ func CobraRunE(cmd *cobra.Command, args []string) error { } for _, node := range filteredNodes { - return overlay.BuildOverlayIndir(node, allNodes, nodeDB.Resources, OverlayNames, OverlayDir) + return overlay.BuildOverlayIndir(node, allNodes, OverlayNames, OverlayDir) } } else { return errors.New("must specify a node to build overlay") @@ -68,9 +68,9 @@ func CobraRunE(cmd *cobra.Command, args []string) error { defer syscall.Umask(oldMask) if len(OverlayNames) > 0 { - err = overlay.BuildSpecificOverlays(filteredNodes, allNodes, nodeDB.Resources, OverlayNames, Workers) + err = overlay.BuildSpecificOverlays(filteredNodes, allNodes, OverlayNames, Workers) } else { - err = overlay.BuildAllOverlays(filteredNodes, allNodes, nodeDB.Resources, Workers) + err = overlay.BuildAllOverlays(filteredNodes, allNodes, Workers) } if err != nil { diff --git a/internal/app/wwctl/overlay/imprt/main.go b/internal/app/wwctl/overlay/imprt/main.go index 1bb37d40..f53144f8 100644 --- a/internal/app/wwctl/overlay/imprt/main.go +++ b/internal/app/wwctl/overlay/imprt/main.go @@ -86,7 +86,7 @@ func CobraRunE(cmd *cobra.Command, args []string) (err error) { } } - return overlay.BuildSpecificOverlays(updateNodes, nodes, n.Resources, []string{overlayName}, Workers) + return overlay.BuildSpecificOverlays(updateNodes, nodes, []string{overlayName}, Workers) } return nil diff --git a/internal/app/wwctl/overlay/show/main.go b/internal/app/wwctl/overlay/show/main.go index 07d89d57..533322e7 100644 --- a/internal/app/wwctl/overlay/show/main.go +++ b/internal/app/wwctl/overlay/show/main.go @@ -62,7 +62,7 @@ func CobraRunE(cmd *cobra.Command, args []string) error { if err != nil { return err } - tstruct, err := overlay.InitStruct(overlayName, nodeConf, allNodes, nodeDB.Resources) + tstruct, err := overlay.InitStruct(overlayName, nodeConf, allNodes) if err != nil { return err } diff --git a/internal/app/wwctl/overlay/show/main_test.go b/internal/app/wwctl/overlay/show/main_test.go index 88d88065..34d84db5 100644 --- a/internal/app/wwctl/overlay/show/main_test.go +++ b/internal/app/wwctl/overlay/show/main_test.go @@ -46,12 +46,8 @@ nfs: export paths: - path: /home export options: rw,sync - mount options: defaults - mount: true - path: /opt - export options: ro,sync,no_root_squash - mount options: defaults - mount: false`) + export options: ro,sync,no_root_squash`) env.WriteFile("etc/warewulf/nodes.conf", `nodeprofiles: diff --git a/internal/app/wwctl/resource/add/main.go b/internal/app/wwctl/resource/add/main.go deleted file mode 100644 index b5cc0214..00000000 --- a/internal/app/wwctl/resource/add/main.go +++ /dev/null @@ -1,31 +0,0 @@ -package add - -import ( - "fmt" - - "github.com/warewulf/warewulf/internal/pkg/node" - - "github.com/spf13/cobra" -) - -func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err error) { - return func(cmd *cobra.Command, args []string) (err error) { - nodeYml, err := node.New() - if err != nil { - return fmt.Errorf("failed to load node configuration: %s", err) - } - if ok := nodeYml.Resources[args[0]] != nil; ok { - return fmt.Errorf("resource %s already exists", args[0]) - } - if nodeYml.Resources == nil { - nodeYml.Resources = make(map[string]node.Resource) - } - res := node.Resource{} - for key, val := range vars.tags { - res[key] = val - } - nodeYml.Resources[args[0]] = res - err = nodeYml.Persist() - return err - } -} diff --git a/internal/app/wwctl/resource/add/main_test.go b/internal/app/wwctl/resource/add/main_test.go deleted file mode 100644 index ef36f95f..00000000 --- a/internal/app/wwctl/resource/add/main_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package add - -import ( - "bytes" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/warewulf/warewulf/internal/pkg/testenv" - "github.com/warewulf/warewulf/internal/pkg/warewulfd" -) - -func Test_resource_add(t *testing.T) { - tests := []struct { - name string - args []string - wantErr bool - stdout string - inDB string - outDb string - }{{ - name: "add resource", - args: []string{"--restagadd", "foo=baar", "test"}, - wantErr: false, - stdout: "", - inDB: `nodeprofiles: {} -nodes: {} -`, - outDb: `nodeprofiles: {} -nodes: {} -resources: - test: - foo: baar -`}} - env := testenv.New(t) - defer env.RemoveAll() - warewulfd.SetNoDaemon() - for _, tt := range tests { - env.WriteFile("etc/warewulf/nodes.conf", tt.inDB) - t.Run(tt.name, func(t *testing.T) { - baseCmd := GetCommand() - baseCmd.SetArgs(tt.args) - buf := new(bytes.Buffer) - baseCmd.SetOut(buf) - baseCmd.SetErr(buf) - err := baseCmd.Execute() - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, buf.String(), tt.stdout) - content := env.ReadFile("etc/warewulf/nodes.conf") - assert.YAMLEq(t, tt.outDb, content) - } - }) - - } -} diff --git a/internal/app/wwctl/resource/add/root.go b/internal/app/wwctl/resource/add/root.go deleted file mode 100644 index 96489b04..00000000 --- a/internal/app/wwctl/resource/add/root.go +++ /dev/null @@ -1,25 +0,0 @@ -package add - -import ( - "github.com/spf13/cobra" -) - -type variables struct { - tags map[string]string -} - -// GetRootCommand returns the root cobra.Command for the application. -func GetCommand() *cobra.Command { - vars := variables{} - baseCmd := &cobra.Command{ - DisableFlagsInUseLine: true, - Use: "add PROFILE", - Short: "Add a new node profile", - Long: "This command adds a new named PROFILE.", - Aliases: []string{"new", "create"}, - RunE: CobraRunE(&vars), - Args: cobra.ExactArgs(1), - } - baseCmd.PersistentFlags().StringToStringVar(&vars.tags, "restagadd", map[string]string{}, "add cluster wide resource tags") - return baseCmd -} diff --git a/internal/app/wwctl/resource/delete/main.go b/internal/app/wwctl/resource/delete/main.go deleted file mode 100644 index d7f98597..00000000 --- a/internal/app/wwctl/resource/delete/main.go +++ /dev/null @@ -1,25 +0,0 @@ -package delete - -import ( - "fmt" - - "github.com/warewulf/warewulf/internal/pkg/node" - - "github.com/spf13/cobra" -) - -func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err error) { - return func(cmd *cobra.Command, args []string) (err error) { - nodeYml, err := node.New() - if err != nil { - return fmt.Errorf("failed to load node configuration: %s", err) - } - for _, res := range args { - if _, ok := nodeYml.Resources[res]; !ok { - return fmt.Errorf("resource %s does not exist", res) - } - delete(nodeYml.Resources, res) - } - return nodeYml.Persist() - } -} diff --git a/internal/app/wwctl/resource/delete/main_test.go b/internal/app/wwctl/resource/delete/main_test.go deleted file mode 100644 index f5343810..00000000 --- a/internal/app/wwctl/resource/delete/main_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package delete - -import ( - "bytes" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/warewulf/warewulf/internal/pkg/testenv" - "github.com/warewulf/warewulf/internal/pkg/warewulfd" -) - -func Test_resource_set(t *testing.T) { - tests := []struct { - name string - wantErr bool - args []string - stdout string - inDB string - outDb string - }{ - { - name: "delete resource", - wantErr: false, - stdout: "", - args: []string{"test1"}, - inDB: `nodeprofiles: {} -nodes: {} -resources: - test1: {} - test2: {} -`, - - outDb: `nodeprofiles: {} -nodes: {} -resources: - test2: {} -`}, - } - env := testenv.New(t) - defer env.RemoveAll() - warewulfd.SetNoDaemon() - for _, tt := range tests { - env.WriteFile("etc/warewulf/nodes.conf", tt.inDB) - t.Run(tt.name, func(t *testing.T) { - baseCmd := GetCommand() - baseCmd.SetArgs(tt.args) - buf := new(bytes.Buffer) - baseCmd.SetOut(buf) - baseCmd.SetErr(buf) - err := baseCmd.Execute() - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.stdout, buf.String()) - content := env.ReadFile("etc/warewulf/nodes.conf") - assert.YAMLEq(t, tt.outDb, content) - } - }) - - } -} diff --git a/internal/app/wwctl/resource/delete/root.go b/internal/app/wwctl/resource/delete/root.go deleted file mode 100644 index 90da6866..00000000 --- a/internal/app/wwctl/resource/delete/root.go +++ /dev/null @@ -1,31 +0,0 @@ -package delete - -import ( - "github.com/spf13/cobra" - "github.com/warewulf/warewulf/internal/pkg/node" -) - -type variables struct { -} - -// GetRootCommand returns the root cobra.Command for the application. -func GetCommand() *cobra.Command { - vars := variables{} - baseCmd := &cobra.Command{ - DisableFlagsInUseLine: true, - Use: "list [RESOURCE]", - Short: "list resource", - Long: "This list all values the named RESOURCE. If no resource is named a list of the resources is printed.", - Aliases: []string{"show", "ls"}, - RunE: CobraRunE(&vars), - Args: cobra.MaximumNArgs(1), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) != 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - nodeDB, _ := node.New() - return nodeDB.ListAllResources(), cobra.ShellCompDirectiveNoFileComp - }, - } - return baseCmd -} diff --git a/internal/app/wwctl/resource/list/main.go b/internal/app/wwctl/resource/list/main.go deleted file mode 100644 index 71265db3..00000000 --- a/internal/app/wwctl/resource/list/main.go +++ /dev/null @@ -1,44 +0,0 @@ -package list - -import ( - "fmt" - - "github.com/warewulf/warewulf/internal/app/wwctl/table" - "github.com/warewulf/warewulf/internal/pkg/node" - "github.com/warewulf/warewulf/internal/pkg/wwlog" - - "github.com/spf13/cobra" -) - -func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err error) { - return func(cmd *cobra.Command, args []string) (err error) { - nodeYml, err := node.New() - if err != nil { - return fmt.Errorf("failed to load node configuration: %s", err) - } - var resList []string - if len(args) == 1 { - resList = []string{args[0]} - } else { - resList = nodeYml.ListAllResources() - } - for _, resname := range resList { - res, err := nodeYml.GetResource(resname) - if err != nil { - return err - } - if !vars.all { - wwlog.Info("%s", resname) - } else { - t := table.New(cmd.OutOrStdout()) - t.AddHeader(resname, "key", "value") - for key, val := range res { - t.AddLine(resname, key, val) - } - t.Print() - - } - } - return nil - } -} diff --git a/internal/app/wwctl/resource/list/main_test.go b/internal/app/wwctl/resource/list/main_test.go deleted file mode 100644 index 07e9125f..00000000 --- a/internal/app/wwctl/resource/list/main_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package list - -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_resource_set(t *testing.T) { - tests := []struct { - name string - args []string - wantErr bool - stdout string - inDB string - }{ - { - name: "list resource", - args: []string{}, - wantErr: false, - stdout: `test1 -test2 -`, - inDB: `nodeprofiles: {} -nodes: {} -resources: - test1: {} - test2: {} -`, - }, - { - name: "list a existing resource", - args: []string{"-a"}, - wantErr: false, - stdout: `test key value ----- --- ----- -test foo baar -`, - inDB: `nodeprofiles: {} -nodes: {} -resources: - test: - foo: baar -`, - }, - } - env := testenv.New(t) - defer env.RemoveAll() - warewulfd.SetNoDaemon() - for _, tt := range tests { - env.WriteFile("etc/warewulf/nodes.conf", tt.inDB) - t.Run(tt.name, func(t *testing.T) { - baseCmd := GetCommand() - baseCmd.SetArgs(tt.args) - buf := new(bytes.Buffer) - baseCmd.SetOut(buf) - baseCmd.SetErr(buf) - wwlog.SetLogWriter(buf) - err := baseCmd.Execute() - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.stdout, buf.String()) - } - }) - - } -} diff --git a/internal/app/wwctl/resource/list/root.go b/internal/app/wwctl/resource/list/root.go deleted file mode 100644 index 550d4aec..00000000 --- a/internal/app/wwctl/resource/list/root.go +++ /dev/null @@ -1,33 +0,0 @@ -package list - -import ( - "github.com/spf13/cobra" - "github.com/warewulf/warewulf/internal/pkg/node" -) - -type variables struct { - all bool -} - -// GetRootCommand returns the root cobra.Command for the application. -func GetCommand() *cobra.Command { - vars := variables{} - baseCmd := &cobra.Command{ - DisableFlagsInUseLine: true, - Use: "list [RESOURCE]", - Short: "list resource", - Long: "This list all values the named RESOURCE. If no resource is named a list of the resources is printed.", - Aliases: []string{"show", "ls"}, - RunE: CobraRunE(&vars), - Args: cobra.MaximumNArgs(1), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) != 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - nodeDB, _ := node.New() - return nodeDB.ListAllResources(), cobra.ShellCompDirectiveNoFileComp - }, - } - baseCmd.PersistentFlags().BoolVarP(&vars.all, "all", "a", false, "Show all resources") - return baseCmd -} diff --git a/internal/app/wwctl/resource/root.go b/internal/app/wwctl/resource/root.go deleted file mode 100644 index d74a61f6..00000000 --- a/internal/app/wwctl/resource/root.go +++ /dev/null @@ -1,30 +0,0 @@ -package resource - -import ( - "github.com/spf13/cobra" - "github.com/warewulf/warewulf/internal/app/wwctl/resource/add" - "github.com/warewulf/warewulf/internal/app/wwctl/resource/delete" - "github.com/warewulf/warewulf/internal/app/wwctl/resource/list" - "github.com/warewulf/warewulf/internal/app/wwctl/resource/set" -) - -var ( - baseCmd = &cobra.Command{ - DisableFlagsInUseLine: true, - Use: "resource COMMAND [OPTIONS]", - Short: "manage resources", - Long: "Management of global resources", - } -) - -func init() { - baseCmd.AddCommand(list.GetCommand()) - baseCmd.AddCommand(set.GetCommand()) - baseCmd.AddCommand(add.GetCommand()) - baseCmd.AddCommand(delete.GetCommand()) -} - -// GetRootCommand returns the root cobra.Command for the application. -func GetCommand() *cobra.Command { - return baseCmd -} diff --git a/internal/app/wwctl/resource/set/main.go b/internal/app/wwctl/resource/set/main.go deleted file mode 100644 index f9d2fd9a..00000000 --- a/internal/app/wwctl/resource/set/main.go +++ /dev/null @@ -1,31 +0,0 @@ -package set - -import ( - "fmt" - - "github.com/warewulf/warewulf/internal/pkg/node" - - "github.com/spf13/cobra" -) - -func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err error) { - return func(cmd *cobra.Command, args []string) (err error) { - nodeYml, err := node.New() - if err != nil { - return fmt.Errorf("failed to load node configuration: %s", err) - } - if ok := nodeYml.Resources[args[0]] != nil; ok { - if nodeYml.Resources == nil { - nodeYml.Resources = make(map[string]node.Resource) - } - res := nodeYml.Resources[args[0]] - for key, val := range vars.tags { - res[key] = val - } - err = nodeYml.Persist() - return err - } else { - return fmt.Errorf("resource %s does not exist", args[0]) - } - } -} diff --git a/internal/app/wwctl/resource/set/main_test.go b/internal/app/wwctl/resource/set/main_test.go deleted file mode 100644 index 733ab744..00000000 --- a/internal/app/wwctl/resource/set/main_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package set - -import ( - "bytes" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/warewulf/warewulf/internal/pkg/testenv" - "github.com/warewulf/warewulf/internal/pkg/warewulfd" -) - -func Test_resource_set(t *testing.T) { - tests := []struct { - name string - args []string - wantErr bool - stdout string - inDB string - outDb string - }{ - { - name: "set resource", - args: []string{"--tagset", "foo=test", "test"}, - wantErr: false, - stdout: "", - inDB: `nodeprofiles: {} -nodes: {} -resources: - test: - foo: baar -`, - outDb: `nodeprofiles: {} -nodes: {} -resources: - test: - foo: test -`}, - { - name: "set non exiting resource", - args: []string{"--tagset", "foo=test", "none"}, - wantErr: true, - stdout: "", - inDB: `nodeprofiles: {} -nodes: {} -resources: - test: - foo: baar -`, - outDb: ``}, - } - env := testenv.New(t) - defer env.RemoveAll() - warewulfd.SetNoDaemon() - for _, tt := range tests { - env.WriteFile("etc/warewulf/nodes.conf", tt.inDB) - t.Run(tt.name, func(t *testing.T) { - baseCmd := GetCommand() - baseCmd.SetArgs(tt.args) - buf := new(bytes.Buffer) - baseCmd.SetOut(buf) - baseCmd.SetErr(buf) - err := baseCmd.Execute() - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.stdout, buf.String()) - content := env.ReadFile("etc/warewulf/nodes.conf") - assert.YAMLEq(t, tt.outDb, content) - } - }) - - } -} diff --git a/internal/app/wwctl/resource/set/root.go b/internal/app/wwctl/resource/set/root.go deleted file mode 100644 index 0d15928a..00000000 --- a/internal/app/wwctl/resource/set/root.go +++ /dev/null @@ -1,33 +0,0 @@ -package set - -import ( - "github.com/spf13/cobra" - "github.com/warewulf/warewulf/internal/pkg/node" -) - -type variables struct { - tags map[string]string -} - -// GetRootCommand returns the root cobra.Command for the application. -func GetCommand() *cobra.Command { - vars := variables{} - baseCmd := &cobra.Command{ - DisableFlagsInUseLine: true, - Use: "set RESOURCE", - Short: "change a resource", - Long: "This command changes the named RESOURCE. Use UNDEF to remove a tag.", - Aliases: []string{"modify", "change"}, - RunE: CobraRunE(&vars), - Args: cobra.ExactArgs(1), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) != 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - nodeDB, _ := node.New() - return nodeDB.ListAllResources(), cobra.ShellCompDirectiveNoFileComp - }, - } - baseCmd.PersistentFlags().StringToStringVar(&vars.tags, "tagset", map[string]string{}, "set cluster wide resource tags") - return baseCmd -} diff --git a/internal/app/wwctl/upgrade/nodes/cobra.go b/internal/app/wwctl/upgrade/nodes/cobra.go index 7980e3e2..bf97455b 100644 --- a/internal/app/wwctl/upgrade/nodes/cobra.go +++ b/internal/app/wwctl/upgrade/nodes/cobra.go @@ -16,6 +16,7 @@ var ( replaceOverlays bool inputPath string outputPath string + inputConfPath string ) func GetCommand() *cobra.Command { @@ -31,6 +32,7 @@ supported by the current version.`, command.Flags().BoolVar(&replaceOverlays, "replace-overlays", false, "Replace 'wwinit' and 'generic' overlays with their split replacements") command.Flags().StringVarP(&inputPath, "input-path", "i", "", "Path to a legacy nodes.conf") command.Flags().StringVarP(&outputPath, "output-path", "o", "", "Path to write the upgraded nodes.conf to") + command.Flags().StringVar(&inputConfPath, "with-warewulfconf", config.ConfigFile, "Path to a legacy warewulf.conf") if err := command.MarkFlagRequired("add-defaults"); err != nil { panic(err) } @@ -49,6 +51,16 @@ func UpgradeNodesConf(cmd *cobra.Command, args []string) error { if outputPath == "" { outputPath = config.Get().Paths.NodesConf() } + + confData, err := os.ReadFile(inputConfPath) + if err != nil { + return err + } + warewulfConf, err := upgrade.ParseConfig(confData) + if err != nil { + return err + } + data, err := os.ReadFile(inputPath) if err != nil { return err @@ -57,7 +69,7 @@ func UpgradeNodesConf(cmd *cobra.Command, args []string) error { if err != nil { return err } - upgraded := legacy.Upgrade(addDefaults, replaceOverlays) + upgraded := legacy.Upgrade(addDefaults, replaceOverlays, warewulfConf) if outputPath == "-" { upgradedYaml, err := upgraded.Dump() if err != nil { diff --git a/internal/pkg/config/nfs.go b/internal/pkg/config/nfs.go index 061eb199..97babbc2 100644 --- a/internal/pkg/config/nfs.go +++ b/internal/pkg/config/nfs.go @@ -21,12 +21,6 @@ func (this NFSConf) Enabled() bool { type NFSExportConf struct { Path string `yaml:"path" default:"/dev/null"` ExportOptions string `yaml:"export options,omitempty" default:"rw,sync,no_subtree_check"` - MountOptions string `yaml:"mount options,omitempty" default:"defaults"` - MountP *bool `yaml:"mount,omitempty" default:"true"` -} - -func (this NFSExportConf) Mount() bool { - return BoolP(this.MountP) } // Implements the Unmarshal interface for NFSConf to set default diff --git a/internal/pkg/config/root_test.go b/internal/pkg/config/root_test.go index 9b466b29..0c0e8e86 100644 --- a/internal/pkg/config/root_test.go +++ b/internal/pkg/config/root_test.go @@ -94,12 +94,8 @@ nfs: export paths: - path: /home export options: rw,sync - mount options: defaults - mount: true - path: /opt export options: ro,sync,no_root_squash - mount options: defaults - mount: false systemd name: nfs-server container mounts: - source: /etc/resolv.conf @@ -138,12 +134,8 @@ container mounts: 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, "defaults", conf.NFS.ExportsExtended[0].MountOptions) - assert.True(t, conf.NFS.ExportsExtended[0].Mount()) 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, "defaults", conf.NFS.ExportsExtended[1].MountOptions) - assert.False(t, conf.NFS.ExportsExtended[1].Mount()) assert.Equal(t, "nfs-server", conf.NFS.SystemdName) assert.Equal(t, "/etc/resolv.conf", conf.MountsContainer[0].Source) diff --git a/internal/pkg/configure/hostfile.go b/internal/pkg/configure/hostfile.go index 42f4fda4..e8bf1759 100644 --- a/internal/pkg/configure/hostfile.go +++ b/internal/pkg/configure/hostfile.go @@ -31,7 +31,7 @@ func Hostfile() (err error) { } hostname, _ := os.Hostname() - tstruct, err := overlay.InitStruct(overlay_.Name(), node.NewNode(hostname), allNodes, nil) + tstruct, err := overlay.InitStruct(overlay_.Name(), node.NewNode(hostname), allNodes) if err != nil { return err } diff --git a/internal/pkg/node/constructors.go b/internal/pkg/node/constructors.go index 8f938d9b..0de328be 100644 --- a/internal/pkg/node/constructors.go +++ b/internal/pkg/node/constructors.go @@ -183,18 +183,6 @@ func (config *NodesYaml) ListAllProfiles() []string { return nodeList } -/* -Return the names of all available remote resources -*/ -func (config *NodesYaml) ListAllResources() []string { - var resList []string - for name := range config.Resources { - resList = append(resList, name) - } - sort.Strings(resList) - return resList -} - /* FindDiscoverableNode returns the first discoverable node and an interface to associate with the discovered interface. If the nodUNDEFe has @@ -223,13 +211,3 @@ func (config *NodesYaml) FindDiscoverableNode() (Node, string, error) { return EmptyNode(), "", ErrNoUnconfigured } - -/* -get the given resource -*/ -func (config *NodesYaml) GetResource(id string) (res Resource, err error) { - if found, ok := config.Resources[id]; ok { - return found, nil - } - return res, ErrNotFound -} diff --git a/internal/pkg/node/constructors_test.go b/internal/pkg/node/constructors_test.go index 5b2e29c8..5bdcd501 100644 --- a/internal/pkg/node/constructors_test.go +++ b/internal/pkg/node/constructors_test.go @@ -17,13 +17,14 @@ nodeprofiles: override: device: ib0 type: profile + resources: + fstab: + - file: /home nodes: test_node1: network devices: net0: device: eth0 - resources: - - NFSHOME test_node2: primary network: net1 network devices: @@ -53,9 +54,6 @@ nodes: network devices: override: device: ib1 -resources: - NFSHOME: - mountpoint: /home ` var ret NodesYaml err := yaml.Unmarshal([]byte(data), &ret) @@ -122,12 +120,6 @@ func Test_Primary_Network(t *testing.T) { assert.Equal(t, "ib1", test_node6.NetDevs["override"].Device) assert.Equal(t, "profile", test_node6.NetDevs["override"].Type) }) - t.Run("resource is defined", func(t *testing.T) { - assert.Contains(t, test_node1.Resources, "NFSHOME") - res, err := c.GetResource(test_node1.Resources[0]) - assert.NoError(t, err) - assert.Contains(t, res, "mountpoint") - }) } var findDiscoverableNodeTests = []struct { diff --git a/internal/pkg/node/datastructure.go b/internal/pkg/node/datastructure.go index 47d66b2f..912e3583 100644 --- a/internal/pkg/node/datastructure.go +++ b/internal/pkg/node/datastructure.go @@ -17,7 +17,6 @@ Structure of which goes to disk type NodesYaml struct { NodeProfiles map[string]*Profile `yaml:"nodeprofiles"` Nodes map[string]*Node `yaml:"nodes"` - Resources map[string]Resource `yaml:"resources,omitempty"` } /* @@ -54,7 +53,7 @@ type Profile struct { PrimaryNetDev string `yaml:"primary network,omitempty" lopt:"primarynet" sopt:"p" comment:"Set the primary network interface"` Disks map[string]*Disk `yaml:"disks,omitempty"` FileSystems map[string]*FileSystem `yaml:"filesystems,omitempty"` - Resources []string `yaml:"resources,omitempty" lopt:"resources" comment:"Set the resources available to the node or profile"` + Resources map[string]Resource `yaml:"resources,omitempty"` } type IpmiConf struct { @@ -127,4 +126,4 @@ type FileSystem struct { MountOptions string `yaml:"mount_options,omitempty" comment:"any special options to be passed to the mount command"` } -type Resource map[string]string +type Resource interface{} diff --git a/internal/pkg/node/fields_test.go b/internal/pkg/node/fields_test.go index eaad50ef..8010d7a9 100644 --- a/internal/pkg/node/fields_test.go +++ b/internal/pkg/node/fields_test.go @@ -103,6 +103,9 @@ func Test_listFields(t *testing.T) { }, }, }, + Resources: map[string]Resource{ + "resource": "resvalue", + }, }, }, fields: []string{ @@ -142,7 +145,7 @@ func Test_listFields(t *testing.T) { "NetDevs[default].Tags[nettag]", "Tags[tag]", "PrimaryNetDev", - "Resources", + "Resources[resource]", }, }, "profile": { @@ -157,6 +160,9 @@ func Test_listFields(t *testing.T) { }, }, }, + Resources: map[string]Resource{ + "resource": "resvalue", + }, }, fields: []string{ "Profiles", @@ -193,7 +199,7 @@ func Test_listFields(t *testing.T) { "NetDevs[default].Tags[nettag]", "Tags[tag]", "PrimaryNetDev", - "Resources", + "Resources[resource]", }, }, } diff --git a/internal/pkg/node/flags.go b/internal/pkg/node/flags.go index 1146f3ed..7ea41956 100644 --- a/internal/pkg/node/flags.go +++ b/internal/pkg/node/flags.go @@ -49,38 +49,47 @@ func (add *NodeConfAdd) CreateAddFlags(baseCmd *cobra.Command) { } func recursiveCreateFlags(obj interface{}, baseCmd *cobra.Command) { - // now iterate of every field - nodeInfoType := reflect.TypeOf(obj) - nodeInfoVal := reflect.ValueOf(obj) - for i := 0; i < nodeInfoVal.Elem().NumField(); i++ { - if !nodeInfoType.Elem().Field(i).IsExported() { + elemType := reflect.TypeOf(obj).Elem() + elemVal := reflect.ValueOf(obj).Elem() + + for i := 0; i < elemVal.NumField(); i++ { + field := elemType.Field(i) + fieldVal := elemVal.Field(i) + + if !field.IsExported() { continue } - if nodeInfoType.Elem().Field(i).Tag.Get("comment") != "" { - field := nodeInfoVal.Elem().Field(i) - createFlags(baseCmd, nodeInfoType.Elem().Field(i), &field) - } else if nodeInfoType.Elem().Field(i).Type.Kind() == reflect.Ptr { - recursiveCreateFlags(nodeInfoVal.Elem().Field(i).Interface(), baseCmd) + if field.Tag.Get("comment") != "" { + createFlags(baseCmd, field, &fieldVal) - } else if nodeInfoType.Elem().Field(i).Type.Kind() == reflect.Map && - nodeInfoType.Elem().Field(i).Type != reflect.TypeOf(map[string]string{}) { - // add a map with key UNDEF so that it can hold values N.B. UNDEF can never be added through command line - key := reflect.ValueOf("UNDEF") - if nodeInfoVal.Elem().Field(i).Len() == 0 { - if nodeInfoVal.Elem().Field(i).IsNil() { - nodeInfoVal.Elem().Field(i).Set(reflect.MakeMap(nodeInfoType.Elem().Field(i).Type)) + } else if field.Anonymous { + recursiveCreateFlags(fieldVal.Addr().Interface(), baseCmd) + + } else if field.Type.Kind() == reflect.Ptr { + recursiveCreateFlags(fieldVal.Interface(), baseCmd) + + } else if field.Type.Kind() == reflect.Struct { + recursiveCreateFlags(fieldVal.Addr().Interface(), baseCmd) + + } else if field.Type.Kind() == reflect.Map { + switch field.Type.Elem().Kind() { + case reflect.String, reflect.Interface: + continue + case reflect.Pointer, reflect.Slice, reflect.Map: + // add a map with key UNDEF so that it can hold values N.B. UNDEF can never be added through command line + key := reflect.ValueOf("UNDEF") + if fieldVal.Len() == 0 { + if fieldVal.IsNil() { + fieldVal.Set(reflect.MakeMap(field.Type)) + } + newPtr := reflect.New(field.Type.Elem().Elem()) + fieldVal.SetMapIndex(key, newPtr) + } else { + key = fieldVal.MapKeys()[0] } - newPtr := reflect.New(nodeInfoType.Elem().Field(i).Type.Elem().Elem()) - nodeInfoVal.Elem().Field(i).SetMapIndex(key, newPtr) - } else { - key = nodeInfoVal.Elem().Field(i).MapKeys()[0] + recursiveCreateFlags(fieldVal.MapIndex(key).Interface(), baseCmd) } - recursiveCreateFlags(nodeInfoVal.Elem().Field(i).MapIndex(key).Interface(), baseCmd) - } else if nodeInfoType.Elem().Field(i).Anonymous { - recursiveCreateFlags(nodeInfoVal.Elem().Field(i).Addr().Interface(), baseCmd) - } else if nodeInfoType.Elem().Field(i).Type.Kind() == reflect.Struct { - recursiveCreateFlags(nodeInfoVal.Elem().Field(i).Addr().Interface(), baseCmd) } } } diff --git a/internal/pkg/node/mergo.go b/internal/pkg/node/mergo.go index 1b6be75c..cd96b537 100644 --- a/internal/pkg/node/mergo.go +++ b/internal/pkg/node/mergo.go @@ -1,43 +1,16 @@ package node import ( - "bytes" - "encoding/gob" "reflect" "strings" "dario.cat/mergo" + "github.com/mohae/deepcopy" "github.com/warewulf/warewulf/internal/pkg/util" "github.com/warewulf/warewulf/internal/pkg/wwlog" ) -// copyProfile creates a deep copy of the given Profile object. -// It uses encoding/gob to serialize and deserialize the Profile, ensuring -// that all nested fields are copied. -// -// Parameters: -// - original: The Profile object to be copied. -// -// Returns: -// - A new Profile object that is a deep copy of the input. -// - An error if serialization or deserialization fails. -func copyProfile(original Profile) (Profile, error) { - var buf bytes.Buffer - enc := gob.NewEncoder(&buf) - dec := gob.NewDecoder(&buf) - profile := Profile{} - if err := enc.Encode(original); err != nil { - return profile, err - } else { - if err := dec.Decode(&profile); err != nil { - return profile, err - } else { - return profile, nil - } - } -} - // getNodeProfiles retrieves a list of profile IDs associated with a specific node ID. // It retrives nested profiles and ensures the list is cleaned of duplicates // and negations (denoted with a '~' prefix). @@ -85,6 +58,33 @@ func (config *NodesYaml) appendProfileProfiles(profiles []string, id string) []s return profiles } +type InterfaceTransformer struct{} + +func (t InterfaceTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error { + if typ.Kind() == reflect.Interface { + return func(dst, src reflect.Value) error { + if !src.IsValid() || src.IsZero() { + return nil + } + + // Handle merging of concrete values + switch src.Interface().(type) { + case map[string]interface{}: + if dst.IsNil() { + dst.Set(reflect.New(src.Elem().Type()).Elem()) + } + return mergo.Merge(dst.Interface(), src.Interface(), mergo.WithAppendSlice, mergo.WithOverride, mergo.WithTransformers(t)) + case []interface{}: + dst.Set(src) + default: + dst.Set(src) + } + return nil + } + } + return nil +} + // MergeNode merges the configuration of a node identified by `id` with all the profiles // associated with it, producing a fully composed `Node` and a `fieldMap` detailing the // sources of various configuration fields. @@ -121,11 +121,9 @@ func (config *NodesYaml) MergeNode(id string) (node Node, fields fieldMap, err e if profile, err := config.GetProfile(profileID); err != nil { wwlog.Warn("profile not found: %s", profileID) continue - } else if profile, err := copyProfile(profile); err != nil { - wwlog.Warn("error processing profile %s: %v", profileID, err) - continue } else { - if err = mergo.Merge(&node.Profile, profile, mergo.WithAppendSlice, mergo.WithOverride); err != nil { + profile := deepcopy.Copy(profile) + if err = mergo.Merge(&node.Profile, profile, mergo.WithAppendSlice, mergo.WithOverride, mergo.WithTransformers(InterfaceTransformer{})); err != nil { return node, fields, err } for _, fieldName := range listFields(profile) { @@ -150,8 +148,15 @@ func (config *NodesYaml) MergeNode(id string) (node Node, fields fieldMap, err e if value, err := getNestedFieldValue(originalNode, fieldName); err == nil && valueStr(value) != "" { source := "" prevSource := fields.Source(fieldName) - if value.Kind() == reflect.Slice && prevSource != "" { - source = strings.Join([]string{prevSource, id}, ",") + if prevSource != "" { + switch value.Kind() { + case reflect.Slice: + source = strings.Join([]string{prevSource, id}, ",") + case reflect.Interface: + if _, ok := value.Interface().([]interface{}); ok { + source = strings.Join([]string{prevSource, id}, ",") + } + } } if value, err := getNestedFieldString(node, fieldName); err == nil { fields.Set(fieldName, source, value) diff --git a/internal/pkg/node/mergo_test.go b/internal/pkg/node/mergo_test.go index f991e181..d79e8776 100644 --- a/internal/pkg/node/mergo_test.go +++ b/internal/pkg/node/mergo_test.go @@ -758,6 +758,30 @@ nodeprofiles: "p2 netdev tag", }, }, + "resources": { + nodesConf: ` +nodeprofiles: + p1: + resources: + fstab: + - spec: warewulf:/home + file: /home + vfstype: nfs +nodes: + n1: + profiles: + - p1 + resources: + fstab: + - spec: warewulf:/opt + file: /opt + vfstype: nfs +`, + nodes: []string{"n1"}, + fields: []string{"Resources[fstab]"}, + sources: []string{"p1,n1"}, + values: []string{"[map[file:/home spec:warewulf:/home vfstype:nfs] map[file:/opt spec:warewulf:/opt vfstype:nfs]]"}, + }, } for name, tt := range tests { @@ -781,13 +805,13 @@ nodeprofiles: } var nodes []Node - for i, _ := range tt.nodes { + for i := range tt.nodes { node, _, mergeErr := registry.MergeNode(tt.nodes[i]) assert.NoError(t, mergeErr) nodes = append(nodes, node) } - for i, _ := range tt.nodes { + for i := range tt.nodes { _, fields, _ := registry.MergeNode(tt.nodes[i]) value, valueErr := getNestedFieldString(nodes[i], tt.fields[i]) assert.NoError(t, valueErr) diff --git a/internal/pkg/node/methods.go b/internal/pkg/node/methods.go index 9d1f54ac..2483f825 100644 --- a/internal/pkg/node/methods.go +++ b/internal/pkg/node/methods.go @@ -154,13 +154,16 @@ func recursiveFlatten(obj interface{}) (hasContent bool) { case reflect.Map: mapIter := valObj.Elem().Field(i).MapRange() for mapIter.Next() { - if mapIter.Value().Kind() == reflect.String { - if mapIter.Value().String() != "" { - hasContent = true + switch mapIter.Value().Kind() { + case reflect.Map, reflect.Pointer, reflect.Slice: + if mapIter.Value().Type().Elem().Kind() == reflect.Struct { + ret := recursiveFlatten(mapIter.Value().Interface()) + hasContent = ret || hasContent + } else { + hasContent = !mapIter.Value().IsZero() || hasContent } - } else { - ret := recursiveFlatten(mapIter.Value().Interface()) - hasContent = ret || hasContent + default: + hasContent = !mapIter.Value().IsZero() || hasContent } } diff --git a/internal/pkg/overlay/datastructure.go b/internal/pkg/overlay/datastructure.go index 2c19d068..471a6899 100644 --- a/internal/pkg/overlay/datastructure.go +++ b/internal/pkg/overlay/datastructure.go @@ -37,7 +37,6 @@ type TemplateStruct struct { Tftp warewulfconf.TFTPConf Paths warewulfconf.BuildConfig AllNodes []node.Node - Resources map[string]node.Resource node.Node // backward compatiblity Container string @@ -47,7 +46,7 @@ type TemplateStruct struct { /* Initialize an TemplateStruct with the given node.NodeInfo */ -func InitStruct(overlayName string, nodeData node.Node, allNodes []node.Node, resources map[string]node.Resource) (TemplateStruct, error) { +func InitStruct(overlayName string, nodeData node.Node, allNodes []node.Node) (TemplateStruct, error) { var tstruct TemplateStruct tstruct.Overlay = overlayName hostname, _ := os.Hostname() @@ -82,7 +81,6 @@ func InitStruct(overlayName string, nodeData node.Node, allNodes []node.Node, re tstruct.BuildTime = dt.Format("01-02-2006 15:04:05 MST") tstruct.BuildTimeUnix = strconv.FormatInt(dt.Unix(), 10) // tstruct.Node.Tags = map[string]string{} - tstruct.Resources = resources var buf bytes.Buffer enc := gob.NewEncoder(&buf) dec := gob.NewDecoder(&buf) diff --git a/internal/pkg/overlay/overlay.go b/internal/pkg/overlay/overlay.go index e18033ac..e0eba792 100644 --- a/internal/pkg/overlay/overlay.go +++ b/internal/pkg/overlay/overlay.go @@ -101,7 +101,7 @@ func (this Overlay) IsDistributionOverlay() bool { return path.Dir(this.Path()) == config.Get().Paths.DistributionOverlaydir() } -func BuildAllOverlays(nodes []node.Node, allNodes []node.Node, resources map[string]node.Resource, workerCount int) error { +func BuildAllOverlays(nodes []node.Node, allNodes []node.Node, workerCount int) error { nodeChan := make(chan node.Node, len(nodes)) errChan := make(chan error, len(nodes)*2) @@ -109,12 +109,12 @@ func BuildAllOverlays(nodes []node.Node, allNodes []node.Node, resources map[str worker := func() { for n := range nodeChan { wwlog.Info("Building system overlays for %s: [%s]", n.Id(), strings.Join(n.SystemOverlay, ", ")) - if err := BuildOverlay(n, allNodes, resources, "system", n.SystemOverlay); err != nil { + if err := BuildOverlay(n, allNodes, "system", n.SystemOverlay); err != nil { errChan <- fmt.Errorf("could not build system overlays %v for node %s: %w", n.SystemOverlay, n.Id(), err) } wwlog.Info("Building runtime overlays for %s: [%s]", n.Id(), strings.Join(n.RuntimeOverlay, ", ")) - if err := BuildOverlay(n, allNodes, resources, "runtime", n.RuntimeOverlay); err != nil { + if err := BuildOverlay(n, allNodes, "runtime", n.RuntimeOverlay); err != nil { errChan <- fmt.Errorf("could not build runtime overlays %v for node %s: %w", n.RuntimeOverlay, n.Id(), err) } } @@ -139,7 +139,7 @@ func BuildAllOverlays(nodes []node.Node, allNodes []node.Node, resources map[str return nil } -func BuildSpecificOverlays(nodes []node.Node, allNodes []node.Node, resources map[string]node.Resource, overlayNames []string, workerCount int) error { +func BuildSpecificOverlays(nodes []node.Node, allNodes []node.Node, overlayNames []string, workerCount int) error { nodeChan := make(chan node.Node, len(nodes)) errChan := make(chan error, len(nodes)) @@ -148,7 +148,7 @@ func BuildSpecificOverlays(nodes []node.Node, allNodes []node.Node, resources ma for n := range nodeChan { wwlog.Info("Building overlay for %s: %v", n, overlayNames) for _, overlayName := range overlayNames { - err := BuildOverlay(n, allNodes, resources, "", []string{overlayName}) + err := BuildOverlay(n, allNodes, "", []string{overlayName}) if err != nil { errChan <- fmt.Errorf("could not build overlay %s for node %s: %w", overlayName, n.Id(), err) } @@ -199,7 +199,7 @@ func BuildHostOverlay() error { if err != nil { return err } - return BuildOverlayIndir(hostData, allNodes, registry.Resources, []string{"host"}, "/") + return BuildOverlayIndir(hostData, allNodes, []string{"host"}, "/") } /* @@ -229,7 +229,7 @@ func FindOverlays() (overlayList []string, err error) { /* Build the given overlays for a node and create a Image for them */ -func BuildOverlay(nodeConf node.Node, allNodes []node.Node, resources map[string]node.Resource, context string, overlayNames []string) error { +func BuildOverlay(nodeConf node.Node, allNodes []node.Node, context string, overlayNames []string) error { if len(overlayNames) == 0 && context == "" { return nil } @@ -254,7 +254,7 @@ func BuildOverlay(nodeConf node.Node, allNodes []node.Node, resources map[string wwlog.Debug("Created temporary directory for %s: %s", name, buildDir) - err = BuildOverlayIndir(nodeConf, allNodes, resources, overlayNames, buildDir) + err = BuildOverlayIndir(nodeConf, allNodes, overlayNames, buildDir) if err != nil { return fmt.Errorf("failed to generate files for %s: %w", name, err) } @@ -283,7 +283,7 @@ func init() { } // Build the given overlays for a node in the given directory. -func BuildOverlayIndir(nodeData node.Node, allNodes []node.Node, resources map[string]node.Resource, overlayNames []string, outputDir string) error { +func BuildOverlayIndir(nodeData node.Node, allNodes []node.Node, overlayNames []string, outputDir string) error { if len(overlayNames) == 0 { return nil } @@ -332,7 +332,7 @@ func BuildOverlayIndir(nodeData node.Node, allNodes []node.Node, resources map[s } else if filepath.Ext(walkPath) == ".ww" { originalOutputPath := outputPath outputPath := strings.TrimSuffix(outputPath, ".ww") - tstruct, err := InitStruct(overlayName, nodeData, allNodes, resources) + tstruct, err := InitStruct(overlayName, nodeData, allNodes) if err != nil { return fmt.Errorf("failed to initial data for %s: %w", nodeData.Id(), err) } diff --git a/internal/pkg/overlay/overlay_test.go b/internal/pkg/overlay/overlay_test.go index 761e20ec..845ba11f 100644 --- a/internal/pkg/overlay/overlay_test.go +++ b/internal/pkg/overlay/overlay_test.go @@ -229,7 +229,7 @@ T3 env.MkdirAll(tt.outputDir) - assert.NoError(t, BuildOverlayIndir(tt.node, []node.Node{tt.node}, nil, tt.overlays, env.GetPath(tt.outputDir))) + assert.NoError(t, BuildOverlayIndir(tt.node, []node.Node{tt.node}, tt.overlays, env.GetPath(tt.outputDir))) dirFiles := tt.outputDirs for outputFile, _ := range tt.outputFiles { dirFiles = append(dirFiles, outputFile) @@ -400,7 +400,7 @@ func Test_BuildOverlay(t *testing.T) { for _, tt := range tests { nodeInfo := node.NewNode(tt.nodeName) t.Run(tt.description, func(t *testing.T) { - err := BuildOverlay(nodeInfo, []node.Node{nodeInfo}, nil, tt.context, tt.overlays) + err := BuildOverlay(nodeInfo, []node.Node{nodeInfo}, tt.context, tt.overlays) assert.NoError(t, err) if tt.image != "" { image := env.GetPath(path.Join("srv/warewulf/overlays", tt.image)) @@ -523,7 +523,7 @@ func Test_BuildAllOverlays(t *testing.T) { } nodes = append(nodes, nodeInfo) } - err := BuildAllOverlays(nodes, nodes, nil, runtime.NumCPU()) + err := BuildAllOverlays(nodes, nodes, runtime.NumCPU()) assert.NoError(t, err) if tt.createdOverlays == nil { dirName := path.Join(provisionDir, "overlays") @@ -615,7 +615,7 @@ func Test_BuildSpecificOverlays(t *testing.T) { nodeInfo := node.NewNode(nodeName) nodes = append(nodes, nodeInfo) } - err := BuildSpecificOverlays(nodes, nodes, nil, tt.overlays, runtime.NumCPU()) + err := BuildSpecificOverlays(nodes, nodes, tt.overlays, runtime.NumCPU()) if !tt.succeed { assert.Error(t, err) } else { diff --git a/internal/pkg/upgrade/config.go b/internal/pkg/upgrade/config.go index b14f7640..3d9c75e4 100644 --- a/internal/pkg/upgrade/config.go +++ b/internal/pkg/upgrade/config.go @@ -4,6 +4,7 @@ import ( "gopkg.in/yaml.v3" "github.com/warewulf/warewulf/internal/pkg/config" + "github.com/warewulf/warewulf/internal/pkg/wwlog" ) func ParseConfig(data []byte) (warewulfYaml *WarewulfYaml, err error) { @@ -175,8 +176,9 @@ func (this *NFSExportConf) Upgrade() (upgraded *config.NFSExportConf) { upgraded = new(config.NFSExportConf) upgraded.Path = this.Path upgraded.ExportOptions = this.ExportOptions - upgraded.MountOptions = this.MountOptions - upgraded.MountP = this.Mount + if *(this.Mount) { + wwlog.Warn("Legacy mount configured for NFS export %s: use `wwctl upgrade nodes --with-warewulfconf=` to port to nodes.conf", this.Path) + } return upgraded } diff --git a/internal/pkg/upgrade/config_test.go b/internal/pkg/upgrade/config_test.go index 9bdd58f2..c0de5c30 100644 --- a/internal/pkg/upgrade/config_test.go +++ b/internal/pkg/upgrade/config_test.go @@ -231,12 +231,8 @@ nfs: export paths: - path: /home export options: rw,sync - mount options: defaults - mount: true - path: /opt export options: ro,sync,no_root_squash - mount options: defaults - mount: false systemd name: nfs-server `, }, @@ -299,12 +295,8 @@ nfs: export paths: - path: /home export options: rw,sync - mount options: defaults - mount: true - path: /opt export options: ro,sync,no_root_squash - mount options: defaults - mount: false systemd name: nfs-server `, }, @@ -387,12 +379,8 @@ nfs: export paths: - path: /home export options: rw,sync - mount options: defaults - mount: true - path: /opt export options: ro,sync,no_root_squash - mount options: defaults - mount: false systemd name: nfs-server ssh: key types: @@ -485,12 +473,8 @@ nfs: export paths: - path: /home export options: rw,sync - mount options: defaults - mount: true - path: /opt export options: ro,sync,no_root_squash - mount options: defaults - mount: false systemd name: nfs-server ssh: key types: diff --git a/internal/pkg/upgrade/node.go b/internal/pkg/upgrade/node.go index 7a940aa8..ba52561d 100644 --- a/internal/pkg/upgrade/node.go +++ b/internal/pkg/upgrade/node.go @@ -1,6 +1,7 @@ package upgrade import ( + "fmt" "net" "strconv" "strings" @@ -50,7 +51,7 @@ type NodesYaml struct { Nodes map[string]*Node } -func (this *NodesYaml) Upgrade(addDefaults bool, replaceOverlays bool) (upgraded *node.NodesYaml) { +func (this *NodesYaml) Upgrade(addDefaults bool, replaceOverlays bool, warewulfconf *WarewulfYaml) (upgraded *node.NodesYaml) { upgraded = new(node.NodesYaml) upgraded.NodeProfiles = make(map[string]*node.Profile) upgraded.Nodes = make(map[string]*node.Node) @@ -93,6 +94,50 @@ func (this *NodesYaml) Upgrade(addDefaults bool, replaceOverlays bool) (upgraded defaultProfile.Ipxe = "default" } } + if warewulfconf != nil && warewulfconf.NFS != nil { + var fstab []map[string]string + for _, export := range warewulfconf.NFS.Exports { + fmt.Printf("PORTING EXPORT: %s\n", export) + fstab = append(fstab, map[string]string{ + "spec": fmt.Sprintf("warewulf:%s", export), + "file": export, + "vfstype": "nfs", + }) + } + for _, export := range warewulfconf.NFS.ExportsExtended { + if export.Mount != nil && *(export.Mount) { + entry := map[string]string{ + "spec": fmt.Sprintf("warewulf:%s", export.Path), + "file": export.Path, + "vfstype": "nfs", + } + if export.MountOptions != "" { + entry["mntops"] = export.MountOptions + } + fstab = append(fstab, entry) + } + } + fmt.Printf("FSTAB: %+v\n", fstab) + if len(fstab) > 0 { + if _, ok := upgraded.NodeProfiles["default"]; !ok { + upgraded.NodeProfiles["default"] = new(node.Profile) + } + if upgraded.NodeProfiles["default"].Resources == nil { + upgraded.NodeProfiles["default"].Resources = make(map[string]node.Resource) + } + if _, ok := upgraded.NodeProfiles["default"].Resources["fstab"]; ok { + if prevFstab, ok := (upgraded.NodeProfiles["default"].Resources["fstab"]).([]map[string]string); ok { + newFstab := append(prevFstab, fstab...) + upgraded.NodeProfiles["default"].Resources["fstab"] = newFstab + } else { + wwlog.Warn("Unable to port NFS mounts from warewulf.conf: incompatible existing fstab resource in default profile") + } + } else { + upgraded.NodeProfiles["default"].Resources["fstab"] = fstab + } + fmt.Printf("RECORDED FSTAB: %+v\n", upgraded.NodeProfiles["default"].Resources["fstab"]) + } + } return upgraded } diff --git a/internal/pkg/upgrade/node_test.go b/internal/pkg/upgrade/node_test.go index 38dd900d..74fce843 100644 --- a/internal/pkg/upgrade/node_test.go +++ b/internal/pkg/upgrade/node_test.go @@ -1,7 +1,6 @@ package upgrade import ( - "strings" "testing" "github.com/stretchr/testify/assert" @@ -16,6 +15,7 @@ var nodesYamlUpgradeTests = []struct { files map[string]string legacyYaml string upgradedYaml string + warewulfConf string }{ { name: "captured vers42 example", @@ -767,6 +767,64 @@ nodeprofiles: - p2 p2: {} nodes: {} +`, + }, + { + name: "Legacy export mounts", + legacyYaml: ` +nodeprofiles: + default: {} +`, + upgradedYaml: ` +nodeprofiles: + default: + resources: + fstab: + - spec: warewulf:/home + file: /home + vfstype: nfs + - spec: warewulf:/opt + file: /opt + vfstype: nfs +nodes: {} +`, + warewulfConf: ` +nfs: + exports: + - /home + - /opt +`, + }, + { + name: "Legacy extended export mounts", + legacyYaml: ` +nodeprofiles: + default: {} +`, + upgradedYaml: ` +nodeprofiles: + default: + resources: + fstab: + - spec: warewulf:/home + file: /home + vfstype: nfs + - spec: warewulf:/opt + file: /opt + mntops: defaults,ro + vfstype: nfs +nodes: {} +`, + warewulfConf: ` +nfs: + export paths: + - path: /home + mount: true + - path: /opt + mount options: defaults,ro + mount: true + - path: /var + mount: false `, }, } @@ -781,12 +839,14 @@ func Test_UpgradeNodesYaml(t *testing.T) { env.WriteFile(fileName, content) } } + conf, err := ParseConfig([]byte(tt.warewulfConf)) + assert.NoError(t, err) legacy, err := ParseNodes([]byte(tt.legacyYaml)) assert.NoError(t, err) - upgraded := legacy.Upgrade(tt.addDefaults, tt.replaceOverlays) + upgraded := legacy.Upgrade(tt.addDefaults, tt.replaceOverlays, conf) upgradedYaml, err := upgraded.Dump() assert.NoError(t, err) - assert.Equal(t, strings.TrimSpace(tt.upgradedYaml), strings.TrimSpace(string(upgradedYaml))) + assert.YAMLEq(t, tt.upgradedYaml, string(upgradedYaml)) }) } } diff --git a/internal/pkg/warewulfd/util.go b/internal/pkg/warewulfd/util.go index 234edbf3..919ee05e 100644 --- a/internal/pkg/warewulfd/util.go +++ b/internal/pkg/warewulfd/util.go @@ -72,9 +72,9 @@ func getOverlayFile(n node.Node, context string, stage_overlays []string, autobu return "", err } if len(stage_overlays) > 0 { - err = overlay.BuildSpecificOverlays([]node.Node{n}, allNodes, registry.Resources, stage_overlays, 1) + err = overlay.BuildSpecificOverlays([]node.Node{n}, allNodes, stage_overlays, 1) } else { - err = overlay.BuildAllOverlays([]node.Node{n}, allNodes, registry.Resources, 1) + err = overlay.BuildAllOverlays([]node.Node{n}, allNodes, 1) } if err != nil { wwlog.Error("Failed to build overlay: %s, %s, %s\n%s", diff --git a/overlays/fstab/internal/fstab_test.go b/overlays/fstab/internal/fstab_test.go index 7403e197..35050e62 100644 --- a/overlays/fstab/internal/fstab_test.go +++ b/overlays/fstab/internal/fstab_test.go @@ -63,6 +63,5 @@ proc /proc proc defaults 0 0 # all with noauto as mounts happens with systemd units /dev/disk/by-partlabel/scratch /scratch btrfs noauto,defaults 0 0 /dev/disk/by-partlabel/swap swap swap noauto,defaults 0 0 -# Resource: NFSHOME -192.168.0.1:/home /home nfs defaults 0 0 +warewulf:/home /home nfs defaults 0 0 ` diff --git a/overlays/fstab/internal/nodes.conf b/overlays/fstab/internal/nodes.conf index e6dbb33d..5be426a9 100644 --- a/overlays/fstab/internal/nodes.conf +++ b/overlays/fstab/internal/nodes.conf @@ -1,7 +1,9 @@ nodes: node1: resources: - - NFSHOME + fstab: + - spec: warewulf:/home + file: /home disks: /dev/vda: wipe_table: true @@ -19,10 +21,3 @@ nodes: /dev/disk/by-partlabel/swap: format: swap path: swap -resources: - NFSHOME: - mountpoint: /home - moptions: defaults - eoptions: rw,rootsquash - epath: /home - server: controller diff --git a/overlays/fstab/internal/warewulf.conf b/overlays/fstab/internal/warewulf.conf index 7c5f020c..fec04483 100644 --- a/overlays/fstab/internal/warewulf.conf +++ b/overlays/fstab/internal/warewulf.conf @@ -19,9 +19,5 @@ nfs: export paths: - path: /home export options: rw,sync - mount options: defaults - mount: true - path: /opt export options: ro,sync,no_root_squash - mount options: defaults - mount: false diff --git a/overlays/fstab/rootfs/etc/fstab.ww b/overlays/fstab/rootfs/etc/fstab.ww index 93462977..6473a206 100644 --- a/overlays/fstab/rootfs/etc/fstab.ww +++ b/overlays/fstab/rootfs/etc/fstab.ww @@ -12,11 +12,7 @@ proc /proc proc defaults 0 0 {{- if $fs.MountOptions }} noauto{{range $index,$opt := $fs.MountOptions }},{{ $opt }}{{ end }} 0 0 {{- else }} noauto,defaults 0 0 {{- end }}{{ end }}{{ end }} -{{- $server := .Ipaddr}}{{/* export the needed values */}} -{{- range $i,$ResName := .Node.Resources }} -{{- if eq (substr 0 3 $ResName) "NFS" }}{{ $nfs := (index $.Resources $ResName)}} -# Resource: {{ $ResName }} -{{- if not (eq $nfs.server "controller") }}{{ $server = $nfs.server }}{{ end }} -{{ $server }}:{{ $nfs.epath}} {{ $nfs.mountpoint }} nfs {{ $nfs.moptions }} 0 0 -{{- end }} -{{- end }} +{{- with $fstab := index .Resources "fstab" }} +{{- range $entry := $fstab }} +{{ index $entry "spec" }} {{ index $entry "file" }} {{ default "nfs" (index $entry "vfstype") }} {{ default "defaults" (index $entry "mntops") }} {{ default 0 (index $entry "freq") }} {{ default 0 (index $entry "passno") }} +{{- end }}{{ end }} diff --git a/overlays/host/internal/host_test.go b/overlays/host/internal/host_test.go index 4faa14a9..ef4f4b08 100644 --- a/overlays/host/internal/host_test.go +++ b/overlays/host/internal/host_test.go @@ -58,7 +58,7 @@ func Test_hostOverlay(t *testing.T) { conf: "", args: []string{"--render", "host", "host", "etc/exports.ww"}, log: host_exports, - header: "# Do not edit after this line", + header: "", }, { name: "host:/etc/hosts", @@ -278,10 +278,11 @@ 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 -# Do not edit after this line -# This block is autogenerated by warewulf -# Resource: NFSHOME + +# 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) + ` const host_hosts string = `backupFile: true diff --git a/overlays/host/internal/nodes.conf b/overlays/host/internal/nodes.conf index 3734cbdc..3b286313 100644 --- a/overlays/host/internal/nodes.conf +++ b/overlays/host/internal/nodes.conf @@ -15,10 +15,3 @@ nodes: device: wwnet0 hwaddr: e6:92:39:49:7b:04 ipaddr: 192.168.3.23 -resources: - NFSHOME: - mountpoint: /home - moptions: defaults - eoptions: rw,sync - epath: /home - server: controller diff --git a/overlays/host/internal/warewulf.conf b/overlays/host/internal/warewulf.conf index 7c5f020c..fec04483 100644 --- a/overlays/host/internal/warewulf.conf +++ b/overlays/host/internal/warewulf.conf @@ -19,9 +19,5 @@ nfs: export paths: - path: /home export options: rw,sync - mount options: defaults - mount: true - path: /opt export options: ro,sync,no_root_squash - mount options: defaults - mount: false diff --git a/overlays/host/internal/warewulf.conf-static b/overlays/host/internal/warewulf.conf-static index cd4b744d..b0eadadd 100644 --- a/overlays/host/internal/warewulf.conf-static +++ b/overlays/host/internal/warewulf.conf-static @@ -18,9 +18,5 @@ nfs: export paths: - path: /home export options: rw,sync - mount options: defaults - mount: true - path: /opt export options: ro,sync,no_root_squash - mount options: defaults - mount: false diff --git a/overlays/host/rootfs/etc/exports.ww b/overlays/host/rootfs/etc/exports.ww index 749f0248..7507c37f 100644 --- a/overlays/host/rootfs/etc/exports.ww +++ b/overlays/host/rootfs/etc/exports.ww @@ -1,10 +1,11 @@ -{{ IncludeBlock "/etc/exports" "# Do not edit after this line" }} -# This block is autogenerated by warewulf +{{- if .Nfs.Enabled }} +# This file is autogenerated by warewulf {{- $network := .Network }} {{- $netmask := .Netmask }} -{{- range $ResName,$Res := .Resources }} -{{- if eq (substr 0 3 $ResName) "NFS" }}{{ if eq $Res.server "controller" }} -# Resource: {{ $ResName }} -{{ $Res.epath}} {{ $network }}/{{ $netmask }}({{ $Res.eoptions }}) -{{- end }}{{ end }} +{{- range .Nfs.ExportsExtended }} +{{ .Path }} {{ $network }}/{{ $netmask }}({{ .ExportOptions }}) {{- end }} +{{- else }} +{{ abort }} +{{- end }} + diff --git a/overlays/hosts/internal/warewulf.conf b/overlays/hosts/internal/warewulf.conf index 7c5f020c..fec04483 100644 --- a/overlays/hosts/internal/warewulf.conf +++ b/overlays/hosts/internal/warewulf.conf @@ -19,9 +19,5 @@ nfs: export paths: - path: /home export options: rw,sync - mount options: defaults - mount: true - path: /opt export options: ro,sync,no_root_squash - mount options: defaults - mount: false diff --git a/overlays/wwinit/internal/warewulf.conf b/overlays/wwinit/internal/warewulf.conf index 7c5f020c..fec04483 100644 --- a/overlays/wwinit/internal/warewulf.conf +++ b/overlays/wwinit/internal/warewulf.conf @@ -19,9 +19,5 @@ nfs: export paths: - path: /home export options: rw,sync - mount options: defaults - mount: true - path: /opt export options: ro,sync,no_root_squash - mount options: defaults - mount: false diff --git a/overlays/wwinit/internal/wwinit_test.go b/overlays/wwinit/internal/wwinit_test.go index 4842c2b9..56bd1e09 100644 --- a/overlays/wwinit/internal/wwinit_test.go +++ b/overlays/wwinit/internal/wwinit_test.go @@ -78,12 +78,8 @@ nfs: export paths: - path: /home export options: rw,sync - mount options: defaults - mount: true - path: /opt export options: ro,sync,no_root_squash - mount options: defaults - mount: false ` const wwinit_config string = `backupFile: true diff --git a/userdocs/contents/configuration.rst b/userdocs/contents/configuration.rst index c6a4ded1..1cc2c897 100644 --- a/userdocs/contents/configuration.rst +++ b/userdocs/contents/configuration.rst @@ -37,12 +37,8 @@ Warewulf (4.5.8): export paths: - path: /home export options: rw,sync - mount options: defaults - mount: true - path: /opt export options: ro,sync,no_root_squash - mount options: defaults - mount: false systemd name: nfs-server container mounts: - source: /etc/resolv.conf @@ -267,4 +263,4 @@ Restart the ``nftables`` service: .. code-block:: console - systemctl restart nftables \ No newline at end of file + systemctl restart nftables diff --git a/userdocs/contents/nodeconfig.rst b/userdocs/contents/nodeconfig.rst index b25f94cb..3e17cc6c 100644 --- a/userdocs/contents/nodeconfig.rst +++ b/userdocs/contents/nodeconfig.rst @@ -302,3 +302,31 @@ And to unset this configuration attribute: # wwctl node list -a n001 | grep Cluster n001 Cluster -- -- + +Resources +========= + +Warewulf nodes (and profiles) support generic "resources" that may hold arbitrarily complex YAML +data. This data, along with tags, may be used by both distribution and site overlays. + +.. code-block:: yaml + + nodeprofiles: + default: + resources: + fstab: + - spec: warewulf:/home + file: /home + vfstype: nfs + mntops: defaults + freq: 0 + passno: 0 + - spec: warewulf:/opt + file: /opt + vfstype: nfs + mntops: defaults,ro + freq: 0 + passno: 0 + +Due to the arbitrary nature of generic resource data, it can only be managed with `wwctl + edit`. diff --git a/userdocs/contents/overlays.rst b/userdocs/contents/overlays.rst index 0327dc9c..3373daa6 100644 --- a/userdocs/contents/overlays.rst +++ b/userdocs/contents/overlays.rst @@ -102,8 +102,21 @@ interface configuration should handle this dynamically.) fstab ----- -The **fstab** overlay configures ``/etc/fstab`` to mount NFS shares defined in -``/etc/warewulf.conf`` and file systems created by Ignition. +The **fstab** overlay configures ``/etc/fstab`` based on the data provided in the "fstab" +resource. It also creates entries for file systems defined by Ignition. + +.. code-block:: yaml + + nodeprofiles: + default: + resources: + fstab: + - spec: warewulf:/home + file: /home + vfstype: nfs + - spec: warewulf:/opt + file: /opt + vfstype: nfs ssh --- diff --git a/userdocs/contributing/development-environment-vagrant.rst b/userdocs/contributing/development-environment-vagrant.rst index cbfab328..ea3f98ef 100644 --- a/userdocs/contributing/development-environment-vagrant.rst +++ b/userdocs/contributing/development-environment-vagrant.rst @@ -213,12 +213,8 @@ Vagrantfile export paths: - path: /home export options: rw,sync - mount options: defaults - mount: true - path: /opt export options: ro,sync,no_root_squash - mount options: defaults - mount: false systemd name: nfs-server CONF diff --git a/userdocs/quickstart/debian12.rst b/userdocs/quickstart/debian12.rst index f2692fa4..abb5c698 100644 --- a/userdocs/quickstart/debian12.rst +++ b/userdocs/quickstart/debian12.rst @@ -88,12 +88,8 @@ address of your cluster's private network interface: export paths: - path: /home export options: rw,sync - mount options: defaults - mount: true - path: /opt export options: ro,sync,no_root_squash - mount options: defaults - mount: false systemd name: nfs-server .. note:: diff --git a/userdocs/quickstart/el.rst b/userdocs/quickstart/el.rst index e41de7b8..40718fe5 100644 --- a/userdocs/quickstart/el.rst +++ b/userdocs/quickstart/el.rst @@ -90,12 +90,8 @@ address of your cluster's private network interface. export paths: - path: /home export options: rw,sync - mount options: defaults - mount: true - path: /opt export options: ro,sync,no_root_squash - mount options: defaults - mount: false systemd name: nfs-server container mounts: - source: /etc/resolv.conf diff --git a/userdocs/quickstart/suse15.rst b/userdocs/quickstart/suse15.rst index 355f36c7..ac963010 100644 --- a/userdocs/quickstart/suse15.rst +++ b/userdocs/quickstart/suse15.rst @@ -69,12 +69,8 @@ address of your cluster's private network interface: export paths: - path: /home export options: rw,sync - mount options: defaults - mount: true - path: /opt export options: ro,sync,no_root_squash - mount options: defaults - mount: false systemd name: nfs-server container mounts: - source: /etc/resolv.conf