feat(ansible): support local user management
Some checks are pending
CI / test (push) Waiting to run
CI / auto-fix (push) Waiting to run
CI / auto-merge (push) Waiting to run

This commit is contained in:
Virgil 2026-04-03 13:23:29 +00:00
parent 65cd1b9e01
commit dac108cab5
3 changed files with 67 additions and 8 deletions

View file

@ -1432,13 +1432,18 @@ func moduleUserWithClient(_ *Executor, client sshRunner, args map[string]any) (*
name := getStringArg(args, "name", "")
state := getStringArg(args, "state", "present")
appendGroups := getBoolArg(args, "append", false)
local := getBoolArg(args, "local", false)
if name == "" {
return nil, mockError("moduleUserWithClient", "user: name required")
}
if state == "absent" {
cmd := sprintf("userdel -r %s 2>/dev/null || true", name)
delCmd := "userdel"
if local {
delCmd = "luserdel"
}
cmd := sprintf("%s -r %s 2>/dev/null || true", delCmd, name)
_, _, _, _ = client.Run(context.Background(), cmd)
return &TaskResult{Changed: true}, nil
}
@ -1482,12 +1487,18 @@ func moduleUserWithClient(_ *Executor, client sshRunner, args map[string]any) (*
// Try usermod first, then useradd
addOptsStr := joinStrings(addOpts, " ")
modOptsStr := joinStrings(modOpts, " ")
addCmd := "useradd"
modCmd := "usermod"
if local {
addCmd = "luseradd"
modCmd = "lusermod"
}
var cmd string
if addOptsStr == "" {
cmd = sprintf("id %s >/dev/null 2>&1 || useradd %s", name, name)
cmd = sprintf("id %s >/dev/null 2>&1 || %s %s", name, addCmd, name)
} else {
cmd = sprintf("id %s >/dev/null 2>&1 && usermod %s %s || useradd %s %s",
name, modOptsStr, name, addOptsStr, name)
cmd = sprintf("id %s >/dev/null 2>&1 && %s %s %s || %s %s %s",
name, modCmd, modOptsStr, name, addCmd, addOptsStr, name)
}
stdout, stderr, rc, err := client.Run(context.Background(), cmd)

View file

@ -1389,13 +1389,18 @@ func (e *Executor) moduleUser(ctx context.Context, client sshExecutorClient, arg
name := getStringArg(args, "name", "")
state := getStringArg(args, "state", "present")
appendGroups := getBoolArg(args, "append", false)
local := getBoolArg(args, "local", false)
if name == "" {
return nil, coreerr.E("Executor.moduleUser", "name required", nil)
}
if state == "absent" {
cmd := sprintf("userdel -r %s 2>/dev/null || true", name)
delCmd := "userdel"
if local {
delCmd = "luserdel"
}
cmd := sprintf("%s -r %s 2>/dev/null || true", delCmd, name)
_, _, _, _ = client.Run(ctx, cmd)
return &TaskResult{Changed: true}, nil
}
@ -1439,12 +1444,18 @@ func (e *Executor) moduleUser(ctx context.Context, client sshExecutorClient, arg
// Try usermod first, then useradd
addOptsStr := join(" ", addOpts)
modOptsStr := join(" ", modOpts)
addCmd := "useradd"
modCmd := "usermod"
if local {
addCmd = "luseradd"
modCmd = "lusermod"
}
var cmd string
if addOptsStr == "" {
cmd = sprintf("id %s >/dev/null 2>&1 || useradd %s", name, name)
cmd = sprintf("id %s >/dev/null 2>&1 || %s %s", name, addCmd, name)
} else {
cmd = sprintf("id %s >/dev/null 2>&1 && usermod %s %s || useradd %s %s",
name, modOptsStr, name, addOptsStr, name)
cmd = sprintf("id %s >/dev/null 2>&1 && %s %s %s || %s %s %s",
name, modCmd, modOptsStr, name, addCmd, addOptsStr, name)
}
stdout, stderr, rc, err := client.Run(ctx, cmd)

View file

@ -148,6 +148,43 @@ func TestModulesAdv_ModuleUser_Good_NoOptsUsesSimpleForm(t *testing.T) {
assert.False(t, result.Failed)
}
func TestModulesAdv_ModuleUser_Good_LocalModeUsesLocalCommands(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`id localuser >/dev/null 2>&1 && lusermod`, "", "", 0)
result, err := moduleUserWithClient(e, mock, map[string]any{
"name": "localuser",
"local": true,
"shell": "/bin/zsh",
"home": "/var/lib/localuser",
"append": true,
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.True(t, mock.containsSubstring("lusermod"))
assert.True(t, mock.containsSubstring("luseradd"))
assert.True(t, mock.containsSubstring("-s /bin/zsh"))
assert.True(t, mock.containsSubstring("-d /var/lib/localuser"))
}
func TestModulesAdv_ModuleUser_Good_LocalModeRemovesLocalUser(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`luserdel -r localuser`, "", "", 0)
result, err := moduleUserWithClient(e, mock, map[string]any{
"name": "localuser",
"local": true,
"state": "absent",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.True(t, mock.hasExecuted(`luserdel -r localuser`))
}
func TestModulesAdv_ModuleUser_Bad_MissingName(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()