Merge branch 'master' into beta

This commit is contained in:
Darko Poljak 2020-11-07 12:21:01 +01:00
commit b17bf193a9
68 changed files with 2143 additions and 183 deletions

View file

@ -371,7 +371,6 @@ eof
Manual steps post release: Manual steps post release:
- cdist-web - cdist-web
- send generated mailinglist.tmp mail - send generated mailinglist.tmp mail
- twitter
eof eof
;; ;;

View file

@ -9,10 +9,11 @@ import cdist.trigger
import cdist.log import cdist.log
import cdist.preos import cdist.preos
import cdist.info import cdist.info
import cdist.scan.commandline
# set of beta sub-commands # set of beta sub-commands
BETA_COMMANDS = set(('install', 'inventory', 'trigger', )) BETA_COMMANDS = set(('install', 'inventory', 'scan', 'trigger', ))
# set of beta arguments for sub-commands # set of beta arguments for sub-commands
BETA_ARGS = { BETA_ARGS = {
'config': set(('tag', 'all_tagged_hosts', 'use_archiving', )), 'config': set(('tag', 'all_tagged_hosts', 'use_archiving', )),
@ -274,8 +275,7 @@ def get_parsers():
'-f', '--file', '-f', '--file',
help=('Read specified file for a list of additional hosts to ' help=('Read specified file for a list of additional hosts to '
'operate on or if \'-\' is given, read stdin (one host per ' 'operate on or if \'-\' is given, read stdin (one host per '
'line). If no host or host file is specified then, by ' 'line).'),
'default, read hosts from stdin.'),
dest='hostfile', required=False) dest='hostfile', required=False)
parser['config_args'].add_argument( parser['config_args'].add_argument(
'-p', '--parallel', nargs='?', metavar='HOST_MAX', '-p', '--parallel', nargs='?', metavar='HOST_MAX',
@ -327,9 +327,7 @@ def get_parsers():
parser['add-host'].add_argument( parser['add-host'].add_argument(
'-f', '--file', '-f', '--file',
help=('Read additional hosts to add from specified file ' help=('Read additional hosts to add from specified file '
'or from stdin if \'-\' (each host on separate line). ' 'or from stdin if \'-\' (each host on separate line). '),
'If no host or host file is specified then, by default, '
'read from stdin.'),
dest='hostfile', required=False) dest='hostfile', required=False)
parser['add-tag'] = parser['invsub'].add_parser( parser['add-tag'] = parser['invsub'].add_parser(
@ -343,20 +341,12 @@ def get_parsers():
parser['add-tag'].add_argument( parser['add-tag'].add_argument(
'-f', '--file', '-f', '--file',
help=('Read additional hosts to add tags from specified file ' help=('Read additional hosts to add tags from specified file '
'or from stdin if \'-\' (each host on separate line). ' 'or from stdin if \'-\' (each host on separate line). '),
'If no host or host file is specified then, by default, '
'read from stdin. If no tags/tagfile nor hosts/hostfile'
' are specified then tags are read from stdin and are'
' added to all hosts.'),
dest='hostfile', required=False) dest='hostfile', required=False)
parser['add-tag'].add_argument( parser['add-tag'].add_argument(
'-T', '--tag-file', '-T', '--tag-file',
help=('Read additional tags to add from specified file ' help=('Read additional tags to add from specified file '
'or from stdin if \'-\' (each tag on separate line). ' 'or from stdin if \'-\' (each tag on separate line). '),
'If no tag or tag file is specified then, by default, '
'read from stdin. If no tags/tagfile nor hosts/hostfile'
' are specified then tags are read from stdin and are'
' added to all hosts.'),
dest='tagfile', required=False) dest='tagfile', required=False)
parser['add-tag'].add_argument( parser['add-tag'].add_argument(
'-t', '--taglist', '-t', '--taglist',
@ -377,9 +367,7 @@ def get_parsers():
parser['del-host'].add_argument( parser['del-host'].add_argument(
'-f', '--file', '-f', '--file',
help=('Read additional hosts to delete from specified file ' help=('Read additional hosts to delete from specified file '
'or from stdin if \'-\' (each host on separate line). ' 'or from stdin if \'-\' (each host on separate line). '),
'If no host or host file is specified then, by default, '
'read from stdin.'),
dest='hostfile', required=False) dest='hostfile', required=False)
parser['del-tag'] = parser['invsub'].add_parser( parser['del-tag'] = parser['invsub'].add_parser(
@ -397,20 +385,13 @@ def get_parsers():
parser['del-tag'].add_argument( parser['del-tag'].add_argument(
'-f', '--file', '-f', '--file',
help=('Read additional hosts to delete tags for from specified ' help=('Read additional hosts to delete tags for from specified '
'file or from stdin if \'-\' (each host on separate line). ' 'file or from stdin if \'-\' (each host on separate '
'If no host or host file is specified then, by default, ' 'line). '),
'read from stdin. If no tags/tagfile nor hosts/hostfile'
' are specified then tags are read from stdin and are'
' deleted from all hosts.'),
dest='hostfile', required=False) dest='hostfile', required=False)
parser['del-tag'].add_argument( parser['del-tag'].add_argument(
'-T', '--tag-file', '-T', '--tag-file',
help=('Read additional tags from specified file ' help=('Read additional tags from specified file '
'or from stdin if \'-\' (each tag on separate line). ' 'or from stdin if \'-\' (each tag on separate line). '),
'If no tag or tag file is specified then, by default, '
'read from stdin. If no tags/tagfile nor'
' hosts/hostfile are specified then tags are read from'
' stdin and are added to all hosts.'),
dest='tagfile', required=False) dest='tagfile', required=False)
parser['del-tag'].add_argument( parser['del-tag'].add_argument(
'-t', '--taglist', '-t', '--taglist',
@ -491,6 +472,35 @@ def get_parsers():
'pattern', nargs='?', help='Glob pattern.') 'pattern', nargs='?', help='Glob pattern.')
parser['info'].set_defaults(func=cdist.info.Info.commandline) parser['info'].set_defaults(func=cdist.info.Info.commandline)
# Scan = config + further
parser['scan'] = parser['sub'].add_parser('scan', add_help=False,
parents=[parser['config']])
parser['scan'] = parser['sub'].add_parser(
'scan', parents=[parser['loglevel'],
parser['beta'],
parser['colored_output'],
parser['common'],
parser['config_main']])
parser['scan'].add_argument(
'-m', '--mode', help='Which modes should run',
action='append', default=[],
choices=['scan', 'trigger'])
parser['scan'].add_argument(
'--config',
action='store_true',
help='Try to configure detected hosts')
parser['scan'].add_argument(
'-I', '--interfaces',
action='append', default=[],
help='On which interfaces to scan/trigger')
parser['scan'].add_argument(
'-d', '--delay',
action='store', default=3600,
help='How long to wait before reconfiguring after last try')
parser['scan'].set_defaults(func=cdist.scan.commandline.commandline)
# Trigger # Trigger
parser['trigger'] = parser['sub'].add_parser( parser['trigger'] = parser['sub'].add_parser(
'trigger', parents=[parser['loglevel'], 'trigger', parents=[parser['loglevel'],

View file

@ -0,0 +1,4 @@
#!/bin/sh -e
getent passwd | awk -F: '{print "user:"$1}'
getent group | awk -F: '{print "group:"$1}'

View file

@ -22,8 +22,8 @@ file_is="$( cat "$__object/explorer/file_is" )"
if [ "$file_is" = 'missing' ] \ if [ "$file_is" = 'missing' ] \
&& [ -z "$__cdist_dry_run" ] \ && [ -z "$__cdist_dry_run" ] \
&& \( [ ! -f "$__object/parameter/file" ] \ && [ ! -f "$__object/parameter/file" ] \
|| [ ! -f "$__object/parameter/directory" ] \) && [ ! -f "$__object/parameter/directory" ]
then then
exit 0 exit 0
fi fi
@ -47,28 +47,26 @@ then
elif [ -f "$__object/parameter/entry" ] elif [ -f "$__object/parameter/entry" ]
then then
acl_should="$( cat "$__object/parameter/entry" )" acl_should="$( cat "$__object/parameter/entry" )"
elif [ -f "$__object/parameter/acl" ]
then
acl_should="$( cat "$__object/parameter/acl" )"
elif
[ -f "$__object/parameter/user" ] \
|| [ -f "$__object/parameter/group" ] \
|| [ -f "$__object/parameter/mask" ] \
|| [ -f "$__object/parameter/other" ]
then
acl_should="$( for param in user group mask other
do
[ ! -f "$__object/parameter/$param" ] && continue
echo "$param" | grep -Eq 'mask|other' && sep=:: || sep=:
echo "$param$sep$( cat "$__object/parameter/$param" )"
done )"
else else
echo 'no parameters set' >&2 echo 'no parameters set' >&2
exit 1 exit 1
fi fi
# instead of setfacl's non-helpful message "Option -m: Invalid argument near character X"
# let's check if target has necessary users and groups, since mistyped or missing
# users/groups in target is most common reason.
echo "$acl_should" \
| grep -Po '(user|group):[^:]+' \
| sort -u \
| while read -r l
do
if ! grep "$l" -Fxq "$__object/explorer/getent"
then
echo "no $l' in target" | sed "s/:/ '/" >&2
exit 1
fi
done
if [ -f "$__object/parameter/default" ] if [ -f "$__object/parameter/default" ]
then then
acl_should="$( echo "$acl_should" \ acl_should="$( echo "$acl_should" \

View file

@ -12,11 +12,14 @@ Fully supported and tested on Linux (ext4 filesystem), partial support for FreeB
See ``setfacl`` and ``acl`` manpages for more details. See ``setfacl`` and ``acl`` manpages for more details.
One of ``--entry`` or ``--source`` must be used.
REQUIRED MULTIPLE PARAMETERS
OPTIONAL MULTIPLE PARAMETERS
---------------------------- ----------------------------
entry entry
Set ACL entry following ``getfacl`` output syntax. Set ACL entry following ``getfacl`` output syntax.
Must be used if ``--source`` is not used.
OPTIONAL PARAMETERS OPTIONAL PARAMETERS
@ -25,6 +28,7 @@ source
Read ACL entries from stdin or file. Read ACL entries from stdin or file.
Ordering of entries is not important. Ordering of entries is not important.
When reading from file, comments and empty lines are ignored. When reading from file, comments and empty lines are ignored.
Must be used if ``--entry`` is not used.
file file
Create/change file with ``__file`` using ``user:group:mode`` pattern. Create/change file with ``__file`` using ``user:group:mode`` pattern.
@ -48,12 +52,6 @@ remove
``mask`` and ``other`` entries can't be removed, but only changed. ``mask`` and ``other`` entries can't be removed, but only changed.
DEPRECATED PARAMETERS
---------------------
Parameters ``acl``, ``user``, ``group``, ``mask`` and ``other`` are deprecated and they
will be removed in future versions. Please use ``entry`` parameter instead.
EXAMPLES EXAMPLES
-------- --------

View file

@ -1 +0,0 @@
see manual for details

View file

@ -1 +0,0 @@
see manual for details

View file

@ -1 +0,0 @@
see manual for details

View file

@ -1 +0,0 @@
see manual for details

View file

@ -1 +0,0 @@
see manual for details

View file

@ -1,5 +1,3 @@
mask
other
source source
file file
directory directory

View file

@ -1,4 +1 @@
entry entry
acl
user
group

View file

@ -32,11 +32,12 @@ EXAMPLES
AUTHORS AUTHORS
------- -------
Steven Armstrong <steven-cdist--@--armstrong.cc> Steven Armstrong <steven-cdist--@--armstrong.cc>
Dennis Camera <dennis.camera--@--ssrq-sds-fds.ch>
COPYING COPYING
------- -------
Copyright \(C) 2014 Steven Armstrong. You can redistribute it Copyright \(C) 2014 Steven Armstrong, 2020 Dennis Camera.
and/or modify it under the terms of the GNU General Public License as You can redistribute it and/or modify it under the terms of the GNU General
published by the Free Software Foundation, either version 3 of the Public License as published by the Free Software Foundation, either version 3 of
License, or (at your option) any later version. the License, or (at your option) any later version.

View file

@ -1,6 +1,7 @@
#!/bin/sh -e #!/bin/sh -e
# #
# 2014 Steven Armstrong (steven-cdist at armstrong.cc) # 2014 Steven Armstrong (steven-cdist at armstrong.cc)
# 2020 Dennis Camera (dennis.camera at ssrq-sds-fds.ch)
# #
# This file is part of cdist. # This file is part of cdist.
# #
@ -19,26 +20,28 @@
# #
os=$(cat "$__global/explorer/os") os=$(cat "${__global:?}/explorer/os")
case "$os" in case ${os}
ubuntu|debian|devuan) in
# No stinking recommends thank you very much. (ubuntu|debian|devuan)
# If I want something installed I will do so myself. __file /etc/apt/apt.conf.d/00InstallRecommends --state present \
__file /etc/apt/apt.conf.d/99-no-recommends \ --owner root --group root --mode 0644 --source - <<-'EOF'
--owner root --group root --mode 644 \ APT::Install-Recommends "false";
--source - << DONE APT::Install-Suggests "false";
APT::Install-Recommends "0"; APT::AutoRemove::RecommendsImportant "false";
APT::Install-Suggests "0"; APT::AutoRemove::SuggestsImportant "false";
APT::AutoRemove::RecommendsImportant "0"; EOF
APT::AutoRemove::SuggestsImportant "0";
DONE # TODO: Remove the following object after some time
;; require=__file/etc/apt/apt.conf.d/00InstallRecommends \
*) __file /etc/apt/apt.conf.d/99-no-recommends --state absent
cat >&2 << DONE ;;
(*)
cat >&2 <<EOF
The developer of this type (${__type##*/}) did not think your operating system The developer of this type (${__type##*/}) did not think your operating system
($os) would have any use for it. If you think otherwise please submit a patch. ($os) would have any use for it. If you think otherwise please submit a patch.
DONE EOF
exit 1 exit 1
;; ;;
esac esac

View file

@ -69,7 +69,8 @@ EXAMPLES
require='__download/opt/cpma/cnq3.zip' \ require='__download/opt/cpma/cnq3.zip' \
__unpack /opt/cpma/cnq3.zip \ __unpack /opt/cpma/cnq3.zip \
--move-existing-destination \ --backup-destination \
--preserve-archive \
--destination /opt/cpma/server --destination /opt/cpma/server

View file

@ -87,11 +87,6 @@ case "$state_should" in
fi fi
;; ;;
pre-exists)
# pre-exists should never reach gencode-remote…
exit 1
;;
absent) absent)
if [ "$type" = "file" ]; then if [ "$type" = "file" ]; then
echo "rm -f '$destination'" echo "rm -f '$destination'"

View file

@ -53,8 +53,10 @@ function _find(_text, _pattern) {
BEGIN { BEGIN {
getline anchor < (ENVIRON["__object"] "/parameter/" position) getline anchor < (ENVIRON["__object"] "/parameter/" position)
getline pattern < (ENVIRON["__object"] "/parameter/" needle) getline pattern < (ENVIRON["__object"] "/parameter/" needle)
getline line < (ENVIRON["__object"] "/parameter/line")
found_line = 0 found_line = 0
correct_line = 0
correct_pos = (position != "after" && position != "before") correct_pos = (position != "after" && position != "before")
} }
{ {
@ -63,15 +65,18 @@ BEGIN {
getline getline
if (_find($0, pattern)) { if (_find($0, pattern)) {
found_line++ found_line++
if (index($0, line) == 1) { correct_line++ }
correct_pos = 1 correct_pos = 1
exit 0 exit 0
} }
} else if (_find($0, pattern)) { } else if (_find($0, pattern)) {
found_line++ found_line++
if (index($0, line) == 1) { correct_line++ }
} }
} else if (position == "before") { } else if (position == "before") {
if (_find($0, pattern)) { if (_find($0, pattern)) {
found_line++ found_line++
if (index($0, line) == 1) { correct_line++ }
getline getline
if (match($0, anchor)) { if (match($0, anchor)) {
correct_pos = 1 correct_pos = 1
@ -81,13 +86,18 @@ BEGIN {
} else { } else {
if (_find($0, pattern)) { if (_find($0, pattern)) {
found_line++ found_line++
if (index($0, line) == 1) { correct_line++ }
exit 0 exit 0
} }
} }
} }
END { END {
if (found_line && correct_pos) { if (found_line && correct_pos) {
print "present" if (correct_line) {
print "present"
} else {
print "matching"
}
} else if (found_line) { } else if (found_line) {
print "wrongposition" print "wrongposition"
} else { } else {

View file

@ -38,7 +38,11 @@ if [ -z "$state_is" ]; then
exit 1 exit 1
fi fi
if [ "$state_should" = "$state_is" ]; then if [ "$state_should" = "$state_is" ] || \
{ [ "$state_should" = "present" ] && [ "$state_is" = "matching" ] ;} || \
{ [ "$state_should" = "replace" ] && [ "$state_is" = "present" ] ;} ; then
# If state matches already, or 'present' is used and regex matches
# or 'replace' is used and the exact line is present, then there is
# nothing to do # nothing to do
exit 0 exit 0
fi fi
@ -61,8 +65,8 @@ fi
add=0 add=0
remove=0 remove=0
case "$state_should" in case "$state_should" in
present) present|replace)
if [ "$state_is" = "wrongposition" ]; then if [ "$state_is" = "wrongposition" ] || [ "$state_is" = "matching" ]; then
echo updated >> "$__messages_out" echo updated >> "$__messages_out"
remove=1 remove=1
else else

View file

@ -31,7 +31,7 @@ file
line line
Specifies the line which should be absent or present. Specifies the line which should be absent or present.
Must be present, if state is 'present'. Must be present, if state is 'present' or 'replace'.
Ignored if regex is given and state is 'absent'. Ignored if regex is given and state is 'absent'.
regex regex
@ -41,10 +41,13 @@ regex
If state is 'absent', ensure all lines matching the regular expression If state is 'absent', ensure all lines matching the regular expression
are absent. are absent.
If state is 'replace', ensure all lines matching the regular expression
are exactly 'line'.
The regular expression is interpreted by awk's match function. The regular expression is interpreted by awk's match function.
state state
'present' or 'absent', defaults to 'present' 'present', 'absent' or 'replace', defaults to 'present'.
onchange onchange
The code to run if line is added, removed or updated. The code to run if line is added, removed or updated.
@ -99,6 +102,12 @@ EXAMPLES
--line '-session required pam_exec.so debug log=/tmp/classify.log /usr/local/libexec/classify' \ --line '-session required pam_exec.so debug log=/tmp/classify.log /usr/local/libexec/classify' \
--after '^session[[:space:]]+include[[:space:]]+password-auth-ac$' --after '^session[[:space:]]+include[[:space:]]+password-auth-ac$'
# Uncomment as needed and set a value in a configuration file.
__line /etc/example.conf \
--line 'SomeSetting SomeValue' \
--regex '^(#[[:space:]]*)?SomeSetting[[:space:]]' \
--state replace
SEE ALSO SEE ALSO
-------- --------

View file

@ -0,0 +1,10 @@
#!/bin/sh -e
for bin in pip3 pip
do
if check="$( command -v "$bin" )"
then
echo "$check"
break
fi
done

View file

@ -32,7 +32,7 @@ pipparam="$__object/parameter/pip"
if [ -f "$pipparam" ]; then if [ -f "$pipparam" ]; then
pip=$(cat "$pipparam") pip=$(cat "$pipparam")
else else
pip="pip" pip="$( "$__type_explorer/pip" )"
fi fi
# If there is no pip, it may get created from somebody else. # If there is no pip, it may get created from somebody else.

View file

@ -38,7 +38,12 @@ pipparam="$__object/parameter/pip"
if [ -f "$pipparam" ]; then if [ -f "$pipparam" ]; then
pip=$(cat "$pipparam") pip=$(cat "$pipparam")
else else
pip="pip" pip="$( cat "$__object/explorer/pip" )"
if [ -z "$pip" ]
then
echo 'pip not found in path' >&2
exit 1
fi
fi fi
runasparam="$__object/parameter/runas" runasparam="$__object/parameter/runas"
@ -55,7 +60,7 @@ case "$state_should" in
then then
echo "su -c '$pip install -q $name' $runas" echo "su -c '$pip install -q $name' $runas"
else else
echo $pip install -q "$name" echo "$pip" install -q "$name"
fi fi
echo "installed" >> "$__messages_out" echo "installed" >> "$__messages_out"
;; ;;
@ -64,7 +69,7 @@ case "$state_should" in
then then
echo "su -c '$pip uninstall -q -y $name' $runas" echo "su -c '$pip uninstall -q -y $name' $runas"
else else
echo $pip uninstall -q -y "$name" echo "$pip" uninstall -q -y "$name"
fi fi
echo "removed" >> "$__messages_out" echo "removed" >> "$__messages_out"
;; ;;

View file

@ -0,0 +1,4 @@
#!/bin/sh -e
if pkg -N >/dev/null 2>&1; then
echo "YES"
fi

View file

@ -18,9 +18,14 @@
# along with cdist. If not, see <http://www.gnu.org/licenses/>. # along with cdist. If not, see <http://www.gnu.org/licenses/>.
# #
# #
# Retrieve the status of a package - parsed dpkg output # Retrieve the status of a package - parsed pkgng output
# #
if ! pkg -N >/dev/null 2>&1; then
# Nothing to do if pkg is not bootstrapped
exit
fi
if [ -f "$__object/parameter/name" ]; then if [ -f "$__object/parameter/name" ]; then
name="$(cat "$__object/parameter/name")" name="$(cat "$__object/parameter/name")"
else else

View file

@ -43,6 +43,7 @@ fi
repo="$(cat "$__object/parameter/repo")" repo="$(cat "$__object/parameter/repo")"
state="$(cat "$__object/parameter/state")" state="$(cat "$__object/parameter/state")"
curr_version="$(cat "$__object/explorer/pkg_version")" curr_version="$(cat "$__object/explorer/pkg_version")"
pkg_bootstrapped="$(cat "$__object/explorer/pkg_bootstrapped")"
add_cmd="pkg install -y" add_cmd="pkg install -y"
rm_cmd="pkg delete -y" rm_cmd="pkg delete -y"
upg_cmd="pkg upgrade -y" upg_cmd="pkg upgrade -y"
@ -73,6 +74,10 @@ execcmd(){
;; ;;
esac esac
if [ -z "${pkg_bootstrapped}" ]; then
echo "pkg bootstrap -y >/dev/null 2>&1"
fi
echo "$_cmd >/dev/null 2>&1" # Silence the output of the command echo "$_cmd >/dev/null 2>&1" # Silence the output of the command
echo "status=\$?" echo "status=\$?"
echo "if [ \"\$status\" -ne \"0\" ]; then" echo "if [ \"\$status\" -ne \"0\" ]; then"

View file

@ -7,7 +7,9 @@ action="$(cat "$__object/parameter/action")"
case "$manager" in case "$manager" in
systemd) systemd)
__systemd_service "$name" --action "$action" test "$action" = "start" && action="running"
test "$action" = "stop" && action="stopped"
__systemd_service "$name" --state "$action"
;; ;;
*) *)
# Unknown: handled by `service $NAME $action` in gencode-remote. # Unknown: handled by `service $NAME $action` in gencode-remote.

View file

@ -0,0 +1,110 @@
#!/bin/sh
#
# 2020 Dennis Camera (dennis.camera at ssrq-sds-fds.ch)
#
# This file is part of cdist.
#
# cdist is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# cdist is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with cdist. If not, see <http://www.gnu.org/licenses/>.
#
# This explorer retrieves the current state of the configuration option
# The output of this explorer is one of these values:
# present
# The configuration option is present and has the value of the
# parameter --value.
# absent
# The configuration option is not defined.
# different
# The configuration option is present but has a different value than the
# parameter --value.
# rearranged
# The configuration option is present (a list) and has the same values as
# the parameter --value, but in a different order.
RS=$(printf '\036')
option=${__object_id:?}
values_is=$(uci -s -N -d "${RS}" get "${option}" 2>/dev/null) || {
echo absent
exit 0
}
if test -f "${__object:?}/parameter/value"
then
should_file="${__object:?}/parameter/value"
else
should_file='/dev/null'
fi
# strip off trailing newline
printf '%s' "${values_is}" \
| awk '
function unquote(s) {
# simplified dequoting of single quoted strings
if (s ~ /^'\''.*'\''$/) {
s = substr(s, 2, length(s) - 2)
sub(/'"'\\\\''"'/, "'\''", s)
}
return s
}
BEGIN {
state = "present" # assume all is fine
}
NR == FNR {
# memoize "should" state
should[FNR] = $0
should_count++
# go to next line (important!)
next
}
# compare "is" state
{ $0 = unquote($0) }
$0 == should[FNR] { next }
FNR > should_count {
# there are more "is" records than "should" -> definitely different
state = "different"
exit
}
{
# see if we can find the value somewhere in should
for (i in should) {
if ($0 == should[i]) {
# ... value found -> rearranged
# FIXME: Duplicate values are not properly handled here. Do they matter?
state = "rearranged"
next
}
}
state = "different"
exit
}
END {
if (FNR < should_count) {
# "is" was shorter than "should" -> different
state = "different"
}
print state
}
' "${should_file}" RS="${RS}" -

View file

@ -0,0 +1,73 @@
# -*- mode: sh; indent-tabs-mode: t -*-
in_list() {
printf '%s\n' "$@" | { grep -qxF "$(read -r ndl; echo "${ndl}")"; }
}
quote() {
for _arg
do
shift
if test -n "$(printf %s "${_arg}" | tr -d -c '\t\n \042-\047\050-\052\073-\077\133\\`|~' | tr -c '' '.')"
then
# needs quoting
set -- "$@" "$(printf "'%s'" "$(printf %s "${_arg}" | sed -e "s/'/'\\\\''/g")")"
else
set -- "$@" "${_arg}"
fi
done
unset _arg
# NOTE: Use printf because POSIX echo interprets escape sequences
printf '%s' "$*"
}
uci_cmd() {
# Usage: uci_cmd [UCI ARGUMENTS]...
mkdir -p "${__object:?}/files"
printf '%s\n' "$(quote "$@")" >>"${__object:?}/files/uci_batch.txt"
}
uci_validate_name() {
# like util.c uci_validate_name()
test -n "$*" && test -z "$(echo "$*" | tr -d '[:alnum:]_')"
}
uci_validate_tuple() (
tok=${1:?}
case $tok
in
(*.*.*)
# check option
option=${tok##*.}
uci_validate_name "${option}" || {
printf 'Invalid option: %s\n' "${option}" >&2
return 1
}
tok=${tok%.*}
;;
(*.*)
# no option (section definition)
;;
(*)
printf 'Invalid tuple: %s\n' "$1" >&2
return 1
;;
esac
case ${tok#*.}
in
(@*) section=$(expr "${tok#*.}" : '@\(.*\)\[-*[0-9]*\]$') ;;
(*) section=${tok#*.} ;;
esac
uci_validate_name "${section}" || {
printf 'Invalid section: %s\n' "${1#*.}" >&2
return 1
}
config=${tok%%.*}
uci_validate_name "${config}" || {
printf 'Invalid config: %s\n' "${config}" >&2
return 1
}
)

View file

@ -0,0 +1,43 @@
changes=$(uci changes)
if test -n "${changes}"
then
echo 'Uncommited UCI changes were found on the target:'
printf '%s\n\n' "${changes}"
echo 'This can be caused by manual changes or due to a previous failed run.'
echo 'Please investigate the situation, revert or commit the changes, and try again.'
exit 1
fi >&2
check_errors() {
# reads stdin and forwards non-empty lines to stderr.
# returns 0 if stdin is empty, else 1.
! grep -e . >&2
}
commit() {
uci commit
}
rollback() {
printf '\nAn error occurred when trying to commit UCI transaction!\n' >&2
uci changes \
| sed -e 's/^-//' -e 's/\..*\$//' \
| sort -u \
| while read -r _package
do
uci revert "${_package}"
echo "${_package}" # for logging
done \
| awk '
BEGIN { printf "Reverted changes in: " }
{ printf "%s%s", (FNR > 1 ? ", " : ""), $0 }
END { printf "\n" }' >&2
return 1
}
uci_apply() {
uci batch 2>&1 | check_errors && commit || rollback
}

View file

@ -0,0 +1,101 @@
#!/bin/sh -e
#
# 2020 Dennis Camera (dennis.camera@ssrq-sds-fds.ch)
#
# This file is part of cdist.
#
# cdist is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# cdist is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with cdist. If not, see <http://www.gnu.org/licenses/>.
#
# shellcheck source=cdist/conf/type/__uci/files/functions.sh
. "${__type:?}/files/functions.sh"
state_is=$(cat "${__object:?}/explorer/state")
state_should=$(cat "${__object:?}/parameter/state")
config=${__object_id:?}
uci_validate_tuple "${config}"
case ${state_should}
in
(present)
if in_list "${state_is}" 'present' 'rearranged'
then
# NOTE: order is ignored so rearranged is also fine.
exit 0
fi
# Determine type
type=$(cat "${__object:?}/parameter/type" 2>/dev/null || true)
case ${type}
in
(option|list) ;;
('')
# Guess type by the number of values
test "$(wc -l "${__object:?}/parameter/value")" -gt 1 \
&& type=list \
|| type=option
;;
(*)
printf 'Invalid --type: %s\n' "${type}" >&2
exit 1
;;
esac
case ${type}
in
(list)
printf 'set_list %s\n' "${config}" >>"${__messages_out:?}"
if test "${state_is}" != 'absent'
then
uci_cmd delete "${config}"
fi
while read -r value
do
uci_cmd add_list "${config}"="${value}"
done <"${__object:?}/parameter/value"
;;
(option)
printf 'set %s\n' "${config}" >>"${__messages_out:?}"
value=$(cat "${__object:?}/parameter/value")
uci_cmd set "${config}"="${value}"
;;
esac
;;
(absent)
if in_list "${state_is}" 'absent'
then
exit 0
fi
printf 'delete %s\n' "${config}" >>"${__messages_out:?}"
uci_cmd delete "${config}"
;;
(*)
printf 'Invalid --state: %s\n' "${state_should}" >&2
exit 1
;;
esac
if test -s "${__object:?}/files/uci_batch.txt"
then
cat "${__type:?}/files/uci_apply.sh"
printf "uci_apply <<'EOF'\n"
cat "${__object:?}/files/uci_batch.txt"
printf '\nEOF\n'
fi

View file

@ -0,0 +1,78 @@
cdist-type__uci(7)
==================
NAME
----
cdist-type__uci - Manage configuration values in UCI
DESCRIPTION
-----------
This cdist type can be used to alter configuration options in OpenWrt's
Unified Configuration Interface (UCI) system.
REQUIRED PARAMETERS
-------------------
value
The value to be set. Can be used multiple times.
This parameter is ignored if ``--state`` is ``absent``.
Due to the way cdist handles arguments, values **must not** contain newline
characters.
Values do not need special quoting for UCI. The only requirement is that the
value is passed to the type as a single shell argument.
OPTIONAL PARAMETERS
-------------------
state
``present`` or ``absent``, defaults to ``present``.
type
If the type should generate an option or a list.
One of: ``option`` or ``list``.
Defaults to auto-detect based on the number of ``--value`` parameters.
BOOLEAN PARAMETERS
------------------
None.
EXAMPLES
--------
.. code-block:: sh
# Set the system hostname
__uci system.@system[0].hostname --value 'OpenWrt'
# Set DHCP option 252: tell DHCP clients to not ask for proxy information.
__uci dhcp.lan.dhcp_option --type list --value '252,"\n"'
# Enable NTP and NTPd (each is applied individually)
__uci system.ntp.enabled --value 1
__uci system.ntp.enable_server --value 1
__uci system.ntp.server --type list \
--value '0.openwrt.pool.ntp.org' \
--value '1.openwrt.pool.ntp.org' \
--value '2.openwrt.pool.ntp.org' \
--value '3.openwrt.pool.ntp.org'
SEE ALSO
--------
- https://openwrt.org/docs/guide-user/base-system/uci
AUTHORS
-------
Dennis Camera <dennis.camera@ssrq-sds-fds.ch>
COPYING
-------
Copyright \(C) 2020 Dennis Camera. You can redistribute it
and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

51
cdist/conf/type/__uci/manifest Executable file
View file

@ -0,0 +1,51 @@
#!/bin/sh -e
#
# 2020 Dennis Camera (dennis.camera@ssrq-sds-fds.ch)
#
# This file is part of cdist.
#
# cdist is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# cdist is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with cdist. If not, see <http://www.gnu.org/licenses/>.
#
os=$(cat "${__global:?}/explorer/os")
state_should=$(cat "${__object:?}/parameter/state")
case ${os}
in
(openwrt)
# okay
;;
(*)
printf "Your operating system (%s) is currently not supported by this type (%s)\n" "${os}" "${__type##*/}" >&2
printf "Please contribute an implementation for it if you can.\n" >&2
exit 1
;;
esac
case ${state_should}
in
(present)
test -s "${__object:?}/parameter/value" || {
echo 'The parameter --value is required.' >&2
exit 1
}
;;
(absent)
;;
(*)
printf 'Invalid --state: %s\n' "${state_should}" >&2
exit 1
;;
esac

View file

View file

@ -0,0 +1 @@
present

View file

@ -0,0 +1,2 @@
state
type

View file

@ -0,0 +1 @@
value

View file

@ -0,0 +1,103 @@
#!/bin/sh -e
#
# 2020 Dennis Camera (dennis.camera at ssrq-sds-fds.ch)
#
# This file is part of cdist.
#
# cdist is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# cdist is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with cdist. If not, see <http://www.gnu.org/licenses/>.
#
# This explorer determines the "prefix" of the --type section matching --match
# if set, or __object_id otherwise.
RS=$(printf '\036')
NL=$(printf '\n '); NL=${NL% }
squote_values() {
sed -e '/=".*"$/{s/="/='\''/;s/"$/'\''/}' \
-e "/='.*'$/"'!{s/=/='\''/;s/$/'\''/}'
}
count_lines() (
IFS=${NL?}
# shellcheck disable=SC2048,SC2086
set -f -- $*; echo $#
)
echo "${__object_id:?}" | grep -q -e '^[^.]\{1,\}\.[^.]\{1,\}$' || {
echo 'Section identifiers are a package and section name separated by a "." (period).' >&2
exit 1
}
test -s "${__object:?}/parameter/match" || {
# If no --match is given, we take the __object_id as the section identifier.
echo "${__object_id:?}"
exit 0
}
test -s "${__object:?}/parameter/type" || {
echo 'Parameters --match and --type must be used together.' >&2
exit 1
}
sect_type_param=$(cat "${__object:?}/parameter/type")
expr "${sect_type_param}" : '[^.]\{1,\}\.[^.]\{1,\}$' >/dev/null 2>&1 || {
echo 'Section types are a package name and section type separated by a "." (period).' >&2
exit 1
}
package_filter=${sect_type_param%%.*}
section_filter=${sect_type_param#*.}
# Find by --match
# NOTE: Apart from section types all values are printed in single quotes by uci show.
match=$(head -n 1 "${__object:?}/parameter/match" | squote_values)
if uci -s -N get "${__object_id:?}" >/dev/null 2>&1
then
# Named section exists: ensure if --match applies to it
# if the "matched" option does not exist (e.g. empty section) we use the
# section unconditionally.
if match_value_is=$(uci -s -N get "${__object_id:?}.${match%%=*}" 2>/dev/null)
then
match_value_should=$(expr "${match}" : ".*='\\(.*\\)'$")
test "${match_value_is}" = "${match_value_should}" || {
printf 'Named section "%s" does not match --match "%s"\n' \
"${__object_id:?}" "${match}" >&2
exit 1
}
fi
echo "${__object_id:?}"
exit 0
fi
# No correctly named section exists already: find one to which --match applies
regex="^${package_filter}\\.@${section_filter}\\[[0-9]\\{1,\\}\\]\\.${match%%=*}="
matched_sections=$(
uci -s -N -d "${RS}" show "${package_filter}" 2>/dev/null \
| grep -e "${regex}" \
| while read -r _line
do
if test "${_line#*=}" = "${match#*=}"
then
echo "${_line}"
fi
done \
| sed -e 's/\.[^.]*=.*$//')
test "$(count_lines "${matched_sections}")" -le 1 || {
printf 'Found multiple matching sections:\n%s\n' "${matched_sections}" >&2
exit 1
}
echo "${matched_sections}"

View file

@ -0,0 +1,48 @@
#!/bin/sh -e
#
# 2020 Dennis Camera (dennis.camera at ssrq-sds-fds.ch)
#
# This file is part of cdist.
#
# cdist is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# cdist is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with cdist. If not, see <http://www.gnu.org/licenses/>.
#
# This explorer retrieves the current options of the configuration section.
RS=$(printf '\036')
section=$("${__type_explorer:?}/match")
test -n "${section}" || exit 0
uci -s -N -d "${RS}" show "${section}" 2>/dev/null \
| awk -v VSEP="${RS}" '
{
# Strip off the config and section parts
is_opt = sub(/^([^.]*\.){2}/, "")
if (!is_opt) {
# this line represents the section -> skip
next
}
if (index($0, VSEP)) {
# Put values each on a line, like --option and --list parameters
opt = substr($0, 1, index($0, "=") - 1)
split(substr($0, length(opt) + 2), values, VSEP)
for (i in values) {
printf "%s=%s\n", opt, values[i]
}
} else {
print
}
}'

View file

@ -1,6 +1,6 @@
#!/bin/sh -e #!/bin/sh -e
# #
# 2019 Ander Punnar (ander-at-kvlt-dot-ee) # 2020 Dennis Camera (dennis.camera at ssrq-sds-fds.ch)
# #
# This file is part of cdist. # This file is part of cdist.
# #
@ -17,23 +17,9 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with cdist. If not, see <http://www.gnu.org/licenses/>. # along with cdist. If not, see <http://www.gnu.org/licenses/>.
# #
# This explorer retrieves the current section type.
# TODO check if filesystem has ACL turned on etc section=$("${__type_explorer:?}/match")
test -n "${section}" || exit 0
if [ -f "$__object/parameter/acl" ] uci -s -N get "${section}" 2>/dev/null || true
then
grep -E '^(default:)?(user|group):' "$__object/parameter/acl" \
| while read -r acl
do
param="$( echo "$acl" | awk -F: '{print $(NF-2)}' )"
check="$( echo "$acl" | awk -F: '{print $(NF-1)}' )"
[ "$param" = 'user' ] && db=passwd || db="$param"
if ! getent "$db" "$check" > /dev/null
then
echo "missing $param '$check'" >&2
exit 1
fi
done
fi

View file

@ -0,0 +1,59 @@
# -*- mode: sh; indent-tabs-mode: t -*-
NL=$(printf '\n '); NL=${NL% }
grep_line() {
{ shift; printf '%s\n' "$@"; } | grep -qxF "$1"
}
print_errors() {
awk -v prefix="${1:-Found errors:}" -v suffix="${2-}" '
BEGIN {
if (getline) {
print prefix
print
rc = 1
}
}
{ print }
END {
if (rc && suffix) print suffix
exit rc
}' >&2
}
quote() {
for _arg
do
shift
if test -n "$(printf %s "${_arg}" | tr -d -c '\t\n \042-\047\050-\052\073-\077\133\\`|~' | tr -c '' '.')"
then
# needs quoting
set -- "$@" "$(printf "'%s'" "$(printf %s "${_arg}" | sed -e "s/'/'\\\\''/g")")"
else
set -- "$@" "${_arg}"
fi
done
unset _arg
printf '%s' "$*"
}
uci_cmd() {
# Usage: uci_cmd [UCI ARGUMENTS]...
mkdir -p "${__object:?}/files"
printf '%s\n' "$(quote "$@")" >>"${__object:?}/files/uci_batch.txt"
}
uci_validate_name() {
# like util.c uci_validate_name()
test -n "$*" && test -z "$(printf %s "$*" | tr -d '[:alnum:]_' | tr -c '' .)"
}
unquote_lines() {
sed -e '/^".*"$/{s/^"//;s/"$//}' \
-e '/'"^'.*'"'$/{s/'"^'"'//;s/'"'$"'//}'
}
validate_options() {
grep -shv -e '^[[:alnum:]_]\{1,\}=' "$@"
}

View file

@ -0,0 +1,91 @@
# -*- mode: awk; indent-tabs-mode:t -*-
# Usage: awk -f option_state.awk option_type option_name
# e.g. awk -f option_state.awk option title
# awk -f option_state.awk list entry
function unquote(s) {
# simplified dequoting of single quoted strings
if (s ~ /^'.*'$/) {
s = substr(s, 2, length(s) - 2)
sub(/'\\''/, "'", s)
}
return s
}
function valueof(line) {
if (line !~ /^[[:alpha:]_]+=/) return 0
return unquote(substr(line, index(line, "=") + 1))
}
BEGIN {
__object = ENVIRON["__object"]
if (!__object) exit 1
opttype = ARGV[1]
optname = ARGV[2]
if (opttype !~ /^(option|list)/ || !optname) {
print "invalid"
exit (e=1)
}
ARGV[1] = __object "/parameter/" opttype
ARGV[2] = __object "/explorer/options"
state = "present"
}
NR == FNR {
# memoize "should" state
if (index($0, optname "=") == 1) {
should[++should_count] = valueof($0)
}
# go to next line (important!)
next
}
{
# compare "is" state
if (index($0, optname "=") != 1)
next
++is_count
v = valueof($0)
if (v == should[is_count]) {
# looks good, but can't say definitely just from this line
} else if (is_count > should_count) {
# there are more "is" records than "should" -> definitely different
state = "different"
exit
} else {
# see if we can find the "is" value somewhere in "should"
for (i in should) {
if (v == should[i]) {
# value found -> could be rearranged
# FIXME: Duplicate values are not properly handled here. Do they matter?
state = "rearranged"
next
}
}
# "is" value could not be found in "should" -> definitely different
state = "different"
exit
}
}
END {
if (e) exit
if (!is_count) {
# no "is" values -> absent
state = "absent"
} else if (is_count < should_count) {
# "is" was shorter than "should" -> different
state = "different"
}
print state
}

View file

@ -0,0 +1 @@
../../__uci/files/uci_apply.sh

View file

@ -0,0 +1,174 @@
#!/bin/sh -e
#
# 2020 Dennis Camera (dennis.camera@ssrq-sds-fds.ch)
#
# This file is part of cdist.
#
# cdist is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# cdist is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with cdist. If not, see <http://www.gnu.org/licenses/>.
#
# shellcheck source=cdist/conf/type/__uci_section/files/functions.sh
. "${__type:?}/files/functions.sh"
section=$(cat "${__object:?}/explorer/match")
state_is=$(test -s "${__object:?}/explorer/type" && echo present || echo absent)
state_should=$(cat "${__object:?}/parameter/state")
case $state_should
in
(present)
test -f "${__object:?}/parameter/type" || {
echo 'Parameter --type is required.' >&2
exit 1
}
type_is=$(cat "${__object:?}/explorer/type")
type_should=$(cat "${__object:?}/parameter/type")
if test -n "${type_is}"
then
sect_type=${type_is}
else
sect_type=${type_should##*.}
fi
if test -z "${section}"
then
# No section exists and --match was used.
# So we generate a new section identifier from $__object_id.
case ${__object_id:?}
in
(*.*) section=${__object_id:?} ;;
(*) section="${type_should%%.*}.${__object_id:?}" ;;
esac
fi
# Collect option names
if test -f "${__object:?}/parameter/list"
then
listnames_should=$(
sed -e 's/=.*$//' "${__object:?}/parameter/list" | sort -u)
fi
if test -f "${__object:?}/parameter/option"
then
optnames_should=$(
sed -e 's/=.*$//' "${__object:?}/parameter/option" | sort -u)
fi
# Make sure the section itself is present
if test "${state_is}" = absent \
|| test "${type_is}" != "${type_should#*.}"
then
printf 'set %s\n' "${section}" >>"${__messages_out:?}"
# shellcheck disable=SC2140
uci_cmd set "${section}"="${sect_type}"
fi
# Delete options/lists not in "should"
sed -e 's/=.*$//' "${__object:?}/explorer/options" \
| while read -r _optname
do
grep_line "${_optname}" "${listnames_should}" "${optnames_should}" || {
printf 'delete %s\n' "${section}.${_optname}" >>"${__messages_out:?}"
uci_cmd delete "${section}.${_optname}"
} </dev/null
done
opt_proc_error() {
printf 'An error occurred during processing of option %s\n' "${1:?}" >&2
exit 1
}
# Set "should" options
echo "${optnames_should}" \
| grep -e . \
| while read -r _optname
do
_opt_state=$(awk -f "${__type:?}/files/option_state.awk" option "${_optname}") \
|| opt_proc_error "${_optname}"
case ${_opt_state}
in
(invalid)
opt_proc_error "${_optname}"
;;
(present)
;;
(*)
printf 'set %s\n' "${section}.${_optname}" >>"${__messages_out:?}"
# shellcheck disable=SC2140
uci_cmd set "${section}.${_optname}"="$(
grep -e "^${_optname}=" "${__object:?}/parameter/option" \
| sed -e 's/^.*=//' \
| unquote_lines \
| head -n 1)"
;;
esac
done
echo "${listnames_should}" \
| grep -e . \
| while read -r _optname
do
_list_state=$(awk -f "${__type:?}/files/option_state.awk" list "${_optname}") \
|| opt_proc_error "${_optname}"
case ${_list_state}
in
(invalid)
opt_proc_error "${_optname}"
;;
(present)
;;
(*)
printf 'set_list %s\n' "${section}.${_optname}" >>"${__messages_out:?}"
if test "${_list_state}" != absent
then
uci_cmd delete "${section}.${_optname}"
fi
grep "^${_optname}=" "${__object:?}/parameter/list" \
| sed -e 's/^.*=//' \
| unquote_lines \
| while read -r _value
do
# shellcheck disable=SC2140
uci_cmd add_list "${section}.${_optname}"="${_value}"
done
;;
esac
done
;;
(absent)
if test "${state_is}" = absent
then
# if explorer found no section there is nothing to delete
exit 0
fi
printf 'delete %s\n' "${section}" >>"${__messages_out:?}"
uci_cmd delete "${section}"
;;
esac
if test -s "${__object:?}/files/uci_batch.txt"
then
cat "${__type:?}/files/uci_apply.sh"
printf "uci_apply <<'EOF'\n"
cat "${__object:?}/files/uci_batch.txt"
printf '\nEOF\n'
fi

View file

@ -0,0 +1,119 @@
cdist-type__uci_section(7)
==========================
NAME
----
cdist-type__uci_section - Manage configuration sections in UCI
DESCRIPTION
-----------
This cdist type can be used to replace whole configuration sections in OpenWrt's
Unified Configuration Interface (UCI) system.
It can be thought of as syntactic sugar for :strong:`cdist-type__uci`\ (7),
as this type will generate the required `__uci` objects to make the section
contain exactly the options specified using ``--option``.
Since many default UCI sections are unnamed, this type allows to find the
matching section by one of its options using the ``--match`` parameter.
**NOTE:** Options already present on the target and not listed in ``--option``
or ``--list`` will be deleted.
REQUIRED PARAMETERS
-------------------
None.
OPTIONAL PARAMETERS
-------------------
list
An option that is part of a list and should be present in the section (as
part of a list). Lists with multiple options can be expressed by using the
same ``<option>`` repeatedly.
The value to this parameter is a ``<option>=<value>`` string.
``<value>`` does not need special quoting for UCI.
The only requirement is that the value is passed to the type as a single
shell argument.
match
Allows to find a section to "replace" through one of its parameters.
The value to this parameter is a ``<option>=<value>`` string.
option
An option that should be present in the section.
This parameter can be used multiple times to specify multiple options.
The value to this parameter is a ``<option>=<value>`` string.
``<value>`` does not need special quoting for UCI.
The only requirement is that the value is passed to the type as a single
shell argument.
state
``present`` or ``absent``, defaults to ``present``.
type
The type of the section in the format: ``<config>.<section-type>``
BOOLEAN PARAMETERS
------------------
None.
EXAMPLES
--------
.. code-block:: sh
# Configure the dropbear daemon
__uci_section dropbear --type dropbear.dropbear \
--match Port=22 --option Port=22 \
--option PasswordAuth=off \
--option RootPasswordAuth=off
# Define a firewall zone comprised of lan and wlan networks
__uci_section firewall.internal --type firewall.zone \
--option name='internal' \
--list network='lan' \
--list network='wlan' \
--option input='ACCEPT' \
--option output='ACCEPT' \
--option forward='ACCEPT'
# Block SSH access from the guest network
__uci_section firewall.block_ssh_from_guest --type firewall.rule \
--option name='Block-SSH-Access-from-Guest' \
--option src='guest' \
--option proto='tcp' \
--option dest_port='22' \
--option target='REJECT'
# Configure a Wi-Fi access point
__uci_section wireless.default_radio0 --type wireless.wifi-iface \
--option device='radio0' \
--option mode='ap' \
--option network='wlan' \
--option ssid='mywifi' \
--option encryption="psk2' \
--option key='hunter2'
SEE ALSO
--------
- https://openwrt.org/docs/guide-user/base-system/uci
- :strong:`cdist-type__uci`\ (7)
AUTHORS
-------
Dennis Camera <dennis.camera@ssrq-sds-fds.ch>
COPYING
-------
Copyright \(C) 2020 Dennis Camera. You can redistribute it
and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

View file

@ -0,0 +1,88 @@
#!/bin/sh -e
#
# 2020 Dennis Camera (dennis.camera@ssrq-sds-fds.ch)
#
# This file is part of cdist.
#
# cdist is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# cdist is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with cdist. If not, see <http://www.gnu.org/licenses/>.
#
# shellcheck source=cdist/conf/type/__uci_section/files/functions.sh
. "${__type:?}/files/functions.sh"
## Check section name and error if invalid!
case ${__object_id:?}
in
(*.*)
uci_validate_name "${__object_id%%.*}" || {
printf 'Invalid package name: %s\n' "${__object_id%%.*}" >&2
exit 1
}
uci_validate_name "${__object_id#*.}" || {
printf 'Invalid section name: %s\n' "${__object_id#*.}" >&2
exit 1
}
;;
(*)
uci_validate_name "${__object_id:?}" || {
printf 'Invalid section name: %s\n' "${__object_id:?}" >&2
exit 1
}
;;
esac
state_should=$(cat "${__object:?}/parameter/state")
case $state_should
in
(present)
test -f "${__object:?}/parameter/type" || {
echo 'Parameter --type is required.' >&2
exit 1
}
type_is=$(cat "${__object:?}/explorer/type")
type_should=$(cat "${__object:?}/parameter/type")
if test -n "${type_is}" && test "${type_is}" != "${type_should##*.}"
then
# Check if section type matches (section exists and --type provided)
printf 'Section type "%s" does not match --type "%s".\n' \
"${type_is}" "${type_should}" >&2
exit 1
fi
# Check options for syntax errors
validate_options "${__object:?}/parameter/list" "${__object:?}/parameter/object" \
| print_errors 'Found erroneous options in arguments:'
# Check for duplicate option names
if test -s "${__object:?}/parameter/option"
then
sed -e 's/=.*$//' "${__object:?}/parameter/option" \
| sort \
| uniq -d \
| print_errors \
'Found duplicate --options:' \
"$(printf '\nUse --list for lists, instead.')"
fi
;;
(absent)
:
;;
(*)
printf 'Invalid --state: %s\n' "${state_should}" >&2
exit 1
;;
esac

View file

@ -0,0 +1 @@
present

View file

@ -0,0 +1 @@
default

View file

@ -0,0 +1,4 @@
match
state
transaction
type

View file

@ -0,0 +1,2 @@
list
option

View file

@ -0,0 +1,4 @@
#!/bin/sh -e
update-alternatives --display "$__object_id" 2>/dev/null \
| awk -F ' - ' '/priority [0-9]+$/ { print $1 }'

View file

@ -0,0 +1,40 @@
#!/bin/sh -e
# fedora's (update-)alternatives --display output doesn't have
# "link <name> is <path>" line, but debian does. so, let's find
# out how they store this information.
#
# debian and friends:
# https://salsa.debian.org/dpkg-team/dpkg/-/blob/master/utils/update-alternatives.c
# see calls to altdb_print_line function
#
# fedora and friends:
# https://github.com/fedora-sysv/chkconfig/blob/master/alternatives.c
# see calls to parseLine function
#
# conclusion: it is safe to assume that (master) link is on second line
for altdir in \
/var/lib/dpkg/alternatives \
/var/lib/alternatives
do
if [ ! -f "$altdir/$__object_id" ]
then
continue
fi
link="$( awk 'NR==2' "$altdir/$__object_id" )"
if [ -n "$link" ]
then
break
fi
done
if [ -z "$link" ]
then
echo "unable to get link for $__object_id" >&2
exit 1
fi
echo "$link"

View file

@ -0,0 +1,12 @@
#!/bin/sh -e
path_is="$( update-alternatives --display "$__object_id" 2>/dev/null \
| awk '/link currently points to/ {print $5}' )"
if [ -z "$path_is" ]
then
echo "unable to get current path for $__object_id" >&2
exit 1
fi
echo "$path_is"

View file

@ -0,0 +1,8 @@
#!/bin/sh -e
if [ -f "$( cat "$__object/parameter/path" )" ]
then
echo 'present'
else
echo 'absent'
fi

View file

@ -1,8 +0,0 @@
#!/bin/sh -e
path="$(cat "$__object/parameter/path")"
name="$__object_id"
link="$(readlink "/etc/alternatives/$name")"
if [ "$path" = "$link" ]
then echo present
else echo absent
fi

View file

@ -1,6 +1,7 @@
#!/bin/sh -e #!/bin/sh -e
# #
# 2013 Nico Schottelius (nico-cdist at schottelius.org) # 2013 Nico Schottelius (nico-cdist at schottelius.org)
# 2020 Ander Punnar (ander@kvlt.ee)
# #
# This file is part of cdist. # This file is part of cdist.
# #
@ -16,12 +17,38 @@
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with cdist. If not, see <http://www.gnu.org/licenses/>. # along with cdist. If not, see <http://www.gnu.org/licenses/>.
#
if [ "$(cat "$__object/explorer/state")" = 'present' ] path_is="$( cat "$__object/explorer/path_is" )"
then exit 0
path_should="$( cat "$__object/parameter/path" )"
if [ "$path_is" = "$path_should" ]
then
exit 0
fi
if [ "$( cat "$__object/explorer/path_should_state" )" = 'absent' ] && [ -z "$__cdist_dry_run" ]
then
echo "$path_should does not exist in target" >&2
exit 1
fi fi
path="$(cat "$__object/parameter/path")"
name="$__object_id" name="$__object_id"
echo "update-alternatives --quiet --set '$name' '$path'"
alternatives="$( cat "$__object/explorer/alternatives" )"
if ! echo "$alternatives" | grep -Fxq "$path_should"
then
if [ ! -f "$__object/parameter/install" ]
then
echo "$path_should is not in $name alternatives." >&2
echo 'Please install missing packages or use --install to add path to alternatives.' >&2
exit 1
fi
link="$( cat "$__object/explorer/link" )"
echo "update-alternatives --install '$link' '$name' '$path_should' 1000"
fi
echo "update-alternatives --set '$name' '$path_should'"

View file

@ -19,6 +19,12 @@ path
Use this path for the given alternative Use this path for the given alternative
BOOLEAN PARAMETERS
------------------
install
Add (``update-alternatives --install``) missing path to alternatives.
EXAMPLES EXAMPLES
-------- --------
@ -36,11 +42,12 @@ SEE ALSO
AUTHORS AUTHORS
------- -------
Nico Schottelius <nico-cdist--@--schottelius.org> Nico Schottelius <nico-cdist--@--schottelius.org>
Ander Punnar <ander@kvlt.ee>
COPYING COPYING
------- -------
Copyright \(C) 2013 Nico Schottelius. You can redistribute it Copyright \(C) 2013 Nico Schottelius and 2020 Ander Punnar. You can
and/or modify it under the terms of the GNU General Public License as redistribute it and/or modify it under the terms of the GNU General Public
published by the Free Software Foundation, either version 3 of the License as published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version. License, or (at your option) any later version.

View file

@ -0,0 +1 @@
install

View file

@ -178,9 +178,11 @@ class Config:
raise cdist.Error(("Cannot read both, manifest and host file, " raise cdist.Error(("Cannot read both, manifest and host file, "
"from stdin")) "from stdin"))
# if no host source is specified then read hosts from stdin
if not (args.hostfile or args.host): if not (args.hostfile or args.host):
args.hostfile = '-' if args.tag or args.all_tagged_hosts:
raise cdist.Error(("Target host tag(s) missing"))
else:
raise cdist.Error(("Target host(s) missing"))
if args.manifest == '-': if args.manifest == '-':
# read initial manifest from stdin # read initial manifest from stdin

View file

@ -299,7 +299,7 @@ class InventoryHost(Inventory):
self.all = all self.all = all
if not self.hosts and not self.hostfile: if not self.hosts and not self.hostfile:
self.hostfile = "-" raise cdist.Error("Host(s) missing")
def _new_hostpath(self, hostpath): def _new_hostpath(self, hostpath):
# create empty file # create empty file
@ -355,7 +355,7 @@ class InventoryTag(Inventory):
else: else:
self.allhosts = False self.allhosts = False
if not self.tags and not self.tagfile: if not self.tags and not self.tagfile:
self.tagfile = "-" raise cdist.Error("Tag(s) missing")
if self.hostfile == "-" and self.tagfile == "-": if self.hostfile == "-" and self.tagfile == "-":
raise cdist.Error("Cannot read both, hosts and tags, from stdin") raise cdist.Error("Cannot read both, hosts and tags, from stdin")

55
cdist/scan/commandline.py Normal file
View file

@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
#
# 2020 Nico Schottelius (nico-cdist at schottelius.org)
#
# This file is part of cdist.
#
# cdist is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# cdist is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with cdist. If not, see <http://www.gnu.org/licenses/>.
#
#
import logging
log = logging.getLogger("scan")
# define this outside of the class to not handle scapy import errors by default
def commandline(args):
log.debug(args)
try:
import cdist.scan.scan as scan
except ModuleNotFoundError:
print('cdist scan requires scapy to be installed')
processes = []
if not args.mode:
# By default scan and trigger, but do not call any action
args.mode = ['scan', 'trigger', ]
if 'trigger' in args.mode:
t = scan.Trigger(interfaces=args.interfaces)
t.start()
processes.append(t)
log.debug("Trigger started")
if 'scan' in args.mode:
s = scan.Scanner(interfaces=args.interfaces, args=args)
s.start()
processes.append(s)
log.debug("Scanner started")
for process in processes:
process.join()

180
cdist/scan/scan.py Normal file
View file

@ -0,0 +1,180 @@
# -*- coding: utf-8 -*-
#
# 2020 Nico Schottelius (nico-cdist at schottelius.org)
#
# This file is part of cdist.
#
# cdist is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# cdist is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with cdist. If not, see <http://www.gnu.org/licenses/>.
#
#
#
# Interface to be implemented:
# - cdist scan --mode {scan, trigger, install, config}, --mode can be repeated
# scan: scan / listen for icmp6 replies
# trigger: send trigger to multicast
# config: configure newly detected hosts
# install: install newly detected hosts
#
# Scanner logic
# - save results to configdir:
# basedir = ~/.cdist/scan/<ipv6-address>
# last_seen = ~/.cdist/scan/<ipv6-address>/last_seen -- record unix time
# or similar
# last_configured = ~/.cdist/scan/<ipv6-address>/last_configured -- record
# unix time or similar
# last_installed = ~/.cdist/scan/<ipv6-address>/last_configured -- record
# unix time or similar
#
#
#
#
# cdist scan --list
# Show all known hosts including last seen flag
#
# Logic for reconfiguration:
#
# - record when configured last time
# - introduce a parameter --reconfigure-after that takes time argument
# - reconfigure if a) host alive and b) reconfigure-after time passed
#
from multiprocessing import Process
import os
import logging
from scapy.all import *
# Datetime overwrites scapy.all.datetime - needs to be imported AFTER
import datetime
log = logging.getLogger("scan")
class Trigger(object):
"""
Trigger an ICMPv6EchoReply from all hosts that are alive
"""
def __init__(self, interfaces=None, verbose=False):
self.interfaces = interfaces
self.verbose = verbose
# Wait 5 seconds before triggering again - FIXME: add parameter
self.sleeptime = 5
def start(self):
self.processes = []
for interface in self.interfaces:
p = Process(target=self.run_interface, args=(interface,))
self.processes.append(p)
p.start()
def join(self):
for process in self.processes:
process.join()
def run_interface(self, interface):
while True:
self.trigger(interface)
time.sleep(self.sleeptime)
def trigger(self, interface):
packet = IPv6(dst=f"ff02::1%{interface}") / ICMPv6EchoRequest()
log.debug(f"Sending request on {interface}")
send(packet, verbose=self.verbose)
class Scanner(object):
"""
Scan for replies of hosts, maintain the up-to-date database
"""
def __init__(self, interfaces=None, args=None, outdir=None):
self.interfaces = interfaces
if outdir:
self.outdir = outdir
else:
self.outdir = os.path.join(os.environ['HOME'], '.cdist', 'scan')
def handle_pkg(self, pkg):
if ICMPv6EchoReply in pkg:
host = pkg['IPv6'].src
log.verbose(f"Host {host} is alive")
dir = os.path.join(self.outdir, host)
fname = os.path.join(dir, "last_seen")
now = datetime.datetime.now()
os.makedirs(dir, exist_ok=True)
# FIXME: maybe adjust the format so we can easily parse again
with open(fname, "w") as fd:
fd.write(f"{now}\n")
def start(self):
self.process = Process(target=self.scan)
self.process.start()
def join(self):
self.process.join()
def scan(self):
log.debug("Scanning - zzzzz")
sniff(iface=self.interfaces,
filter="icmp6",
prn=self.handle_pkg)
if __name__ == '__main__':
t = Trigger(interfaces=["wlan0"])
t.start()
# Scanner can listen on many interfaces at the same time
s = Scanner(interfaces=["wlan0"])
s.scan()
# Join back the trigger processes
t.join()
# Test in my lan shows:
# [18:48] bridge:cdist% ls -1d fe80::*
# fe80::142d:f0a5:725b:1103
# fe80::20d:b9ff:fe49:ac11
# fe80::20d:b9ff:fe4c:547d
# fe80::219:d2ff:feb2:2e12
# fe80::21b:fcff:feee:f446
# fe80::21b:fcff:feee:f45c
# fe80::21b:fcff:feee:f4b1
# fe80::21b:fcff:feee:f4ba
# fe80::21b:fcff:feee:f4bc
# fe80::21b:fcff:feee:f4c1
# fe80::21d:72ff:fe86:46b
# fe80::42b0:34ff:fe6f:f6f0
# fe80::42b0:34ff:fe6f:f863
# fe80::42b0:34ff:fe6f:f9b2
# fe80::4a5d:60ff:fea1:e55f
# fe80::77a3:5e3f:82cc:f2e5
# fe80::9e93:4eff:fe6c:c1f4
# fe80::ba69:f4ff:fec5:6041
# fe80::ba69:f4ff:fec5:8db7
# fe80::bad8:12ff:fe65:313d
# fe80::bad8:12ff:fe65:d9b1
# fe80::ce2d:e0ff:fed4:2611
# fe80::ce32:e5ff:fe79:7ea7
# fe80::d66d:6dff:fe33:e00
# fe80::e2ff:f7ff:fe00:20e6
# fe80::f29f:c2ff:fe7c:275e

View file

@ -307,11 +307,10 @@ class InventoryTestCase(test.CdistTestCase):
raise e raise e
# InventoryTag # InventoryTag
@unittest.expectedFailure
def test_inventory_tag_init(self): def test_inventory_tag_init(self):
invTag = inventory.InventoryTag(db_basedir=inventory_dir, invTag = inventory.InventoryTag(db_basedir=inventory_dir,
action="add") action="add")
self.assertTrue(invTag.allhosts)
self.assertEqual(invTag.tagfile, "-")
def test_inventory_tag_stdin_multiple_hosts(self): def test_inventory_tag_stdin_multiple_hosts(self):
try: try:

View file

@ -5,6 +5,21 @@ next:
* Core: Add trigger functionality (Nico Schottelius, Darko Poljak) * Core: Add trigger functionality (Nico Schottelius, Darko Poljak)
* Core: Implement core support for python types (Darko Poljak) * Core: Implement core support for python types (Darko Poljak)
6.9.0: 2020-11-07
* Core: Clarify stdin input (Darko Poljak)
* Type __package_pip: Detect pip binary (Ander Punnar)
* Documentation: Add custom remote copy/exec examples (Darko Poljak)
* Type __package_pkgng_freebsd: Bootstrap pkg if necessary (Evil Ham)
* Type __service: Fix calling __systemd_service (Mark Verboom)
* Type __line: Add 'replace' state (Evil Ham)
* Type __download: Fix man page (Matthias Stecher)
* Type __acl: Remove deprecated parameters, fix bugs (Ander Punnar)
* Type __update_alternatives: Rewrite, support --install (Ander Punnar)
* Type __file: Fix state pre-exists (Dennis Camera)
* Type __apt_norecommends: Use 00InstallRecommends file as debian-installer does (Dennis Camera)
* New types: __uci, __uci_section (Dennis Camera)
* Core: Introduce scanner (noninvasive, beta) (Nico Schottelius)
6.8.0: 2020-09-11 6.8.0: 2020-09-11
* Type __locale_system: Fix for debian and ubuntu (Ander Punnar) * Type __locale_system: Fix for debian and ubuntu (Ander Punnar)
* Type __unpack: Add --tar-extra-args parameter (Ander Punnar) * Type __unpack: Add --tar-extra-args parameter (Ander Punnar)

View file

@ -0,0 +1,57 @@
* The scanner, 2020-10-29, Hacking Villa Diesbach
** Motivation
- The purpose of cdist is to ensure systems are in a configured state
- If systems reboot into a clean (think: netboot) state they are
stuck in an unconfigured mode
- We can either trigger *from* those machines
- this is what cdist trigger is for
- Or we can regulary *scan* for machines
- This method does not need any modification to standard OS
** How it works
- cdist scan uses the all nodes multicast group ff02::1
- It sends a ping packet there in regular intervals
- This even works in non-IPv6 networks, as all operating systems
are IPv6 capable and usually IPv6 enabled by default
- Link local is always accessible!
- cdist scan receives an answer from all alive hosts
- These results are stored in ~/.cdist/scan/${hostip}
- We record the last_seen date ~/.cdist/scan/${hostip}/last_seen
- After a host is detected, cdist *can* try to configure it
- It saves the result (+/- logging needs to be defined) in
~/.cdist/scan/${hostip}/{config, install}_result
- If logging is saved: maybe in ~/.cdist/scan/${hostip}/{config, install}_log
- Final naming TBD
** Benefits from the scanning approach
- We know when a host is alive/dead
- We can use standard OS w/o trigger customisation
- Only requirement: we can ssh into it
- Can make use f.i. of Alpine Linux w/ ssh keys feeding in
- We can trigger regular reconfiguration
- If alive && last_config_time > 1d -> reconfigure
- Data can be exported to f.i. prometheus
- Record when configured (successfully)
- Record when seen
- Enables configurations in stateless environments
** Sample output v2020-10-29
23:14] bridge:~% sudo cdist scan -b -I wlan0 -vv
VERBOSE: cdist: version 6.8.0-36-g91d99bf0
VERBOSE: scan: Host fe80::21d:72ff:fe86:46b is alive
VERBOSE: scan: Host fe80::ce2d:e0ff:fed4:2611 is alive
VERBOSE: scan: Host fe80::21b:fcff:feee:f4c1 is alive
VERBOSE: scan: Host fe80::e2ff:f7ff:fe00:20e6 is alive
VERBOSE: scan: Host fe80::20d:b9ff:fe49:ac11 is alive
VERBOSE: scan: Host fe80::9e93:4eff:fe6c:c1f4 is alive
VERBOSE: scan: Host fe80::ce32:e5ff:fe79:7ea7 is alive
VERBOSE: scan: Host fe80::219:d2ff:feb2:2e12 is alive
VERBOSE: scan: Host fe80::d66d:6dff:fe33:e00 is alive
VERBOSE: scan: Host fe80::21b:fcff:feee:f446 is alive
VERBOSE: scan: Host fe80::21b:fcff:feee:f4b1 is alive
VERBOSE: scan: Host fe80::20d:b9ff:fe4c:547d is alive
VERBOSE: scan: Host fe80::bad8:12ff:fe65:313d is alive
VERBOSE: scan: Host fe80::42b0:34ff:fe6f:f6f0 is alive
VERBOSE: scan: Host fe80::ba69:f4ff:fec5:6041 is alive
VERBOSE: scan: Host fe80::f29f:c2ff:fe7c:275e is alive
VERBOSE: scan: Host fe80::ba69:f4ff:fec5:8db7 is alive
VERBOSE: scan: Host fe80::42b0:34ff:fe6f:f863 is alive
VERBOSE: scan: Host fe80::21b:fcff:feee:f4bc is alive
...

View file

@ -2,10 +2,10 @@
# #
# You can set these variables from the command line. # You can set these variables from the command line.
SPHINXOPTS = SPHINXOPTS ?=
SPHINXBUILD = sphinx-build SPHINXBUILD ?= sphinx-build
PAPER = PAPER ?=
BUILDDIR = ../dist BUILDDIR ?= ../dist
# for cache, etc. # for cache, etc.
_BUILDDIR = _build _BUILDDIR = _build

View file

@ -10,7 +10,7 @@ By default this is accomplished with ssh and scp respectively.
The default implementations used by cdist are:: The default implementations used by cdist are::
__remote_exec: ssh -o User=root __remote_exec: ssh -o User=root
__remote_copy: scp -o User=root __remote_copy: scp -o User=root -q
The user can override these defaults by providing custom implementations and The user can override these defaults by providing custom implementations and
passing them to cdist with the --remote-exec and/or --remote-copy arguments. passing them to cdist with the --remote-exec and/or --remote-copy arguments.
@ -26,3 +26,390 @@ specified by enclosed in square brackets (see :strong:`ssh`\ (1) and
With this simple interface the user can take total control of how cdist With this simple interface the user can take total control of how cdist
interacts with the target when required, while the default implementation interacts with the target when required, while the default implementation
remains as simple as possible. remains as simple as possible.
Examples
--------
Here are examples of using alternative __remote_copy and __remote_exec scripts.
All scripts from below are present in cdist sources in `other/examples/remote`
directory.
ssh
~~~
Same as cdist default.
**copy**
Usage: cdist config --remote-copy "/path/to/this/script" target_host
.. code-block:: sh
#echo "$@" | logger -t "cdist-ssh-copy"
scp -o User=root -q $@
**exec**
Usage: cdist config --remote-exec "/path/to/this/script" target_host
.. code-block:: sh
#echo "$@" | logger -t "cdist-ssh-exec"
ssh -o User=root $@
local
~~~~~
This effectively turns remote calling into local calling. Probably most useful
for the unit testing.
**copy**
.. code-block:: sh
code="$(echo "$@" | sed "s|\([[:space:]]\)$__target_host:|\1|g")"
cp -L $code
**exec**
.. code-block:: sh
target_host=$1; shift
echo "$@" | /bin/sh
chroot
~~~~~~
**copy**
Usage: cdist config --remote-copy "/path/to/this/script /path/to/your/chroot" target-id
.. code-block:: sh
log() {
#echo "$@" | logger -t "cdist-chroot-copy"
:
}
chroot="$1"; shift
target_host="$__target_host"
# replace target_host with chroot location
code="$(echo "$@" | sed "s|$target_host:|$chroot|g")"
log "target_host: $target_host"
log "chroot: $chroot"
log "$@"
log "$code"
# copy files into chroot
cp $code
log "-----"
**exec**
Usage: cdist config --remote-exec "/path/to/this/script /path/to/your/chroot" target-id
.. code-block:: sh
log() {
#echo "$@" | logger -t "cdist-chroot-exec"
:
}
chroot="$1"; shift
target_host="$1"; shift
script=$(mktemp "${chroot}/tmp/chroot-${0##*/}.XXXXXXXXXX")
trap cleanup INT TERM EXIT
cleanup() {
[ $__cdist_debug ] || rm "$script"
}
log "target_host: $target_host"
log "script: $script"
log "@: $@"
echo "#!/bin/sh -l" > "$script"
echo "$@" >> "$script"
chmod +x "$script"
relative_script="${script#$chroot}"
log "relative_script: $relative_script"
# run in chroot
chroot "$chroot" "$relative_script"
log "-----"
rsync
~~~~~
**copy**
Usage: cdist config --remote-copy /path/to/this/script target_host
.. code-block:: sh
# For rsync to do the right thing, the source has to end with "/" if it is
# a directory. The below preprocessor loop takes care of that.
# second last argument is the source
source_index=$(($#-1))
index=0
for arg in $@; do
if [ $index -eq 0 ]; then
# reset $@
set --
fi
index=$((index+=1))
if [ $index -eq $source_index -a -d "$arg" ]; then
arg="${arg%/}/"
fi
set -- "$@" "$arg"
done
rsync --backup --suffix=~cdist -e 'ssh -o User=root' $@
schroot
~~~~~~~
__remote_copy and __remote_exec scripts to run cdist against a chroot on the
target host over ssh.
**copy**
Usage: cdist config --remote-copy "/path/to/this/script schroot-chroot-name" target_host
.. code-block:: sh
log() {
#echo "$@" | logger -t "cdist-schroot-copy"
:
}
chroot_name="$1"; shift
target_host="$__target_host"
# get directory for given chroot_name
chroot="$(ssh -o User=root -q $target_host schroot -c $chroot_name --config | awk -F = '/directory=/ {print $2}')"
# prefix destination with chroot
code="$(echo "$@" | sed "s|$target_host:|$target_host:$chroot|g")"
log "target_host: $target_host"
log "chroot_name: $chroot_name"
log "chroot: $chroot"
log "@: $@"
log "code: $code"
# copy files into remote chroot
scp -o User=root -q $code
log "-----"
**exec**
Usage: cdist config --remote-exec "/path/to/this/script schroot-chroot-name" target_host
.. code-block:: sh
log() {
#echo "$@" | logger -t "cdist-schroot-exec"
:
}
chroot_name="$1"; shift
target_host="$1"; shift
code="ssh -o User=root -q $target_host schroot -c $chroot_name -- $@"
log "target_host: $target_host"
log "chroot_name: $chroot_name"
log "@: $@"
log "code: $code"
# run in remote chroot
$code
log "-----"
schroot-uri
~~~~~~~~~~~
__remote_exec/__remote_copy script to run cdist against a schroot target URI.
Usage::
cdist config \
--remote-exec "/path/to/this/script exec" \
--remote-copy "/path/to/this/script copy" \
target_uri
# target_uri examples:
schroot:///chroot-name
schroot://foo.ethz.ch/chroot-name
schroot://user-name@foo.ethz.ch/chroot-name
# and how to match them in .../manifest/init
case "$target_host" in
schroot://*)
# any schroot
;;
schroot://foo.ethz.ch/*)
# any schroot on specific host
;;
schroot://foo.ethz.ch/chroot-name)
# specific schroot on specific host
;;
schroot:///chroot-name)
# specific schroot on localhost
;;
esac
**copy/exec**
.. code-block:: sh
my_name="${0##*/}"
mode="$1"; shift
log() {
# uncomment me for debugging
#echo "$@" | logger -t "cdist-$my_name-$mode"
:
}
die() {
echo "$@" >&2
exit 1
}
uri="$__target_host"
scheme="${uri%%:*}"; rest="${uri#$scheme:}"; rest="${rest#//}"
authority="${rest%%/*}"; rest="${rest#$authority}"
path="${rest%\?*}"; rest="${rest#$path}"
schroot_name="${path#/}"
[ "$scheme" = "schroot" ] || die "Failed to parse scheme from __target_host ($__target_host). Expected 'schroot', got '$scheme'"
[ -n "$schroot_name" ] || die "Failed to parse schroot name from __target_host: $__target_host"
case "$authority" in
'')
# authority is empty, neither user nor host given
user=""
host=""
;;
*@*)
# authority contains @, take user from authority
user="${authority%@*}"
host="${authority#*@}"
;;
*)
# no user in authority, default to root
user="root"
host="$authority"
;;
esac
log "mode: $mode"
log "@: $@"
log "uri: $uri"
log "scheme: $scheme"
log "authority: $authority"
log "user: $user"
log "host: $host"
log "path: $path"
log "schroot_name: $schroot_name"
exec_prefix=""
copy_prefix=""
if [ -n "$host" ]; then
# we are working on a remote host
exec_prefix="ssh -o User=$user -q $host"
copy_prefix="scp -o User=$user -q"
copy_destination_prefix="$host:"
else
# working on local machine
copy_prefix="cp"
copy_destination_prefix=""
fi
log "exec_prefix: $exec_prefix"
log "copy_prefix: $copy_prefix"
log "copy_destination_prefix: $copy_destination_prefix"
case "$mode" in
exec)
# In exec mode the first argument is the __target_host which we already got from env. Get rid of it.
shift
code="$exec_prefix schroot -c $schroot_name -- sh -c '$@'"
;;
copy)
# get directory for given chroot_name
schroot_directory="$($exec_prefix schroot -c $schroot_name --config | awk -F = '/directory=/ {print $2}')"
[ -n "$schroot_directory" ] || die "Failed to retreive schroot directory for schroot: $schroot_name"
log "schroot_directory: $schroot_directory"
# prefix destination with chroot
code="$copy_prefix $(echo "$@" | sed "s|$uri:|${copy_destination_prefix}${schroot_directory}|g")"
;;
*) die "Unknown mode: $mode";;
esac
log "code: $code"
# Run the code
$code
log "-----"
sudo
~~~~
**copy**
Use rsync over ssh to copy files. Uses the "--rsync-path" option
to run the remote rsync instance with sudo.
This command assumes your ssh configuration is already set up in ~/.ssh/config.
Usage: cdist config --remote-copy /path/to/this/script target_host
.. code-block:: sh
# For rsync to do the right thing, the source has to end with "/" if it is
# a directory. The below preprocessor loop takes care of that.
# second last argument is the source
source_index=$(($#-1))
index=0
for arg in $@; do
if [ $index -eq 0 ]; then
# reset $@
set --
fi
index=$((index+=1))
if [ $index -eq $source_index -a -d "$arg" ]; then
arg="${arg%/}/"
fi
set -- "$@" "$arg"
done
rsync --copy-links --rsync-path="sudo rsync" -e 'ssh' "$@"
**exec**
Prefixes all remote commands with sudo.
This command assumes your ssh configuration is already set up in ~/.ssh/config.
Usage: cdist config --remote-exec "/path/to/this/script" target_host
.. code-block:: sh
host="$1"; shift
ssh -q "$host" sudo sh -c \""$@"\"

View file

@ -187,10 +187,8 @@ Install command is currently in beta.
**-f HOSTFILE, --file HOSTFILE** **-f HOSTFILE, --file HOSTFILE**
Read specified file for a list of additional hosts to operate on Read specified file for a list of additional hosts to operate on
or if '-' is given, read stdin (one host per line). or if '-' is given, read stdin (one host per line). For the file
If no host or host file is specified then, by default, format see :strong:`HOSTFILE FORMAT` below.
read hosts from stdin. For the file format see
:strong:`HOSTFILE FORMAT` below.
**-g CONFIG_FILE, --config-file CONFIG_FILE** **-g CONFIG_FILE, --config-file CONFIG_FILE**
Use specified custom configuration file. Use specified custom configuration file.
@ -309,9 +307,8 @@ Add host(s) to inventory database.
**-f HOSTFILE, --file HOSTFILE** **-f HOSTFILE, --file HOSTFILE**
Read additional hosts to add from specified file or Read additional hosts to add from specified file or
from stdin if '-' (each host on separate line). If no from stdin if '-' (each host on separate line).
host or host file is specified then, by default, read Hostfile format is the same as config hostfile format.
from stdin. Hostfile format is the same as config hostfile format.
**-g CONFIG_FILE, --config-file CONFIG_FILE** **-g CONFIG_FILE, --config-file CONFIG_FILE**
Use specified custom configuration file. Use specified custom configuration file.
@ -337,11 +334,8 @@ Add tag(s) to inventory database.
**-f HOSTFILE, --file HOSTFILE** **-f HOSTFILE, --file HOSTFILE**
Read additional hosts to add tags from specified file Read additional hosts to add tags from specified file
or from stdin if '-' (each host on separate line). If or from stdin if '-' (each host on separate line).
no host or host file is specified then, by default, Hostfile format is the same as config hostfile format.
read from stdin. If no tags/tagfile nor hosts/hostfile
are specified then tags are read from stdin and are
added to all hosts. Hostfile format is the same as config hostfile format.
**-g CONFIG_FILE, --config-file CONFIG_FILE** **-g CONFIG_FILE, --config-file CONFIG_FILE**
Use specified custom configuration file. Use specified custom configuration file.
@ -356,11 +350,8 @@ Add tag(s) to inventory database.
**-T TAGFILE, --tag-file TAGFILE** **-T TAGFILE, --tag-file TAGFILE**
Read additional tags to add from specified file or Read additional tags to add from specified file or
from stdin if '-' (each tag on separate line). If no from stdin if '-' (each tag on separate line).
tag or tag file is specified then, by default, read Tagfile format is the same as config hostfile format.
from stdin. If no tags/tagfile nor hosts/hostfile are
specified then tags are read from stdin and are added
to all hosts. Tagfile format is the same as config hostfile format.
**-t TAGLIST, --taglist TAGLIST** **-t TAGLIST, --taglist TAGLIST**
Tag list to be added for specified host(s), comma Tag list to be added for specified host(s), comma
@ -382,9 +373,8 @@ Delete host(s) from inventory database.
**-f HOSTFILE, --file HOSTFILE** **-f HOSTFILE, --file HOSTFILE**
Read additional hosts to delete from specified file or Read additional hosts to delete from specified file or
from stdin if '-' (each host on separate line). If no from stdin if '-' (each host on separate line).
host or host file is specified then, by default, read Hostfile format is the same as config hostfile format.
from stdin. Hostfile format is the same as config hostfile format.
**-g CONFIG_FILE, --config-file CONFIG_FILE** **-g CONFIG_FILE, --config-file CONFIG_FILE**
Use specified custom configuration file. Use specified custom configuration file.
@ -414,11 +404,8 @@ Delete tag(s) from inventory database.
**-f HOSTFILE, --file HOSTFILE** **-f HOSTFILE, --file HOSTFILE**
Read additional hosts to delete tags for from Read additional hosts to delete tags for from
specified file or from stdin if '-' (each host on specified file or from stdin if '-' (each host on
separate line). If no host or host file is specified separate line). Hostfile format is the same as
then, by default, read from stdin. If no tags/tagfile config hostfile format.
nor hosts/hostfile are specified then tags are read
from stdin and are deleted from all hosts. Hostfile
format is the same as config hostfile format.
**-g CONFIG_FILE, --config-file CONFIG_FILE** **-g CONFIG_FILE, --config-file CONFIG_FILE**
Use specified custom configuration file. Use specified custom configuration file.
@ -433,11 +420,8 @@ Delete tag(s) from inventory database.
**-T TAGFILE, --tag-file TAGFILE** **-T TAGFILE, --tag-file TAGFILE**
Read additional tags from specified file or from stdin Read additional tags from specified file or from stdin
if '-' (each tag on separate line). If no tag or tag if '-' (each tag on separate line).
file is specified then, by default, read from stdin. Tagfile format is the same as config hostfile format.
If no tags/tagfile nor hosts/hostfile are specified
then tags are read from stdin and are added to all
hosts. Tagfile format is the same as config hostfile format.
**-t TAGLIST, --taglist TAGLIST** **-t TAGLIST, --taglist TAGLIST**
Tag list to be deleted for specified host(s), comma Tag list to be deleted for specified host(s), comma