pfexec become plugin: fix broken defaults for illumos/SmartOS#11623
pfexec become plugin: fix broken defaults for illumos/SmartOS#11623LuminousMonkey wants to merge 5 commits intoansible-collections:mainfrom
Conversation
The pfexec become plugin has had incorrect defaults since it was migrated from Ansible core, making it unusable on illumos without manual workarounds: 1. become_flags defaulted to '-H -S -n' which are sudo flags. pfexec does not accept any of these options, causing: 'exec: illegal option -- H' 2. wrap_exe defaulted to false. Unlike sudo, pfexec does not interpret shell constructs internally. Since Ansible generates compound commands (echo BECOME-SUCCESS-xxx ; python3), these must be wrapped in /bin/sh -c for pfexec to execute them. These issues were originally reported in 2016 (ansible/ansible#15642), migrated to community.general as ansible-collections#3671, and partially fixed by PR ansible-collections#3889 in 2022 (which corrected quoting but not the defaults). Users have had to work around this with explicit inventory settings ever since. Changes: - become_flags default: '-H -S -n' -> '' (empty) - wrap_exe default: false -> true - build_become_command: handle empty flags cleanly - Updated tests to match corrected defaults - Added test for custom flags - Improved wrap_exe description to explain why it should be enabled
|
cc @None |
|
Thanks for your contribution! This one is tricky. Changing defaults usually requires a longer deprecation, since they are breaking changes.
In any case, the plugin is usable in its current form - but you have to set two configuration options (namely the ones whose default is changed by this PR). |
|
(The integration test failures are unrelated, but the extra sanity test failures need to be fixed.) |
|
Sorry about that, I admit that I don't actually touch any Python, so it was just a formatting thing for that sanity check? I was just using Ansible for working with our SmartOS server, and got a bit of a surprise using the pfexec component. I think I've sorted the sanity check now? |
|
changes lgtm, the flags are legacy from when become plugins shared global defaults |
I think so, since everything's green now :) |
|
Very tricky indeed. About the About the |
russoz
left a comment
There was a problem hiding this comment.
Just a couple of comments on docs and code.
plugins/become/pfexec.py
Outdated
| - Toggle to wrap the command C(pfexec) calls in C(shell -c) or not. | ||
| - Unlike C(sudo), C(pfexec) does not interpret shell constructs internally, | ||
| so commands containing shell operators must be wrapped in a shell invocation. | ||
| - This should generally be left enabled. |
There was a problem hiding this comment.
This comment is kinda redundant by the fact that the default is now true.
plugins/become/pfexec.py
Outdated
| if flags: | ||
| return f"{exe} {flags} {become_cmd}" | ||
| return f"{exe} {become_cmd}" |
There was a problem hiding this comment.
I see no point in the conditional: become_flags defaults to "", so this could be made simply:
| if flags: | |
| return f"{exe} {flags} {become_cmd}" | |
| return f"{exe} {become_cmd}" | |
| return f"{exe} {flags} {become_cmd}" |
There was a problem hiding this comment.
Why not do return " ".join(part for part in [exe, flags, become_cmd] if part) instead? That has no conditional, and doesn't insert unnecessary spaces.
There was a problem hiding this comment.
Much better! Although, just to pick at you, it does have 3 conditionals ;-). Totally worth the trade.
Remove redundant 'should generally be left enabled' description line and simplify become command return by removing unnecessary flags conditional.
| print(cmd) | ||
| assert re.match(f"""{pfexec_exe} {pfexec_flags} 'echo {success}; {default_cmd}'""", cmd) is not None | ||
| # With wrap_exe=true (default), command is wrapped in shell -c | ||
| assert re.match(f"""{pfexec_exe} {default_exe} -c 'echo {success}; {default_cmd}'""", cmd) is not None |
There was a problem hiding this comment.
Please note that the unit tests are failing now because of the extra space generated (which is not a problem in itself), requiring the regexp to be more flexible:
| assert re.match(f"""{pfexec_exe} {default_exe} -c 'echo {success}; {default_cmd}'""", cmd) is not None | |
| assert re.match(rf"""{pfexec_exe}\s+{default_exe} -c 'echo {success}; {default_cmd}'""", cmd) is not None |
Match double space in test assertions when become_flags defaults to empty string, consistent with doas, dzdo, and pbrun test patterns.
|
I've updated the test regexes to use double spaces for empty flags, matching the existing pattern in test_doas, test_dzdo, and test_pbrun. Unit tests all pass locally, sorry, I should have run them before the last commit. wrap_exe=true and raw, works okay with my books on my SmartOS server: wrap_exe=true is transparent for raw commands wrapping in /bin/sh -c doesn't seem to change behaviour. |
Why not use #11623 (comment) instead of adjusting the whitespcae in the tests? |
In that case we should deprecate the current default and switch it in a later version. To deprecate the default, remove it (the |
I figured just make it consistent with all the other tests, like I said, I'm not really 100% with Python, just a tourist here. :) I'm away from my computer, so I'll make the update inline with that comment when I can, and the other changes you suggested. |
SUMMARY
The
pfexecbecome plugin has had incorrect defaults since it was migrated from Ansible core, making it unusable on illumos/SmartOS/OmniOS/OpenIndiana without manual workarounds in the user's inventory or playbook configuration.Two default values are wrong:
become_flagsdefaults to"-H -S -n"— these aresudoflags, notpfexecflags. The illumospfexeccommand does not accept any of these options (-Hsets HOME in sudo,-Sreads password from stdin,-nis non-interactive mode). This causesexec: illegal option -- Hon every invocation.wrap_exedefaults tofalse. Unlikesudo,pfexecdoes not interpret shell constructs internally — it callsexec()directly on its argument. Since Ansible generates compound commands containing shell operators (e.g.echo BECOME-SUCCESS-xxx ; /opt/local/bin/python3), these must be wrapped in/bin/sh -c '...'forpfexecto execute them. Without wrapping, pfexec tries to exec the compound string as a single binary name and fails silently.History:
become_flags: ""+ansible_pfexec_wrap_execution: yes), but the defaults were never correctedChanges in this PR:
become_flagsdefault:"-H -S -n"→""(pfexec accepts no flags by default)wrap_exedefault:false→true(required for Ansible's compound commands)build_become_command: handle empty flags without injecting extra whitespacewrap_exedescription explaining why it should be enabledTested on: SmartOS (illumos) native zones with Ansible 13.4.0 / community.general 12.4.0, managing infrastructure via
pfexecwith RBAC profiles. Confirmed working withansible.builtin.user,ansible.builtin.command,ansible.builtin.file,ansible.builtin.template,ansible.builtin.uri, andansible.builtin.lineinfilemodules.Fixes #3671
ISSUE TYPE
COMPONENT NAME
plugins/become/pfexec.py
ADDITIONAL INFORMATION
Before (broken):
Users had to add workarounds to every inventory or playbook:
After (works with no user configuration):
No
become_flagsorwrap_exeoverrides needed.Why these defaults are correct for pfexec:
Empty flags:
pfexecis RBAC-based. Privileges are granted by profiles in/etc/security/user_attrand/etc/security/exec_attr. There is no password prompting (-S/-nare meaningless), andpfexecpreserves the caller's environment (-His meaningless). Thepfexecbinary accepts onlypfexec [-P privset] cmd [arg ..].wrap_exe=true:
sudohas an internal shell parser that handles compound commands.pfexecdoes not — it callsexec()directly. Since Ansible always generates compound commands (theBECOME-SUCCESSecho followed by the Python interpreter), the command must be wrapped in/bin/sh -c '...'for pfexec to execute it.