Skip to content

Commit 84c9ef2

Browse files
russozclaudeJonny007-MKD
authored
openwrt_action plugin utils: add shell module_utils support (#233)
* feat(openwrt_action): add shell module_utils support Subclasses declare `module_utils = ["libname", ...]`; the action plugin transfers each `plugins/module_utils/<name>.sh` to `<tmpdir>/module_utils/` on the remote in declaration order, passes the list as `_openwrt_libs`, and wrapper.sh sources them before the module script. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(wrapper): split into _core.sh and _file.sh module_utils wrapper.sh is now a thin orchestrator (~40 lines): bootstrap, source _openwrt_libs in order, main exec. All framework code moves to module_utils/_core.sh (always transferred automatically) and file/path/crypto utilities move to module_utils/_file.sh (opt-in via module_utils = ["_file"] in action plugin subclasses). Action plugins for copy, file, lineinfile, stat, slurp, uci declare module_utils = ["_file"]. OpenwrtActionBase always prepends _core to the transfer list regardless of module_utils. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * add changelog frag * refactor(wrapper): remove redundant WANT_JSON declarations wrapper.sh sets WANT_JSON=1 before sourcing anything; the declarations in _core.sh and uci.sh are unreachable before Ansible's static scan and redundant at runtime. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Apply suggestions from code review Co-authored-by: Jonny007-MKD <Jonny007-MKD@users.noreply.github.com> * docs(module_utils): add description comments to _core.sh and _file.sh Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Jonny007-MKD <Jonny007-MKD@users.noreply.github.com>
1 parent cd610d7 commit 84c9ef2

19 files changed

Lines changed: 577 additions & 428 deletions

File tree

antsibull-nox.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ run_mypy = false
2929

3030
[sessions.extra_checks]
3131
run_no_unwanted_files = true
32+
no_unwanted_files_other_extensions = [".py", ".sh"]
3233
no_unwanted_files_module_extensions = [".py", ".sh", ".yml"]
3334
no_unwanted_files_yaml_extensions = [".yml"]
3435
run_no_trailing_whitespace = true
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
minor_changes:
2+
- "openwrt_action plugin utils - add shell ``module_utils`` support, allowing action plugins to declare shell libraries that are transferred to the remote node and sourced before the module script runs
3+
(https://github.com/ansible-collections/community.openwrt/issues/44, https://github.com/ansible-collections/community.openwrt/pull/233)."
4+
- "wrapper - refactor into a thin bootstrapper that sources each entry in ``_openwrt_libs`` before executing the module; all shared framework code moved to ``module_utils/_core.sh``
5+
(https://github.com/ansible-collections/community.openwrt/issues/44, https://github.com/ansible-collections/community.openwrt/pull/233)."
6+
- "copy action plugin - declare ``_file`` shell module utils dependency
7+
(https://github.com/ansible-collections/community.openwrt/issues/44, https://github.com/ansible-collections/community.openwrt/pull/233)."
8+
- "file action plugin - declare ``_file`` shell module utils dependency
9+
(https://github.com/ansible-collections/community.openwrt/issues/44, https://github.com/ansible-collections/community.openwrt/pull/233)."
10+
- "lineinfile action plugin - declare ``_file`` shell module utils dependency
11+
(https://github.com/ansible-collections/community.openwrt/issues/44, https://github.com/ansible-collections/community.openwrt/pull/233)."
12+
- "slurp action plugin - declare ``_file`` shell module utils dependency
13+
(https://github.com/ansible-collections/community.openwrt/issues/44, https://github.com/ansible-collections/community.openwrt/pull/233)."
14+
- "stat action plugin - declare ``_file`` shell module utils dependency
15+
(https://github.com/ansible-collections/community.openwrt/issues/44, https://github.com/ansible-collections/community.openwrt/pull/233)."
16+
- "uci action plugin - declare ``_file`` shell module utils dependency
17+
(https://github.com/ansible-collections/community.openwrt/issues/44, https://github.com/ansible-collections/community.openwrt/pull/233)."

plugins/action/copy.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ class ActionModule(OpenwrtActionBase):
2323
backup, and other file operations.
2424
"""
2525

26+
module_utils = ["_file"]
27+
2628
def run(self, tmp=None, task_vars=None):
2729
"""Execute the copy action plugin"""
2830

plugins/action/file.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@
88

99

1010
class ActionModule(OpenwrtActionBase):
11-
pass
11+
module_utils = ["_file"]

plugins/action/lineinfile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@
88

99

1010
class ActionModule(OpenwrtActionBase):
11-
pass
11+
module_utils = ["_file"]

plugins/action/slurp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@
88

99

1010
class ActionModule(OpenwrtActionBase):
11-
pass
11+
module_utils = ["_file"]

plugins/action/stat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@
88

99

1010
class ActionModule(OpenwrtActionBase):
11-
pass
11+
module_utils = ["_file"]

plugins/action/uci.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@
88

99

1010
class ActionModule(OpenwrtActionBase):
11-
pass
11+
module_utils = ["_file"]

plugins/module_utils/_core.sh

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
# shellcheck shell=ash
2+
# Copyright (c) 2017 Markus Weippert
3+
# Copyright (c) 2026, Alexei Znamensky
4+
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
5+
# SPDX-License-Identifier: GPL-3.0-or-later
6+
7+
# Core shell framework for community.openwrt modules.
8+
# Sourced by wrapper.sh on every module execution.
9+
# Provides:
10+
# parameter parsing (JSON and legacy)
11+
# exit/result handling
12+
# check mode support
13+
# diff support
14+
# control flow helpers (changed, fail, succeed, try, final)
15+
16+
_ANSIBLE_PARAMS="
17+
_ansible_version/s
18+
_ansible_no_log/b
19+
_ansible_module_name/s
20+
_ansible_syslog_facility/s
21+
_ansible_socket/s
22+
_ansible_verbosity/i
23+
_ansible_diff/b
24+
_ansible_debug/b
25+
_ansible_check_mode/b
26+
"
27+
CHANGED=""
28+
FACT_VARS=""
29+
JSON_PREFIX=""
30+
MESSAGE=""
31+
NO_EXIT_JSON=""
32+
PARAMS=""
33+
RESPONSE_VARS=""
34+
SKIPPED=""
35+
SUPPORTS_CHECK_MODE="1"
36+
37+
N="
38+
"
39+
T=" "
40+
41+
init() { :; }
42+
validate() { :; }
43+
main() { :; }
44+
cleanup() { :; }
45+
46+
_exit_add_vars() {
47+
local _var _name _type _always _value _IFS
48+
for _var; do
49+
_IFS="$IFS"; IFS="/"; set -- $_var; IFS="$_IFS"
50+
_var="$1"; _type="$(_map_type "$2")"; _always="$3"
51+
_IFS="$IFS"; IFS="="; set -- $_var; IFS="$_IFS"
52+
_name="$1"; _var="${2:-$_name}"
53+
eval "_value=\"\$$_var\""
54+
[ -z "$_value" -a -z "$_always" ] ||
55+
eval "json_add_$_type \"\$_name\" \"\$_value\""
56+
done
57+
}
58+
59+
_init_done=""
60+
_exit() {
61+
local _rc="$?"
62+
local _v _k
63+
[ -z "$_init_done" ] || cleanup "$_rc" || :
64+
[ -z "$NO_EXIT_JSON" ] || return $_rc
65+
json_set_namespace result
66+
json_add_boolean changed $([ -z "$CHANGED" ]; echo $?)
67+
json_add_boolean failed $([ $_rc -eq 0 ]; echo $?)
68+
[ -z "$SKIPPED" ] || json_add_boolean skipped 1
69+
[ -z "$MESSAGE" ] || json_add_string msg "$MESSAGE"
70+
[ -z "$_init_done" ] || {
71+
[ -z "$RESPONSE_VARS" ] || _exit_add_vars $RESPONSE_VARS
72+
[ -z "$FACT_VARS" ] || {
73+
json_add_object ansible_facts
74+
_exit_add_vars "$FACT_VARS"
75+
json_close_object
76+
}
77+
}
78+
[ -z "$_ansible_diff" -o -z "$_diff_set" ] || {
79+
json_add_object diff
80+
json_add_string before "$_diff_before${_diff_before:+$N}"
81+
json_add_string after "$_diff_after${_diff_after:+$N}"
82+
[ -z "$_diff_before_header" ] ||
83+
json_add_string before_header "$_diff_before_header"
84+
[ -z "$_diff_after_header" ] ||
85+
json_add_string after_header "$_diff_after_header"
86+
json_close_object
87+
}
88+
echo; json_dump
89+
json_cleanup
90+
return $_rc
91+
}
92+
trap _exit EXIT
93+
94+
_map_type() {
95+
[ -n "$1" ] || { echo "string"; return 0; }
96+
case "$1" in
97+
any) echo "any";;
98+
s|str|string) echo "string";;
99+
i|int|integer) echo "int";;
100+
b|bool|boolean) echo "boolean";;
101+
f|d|float|double) echo "double";;
102+
l|a|list|array) echo "array";;
103+
o|h|obj|object|hash|map) echo "object";;
104+
*) fail "unknown type: $1";;
105+
esac
106+
}
107+
108+
_verify_value_type() {
109+
local _value="$1"
110+
local _type="$2"
111+
case "$_type" in
112+
int) printf "%d" "$_value" >/dev/null 2>&1 || return 1;;
113+
double) printf "%f" "$_value" >/dev/null 2>&1 || return 1;;
114+
boolean)
115+
case "$_value" in
116+
yes|true|True|1) _value="1";;
117+
no|false|False|0) _value="";;
118+
*) return 1;;
119+
esac;;
120+
esac
121+
echo "$_value"
122+
}
123+
124+
_parse_legacy_params() {
125+
local _var _type _required _default _alias
126+
local _param _value _IFS
127+
for _param in $PARAMS; do
128+
eval "${_param%%[/=]*}=\"\""
129+
done
130+
eval "$(cat "$_params")" || fail "could not parse params"
131+
for _param in $PARAMS $_ANSIBLE_PARAMS; do
132+
_value=""; _IFS="$IFS"; IFS="/"; set -- $_param; IFS="$_IFS"
133+
_var="$1"; _type="$(_map_type "$2")"; _required="$3"; _default="$4"
134+
_IFS="$IFS"; IFS="="; set -- $_var; IFS="$_IFS"; _var="$1"
135+
for _alias; do
136+
eval "_value=\"\$$_alias\""
137+
[ -z "$_value" ] || break
138+
done
139+
eval "export -- _orig_$_var=\"\$_value\""
140+
[ -z "$_value" ] && {
141+
[ -z "$_required" ] || fail "$_var is required"
142+
[ -z "$_default" ] ||
143+
_value="$(_verify_value_type "$_default" "$_type")"
144+
} || {
145+
_value="$(_verify_value_type "$_value" "$_type")" ||
146+
fail "$_var must be $_type"
147+
}
148+
eval "export -- $_var=\"\$_value\" _type_$_var=\"\$_type\""
149+
done
150+
return 0
151+
}
152+
153+
_parse_json_params() {
154+
local _var _type _required _default _alias
155+
local _param _value _found _is_type _IFS
156+
json_set_namespace params
157+
json_load "$(cat -- "$_params")" || fail "could not parse params"
158+
for _param in $PARAMS $_ANSIBLE_PARAMS; do
159+
_value=""; _IFS="$IFS"; IFS="/"; set -- $_param; IFS="$_IFS"
160+
_var="$1"; _type="$(_map_type "$2")"; _required="$3"; _default="$4"
161+
_IFS="$IFS"; IFS="="; set -- $_var; IFS="$_IFS"; _var="$1"
162+
_is_type=""; _found=""
163+
for _alias; do
164+
json_get_type _is_type "$_alias" &&
165+
json_get_var _value "$_alias" ||
166+
continue
167+
_found="1"; break
168+
done
169+
eval "export -- _orig_$_var=\"\$_value\" _defined_$_var=\"$_found\""
170+
[ -n "$_found" ] || {
171+
[ -z "$_required" ] || fail "$_var is required"
172+
[ -z "$_default" ] ||
173+
_value="$(_verify_value_type "$_default" "$_type")"
174+
}
175+
case "$_is_type" in
176+
array|object)
177+
[ "$_type" = "$_is_type" -o "$_type" = "any" ] ||
178+
fail "$_var must be $_type"
179+
eval "export -- _type_${_var}=\"\$_is_type\""
180+
json_add_string "_$_var" "$_alias";;
181+
*)
182+
[ -z "$_found" ] ||
183+
_value="$(_verify_value_type "$_value" "$_type")" ||
184+
fail "$_var must be $_type"
185+
eval "export -- $_var=\"\$_value\" _type_$_var=\"\$_type\""
186+
json_add_string "$_var" "$_value";;
187+
esac
188+
done
189+
return 0
190+
}
191+
192+
_parse_params() {
193+
[ -n "$WANT_JSON" ] && _parse_json_params || _parse_legacy_params
194+
}
195+
196+
_support_check_mode() {
197+
[ -n "$_ansible_check_mode" -a -z "$SUPPORTS_CHECK_MODE" ] || return 0
198+
SKIPPED="1"
199+
MESSAGE="module does not support check mode"
200+
exit 0
201+
}
202+
203+
json_select_real() {
204+
local real_var
205+
json_get_var real_var "_$1"
206+
json_select "$real_var"
207+
}
208+
209+
changed() {
210+
CHANGED="1"
211+
}
212+
213+
unchanged() {
214+
CHANGED=""
215+
}
216+
217+
succeed() {
218+
[ -n "$*" ] && MESSAGE="$*"
219+
exit 0
220+
}
221+
222+
fail() {
223+
MESSAGE="$*"
224+
exit 1
225+
}
226+
227+
_result=""
228+
try() {
229+
[ $# -eq 1 ] && {
230+
_result="$(eval "$1" 2>&1)" || fail "$*: $_result"
231+
} || {
232+
_result="$("$@" 2>&1)" || fail "$*: $_result"
233+
}
234+
}
235+
236+
final() {
237+
try "$@"
238+
exit 0
239+
}
240+
241+
_diff_set=""
242+
_diff_before=""
243+
_diff_after=""
244+
_diff_before_header=""
245+
_diff_after_header=""
246+
set_diff() {
247+
_diff_before="${1:-$_diff_before}"
248+
_diff_after="${2:-$_diff_after}"
249+
_diff_before_header="${3:-$_diff_before_header}"
250+
_diff_after_header="${4:-$_diff_after_header}"
251+
_diff_set="1"
252+
}

0 commit comments

Comments
 (0)