Omit duplicate passwd and group entries in syncuser overlay
Signed-off-by: Jonathon Anderson <janderson@ciq.com>
This commit is contained in:
10
CHANGELOG.md
10
CHANGELOG.md
@@ -11,11 +11,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Added missing hostlist support for `wwctl node` and `wwctl overlay build`. #1635
|
||||
- Added support for comma-separated hostlist patterns. #1635
|
||||
- Added default value for `warewulf.conf:dhcp.template`. #1725
|
||||
- Added `UniqueField` template function. #829
|
||||
|
||||
### Changed
|
||||
|
||||
- Hide internal `wwctl completion` and `wwctl genconfig` commands. #1716
|
||||
- Make .ww suffix optional during `wwctl overlay show --render`. #649
|
||||
- DHCP template generates as much of the subnet and range definition as possible. #1469
|
||||
- Updated overlay flags to `wwctl <node|profile> <add|set> [--runtime-overlays|--system-overlays]`. #1495
|
||||
- syncuser overlay reads host passwd and group database from sysconfdir. #1736
|
||||
- syncuser overlay skips duplicate users and groups in passwd and group databases. #829
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -31,11 +36,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Removed partial support for regex searches in node and profile lists. #1635
|
||||
- Remove redundant `wwctl genconfig completions` command. #1716
|
||||
|
||||
### Changed
|
||||
|
||||
- DHCP template generates as much of the subnet and range definition as possible. #1469
|
||||
- Updated overlay flags to `wwctl <node|profile> <add|set> [--runtime-overlays|--system-overlays]`. #1495
|
||||
|
||||
## v4.6.0rc2, 2025-02-07
|
||||
|
||||
### Added
|
||||
|
||||
@@ -130,3 +130,34 @@ func importSoftlink(lnk string) string {
|
||||
func softlink(target string) string {
|
||||
return fmt.Sprintf("{{ /* softlink \"%s\" */ }}", target)
|
||||
}
|
||||
|
||||
// UniqueField returns a filtered version of a multi-line input string. input is
|
||||
// expected to be a field-separated format with one record per line (terminated
|
||||
// by `\n`). Order of lines is preserved, with the first matching line taking
|
||||
// precedence.
|
||||
//
|
||||
// For example, parsing /etc/passwd filter /etc/passwd for unique user names:
|
||||
//
|
||||
// Lines without the index field (e.g., blank lines) are always included in the
|
||||
// output.
|
||||
//
|
||||
// UniqueField(":", 0, passwdContent)
|
||||
func UniqueField(sep string, index int, input string) string {
|
||||
inputLines := strings.Split(input, "\n")
|
||||
var outputLines []string
|
||||
found := make(map[string]bool)
|
||||
for _, line := range inputLines {
|
||||
inputFields := strings.Split(line, sep)
|
||||
if len(inputFields) > index {
|
||||
field := inputFields[index]
|
||||
if field != "" {
|
||||
if found[field] {
|
||||
continue
|
||||
}
|
||||
found[field] = true
|
||||
}
|
||||
}
|
||||
outputLines = append(outputLines, line)
|
||||
}
|
||||
return strings.Join(outputLines, "\n")
|
||||
}
|
||||
|
||||
@@ -59,3 +59,50 @@ func Test_createIgnitionJson(t *testing.T) {
|
||||
node := nodeInfos[0]
|
||||
assert.JSONEq(t, expected_json, createIgnitionJson(&node))
|
||||
}
|
||||
|
||||
func Test_UniqueField(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
input string
|
||||
sep string
|
||||
field int
|
||||
output string
|
||||
}{
|
||||
"empty": {
|
||||
input: ``,
|
||||
sep: ":",
|
||||
field: 0,
|
||||
output: ``,
|
||||
},
|
||||
|
||||
"unique input": {
|
||||
input: `
|
||||
name1:aaaa
|
||||
name2:bbbb
|
||||
`,
|
||||
sep: ":",
|
||||
field: 0,
|
||||
output: `
|
||||
name1:aaaa
|
||||
name2:bbbb
|
||||
`,
|
||||
},
|
||||
|
||||
"duplicate field": {
|
||||
input: `
|
||||
name1: aaaa
|
||||
name1: bbbb
|
||||
`,
|
||||
sep: ":",
|
||||
field: 0,
|
||||
output: `
|
||||
name1: aaaa
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.output, UniqueField(tt.sep, tt.field, tt.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,6 +500,7 @@ func RenderTemplateFile(fileName string, data TemplateStruct) (
|
||||
backupFile = false
|
||||
return ""
|
||||
},
|
||||
"UniqueField": UniqueField,
|
||||
}
|
||||
|
||||
// Merge sprig.FuncMap with our FuncMap
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
nodes:
|
||||
node1: {}
|
||||
node1:
|
||||
image name: rockylinux-9
|
||||
node2:
|
||||
image name: rockylinux-9
|
||||
tags:
|
||||
PasswordlessRoot: true
|
||||
|
||||
@@ -11,35 +11,61 @@ import (
|
||||
)
|
||||
|
||||
func Test_syncuserOverlay(t *testing.T) {
|
||||
t.Skip("syncuser is not yet isolated from the host")
|
||||
|
||||
env := testenv.New(t)
|
||||
defer env.RemoveAll()
|
||||
env.ImportFile("etc/warewulf/nodes.conf", "nodes.conf")
|
||||
env.ImportFile("var/lib/warewulf/overlays/syncuser/rootfs/etc/passwd.ww", "../../../../../overlays/syncuser/rootfs/etc/passwd.ww")
|
||||
env.ImportFile("var/lib/warewulf/overlays/syncuser/rootfs/etc/group.ww", "../../../../../overlays/syncuser/rootfs/etc/group.ww")
|
||||
env.WriteFile("var/lib/warewulf/chroots/rockylinux-9/rootfs/etc/passwd", `root:x:0:0:root:/root:/bin/bash`)
|
||||
env.WriteFile("var/lib/warewulf/chroots/rockylinux-9/rootfs/etc/group", `root:x:0:`)
|
||||
env.ImportFile("var/lib/warewulf/overlays/syncuser/rootfs/etc/passwd.ww", "../rootfs/etc/passwd.ww")
|
||||
env.ImportFile("var/lib/warewulf/overlays/syncuser/rootfs/etc/group.ww", "../rootfs/etc/group.ww")
|
||||
env.WriteFile("etc/passwd", `
|
||||
root:x:0:0:root:/root:/bin/bash
|
||||
user:x:1000:1000:user:/home/user:/bin/bash
|
||||
`)
|
||||
env.WriteFile("etc/group", `
|
||||
root:x:0:
|
||||
user:x:1000:
|
||||
`)
|
||||
env.WriteFile("var/lib/warewulf/chroots/rockylinux-9/rootfs/etc/passwd", `
|
||||
root:x:0:0:root:/root:/bin/bash
|
||||
`)
|
||||
env.WriteFile("var/lib/warewulf/chroots/rockylinux-9/rootfs/etc/group", `
|
||||
root:x:0:
|
||||
`)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
tests := map[string]struct {
|
||||
args []string
|
||||
log string
|
||||
}{
|
||||
{
|
||||
name: "syncuser:passwd.ww",
|
||||
"syncuser:passwd.ww": {
|
||||
args: []string{"--render", "node1", "syncuser", "etc/passwd.ww"},
|
||||
log: syncuser_passwd,
|
||||
log: `backupFile: true
|
||||
writeFile: true
|
||||
Filename: etc/passwd
|
||||
root:x:0:0:root:/root:/bin/bash
|
||||
user:x:1000:1000:user:/home/user:/bin/bash
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "syncuser:group.ww",
|
||||
"syncuser:passwd.ww (passwordless root)": {
|
||||
args: []string{"--render", "node2", "syncuser", "etc/passwd.ww"},
|
||||
log: `backupFile: true
|
||||
writeFile: true
|
||||
Filename: etc/passwd
|
||||
root::0:0:root:/root:/bin/bash
|
||||
user:x:1000:1000:user:/home/user:/bin/bash
|
||||
`,
|
||||
},
|
||||
"syncuser:group.ww": {
|
||||
args: []string{"--render", "node1", "syncuser", "etc/group.ww"},
|
||||
log: syncuser_group,
|
||||
log: `backupFile: true
|
||||
writeFile: true
|
||||
Filename: etc/group
|
||||
root:x:0:
|
||||
user:x:1000:
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
cmd := show.GetCommand()
|
||||
cmd.SetArgs(tt.args)
|
||||
stdout := bytes.NewBufferString("")
|
||||
@@ -56,17 +82,3 @@ func Test_syncuserOverlay(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const syncuser_passwd string = `backupFile: true
|
||||
writeFile: true
|
||||
Filename: etc/passwd
|
||||
# Uncomment the following line to enable passwordless root login
|
||||
# root::0:0:root:/root:/bin/bash
|
||||
root:x:0:0:root:/root:/bin/bash
|
||||
`
|
||||
|
||||
const syncuser_group string = `backupFile: true
|
||||
writeFile: true
|
||||
Filename: etc/group
|
||||
root:x:0:
|
||||
`
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
{{IncludeFrom $.ImageName "/etc/group"}}
|
||||
{{Include "/etc/group"}}
|
||||
{{
|
||||
printf "%s\n%s"
|
||||
(IncludeFrom $.ImageName "/etc/group" | trim)
|
||||
(Include (printf "%s/%s" .Paths.Sysconfdir "group") | trim)
|
||||
| UniqueField ":" 0 | trim
|
||||
}}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# Uncomment the following line to enable passwordless root login
|
||||
# root::0:0:root:/root:/bin/bash
|
||||
{{IncludeFrom $.ImageName "/etc/passwd"}}
|
||||
{{Include "/etc/passwd"}}
|
||||
{{- $sources := list }}
|
||||
{{- if and .Tags.PasswordlessRoot (eq (lower .Tags.PasswordlessRoot) "true") }}
|
||||
{{- $sources = append $sources "root::0:0:root:/root:/bin/bash" }}
|
||||
{{- end }}
|
||||
{{- $sources = append $sources (IncludeFrom $.ImageName "/etc/passwd" | trim) }}
|
||||
{{- $sources = append $sources (Include (printf "%s/%s" .Paths.Sysconfdir "passwd") | trim) }}
|
||||
{{- join "\n" $sources | UniqueField ":" 0 | trim }}
|
||||
|
||||
@@ -161,6 +161,10 @@ all users on both the Warewulf server and from the image. To function
|
||||
properly, ``wwctl image syncuser`` must have also been run on the image
|
||||
image to synchronize its user and group IDs with those of the server.
|
||||
|
||||
If a ``PasswordlessRoot`` tag is set to "true", the overlay will also insert a
|
||||
"passwordless" root entry. This can be particularly useful for accessing a
|
||||
cluster node when its network interface is not properly configured.
|
||||
|
||||
ignition
|
||||
--------
|
||||
|
||||
|
||||
@@ -135,13 +135,13 @@ The following template will create a file called
|
||||
</interface>
|
||||
{{ end -}}
|
||||
|
||||
Special Commands
|
||||
----------------
|
||||
Special Functions
|
||||
-----------------
|
||||
|
||||
Include
|
||||
^^^^^^^
|
||||
|
||||
A file from the host can be include with following template
|
||||
A file from the host can be included with following template
|
||||
|
||||
.. code-block::
|
||||
|
||||
@@ -228,6 +228,25 @@ readlink
|
||||
|
||||
Evaluates the soft link on the Warewulf server and returns the target.
|
||||
|
||||
UniqueField
|
||||
^^^^^^^^^^^
|
||||
|
||||
UniqueField returns a filtered version of a multi-line input string. input is
|
||||
expected to be a field-separated format with one record per line (terminated by
|
||||
`\n`). Order of lines is preserved, with the first matching line taking
|
||||
precedence.
|
||||
|
||||
For example, the following template snippet has been used in the ``syncuser`` overlay
|
||||
to generate a combined ``/etc/passwd``.
|
||||
|
||||
.. code-block::
|
||||
|
||||
{{
|
||||
printf "%s\n%s"
|
||||
(IncludeFrom $.ImageName "/etc/passwd" | trim)
|
||||
(Include (printf "%s/%s" .Paths.Sysconfdir "passwd") | trim)
|
||||
| UniqueField ":" 0 | trim
|
||||
}}
|
||||
|
||||
Node specific files
|
||||
-------------------
|
||||
|
||||
Reference in New Issue
Block a user