diff --git a/.gitattributes b/.gitattributes index 45c10d7b..01d20f30 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,5 +4,5 @@ docs/speeches export-ignore docs/video export-ignore docs/src/man7 export-ignore -bin/build-helper export-ignore +bin/cdist-build-helper export-ignore README-maintainers export-ignore diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e215652c..a4bc67aa 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,20 +1,23 @@ +--- +image: code.ungleich.ch:5050/ungleich-public/cdist/cdist-ci:latest + stages: - test -image: code.ungleich.ch:5050/ungleich-public/cdist/cdist-ci:latest - -unit_tests: - stage: test - script: - - ./bin/build-helper version - - ./bin/build-helper test - -pycodestyle: - stage: test - script: - - ./bin/build-helper pycodestyle +before_script: + - ./bin/cdist-build-helper version shellcheck: stage: test script: - - ./bin/build-helper shellcheck + - ./bin/cdist-build-helper shellcheck + +pycodestyle: + stage: test + script: + - ./bin/cdist-build-helper pycodestyle + +unit_tests: + stage: test + script: + - ./bin/cdist-build-helper test diff --git a/Makefile b/Makefile index f89ac1e7..89286310 100644 --- a/Makefile +++ b/Makefile @@ -35,9 +35,9 @@ DOCS_SRC_DIR=./docs/src SPEECHDIR=./docs/speeches TYPEDIR=./cdist/conf/type -SPHINXM=make -C $(DOCS_SRC_DIR) man -SPHINXH=make -C $(DOCS_SRC_DIR) html -SPHINXC=make -C $(DOCS_SRC_DIR) clean +SPHINXM=$(MAKE) -C $(DOCS_SRC_DIR) man +SPHINXH=$(MAKE) -C $(DOCS_SRC_DIR) html +SPHINXC=$(MAKE) -C $(DOCS_SRC_DIR) clean ################################################################################ # Manpages @@ -81,7 +81,7 @@ version: } # Manpages #3: generic part -man: version $(MANTYPES) $(DOCSREF) +man: version configskel $(MANTYPES) $(DOCSREF) $(DOCSTYPESREF) $(SPHINXM) html: version configskel $(MANTYPES) $(DOCSREF) $(DOCSTYPESREF) @@ -104,7 +104,7 @@ DOTMANTYPES=$(subst /man.rst,.rst,$(DOTMANTYPEPREFIX)) $(DOTMAN7DSTDIR)/cdist-type%.rst: $(DOTTYPEDIR)/%/man.rst ln -sf "$^" $@ -dotman: version $(DOTMANTYPES) +dotman: version configskel $(DOTMANTYPES) $(DOCSREF) $(DOCSTYPESREF) $(SPHINXM) ################################################################################ diff --git a/README-maintainers b/README-maintainers index af57f475..5766dd7d 100644 --- a/README-maintainers +++ b/README-maintainers @@ -1,4 +1,4 @@ -Maintainers should use ./bin/build-helper script. +Maintainers should use ./bin/cdist-build-helper script. Makefile is intended for end users. It can be used for non-maintaining targets that can be run from pure source (without git repository). diff --git a/README.md b/README.md index de6901c7..a468dd86 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ For community-maintained types there is ## Participating -IRC: ``#cdist`` @ freenode +IRC: ``#cdist`` @ [libera](https://libera.chat) Matrix: ``#cdist:ungleich.ch`` -Mattermost: https://chat.ungleich.ch/ungleich/channels/cdist +Matrix and IRC are bridged. diff --git a/bin/cdist b/bin/cdist index 645020a1..adb06a8d 100755 --- a/bin/cdist +++ b/bin/cdist @@ -1,7 +1,8 @@ -#!/bin/sh +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # -# 2012 Nico Schottelius (nico-cdist at schottelius.org) +# 2010-2016 Nico Schottelius (nico-cdist at schottelius.org) +# 2016 Darko Poljak (darko.poljak at gmail.com) # # This file is part of cdist. # @@ -20,14 +21,83 @@ # # -# Wrapper for real script to allow execution from checkout -dir=${0%/*} +import logging +import os +import sys -# Ensure version is present - the bundled/shipped version contains a static version, -# the git version contains a dynamic version -"$dir/build-helper" version +# See if this file's parent is cdist module +# and if so add it to module search path. +cdist_dir = os.path.realpath( + os.path.join( + os.path.dirname(os.path.realpath(__file__)), + os.pardir)) +cdist_init_dir = os.path.join(cdist_dir, 'cdist', '__init__.py') +if os.path.exists(cdist_init_dir): + sys.path.insert(0, cdist_dir) -libdir=$(cd "${dir}/../" && pwd -P) -export PYTHONPATH="${libdir}" +import cdist # noqa 402 +import cdist.argparse # noqa 402 +import cdist.banner # noqa 402 +import cdist.config # noqa 402 +import cdist.install # noqa 402 +import cdist.shell # noqa 402 +import cdist.inventory # noqa 402 -"$dir/../scripts/cdist" "$@" + +def commandline(): + """Parse command line""" + + # preos subcommand hack + if len(sys.argv) > 1 and sys.argv[1] == 'preos': + return cdist.preos.PreOS.commandline(sys.argv[1:]) + parser, cfg = cdist.argparse.parse_and_configure(sys.argv[1:]) + args = cfg.get_args() + + # Work around python 3.3 bug: + # http://bugs.python.org/issue16308 + # http://bugs.python.org/issue9253 + + # FIXME: catching AttributeError also hides + # real problems.. try a different way + + # FIXME: we always print main help, not + # the help of the actual parser being used! + try: + getattr(args, "func") + except AttributeError: + parser['main'].print_help() + sys.exit(0) + + args.func(args) + + +if __name__ == "__main__": + if sys.version_info[:3] < cdist.MIN_SUPPORTED_PYTHON_VERSION: + print( + 'Python >= {} is required on the source host.'.format( + ".".join(map(str, cdist.MIN_SUPPORTED_PYTHON_VERSION))), + file=sys.stderr) + sys.exit(1) + + exit_code = 0 + + try: + import re + import os + + if re.match("__", os.path.basename(sys.argv[0])): + import cdist.emulator + emulator = cdist.emulator.Emulator(sys.argv) + emulator.run() + else: + commandline() + + except KeyboardInterrupt: + exit_code = 2 + + except cdist.Error as e: + log = logging.getLogger("cdist") + log.error(e) + exit_code = 1 + + sys.exit(exit_code) diff --git a/bin/build-helper b/bin/cdist-build-helper similarity index 92% rename from bin/build-helper rename to bin/cdist-build-helper index ed41e438..6f514ef5 100755 --- a/bin/build-helper +++ b/bin/cdist-build-helper @@ -1,6 +1,6 @@ #!/bin/sh # -# 2011-2013 Nico Schottelius (nico-cdist at schottelius.org) +# 2011-2022 Nico Schottelius (nico-cdist at schottelius.org) # 2016-2019 Darko Poljak (darko.poljak at gmail.com) # # This file is part of cdist. @@ -45,7 +45,7 @@ usage() { shellcheck-manifests shellcheck-local-gencodes shellcheck-remote-gencodes - shellcheck-scripts + shellcheck-bin shellcheck-gencodes shellcheck-types shellcheck @@ -100,7 +100,7 @@ case "$option" in if (\$0 ~ /^$end/) { exit } else { - print \$0 + print \$0 } } }" "$basedir/docs/changelog" @@ -135,7 +135,7 @@ case "$option" in version=$1; shift - ( + ( cat << eof Subject: cdist $version has been released @@ -336,7 +336,7 @@ eof make docs-clean make docs - ############################################################# + ############################################################# # Everything green, let's do the release # Tag the current commit @@ -371,7 +371,6 @@ eof Manual steps post release: - cdist-web - send generated mailinglist.tmp mail - - twitter eof ;; @@ -406,7 +405,7 @@ eof ;; pycodestyle|pep8) - pycodestyle "${basedir}" "${basedir}/scripts/cdist" + pycodestyle "${basedir}" "${basedir}/bin/cdist" ;; check-pycodestyle) @@ -461,27 +460,34 @@ eof test ! -s "${SHELLCHECKTMP}" || { cat "${SHELLCHECKTMP}"; exit 1; } ;; - shellcheck-scripts) + # NOTE: shellcheck-scripts is kept for compatibility + shellcheck-bin|shellcheck-scripts) # shellcheck disable=SC2086 - ${SHELLCHECKCMD} scripts/cdist-dump scripts/cdist-new-type > "${SHELLCHECKTMP}" + ${SHELLCHECKCMD} bin/cdist-dump bin/cdist-new-type > "${SHELLCHECKTMP}" test ! -s "${SHELLCHECKTMP}" || { cat "${SHELLCHECKTMP}"; exit 1; } ;; shellcheck-gencodes) - "$0" shellcheck-local-gencodes || exit 1 - "$0" shellcheck-remote-gencodes || exit 1 + errors=false + "$0" shellcheck-local-gencodes || errors=true + "$0" shellcheck-remote-gencodes || errors=true + ! $errors || exit 1 ;; shellcheck-types) - "$0" shellcheck-type-explorers || exit 1 - "$0" shellcheck-manifests || exit 1 - "$0" shellcheck-gencodes || exit 1 + errors=false + "$0" shellcheck-type-explorers || errors=true + "$0" shellcheck-manifests || errors=true + "$0" shellcheck-gencodes || errors=true + ! $errors || exit 1 ;; shellcheck) - "$0" shellcheck-global-explorers || exit 1 - "$0" shellcheck-types || exit 1 - "$0" shellcheck-scripts || exit 1 + errors=false + "$0" shellcheck-global-explorers || errors=true + "$0" shellcheck-types || errors=true + "$0" shellcheck-bin || errors=true + ! $errors || exit 1 ;; shellcheck-type-files) @@ -491,12 +497,14 @@ eof ;; shellcheck-with-files) - "$0" shellcheck || exit 1 - "$0" shellcheck-type-files || exit 1 + errors=false + "$0" shellcheck || errors=true + "$0" shellcheck-type-files || errors=true + ! $errors || exit 1 ;; shellcheck-build-helper) - ${SHELLCHECKCMD} ./bin/build-helper + ${SHELLCHECKCMD} ./bin/cdist-build-helper ;; check-shellcheck) @@ -526,7 +534,8 @@ eof ;; version) - printf "VERSION = \"%s\"\n" "$(git describe)" > cdist/version.py + target_version="$(git describe | sed 's/-/.dev/; s/-/+/g')" + printf "VERSION = \"%s\"\n" "${target_version}" > cdist/version.py ;; target-version) diff --git a/scripts/cdist-dump b/bin/cdist-dump similarity index 100% rename from scripts/cdist-dump rename to bin/cdist-dump diff --git a/scripts/cdist-new-type b/bin/cdist-new-type similarity index 100% rename from scripts/cdist-new-type rename to bin/cdist-new-type diff --git a/cdist/__init__.py b/cdist/__init__.py index be573170..31d49889 100644 --- a/cdist/__init__.py +++ b/cdist/__init__.py @@ -22,12 +22,27 @@ import os import hashlib +import subprocess import cdist.log -import cdist.version -VERSION = cdist.version.VERSION +VERSION = 'unknown version' + +try: + import cdist.version + VERSION = cdist.version.VERSION +except ModuleNotFoundError: + cdist_dir = os.path.abspath( + os.path.join(os.path.dirname(__file__), os.pardir)) + if os.path.isdir(os.path.join(cdist_dir, '.git')): + try: + VERSION = subprocess.check_output( + ['git', 'describe', '--always'], + cwd=cdist_dir, + universal_newlines=True) + except Exception: + pass BANNER = """ .. . .x+=:. s @@ -49,7 +64,7 @@ REMOTE_EXEC = "ssh -o User=root" REMOTE_CMDS_CLEANUP_PATTERN = "ssh -o User=root -O exit -S {}" -MIN_SUPPORTED_PYTHON_VERSION = '3.5' +MIN_SUPPORTED_PYTHON_VERSION = (3, 5) class Error(Exception): diff --git a/cdist/argparse.py b/cdist/argparse.py index 77303591..8f7bbb85 100644 --- a/cdist/argparse.py +++ b/cdist/argparse.py @@ -8,10 +8,11 @@ import cdist.configuration import cdist.log import cdist.preos import cdist.info +import cdist.scan.commandline # set of beta sub-commands -BETA_COMMANDS = set(('install', 'inventory', )) +BETA_COMMANDS = set(('install', 'inventory', 'scan', )) # set of beta arguments for sub-commands BETA_ARGS = { 'config': set(('tag', 'all_tagged_hosts', 'use_archiving', )), @@ -273,8 +274,7 @@ def get_parsers(): '-f', '--file', help=('Read specified file for a list of additional hosts to ' 'operate on or if \'-\' is given, read stdin (one host per ' - 'line). If no host or host file is specified then, by ' - 'default, read hosts from stdin.'), + 'line).'), dest='hostfile', required=False) parser['config_args'].add_argument( '-p', '--parallel', nargs='?', metavar='HOST_MAX', @@ -326,9 +326,7 @@ def get_parsers(): parser['add-host'].add_argument( '-f', '--file', help=('Read additional hosts to add from specified file ' - 'or from stdin if \'-\' (each host on separate line). ' - 'If no host or host file is specified then, by default, ' - 'read from stdin.'), + 'or from stdin if \'-\' (each host on separate line). '), dest='hostfile', required=False) parser['add-tag'] = parser['invsub'].add_parser( @@ -342,20 +340,12 @@ def get_parsers(): parser['add-tag'].add_argument( '-f', '--file', help=('Read additional hosts to add tags from specified file ' - '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.'), + 'or from stdin if \'-\' (each host on separate line). '), dest='hostfile', required=False) parser['add-tag'].add_argument( '-T', '--tag-file', help=('Read additional tags to add from specified file ' - '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.'), + 'or from stdin if \'-\' (each tag on separate line). '), dest='tagfile', required=False) parser['add-tag'].add_argument( '-t', '--taglist', @@ -376,9 +366,7 @@ def get_parsers(): parser['del-host'].add_argument( '-f', '--file', help=('Read additional hosts to delete from specified file ' - 'or from stdin if \'-\' (each host on separate line). ' - 'If no host or host file is specified then, by default, ' - 'read from stdin.'), + 'or from stdin if \'-\' (each host on separate line). '), dest='hostfile', required=False) parser['del-tag'] = parser['invsub'].add_parser( @@ -396,20 +384,13 @@ def get_parsers(): parser['del-tag'].add_argument( '-f', '--file', help=('Read additional hosts to delete tags for from specified ' - 'file 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' - ' deleted from all hosts.'), + 'file or from stdin if \'-\' (each host on separate ' + 'line). '), dest='hostfile', required=False) parser['del-tag'].add_argument( '-T', '--tag-file', help=('Read additional tags from specified file ' - '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.'), + 'or from stdin if \'-\' (each tag on separate line). '), dest='tagfile', required=False) parser['del-tag'].add_argument( '-t', '--taglist', @@ -490,6 +471,44 @@ def get_parsers(): 'pattern', nargs='?', help='Glob pattern.') parser['info'].set_defaults(func=cdist.info.Info.commandline) + # Scan = config + further + 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', 'config']) + parser['scan'].add_argument( + '--list', + action='store_true', + help='List the known hosts and exit') + parser['scan'].add_argument( + '--config', + action='store_true', + help='Try to configure detected hosts') + parser['scan'].add_argument( + '-I', '--interface', + action='append', default=[], required=True, + help='On which interfaces to scan/trigger') + parser['scan'].add_argument( + '--name-mapper', + action='store', default=None, + help='Map addresses to names, required for config mode') + parser['scan'].add_argument( + '-d', '--config-delay', + action='store', default=3600, type=int, + help='How long (seconds) to wait before reconfiguring after last try') + parser['scan'].add_argument( + '-t', '--trigger-delay', + action='store', default=5, type=int, + help='How long (seconds) to wait between ICMPv6 echo requests') + parser['scan'].set_defaults(func=cdist.scan.commandline.commandline) + for p in parser: parser[p].epilog = EPILOG @@ -523,10 +542,10 @@ def parse_and_configure(argv, singleton=True): log = logging.getLogger("cdist") - log.verbose("version %s" % cdist.VERSION) - log.trace('command line args: {}'.format(cfg.command_line_args)) - log.trace('configuration: {}'.format(cfg.get_config())) - log.trace('configured args: {}'.format(args)) + log.verbose("version %s", cdist.VERSION) + log.trace('command line args: %s', cfg.command_line_args) + log.trace('configuration: %s', cfg.get_config()) + log.trace('configured args: %s', args) check_beta(vars(args)) diff --git a/cdist/conf/explorer/cpu_cores b/cdist/conf/explorer/cpu_cores index c6744142..81e5294e 100755 --- a/cdist/conf/explorer/cpu_cores +++ b/cdist/conf/explorer/cpu_cores @@ -33,6 +33,7 @@ case "$os" in ;; "freebsd"|"netbsd") + PATH=$(getconf PATH) sysctl -n hw.ncpu ;; diff --git a/cdist/conf/explorer/disks b/cdist/conf/explorer/disks index 24540601..56d62d10 100755 --- a/cdist/conf/explorer/disks +++ b/cdist/conf/explorer/disks @@ -30,9 +30,8 @@ case $uname_s in sysctl -n hw.disknames | grep -Eo '[lsw]d[0-9]+' ;; NetBSD) - PATH="${PATH}:/usr/local/sbin:/usr/sbin:/sbin" - sysctl -n hw.disknames \ - | awk 'BEGIN { RS = " " } /^[lsw]d[0-9]+/' + PATH=$(getconf PATH) + sysctl -n hw.disknames | awk -v RS=' ' '/^[lsw]d[0-9]+/' ;; Linux) # list of major device numbers toexclude: diff --git a/cdist/conf/explorer/lsb_codename b/cdist/conf/explorer/lsb_codename index 26bb8e3d..c9fb5cdf 100755 --- a/cdist/conf/explorer/lsb_codename +++ b/cdist/conf/explorer/lsb_codename @@ -21,6 +21,9 @@ set +e case "$("$__explorer/os")" in + checkpoint) + awk '{printf("%s\n", $(NF-1))}' /etc/cp-release + ;; openwrt) # shellcheck disable=SC1091 (. /etc/openwrt_release && echo "$DISTRIB_CODENAME") diff --git a/cdist/conf/explorer/lsb_description b/cdist/conf/explorer/lsb_description index b1009627..7279a9c2 100755 --- a/cdist/conf/explorer/lsb_description +++ b/cdist/conf/explorer/lsb_description @@ -21,6 +21,9 @@ set +e case "$("$__explorer/os")" in + checkpoint) + cat /etc/cp-release + ;; openwrt) # shellcheck disable=SC1091 (. /etc/openwrt_release && echo "$DISTRIB_DESCRIPTION") diff --git a/cdist/conf/explorer/lsb_id b/cdist/conf/explorer/lsb_id index 82ff9977..1f91cc40 100755 --- a/cdist/conf/explorer/lsb_id +++ b/cdist/conf/explorer/lsb_id @@ -21,6 +21,9 @@ set +e case "$("$__explorer/os")" in + checkpoint) + echo "CheckPoint" + ;; openwrt) # shellcheck disable=SC1091 (. /etc/openwrt_release && echo "$DISTRIB_ID") diff --git a/cdist/conf/explorer/lsb_release b/cdist/conf/explorer/lsb_release index 5ebfff1a..0bb9f7fe 100755 --- a/cdist/conf/explorer/lsb_release +++ b/cdist/conf/explorer/lsb_release @@ -21,6 +21,9 @@ set +e case "$("$__explorer/os")" in + checkpoint) + sed /etc/cp-release -e 's/.* R\([1-9][0-9]*\)\.[0-9]*$/\1/' + ;; openwrt) # shellcheck disable=SC1091 (. /etc/openwrt_release && echo "$DISTRIB_RELEASE") diff --git a/cdist/conf/explorer/machine_type b/cdist/conf/explorer/machine_type index 1c84f4d7..c31f5ca6 100755 --- a/cdist/conf/explorer/machine_type +++ b/cdist/conf/explorer/machine_type @@ -1,8 +1,6 @@ -#!/bin/sh +#!/bin/sh -e # -# 2014 Daniel Heule (hda at sfs.biz) -# 2014 Thomas Oettli (otho at sfs.biz) -# 2020 Evilham (contact at evilham.com) +# 2021 Dennis Camera (cdist at dtnr.ch) # # This file is part of cdist. # @@ -19,91 +17,1019 @@ # You should have received a copy of the GNU General Public License # along with cdist. If not, see . # +# This explorer tries to determine what type of machine the target to be +# configured is (container, virtual machine, bare-metal). +# +# It will print one line for each layer it can detect. +# The format of all lines is: TYPE[ VERB VENDOR] +# +# VERB does not have a special meaning, it is just for better readability. +# +# e.g. +# container +# container on lxc +# virtual by kvm-spapr +# +# The third word of each line (except the first) can be composed of different +# parts concatenated with a `-' (minus) character, with each component being +# a specification of the previous, e.g.: +# - lxc-libvirt (LXC container, managed by libvirt) +# - lpar-s390 / lpar-power (LPAR running on IBM S/390 or POWER, respectively) +# - xen-hvm / xen-pv (Xen HVM vs para-virtualization) +# +# If this explorer cannot collect enough information about virtualization it +# will fall back to 'physical'. +# -os=$("$__explorer/os") +# Add /sbin and /usr/sbin to the path so we can find system +# binaries like dmidecode. +PATH=$(getconf PATH 2>/dev/null) || PATH='/usr/bin:/bin' +PATH="/sbin:/usr/sbin:${PATH}" +export PATH -vendor_string_to_machine_type() { - for vendor in vmware bochs kvm qemu virtualbox bhyve; do - if echo "${1}" | grep -q -i "${vendor}"; then - if [ "${vendor}" = "bochs" ] || [ "${vendor}" = "qemu" ]; then - vendor="kvm" - fi - echo "virtual_by_${vendor}" - exit - fi - done +arch=$(uname -m | sed -e 's/i.86/i386/' -e 's/arm.*/arm/') +uname_s=$(uname -s) + + +is_command() { command -v "$1" >/dev/null 2>&1; } + +files_same() { + # shellcheck disable=SC2012 + LC_ALL=C df -P "$1" "$2" 2>/dev/null | { + read -r _ # skip header line + read -r fs1 _ _ _ _ mp1 + read -r fs2 _ _ _ _ mp2 + test "${fs1}" = "${fs2}" -a "${mp1}" = "${mp2}" || return 1 + } && + ls -1Ldi "$1" "$2" 2>/dev/null | { + read -r ino1 _ + read -r ino2 _ + test "${ino1}" = "${ino2}" || return 1 + } } -case "$os" in - "freebsd") - # FreeBSD does not have /proc/cpuinfo even when procfs is used. - # Instead there is a sysctl kern.vm_guest. - # Which is 'none' if physical, else the virtualisation. - vm_guest="$(sysctl -n kern.vm_guest 2>/dev/null || true)" - if [ -n "${vm_guest}" ]; then - if [ "${vm_guest}" = "none" ]; then - echo "physical" - exit - fi - echo "virtual_by_${vm_guest}" - exit - fi - ;; +is_oneof() ( + x=$1; shift + for y + do + test "${x}" = "${y}" || continue + return 0 + done + return 1 +) - "openbsd") - # OpenBSD can also use the sysctl's: hw.vendor or hw.product. - # Note we can be reasonably sure about a machine being virtualised - # as long as we can identify the virtualisation technology. - # But not so much about it being physical... - # Patches are welcome / reach out if you have better ideas. - for sysctl in hw.vendor hw.product; do - # This exits if we can make a reasonable judgement - vendor_string_to_machine_type "$(sysctl -n "${sysctl}")" - done - ;; +tolower() { LC_ALL=C tr '[:upper:]' '[:lower:]'; } - *) - # Defaulting to linux for compatibility with previous cdist behaviour +# shellcheck disable=SC2086 +glob_exists() { set -- $1; test -e "$1"; } - if [ -d "/proc/vz" ] && [ ! -d "/proc/bc" ]; then - echo openvz - exit - fi +get_dmi_field() { + if is_oneof "${uname_s}" NetBSD + then + case $1 + in + (system-manufacturer) _mib=machdep.dmi.system-vendor ;; + (system-product-name) _mib=machdep.dmi.system-product ;; + (system-version|system-uuid) _mib=machdep.dmi.$1 ;; + (bios-vendor|bios-version) _mib=machdep.dmi.$1 ;; + (biod-release-date) _mib=machdep.dmi.bios-date ;; + (*) _mib= ;; + esac - if [ -e "/proc/1/environ" ] && - tr '\000' '\n' < "/proc/1/environ" | grep -Eiq '^container='; then - echo lxc - exit - fi + test -n "${_mib}" && get_sysctl "${_mib}" | grep -e . && return + fi - if [ -r /proc/cpuinfo ]; then - # this should only exist on virtual guest machines, - # tested on vmware, xen, kvm, bhyve - if grep -q "hypervisor" /proc/cpuinfo; then - # this file is aviable in xen guest systems - if [ -r /sys/hypervisor/type ]; then - if grep -q -i "xen" /sys/hypervisor/type; then - echo virtual_by_xen - exit - fi - else - for vendor_file in /sys/class/dmi/id/product_name \ - /sys/class/dmi/id/sys_vendor \ - /sys/class/dmi/id/chasis_vendor; do - if [ -r ${vendor_file} ]; then - # This exits if we can make a reasonable judgement - vendor_string_to_machine_type "$(cat "${vendor_file}")" - fi - done - fi - echo "virtual_by_unknown" - exit - else - echo "physical" - exit - fi - fi - ;; -esac + if is_command dmidecode + then + dmidecode -s "$1" + elif test -d "${dmi_sysfs-}" + then + case $1 + in + (system-manufacturer) _filename=sys_vendor ;; + (system-product-name) _filename=product_name ;; + (*) _filename=$(echo "$1" | tr - _) ;; + esac + if test -r "${dmi_sysfs-}/${_filename}" + then + cat "${dmi_sysfs}/${_filename}" + fi + unset _filename + elif test "${uname_s}" = OpenBSD + then + # NOTE: something similar to system-manufacutrer and system-product-name + # is available on OpenBSD in sysctl + case $1 + in + (system-manufacturer) _mib=hw.vendor ;; + (system-product-name) _mib=hw.product ;; + (*) _mib= ;; + esac -echo "unknown" + test -n "${_mib}" && get_sysctl "${_mib}" | grep -e . && return + fi + + return 1 +} + +has_cpuinfo() { test -e /proc/cpuinfo; } + +get_sysctl() { + is_command sysctl && sysctl -n "$1" 2>/dev/null +} + +detected_layer() { + test -n "${_toplayer:-}" || echo "${_toplayer:=${1:?}}" +} + + +# Check for chroot + +has_chroot_systemd() { + is_command systemd-detect-virt && systemd-detect-virt --help | grep -q -e '^ -r' +} + +check_chroot_systemd() { + systemd-detect-virt -r +} + +has_chroot_debian_ischroot() { + is_command ischroot +} + +check_chroot_debian_ischroot() { + ischroot --default-false +} + +has_chroot_procfs() { + test -d /proc/ +} + +check_chroot_procfs() ( + is_chroot=false # default + if test -e /proc/1/root && ! files_same /proc/1/root / + then + is_chroot=true + fi + if test -e /proc/1/mountinfo -a -e /proc/self/mountinfo + then + has_mountinfo=true + cmp -s /proc/1/mountinfo /proc/self/mountinfo || is_chroot=true + fi + + if ${is_chroot} + then + # try to determine where the chroot has been mounted + rootdev=$(LC_ALL=C df -P / | awk 'NR==2{print $1}') + + if test -e "${rootdev}" + then + # escape chroot to determine where the device containing the + # chroot's / is mounted + rootdevmnt=$(LC_ALL=C chroot /proc/1/root df -P "${rootdev}" | awk 'NR==2{print $6}') + + # shellcheck disable=SC2012 + root_ino=$(ls -1di / | awk '{print $1}') + + # escape chroot and find mount point by inode + chroot /proc/1/root find "${rootdevmnt}" -xdev -type d -inum "${root_ino}" + elif ${has_mountinfo} + then + while read -r mntid _ _ _ cmntpnt _ + do + read -r _ _ _ _ hmntpnt _ <<-EOF + $(grep -e "^$((mntid)) " /proc/1/mountinfo) + EOF + printf '%s\n' "${hmntpnt%${cmntpnt}}" + done /dev/null) && + case ${_ctengine} + in + (''|'none') + return 1 ;; + ('container-other') + return 0 ;; + ('systemd-nspawn') + echo systemd_nspawn ;; + (*) + echo "${_ctengine}" ;; + esac +) + +has_ct_pid_1() { + test -r /run/systemd/container -o -r /proc/1/environ +} + +translate_container_name() { + case $1 + in + ('lxc') + echo lxc ;; + ('lxc-libvirt') + echo lxc-libvirt ;; + ('podman') + echo podman ;; + ('systemd-nspawn') + echo systemd_nspawn ;; + (*) + return 1 ;; + esac + return 0 +} + +check_ct_pid_1() { + if test -r /run/systemd/container + then + translate_container_name "$(head -n1 /run/systemd/container)" \ + && return 0 + fi + + if test -r /proc/1/environ + then + translate_container_name "$( + LC_ALL=C tr '\000' '\n' /dev/null + then + # https://github.com/Microsoft/WSL/issues/423#issuecomment-221627364 + echo wsl + elif test -d /var/.cagefs + then + # https://docs.cloudlinux.com/cloudlinux_os_components/#cagefs + # CageFS is not "really" a container, but it isn't a chroot either. + echo cagefs + elif test -e /proc/self/status && grep -q -e '^VxID: [0-9]\{1,\}' /proc/self/status + then + # Linux-VServer + if grep -q -x -F 'VxID: 0' /proc/self/status + then + # host + return 1 + else + # guest + echo linux_vserver + fi + else + return 1 + fi +} + +check_ct_os_specific() ( + if jailed=$(get_sysctl security.jail.jailed) && test "${jailed}" = 1 + then + # FreeBSD jail + echo jail + return 0 + fi + + if is_command zonename && test "$(zonename)" != global + then + # Solaris zone + echo zone + return 0 + fi + + return 1 +) + + +# Check for hypervisor + +guess_hypervisor_from_cpu_model() { + case $1 + in + (*\ KVM\ *) + echo kvm ;; + (*\ QEMU\ *|QEMU\ *) + echo qemu ;; + (*) + return 1 ;; + esac +} + +has_vm_systemd() { + is_command systemd-detect-virt && systemd-detect-virt --help | grep -q -e '^ -v' +} + +check_vm_systemd() ( + _hypervisor=$(systemd-detect-virt -v 2>/dev/null) && + case ${_hypervisor} + in + (''|'none') + return 1 ;; + ('amazon') + echo aws ;; + ('bochs') + echo kvm ;; + ('microsoft') + # assumption + echo hyperv ;; + ('oracle') + echo virtualbox ;; + ('vm-other') + return 0 ;; + (*) + echo "${_hypervisor}" ;; + esac +) + +has_vm_cpuinfo() { has_cpuinfo; } + +check_vm_cpuinfo() { + if grep -q -F 'User Mode Linux' /proc/cpuinfo \ + || grep -q -F 'UML' /proc/cpuinfo + then + # User Mode Linux + echo uml + elif grep -q -e '^vendor_id.*: PowerVM Lx86' /proc/cpuinfo + then + # IBM PowerVM Lx86 (Linux/x86 emulator) + echo powervm_lx86 + elif grep -q -e '^vendor_id.*: IBM/S390' /proc/cpuinfo + then + # IBM SystemZ (S/390) + if test -f /proc/sysinfo + then + if grep -q -e '^VM[0-9]* Control Program: KVM/Linux' /proc/sysinfo + then + echo kvm-s390 + return 0 + elif grep -q -e '^VM[0-9]* Control Program: z/VM' /proc/sysinfo + then + echo zvm + return 0 + elif grep -q -e '^LPAR ' /proc/sysinfo + then + echo zvm-lpar + return 0 + fi + fi + return 1 + else + if grep -q -e '^model name.*:' /proc/cpuinfo + then + sed -n -e 's/^model name[^:]*: *//p' /proc/cpuinfo \ + | while read -r _cpu_model + do + guess_hypervisor_from_cpu_model "${_cpu_model}" + done \ + | sort \ + | uniq -c \ + | awk ' + { if ($1 > most_c) { most_c = $1; most_s = $2 } } + END { + if (most_s) print most_s + exit !most_s + }' \ + && return 0 + fi + return 1 + fi +} + +check_vm_arch_specific() { + case ${arch} + in + (ppc64|ppc64le) + # Check PPC64 LPAR, KVM + + # example /proc/cpuinfo line indicating 'not baremetal' + # platform : pSeries + # + # example /proc/ppc64/lparcfg systemtype line + # system_type=IBM pSeries (emulated by qemu) + + if has_cpuinfo && grep -q -e 'platform.**pSeries' /proc/cpuinfo + then + if test -e /proc/ppc64/lparcfg + then + # Assume LPAR, now detect shared or dedicated + if grep -q -x -F 'shared_processor_mode=1' /proc/ppc64/lparcfg + then + echo powervm-shared + return 0 + else + echo powervm-dedicated + return 0 + fi + fi + fi + ;; + (sparc*) + # Check for SPARC LDoms + + if test -e /dev/mdesc + then + if test -d /sys/class/vlds/ctrl -a -d /sys/class/vlds/sp + then + # control LDom + return 1 + else + # guest LDom + echo ldom-sparc + fi + + # MDPROP=/usr/lib/ldoms/mdprop.py + # if test -x "${MDPROP}" + # then + # if test -n "$("${MDPROP}" -v iodevice device-type=pciex)" + # then + # echo ldoms-root + # echo ldoms-io + # elif test -n "$("${MDPROP}" -v iov-device vf-id=0)" + # then + # echo ldoms-io + # fi + # fi + return 0 + fi + ;; + (i?86|x86*|amd64|i86pc) + # VMM CPUID flag denotes that this system is running under a VMM + if is_oneof "${uname_s}" Darwin + then + get_sysctl machdep.cpu.features | tr ' ' '\n' | grep -qixF VMM \ + && return 0 + fi + if has_cpuinfo \ + && grep -q -i -e '^flags.*:.*\(hypervisor\|vmm\)\( \|$\)' /proc/cpuinfo + then + return 0 + fi + ;; + (ia64) + if test -d /sys/bus/xen -a ! -d /sys/bus/xen-backend + then + # PV-on-HVM drivers installed in a Xen guest + echo xen-hvm + return 0 + fi + ;; + esac + return 1 +} + +has_vm_dmi() { + # Check for various products in SMBIOS/DMI. + # Note that DMI doesn't exist on all architectures (only x86 and some ARM). + # On other architectures the $dmi variable will be empty. + + if test -d /sys/class/dmi/id/ + then + dmi_sysfs=/sys/class/dmi/id + elif test -d /sys/devices/virtual/dmi/id/ + then + dmi_sysfs=/sys/devices/virtual/dmi/id + fi + + # shellcheck disable=SC2015 + { + is_command dmidecode \ + && ( + # dmidecode needs to exit 0 and not print the No SMBIOS/DMI line + dmi_out=$(dmidecode 2>&1) \ + && ! printf '%s\n' "${dmi_out}" \ + | grep -qF 'No SMBIOS nor DMI entry point found, sorry.' + ) \ + || test -d "${dmi_sysfs}" + } +} + +check_vm_dmi() { + case $(get_dmi_field system-product-name) + in + (*.metal) + if test "$(get_dmi_field system-manufacturer)" = 'Amazon EC2' + then + # AWS EC2 bare metal -> no virtualisation + return 1 + fi + ;; + ('BHYVE') + echo bhyve + return 0 + ;; + ('Google Compute Engine') + echo gce + return 0 + ;; + ('RHEV Hypervisor') + # Red Hat Enterprise Virtualization + echo rhev + return 0 + ;; + ('KVM'|'Bochs'|'KVM Virtual Machine') + echo kvm + return 0 + ;; + ('Parallels Virtual Platform') + echo parallels + return 0 + ;; + ('VirtualBox') + echo virtualbox + return 0 + ;; + ('VMware Virtual Platform') + echo vmware + return 0 + ;; + esac + + case $(get_dmi_field system-manufacturer) + in + ('Alibaba'*) + case $(get_dmi_field system-product-name) + in + ('Alibaba Cloud ECS') + echo alibaba-ecs + ;; + (*) + echo alibaba + ;; + esac + return 0 + ;; + ('Amazon EC2') + # AWS on bare-metal or KVM + echo aws-ec2 + return 0 + ;; + ('innotek GmbH'|'Oracle Corporation') + echo virtualbox + return 0 + ;; + ('Joyent') + if test "$(get_dmi_field system-product-name)" = 'SmartDC HVM' + then + # SmartOS KVM + echo kvm-smartdc_hvm + return 0 + fi + ;; + ('Microsoft Corporation'*) + if test "$(get_dmi_field system-product-name)" = 'Virtual Machine' + then + if test -e /proc/irq/7/hyperv \ + || expr "$(get_dmi_field bios-version)" : 'VRTUAL.*' >/dev/null + then + echo hyperv + return 0 + fi + + case $(get_dmi_field system-version) + in + (VPC[0-9]*|VS2005*|*[Vv]irtual*[Pp][Cc]*) + echo virtualpc + return 0 + ;; + (*) + echo hyperv + return 0 + ;; + esac + fi + ;; + ('Nutanix') + # Nutanix AHV. Similar to KVM. + if test "$(get_dmi_field system-product-name)" = 'AHV' + then + echo nutanix_ahv + return 0 + fi + ;; + ('oVirt') + echo ovirt + return 0 + ;; + ('Parallels Software International Inc.') + echo parallels + return 0 + ;; + ('QEMU') + echo qemu + return 0 + ;; + ('VMware, Inc.') + echo vmware + return 0 + ;; + esac + + case $(get_dmi_field bios-vendor) + in + ('Amazon EC2') + # AWS on bare-metal or KVM + echo aws-ec2 + return 0 + ;; + ('BHYVE') + echo bhyve + return 0 + ;; + ('innotek GmbH') + echo virtualbox + return 0 + ;; + ('Parallels Software International Inc.') + echo parallels + return 0 + ;; + ('Xen') + if get_dmi_field bios-version | grep -q -e '\([0-9]\{1,\}\.\)\{2\}amazon' + then + # AWS on Xen + echo aws-xen + return 0 + fi + ;; + esac + + return 1 +} + +check_vm_hyp_specific() { + if is_command vmware-checkvm && vmware-checkvm >/dev/null + then + # vmware-checkvm is provided by VMware's open-vm-tools + echo vmware + return 0 + elif test -d /proc/xen + then + test -r /proc/xen/capabilities && + if grep -q -F 'control_d' /proc/xen/capabilities 2>/dev/null + then + # Xen dom0 + return 1 + else + # Xen domU + echo xen + return 0 + fi + fi + return 1 +} + +has_vm_dt() { + # OpenFirmware/Das U-Boot device-tree + test -d /proc/device-tree +} + +check_vm_dt() { + case ${arch} + in + (arm|aarch64) + if test -r /proc/device-tree/hypervisor/compatible + then + if grep -q -F 'xen' /proc/device-tree/hypervisor/compatible + then + echo xen + return 0 + elif grep -q -F 'vmware' /proc/device-tree/hypervisor/compatible + then + # e.g. VMware ESXi on ARM + echo vmware + return 0 + fi + fi + if glob_exists /proc/device-tree/fw-cfg@*/compatible + then + # qemu,fw-cfg-mmio + sed -e 's/,.*$//' /proc/device-tree/fw-cfg@*/compatible | head -n1 + return 0 + fi + if grep -q -F 'dummy-virt' /proc/device-tree/compatible + then + echo lkvm + return 0 + fi + ;; + (ppc64*) + if test -d /proc/device-tree/hypervisor \ + && grep -qF 'linux,kvm' /proc/device-tree/hypervisor/compatible + then + # We are running as a spapr KVM guest on ppc64 + echo kvm-spapr + return 0 + fi + if test -r /proc/device-tree/ibm,partition-name \ + && test -r /proc/device-tree/hmc-managed\? \ + && test -r /proc/device-tree/chosen/qemu,graphic-width + then + echo powervm + fi + ;; + esac + return 1 +} + +has_vm_sys_hypervisor() { + test -d /sys/hypervisor/ +} + +check_vm_sys_hypervisor() { + test -r /sys/hypervisor/type && + case $(head -n1 /sys/hypervisor/type) + in + (xen) + # Ordinary kernel with pv_ops. There does not seem to be + # enough information at present to tell whether this is dom0 + # or domU. + echo xen + return 0 + ;; + esac + return 1 +} + +check_vm_os_specific() { + _hyp_generic=false + + case ${uname_s} + in + (Darwin) + if hv_vmm_present=$(get_sysctl kern.hv_vmm_present) \ + && test "${hv_vmm_present}" -ne 0 + then + _hyp_generic=true + fi + ;; + (FreeBSD) + # FreeBSD does not have /proc/cpuinfo even when procfs is used. + # Instead there is a sysctl kern.vm_guest. + # Which is 'none' if physical, else the virtualisation. + vm_guest=$(get_sysctl kern.vm_guest | tolower) && + case ${vm_guest} + in + (none) ;; + (generic) _hyp_generic=true ;; + (*) + # kernel could detect hypervisor + case ${vm_guest} + in + (hv) echo hyperv ;; + (vbox) echo virtualbox ;; + (*) echo "${vm_guest}" ;; + esac + return 0 + ;; + esac + ;; + (NetBSD) + machdep_hv=$(get_sysctl machdep.hypervisor | tolower) && + case ${machdep_hv} + in + (none) ;; + (generic) _hyp_generic=true ;; + (*) + # kernel could detect hypervisor + case ${machdep_hv} + in + (hyper-v) echo hyperv ;; + (xenhvm*) echo xen-hvm ;; + (xenpv*) echo xen-pv ;; + (xen*) echo xen ;; + (*) echo "${machdep_hv}" ;; + esac + return 0 + ;; + esac + ;; + (OpenBSD) + if is_command hostctl && glob_exists /dev/pvbus[0-9]* + then + for _pvbus in /dev/pvbus[0-9]* + do + _h_out=$(hostctl -f "${_pvbus}" -t 2>/dev/null) || continue + case $(expr "${_h_out}" : '[^:]*: *\(.*\)$') + in + (KVM) echo kvm ;; + (Hyper-V) echo hyperv ;; + (VMware) echo vmware ;; + (Xen) echo xen ;; + (bhyve) echo bhyve ;; + (OpenBSD) echo openbsd_vmm ;; + esac + return 0 + done + fi + ;; + (SunOS) + diag_conf=$(prtdiag | sed -n -e 's/.*Configuration: *//p' -e '/^$/q') + # NOTE: Don't use -e or -F in Solaris grep + if printf '%s\n' "${diag_conf}" | grep -q -i QEMU + then + echo qemu + return 0 + elif printf '%s\n' "${diag_conf}" | grep -q -i VMware + then + echo vmware + return 0 + fi + ;; + (Linux) + if is_command dmesg + then + while read -r line + do + case ${line} + in + ('Booting paravirtualized kernel on ') + case $(expr "${line}" : '.* kernel on \(.*\)') + in + ('Xen') + echo xen-pv; return 0 ;; + ('bare hardware') + return 1 ;; + esac + ;; + ('Hypervisor detected') + case $(expr "${line}" : '.*: *\(.*\)') + in + ('ACRN') + echo acrn ;; + ('Jailhouse') + echo jailhouse ;; + ('KVM') + echo kvm ;; + ('Microsoft Hyper-V') + echo hyperv ;; + ('VMware') + echo vmware ;; + ('Xen HVM') + echo xen-hvm ;; + ('Xen PV') + echo xen-pv ;; + esac + return 0 + ;; + (lpar:*' under hypervisor') + return 0 ;; + esac + done <<-EOF + $(dmesg 2>/dev/null | awk ' + /Booting paravirtualized kernel on / + /Hypervisor detected: / + /lpar: .* under hypervisor/ + ') + EOF + fi + esac + + # Try to guess hypervisor based on CPU model (sysctl hw.model if available) + if cpu_model=$(get_sysctl hw.model) + then + guess_hypervisor_from_cpu_model "${cpu_model}" && return 0 + fi + + if ${_hyp_generic} + then + # cannot say which hypervisor, but one was detected + return 0 + else + return 1 + fi +} + +run_stage() { + if type "has_$1_$2" >/dev/null 2>&1 + then + "has_$1_$2" + else + true + fi \ + && "check_$1_$2" +} + + +# Execute chroot stages + +for stage in \ + procfs debian_ischroot systemd +do + chrootpnt=$(run_stage chroot ${stage}) || continue + is_chrooted=true + detected_layer 'chroot' + if test -n "${chrootpnt}" + then + echo chroot at "${chrootpnt}" + break + fi +done +if ${is_chrooted:-false} && test -z "${chrootpnt}" +then + # could determine chroot, but not its mount point + echo chroot +fi + + +# Execute container stages + +for stage in \ + systemd pid_1 cgroup files os_specific +do + ctengine=$(run_stage ct ${stage}) || continue + detected_layer 'container' + is_contained=true + if test -n "${ctengine}" + then + echo container on "${ctengine}" + break + fi +done +if ${is_contained:-false} && test -z "${ctengine}" +then + # none of the stages could determine the specific container engine, but + # we are running in some container. + echo container +fi + + +# Execute virtual machine / hypervisor stages + +for stage in \ + systemd os_specific hyp_specific sys_hypervisor dt dmi cpuinfo arch_specific +do + hypervisor=$(run_stage vm ${stage}) || continue + detected_layer 'virtual machine' + is_virtual=true + if test -n "${hypervisor}" + then + echo virtual by "${hypervisor}" + break + fi +done +if ${is_virtual:-false} && test -z "${hypervisor}" +then + # none of the stages could determine the specific hypervisor, but + # we are virtual. + echo virtual +fi + + +# Fallback + +detected_layer physical diff --git a/cdist/conf/explorer/memory b/cdist/conf/explorer/memory index 302b4cda..c6d113cf 100755 --- a/cdist/conf/explorer/memory +++ b/cdist/conf/explorer/memory @@ -1,8 +1,9 @@ -#!/bin/sh +#!/bin/sh -e # # 2014 Daniel Heule (hda at sfs.biz) # 2014 Thomas Oettli (otho at sfs.biz) # Copyright 2017, Philippe Gregoire +# 2020 Dennis Camera # # This file is part of cdist. # @@ -19,23 +20,73 @@ # You should have received a copy of the GNU General Public License # along with cdist. If not, see . # -# +# Returns the amount of memory physically installed in the system, or if that +# cannot be determined the amount available to the operating system kernel, +# in kibibytes (kiB). -# FIXME: other system types (not linux ...) +str2bytes() { + awk -F' ' ' + $2 == "B" || !$2 { print $1 } + $2 == "kB" { printf "%.f\n", ($1 * 1000) } + $2 == "MB" { printf "%.f\n", ($1 * 1000 * 1000) } + $2 == "GB" { printf "%.f\n", ($1 * 1000 * 1000 * 1000) } + $2 == "TB" { printf "%.f\n", ($1 * 1000 * 1000 * 1000 * 1000) } + $2 == "kiB" { printf "%.f\n", ($1 * 1024) } + $2 == "MiB" { printf "%.f\n", ($1 * 1024 * 1024) } + $2 == "GiB" { printf "%.f\n", ($1 * 1024 * 1024 * 1024) } + $2 == "TiB" { printf "%.f\n", ($1 * 1024 * 1024 * 1024 * 1024) }' +} -os=$("$__explorer/os") -case "$os" in - "macosx") - echo "$(sysctl -n hw.memsize)/1024" | bc - ;; +bytes2kib() { + awk '$0 > 0 { printf "%.f\n", ($0 / 1024) }' +} - *"bsd") - echo "$(sysctl -n hw.physmem) / 1048576" | bc - ;; - *) - if [ -r /proc/meminfo ]; then - grep "MemTotal:" /proc/meminfo | awk '{print $2}' - fi - ;; +case $(uname -s) +in + (Darwin) + sysctl -n hw.memsize | bytes2kib + ;; + (FreeBSD) + sysctl -n hw.realmem | bytes2kib + ;; + (NetBSD|OpenBSD) + # NOTE: This reports "usable" memory, not physically installed memory. + command -p sysctl -n hw.physmem | bytes2kib + ;; + (SunOS) + # Make sure that awk from xpg4 is used for the scripts to work + export PATH="/usr/xpg4/bin:${PATH}" + prtconf \ + | awk -F ': ' ' + $1 == "Memory size" { sub(/Megabytes/, "MiB", $2); print $2 } + /^$/ { exit }' \ + | str2bytes \ + | bytes2kib + ;; + (Linux) + if test -d /sys/devices/system/memory + then + # Use memory blocks if the architecture (e.g. x86, PPC64, s390) + # supports them (they denote physical memory) + num_mem_blocks=$(cat /sys/devices/system/memory/memory[0-9]*/state | grep -cxF online) + mem_block_size=$(cat /sys/devices/system/memory/block_size_bytes) + + echo $((num_mem_blocks * 0x$mem_block_size)) | bytes2kib && exit + fi + if test -r /proc/meminfo + then + # Fall back to meminfo file on other architectures (e.g. ARM, MIPS, + # PowerPC) + # NOTE: This is "usable" memory, not physically installed memory. + awk -F ': +' '$1 == "MemTotal" { sub(/B$/, "iB", $2); print $2 }' /proc/meminfo \ + | str2bytes \ + | bytes2kib + fi + ;; + (*) + printf "Your kernel (%s) is currently not supported by the memory explorer\n" "$(uname -s)" >&2 + printf "Please contribute an implementation for it if you can.\n" >&2 + exit 1 + ;; esac diff --git a/cdist/conf/explorer/os b/cdist/conf/explorer/os index 2d2aede6..b9232ee4 100755 --- a/cdist/conf/explorer/os +++ b/cdist/conf/explorer/os @@ -116,6 +116,13 @@ if [ -f /etc/slackware-version ]; then exit 0 fi +# Appliances + +if grep -q '^Check Point Gaia' /etc/cp-release 2>/dev/null; then + echo checkpoint + exit 0 +fi + uname_s="$(uname -s)" # Assume there is no tr on the client -> do lower case ourselves @@ -144,7 +151,9 @@ esac if [ -f /etc/os-release ]; then # after sles15, suse don't provide an /etc/SuSE-release anymore, but there is almost no difference between sles and opensuse leap, so call it suse - if grep -q ^ID_LIKE=\"suse\" /etc/os-release 2>/dev/null; then + # shellcheck disable=SC1091 + if (. /etc/os-release && echo "${ID_LIKE}" | grep -q '\(^\|\ \)suse\($\|\ \)') + then echo suse exit 0 fi diff --git a/cdist/conf/explorer/os_release b/cdist/conf/explorer/os_release index 6489446b..ec85046f 100644 --- a/cdist/conf/explorer/os_release +++ b/cdist/conf/explorer/os_release @@ -34,5 +34,9 @@ elif test -f /var/run/os-release then # FreeBSD (created by os-release service) cat /var/run/os-release +elif test -f /etc/cp-release +then + # Checkpoint firewall or management (actually linux based) + cat /etc/cp-release fi diff --git a/cdist/conf/explorer/os_version b/cdist/conf/explorer/os_version index 1d54ea60..fc59fd14 100755 --- a/cdist/conf/explorer/os_version +++ b/cdist/conf/explorer/os_version @@ -1,6 +1,7 @@ -#!/bin/sh +#!/bin/sh -e # # 2010-2011 Nico Schottelius (nico-cdist at schottelius.org) +# 2020-2021 Dennis Camera (dennis.camera at ssrq-sds-fds.ch) # # This file is part of cdist. # @@ -17,12 +18,22 @@ # You should have received a copy of the GNU General Public License # along with cdist. If not, see . # -# # All os variables are lower case # -# -case "$("$__explorer/os")" in +rc_getvar() { + awk -F= -v varname="$2" ' + function unquote(s) { + if (s ~ /^".*"$/ || s ~ /^'\''.*'\''$/) + return substr(s, 2, length(s) - 2) + else + return s + } + $1 == varname { print unquote(substr($0, index($0, "=") + 1)) }' "$1" +} + +case $("${__explorer:?}/os") +in amazon) cat /etc/system-release ;; @@ -30,11 +41,58 @@ case "$("$__explorer/os")" in # empty, but well... cat /etc/arch-release ;; + checkpoint) + awk '{version=$NF; printf("%s\n", substr(version, 2))}' /etc/cp-release + ;; debian) - cat /etc/debian_version + debian_version=$(cat /etc/debian_version) + case $debian_version + in + testing/unstable) + # previous to Debian 4.0 testing/unstable was used + # cf. https://metadata.ftp-master.debian.org/changelogs/main/b/base-files/base-files_11_changelog + echo 3.99 + ;; + */sid) + # sid versions don't have a number, so we decode by codename: + case $(expr "$debian_version" : '\([a-z]\{1,\}\)/') + in + trixie) echo 12.99 ;; + bookworm) echo 11.99 ;; + bullseye) echo 10.99 ;; + buster) echo 9.99 ;; + stretch) echo 8.99 ;; + jessie) echo 7.99 ;; + wheezy) echo 6.99 ;; + squeeze) echo 5.99 ;; + lenny) echo 4.99 ;; + *) echo 99.99 ;; + esac + ;; + *) + echo "$debian_version" + ;; + esac ;; devuan) - cat /etc/devuan_version + devuan_version=$(cat /etc/devuan_version) + case ${devuan_version} + in + (*/ceres) + # ceres versions don't have a number, so we decode by codename: + case ${devuan_version} + in + (daedalus/ceres) echo 4.99 ;; + (chimaera/ceres) echo 3.99 ;; + (beowulf/ceres) echo 2.99 ;; + (ascii/ceres) echo 1.99 ;; + (*) exit 1 + esac + ;; + (*) + echo "${devuan_version}" + ;; + esac ;; fedora) cat /etc/fedora-release @@ -43,7 +101,20 @@ case "$("$__explorer/os")" in cat /etc/gentoo-release ;; macosx) - sw_vers -productVersion + # NOTE: Legacy versions (< 10.3) do not support options + sw_vers | awk -F ':[ \t]+' '$1 == "ProductVersion" { print $2 }' + ;; + freebsd) + # Apparently uname -r is not a reliable way to get the patch level. + # See: https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=251743 + if command -v freebsd-version >/dev/null 2>&1 + then + # get userland version + freebsd-version -u + else + # fallback to kernel release for FreeBSD < 10.0 + uname -r + fi ;; *bsd|solaris) uname -r @@ -68,9 +139,22 @@ case "$("$__explorer/os")" in fi ;; ubuntu) - lsb_release -sr + if command -v lsb_release >/dev/null 2>&1 + then + lsb_release -sr + elif test -r /usr/lib/os-release + then + # fallback to /usr/lib/os-release if lsb_release is not present (like + # on minimized Ubuntu installations) + rc_getvar /usr/lib/os-release VERSION_ID + elif test -r /etc/lsb-release + then + # extract DISTRIB_RELEASE= variable from /etc/lsb-release on old + # versions without /usr/lib/os-release. + rc_getvar /etc/lsb-release DISTRIB_RELEASE + fi ;; alpine) cat /etc/alpine-release ;; -esac \ No newline at end of file +esac diff --git a/cdist/conf/type/__acl/explorer/getent b/cdist/conf/type/__acl/explorer/getent new file mode 100755 index 00000000..7e6c2c30 --- /dev/null +++ b/cdist/conf/type/__acl/explorer/getent @@ -0,0 +1,4 @@ +#!/bin/sh -e + +getent passwd | awk -F: '{print "user:"$1}' +getent group | awk -F: '{print "group:"$1}' diff --git a/cdist/conf/type/__acl/gencode-remote b/cdist/conf/type/__acl/gencode-remote index e5404a9d..32318e91 100755 --- a/cdist/conf/type/__acl/gencode-remote +++ b/cdist/conf/type/__acl/gencode-remote @@ -22,8 +22,8 @@ file_is="$( cat "$__object/explorer/file_is" )" if [ "$file_is" = 'missing' ] \ && [ -z "$__cdist_dry_run" ] \ - && \( [ ! -f "$__object/parameter/file" ] \ - || [ ! -f "$__object/parameter/directory" ] \) + && [ ! -f "$__object/parameter/file" ] \ + && [ ! -f "$__object/parameter/directory" ] then exit 0 fi @@ -47,28 +47,26 @@ then elif [ -f "$__object/parameter/entry" ] then 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 echo 'no parameters set' >&2 exit 1 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" ] then acl_should="$( echo "$acl_should" \ diff --git a/cdist/conf/type/__acl/man.rst b/cdist/conf/type/__acl/man.rst index 28412871..307be72b 100644 --- a/cdist/conf/type/__acl/man.rst +++ b/cdist/conf/type/__acl/man.rst @@ -12,11 +12,14 @@ Fully supported and tested on Linux (ext4 filesystem), partial support for FreeB See ``setfacl`` and ``acl`` manpages for more details. +One of ``--entry`` or ``--source`` must be used. -REQUIRED MULTIPLE PARAMETERS + +OPTIONAL MULTIPLE PARAMETERS ---------------------------- entry Set ACL entry following ``getfacl`` output syntax. + Must be used if ``--source`` is not used. OPTIONAL PARAMETERS @@ -25,6 +28,7 @@ source Read ACL entries from stdin or file. Ordering of entries is not important. When reading from file, comments and empty lines are ignored. + Must be used if ``--entry`` is not used. file 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. -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 -------- diff --git a/cdist/conf/type/__acl/parameter/deprecated/acl b/cdist/conf/type/__acl/parameter/deprecated/acl deleted file mode 100644 index 94e14159..00000000 --- a/cdist/conf/type/__acl/parameter/deprecated/acl +++ /dev/null @@ -1 +0,0 @@ -see manual for details diff --git a/cdist/conf/type/__acl/parameter/deprecated/group b/cdist/conf/type/__acl/parameter/deprecated/group deleted file mode 100644 index 94e14159..00000000 --- a/cdist/conf/type/__acl/parameter/deprecated/group +++ /dev/null @@ -1 +0,0 @@ -see manual for details diff --git a/cdist/conf/type/__acl/parameter/deprecated/mask b/cdist/conf/type/__acl/parameter/deprecated/mask deleted file mode 100644 index 94e14159..00000000 --- a/cdist/conf/type/__acl/parameter/deprecated/mask +++ /dev/null @@ -1 +0,0 @@ -see manual for details diff --git a/cdist/conf/type/__acl/parameter/deprecated/other b/cdist/conf/type/__acl/parameter/deprecated/other deleted file mode 100644 index 94e14159..00000000 --- a/cdist/conf/type/__acl/parameter/deprecated/other +++ /dev/null @@ -1 +0,0 @@ -see manual for details diff --git a/cdist/conf/type/__acl/parameter/deprecated/user b/cdist/conf/type/__acl/parameter/deprecated/user deleted file mode 100644 index 94e14159..00000000 --- a/cdist/conf/type/__acl/parameter/deprecated/user +++ /dev/null @@ -1 +0,0 @@ -see manual for details diff --git a/cdist/conf/type/__acl/parameter/optional b/cdist/conf/type/__acl/parameter/optional index cdcbc0b8..5a0c29a3 100644 --- a/cdist/conf/type/__acl/parameter/optional +++ b/cdist/conf/type/__acl/parameter/optional @@ -1,5 +1,3 @@ -mask -other source file directory diff --git a/cdist/conf/type/__acl/parameter/optional_multiple b/cdist/conf/type/__acl/parameter/optional_multiple index c615d507..4c884f03 100644 --- a/cdist/conf/type/__acl/parameter/optional_multiple +++ b/cdist/conf/type/__acl/parameter/optional_multiple @@ -1,4 +1 @@ entry -acl -user -group diff --git a/cdist/conf/type/__apt_backports/man.rst b/cdist/conf/type/__apt_backports/man.rst new file mode 100644 index 00000000..7036fb84 --- /dev/null +++ b/cdist/conf/type/__apt_backports/man.rst @@ -0,0 +1,104 @@ +cdist-type__debian_backports(7) +=============================== + +NAME +---- +cdist-type__apt_backports - Install backports + + +DESCRIPTION +----------- +This singleton type installs backports for the current OS release. +It aborts if backports are not supported for the specified OS or +no version codename could be fetched (like Debian unstable). + +The package index will be automatically updated if required. + +It supports backports from following OSes: + +- Debian +- Devuan +- Ubuntu + + +REQUIRED PARAMETERS +------------------- +None. + + +OPTIONAL PARAMETERS +------------------- +state + Represents the state of the backports repository. ``present`` or + ``absent``, defaults to ``present``. + + Will be directly passed to :strong:`cdist-type__apt_source`\ (7). + +mirror + The mirror to fetch the backports from. Will defaults to the generic + mirror of the current OS. + + Will be directly passed to :strong:`cdist-type__apt_source`\ (7). + + +BOOLEAN PARAMETERS +------------------ +None. + + +MESSAGES +-------- +None. + + +EXAMPLES +-------- + +.. code-block:: sh + + # setup the backports + __apt_backports + __apt_backports --state absent + __apt_backports --state present --mirror "http://ftp.de.debian.org/debian/" + + # install a backports package + # currently for the buster release backports + require="__apt_backports" __package_apt wireguard \ + --target-release buster-backports + + +ABORTS +------ +Aborts if the detected os is not Debian. + +Aborts if no distribuition codename could be detected. This is common for the +unstable distribution, but there is no backports repository for it already. + + +CAVEATS +------- +For Ubuntu, it setup all componenents for the backports repository: ``main``, +``restricted``, ``universe`` and ``multiverse``. The user may not want to +install proprietary packages, which will only be installed if the user +explicitly uses the backports target-release. The user may change this behavior +to install backports packages without the need of explicitly select it. + + +SEE ALSO +-------- +`Official Debian Backports site `_ + +:strong:`cdist-type__apt_source`\ (7) + + +AUTHORS +------- +Matthias Stecher + + +COPYING +------- +Copyright \(C) 2020 Matthias Stecher. 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. diff --git a/cdist/conf/type/__apt_backports/manifest b/cdist/conf/type/__apt_backports/manifest new file mode 100755 index 00000000..6fcd9212 --- /dev/null +++ b/cdist/conf/type/__apt_backports/manifest @@ -0,0 +1,82 @@ +#!/bin/sh -e +# __apt_backports/manifest +# +# 2020 Matthias Stecher (matthiasstecher at gmx.de) +# +# 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 . +# +# +# Enables/disables backports repository. Utilises __apt_source for it. +# + + +# Get the distribution codename by /etc/os-release. +# is already executed in a subshell by string substitution +# lsb_release may not be given in all installations +codename_os_release() { + # shellcheck disable=SC1090 + # shellcheck disable=SC1091 + . "$__global/explorer/os_release" + printf "%s" "$VERSION_CODENAME" +} + +# detect backport distribution +os="$(cat "$__global/explorer/os")" +case "$os" in + debian) + dist="$( codename_os_release )" + components="main" + mirror="http://deb.debian.org/debian/" + ;; + devuan) + dist="$( codename_os_release )" + components="main" + mirror="http://deb.devuan.org/merged" + ;; + ubuntu) + dist="$( codename_os_release )" + components="main restricted universe multiverse" + mirror="http://archive.ubuntu.com/ubuntu" + ;; + + *) + printf "Backports for %s are not supported!\n" "$os" >&2 + exit 1 + ;; +esac + +# error if no codename given (e.g. on Debian unstable) +if [ -z "$dist" ]; then + printf "No backports for unkown version of distribution %s!\n" "$os" >&2 + exit 1 +fi + + +# parameters +state="$(cat "$__object/parameter/state")" + +# mirror already set for the os, only override user-values +if [ -f "$__object/parameter/mirror" ]; then + mirror="$(cat "$__object/parameter/mirror")" +fi + + +# install the given backports repository +__apt_source "${dist}-backports" \ + --state "$state" \ + --distribution "${dist}-backports" \ + --component "$components" \ + --uri "$mirror" diff --git a/cdist/conf/type/__apt_backports/parameter/default/state b/cdist/conf/type/__apt_backports/parameter/default/state new file mode 100644 index 00000000..e7f6134f --- /dev/null +++ b/cdist/conf/type/__apt_backports/parameter/default/state @@ -0,0 +1 @@ +present diff --git a/cdist/conf/type/__apt_backports/parameter/optional b/cdist/conf/type/__apt_backports/parameter/optional new file mode 100644 index 00000000..4b05c235 --- /dev/null +++ b/cdist/conf/type/__apt_backports/parameter/optional @@ -0,0 +1,2 @@ +state +mirror diff --git a/cdist/conf/type/__pf_apply/singleton b/cdist/conf/type/__apt_backports/singleton similarity index 100% rename from cdist/conf/type/__pf_apply/singleton rename to cdist/conf/type/__apt_backports/singleton diff --git a/cdist/conf/type/__apt_key/explorer/state b/cdist/conf/type/__apt_key/explorer/state index 38f1bd3c..8ab268c1 100755 --- a/cdist/conf/type/__apt_key/explorer/state +++ b/cdist/conf/type/__apt_key/explorer/state @@ -27,18 +27,25 @@ else keyid="$__object_id" fi +# From apt-key(8): +# Use of apt-key is deprecated, except for the use of apt-key del in +# maintainer scripts to remove existing keys from the main keyring. +# If such usage of apt-key is desired the additional installation of +# the GNU Privacy Guard suite (packaged in gnupg) is required. +if [ -f "${__object}/parameter/use-deprecated-apt-key" ]; then + if apt-key export "$keyid" | head -n 1 | grep -Fqe "BEGIN PGP PUBLIC KEY BLOCK" + then echo present + else echo absent + fi + exit +fi + keydir="$(cat "$__object/parameter/keydir")" keyfile="$keydir/$__object_id.gpg" -if [ -d "$keydir" ] +if [ -f "$keyfile" ] then - if [ -f "$keyfile" ] - then echo present - else echo absent - fi -else - # fallback to deprecated apt-key - apt-key export "$keyid" | head -n 1 | grep -Fqe "BEGIN PGP PUBLIC KEY BLOCK" \ - && echo present \ - || echo absent + echo present + exit fi +echo absent diff --git a/cdist/conf/type/__apt_key/gencode-remote b/cdist/conf/type/__apt_key/gencode-remote index 0c96ff67..17dc9bfc 100755 --- a/cdist/conf/type/__apt_key/gencode-remote +++ b/cdist/conf/type/__apt_key/gencode-remote @@ -25,11 +25,7 @@ else fi state_should="$(cat "$__object/parameter/state")" state_is="$(cat "$__object/explorer/state")" - -if [ "$state_should" = "$state_is" ]; then - # nothing to do - exit 0 -fi +method="$(cat "$__object/key_method")" keydir="$(cat "$__object/parameter/keydir")" keyfile="$keydir/$__object_id.gpg" @@ -37,30 +33,18 @@ keyfile="$keydir/$__object_id.gpg" case "$state_should" in present) keyserver="$(cat "$__object/parameter/keyserver")" - - if [ -f "$__object/parameter/uri" ]; then - uri="$(cat "$__object/parameter/uri")" - - if [ -d "$keydir" ]; then - cat << EOF - -curl -s -L \\ - -o "$keyfile" \\ - "$uri" - -key="\$( cat "$keyfile" )" - -if echo "\$key" | grep -Fq 'BEGIN PGP PUBLIC KEY BLOCK' -then - echo "\$key" | gpg --dearmor > "$keyfile" -fi - -EOF - else - # fallback to deprecated apt-key - echo "curl -s -L '$uri' | apt-key add -" + # Using __download or __file as key source + # Propagate messages if needed + if [ "${method}" = "uri" ] || [ "${method}" = "source" ]; then + if grep -Eq "^__(file|download)$keyfile" "$__messages_in"; then + echo "added '$keyid'" >> "$__messages_out" fi - elif [ -d "$keydir" ]; then + exit 0 + elif [ "${state_is}" = "present" ]; then + exit 0 + fi + # Using key servers to fetch the key + if [ ! -f "$__object/parameter/use-deprecated-apt-key" ]; then # we need to kill gpg after 30 seconds, because gpg # can get stuck if keyserver is not responding. # exporting env var and not exit 1, @@ -100,13 +84,16 @@ EOF echo "added '$keyid'" >> "$__messages_out" ;; absent) - if [ -f "$keyfile" ]; then - echo "rm '$keyfile'" - else + # Removal for keys added from a keyserver without this flag + # is done in the manifest + if [ "$state_is" != "absent" ] && \ + [ -f "$__object/parameter/use-deprecated-apt-key" ]; then # fallback to deprecated apt-key echo "apt-key del \"$keyid\"" + echo "removed '$keyid'" >> "$__messages_out" + # Propagate messages if needed + elif grep -Eq "^__file$keyfile" "$__messages_in"; then + echo "removed '$keyid'" >> "$__messages_out" fi - - echo "removed '$keyid'" >> "$__messages_out" ;; esac diff --git a/cdist/conf/type/__apt_key/man.rst b/cdist/conf/type/__apt_key/man.rst index 234bc715..e35eaa0f 100644 --- a/cdist/conf/type/__apt_key/man.rst +++ b/cdist/conf/type/__apt_key/man.rst @@ -10,6 +10,14 @@ DESCRIPTION ----------- Manages the list of keys used by apt to authenticate packages. +This is done by placing the requested key in a file named +``$__object_id.gpg`` in the ``keydir`` directory. + +This is supported by modern releases of Debian-based distributions. + +In order of preference, exactly one of: ``source``, ``uri`` or ``keyid`` +must be specified. + REQUIRED PARAMETERS ------------------- @@ -18,21 +26,49 @@ None. OPTIONAL PARAMETERS ------------------- +keydir + keyring directory, defaults to ``/etc/apt/trusted.pgp.d``, which is + enabled system-wide by default. + +source + path to a file containing the GPG key of the repository. + Using this is recommended as it ensures that the manifest/type manintainer + has validated the key. + If ``-``, the GPG key is read from the type's stdin. + state 'present' or 'absent'. Defaults to 'present' +uri + the URI from which to download the key. + It is highly recommended that you only use protocols with TLS like HTTPS. + This uses ``__download`` but does not use checksums, if you want to ensure + that the key doesn't change, you are better off downloading it and using + ``--source``. + + +DEPRECATED OPTIONAL PARAMETERS +------------------------------ keyid - the id of the key to add. Defaults to __object_id + the id of the key to download from the ``keyserver``. + This is to be used in absence of ``--source`` and ``--uri`` or together + with ``--use-deprecated-apt-key`` for key removal. + Defaults to ``$__object_id``. keyserver - the keyserver from which to fetch the key. If omitted the default set - in ./parameter/default/keyserver is used. + the keyserver from which to fetch the key. + Defaults to ``pool.sks-keyservers.net``. -keydir - key save location, defaults to ``/etc/apt/trusted.pgp.d`` -uri - the URI from which to download the key +DEPRECATED BOOLEAN PARAMETERS +----------------------------- +use-deprecated-apt-key + ``apt-key(8)`` will last be available in Debian 11 and Ubuntu 22.04. + You can use this parameter to force usage of ``apt-key(8)``. + Please only use this parameter to *remove* keys from the keyring, + in order to prepare for removal of ``apt-key``. + Adding keys should be done without this parameter. + This parameter will be removed when Debian 11 stops being supported. EXAMPLES @@ -40,33 +76,39 @@ EXAMPLES .. code-block:: sh - # Add Ubuntu Archive Automatic Signing Key - __apt_key 437D05B5 - # Same thing - __apt_key 437D05B5 --state present - # Get rid of it - __apt_key 437D05B5 --state absent + # add a key that has been verified by a type maintainer + __apt_key jitsi_meet_2021 \ + --source cdist-contrib/type/__jitsi_meet/files/apt_2021.gpg - # same thing with human readable name and explicit keyid - __apt_key UbuntuArchiveKey --keyid 437D05B5 + # remove an old, deprecated or expired key + __apt_key jitsi_meet_2016 --state absent - # same thing with other keyserver - __apt_key UbuntuArchiveKey --keyid 437D05B5 --keyserver keyserver.ubuntu.com + # Get rid of a key that might have been added to + # /etc/apt/trusted.gpg with apt-key + __apt_key 0x40976EAF437D05B5 --use-deprecated-apt-key --state absent - # download key from the internet - __apt_key rabbitmq \ - --uri http://www.rabbitmq.com/rabbitmq-signing-key-public.asc + # add a key that we define in-line + __apt_key jitsi_meet_2021 --source '-' < Ander Punnar +Evilham COPYING ------- -Copyright \(C) 2011-2019 Steven Armstrong and Ander Punnar. You can +Copyright \(C) 2011-2021 Steven Armstrong, Ander Punnar and Evilham. 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. diff --git a/cdist/conf/type/__apt_key/manifest b/cdist/conf/type/__apt_key/manifest index 010357cd..889a764a 100755 --- a/cdist/conf/type/__apt_key/manifest +++ b/cdist/conf/type/__apt_key/manifest @@ -2,7 +2,105 @@ __package gnupg -if [ -f "$__object/parameter/uri" ] -then __package curl -else __package dirmngr +state_should="$(cat "${__object}/parameter/state")" + +incompatible_args() +{ + cat >> /dev/stderr <<-EOF + This type does not support --${1} and --${method} simultaneously. + EOF + exit 1 +} + +if [ -f "${__object}/parameter/source" ]; then + method="source" + src="$(cat "${__object}/parameter/source")" + if [ "${src}" = "-" ]; then + src="${__object}/stdin" + fi +fi +if [ -f "${__object}/parameter/uri" ]; then + if [ -n "${method}" ]; then + incompatible_args uri + fi + method="uri" + src="$(cat "${__object}/parameter/uri")" +fi +if [ -f "${__object}/parameter/keyid" ]; then + if [ -n "${method}" ]; then + incompatible_args keyid + fi + method="keyid" +fi +# Keep old default +if [ -z "${method}" ]; then + method="keyid" +fi +# Save this for later in gencode-remote +echo "${method}" > "${__object}/key_method" + +# Required remotely (most likely already installed) +__package dirmngr +# We need this in case a key has to be dearmor'd +__package gnupg +export require="__package/gnupg" + +if [ -f "${__object}/parameter/use-deprecated-apt-key" ]; then + # This is required if apt-key(8) is to be used + if [ "${method}" = "source" ] || [ "${method}" = "uri" ]; then + incompatible_args use-deprecated-apt-key + fi +else + if [ "${state_should}" = "absent" ] && \ + [ -f "${__object}/parameter/keyid" ]; then + cat >> /dev/stderr < +Dennis Camera COPYING ------- -Copyright \(C) 2014 Steven Armstrong. 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. +Copyright \(C) 2014 Steven Armstrong, 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. diff --git a/cdist/conf/type/__apt_norecommends/manifest b/cdist/conf/type/__apt_norecommends/manifest index e737df89..fc187784 100755 --- a/cdist/conf/type/__apt_norecommends/manifest +++ b/cdist/conf/type/__apt_norecommends/manifest @@ -1,6 +1,7 @@ #!/bin/sh -e # # 2014 Steven Armstrong (steven-cdist at armstrong.cc) +# 2020 Dennis Camera (dennis.camera at ssrq-sds-fds.ch) # # This file is part of cdist. # @@ -19,26 +20,28 @@ # -os=$(cat "$__global/explorer/os") +os=$(cat "${__global:?}/explorer/os") -case "$os" in - ubuntu|debian|devuan) - # No stinking recommends thank you very much. - # If I want something installed I will do so myself. - __file /etc/apt/apt.conf.d/99-no-recommends \ - --owner root --group root --mode 644 \ - --source - << DONE -APT::Install-Recommends "0"; -APT::Install-Suggests "0"; -APT::AutoRemove::RecommendsImportant "0"; -APT::AutoRemove::SuggestsImportant "0"; -DONE - ;; - *) - cat >&2 << DONE +case ${os} +in + (ubuntu|debian|devuan) + __file /etc/apt/apt.conf.d/00InstallRecommends --state present \ + --owner root --group root --mode 0644 --source - <<-'EOF' + APT::Install-Recommends "false"; + APT::Install-Suggests "false"; + APT::AutoRemove::RecommendsImportant "false"; + APT::AutoRemove::SuggestsImportant "false"; + EOF + + # 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 < + + +COPYING +------- +Copyright \(C) 2021 Daniel Fancsali. 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. diff --git a/cdist/conf/type/__apt_pin/manifest b/cdist/conf/type/__apt_pin/manifest new file mode 100755 index 00000000..983b2b42 --- /dev/null +++ b/cdist/conf/type/__apt_pin/manifest @@ -0,0 +1,68 @@ +#!/bin/sh -e +# +# 2021 Daniel Fancsali (fancsali@gmail.com) +# +# 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 . +# + + +name="$__object_id" + +os=$(cat "$__global/explorer/os") +state="$(cat "$__object/parameter/state")" + +if [ -f "$__object/parameter/package" ]; then + package="$(cat "$__object/parameter/package")" +else + package=$name +fi + +distribution="$(cat "$__object/parameter/distribution")" +priority="$(cat "$__object/parameter/priority")" + + +case "$os" in + debian|ubuntu|devuan) + ;; + *) + printf "This type is specific to Debian and it's derivatives" >&2 + exit 1 + ;; +esac + +case $distribution in + stable|testing|unstable|experimental) + pin="release a=$distribution" + ;; + *) + pin="release n=$distribution" + ;; +esac + + +__file "/etc/apt/preferences.d/$name" \ + --owner root --group root --mode 0644 \ + --state "$state" \ + --source - << EOF +# Created by cdist ${__type##*/} +# Do not change. Changes will be overwritten. +# + +# $name +Package: $package +Pin: $pin +Pin-Priority: $priority +EOF diff --git a/cdist/conf/type/__apt_pin/nonparallel b/cdist/conf/type/__apt_pin/nonparallel new file mode 100644 index 00000000..e69de29b diff --git a/cdist/conf/type/__apt_pin/parameter/default/priority b/cdist/conf/type/__apt_pin/parameter/default/priority new file mode 100644 index 00000000..1b79f38e --- /dev/null +++ b/cdist/conf/type/__apt_pin/parameter/default/priority @@ -0,0 +1 @@ +500 diff --git a/cdist/conf/type/__apt_pin/parameter/default/state b/cdist/conf/type/__apt_pin/parameter/default/state new file mode 100644 index 00000000..e7f6134f --- /dev/null +++ b/cdist/conf/type/__apt_pin/parameter/default/state @@ -0,0 +1 @@ +present diff --git a/cdist/conf/type/__apt_pin/parameter/optional b/cdist/conf/type/__apt_pin/parameter/optional new file mode 100644 index 00000000..847e703d --- /dev/null +++ b/cdist/conf/type/__apt_pin/parameter/optional @@ -0,0 +1,3 @@ +state +package +priority diff --git a/cdist/conf/type/__apt_pin/parameter/required b/cdist/conf/type/__apt_pin/parameter/required new file mode 100644 index 00000000..c8572d92 --- /dev/null +++ b/cdist/conf/type/__apt_pin/parameter/required @@ -0,0 +1 @@ +distribution diff --git a/cdist/conf/type/__apt_ppa/files/remove-apt-repository b/cdist/conf/type/__apt_ppa/files/remove-apt-repository deleted file mode 100755 index 3eb7d491..00000000 --- a/cdist/conf/type/__apt_ppa/files/remove-apt-repository +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python -# -# Remove the given apt repository. -# -# Exit with: -# 0: if it worked -# 1: if not -# 2: on other error - -import os -import sys -from aptsources import distro, sourceslist -from softwareproperties import ppa -from softwareproperties.SoftwareProperties import SoftwareProperties - - -def remove_if_empty(file_name): - with open(file_name, 'r') as f: - if f.read().strip(): - return - os.unlink(file_name) - -def remove_repository(repository): - #print 'repository:', repository - codename = distro.get_distro().codename - #print 'codename:', codename - (line, file) = ppa.expand_ppa_line(repository.strip(), codename) - #print 'line:', line - #print 'file:', file - deb_source_entry = sourceslist.SourceEntry(line, file) - src_source_entry = sourceslist.SourceEntry('deb-src{}'.format(line[3:]), file) - - try: - sp = SoftwareProperties() - sp.remove_source(deb_source_entry) - try: - # If there's a deb-src entry, remove that too - sp.remove_source(src_source_entry) - except: - pass - remove_if_empty(file) - return True - except ValueError: - print >> sys.stderr, "Error: '%s' doesn't exists in a sourcelist file" % line - return False - -if __name__ == '__main__': - if (len(sys.argv) != 2): - print >> sys.stderr, 'Error: need a repository as argument' - sys.exit(2) - repository = sys.argv[1] - if remove_repository(repository): - sys.exit(0) - else: - sys.exit(1) diff --git a/cdist/conf/type/__apt_ppa/gencode-remote b/cdist/conf/type/__apt_ppa/gencode-remote index 84ebebfe..e41341b8 100755 --- a/cdist/conf/type/__apt_ppa/gencode-remote +++ b/cdist/conf/type/__apt_ppa/gencode-remote @@ -29,9 +29,9 @@ fi case "$state_should" in present) - echo "add-apt-repository '$name'" + echo "add-apt-repository -y '$name'" ;; absent) - echo "remove-apt-repository '$name'" + echo "add-apt-repository -r -y '$name'" ;; esac diff --git a/cdist/conf/type/__apt_ppa/manifest b/cdist/conf/type/__apt_ppa/manifest index c6f4e876..57e85442 100755 --- a/cdist/conf/type/__apt_ppa/manifest +++ b/cdist/conf/type/__apt_ppa/manifest @@ -20,9 +20,4 @@ __package software-properties-common -require="__package/software-properties-common" \ - __file /usr/local/bin/remove-apt-repository \ - --source "$__type/files/remove-apt-repository" \ - --mode 0755 - require="$__object_name" __apt_update_index diff --git a/cdist/conf/type/__apt_source/files/source.list.template b/cdist/conf/type/__apt_source/files/source.list.template index d4420e96..a28bb45f 100755 --- a/cdist/conf/type/__apt_source/files/source.list.template +++ b/cdist/conf/type/__apt_source/files/source.list.template @@ -2,13 +2,14 @@ set -u entry="$uri $distribution $component" + cat << DONE # Created by cdist ${__type##*/} # Do not change. Changes will be overwritten. # # $name -deb ${forcedarch} $entry +deb ${options} $entry DONE if [ -f "$__object/parameter/include-src" ]; then echo "deb-src $entry" diff --git a/cdist/conf/type/__apt_source/gencode-remote b/cdist/conf/type/__apt_source/gencode-remote index 1e8592c6..973b0f6c 100755 --- a/cdist/conf/type/__apt_source/gencode-remote +++ b/cdist/conf/type/__apt_source/gencode-remote @@ -22,7 +22,21 @@ name="$__object_id" destination="/etc/apt/sources.list.d/${name}.list" +# There are special arguments to apt(8) to prevent aborts if apt woudn't been +# updated after the 19th April 2021 till the bullseye release. The additional +# arguments acknoledge the happend suite change (the apt(8) update does the +# same by itself). +# +# Using '-o $config' instead of the --allow-releaseinfo-change-* parameter +# allows backward compatablility to pre-buster Debian versions. +# +# See more: ticket #861 +# https://code.ungleich.ch/ungleich-public/cdist/-/issues/861 +apt_opts="-o Acquire::AllowReleaseInfoChange::Suite=true -o Acquire::AllowReleaseInfoChange::Version=true" + +# run 'apt-get update' only if something changed with our sources.list file +# it will be run a second time on error as a redundancy messure to success if grep -q "^__file${destination}" "$__messages_in"; then - printf 'apt-get update || apt-get update\n' + printf 'apt-get %s update || apt-get %s update\n' "$apt_opts" "$apt_opts" fi diff --git a/cdist/conf/type/__apt_source/man.rst b/cdist/conf/type/__apt_source/man.rst index d1acb388..d317a135 100644 --- a/cdist/conf/type/__apt_source/man.rst +++ b/cdist/conf/type/__apt_source/man.rst @@ -23,6 +23,9 @@ OPTIONAL PARAMETERS arch set this if you need to force and specific arch (ubuntu specific) +signed-by + provide a GPG key fingerprint or keyring path for signature checks + state 'present' or 'absent', defaults to 'present' @@ -56,6 +59,11 @@ EXAMPLES --uri http://archive.canonical.com/ \ --component partner --state present + __apt_source goaccess \ + --uri http://deb.goaccess.io/ \ + --component main \ + --signed-by C03B48887D5E56B046715D3297BD1A0133449C3D + AUTHORS ------- diff --git a/cdist/conf/type/__apt_source/manifest b/cdist/conf/type/__apt_source/manifest index 35f15909..fd1ec47f 100755 --- a/cdist/conf/type/__apt_source/manifest +++ b/cdist/conf/type/__apt_source/manifest @@ -21,6 +21,7 @@ name="$__object_id" state="$(cat "$__object/parameter/state")" uri="$(cat "$__object/parameter/uri")" +options="" if [ -f "$__object/parameter/distribution" ]; then distribution="$(cat "$__object/parameter/distribution")" @@ -31,9 +32,15 @@ fi component="$(cat "$__object/parameter/component")" if [ -f "$__object/parameter/arch" ]; then - forcedarch="[arch=$(cat "$__object/parameter/arch")]" -else - forcedarch="" + options="arch=$(cat "$__object/parameter/arch")" +fi + +if [ -f "$__object/parameter/signed-by" ]; then + options="$options signed-by=$(cat "$__object/parameter/signed-by")" +fi + +if [ "$options" ]; then + options="[$options]" fi # export variables for use in template @@ -41,7 +48,7 @@ export name export uri export distribution export component -export forcedarch +export options # generate file from template mkdir "$__object/files" diff --git a/cdist/conf/type/__apt_source/parameter/optional b/cdist/conf/type/__apt_source/parameter/optional index 87537335..0b5470a1 100644 --- a/cdist/conf/type/__apt_source/parameter/optional +++ b/cdist/conf/type/__apt_source/parameter/optional @@ -1,4 +1,5 @@ state distribution component -arch \ No newline at end of file +arch +signed-by diff --git a/cdist/conf/type/__apt_update_index/gencode-remote b/cdist/conf/type/__apt_update_index/gencode-remote index 70b59710..2d7f9030 100755 --- a/cdist/conf/type/__apt_update_index/gencode-remote +++ b/cdist/conf/type/__apt_update_index/gencode-remote @@ -18,9 +18,23 @@ # along with cdist. If not, see . # + +# There are special arguments to apt(8) to prevent aborts if apt woudn't been +# updated after the 19th April 2021 till the bullseye release. The additional +# arguments acknoledge the happend suite change (the apt(8) update does the +# same by itself). +# +# Using '-o $config' instead of the --allow-releaseinfo-change-* parameter +# allows backward compatablility to pre-buster Debian versions. +# +# See more: ticket #861 +# https://code.ungleich.ch/ungleich-public/cdist/-/issues/861 +apt_opts="-o Acquire::AllowReleaseInfoChange::Suite=true -o Acquire::AllowReleaseInfoChange::Version=true" + # run 'apt-get update' if anything in /etc/apt is newer then /var/lib/apt/lists +# it will be run a second time on error as a redundancy messure to success cat << DONE if find /etc/apt -mindepth 1 -cnewer /var/lib/apt/lists | grep . > /dev/null; then - apt-get update || apt-get update + apt-get $apt_opts update || apt-get $apt_opts update fi DONE diff --git a/cdist/conf/type/__block/gencode-remote b/cdist/conf/type/__block/gencode-remote index 1f5cc033..7a1f4064 100755 --- a/cdist/conf/type/__block/gencode-remote +++ b/cdist/conf/type/__block/gencode-remote @@ -46,28 +46,29 @@ fi remove_block() { cat << DONE -tmpfile=\$(mktemp ${file}.cdist.XXXXXXXXXX) +tmpfile=\$(mktemp ${quoted_file}.cdist.XXXXXXXXXX) # preserve ownership and permissions of existing file -if [ -f "$file" ]; then - cp -p "$file" "\$tmpfile" +if [ -f $quoted_file ]; then + cp -p $quoted_file "\$tmpfile" fi -awk -v prefix=^$(quote "$prefix")\$ -v suffix=^$(quote "$suffix")\$ ' +awk -v prefix=$(quote "$prefix") -v suffix=$(quote "$suffix") ' { - if (match(\$0,prefix)) { + if (\$0 == prefix) { triggered=1 } if (triggered) { - if (match(\$0,suffix)) { + if (\$0 == suffix) { triggered=0 } } else { print } -}' "$file" > "\$tmpfile" -mv -f "\$tmpfile" "$file" +}' $quoted_file > "\$tmpfile" +mv -f "\$tmpfile" $quoted_file DONE } +quoted_file="$(quote "$file")" case "$state_should" in present) if [ "$state_is" = "changed" ]; then @@ -77,7 +78,7 @@ case "$state_should" in echo add >> "$__messages_out" fi cat << DONE -cat >> "$file" << ${__type##*/}_DONE +cat >> $quoted_file << '${__type##*/}_DONE' $(cat "$block") ${__type##*/}_DONE DONE diff --git a/cdist/conf/type/__debconf_set_selections/explorer/state b/cdist/conf/type/__debconf_set_selections/explorer/state new file mode 100644 index 00000000..f8a3f6c8 --- /dev/null +++ b/cdist/conf/type/__debconf_set_selections/explorer/state @@ -0,0 +1,142 @@ +#!/bin/sh -e +# +# 2021 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 . +# +# Determine current debconf selections' state. +# Prints one of: +# present: all selections are already set as they should. +# different: one or more of the selections have a different value. +# absent: one or more of the selections are not (currently) defined. +# + +test -x /usr/bin/perl || { + # cannot find perl (no perl ~ no debconf) + echo 'absent' + exit 0 +} + +linesfile="${__object:?}/parameter/line" +test -s "${linesfile}" || { + if test -s "${__object:?}/parameter/file" + then + echo absent + else + echo present + fi + exit 0 +} + +# assert __type_explorer is set (because it is used by the Perl script) +: "${__type_explorer:?}" + +/usr/bin/perl -- - "${linesfile}" <<'EOF' +use strict; +use warnings "all"; + +use Fcntl qw(:DEFAULT :flock); + +use Debconf::Db; +use Debconf::Question; + +# Extract @known... arrays from debconf-set-selections +# These values are required to distinguish flags and values in the given lines. +# DC: I couldn't think of a more ugly solution to the problem… +my @knownflags; +my @knowntypes; +my $debconf_set_selections = '/usr/bin/debconf-set-selections'; +if (-e $debconf_set_selections) { + my $sed_known = 's/^my \(@known\(flags\|types\) = qw([a-z ]*);\).*$/\1/p'; + eval `sed -n '$sed_known' '$debconf_set_selections'`; +} + +sub mungeline ($) { + my $line = shift; + chomp $line; + $line =~ s/\r$//; + return $line; +} + +sub fatal { printf STDERR @_; exit 1; } + +my $state = 'present'; + +sub state { + my $new = shift; + if ($state eq 'present' + or ($state eq 'different' and $new eq 'absent')) { + $state = $new; + } +} + + +# Load Debconf DB but manually lock on the state explorer script, +# because Debconf aborts immediately if executed concurrently. +# This is not really an ideal solution because the Debconf DB could be locked by +# another process (e.g. apt-get), but no way to achieve this could be found. +# If you know how to, please provide a patch. +my $lockfile = "%ENV{'__type_explorer'}/state"; +if (open my $lock_fh, '+<', $lockfile) { + flock $lock_fh, LOCK_EX or die "Cannot lock $lockfile"; +} +{ + Debconf::Db->load(readonly => 'true'); +} + + +while (<>) { + # Read and process lines (taken from debconf-set-selections) + $_ = mungeline($_); + while (/\\$/ && ! eof) { + s/\\$//; + $_ .= mungeline(<>); + } + next if /^\s*$/ || /^\s*\#/; + + my ($owner, $label, $type, $content) = /^\s*(\S+)\s+(\S+)\s+(\S+)(?:\s(.*))?/ + or fatal "invalid line: %s\n", $_; + $content = '' unless defined $content; + + + # Compare is and should state + my $q = Debconf::Question->get($label); + + unless (defined $q) { + # probably a preseed + state 'absent'; + next; + } + + if (grep { $_ eq $q->type } @knownflags) { + # This line wants to set a flag, presumably. + if ($q->flag($q->type) ne $content) { + state 'different'; + } + } else { + # Otherwise, it's probably a value… + if ($q->value ne $content) { + state 'different'; + } + + unless (grep { $_ eq $owner } (split /, /, $q->owners)) { + state 'different'; + } + } +} + +printf "%s\n", $state; +EOF diff --git a/cdist/conf/type/__debconf_set_selections/gencode-remote b/cdist/conf/type/__debconf_set_selections/gencode-remote index e99aef40..9ba28f09 100755 --- a/cdist/conf/type/__debconf_set_selections/gencode-remote +++ b/cdist/conf/type/__debconf_set_selections/gencode-remote @@ -1,6 +1,7 @@ #!/bin/sh -e # # 2011-2014 Nico Schottelius (nico-cdist at schottelius.org) +# 2021 Dennis Camera (dennis.camera at ssrq-sds-fds.ch) # # This file is part of cdist. # @@ -17,16 +18,37 @@ # You should have received a copy of the GNU General Public License # along with cdist. If not, see . # -# -# Setup selections -# -filename="$(cat "$__object/parameter/file")" - -if [ "$filename" = "-" ]; then - filename="$__object/stdin" +if test -f "${__object:?}/parameter/line" +then + filename="${__object:?}/parameter/line" +elif test -s "${__object:?}/parameter/file" +then + filename=$(cat "${__object:?}/parameter/file") + if test "${filename}" = '-' + then + filename="${__object:?}/stdin" + fi +else + printf 'Neither --line nor --file set.\n' >&2 + exit 1 fi -echo "debconf-set-selections << __file-eof" -cat "$filename" -echo "__file-eof" +# setting no lines makes no sense +test -s "${filename}" || exit 0 + +state_is=$(cat "${__object:?}/explorer/state") + +if test "${state_is}" != 'present' +then + cat <<-CODE + debconf-set-selections <<'EOF' + $(cat "${filename}") + EOF + CODE + + awk ' + { + printf "set %s %s %s %s\n", $1, $2, $3, $4 + }' "${filename}" >>"${__messages_out:?}" +fi diff --git a/cdist/conf/type/__debconf_set_selections/man.rst b/cdist/conf/type/__debconf_set_selections/man.rst index 58c25b81..fd0040ae 100644 --- a/cdist/conf/type/__debconf_set_selections/man.rst +++ b/cdist/conf/type/__debconf_set_selections/man.rst @@ -8,15 +8,33 @@ cdist-type__debconf_set_selections - Setup debconf selections DESCRIPTION ----------- -On Debian and alike systems debconf-set-selections(1) can be used +On Debian and alike systems :strong:`debconf-set-selections`\ (1) can be used to setup configuration parameters. REQUIRED PARAMETERS ------------------- +cf. ``--line``. + + +OPTIONAL PARAMETERS +------------------- file - Use the given filename as input for debconf-set-selections(1) - If filename is "-", read from stdin. + Use the given filename as input for :strong:`debconf-set-selections`\ (1) + If filename is ``-``, read from stdin. + + **This parameter is deprecated, because it doesn't work with state detection.** +line + A line in :strong:`debconf-set-selections`\ (1) compatible format. + This parameter can be used multiple times to set multiple options. + + (This parameter is actually required, but marked optional because the + deprecated ``--file`` is still accepted.) + + +BOOLEAN PARAMETERS +------------------ +None. EXAMPLES @@ -24,30 +42,29 @@ EXAMPLES .. code-block:: sh - # Setup configuration for nslcd - __debconf_set_selections nslcd --file /path/to/file + # Setup gitolite's gituser + __debconf_set_selections nslcd --line 'gitolite gitolite/gituser string git' - # Setup configuration for nslcd from another type - __debconf_set_selections nslcd --file "$__type/files/preseed/nslcd" - - __debconf_set_selections nslcd --file - << eof - gitolite gitolite/gituser string git - eof + # Setup configuration for nslcd from a file. + # NB: Multiple lines can be passed to --line, although this can be considered a hack. + __debconf_set_selections nslcd --line "$(cat "${__files:?}/preseed/nslcd.debconf")" SEE ALSO -------- -:strong:`debconf-set-selections`\ (1), :strong:`cdist-type__update_alternatives`\ (7) +- :strong:`cdist-type__update_alternatives`\ (7) +- :strong:`debconf-set-selections`\ (1) AUTHORS ------- -Nico Schottelius +| Nico Schottelius +| Dennis Camera COPYING ------- -Copyright \(C) 2011-2014 Nico Schottelius. 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. +Copyright \(C) 2011-2014 Nico Schottelius, 2021 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. diff --git a/cdist/conf/type/__debconf_set_selections/manifest b/cdist/conf/type/__debconf_set_selections/manifest new file mode 100755 index 00000000..0f4fb2e2 --- /dev/null +++ b/cdist/conf/type/__debconf_set_selections/manifest @@ -0,0 +1,21 @@ +#!/bin/sh -e +# +# 2021 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 . +# + +__package_apt debconf diff --git a/cdist/conf/type/__debconf_set_selections/nonparallel b/cdist/conf/type/__debconf_set_selections/nonparallel new file mode 100644 index 00000000..e69de29b diff --git a/cdist/conf/type/__debconf_set_selections/parameter/deprecated/file b/cdist/conf/type/__debconf_set_selections/parameter/deprecated/file new file mode 100644 index 00000000..09db545a --- /dev/null +++ b/cdist/conf/type/__debconf_set_selections/parameter/deprecated/file @@ -0,0 +1 @@ +'file' has been deprecated in favour of 'line' in order to provide idempotency. diff --git a/cdist/conf/type/__debconf_set_selections/parameter/required b/cdist/conf/type/__debconf_set_selections/parameter/optional similarity index 100% rename from cdist/conf/type/__debconf_set_selections/parameter/required rename to cdist/conf/type/__debconf_set_selections/parameter/optional diff --git a/cdist/conf/type/__debconf_set_selections/parameter/optional_multiple b/cdist/conf/type/__debconf_set_selections/parameter/optional_multiple new file mode 100644 index 00000000..a999a0c2 --- /dev/null +++ b/cdist/conf/type/__debconf_set_selections/parameter/optional_multiple @@ -0,0 +1 @@ +line diff --git a/cdist/conf/type/__directory/explorer/stat b/cdist/conf/type/__directory/explorer/stat index 105d894f..f817cb02 100755 --- a/cdist/conf/type/__directory/explorer/stat +++ b/cdist/conf/type/__directory/explorer/stat @@ -30,10 +30,10 @@ fallback() { gid=$(echo "$ls_line" | awk '{ print $4 }') owner=$(awk -F: -v uid="$uid" '$3 == uid { print $1; f=1 } END { if (!f) print "UNKNOWN" }' /etc/passwd) - group=$(awk -F: -v uid="$uid" '$3 == uid { print $1; f=1 } END { if (!f) print "UNKNOWN" }' /etc/group) + group=$(awk -F: -v gid="$gid" '$3 == gid { print $1; f=1 } END { if (!f) print "UNKNOWN" }' /etc/group) mode_text=$(echo "$ls_line" | awk '{ print $1 }') - mode=$(echo "$mode_text" | awk '{ k=0; for (i=0; i<=8; i++) k += ((substr($1, i+2, 1) ~ /[rwx]/) * 2^(8-i)); printf("%0o", k) }') + mode=$(echo "$mode_text" | awk '{for(i=8;i>=0;--i){c=substr($1,10-i,1);k+=((c~/[rwxst]/)*2^i);if(!(i%3))k+=(tolower(c)~/[lst]/)*2^(9+i/3)}printf("%04o",k)}') printf 'type: %s\nowner: %d %s\ngroup: %d %s\nmode: %s %s\n' \ "$("$__type_explorer/type")" \ @@ -45,56 +45,27 @@ fallback() { # nothing to work with, nothing we could do [ -e "$destination" ] || exit 0 -if ! command -v stat >/dev/null -then +command -v stat >/dev/null 2>&1 || { fallback exit -fi +} -case $("$__explorer/os") in - "freebsd"|"netbsd"|"openbsd"|"macosx") - stat -f "type: %HT +case $("$__explorer/os") +in + freebsd|netbsd|openbsd|macosx) + stat -f 'type: %HT owner: %Du %Su group: %Dg %Sg -mode: %Lp %Sp -" "$destination" | awk '/^type/ { print tolower($0); next } { print }' +mode: %Mp%03Lp %Sp +' "$destination" | awk '/^type/ { print tolower($0); next } { print }' ;; - solaris) - ls1="$( ls -ld "$destination" )" - ls2="$( ls -ldn "$destination" )" - - if [ -f "$__object/parameter/mode" ] - then mode_should="$( cat "$__object/parameter/mode" )" - fi - - # yes, it is ugly hack, but if you know better way... - if [ -z "$( find "$destination" -perm "$mode_should" )" ] - then octets=888 - else octets="$( echo "$mode_should" | sed 's/^0//' )" - fi - - case "$( echo "$ls1" | cut -c1-1 )" in - -) echo 'type: regular file' ;; - d) echo 'type: directory' ;; - esac - - echo "owner: $( echo "$ls2" \ - | awk '{print $3}' ) $( echo "$ls1" \ - | awk '{print $3}' )" - - echo "group: $( echo "$ls2" \ - | awk '{print $4}' ) $( echo "$ls1" \ - | awk '{print $4}' )" - - echo "mode: $octets $( echo "$ls1" | awk '{print $1}' )" - ;; *) # NOTE: Do not use --printf here as it is not supported by BusyBox stat. # NOTE: BusyBox's stat might not support the "-c" option, in which case # we fall through to the shell fallback. - stat -c "type: %F + stat -c 'type: %F owner: %u %U group: %g %G -mode: %a %A" "$destination" 2>/dev/null || fallback - ;; +mode: %04a %A' "$destination" 2>/dev/null || fallback + ;; esac diff --git a/cdist/conf/type/__directory/gencode-remote b/cdist/conf/type/__directory/gencode-remote index a1a32ea2..d9c00b56 100755 --- a/cdist/conf/type/__directory/gencode-remote +++ b/cdist/conf/type/__directory/gencode-remote @@ -97,9 +97,11 @@ case "$state_should" in value_should="$(cat "$__object/parameter/$attribute")" value_is="$(get_current_value "$attribute" "$value_should")" - # change 0xxx format to xxx format => same as stat returns + # format mode in four digits => same as stat returns if [ "$attribute" = mode ]; then - value_should="$(echo "$value_should" | sed 's/^0\(...\)/\1/')" + # Convert to four-digit octal number (printf interprets + # strings with leading 0s as octal!) + value_should=$(printf '%04o' "0${value_should}") fi if [ "$set_attributes" = 1 ] || [ "$value_should" != "$value_is" ]; then diff --git a/cdist/conf/type/__dot_file/man.rst b/cdist/conf/type/__dot_file/man.rst index ae65eb95..c8f36712 100644 --- a/cdist/conf/type/__dot_file/man.rst +++ b/cdist/conf/type/__dot_file/man.rst @@ -25,6 +25,9 @@ user OPTIONAL PARAMETERS ------------------- +dirmode + forwarded to :strong:`__directory` type as mode + mode forwarded to :strong:`__file` type @@ -34,6 +37,12 @@ state source forwarded to :strong:`__file` type +file + forwarded to :strong:`__file` type + This can be used if multiple users need to have a dotfile updated, + which will result in duplicate object id errors. When using the + file parameter the object id can be some unique value. + MESSAGES -------- @@ -58,6 +67,15 @@ EXAMPLES # Install default xmonad config for user 'eve'. Parent directory is created automatically. __dot_file .xmonad/xmonad.hs --user eve --state exists --source "$__files/xmonad.hs" + # install .vimrc for root and some users + for user in root userx usery userz; do + __dot_file "${user}_dot_vimrc" \ + --user $user \ + --file .vimrc \ + --state exists \ + --source "$__files/$user/.vimrc" + done + SEE ALSO -------- diff --git a/cdist/conf/type/__dot_file/manifest b/cdist/conf/type/__dot_file/manifest index 5e4957e5..a38ed943 100755 --- a/cdist/conf/type/__dot_file/manifest +++ b/cdist/conf/type/__dot_file/manifest @@ -19,13 +19,20 @@ set -eu user="$(cat "${__object}/parameter/user")" home="$(cat "${__object}/explorer/home")" primary_group="$(cat "${__object}/explorer/primary_group")" +dirmode="$(cat "${__object}/parameter/dirmode")" +if [ -f "${__object}/parameter/file" ]; then + file="$(cat "${__object}/parameter/file")" +else + file="${__object_id}" +fi + # Create parent directory. Type __directory has flag 'parents', but it # will leave us with root-owned directory in user home, which is not # acceptable. So we create parent directories one-by-one. XXX: maybe # it should be fixed in '__directory'? set -- -subpath=${__object_id} +subpath=${file} while subpath="$(dirname "${subpath}")" ; do [ "${subpath}" = . ] && break set -- "${subpath}" "$@" @@ -36,6 +43,7 @@ export CDIST_ORDER_DEPENDENCY for dir ; do __directory "${home}/${dir}" \ --group "${primary_group}" \ + --mode "${dirmode}" \ --owner "${user}" done @@ -62,4 +70,4 @@ if [ "${source}" = "-" ] ; then fi unset source -__file "${home}/${__object_id}" --owner "$user" --group "$primary_group" "$@" +__file "${home}/${file}" --owner "$user" --group "$primary_group" "$@" diff --git a/cdist/conf/type/__dot_file/parameter/default/dirmode b/cdist/conf/type/__dot_file/parameter/default/dirmode new file mode 100644 index 00000000..e9745d1f --- /dev/null +++ b/cdist/conf/type/__dot_file/parameter/default/dirmode @@ -0,0 +1 @@ +0700 diff --git a/cdist/conf/type/__dot_file/parameter/optional b/cdist/conf/type/__dot_file/parameter/optional index ccab9fa6..9f7f83fb 100644 --- a/cdist/conf/type/__dot_file/parameter/optional +++ b/cdist/conf/type/__dot_file/parameter/optional @@ -1,3 +1,4 @@ state mode source +dirmode diff --git a/cdist/conf/type/__download/explorer/remote_cmd_get b/cdist/conf/type/__download/explorer/remote_cmd_get new file mode 100755 index 00000000..9f1cd59c --- /dev/null +++ b/cdist/conf/type/__download/explorer/remote_cmd_get @@ -0,0 +1,16 @@ +#!/bin/sh -e + +if [ -f "$__object/parameter/cmd-get" ] +then + cat "$__object/parameter/cmd-get" +elif + command -v curl > /dev/null +then + echo "curl -sSL -o - '%s'" +elif + command -v fetch > /dev/null +then + echo "fetch -o - '%s'" +else + echo "wget -O - '%s'" +fi diff --git a/cdist/conf/type/__download/explorer/remote_cmd_sum b/cdist/conf/type/__download/explorer/remote_cmd_sum new file mode 100755 index 00000000..84df663c --- /dev/null +++ b/cdist/conf/type/__download/explorer/remote_cmd_sum @@ -0,0 +1,82 @@ +#!/bin/sh -e + +if [ ! -f "$__object/parameter/sum" ] +then + exit 0 +fi + +if [ -f "$__object/parameter/cmd-sum" ] +then + cat "$__object/parameter/cmd-sum" + exit 0 +fi + +sum_should="$( cat "$__object/parameter/sum" )" + +if echo "$sum_should" | grep -Fq ':' +then + sum_hash="$( echo "$sum_should" | cut -d : -f 1 )" +else + if echo "$sum_should" | grep -Eq '^[0-9]+\s[0-9]+$' + then + sum_hash='cksum' + elif + echo "$sum_should" | grep -Eiq '^[a-f0-9]{32}$' + then + sum_hash='md5' + elif + echo "$sum_should" | grep -Eiq '^[a-f0-9]{40}$' + then + sum_hash='sha1' + elif + echo "$sum_should" | grep -Eiq '^[a-f0-9]{64}$' + then + sum_hash='sha256' + else + echo 'hash format detection failed' >&2 + exit 1 + fi +fi + +os="$( "$__explorer/os" )" + +case "$sum_hash" in + cksum) + echo "cksum %s | awk '{print \$1\" \"\$2}'" + ;; + md5) + case "$os" in + freebsd) + echo "md5 -q %s" + ;; + *) + echo "md5sum %s | awk '{print \$1}'" + ;; + esac + ;; + sha1) + case "$os" in + freebsd) + echo "sha1 -q %s" + ;; + *) + echo "sha1sum %s | awk '{print \$1}'" + ;; + esac + ;; + sha256) + case "$os" in + freebsd) + echo "sha256 -q %s" + ;; + *) + echo "sha256sum %s | awk '{print \$1}'" + ;; + esac + ;; + *) + # we arrive here only if --sum is given with unknown format prefix + echo "unknown hash format: $sum_hash" >&2 + exit 1 + ;; +esac diff --git a/cdist/conf/type/__download/explorer/state b/cdist/conf/type/__download/explorer/state index 6a50f5a5..8c5d5ce1 100755 --- a/cdist/conf/type/__download/explorer/state +++ b/cdist/conf/type/__download/explorer/state @@ -1,20 +1,45 @@ #!/bin/sh -e -dst="/$__object_id" +if [ -f "$__object/parameter/destination" ] +then + dst="$( cat "$__object/parameter/destination" )" +else + dst="/$__object_id" +fi + +if [ ! -f "$dst" ] +then + echo 'absent' + exit 0 +fi + +if [ ! -f "$__object/parameter/sum" ] +then + echo 'present' + exit 0 +fi + +sum_should="$( cat "$__object/parameter/sum" )" + +if echo "$sum_should" | grep -Fq ':' +then + sum_should="$( echo "$sum_should" | cut -d : -f 2 )" +fi + +sum_cmd="$( "$__type_explorer/remote_cmd_sum" )" # shellcheck disable=SC2059 -cmd="$( printf "$( cat "$__object/parameter/cmd-sum" )" "$dst" )" +sum_is="$( eval "$( printf "$sum_cmd" "'$dst'" )" )" -sum="$( cat "$__object/parameter/sum" )" - -if [ -f "$dst" ] +if [ -z "$sum_is" ] then - if [ "$( eval "$cmd" )" = "$sum" ] - then - echo 'present' - else - echo 'mismatch' - fi -else - echo 'absent' + echo 'existing destination checksum failed' >&2 + exit 1 +fi + +if [ "$sum_is" = "$sum_should" ] +then + echo 'present' +else + echo 'mismatch' fi diff --git a/cdist/conf/type/__download/gencode-local b/cdist/conf/type/__download/gencode-local index 49e9c699..d1b0d0d5 100755 --- a/cdist/conf/type/__download/gencode-local +++ b/cdist/conf/type/__download/gencode-local @@ -1,23 +1,143 @@ #!/bin/sh -e +download="$( cat "$__object/parameter/download" )" + state_is="$( cat "$__object/explorer/state" )" -if [ "$state_is" = 'present' ] +if [ "$download" != 'local' ] || [ "$state_is" = 'present' ] then exit 0 fi url="$( cat "$__object/parameter/url" )" -cmd="$( cat "$__object/parameter/cmd-get" )" +if [ -f "$__object/parameter/destination" ] +then + dst="$( cat "$__object/parameter/destination" )" +else + dst="/$__object_id" +fi -tmp="$( mktemp )" +if [ -f "$__object/parameter/cmd-get" ] +then + cmd="$( cat "$__object/parameter/cmd-get" )" -dst="/$__object_id" +elif command -v curl > /dev/null +then + cmd="curl -sSL -o - '%s'" -printf "$cmd > %s\n" \ - "$url" \ - "$tmp" +elif command -v fetch > /dev/null +then + cmd="fetch -o - '%s'" + +elif command -v wget > /dev/null +then + cmd="wget -O - '%s'" + +else + echo 'local download failed, no usable utility' >&2 + exit 1 +fi + +echo "download_tmp=\"\$( mktemp )\"" + +# shellcheck disable=SC2059 +printf "$cmd > \"\$download_tmp\"\n" "$url" + +if [ -f "$__object/parameter/sum" ] +then + sum_should="$( cat "$__object/parameter/sum" )" + + if [ -f "$__object/parameter/cmd-sum" ] + then + local_cmd_sum="$( cat "$__object/parameter/cmd-sum" )" + else + if echo "$sum_should" | grep -Fq ':' + then + sum_hash="$( echo "$sum_should" | cut -d : -f 1 )" + + sum_should="$( echo "$sum_should" | cut -d : -f 2 )" + else + if echo "$sum_should" | grep -Eq '^[0-9]+\s[0-9]+$' + then + sum_hash='cksum' + elif + echo "$sum_should" | grep -Eiq '^[a-f0-9]{32}$' + then + sum_hash='md5' + elif + echo "$sum_should" | grep -Eiq '^[a-f0-9]{40}$' + then + sum_hash='sha1' + elif + echo "$sum_should" | grep -Eiq '^[a-f0-9]{64}$' + then + sum_hash='sha256' + else + echo 'hash format detection failed' >&2 + exit 1 + fi + fi + + case "$sum_hash" in + cksum) + local_cmd_sum="cksum %s | awk '{print \$1\" \"\$2}'" + ;; + md5) + if command -v md5 > /dev/null + then + local_cmd_sum="md5 -q %s" + elif + command -v md5sum > /dev/null + then + local_cmd_sum="md5sum %s | awk '{print \$1}'" + fi + ;; + sha1) + if command -v sha1 > /dev/null + then + local_cmd_sum="sha1 -q %s" + elif + command -v sha1sum > /dev/null + then + local_cmd_sum="sha1sum %s | awk '{print \$1}'" + fi + ;; + sha256) + if command -v sha256 > /dev/null + then + local_cmd_sum="sha256 -q %s" + elif + command -v sha256sum > /dev/null + then + local_cmd_sum="sha256sum %s | awk '{print \$1}'" + fi + ;; + *) + # we arrive here only if --sum is given with unknown format prefix + echo "unknown hash format: $sum_hash" >&2 + exit 1 + ;; + esac + + if [ -z "$local_cmd_sum" ] + then + echo 'local checksum verification failed, no usable utility' >&2 + exit 1 + fi + fi + + # shellcheck disable=SC2059 + echo "sum_is=\"\$( $( printf "$local_cmd_sum" "\"\$download_tmp\"" ) )\"" + + echo "if [ \"\$sum_is\" != '$sum_should' ]; then" + + echo "echo 'local download checksum mismatch' >&2" + + echo "rm -f \"\$download_tmp\"" + + echo 'exit 1; fi' +fi if echo "$__target_host" | grep -Eq '^[0-9a-fA-F:]+$' then @@ -26,10 +146,10 @@ else target_host="$__target_host" fi -printf '%s %s %s:%s\n' \ +# shellcheck disable=SC2016 +printf '%s "$download_tmp" %s:%s\n' \ "$__remote_copy" \ - "$tmp" \ "$target_host" \ "$dst" -echo "rm -f '$tmp'" +echo "rm -f \"\$download_tmp\"" diff --git a/cdist/conf/type/__download/gencode-remote b/cdist/conf/type/__download/gencode-remote new file mode 100755 index 00000000..e49bcec3 --- /dev/null +++ b/cdist/conf/type/__download/gencode-remote @@ -0,0 +1,59 @@ +#!/bin/sh -e + +download="$( cat "$__object/parameter/download" )" + +state_is="$( cat "$__object/explorer/state" )" + +if [ "$download" = 'remote' ] && [ "$state_is" != 'present' ] +then + cmd_get="$( cat "$__object/explorer/remote_cmd_get" )" + + url="$( cat "$__object/parameter/url" )" + + if [ -f "$__object/parameter/destination" ] + then + dst="$( cat "$__object/parameter/destination" )" + else + dst="/$__object_id" + fi + + echo "download_tmp=\"\$( mktemp )\"" + + # shellcheck disable=SC2059 + printf "$cmd_get > \"\$download_tmp\"\n" "$url" + + if [ -f "$__object/parameter/sum" ] + then + sum_should="$( cat "$__object/parameter/sum" )" + + if [ -f "$__object/parameter/cmd-sum" ] + then + remote_cmd_sum="$( cat "$__object/parameter/cmd-sum" )" + else + remote_cmd_sum="$( cat "$__object/explorer/remote_cmd_sum" )" + + if echo "$sum_should" | grep -Fq ':' + then + sum_should="$( echo "$sum_should" | cut -d : -f 2 )" + fi + fi + + # shellcheck disable=SC2059 + echo "sum_is=\"\$( $( printf "$remote_cmd_sum" "\"\$download_tmp\"" ) )\"" + + echo "if [ \"\$sum_is\" != '$sum_should' ]; then" + + echo "echo 'remote download checksum mismatch' >&2" + + echo "rm -f \"\$download_tmp\"" + + echo 'exit 1; fi' + fi + + echo "mv \"\$download_tmp\" '$dst'" +fi + +if [ -f "$__object/parameter/onchange" ] && [ "$state_is" != "present" ] +then + cat "$__object/parameter/onchange" +fi diff --git a/cdist/conf/type/__download/man.rst b/cdist/conf/type/__download/man.rst index c973448f..c16510a9 100644 --- a/cdist/conf/type/__download/man.rst +++ b/cdist/conf/type/__download/man.rst @@ -3,36 +3,70 @@ cdist-type__download(7) NAME ---- -cdist-type__download - Download file to local storage and copy it to target host +cdist-type__download - Download a file DESCRIPTION ----------- -You must use persistent storage in target host for destination file -(``$__object_id``) because it will be used for checksum calculation -in order to decide if file must be downloaded. +By default type will try to use ``curl``, ``fetch`` or ``wget``. +If download happens in target (see ``--download``) then type will +fallback to (and install) ``wget``. + +If download happens in local machine, then environment variables like +``{http,https,ftp}_proxy`` etc can be used on cdist execution +(``http_proxy=foo cdist config ...``). + +To change downloaded file's owner, group or permissions, use ``require='__download/path/to/file' __file ...``. REQUIRED PARAMETERS ------------------- url - URL from which to download the file. - -sum - Checksum of downloaded file. + File's URL. OPTIONAL PARAMETERS ------------------- +destination + Downloaded file's destination in target. If unset, ``$__object_id`` is used. + +sum + Supported formats: ``cksum`` output without file name, MD5, SHA1 and SHA256. + + Type tries to detect hash format with regexes, but prefixes + ``cksum:``, ``md5:``, ``sha1:`` and ``sha256:`` are also supported. + + Checksum have two purposes - state check and post-download verification. + In state check, if destination checksum mismatches, then content of URL + will be downloaded to temporary file. If downloaded temporary file's + checksum matches, then it will be moved to destination (overwritten). + + For local downloads it is expected that usable utilities for checksum + calculation exist in the system. + +download + If ``local`` (default), then file is downloaded to local storage and copied + to target host. If ``remote``, then download happens in target. + + For local downloads it is expected that usable utilities for downloading + exist in the system. Type will try to use ``curl``, ``fetch`` or ``wget``. + cmd-get Command used for downloading. - Default is ``wget -O- '%s'``. Command must output to ``stdout``. + Parameter will be used for ``printf`` and must include only one + format specification ``%s`` which will become URL. + For example: ``wget -O - '%s'``. cmd-sum Command used for checksum calculation. - Default is ``md5sum '%s' | awk '{print $1}'``. Command output and ``--sum`` parameter must match. + Parameter will be used for ``printf`` and must include only one + format specification ``%s`` which will become destination. + For example: ``md5sum '%s' | awk '{print $1}'``. + +onchange + Execute this command after download. EXAMPLES @@ -49,7 +83,8 @@ EXAMPLES require='__download/opt/cpma/cnq3.zip' \ __unpack /opt/cpma/cnq3.zip \ - --move-existing-destination \ + --backup-destination \ + --preserve-archive \ --destination /opt/cpma/server @@ -60,7 +95,7 @@ Ander Punnar COPYING ------- -Copyright \(C) 2020 Ander Punnar. You can redistribute it +Copyright \(C) 2021 Ander Punnar. 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. diff --git a/cdist/conf/type/__download/manifest b/cdist/conf/type/__download/manifest new file mode 100755 index 00000000..3d4c498b --- /dev/null +++ b/cdist/conf/type/__download/manifest @@ -0,0 +1,6 @@ +#!/bin/sh -e + +if grep -Eq '^wget' "$__object/explorer/remote_cmd_get" +then + __package wget +fi diff --git a/cdist/conf/type/__download/parameter/default/cmd-get b/cdist/conf/type/__download/parameter/default/cmd-get deleted file mode 100644 index 2daa38a1..00000000 --- a/cdist/conf/type/__download/parameter/default/cmd-get +++ /dev/null @@ -1 +0,0 @@ -wget -O- '%s' diff --git a/cdist/conf/type/__download/parameter/default/cmd-sum b/cdist/conf/type/__download/parameter/default/cmd-sum deleted file mode 100644 index 3e8a9295..00000000 --- a/cdist/conf/type/__download/parameter/default/cmd-sum +++ /dev/null @@ -1 +0,0 @@ -md5sum '%s' | awk '{print $1}' diff --git a/cdist/conf/type/__download/parameter/default/download b/cdist/conf/type/__download/parameter/default/download new file mode 100644 index 00000000..40830374 --- /dev/null +++ b/cdist/conf/type/__download/parameter/default/download @@ -0,0 +1 @@ +local diff --git a/cdist/conf/type/__download/parameter/optional b/cdist/conf/type/__download/parameter/optional index 22783e02..e809ef78 100644 --- a/cdist/conf/type/__download/parameter/optional +++ b/cdist/conf/type/__download/parameter/optional @@ -1,2 +1,6 @@ cmd-get cmd-sum +destination +download +onchange +sum diff --git a/cdist/conf/type/__download/parameter/required b/cdist/conf/type/__download/parameter/required index 6ea4c38f..96cdd3b9 100644 --- a/cdist/conf/type/__download/parameter/required +++ b/cdist/conf/type/__download/parameter/required @@ -1,2 +1 @@ url -sum diff --git a/cdist/conf/type/__dpkg_architecture/explorer/architecture b/cdist/conf/type/__dpkg_architecture/explorer/architecture new file mode 100755 index 00000000..03e7e386 --- /dev/null +++ b/cdist/conf/type/__dpkg_architecture/explorer/architecture @@ -0,0 +1,26 @@ +#!/bin/sh -e +# __dpkg_architecture/explorer/architecture +# +# 2020 Matthias Stecher +# +# 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 . +# + +# Get the main architecture of this machine + + +# print or die in the gencode-remote +dpkg --print-architecture || true diff --git a/cdist/conf/type/__dpkg_architecture/explorer/foreign-architectures b/cdist/conf/type/__dpkg_architecture/explorer/foreign-architectures new file mode 100755 index 00000000..a150d307 --- /dev/null +++ b/cdist/conf/type/__dpkg_architecture/explorer/foreign-architectures @@ -0,0 +1,26 @@ +#!/bin/sh -e +# __dpkg_architecture/explorer/foreign-architectures +# +# 2020 Matthias Stecher +# +# 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 . +# + +# Print all additional architectures + + +# print or die in the gencode-remote +dpkg --print-foreign-architectures || true diff --git a/cdist/conf/type/__dpkg_architecture/gencode-remote b/cdist/conf/type/__dpkg_architecture/gencode-remote new file mode 100755 index 00000000..47fb24e7 --- /dev/null +++ b/cdist/conf/type/__dpkg_architecture/gencode-remote @@ -0,0 +1,82 @@ +#!/bin/sh -e +# __dpkg_architecture/gencode-remote +# +# 2020 Matthias Stecher +# +# 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 . +# + + +# Get parameter and explorer +state_should="$(cat "$__object/parameter/state")" +arch_wanted="$__object_id" +main_arch="$(cat "$__object/explorer/architecture")" + +# Exit here if dpkg do not work (empty explorer) +if [ -z "$main_arch" ]; then + echo "dpkg is not available or unable to detect a architecture!" >&2 + exit 1 +fi + + +# Check if requested architecture is the main one +if [ "$arch_wanted" = "$main_arch" ]; then + # higher than present; we can not remove it + state_is="present" + caution="yes" + +# Check if the architecture not already used +elif grep -qFx "$arch_wanted" "$__object/explorer/foreign-architectures"; then + state_is="present" + +# arch does not exist +else + state_is="absent" +fi + + +# Check what to do +if [ "$state_is" != "$state_should" ]; then + case "$state_should" in + present) + # print add code + printf "dpkg --add-architecture '%s'\n" "$arch_wanted" + # updating the index to make the new architecture available + echo "apt update" + + echo added >> "$__messages_out" + ;; + + absent) + if [ "$caution" ]; then + printf "can not remove the main arch '%s' of the system!\n" "$main_arch" >&2 + exit 1 + fi + + # removing all existing packages for the architecture + printf "apt purge '.*:%s'\n" "$arch_wanted" + # print remove code + printf "dpkg --remove-architecture '%s'\n" "$arch_wanted" + + echo removed >> "$__messages_out" + ;; + + *) + printf "state '%s' is unknown!\n" "$state_should" >&2 + exit 1 + ;; + esac +fi diff --git a/cdist/conf/type/__dpkg_architecture/man.rst b/cdist/conf/type/__dpkg_architecture/man.rst new file mode 100644 index 00000000..fa196229 --- /dev/null +++ b/cdist/conf/type/__dpkg_architecture/man.rst @@ -0,0 +1,103 @@ +cdist-type__dpkg_architecture(7) +================================ + +NAME +---- +cdist-type__dpkg_architecture - Handles foreign architectures on debian-like +systems managed by `dpkg` + + +DESCRIPTION +----------- +This type handles foreign architectures on systems managed by +:strong:`dpkg`\ (1). The object id is the name of the architecture accepted by +`dpkg`, which should be added or removed. + +If the architecture is not setup on the system, it adds a new architecture as a +new foreign architecture in `dpkg`. Then, it updates the apt package index to +make packages from the new architecture available. + +If the architecture should be removed, it will remove it if it is not the base +architecture on where the system was installed on. Before it, it will purge +every package based on the "to be removed" architecture via `apt` to be able to +remove the selected architecture. + + +REQUIRED PARAMETERS +------------------- +None. + + +OPTIONAL PARAMETERS +------------------- +state + ``present`` or ``absent``. Defaults to ``present``. + + +MESSAGES +-------- +added + Added the specified architecture + +removed + Removed the specified architecture + + +ABORTS +------ +Aborts in the following cases: + +If :strong:`dpkg`\ (1) is not available. It will abort with a proper error +message. + +If the architecture is the same as the base architecture the system is build +upon it (returned by ``dpkg --print-architecture``) and it should be removed. + +It will fail if it can not execute :strong:`apt`\ (8). It is assumed that it is +already installed. + + +EXAMPLES +-------- + +.. code-block:: sh + + # add i386 (32 bit) architecture + __dpkg_architecture i386 + + # remove it again :) + __dpkg_architecture i386 --state absent + + +SEE ALSO +-------- +`Multiarch on Debian systems `_ + +`How to setup multiarch on Debian `_ + +:strong:`dpkg`\ (1) +:strong:`cdist-type__package_dpkg`\ (7) +:strong:`cdist-type__package_apt`\ (7) + +Useful commands: + +.. code-block:: sh + + # base architecture installed on this system + dpkg --print-architecture + + # extra architectures added + dpkg --print-foreign-architectures + + +AUTHORS +------- +Matthias Stecher + + +COPYING +------- +Copyright \(C) 2020 Matthias Stecher. You can redistribute it +and/or modify it under the terms of the GNU General Public License as +ublished by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. diff --git a/cdist/conf/type/__dpkg_architecture/nonparallel b/cdist/conf/type/__dpkg_architecture/nonparallel new file mode 100644 index 00000000..e69de29b diff --git a/cdist/conf/type/__dpkg_architecture/parameter/default/state b/cdist/conf/type/__dpkg_architecture/parameter/default/state new file mode 100644 index 00000000..e7f6134f --- /dev/null +++ b/cdist/conf/type/__dpkg_architecture/parameter/default/state @@ -0,0 +1 @@ +present diff --git a/cdist/conf/type/__dpkg_architecture/parameter/optional b/cdist/conf/type/__dpkg_architecture/parameter/optional new file mode 100644 index 00000000..ff72b5c7 --- /dev/null +++ b/cdist/conf/type/__dpkg_architecture/parameter/optional @@ -0,0 +1 @@ +state diff --git a/cdist/conf/type/__file/explorer/stat b/cdist/conf/type/__file/explorer/stat index 91c8cc84..29b3c8a3 100755 --- a/cdist/conf/type/__file/explorer/stat +++ b/cdist/conf/type/__file/explorer/stat @@ -31,10 +31,10 @@ fallback() { gid=$(echo "$ls_line" | awk '{ print $4 }') owner=$(awk -F: -v uid="$uid" '$3 == uid { print $1; f=1 } END { if (!f) print "UNKNOWN" }' /etc/passwd) - group=$(awk -F: -v uid="$uid" '$3 == uid { print $1; f=1 } END { if (!f) print "UNKNOWN" }' /etc/group) + group=$(awk -F: -v gid="$gid" '$3 == gid { print $1; f=1 } END { if (!f) print "UNKNOWN" }' /etc/group) mode_text=$(echo "$ls_line" | awk '{ print $1 }') - mode=$(echo "$mode_text" | awk '{ k=0; for (i=0; i<=8; i++) k += ((substr($1, i+2, 1) ~ /[rwx]/) * 2^(8-i)); printf("%0o", k) }') + mode=$(echo "$mode_text" | awk '{for(i=8;i>=0;--i){c=substr($1,10-i,1);k+=((c~/[rwxst]/)*2^i);if(!(i%3))k+=(tolower(c)~/[lst]/)*2^(9+i/3)}printf("%04o",k)}') size=$(echo "$ls_line" | awk '{ print $5 }') links=$(echo "$ls_line" | awk '{ print $2 }') @@ -53,64 +53,32 @@ fallback() { [ -e "$destination" ] || exit 0 -if ! command -v stat >/dev/null -then +command -v stat >/dev/null 2>&1 || { fallback exit -fi +} case $("$__explorer/os") in freebsd|netbsd|openbsd|macosx) - stat -f "type: %HT + stat -f 'type: %HT owner: %Du %Su group: %Dg %Sg -mode: %Lp %Sp +mode: %Mp%03Lp %Sp size: %Dz links: %Dl -" "$destination" | awk '/^type/ { print tolower($0); next } { print }' +' "$destination" | awk '/^type/ { print tolower($0); next } { print }' ;; - solaris) - ls1="$( ls -ld "$destination" )" - ls2="$( ls -ldn "$destination" )" - - if [ -f "$__object/parameter/mode" ] - then mode_should="$( cat "$__object/parameter/mode" )" - fi - - # yes, it is ugly hack, but if you know better way... - if [ -z "$( find "$destination" -perm "$mode_should" )" ] - then octets=888 - else octets="$( echo "$mode_should" | sed 's/^0//' )" - fi - - case "$( echo "$ls1" | cut -c1-1 )" in - -) echo 'type: regular file' ;; - d) echo 'type: directory' ;; - esac - - echo "owner: $( echo "$ls2" \ - | awk '{print $3}' ) $( echo "$ls1" \ - | awk '{print $3}' )" - - echo "group: $( echo "$ls2" \ - | awk '{print $4}' ) $( echo "$ls1" \ - | awk '{print $4}' )" - - echo "mode: $octets $( echo "$ls1" | awk '{print $1}' )" - echo "size: $( echo "$ls1" | awk '{print $5}' )" - echo "links: $( echo "$ls1" | awk '{print $2}' )" - ;; *) # NOTE: Do not use --printf here as it is not supported by BusyBox stat. # NOTE: BusyBox's stat might not support the "-c" option, in which case # we fall through to the shell fallback. - stat -c "type: %F + stat -c 'type: %F owner: %u %U group: %g %G -mode: %a %A +mode: %04a %A size: %s -links: %h" "$destination" 2>/dev/null || fallback - ;; +links: %h' "$destination" 2>/dev/null || fallback + ;; esac diff --git a/cdist/conf/type/__file/gencode-local b/cdist/conf/type/__file/gencode-local index 231b6927..5a303308 100755 --- a/cdist/conf/type/__file/gencode-local +++ b/cdist/conf/type/__file/gencode-local @@ -1,7 +1,7 @@ #!/bin/sh -e # # 2011-2012 Nico Schottelius (nico-cdist at schottelius.org) -# 2013 Steven Armstrong (steven-cdist armstrong.cc) +# 2013-2022 Steven Armstrong (steven-cdist armstrong.cc) # # This file is part of cdist. # @@ -72,6 +72,7 @@ if [ "$state_should" = "present" ] || [ "$state_should" = "exists" ]; then if [ "$type" != "file" ]; then # destination is not a regular file, upload source to replace it upload_file=1 + echo upload >> "$__messages_out" else local_cksum="$(cksum < "$source")" remote_cksum="$(cat "$__object/explorer/cksum")" @@ -88,27 +89,39 @@ if [ "$state_should" = "present" ] || [ "$state_should" = "exists" ]; then mkdir "$__object/files" touch "$__object/files/set-attributes" - # upload file to temp location - tempfile_template="${destination}.cdist.XXXXXXXXXX" - cat << DONE -destination_upload="\$($__remote_exec $__target_host "mktemp $tempfile_template")" -DONE - if [ "$upload_file" ]; then - echo upload >> "$__messages_out" - # IPv6 fix - if echo "${__target_host}" | grep -q -E '^[0-9a-fA-F:]+$' - then - my_target_host="[${__target_host}]" - else - my_target_host="${__target_host}" - fi - cat << DONE -$__remote_copy "$source" "${my_target_host}:\$destination_upload" -DONE + if [ "$create_file" ]; then + # When creating an empty file we create it locally and then + # upload it so that permissions can be set before moving the file + # into place. + source="$__object/files/empty" + touch "$source" fi -# move uploaded file into place -cat << DONE -$__remote_exec $__target_host "rm -rf \"$destination\"; mv \"\$destination_upload\" \"$destination\"" + + # upload file to temp location + upload_destination="${destination}.cdist.${__cdist_object_marker}.$$" + # Yes, we are aware that this is a race condition. + # However: + # a) cdist usually writes to directories that are not user writable + # (probably > 99.9%) + # b) if they are user owned, the user / attacker always wins + # (probably < 0.1%) + # c) the only case which we could improve are tmp directories and we + # don't think managing tmp directories with cdist is a typical case + # ("the rest %)" + + # Tell gencode-remote to where we uploaded the file so it can move + # it to its final destination. + echo "$upload_destination" > "$__object/files/upload-destination" + + # IPv6 fix + if echo "${__target_host}" | grep -q -E '^[0-9a-fA-F:]+$' + then + my_target_host="[${__target_host}]" + else + my_target_host="${__target_host}" + fi + cat << DONE +$__remote_copy "$source" "${my_target_host}:${upload_destination}" DONE fi fi diff --git a/cdist/conf/type/__file/gencode-remote b/cdist/conf/type/__file/gencode-remote index 815593bd..1a9ff69c 100755 --- a/cdist/conf/type/__file/gencode-remote +++ b/cdist/conf/type/__file/gencode-remote @@ -1,7 +1,7 @@ #!/bin/sh -e # # 2011-2013 Nico Schottelius (nico-cdist at schottelius.org) -# 2013 Steven Armstrong (steven-cdist armstrong.cc) +# 2013-2022 Steven Armstrong (steven-cdist armstrong.cc) # # This file is part of cdist. # @@ -62,15 +62,24 @@ set_mode() { case "$state_should" in present|exists) + if [ -f "$__object/files/upload-destination" ]; then + final_destination="$destination" + # We change the 'global' $destination variable here so we can + # change attributes of the new/uploaded file before moving it + # to it's final destination. + destination="$(cat "$__object/files/upload-destination")" + fi # Note: Mode - needs to happen last as a chown/chgrp can alter mode by # clearing S_ISUID and S_ISGID bits (see chown(2)) for attribute in group owner mode; do if [ -f "$__object/parameter/$attribute" ]; then value_should="$(cat "$__object/parameter/$attribute")" - # change 0xxx format to xxx format => same as stat returns + # format mode in four digits => same as stat returns if [ "$attribute" = mode ]; then - value_should="$(echo "$value_should" | sed 's/^0\(...\)/\1/')" + # Convert to four-digit octal number (printf interprets + # strings with leading 0s as octal!) + value_should=$(printf '%04o' "0${value_should}") fi value_is="$(get_current_value "$attribute" "$value_should")" @@ -79,17 +88,17 @@ case "$state_should" in fi fi done + if [ -f "$__object/files/upload-destination" ]; then + # move uploaded file into place + printf 'rm -rf "%s"\n' "$final_destination" + printf 'mv "%s" "%s"\n' "$destination" "$final_destination" + fi if [ -f "$__object/files/set-attributes" ]; then # set-attributes is created if file is created or uploaded in gencode-local fire_onchange=1 fi ;; - pre-exists) - # pre-exists should never reach gencode-remote… - exit 1 - ;; - absent) if [ "$type" = "file" ]; then echo "rm -f '$destination'" @@ -98,6 +107,10 @@ case "$state_should" in fi ;; + pre-exists) + : + ;; + *) echo "Unknown state: $state_should" >&2 exit 1 diff --git a/cdist/conf/type/__filesystem/explorer/lsblk b/cdist/conf/type/__filesystem/explorer/lsblk index 9ae544ac..d376c09f 100644 --- a/cdist/conf/type/__filesystem/explorer/lsblk +++ b/cdist/conf/type/__filesystem/explorer/lsblk @@ -18,16 +18,16 @@ # along with cdist. If not, see . # -os=$("$__explorer/os") +os=$("${__explorer:?}/os") -if [ -f "$__object/parameter/device" ]; then +if [ -f "${__object:?}/parameter/device" ]; then blkdev="$(cat "$__object/parameter/device")" else - blkdev="$__object_id" + blkdev="${__object_id:?}" fi case "$os" in - centos|fedora|redhat|suse|gentoo) + alpine|centos|fedora|gentoo|redhat|suse|ubuntu) if [ ! -x "$(command -v lsblk)" ]; then echo "lsblk is required for __filesystem type" >&2 exit 1 diff --git a/cdist/conf/type/__git/explorer/group b/cdist/conf/type/__git/explorer/group index 3ddf9656..ab4396b1 100644 --- a/cdist/conf/type/__git/explorer/group +++ b/cdist/conf/type/__git/explorer/group @@ -1,5 +1,24 @@ -#!/bin/sh +#!/bin/sh -e -destination="/$__object_id/.git" +destination="/${__object_id:?}/.git" -stat --print "%G" "${destination}" 2>/dev/null || exit 0 +# shellcheck disable=SC2012 +group_gid=$(ls -ldn "${destination}" | awk '{ print $4 }') + +# NOTE: +1 because $((notanum)) prints 0. +if test $((group_gid + 1)) -ge 0 +then + group_should=$(cat "${__object:?}/parameter/group") + + if expr "${group_should}" : '[0-9]*$' >/dev/null + then + printf '%u\n' "${group_gid}" + else + if command -v getent > /dev/null + then + getent group "${group_gid}" | cut -d : -f 1 + else + awk -F: -v gid="${group_gid}" '$3 == gid { print $1 }' /etc/group + fi + fi +fi diff --git a/cdist/conf/type/__git/explorer/owner b/cdist/conf/type/__git/explorer/owner index 4c3cd431..4a4d0d13 100644 --- a/cdist/conf/type/__git/explorer/owner +++ b/cdist/conf/type/__git/explorer/owner @@ -1,5 +1,19 @@ -#!/bin/sh +#!/bin/sh -e -destination="/$__object_id/.git" +destination="/${__object_id:?}/.git" -stat --print "%U" "${destination}" 2>/dev/null || exit 0 +# shellcheck disable=SC2012 +owner_uid=$(ls -ldn "${destination}" | awk '{ print $3 }') + +# NOTE: +1 because $((notanum)) prints 0. +if test $((owner_uid + 1)) -ge 0 +then + owner_should=$(cat "${__object:?}/parameter/owner") + + if expr "${owner_should}" : '[0-9]*$' >/dev/null + then + printf '%u\n' "${owner_uid}" + else + printf '%s\n' "$(id -u -n "${owner_uid}")" + fi +fi diff --git a/cdist/conf/type/__grafana_dashboard/manifest b/cdist/conf/type/__grafana_dashboard/manifest index d145c4c3..0d944482 100755 --- a/cdist/conf/type/__grafana_dashboard/manifest +++ b/cdist/conf/type/__grafana_dashboard/manifest @@ -15,7 +15,7 @@ case $os in # Differntation not needed anymore apt_source_distribution=stable ;; - 10*) + 10*|11*) # Differntation not needed anymore apt_source_distribution=stable ;; diff --git a/cdist/conf/type/__haproxy_dualstack/files/http b/cdist/conf/type/__haproxy_dualstack/files/http new file mode 100644 index 00000000..0508a465 --- /dev/null +++ b/cdist/conf/type/__haproxy_dualstack/files/http @@ -0,0 +1,8 @@ +frontend http + bind BIND@:80 + mode http + option httplog + default_backend http + +backend http + mode http diff --git a/cdist/conf/type/__haproxy_dualstack/files/https b/cdist/conf/type/__haproxy_dualstack/files/https new file mode 100644 index 00000000..73deac46 --- /dev/null +++ b/cdist/conf/type/__haproxy_dualstack/files/https @@ -0,0 +1,10 @@ +frontend https + bind BIND@:443 + mode tcp + option tcplog + tcp-request inspect-delay 5s + tcp-request content accept if { req_ssl_hello_type 1 } + default_backend https + +backend https + mode tcp diff --git a/cdist/conf/type/__haproxy_dualstack/files/imaps b/cdist/conf/type/__haproxy_dualstack/files/imaps new file mode 100644 index 00000000..b1ec3793 --- /dev/null +++ b/cdist/conf/type/__haproxy_dualstack/files/imaps @@ -0,0 +1,12 @@ +frontend imaps + bind BIND@:143 + bind BIND@:993 + + mode tcp + option tcplog + tcp-request inspect-delay 5s + tcp-request content accept if { req_ssl_hello_type 1 } + default_backend imaps + +backend imaps + mode tcp diff --git a/cdist/conf/type/__haproxy_dualstack/files/smtps b/cdist/conf/type/__haproxy_dualstack/files/smtps new file mode 100644 index 00000000..dce6ed4a --- /dev/null +++ b/cdist/conf/type/__haproxy_dualstack/files/smtps @@ -0,0 +1,12 @@ +frontend smtps + bind BIND@:25 + bind BIND@:465 + + mode tcp + option tcplog + tcp-request inspect-delay 5s + tcp-request content accept if { req_ssl_hello_type 1 } + default_backend smtps + +backend smtps + mode tcp diff --git a/cdist/conf/type/__haproxy_dualstack/man.rst b/cdist/conf/type/__haproxy_dualstack/man.rst new file mode 100644 index 00000000..6c131cbe --- /dev/null +++ b/cdist/conf/type/__haproxy_dualstack/man.rst @@ -0,0 +1,121 @@ +cdist-type__haproxy_dualstack(7) +================================ + + +NAME +---- +cdist-type__haproxy_dualstack - Proxy services from a dual-stack server + + +DESCRIPTION +----------- +This (singleton) type installs and configures haproxy to act as a dual-stack +proxy for single-stack services. + +This can be useful to add IPv4 support to IPv6-only services while only using +one IPv4 for many such services. + +By default this type uses the plain TCP proxy mode, which means that there is no +need for TLS termination on this host when SNI is supported. +This also means that proxied services will not receive the client's IP address, +but will see the proxy's IP address instead (that of `$__target_host`). + +This can be solved by using the PROXY protocol, but do take into account that, +e.g. nginx cannot serve both regular HTTP(S) and PROXY protocols on the same +port, so you will need to use other ports for that. + +As a recommendation in this type: use TCP ports 8080 and 591 respectively to +serve HTTP and HTTPS using the PROXY protocol. + +See the EXAMPLES for more details. + + +OPTIONAL PARAMETERS +------------------- +v4proxy + Proxy incoming IPv4 connections to the equivalent IPv6 endpoint. + In its simplest use, it must be a NAME with an `AAAA` DNS entry, which is + the IP address actually providing the proxied services. + The full format of this argument is: + `[proxy:]NAME[[:PROTOCOL_1=PORT_1]...[:PROTOCOL_N=PORT_N]]` + Where starting with `proxy:` determines that the PROXY protocol must be + used and each `:PROTOCOL=PORT` (e.g. `:http=8080` or `:https=591`) is a PORT + override for the given PROTOCOL (see `--protocol`), if not present the + PROTOCOL's default port will be used. + + +v6proxy + Proxy incoming IPv6 connections to the equivalent IPv4 endpoint. + In its simplest use, it must be a NAME with an `A` DNS entry, which is + the IP address actually providing the proxied services. + See `--v4proxy` for more options and details. + +protocol + Can be passed multiple times or as a space-separated list of protocols. + Currently supported protocols are: `http`, `https`, `imaps`, `smtps`. + This defaults to: `http https imaps smtps`. + + +EXAMPLES +-------- + +.. code-block:: sh + + # Proxy the IPv6-only services so IPv4-only clients can access them + # This uses HAProxy's TCP mode for http, https, imaps and smtps + __haproxy_dualstack \ + --v4proxy ipv6.chat \ + --v4proxy matrix.ungleich.ch + + # Proxy the IPv6-only HTTP(S) services so IPv4-only clients can access them + # Note this means that the backend IPv6-only server will only see + # the IPv6 address of the haproxy host managed by cdist, which can be + # troublesome if this information is relevant for analytics/security/... + # See the PROXY example below + __haproxy_dualstack \ + --protocol http --protocol https \ + --v4proxy ipv6.chat \ + --v4proxy matrix.ungleich.ch + + # Use the PROXY protocol to proxy the IPv6-only HTTP(S) services enabling + # IPv4-only clients to access them while maintaining the client's IP address + __haproxy_dualstack \ + --protocol http --protocol https \ + --v4proxy proxy:ipv6.chat:http=8080:https=591 \ + --v4proxy proxy:matrix.ungleich.ch:http=8080:https=591 + # Note however that the PROXY protocol is not compatible with regular + # HTTP(S) protocols, so your nginx will have to listen on different ports + # with the PROXY settings. + # Note that you will need to restrict access to the 8080 port to prevent + # Client IP spoofing. + # This can be something like: + # server { + # # listen for regular HTTP connections + # listen [::]:80 default_server; + # listen 80 default_server; + # # listen for PROXY HTTP connections + # listen [::]:8080 proxy_protocol; + # # Accept the Client's IP from the PROXY protocol + # real_ip_header proxy_protocol; + # } + + +SEE ALSO +-------- +- https://www.haproxy.com/blog/enhanced-ssl-load-balancing-with-server-name-indication-sni-tls-extension/ +- https://www.haproxy.com/blog/haproxy/proxy-protocol/ +- https://docs.nginx.com/nginx/admin-guide/load-balancer/using-proxy-protocol/ + + +AUTHORS +------- +ungleich +Evilham + + +COPYING +------- +Copyright \(C) 2021 ungleich glarus ag. 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. diff --git a/cdist/conf/type/__haproxy_dualstack/manifest b/cdist/conf/type/__haproxy_dualstack/manifest new file mode 100644 index 00000000..d110eea6 --- /dev/null +++ b/cdist/conf/type/__haproxy_dualstack/manifest @@ -0,0 +1,155 @@ +#!/bin/sh -eu + +__package haproxy +require="__package/haproxy" __start_on_boot haproxy + +tmpdir="$__object/files" +mkdir "$tmpdir" +configtmp="$__object/files/haproxy.cfg" + +os=$(cat "$__global/explorer/os") +case $os in + freebsd) + CONFIG_FILE="/usr/local/etc/haproxy.conf" + cat < "$configtmp" +global + maxconn 4000 + user nobody + group nogroup + daemon + +EOF + + ;; + *) + CONFIG_FILE="/etc/haproxy/haproxy.cfg" + cat < "$configtmp" +global + log [::1] local2 + chroot /var/lib/haproxy + pidfile /var/run/haproxy.pid + maxconn 4000 + user haproxy + group haproxy + daemon + + # turn on stats unix socket + stats socket /var/lib/haproxy/stats + +EOF + ;; +esac + +cat <> "$configtmp" +defaults + retries 3 + log global + timeout http-request 10s + timeout queue 1m + timeout connect 10s + timeout client 1m + timeout server 1m + timeout http-keep-alive 10s + timeout check 10s +EOF + +dig_cmd="$(command -v dig || true)" +get_ip() { + # Usage: get_ip (ipv4|ipv6) NAME + # uses "dig" if available, else fallback to "host" + case $1 in + ipv4) + if [ -n "${dig_cmd}" ]; then + ${dig_cmd} +short A "$2" + else + host -t A "$2" | cut -d ' ' -f 4 | grep -v 'found:' + fi + ;; + ipv6) + if [ -n "${dig_cmd}" ]; then + ${dig_cmd} +short AAAA "$2" + else + host -t AAAA "$2" | cut -d ' ' -f 5 | grep -v 'NXDOMAIN' + fi + ;; + esac +} + +PROTOCOLS="$(cat "$__object/parameter/protocol")" + +for proxy in v4proxy v6proxy; do + param=$__object/parameter/$proxy + # no backend? skip generating code + if [ ! -f "$param" ]; then + continue + fi + + # turn backend name into bind parameter: v4backend -> ipv4@ + bind=$(echo $proxy | sed -e 's/^/ip/' -e 's/proxy//') + + case $bind in + ipv4) + backendproto=ipv6 + ;; + ipv6) + backendproto=ipv4 + ;; + esac + + for proto in ${PROTOCOLS}; do + # Add protocol "header" + printf "\n# %s %s \n" "${bind}" "${proto}" >> "$configtmp" + + sed -e "s/BIND/$bind/" \ + -e "s/\(frontend[[:space:]].*\)/\1$bind/" \ + -e "s/\(backend[[:space:]].*\)/\\1$bind/" \ + "$__type/files/$proto" >> "$configtmp" + + while read -r hostdefinition; do + if echo "$hostdefinition" | grep -qE '^proxy:'; then + # Proxy protocol was requested + host="$(echo "$hostdefinition" | sed -E 's/^proxy:([^:]+).*$/\1/')" + send_proxy=" send-proxy" + else + # Just use tcp proxy mode + host="$hostdefinition" + send_proxy="" + fi + if echo "$hostdefinition" | grep -qE ":${proto}="; then + # Use custom port definition if requested + port="$(echo "$hostdefinition" | sed -E "s/^(.*:)?${proto}=([0-9]+).*$/:\2/")" + else + # Else use the default + port="" + fi + servername=$host + + res=$(get_ip "$bind" "$servername") + + if [ -z "$res" ]; then + echo "$servername does not resolve - aborting config" >&2 + exit 1 + fi + + # Treat protocols without TLS+SNI specially + if [ "$proto" = http ]; then + echo " use-server $servername if { hdr(host) -i $host }" >> "$configtmp" + else + echo " use-server $servername if { req_ssl_sni -i $host }" >> "$configtmp" + fi + + # Create the "server" itself. + # Note that port and send_proxy will be empty unless + # they were requested by the type user + echo " server $servername ${backendproto}@${host}${port}${send_proxy}" >> "$configtmp" + + done < "$param" + done +done + +# Create config file +require="__package/haproxy" __file ${CONFIG_FILE} --source "$configtmp" --mode 0644 + +require="__file${CONFIG_FILE}" __check_messages "haproxy_reload" \ + --pattern "^__file${CONFIG_FILE}" \ + --execute "service haproxy reload || service haproxy restart" diff --git a/cdist/conf/type/__haproxy_dualstack/parameter/default/protocol b/cdist/conf/type/__haproxy_dualstack/parameter/default/protocol new file mode 100644 index 00000000..dc8bb7bf --- /dev/null +++ b/cdist/conf/type/__haproxy_dualstack/parameter/default/protocol @@ -0,0 +1 @@ +http https imaps smtps diff --git a/cdist/conf/type/__haproxy_dualstack/parameter/optional_multiple b/cdist/conf/type/__haproxy_dualstack/parameter/optional_multiple new file mode 100644 index 00000000..8c482bd4 --- /dev/null +++ b/cdist/conf/type/__haproxy_dualstack/parameter/optional_multiple @@ -0,0 +1,3 @@ +protocol +v4proxy +v6proxy diff --git a/cdist/conf/type/__haproxy_dualstack/singleton b/cdist/conf/type/__haproxy_dualstack/singleton new file mode 100644 index 00000000..e69de29b diff --git a/cdist/conf/type/__hostname/gencode-remote b/cdist/conf/type/__hostname/gencode-remote index ae224611..c1a97ac8 100755 --- a/cdist/conf/type/__hostname/gencode-remote +++ b/cdist/conf/type/__hostname/gencode-remote @@ -20,26 +20,27 @@ # along with cdist. If not, see . # -os=$(cat "$__global/explorer/os") -name_running=$(cat "$__global/explorer/hostname") -has_hostnamectl=$(cat "$__object/explorer/has_hostnamectl") +os=$(cat "${__global:?}/explorer/os") +name_running=$(cat "${__global:?}/explorer/hostname") +has_hostnamectl=$(cat "${__object:?}/explorer/has_hostnamectl") -if test -s "$__object/parameter/name" +if test -s "${__object:?}/parameter/name" then - name_should=$(cat "$__object/parameter/name") + name_should=$(cat "${__object:?}/parameter/name") else - case $os + case ${os} in # RedHat-derivatives and BSDs - centos|fedora|redhat|scientific|freebsd|macosx|netbsd|openbsd) + (centos|fedora|redhat|scientific|freebsd|macosx|netbsd|openbsd) # Hostname is FQDN - name_should="${__target_host}" - ;; - *) + name_should=${__target_host:?} + ;; + (*) # Hostname is only first component of FQDN - name_should="${__target_host%%.*}" - ;; + name_should=${__target_host:?} + name_should=${name_should%%.*} + ;; esac fi @@ -47,43 +48,46 @@ fi ################################################################################ # Check if the (running) hostname is already correct # -test "$name_running" != "$name_should" || exit 0 +test "${name_running}" != "${name_should}" || exit 0 ################################################################################ # Setup hostname # -echo 'changed' >>"$__messages_out" +echo 'changed' >>"${__messages_out:?}" # Use the good old way to set the hostname. -case $os +case ${os} in - alpine|debian|devuan|ubuntu) + (alpine|debian|devuan|ubuntu) echo 'hostname -F /etc/hostname' - ;; - archlinux) + ;; + (archlinux) echo 'command -v hostnamectl >/dev/null 2>&1' \ - "&& hostnamectl set-hostname '$name_should'" \ - "|| hostname '$name_should'" - ;; - centos|fedora|redhat|scientific|freebsd|netbsd|openbsd|gentoo|void) - echo "hostname '$name_should'" - ;; - macosx) - echo "scutil --set HostName '$name_should'" - ;; - solaris) - echo "uname -S '$name_should'" - ;; - slackware|suse|opensuse-leap) + "&& hostnamectl set-hostname '${name_should}'" \ + "|| hostname '${name_should}'" + ;; + (centos|fedora|redhat|scientific|freebsd|netbsd|openbsd|gentoo|void) + echo "hostname '${name_should}'" + ;; + (openwrt) + echo "echo '${name_should}' >/proc/sys/kernel/hostname" + ;; + (macosx) + echo "scutil --set HostName '${name_should}'" + ;; + (solaris) + echo "uname -S '${name_should}'" + ;; + (slackware|suse) # We do not read from /etc/HOSTNAME, because the running # hostname is the first component only while the file contains # the FQDN. - echo "hostname '$name_should'" - ;; - *) + echo "hostname '${name_should}'" + ;; + (*) # Fall back to set the hostname using hostnamectl, if available. - if test -n "$has_hostnamectl" + if test -n "${has_hostnamectl}" then # Don't use hostnamectl as the primary means to set the hostname for # systemd systems, because it cannot be trusted to work reliably and @@ -94,7 +98,8 @@ in echo "test \"\$(hostname)\" = \"\$(cat /etc/hostname)\"" \ " || hostname -F /etc/hostname" else - printf "echo 'Unsupported OS: %s' >&2\nexit 1\n" "$os" + printf "echo 'Unsupported OS: %s' >&2\n" "${os}" + printf 'exit 1\n' fi - ;; + ;; esac diff --git a/cdist/conf/type/__hostname/manifest b/cdist/conf/type/__hostname/manifest index e1e356a0..b80aa2ef 100755 --- a/cdist/conf/type/__hostname/manifest +++ b/cdist/conf/type/__hostname/manifest @@ -20,69 +20,49 @@ # along with cdist. If not, see . # -not_supported() { - echo "Your operating system ($os) is currently not supported by this type (${__type##*/})." >&2 - echo "Please contribute an implementation for it if you can." >&2 - exit 1 -} - set_hostname_systemd() { echo "$1" | __file /etc/hostname --source - } -os=$(cat "$__global/explorer/os") -os_version=$(cat "$__global/explorer/os_version") -os_major=$(echo "$os_version" | grep -o '^[0-9][0-9]*' || true) +os=$(cat "${__global:?}/explorer/os") -max_len=$(cat "$__object/explorer/max_len") -has_hostnamectl=$(cat "$__object/explorer/has_hostnamectl") +max_len=$(cat "${__object:?}/explorer/max_len") +has_hostnamectl=$(cat "${__object:?}/explorer/has_hostnamectl") -if test -s "$__object/parameter/name" +if test -s "${__object:?}/parameter/name" then - name_should=$(cat "$__object/parameter/name") + name_should=$(cat "${__object:?}/parameter/name") else - case $os + case ${os} in # RedHat-derivatives and BSDs - centos|fedora|redhat|scientific|freebsd|netbsd|openbsd|slackware) + (centos|fedora|redhat|scientific|freebsd|netbsd|openbsd|slackware|suse) # Hostname is FQDN - name_should="${__target_host}" - ;; - suse|opensuse-leap) - # Classic SuSE stores the FQDN in /etc/HOSTNAME, while - # systemd does not. The running hostname is the first - # component in both cases. - # In versions before 15.x, the FQDN is stored in /etc/hostname. - if test -n "$has_hostnamectl" && test "$os_major" -ge 15 \ - && test "$os_major" -ne 42 - then - name_should="${__target_host%%.*}" - else - name_should="${__target_host}" - fi - ;; + name_should=${__target_host:?} + ;; *) # Hostname is only first component of FQDN on all other systems. - name_should="${__target_host%%.*}" - ;; + name_should=${__target_host:?} + name_should=${name_should%%.*} + ;; esac fi -if test -n "$max_len" && test "$(printf '%s' "$name_should" | wc -c)" -gt "$max_len" +if test -n "${max_len}" && test "$(printf '%s' "${name_should}" | wc -c)" -gt "${max_len}" then printf "Host name too long. Up to %u characters allowed.\n" "${max_len}" >&2 exit 1 fi -case $os +case ${os} in - alpine|debian|devuan|ubuntu|void) - echo "$name_should" | __file /etc/hostname --source - - ;; - archlinux) - if test -n "$has_hostnamectl" + (alpine|debian|devuan|ubuntu|void) + echo "${name_should}" | __file /etc/hostname --source - + ;; + (archlinux) + if test -n "${has_hostnamectl}" then - set_hostname_systemd "$name_should" + set_hostname_systemd "${name_should}" else echo 'Ancient ArchLinux variants without hostnamectl are not supported.' >&2 exit 1 @@ -97,8 +77,8 @@ in # --value "\"$name_should\"" fi ;; - centos|fedora|redhat|scientific) - if test -z "$has_hostnamectl" + (centos|fedora|redhat|scientific) + if test -z "${has_hostnamectl}" then # Only write to /etc/sysconfig/network on non-systemd versions. # On systemd-based versions this entry is ignored. @@ -106,59 +86,83 @@ in --file /etc/sysconfig/network \ --delimiter '=' --exact_delimiter \ --key HOSTNAME \ - --value "\"$name_should\"" + --value "\"${name_should}\"" else - set_hostname_systemd "$name_should" + set_hostname_systemd "${name_should}" fi - ;; - gentoo) + ;; + (gentoo) # Only write to /etc/conf.d/hostname on OpenRC-based installations. # On systemd use hostnamectl(1) in gencode-remote. - if test -z "$has_hostnamectl" + if test -z "${has_hostnamectl}" then __key_value '/etc/conf.d/hostname:hostname' \ --file /etc/conf.d/hostname \ --delimiter '=' --exact_delimiter \ --key 'hostname' \ - --value "\"$name_should\"" + --value "\"${name_should}\"" else set_hostname_systemd "$name_should" fi - ;; - freebsd) + ;; + (freebsd) __key_value '/etc/rc.conf:hostname' \ --file /etc/rc.conf \ --delimiter '=' --exact_delimiter \ --key 'hostname' \ - --value "\"$name_should\"" - ;; - macosx) + --value "\"${name_should}\"" + ;; + (macosx) # handled in gencode-remote - : - ;; - netbsd) + ;; + (netbsd) __key_value '/etc/rc.conf:hostname' \ --file /etc/rc.conf \ --delimiter '=' --exact_delimiter \ --key 'hostname' \ - --value "\"$name_should\"" + --value "\"${name_should}\"" # To avoid confusion, ensure that the hostname is only stored once. __file /etc/myname --state absent - ;; - openbsd) - echo "$name_should" | __file /etc/myname --source - - ;; - slackware) + ;; + (openbsd) + echo "${name_should}" | __file /etc/myname --source - + ;; + (openwrt) + __uci system.@system[0].hostname --value "${name_should}" + # --transaction hostname + ;; + (slackware) # We write the FQDN into /etc/HOSTNAME. But /etc/rc.d/rc.M will only # read the first component from this file and set it as the running # hostname on boot. - echo "$name_should" | __file /etc/HOSTNAME --source - - ;; - solaris) - echo "$name_should" | __file /etc/nodename --source - - ;; - suse|opensuse-leap) + echo "${name_should}" | __file /etc/HOSTNAME --source - + ;; + (solaris) + echo "${name_should}" | __file /etc/nodename --source - + ;; + (suse) + if test -s "${__global:?}/explorer/os_release" + then + # shellcheck source=/dev/null + os_version=$(. "${__global:?}/explorer/os_release" && echo "${VERSION}") + else + os_version=$(sed -n 's/^VERSION\ *=\ *//p' "${__global:?}/explorer/os_version") + fi + os_major=$(expr "${os_version}" : '\([0-9]\{1,\}\)') + + # Classic SuSE stores the FQDN in /etc/HOSTNAME, while + # systemd does not. The running hostname is the first + # component in both cases. + # In versions before 15.x, the FQDN is stored in /etc/hostname. + if test -n "${has_hostnamectl}" \ + && test "${os_major}" -ge 15 \ + && test "${os_major}" -ne 42 + then + # strip away everything but the first part from $name_should + name_should=${name_should%%.*} + fi + # Modern SuSE provides /etc/HOSTNAME as a symlink for # backwards-compatibility. Unfortunately it cannot be used # here as __file does not follow the symlink. @@ -167,23 +171,25 @@ in # not work correctly on openSUSE 12.x which provides # hostnamectl but not /etc/hostname. - if test -n "$has_hostnamectl" -a "$os_major" -gt 12 + if test -n "${has_hostnamectl}" -a "${os_major}" -gt 12 then - hostname_file='/etc/hostname' + hostname_file=/etc/hostname else - hostname_file='/etc/HOSTNAME' + hostname_file=/etc/HOSTNAME fi - echo "$name_should" | __file "$hostname_file" --source - - ;; - *) + echo "${name_should}" | __file "${hostname_file}" --source - + ;; + (*) # On other operating systems we fall back to systemd's # hostnamectl if available… - if test -n "$has_hostnamectl" + if test -n "${has_hostnamectl}" then - set_hostname_systemd "$name_should" + set_hostname_systemd "${name_should}" else - not_supported + echo "Your operating system (${os}) is currently not supported by this type (${__type##*/})." >&2 + echo "Please contribute an implementation for it if you can." >&2 + exit 1 fi - ;; + ;; esac diff --git a/cdist/conf/type/__hosts/manifest b/cdist/conf/type/__hosts/manifest index 0d9e61f8..8103ebd5 100755 --- a/cdist/conf/type/__hosts/manifest +++ b/cdist/conf/type/__hosts/manifest @@ -19,21 +19,24 @@ # along with this program. If not, see . # -set -e -u +set -e hostname=$__object_id -state=$(cat "$__object/parameter/state") -marker="# __hosts/$hostname" +state=$(cat "${__object}/parameter/state") +marker="# __hosts/${hostname}" -if [ "$state" = 'absent' ] +if test "${state}" != 'absent' then - set -- --regex "$marker" -else - ip=$(cat "$__object/parameter/ip") - aliases=$(while read -r a; do printf '\t%s' "$a"; done <"$__object/parameter/alias") + ip=$(cat "${__object}/parameter/ip") + if test -s "${__object}/parameter/alias" + then + aliases=$(while read -r a; do printf '\t%s' "$a"; done <"$__object/parameter/alias") + fi set -- --line "$(printf '%s\t%s%s %s' \ - "$ip" "$hostname" "$aliases" "$marker")" + "${ip}" "${hostname}" "${aliases}" "${marker}")" +else + set -- --regex "$(echo "${marker}" | sed -e 's/\./\\./')$" fi -__line "__hosts/$hostname" --file /etc/hosts --state "$state" "$@" +__line "/etc/hosts:${hostname}" --file /etc/hosts --state "${state}" "$@" diff --git a/cdist/conf/type/__pf_apply/explorer/rcvar b/cdist/conf/type/__hwclock/explorer/adjtime_mode similarity index 64% rename from cdist/conf/type/__pf_apply/explorer/rcvar rename to cdist/conf/type/__hwclock/explorer/adjtime_mode index 7c8d535f..2b27bedc 100755 --- a/cdist/conf/type/__pf_apply/explorer/rcvar +++ b/cdist/conf/type/__hwclock/explorer/adjtime_mode @@ -1,6 +1,6 @@ -#!/bin/sh +#!/bin/sh -e # -# 2012 Jake Guffey (jake.guffey at eprotex.com) +# 2020 Dennis Camera (dennis.camera at ssrq-sds-fds.ch) # # This file is part of cdist. # @@ -17,20 +17,12 @@ # You should have received a copy of the GNU General Public License # along with cdist. If not, see . # -# -# Get the location of the pf ruleset on the target host. +# Prints the clock mode read from the /etc/adjtime file, if present. # -# Debug -#exec >&2 -#set -x - -# Check /etc/rc.conf for pf's configuration file name. Default to /etc/pf.conf - -RC="/etc/rc.conf" -PFCONF="$(grep '^pf_rules=' ${RC} | cut -d= -f2 | sed 's/"//g')" -echo "${PFCONF:-"/etc/pf.conf"}" - -# Debug -#set +x +# not all operating systems use an adjfile +test -f /etc/adjtime || exit 0 +# 3rd line is clock mode +# adjtime(5) https://man7.org/linux/man-pages/man5/adjtime.5.html +sed -n 3p /etc/adjtime diff --git a/cdist/conf/type/__hwclock/explorer/timedatectl_localrtc b/cdist/conf/type/__hwclock/explorer/timedatectl_localrtc new file mode 100755 index 00000000..8239122e --- /dev/null +++ b/cdist/conf/type/__hwclock/explorer/timedatectl_localrtc @@ -0,0 +1,27 @@ +#!/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 . +# +# Prints the LocalRTC property using timedatectl on systemd-based systems. +# + +command -v timedatectl >/dev/null 2>&1 || exit 0 + +# NOTE: Older versions of timedatectl do not support `timedatectl show' +timedatectl --no-pager status \ +| awk -F': ' '$1 ~ "RTC in local TZ$" { sub(/[ \t]*$/, "", $2); print $2 }' diff --git a/cdist/conf/type/__hwclock/gencode-remote b/cdist/conf/type/__hwclock/gencode-remote new file mode 100755 index 00000000..5995fb23 --- /dev/null +++ b/cdist/conf/type/__hwclock/gencode-remote @@ -0,0 +1,62 @@ +#!/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 . +# + +mode=$(cat "${__object:?}/parameter/mode") + +timedatectl_localrtc=$(cat "${__object:?}/explorer/timedatectl_localrtc") +adjtime_mode=$(cat "${__object:?}/explorer/adjtime_mode") + + +case ${mode} +in + (localtime) + adjtime_str=LOCAL + local_rtc_str=yes + ;; + (UTC|utc) + adjtime_str=UTC + local_rtc_str=no + ;; + (*) + printf 'Invalid value for --mode: %s\n' "${mode}" >&2 + printf 'Acceptable values are: localtime, utc.\n' >&2 + exit 1 +esac + + +if test -n "${timedatectl_localrtc}" +then + # systemd + timedatectl_should=${local_rtc_str} + if test "${timedatectl_localrtc}" != "${timedatectl_should}" + then + printf 'timedatectl set-local-rtc %s\n' "${timedatectl_should}" + fi +elif test -n "${adjtime_mode}" +then + # others (update /etc/adjtime if present) + if test "${adjtime_mode}" != "${adjtime_str}" + then + # Update /etc/adjtime (3rd line is clock mode) + # adjtime(5) https://man7.org/linux/man-pages/man5/adjtime.5.html + # FIXME: Should maybe add third line if adjfile only contains two lines + printf "sed -i '3c\\\\\\n%s\\n' /etc/adjtime\\n" "${adjtime_str}" + fi +fi diff --git a/cdist/conf/type/__hwclock/man.rst b/cdist/conf/type/__hwclock/man.rst new file mode 100644 index 00000000..65eb648f --- /dev/null +++ b/cdist/conf/type/__hwclock/man.rst @@ -0,0 +1,63 @@ +cdist-type__hwclock(7) +====================== + +NAME +---- +cdist-type__hwclock - Manage the hardware real time clock. + + +DESCRIPTION +----------- +This type can be used to control how the hardware clock is used by the operating +system. + + +REQUIRED PARAMETERS +------------------- +mode + What mode the hardware clock is in. + + Acceptable values: + + localtime + The hardware clock is set to local time (common for systems also running + Windows.) + UTC + The hardware clock is set to UTC (common on UNIX systems.) + + +OPTIONAL PARAMETERS +------------------- +None. + + +BOOLEAN PARAMETERS +------------------ +None. + + +EXAMPLES +-------- + +.. code-block:: sh + + # Make the operating system treat the time read from the hwclock as UTC. + __hwclock --mode UTC + + +SEE ALSO +-------- +:strong:`hwclock`\ (8) + + +AUTHORS +------- +Dennis Camera + + +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. diff --git a/cdist/conf/type/__hwclock/manifest b/cdist/conf/type/__hwclock/manifest new file mode 100755 index 00000000..7d9ab88f --- /dev/null +++ b/cdist/conf/type/__hwclock/manifest @@ -0,0 +1,222 @@ +#!/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 . +# + +# TODO: Consider supporting BADYEAR + +os=$(cat "${__global:?}/explorer/os") +mode=$(cat "${__object:?}/parameter/mode") + +has_systemd_timedatectl=$(test -s "${__object:?}/explorer/timedatectl_localrtc" && echo true || echo false) + + +case ${mode} +in + (localtime) + local_clock=true + ;; + (UTC|utc) + local_clock=false + ;; + (*) + printf 'Invalid value for --mode: %s\n' "${mode}" >&2 + printf 'Acceptable values are: UTC, localtime.\n' >&2 + exit 1 +esac + + +case ${os} +in + (alpine|gentoo) + if ! $has_systemd_timedatectl + then + # NOTE: Gentoo also supports systemd, in which case /etc/conf.d is + # not used. So we check for systemd presence here and only + # update /etc/conf.d if systemd is not installed. + # https://wiki.gentoo.org/wiki/System_time#Hardware_clock + + export CDIST_ORDER_DEPENDENCY=true + __file /etc/conf.d/hwclock --state present \ + --owner root --group root --mode 0644 + __key_value /etc/conf.d/hwclock:clock \ + --file /etc/conf.d/hwclock \ + --key clock \ + --delimiter '=' --exact_delimiter \ + --value "\"$($local_clock && echo local || echo UTC)\"" + unset CDIST_ORDER_DEPENDENCY + fi + ;; + (centos|fedora|redhat|scientific) + os_version=$(cat "${__global:?}/explorer/os_version") + os_major=$(expr "${os_version}" : '.* release \([0-9]*\)') + case ${os} + in + (centos|scientific) + update_sysconfig=$(test "${os_major}" -lt 6 && echo true || echo false) + ;; + (fedora) + update_sysconfig=$(test "${os_major}" -lt 10 && echo true || echo false) + ;; + (redhat|*) + case ${os_version} + in + ('Red Hat Enterprise Linux'*) + update_sysconfig=$(test "${os_major}" -lt 6 && echo true || echo false) + ;; + ('Red Hat Linux'*) + update_sysconfig=true + ;; + (*) + printf 'Could not determine Red Hat distribution.\n' >&2 + printf "Please contribute an implementation for it if you can.\n" >&2 + exit 1 + ;; + esac + ;; + esac + + if ${update_sysconfig:?} + then + export CDIST_ORDER_DEPENDENCY=true + __file /etc/sysconfig/clock --state present \ + --owner root --group root --mode 0644 + __key_value /etc/sysconfig/clock:UTC \ + --file /etc/sysconfig/clock \ + --key UTC \ + --delimiter '=' --exact_delimiter \ + --value "$($local_clock && echo false || echo true)" + unset CDIST_ORDER_DEPENDENCY + fi + ;; + (debian|devuan|ubuntu) + os_major=$(sed 's/[^0-9].*$//' "${__global:?}/explorer/os_version") + + case ${os} + in + (debian) + if test "${os_major}" -ge 7 + then + update_rcS=false + elif test "${os_major}" -ge 3 + then + update_rcS=true + else + # Debian 2.2 should be supportable using rcS. + # Debian 2.1 uses the ancient GMT key. + # Debian 1.3 does not have rcS. + printf "Your operating system (Debian %s) is currently not supported by this type (%s)\n" \ + "$(cat "${__global:?}/explorer/os_version")" "${__type##*/}" >&2 + printf "Please contribute an implementation for it if you can.\n" >&2 + exit 1 + fi + ;; + (devuan) + update_rcS=false + ;; + (ubuntu) + update_rcS=$(test "${os_major}" -lt 16 && echo true || echo false) + ;; + esac + + if ${update_rcS} + then + export CDIST_ORDER_DEPENDENCY=true + __file /etc/default/rcS --state present \ + --owner root --group root --mode 0644 + __key_value /etc/default/rcS:UTC \ + --file /etc/default/rcS \ + --key UTC \ + --delimiter '=' --exact_delimiter \ + --value "$($local_clock && echo no || echo yes)" + unset CDIST_ORDER_DEPENDENCY + fi + ;; + (freebsd) + # cf. adjkerntz(8) + __file /etc/wall_cmos_clock \ + --state "$($local_clock && echo present || echo absent)" \ + --owner root --group wheel --mode 0444 + ;; + (netbsd) + # https://wiki.netbsd.org/guide/boot/#index9h2 + __key_value /etc/rc.conf:rtclocaltime \ + --file /etc/rc.conf \ + --key rtclocaltime \ + --delimiter '=' --exact_delimiter \ + --value "$($local_clock && echo YES || echo NO)" + ;; + (slackware) + __file /etc/hardwareclock --owner root --group root --mode 0644 \ + --source - <<-EOF + # /etc/hardwareclock + # + # Tells how the hardware clock time is stored. + # This file is managed by cdist. + + $($local_clock && echo localtime || echo UTC) + EOF + ;; + (suse) + if test -s "${__global:?}/explorer/os_release" + then + # shellcheck source=/dev/null + os_version=$(. "${__global:?}/explorer/os_release" && echo "${VERSION}") + else + os_version=$(sed -n 's/^VERSION\ *=\ *//p' "${__global:?}/explorer/os_version") + fi + os_major=$(expr "${os_version}" : '\([0-9]\{1,\}\)') + + # TODO: Consider using `yast2 timezone set hwclock' instead + if expr "${os_major}" \< 12 + then + # Starting with SuSE 12 (first systemd-based version) + # /etc/sysconfig/clock does not contain the HWCLOCK line + # anymore. + # With SuSE 13, it has been reduced to TIMEZONE configuration. + __key_value /etc/sysconfig/clock:HWCLOCK \ + --file /etc/sysconfig/clock \ + --delimiter '=' --exact_delimiter \ + --key HWCLOCK \ + --value "$($local_clock && echo '"--localtime"' || echo '"-u"')" + fi + ;; + (void) + export CDIST_ORDER_DEPENDENCY=true + __file /etc/rc.conf \ + --owner root --group root --mode 0644 \ + --state present + __key_value /etc/rc.conf:HARDWARECLOCK \ + --file /etc/rc.conf \ + --delimiter '=' --exact_delimiter \ + --key HARDWARECLOCK \ + --value "\"$($local_clock && echo localtime || echo UTC)\"" + unset CDIST_ORDER_DEPENDENCY + ;; + (*) + if ! $has_systemd_timedatectl + then + 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 + fi + ;; +esac + +# NOTE: timedatectl set-local-rtc for systemd is in gencode-remote +# NOTE: /etc/adjtime is also updated in gencode-remote diff --git a/cdist/conf/type/__hwclock/parameter/required b/cdist/conf/type/__hwclock/parameter/required new file mode 100644 index 00000000..17ab372f --- /dev/null +++ b/cdist/conf/type/__hwclock/parameter/required @@ -0,0 +1 @@ +mode diff --git a/cdist/conf/type/__hwclock/singleton b/cdist/conf/type/__hwclock/singleton new file mode 100644 index 00000000..e69de29b diff --git a/cdist/conf/type/__iptables_apply/files/init-script b/cdist/conf/type/__iptables_apply/files/init-script index d9c79ef7..e42017ae 100644 --- a/cdist/conf/type/__iptables_apply/files/init-script +++ b/cdist/conf/type/__iptables_apply/files/init-script @@ -1,7 +1,4 @@ #!/bin/sh -# Nico Schottelius -# Zürisee, Mon Sep 2 18:38:27 CEST 2013 -# ### BEGIN INIT INFO # Provides: iptables # Required-Start: $local_fs $remote_fs @@ -14,34 +11,72 @@ # and saves/restores previous status ### END INIT INFO +# Originally written by: +# Nico Schottelius +# Zürisee, Mon Sep 2 18:38:27 CEST 2013 +# +# 2013 Nico Schottelius (nico-cdist at schottelius.org) +# 2020 Matthias Stecher (matthiasstecher at gmx.de) +# +# This file is distributed with cdist and licenced under the +# GNU GPLv3+ WITHOUT ANY WARRANTY. + + +# Read files and execute the content with the given commands +# +# Arguments: +# 1: Directory +# 2..n: Commands which should be used to execute the file content +gothrough() { + cd "$1" || return + shift + + # iterate through all rules and continue if it's not a file + for rule in *; do + [ -f "$rule" ] || continue + echo "Appling iptables rule $rule ..." + + # execute it with all commands specificed + ruleparam="$(cat "$rule")" + for cmd in "$@"; do + # Command and Rule should be split. + # shellcheck disable=SC2046 + command $cmd $ruleparam + done + done +} + +# Shortcut for iptables command to do IPv4 and v6 +# only applies to the "reset" target +iptables() { + command iptables "$@" + command ip6tables "$@" +} basedir=/etc/iptables.d -status="${basedir}/.pre-start" +status4="${basedir}/.pre-start" +status6="${basedir}/.pre-start6" case $1 in start) # Save status - iptables-save > "$status" + iptables-save > "$status4" + ip6tables-save > "$status6" # Apply our ruleset - cd "$basedir" || exit - count="$(find . ! -name . -prune | wc -l)" - - # Only do something if there are rules - if [ "$count" -ge 1 ]; then - for rule in *; do - echo "Applying iptables rule $rule ..." - # Rule should be split. - # shellcheck disable=SC2046 - iptables $(cat "$rule") - done - fi + gothrough "$basedir" iptables + #gothrough "$basedir/v4" iptables # conflicts with $basedir + gothrough "$basedir/v6" ip6tables + gothrough "$basedir/all" iptables ip6tables ;; stop) # Restore from status before, if there is something to restore - if [ -f "$status" ]; then - iptables-restore < "$status" + if [ -f "$status4" ]; then + iptables-restore < "$status4" + fi + if [ -f "$status6" ]; then + ip6tables-restore < "$status6" fi ;; restart) diff --git a/cdist/conf/type/__iptables_apply/man.rst b/cdist/conf/type/__iptables_apply/man.rst index 76e1f6bf..3bef92cc 100644 --- a/cdist/conf/type/__iptables_apply/man.rst +++ b/cdist/conf/type/__iptables_apply/man.rst @@ -10,7 +10,24 @@ DESCRIPTION ----------- This cdist type deploys an init script that triggers the configured rules and also re-applies them on -configuration. +configuration. Rules are written from __iptables_rule +into the folder ``/etc/iptables.d/``. + +It reads all rules from the base folder as rules for IPv4. +Rules in the subfolder ``v6/`` are IPv6 rules. Rules in +the subfolder ``all/`` are applied to both rule tables. All +files contain the arguments for a single ``iptables`` and/or +``ip6tables`` command. + +Rules are applied in the following order: +1. All IPv4 rules +2. All IPv6 rules +2. All rules that should be applied to both tables + +The order of the rules that will be applied are definite +from the result the shell glob returns, which should be +alphabetical. If rules must be applied in a special order, +prefix them with a number like ``02-some-rule``. REQUIRED PARAMETERS @@ -24,7 +41,7 @@ None EXAMPLES -------- -None (__iptables_apply is used by __iptables_rule) +None (__iptables_apply is used by __iptables_rule automatically) SEE ALSO @@ -35,11 +52,13 @@ SEE ALSO AUTHORS ------- Nico Schottelius +Matthias Stecher COPYING ------- -Copyright \(C) 2013 Nico Schottelius. 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. +Copyright \(C) 2013 Nico Schottelius. +Copyright \(C) 2020 Matthias Stecher. +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. diff --git a/cdist/conf/type/__iptables_rule/man.rst b/cdist/conf/type/__iptables_rule/man.rst index 92d8859f..afb71e01 100644 --- a/cdist/conf/type/__iptables_rule/man.rst +++ b/cdist/conf/type/__iptables_rule/man.rst @@ -11,6 +11,10 @@ DESCRIPTION This cdist type allows you to manage iptable rules in a distribution independent manner. +See :strong:`cdist-type__iptables_apply`\ (7) for the +execution order of these rules. It will be executed +automaticly to apply all rules non-volaite. + REQUIRED PARAMETERS ------------------- @@ -25,6 +29,24 @@ state 'present' or 'absent', defaults to 'present' +BOOLEAN PARAMETERS +------------------ +All rules without any of these parameters will be treated like ``--v4`` because +of backward compatibility. + +v4 + Explicitly set it as rule for IPv4. If IPv6 is set, too, it will be + threaten like ``--all``. Will be the default if nothing else is set. + +v6 + Explicitly set it as rule for IPv6. If IPv4 is set, too, it will be + threaten like ``--all``. + +all + Set the rule for both IPv4 and IPv6. It will be saved separately from the + other rules. + + EXAMPLES -------- @@ -48,6 +70,16 @@ EXAMPLES --state absent + # IPv4-only rule for ICMPv4 + __iptables_rule icmp-v4 --v4 --rule "-A INPUT -p icmp -j ACCEPT" + # IPv6-only rule for ICMPv6 + __iptables_rule icmp-v6 --v6 --rule "-A INPUT -p icmpv6 -j ACCEPT" + + # doing something for the dual stack + __iptables_rule fwd-eth0-eth1 --v4 --v6 --rule "-A INPUT -i eth0 -o eth1 -j ACCEPT" + __iptables_rule fwd-eth1-eth0 --all --rule "-A -o eth1 -i eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT" + + SEE ALSO -------- :strong:`cdist-type__iptables_apply`\ (7), :strong:`iptables`\ (8) @@ -56,11 +88,13 @@ SEE ALSO AUTHORS ------- Nico Schottelius +Matthias Stecher COPYING ------- -Copyright \(C) 2013 Nico Schottelius. 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. +Copyright \(C) 2013 Nico Schottelius. +Copyright \(C) 2020 Matthias Stecher. +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. diff --git a/cdist/conf/type/__iptables_rule/manifest b/cdist/conf/type/__iptables_rule/manifest index ed78787f..d4394c25 100755 --- a/cdist/conf/type/__iptables_rule/manifest +++ b/cdist/conf/type/__iptables_rule/manifest @@ -1,6 +1,7 @@ #!/bin/sh -e # # 2013 Nico Schottelius (nico-cdist at schottelius.org) +# 2020 Matthias Stecher (matthiasstecher at gmx.de) # # This file is part of cdist. # @@ -24,12 +25,36 @@ base_dir=/etc/iptables.d name="$__object_id" state="$(cat "$__object/parameter/state")" +if [ -f "$__object/parameter/v4" ]; then + only_v4="yes" + # $specific_dir is $base_dir +fi +if [ -f "$__object/parameter/v6" ]; then + only_v6="yes" + specific_dir="$base_dir/v6" +fi +# If rules should be set for both protocols +if { [ "$only_v4" = "yes" ] && [ "$only_v6" = "yes" ]; } || + [ -f "$__object/parameter/all" ]; then + + # all to a specific directory + specific_dir="$base_dir/all" +fi + +# set rule directory based on if it's the base or subdirectory +rule_dir="${specific_dir:-$base_dir}" + ################################################################################ # Basic setup # __directory "$base_dir" --state present +# sub-directory if required +if [ "$specific_dir" ]; then + require="__directory/$base_dir" __directory "$specific_dir" --state present +fi + # Have apply do the real job require="$__object_name" __iptables_apply @@ -37,6 +62,15 @@ require="$__object_name" __iptables_apply # The rule # -require="__directory/$base_dir" __file "$base_dir/${name}" \ - --source "$__object/parameter/rule" \ - --state "$state" +for dir in "$base_dir" "$base_dir/v6" "$base_dir/all"; do + # defaults to absent except the directory that should contain the file + if [ "$rule_dir" = "$dir" ]; then + curr_state="$state" + else + curr_state="absent" + fi + + require="__directory/$rule_dir" __file "$dir/$name" \ + --source "$__object/parameter/rule" \ + --state "$curr_state" +done diff --git a/cdist/conf/type/__iptables_rule/parameter/boolean b/cdist/conf/type/__iptables_rule/parameter/boolean new file mode 100644 index 00000000..76882272 --- /dev/null +++ b/cdist/conf/type/__iptables_rule/parameter/boolean @@ -0,0 +1,3 @@ +all +v4 +v6 diff --git a/cdist/conf/type/__key_value/explorer/state b/cdist/conf/type/__key_value/explorer/state index 7b2de1df..d24600af 100755 --- a/cdist/conf/type/__key_value/explorer/state +++ b/cdist/conf/type/__key_value/explorer/state @@ -40,7 +40,9 @@ else fi export key state delimiter value exact_delimiter -awk -f - "$file" <<"AWK_EOF" +awk_bin=$(PATH=$(getconf PATH 2>/dev/null) && command -v awk || echo awk) + +"${awk_bin}" -f - "$file" <<"AWK_EOF" BEGIN { state=ENVIRON["state"] key=ENVIRON["key"] diff --git a/cdist/conf/type/__key_value/files/remote_script.sh b/cdist/conf/type/__key_value/files/remote_script.sh index f7a1add5..faf080cb 100644 --- a/cdist/conf/type/__key_value/files/remote_script.sh +++ b/cdist/conf/type/__key_value/files/remote_script.sh @@ -24,7 +24,10 @@ if [ -f "$file" ]; then else touch "$file" fi -awk -f - "$file" >"$tmpfile" <<"AWK_EOF" + +awk_bin=$(PATH=$(getconf PATH 2>/dev/null) && command -v awk || echo awk) + +"${awk_bin}" -f - "$file" >"$tmpfile" <<"AWK_EOF" BEGIN { # import variables in a secure way .. state=ENVIRON["state"] diff --git a/cdist/conf/type/__key_value/gencode-remote b/cdist/conf/type/__key_value/gencode-remote index 13cc27c7..1174400e 100755 --- a/cdist/conf/type/__key_value/gencode-remote +++ b/cdist/conf/type/__key_value/gencode-remote @@ -25,7 +25,7 @@ state_should="$(cat "$__object/parameter/state")" state_is="$(cat "$__object/explorer/state")" fire_onchange='' -if [ "$state_is" = "$state_should" ]; then +if [ "$state_is" = "$state_should" ]; then exit 0 fi diff --git a/cdist/conf/type/__letsencrypt_cert/explorer/certbot-path b/cdist/conf/type/__letsencrypt_cert/explorer/certbot-path deleted file mode 100755 index 3c6076df..00000000 --- a/cdist/conf/type/__letsencrypt_cert/explorer/certbot-path +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -e - -command -v certbot 2>/dev/null || true diff --git a/cdist/conf/type/__letsencrypt_cert/explorer/certificate-data b/cdist/conf/type/__letsencrypt_cert/explorer/certificate-data new file mode 100755 index 00000000..ff62e742 --- /dev/null +++ b/cdist/conf/type/__letsencrypt_cert/explorer/certificate-data @@ -0,0 +1,78 @@ +#!/bin/sh -e +certbot_path="$(command -v certbot 2>/dev/null || true)" +# Defaults +certificate_exists="no" +certificate_is_test="no" + +if [ -n "${certbot_path}" ]; then + # Find python executable that has access to certbot's module + python_path=$(sed -n '1s/^#! *//p' "${certbot_path}") + + # Use a lock for cdist due to certbot not exiting with failure + # or having any flags for concurrent use. + _certbot() { + ${python_path} - 2>/dev/null <> /dev/stderr + exit 1 + ;; + esac + + hook_contents_tail="$(cat < "${existing_domains}" + certificate_is_test="$(_explorer_var certificate_is_test)" sort -uo "${requested_domains}" "${requested_domains}" sort -uo "${existing_domains}" "${existing_domains}" diff --git a/cdist/conf/type/__letsencrypt_cert/man.rst b/cdist/conf/type/__letsencrypt_cert/man.rst index 85eb88ea..43be8424 100644 --- a/cdist/conf/type/__letsencrypt_cert/man.rst +++ b/cdist/conf/type/__letsencrypt_cert/man.rst @@ -1,16 +1,33 @@ cdist-type__letsencrypt_cert(7) =============================== + NAME ---- cdist-type__letsencrypt_cert - Get an SSL certificate from Let's Encrypt + DESCRIPTION ----------- Automatically obtain a Let's Encrypt SSL certificate using Certbot. +This type attempts to setup automatic renewals always. In many Linux +distributions, that is the case out of the box, see: +https://certbot.eff.org/docs/using.html#automated-renewals + +For Alpine Linux and Arch Linux, we setup a system-wide cronjob that +attempts to renew certificates daily. + +If you are using FreeBSD, we configure periodic(8) as recommended by +the port mantainer, so there will be a weekly attempt at renewal. + +If your OS is not mentioned here or on Certbot's docs as having +support for automated renewals, please make sure you check your OS +and possibly patch this type so the system-wide cronjob is installed. + + REQUIRED PARAMETERS ------------------- @@ -21,6 +38,7 @@ object id admin-email Where to send Let's Encrypt emails like "certificate needs renewal". + OPTIONAL PARAMETERS ------------------- @@ -36,25 +54,68 @@ webroot The path to your webroot, as set up in your webserver config. If this parameter is not present, Certbot will be run in standalone mode. + OPTIONAL MULTIPLE PARAMETERS ---------------------------- -renew-hook - Renew hook command directly passed to Certbot in cron job. - domain Domains to be included in the certificate. When specified then object id is not used as a domain. +deploy-hook + Command to be executed only when the certificate associated with this + ``$__object_id`` is issued or renewed. + You can specify it multiple times, but any failure will prevent further + commands from being executed. + + For this command, the + shell variable ``$RENEWED_LINEAGE`` will point to the + config live subdirectory (for example, + ``/etc/letsencrypt/live/${__object_id}``) containing the + new certificates and keys; the shell variable + ``$RENEWED_DOMAINS`` will contain a space-delimited list + of renewed certificate domains (for example, + ``example.com www.example.com``) + +pre-hook + Command to be run in a shell before obtaining any + certificates. + You can specify it multiple times, but any failure will prevent further + commands from being executed. + + Note these run regardless of which certificate is attempted, you may want to + manage these system-wide hooks with ``__file`` in + ``/etc/letsencrypt/renewal-hooks/pre/``. + + Intended primarily for renewal, where it + can be used to temporarily shut down a webserver that + might conflict with the standalone plugin. This will + only be called if a certificate is actually to be + obtained/renewed. + +post-hook + Command to be run in a shell after attempting to + obtain/renew certificates. + You can specify it multiple times, but any failure will prevent further + commands from being executed. + + Note these run regardless of which certificate was attempted, you may want to + manage these system-wide hooks with ``__file`` in + ``/etc/letsencrypt/renewal-hooks/post/``. + + Can be used to deploy + renewed certificates, or to restart any servers that + were stopped by --pre-hook. This is only run if an + attempt was made to obtain/renew a certificate. + + BOOLEAN PARAMETERS ------------------ -automatic-renewal - Install a cron job, which attempts to renew certificates daily. - staging Obtain a test certificate from a staging server. + MESSAGES -------- @@ -67,6 +128,7 @@ create remove Certificate was removed. + EXAMPLES -------- @@ -75,8 +137,7 @@ EXAMPLES # use object id as domain __letsencrypt_cert example.com \ --admin-email root@example.com \ - --automatic-renewal \ - --renew-hook "service nginx reload" \ + --deploy-hook "service nginx reload" \ --webroot /data/letsencrypt/root .. code-block:: sh @@ -85,11 +146,10 @@ EXAMPLES # and example.com needs to be included again with domain parameter __letsencrypt_cert example.com \ --admin-email root@example.com \ - --automatic-renewal \ --domain example.com \ --domain foo.example.com \ --domain bar.example.com \ - --renew-hook "service nginx reload" \ + --deploy-hook "service nginx reload" \ --webroot /data/letsencrypt/root AUTHORS @@ -99,11 +159,13 @@ AUTHORS | Kamila Součková | Darko Poljak | Ľubomír Kučera +| Evilham + COPYING ------- -Copyright \(C) 2017-2018 Nico Schottelius, Kamila Součková, Darko Poljak and +Copyright \(C) 2017-2021 Nico Schottelius, Kamila Součková, Darko Poljak and Ľubomír Kučera. 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. diff --git a/cdist/conf/type/__letsencrypt_cert/manifest b/cdist/conf/type/__letsencrypt_cert/manifest old mode 100755 new mode 100644 index b4464366..39067f3b --- a/cdist/conf/type/__letsencrypt_cert/manifest +++ b/cdist/conf/type/__letsencrypt_cert/manifest @@ -1,18 +1,20 @@ #!/bin/sh -certbot_fullpath="$(cat "${__object:?}/explorer/certbot-path")" +certbot_fullpath="$(grep "^certbot_path:" "${__object:?}/explorer/certificate-data" | cut -d ':' -f 2-)" +state=$(cat "${__object}/parameter/state") +os="$(cat "${__global:?}/explorer/os")" if [ -z "${certbot_fullpath}" ]; then - os="$(cat "${__global:?}/explorer/os")" os_version="$(cat "${__global}/explorer/os_version")" - + # Use this, very common value, as a default. It is OS-dependent + certbot_fullpath="/usr/bin/certbot" case "$os" in - archlinux) - __package certbot - ;; - alpine) - __package certbot - ;; + archlinux) + __package certbot + ;; + alpine) + __package certbot + ;; debian) case "$os_version" in 8*) @@ -39,7 +41,7 @@ if [ -z "${certbot_fullpath}" ]; then require="__apt_source/stretch-backports" __package_apt certbot \ --target-release stretch-backports ;; - 10*) + 10*|11*) __package_apt certbot ;; @@ -48,9 +50,7 @@ if [ -z "${certbot_fullpath}" ]; then exit 1 ;; esac - - certbot_fullpath=/usr/bin/certbot - ;; + ;; devuan) case "$os_version" in jessie) @@ -83,17 +83,14 @@ if [ -z "${certbot_fullpath}" ]; then exit 1 ;; esac - - certbot_fullpath=/usr/bin/certbot ;; freebsd) - __package py27-certbot - - certbot_fullpath=/usr/local/bin/certbot + __package py39-certbot + certbot_fullpath="/usr/local/bin/certbot" ;; ubuntu) - __package certbot - ;; + __package certbot + ;; *) echo "Unsupported os: $os" >&2 exit 1 @@ -101,18 +98,61 @@ if [ -z "${certbot_fullpath}" ]; then esac fi -if [ -f "${__object}/parameter/automatic-renewal" ]; then - renew_hook_param="${__object}/parameter/renew-hook" - renew_hook="" - if [ -f "${renew_hook_param}" ]; then - while read -r hook; do - renew_hook="${renew_hook} --renew-hook \"${hook}\"" - done < "${renew_hook_param}" - fi +# Other OS-dependent values that we want to set every time +LE_DIR="/etc/letsencrypt" +certbot_cronjob_state="absent" +case "$os" in + archlinux|alpine) + certbot_cronjob_state="present" + ;; + freebsd) + LE_DIR="/usr/local/etc/letsencrypt" + # FreeBSD uses periodic(8) instead of crontabs for this + __line "periodic.conf_weekly_certbot" \ + --file "/etc/periodic.conf" \ + --regex "^(#[[:space:]]*)?weekly_certbot_enable=.*" \ + --state "replace" \ + --line 'weekly_certbot_enable="YES"' + ;; + *) + ;; +esac - __cron letsencrypt-certbot \ - --user root \ - --command "${certbot_fullpath} renew -q ${renew_hook}" \ - --hour 0 \ - --minute 47 +# This is only necessary in certain OS +__cron letsencrypt-certbot \ + --user root \ + --command "${certbot_fullpath} renew -q" \ + --hour 0 \ + --minute 47 \ + --state "${certbot_cronjob_state}" + +# Ensure hook directories +HOOKS_DIR="${LE_DIR}/renewal-hooks" +__directory "${LE_DIR}" --mode 0755 +require="__directory/${LE_DIR}" __directory "${HOOKS_DIR}" --mode 0755 + +if [ -f "${__object}/parameter/domain" ]; then + domains="$(sort "${__object}/parameter/domain")" +else + domains="${__object_id}" fi + +# Install hooks as needed +for hook in deploy pre post; do + # Using something unique and specific to this object + hook_file="${HOOKS_DIR}/${hook}/${__object_id}.cdist.sh" + + # This defines hook_contents + # shellcheck source=cdist/conf/type/__letsencrypt_cert/files/gen_hook.sh + . "${__type}/files/gen_hook.sh" + + # Ensure hook directory exists + require="__directory/${HOOKS_DIR}" __directory "${HOOKS_DIR}/${hook}" \ + --mode 0755 + require="__directory/${HOOKS_DIR}/${hook}" __file "${hook_file}" \ + --mode 0555 \ + --source '-' \ + --state "${hook_state}" <> "$__messages_out" remove=1 else diff --git a/cdist/conf/type/__line/man.rst b/cdist/conf/type/__line/man.rst index f76cab64..70490f68 100644 --- a/cdist/conf/type/__line/man.rst +++ b/cdist/conf/type/__line/man.rst @@ -31,7 +31,7 @@ file line 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'. regex @@ -41,10 +41,13 @@ regex If state is 'absent', ensure all lines matching the regular expression 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. state - 'present' or 'absent', defaults to 'present' + 'present', 'absent' or 'replace', defaults to 'present'. onchange 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' \ --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 -------- diff --git a/cdist/conf/type/__locale/deprecated b/cdist/conf/type/__locale/deprecated new file mode 100644 index 00000000..5a06b28e --- /dev/null +++ b/cdist/conf/type/__locale/deprecated @@ -0,0 +1 @@ +This type is deprecated. Please use __localedef instead. diff --git a/cdist/conf/type/__rsync/gencode-remote b/cdist/conf/type/__locale/explorer/state similarity index 57% rename from cdist/conf/type/__rsync/gencode-remote rename to cdist/conf/type/__locale/explorer/state index 074246af..4494fcbc 100755 --- a/cdist/conf/type/__rsync/gencode-remote +++ b/cdist/conf/type/__locale/explorer/state @@ -1,6 +1,7 @@ #!/bin/sh -e +# __locale/explorer/state # -# 2015 Dominique Roux (dominique.roux4 at gmail.com) +# 2020 Matthias Stecher (matthiasstecher at gmx.de) # # This file is part of cdist. # @@ -17,21 +18,19 @@ # You should have received a copy of the GNU General Public License # along with cdist. If not, see . # +# +# Check if the locale is already installed on the system. +# Outputs 'present' or 'absent' depending if the locale exists. +# -if [ -f "$__object/parameter/destination" ]; then - destination=$(cat "$__object/parameter/destination") + +# Get user-defined locale +# locale name is echoed differently than the user propably set it (for UTF-8) +locale="$(echo "$__object_id" | sed 's/UTF-8/utf8/')" + +# Check if the given locale exists on the system +if localedef --list-archive | grep -qFx "$locale"; then + echo present else - destination="/$__object_id" -fi - -ownergroup="" -if [ -f "$__object/parameter/owner" ]; then - ownergroup=$(cat "$__object/parameter/owner") -fi -if [ -f "$__object/parameter/group" ]; then - ownergroup="${ownergroup}:$(cat "$__object/parameter/group")" -fi - -if [ "$ownergroup" ]; then - echo chown -R "$ownergroup" "$destination" + echo absent fi diff --git a/cdist/conf/type/__locale/gencode-remote b/cdist/conf/type/__locale/gencode-remote index 1feb9884..4639cef8 100755 --- a/cdist/conf/type/__locale/gencode-remote +++ b/cdist/conf/type/__locale/gencode-remote @@ -23,6 +23,15 @@ locale="$__object_id" +state_is=$(cat "$__object/explorer/state") +state_should=$(cat "$__object/parameter/state") + +# short circuit if there is nothing to do +if [ "$state_is" = "$state_should" ]; then + exit 0 +fi + + # Hardcoded, create a pull request with # branching on $os in case it is at another location alias=/usr/share/locale/locale.alias @@ -35,8 +44,6 @@ charmap=$(echo "$locale" | cut -d . -f 2) # W-T-F! locale_remove=$(echo "$locale" | sed 's/UTF-8/utf8/') -state=$(cat "$__object/parameter/state") - os=$(cat "$__global/explorer/os") # Nothing to be done on alpine @@ -46,7 +53,7 @@ case "$os" in ;; esac -case "$state" in +case "$state_should" in present) echo localedef -A "$alias" -f "$charmap" -i "$input" "$locale" ;; @@ -54,7 +61,7 @@ case "$state" in echo localedef --delete-from-archive "$locale_remove" ;; *) - echo "Unsupported state: $state" >&2 + echo "Unsupported state: $state_should" >&2 exit 1 ;; esac diff --git a/cdist/conf/type/__locale_system/manifest b/cdist/conf/type/__locale_system/manifest index 4a1fdeed..4b996ebc 100755 --- a/cdist/conf/type/__locale_system/manifest +++ b/cdist/conf/type/__locale_system/manifest @@ -3,6 +3,7 @@ # 2012-2016 Steven Armstrong (steven-cdist at armstrong.cc) # 2016 Carlos Ortigoza (carlos.ortigoza at ungleich.ch) # 2016 Nico Schottelius (nico.schottelius at ungleich.ch) +# 2020 Dennis Camera (dennis.camera at ssrq-sds-fds.ch) # # This file is part of cdist. # @@ -23,17 +24,171 @@ # Configure system-wide locale by modifying i18n file. # +version_ge() { + awk -F '[^0-9.]' -v target="${1:?}" ' + function max(x, y) { return x > y ? x : y } + BEGIN { + getline + nx = split($1, x, ".") + ny = split(target, y, ".") + for (i = 1; i <= max(nx, ny); ++i) { + diff = int(x[i]) - int(y[i]) + if (diff == 0) continue + exit (diff < 0) + } + }' +} + + +key=$__object_id +onchange_cmd= # none, by default +quote_value=false + +catval() { + # shellcheck disable=SC2059 + printf "$($quote_value && echo '"%s"' || echo '%s')" "$(cat "$1")" +} + +state_should=$(cat "${__object}/parameter/state") + os=$(cat "$__global/explorer/os") -case "$os" in - debian|devuan|ubuntu) +case $os +in + debian) + if version_ge 4 <"${__global}/explorer/os_version" + then + # Debian 4 (etch) and later + locale_conf="/etc/default/locale" + else + locale_conf="/etc/environment" + fi + ;; + devuan) locale_conf="/etc/default/locale" ;; + ubuntu) + if version_ge 6.10 <"${__global}/explorer/os_version" + then + # Ubuntu 6.10 (edgy) and later + locale_conf="/etc/default/locale" + else + locale_conf="/etc/environment" + fi + ;; archlinux) locale_conf="/etc/locale.conf" ;; - redhat|centos) - locale_conf="/etc/sysconfig/i18n" + centos|redhat|scientific) + # shellcheck source=/dev/null + version_id=$(. "${__global}/explorer/os_release" && echo "${VERSION_ID:-0}") + if echo "${version_id}" | version_ge 7 + then + locale_conf="/etc/locale.conf" + else + locale_conf="/etc/sysconfig/i18n" + fi + ;; + fedora) + # shellcheck source=/dev/null + version_id=$(. "${__global}/explorer/os_release" && echo "${VERSION_ID:-0}") + if echo "${version_id}" | version_ge 18 + then + locale_conf="/etc/locale.conf" + quote_value=false + else + locale_conf="/etc/sysconfig/i18n" + fi + ;; + gentoo) + case $(cat "${__global}/explorer/init") + in + (*openrc*) + locale_conf="/etc/env.d/02locale" + onchange_cmd="env-update --no-ldconfig" + quote_value=true + ;; + (systemd) + locale_conf="/etc/locale.conf" + ;; + esac + ;; + freebsd|netbsd) + # NetBSD doesn't have a separate configuration file to set locales. + # In FreeBSD locales could be configured via /etc/login.conf but parsing + # that would be annoying, so the shell login file will have to do. + # "Non-POSIX" shells like csh will not be updated here. + + locale_conf="/etc/profile" + quote_value=true + value="$(catval "${__object}/parameter/value"); export ${key}" + ;; + solaris) + locale_conf="/etc/default/init" + locale_conf_group="sys" + + if version_ge 5.11 <"${__global}/explorer/os_version" + then + # mode on Oracle Solaris 11 is actually 0444, + # but the write bit makes sense, IMO + locale_conf_mode=0644 + + # Oracle Solaris 11.2 and later uses SMF to store environment info. + # This is a hack, but I didn't feel like modifying the whole type + # just for some Oracle nonsense. + # 11.3 apparently added nlsadm(1m), but it is missing from 11.2. + # Illumos continues to use /etc/default/init + # NOTE: Remember not to use "cool" POSIX features like -q or -e with + # Solaris grep. + release_regex='Oracle Solaris 11.[2-9][0-9]*' + case $state_should + in + (present) + svccfg_cmd="svccfg -s svc:/system/environment:init setprop environment/${key} = astring: '$(cat "${__object}/parameter/value")'" + ;; + (absent) + svccfg_cmd="svccfg -s svc:/system/environment:init delprop environment/${key}" + ;; + esac + refresh_cmd='svcadm refresh svc:/system/environment' + onchange_cmd="grep '${release_regex}' /etc/release >&- || exit 0; ${svccfg_cmd:-:} && ${refresh_cmd}" + else + locale_conf_mode=0555 + fi + ;; + slackware) + # NOTE: lang.csh (csh config) is ignored here. + locale_conf="/etc/profile.d/lang.sh" + locale_conf_mode=0755 + key="export ${__object_id}" + ;; + suse) + if test -s "${__global}/explorer/os_release" + then + # shellcheck source=/dev/null + os_version=$(. "${__global}/explorer/os_release" && echo "${VERSION}") + else + os_version=$(sed -n 's/^VERSION\ *=\ *//p' "${__global}/explorer/os_version") + fi + os_major=$(expr "${os_version}" : '\([0-9]\{1,\}\)') + + # https://documentation.suse.com/sles/15-SP2/html/SLES-all/cha-suse.html#sec-suse-l10n + if expr "${os_major}" '>=' 15 \& "${os_major}" != 42 + then + # It seems that starting with SuSE 15 the systemd /etc/locale.conf + # is the preferred way to set locales, although + # /etc/sysconfig/language is still available. + # Older documentation doesn't mention /etc/locale.conf, even though + # is it created when localectl is used. + locale_conf="/etc/locale.conf" + else + locale_conf="/etc/sysconfig/language" + quote_value=true + key="RC_${__object_id}" + fi + ;; + voidlinux) + locale_conf="/etc/locale.conf" ;; *) echo "Your operating system ($os) is currently not supported by this type (${__type##*/})." >&2 @@ -42,14 +197,16 @@ case "$os" in ;; esac -__file "$locale_conf" \ - --owner root --group root --mode 644 \ - --state exists +__file "${locale_conf}" --state exists \ + --owner "${locale_conf_owner:-0}" \ + --group "${locale_conf_group:-0}" \ + --mode "${locale_conf_mode:-0644}" -require="__file/$locale_conf" \ - __key_value "$locale_conf:$__object_id" \ - --file "$locale_conf" \ - --key "$__object_id" \ - --delimiter = \ - --state "$(cat "$__object/parameter/state")" \ - --value "$(cat "$__object/parameter/value")" +require="__file/${locale_conf}" \ +__key_value "${locale_conf}:${key#export }" \ + --file "${locale_conf}" \ + --key "${key}" \ + --delimiter '=' --exact_delimiter \ + --state "${state_should}" \ + --value "${value:-$(catval "${__object}/parameter/value")}" \ + --onchange "${onchange_cmd}" diff --git a/cdist/conf/type/__localedef/explorer/state b/cdist/conf/type/__localedef/explorer/state new file mode 100755 index 00000000..3ba57661 --- /dev/null +++ b/cdist/conf/type/__localedef/explorer/state @@ -0,0 +1,100 @@ +#!/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 . +# +# This explorer determines if the locale is defined on the target system. +# Will print nothing on error. +# +# Possible output: +# present: +# the main locale (and possibly aliases) is present +# absent: +# neither the main locale nor any aliases are present +# alias-present: +# the main locale is absent, but at least one of its aliases is present +# + +# Hardcoded, create a pull request in case it is at another location for +# some other distro. (cf. gencode-remote) +aliasfile='/usr/share/locale/locale.alias' + +command -v locale >/dev/null 2>&1 || exit 0 + +locales=$(locale -a) + +parse_locale() { + # This function will split locales into their parts. Locale strings are + # usually of the form: [language[_territory][.codeset][@modifier]] + # For simplicity, language and territory are not separated by this function. + # Old Linux systems were also using "english" or "german" as locale strings. + # Usage: parse_locale locale_str lang_var codeset_var modifier_var + eval "${2:?}"="$(expr "$1" : '\([^.@]*\)')" + eval "${3:?}"="$(expr "$1" : '[^.]*\.\([^@]*\)')" + eval "${4:?}"="$(expr "$1" : '.*@\(.*\)$')" +} + +format_locale() { + # Usage: format_locale language codeset modifier + printf '%s' "$1" + test -z "$2" || printf '.%s' "$2" + test -z "$3" || printf '@%s' "$3" + printf '\n' +} + +gnu_normalize_codeset() { + # reimplementation of glibc/locale/programs/localedef.c normalize_codeset() + echo "$*" | tr '[:upper:]' '[:lower:]' | tr -cd '[:alnum:]' +} + +locale_available() ( + echo "${locales}" | grep -qxF "$1" || { + # glibc uses "normalized" locale names in archives. + # If a locale is stored in an archive, the normalized name will be + # printed by locale, so that needs to be checked, too. + localename=$( + parse_locale "$1" _lang _codeset _modifier \ + && format_locale "${_lang:?}" "$(gnu_normalize_codeset "${_codeset?}")" \ + "${_modifier?}") + echo "${locales}" | grep -qxF "${localename}" + } +) + +if locale_available "${__object_id:?}" +then + echo present +else + # NOTE: locale.alias can be symlinked. + if test -e "${aliasfile}" + then + # Check if one of the aliases of the locale is defined + baselocale=$( + parse_locale "${__object_id:?}" _lang _codeset _modifiers \ + && format_locale "${_lang}" "${_codeset}") + while read -r _alias _localename + do + if test "${_localename}" = "${baselocale}" \ + && echo "${locales}" | grep -qxF "${_alias}" + then + echo alias-present + exit 0 + fi + done <"${aliasfile}" + fi + + echo absent +fi diff --git a/cdist/conf/type/__localedef/files/lib/glibc.sh b/cdist/conf/type/__localedef/files/lib/glibc.sh new file mode 100644 index 00000000..6ace80d4 --- /dev/null +++ b/cdist/conf/type/__localedef/files/lib/glibc.sh @@ -0,0 +1,5 @@ +# -*- mode: sh; indent-tabs-mode: t -*- + +gnu_normalize_codeset() { + echo "$*" | tr -cd '[:alnum:]' | tr '[:upper:]' '[:lower:]' +} diff --git a/cdist/conf/type/__localedef/files/lib/locale.sh b/cdist/conf/type/__localedef/files/lib/locale.sh new file mode 100644 index 00000000..b5e61374 --- /dev/null +++ b/cdist/conf/type/__localedef/files/lib/locale.sh @@ -0,0 +1,20 @@ +# -*- mode: sh; indent-tabs-mode:t -*- + +parse_locale() { + # This function will split locales into their parts. Locale strings are + # usually of the form: [language[_territory][.codeset][@modifier]] + # For simplicity, language and territory are not separated by this function. + # Old Linux systems were also using "english" or "german" as locale strings. + # Usage: parse_locale locale_str lang_var codeset_var modifier_var + eval "${2:?}"="$(expr "$1" : '\([^.@]*\)')" + eval "${3:?}"="$(expr "$1" : '[^.]*\.\([^@]*\)')" + eval "${4:?}"="$(expr "$1" : '.*@\(.*\)$')" +} + +format_locale() { + # Usage: format_locale language codeset modifier + printf '%s' "$1" + test -z "$2" || printf '.%s' "$2" + test -z "$3" || printf '@%s' "$3" + printf '\n' +} diff --git a/cdist/conf/type/__localedef/gencode-remote b/cdist/conf/type/__localedef/gencode-remote new file mode 100755 index 00000000..4538151f --- /dev/null +++ b/cdist/conf/type/__localedef/gencode-remote @@ -0,0 +1,136 @@ +#!/bin/sh -e +# +# 2013-2019 Nico Schottelius (nico-cdist at schottelius.org) +# 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 . +# +# Manage system locales using localedef(1). +# + +# shellcheck source=cdist/conf/type/__localedef/files/lib/locale.sh +. "${__type:?}/files/lib/locale.sh" +# shellcheck source=cdist/conf/type/__localedef/files/lib/glibc.sh +. "${__type:?}/files/lib/glibc.sh" + +state_is=$(cat "${__object:?}/explorer/state") +state_should=$(cat "${__object:?}/parameter/state") + +test "${state_should}" = 'present' -o "${state_should}" = 'absent' || { + printf 'Invalid state: %s\n' "${state_should}" >&2 + exit 1 +} + +# NOTE: If state explorer fails (e.g. locale(1) missing), the following check +# will always fail and let definition/removal run. +if test "${state_is}" = "${state_should}" +then + exit 0 +fi + +locale=${__object_id:?} +os=$(cat "${__global:?}/explorer/os") + +if expr "${locale}" : '.*/' >/dev/null +then + printf 'Paths as locales are not supported.\n' >&2 + printf '__object_id is: %s\n' "${locale}" >&2 + exit 1 +fi + +: "${lang=}" "${codeset=}" "${modifier=}" # declare variables for shellcheck +parse_locale "${locale}" lang codeset modifier + + +case ${os} +in + (alpine|openwrt) + printf '%s does not support locales.\n' "${os}" >&2 + exit 1 + ;; + (archlinux|debian|devuan|ubuntu|suse|centos|fedora|redhat|scientific) + # FIXME: The code below only works for glibc-based installations. + + # NOTE: Hardcoded, create a pull request in case it is at another + # location for some opther distro. + # NOTE: locale.alias can be symlinked (e.g. Debian) + aliasfile='/usr/share/locale/locale.alias' + + case ${state_should} + in + (present) + input=$(format_locale "${lang}" '' "${modifier}") + cat <<-EOF + set -- + if test -e '${aliasfile}' + then + set -- -A '${aliasfile}' + fi + + localedef -i '${input}' -f '${codeset}' "\$@" '${locale}' + EOF + ;; + (absent) + main_localename=$(format_locale "${lang}" "$(gnu_normalize_codeset "${codeset}")" "${modifier}") + + cat <<-EOF + while read -r _alias _localename + do + if test "\${_localename}" = '$(format_locale "${lang}" "${codeset}")' + then + localedef --delete-from-archive "\${_alias}" + fi + done <'${aliasfile}' + EOF + + if test "${state_is}" = present + then + printf "localedef --delete-from-archive '%s'\n" "${main_localename}" + fi + ;; + esac + ;; + (freebsd) + case ${state_should} + in + (present) + if expr "$(grep -oe '^[0-9]*' "${__global:?}/explorer/os_version")" '>=' 11 >/dev/null + then + # localedef(1) is available with FreeBSD >= 11 + printf "localedef -i '%s' -f '%s' '%s'\n" "${input}" "${codeset}" "${locale}" + else + printf 'localedef(1) was added to FreeBSD starting with version 11.\n' >&2 + printf 'Please upgrade your FreeBSD installation to use %s.\n' "${__type##*/}" >&2 + exit 1 + fi + ;; + (absent) + printf "rm -R '/usr/share/locale/%s'\n" "${locale}" + ;; + esac + ;; + (netbsd|openbsd) + # NetBSD/OpenBSD are missing localedef(1). + # We also do not delete defined locales because they can't be recreated. + echo "${os} is lacking localedef(1). Locale management unavailable." >&2 + exit 1 + ;; + (*) + echo "Your operating system (${os}) is currently not supported by this type (${__type##*/})." >&2 + echo "Please contribute an implementation for it if you can." >&2 + exit 1 + ;; +esac diff --git a/cdist/conf/type/__localedef/man.rst b/cdist/conf/type/__localedef/man.rst new file mode 100644 index 00000000..454ce9d1 --- /dev/null +++ b/cdist/conf/type/__localedef/man.rst @@ -0,0 +1,60 @@ +cdist-type__localedef(7) +======================== + +NAME +---- +cdist-type__localedef - Define and remove system locales + + +DESCRIPTION +----------- +This cdist type allows you to define locales on the system using +:strong:`localedef`\ (1) or remove them. +On systems that don't support definition of new locales, the type will raise an +error. + +**NB:** This type respects the glibc ``locale.alias`` file, +i.e. it defines alias locales or deletes aliases of a locale when it is removed. +It is not possible, however, to use alias names to define locales or only remove +certain aliases of a locale. + + +OPTIONAL PARAMETERS +------------------- +state + ``present`` or ``absent``. Defaults to ``present``. + + +EXAMPLES +-------- + +.. code-block:: sh + + # Add locale de_CH.UTF-8 + __localedef de_CH.UTF-8 + + # Same as above, but more explicit + __localedef de_CH.UTF-8 --state present + + # Remove colourful British English + __localedef en_GB.UTF-8 --state absent + + +SEE ALSO +-------- +:strong:`locale`\ (1), +:strong:`localedef`\ (1), +:strong:`cdist-type__locale_system`\ (7) + + +AUTHORS +------- +| Dennis Camera +| Nico Schottelius + + +COPYING +------- +Copyright \(C) 2013-2019 Nico Schottelius, 2020 Dennis Camera. Free use of this +software is granted under the terms of the GNU General Public License version 3 +or later (GPLv3+). diff --git a/cdist/conf/type/__localedef/manifest b/cdist/conf/type/__localedef/manifest new file mode 100755 index 00000000..3ab3ad8c --- /dev/null +++ b/cdist/conf/type/__localedef/manifest @@ -0,0 +1,30 @@ +#!/bin/sh -e +# +# 2013-2019 Nico Schottelius (nico-cdist at schottelius.org) +# 2015 David Hürlimann (david at ungleich.ch) +# 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 . +# +# Install required packages. +# + +case $(cat "${__global:?}/explorer/os") +in + (debian|devuan) + __package_apt locales --state present + ;; +esac diff --git a/cdist/conf/type/__localedef/parameter/default/state b/cdist/conf/type/__localedef/parameter/default/state new file mode 100644 index 00000000..e7f6134f --- /dev/null +++ b/cdist/conf/type/__localedef/parameter/default/state @@ -0,0 +1 @@ +present diff --git a/cdist/conf/type/__localedef/parameter/optional b/cdist/conf/type/__localedef/parameter/optional new file mode 100644 index 00000000..ff72b5c7 --- /dev/null +++ b/cdist/conf/type/__localedef/parameter/optional @@ -0,0 +1 @@ +state diff --git a/cdist/conf/type/__openldap_server/man.rst b/cdist/conf/type/__openldap_server/man.rst index fbad21d8..fa714ec0 100644 --- a/cdist/conf/type/__openldap_server/man.rst +++ b/cdist/conf/type/__openldap_server/man.rst @@ -31,8 +31,8 @@ manager-password-hash Generate e.g. with: `slappasswd -s weneedgoodsecurity`. See `slappasswd(8C)`, `slapd.conf(5)`. TODO: implement this: http://blog.adamsbros.org/2015/06/09/openldap-ssha-salted-hashes-by-hand/ - to derive from the manager-password parameter and ensure idempotency (care with salts). - At that point, manager-password-hash should be deprecated and ignored. + to derive from the manager-password parameter and ensure idempotency (care with salts). + At that point, manager-password-hash should be deprecated and ignored. serverid The server for the directory. @@ -103,8 +103,8 @@ syncrepl-host Set once per host that will replicate the directory. module - LDAP module to load. See `slapd.conf(5)`. - Default value is OS-dependent, see manifest. + LDAP module to load. See `slapd.conf(5)`. Some dependencies might have to + be installed beforehand. Default value is OS-dependent, see manifest. schema Name of LDAP schema to load. Must be the name without extension of a diff --git a/cdist/conf/type/__openldap_server/manifest b/cdist/conf/type/__openldap_server/manifest index 84ba176f..2aeece26 100644 --- a/cdist/conf/type/__openldap_server/manifest +++ b/cdist/conf/type/__openldap_server/manifest @@ -25,6 +25,7 @@ case "${os}" in SLAPD_DATA_DIR="/var/db/openldap-data" SLAPD_RUN_DIR="/var/run/openldap" SLAPD_MODULE_PATH="/usr/local/libexec/openldap" + SLAPD_MODULE_TYPE="la" if [ -z "${slapd_modules}" ]; then # It looks like ppolicy and syncprov must be compiled slapd_modules="back_mdb back_monitor" @@ -43,13 +44,34 @@ case "${os}" in SLAPD_DATA_DIR="/var/lib/ldap" SLAPD_RUN_DIR="/var/run/slapd" SLAPD_MODULE_PATH="/usr/lib/ldap" + SLAPD_MODULE_TYPE="la" if [ -z "${slapd_modules}" ]; then slapd_modules="back_mdb ppolicy syncprov back_monitor" fi + CONF_OWNER="openldap" + CONF_GROUP="openldap" if [ -z "${tls_cipher_suite}" ]; then tls_cipher_suite="NORMAL" fi ;; + alpine) + PKGS="openldap openldap-clients" + ETC="/etc" + SLAPD_DIR="/etc/openldap" + SLAPD_DATA_DIR="/var/lib/openldap" + SLAPD_RUN_DIR="/var/run/openldap" + SLAPD_MODULE_PATH="/usr/lib/openldap" + SLAPD_MODULE_TYPE="so" + if [ -z "${slapd_modules}" ]; then + slapd_modules="back_mdb ppolicy syncprov back_monitor" + PKGS="$PKGS openldap-back-mdb openldap-back-monitor openldap-overlay-all" + fi + CONF_OWNER="ldap" + CONF_GROUP="$SLAPD_USER" + if [ -z "${tls_cipher_suite}" ]; then + tls_cipher_suite="DEFAULT" + fi + ;; *) echo "Don't know the openldap defaults for: $os" >&2 exit 1 @@ -156,6 +178,12 @@ case "${os}" in --line "SLAPD_SERVICES=\"${slapd_urls}\"" \ --state present ;; + alpine) + require="__package/${PKG_MAIN}" __line add_slapd_services \ + --file ${ETC}/conf.d/slapd \ + --line "command_args=\"-h '${slapd_urls}'\"" \ + --state present + ;; *) # Nothing to do here, move on. ;; @@ -170,20 +198,22 @@ if [ -z "${_skip_letsencrypt_cert}" ]; then fi # shellcheck disable=SC2086 - __letsencrypt_cert "${name}" --admin-email "${admin_email}" \ - --renew-hook "cp ${ETC}/letsencrypt/live/${name}/*.pem ${SLAPD_DIR}/sasl2 && chown -R openldap:openldap ${SLAPD_DIR}/sasl2 && service slapd restart" \ - --automatic-renewal ${staging} + __directory ${SLAPD_DIR}/sasl2 + require="__directory/${SLAPD_DIR}/sasl2" __letsencrypt_cert "${name}" \ + --admin-email "${admin_email}" \ + --renew-hook "cp ${ETC}/letsencrypt/live/${name}/*.pem ${SLAPD_DIR}/sasl2 && chown -R ${CONF_OWNER}:${CONF_GROUP} ${SLAPD_DIR}/sasl2 && service slapd restart" \ + --automatic-renewal "${staging}" fi require="__package/${PKG_MAIN}" __directory ${SLAPD_DIR}/slapd.d --state absent if [ -z "${_skip_letsencrypt_cert}" ]; then require="__package/${PKG_MAIN} __letsencrypt_cert/${name}" \ - __file ${SLAPD_DIR}/slapd.conf --owner ${CONF_OWNER} --group ${CONF_GROUP} --mode 644 \ + __file "${SLAPD_DIR}/slapd.conf" --owner "${CONF_OWNER}" --group "${CONF_GROUP}" --mode 644 \ --source "${ldapconf}" else require="__package/${PKG_MAIN}" \ - __file ${SLAPD_DIR}/slapd.conf --owner ${CONF_OWNER} --group ${CONF_GROUP} --mode 644 \ + __file "${SLAPD_DIR}/slapd.conf" --owner "${CONF_OWNER}" --group "${CONF_GROUP}" --mode 644 \ --source "${ldapconf}" fi @@ -210,7 +240,7 @@ done # Add specified modules echo "modulepath ${SLAPD_MODULE_PATH}" >> "${ldapconf}" for module in ${slapd_modules}; do - echo "moduleload ${module}.la" >> "${ldapconf}" + echo "moduleload ${module}.${SLAPD_MODULE_TYPE}" >> "${ldapconf}" done # Rest of the config diff --git a/cdist/conf/type/__package_apt/gencode-remote b/cdist/conf/type/__package_apt/gencode-remote index e02564a2..79c0d9d3 100755 --- a/cdist/conf/type/__package_apt/gencode-remote +++ b/cdist/conf/type/__package_apt/gencode-remote @@ -42,6 +42,13 @@ else target_release="" fi +if [ -f "$__object/parameter/install-recommends" ]; then + # required if __apt_norecommends is used + recommendsparam="-o APT::Install-Recommends=1" +else + recommendsparam="-o APT::Install-Recommends=0" +fi + if [ -f "$__object/parameter/purge-if-absent" ]; then purgeparam="--purge" else @@ -62,30 +69,42 @@ case "$state_is" in ;; esac -# Hint if we need to avoid questions at some point: -# DEBIAN_PRIORITY=critical can reduce the number of questions -aptget="DEBIAN_FRONTEND=noninteractive apt-get --quiet --yes --no-install-recommends -o Dpkg::Options::=\"--force-confdef\" -o Dpkg::Options::=\"--force-confold\"" - if [ "$state_is" = "$state_should" ]; then if [ -z "$version" ] || [ "$version" = "$version_is" ]; then exit 0; fi fi +# Hint if we need to avoid questions at some point: +# DEBIAN_PRIORITY=critical can reduce the number of questions +aptget="DEBIAN_FRONTEND=noninteractive apt-get --quiet --yes -o Dpkg::Options::=\"--force-confdef\" -o Dpkg::Options::=\"--force-confold\"" + case "$state_should" in present) + # There are special arguments to apt(8) to prevent aborts if apt woudn't been + # updated after the 19th April 2021 till the bullseye release. The additional + # arguments acknoledge the happend suite change (the apt(8) update does the + # same by itself). + # + # Using '-o $config' instead of the --allow-releaseinfo-change-* parameter + # allows backward compatablility to pre-buster Debian versions. + # + # See more: ticket #861 + # https://code.ungleich.ch/ungleich-public/cdist/-/issues/861 + apt_opts="-o Acquire::AllowReleaseInfoChange::Suite=true -o Acquire::AllowReleaseInfoChange::Version=true" + # following is bit ugly, but important hack. # due to how cdist config run works, there isn't # currently better way to do it :( cat << EOF if [ ! -f /var/cache/apt/pkgcache.bin ] || [ "\$( stat --format %Y /var/cache/apt/pkgcache.bin )" -lt "\$( date +%s -d '-1 day' )" ] -then echo apt-get update > /dev/null 2>&1 || true +then echo apt-get $apt_opts update > /dev/null 2>&1 || true fi EOF if [ -n "$version" ]; then name="${name}=${version}" fi - echo "$aptget install $target_release '$name'" + echo "$aptget $recommendsparam install $target_release '$name'" echo "installed" >> "$__messages_out" ;; absent) diff --git a/cdist/conf/type/__package_apt/man.rst b/cdist/conf/type/__package_apt/man.rst index a1691eac..4e6101a5 100644 --- a/cdist/conf/type/__package_apt/man.rst +++ b/cdist/conf/type/__package_apt/man.rst @@ -9,7 +9,9 @@ cdist-type__package_apt - Manage packages with apt-get DESCRIPTION ----------- apt-get is usually used on Debian and variants (like Ubuntu) to -manage packages. +manage packages. The package will be installed without recommended +or suggested packages. If such packages are required, install them +separatly or use the parameter ``--install-recommends``. This type will also update package index, if it is older than one day, to avoid missing package error messages. @@ -23,7 +25,7 @@ None OPTIONAL PARAMETERS ------------------- name - If supplied, use the name and not the object id as the package name. + If supplied, use the name and not the object id as the package name. state Either "present" or "absent", defaults to "present" @@ -39,6 +41,15 @@ version BOOLEAN PARAMETERS ------------------ +install-recommends + If the package will be installed, it also installs recommended packages + with it. It will not install recommended packages if the original package + is already installed. + + In most cases, it is recommended to install recommended packages separatly + to control which additional packages will be installed to avoid useless + installed packages. + purge-if-absent If this parameter is given when state is `absent`, the package is purged from the system (using `--purge`). diff --git a/cdist/conf/type/__package_apt/parameter/boolean b/cdist/conf/type/__package_apt/parameter/boolean index f9a0f6b0..a2e433f3 100644 --- a/cdist/conf/type/__package_apt/parameter/boolean +++ b/cdist/conf/type/__package_apt/parameter/boolean @@ -1 +1,2 @@ +install-recommends purge-if-absent diff --git a/cdist/conf/type/__package_luarocks/manifest b/cdist/conf/type/__package_luarocks/manifest index 7d8262ca..9e4499b2 100755 --- a/cdist/conf/type/__package_luarocks/manifest +++ b/cdist/conf/type/__package_luarocks/manifest @@ -19,5 +19,5 @@ # along with cdist. If not, see . # -__package luarocks --state installed -__package make --state installed +__package luarocks --state present +__package make --state present diff --git a/cdist/conf/type/__package_pip/explorer/distinfo-dir b/cdist/conf/type/__package_pip/explorer/distinfo-dir new file mode 100755 index 00000000..18e169ae --- /dev/null +++ b/cdist/conf/type/__package_pip/explorer/distinfo-dir @@ -0,0 +1,45 @@ +#!/bin/sh +# +# 2021 Matthias Stecher (matthiasstecher at gmx.de) +# +# 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 . +# + + +nameparam="$__object/parameter/name" +if [ -f "$nameparam" ]; then + name=$(cat "$nameparam") +else + name="$__object_id" +fi + +pipparam="$__object/parameter/pip" +if [ -f "$pipparam" ]; then + pip=$(cat "$pipparam") +else + pip="$( "$__type_explorer/pip" )" +fi + + +if command -v "$pip" >/dev/null 2>&1; then + # assemble the path where pip stores all pip package info + "$pip" show "$name" \ + | awk -F': ' ' + $1 == "Name" {name=$2; gsub(/-/,"_",name); next} + $1 == "Version" {version=$2; next} + $1 == "Location" {location=$2; next} + END {if (version != "") printf "%s/%s-%s.dist-info", location, name, version}' +fi diff --git a/cdist/conf/type/__package_pip/explorer/extras b/cdist/conf/type/__package_pip/explorer/extras new file mode 100755 index 00000000..bbdc17ab --- /dev/null +++ b/cdist/conf/type/__package_pip/explorer/extras @@ -0,0 +1,66 @@ +#!/bin/sh +# +# 2021 Matthias Stecher (matthiasstecher at gmx.de) +# +# 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 . +# +# +# Checks if the given extras are really installed or not. It will be +# done by querring all dependencies for that extra and return it as +# "to be installed" if no dependency was found. +# + + +distinfo_dir="$("$__type_explorer/distinfo-dir")" + +# check if we have something to check +if [ "$distinfo_dir" ] && [ -s "$__object/parameter/extra" ] +then + # save cause freezing is slow + mkdir "$__object/files" + pip_freeze="$__object/files/pip-freeze.tmp" + pip3 freeze > "$pip_freeze" + + # If all is set, it searches all available extras to separatly check them. + # It would work with just 'all' (cause dependencies are specified for + # 'all'), but will not update if one extra is already present. Side effect + # is that it will not use [all] but instead name all extras seperatly. + for extra in $(if grep -qFx all "$__object/parameter/extra"; + then awk -F': ' '$1 == "Provides-Extra" && $2 != "all"{print $2}' "$distinfo_dir/METADATA"; + else tr ',' '\n' < "$__object/parameter/extra"; + fi) + do + # create a grep BRE pattern to search all packages + # maybe a file full of patterns for -F could be written + grep_pattern="$( + awk -F'(: | ; )' -v check="$extra" ' + $1 == "Requires-Dist" { + split($2, r, " "); + sub("extra == ", "", $3); gsub("'"'"'", "", $3); + if($3 == check) print r[1] + }' "$distinfo_dir/METADATA" \ + | sed ':a; $!N; s/\n/\\|/; ta' + )" + + # echo the extra if no packages where found for it + # if there is no pattern, we don't need to search ;-) + # pip matches packages case-insensetive, we need to do that, too + if [ "$grep_pattern" ] && ! grep -qi "$grep_pattern" "$pip_freeze" + then + echo "$extra" + fi + done +fi diff --git a/cdist/conf/type/__package_pip/explorer/pip b/cdist/conf/type/__package_pip/explorer/pip new file mode 100755 index 00000000..cf9fae89 --- /dev/null +++ b/cdist/conf/type/__package_pip/explorer/pip @@ -0,0 +1,10 @@ +#!/bin/sh -e + +for bin in pip3 pip +do + if check="$( command -v "$bin" )" + then + echo "$check" + break + fi +done diff --git a/cdist/conf/type/__package_pip/explorer/state b/cdist/conf/type/__package_pip/explorer/state old mode 100644 new mode 100755 index 5be07280..3cc98ab9 --- a/cdist/conf/type/__package_pip/explorer/state +++ b/cdist/conf/type/__package_pip/explorer/state @@ -32,7 +32,7 @@ pipparam="$__object/parameter/pip" if [ -f "$pipparam" ]; then pip=$(cat "$pipparam") else - pip="pip" + pip="$( "$__type_explorer/pip" )" fi # If there is no pip, it may get created from somebody else. diff --git a/cdist/conf/type/__package_pip/gencode-remote b/cdist/conf/type/__package_pip/gencode-remote index dcc4fdf9..9abe28bf 100755 --- a/cdist/conf/type/__package_pip/gencode-remote +++ b/cdist/conf/type/__package_pip/gencode-remote @@ -2,6 +2,7 @@ # # 2012 Nico Schottelius (nico-cdist at schottelius.org) # 2016 Darko Poljak (darko.poljak at gmail.com) +# 2021 Matthias Stecher (matthiasstecher at gmx.de) # # This file is part of cdist. # @@ -25,7 +26,10 @@ state_is=$(cat "$__object/explorer/state") state_should="$(cat "$__object/parameter/state")" -[ "$state_is" = "$state_should" ] && exit 0 +# short circuit if state is the same and no extras to install +[ "$state_is" = "$state_should" ] && ! [ -s "$__object/explorer/extras" ] \ + && exit 0 + nameparam="$__object/parameter/name" if [ -f "$nameparam" ]; then @@ -38,7 +42,12 @@ pipparam="$__object/parameter/pip" if [ -f "$pipparam" ]; then pip=$(cat "$pipparam") else - pip="pip" + pip="$( cat "$__object/explorer/pip" )" + if [ -z "$pip" ] + then + echo 'pip not found in path' >&2 + exit 1 + fi fi runasparam="$__object/parameter/runas" @@ -51,11 +60,19 @@ fi case "$state_should" in present) + if [ -s "$__object/explorer/extras" ] + then + # all extras are passed to pip in a comma-separated list in the name + # sed loops through all input lines and add commas between them + extras="$(sed ':a; $!N; s/\n/,/; ta' "$__object/explorer/extras")" + name="${name}[${extras}]" + fi + if [ "$runas" ] then echo "su -c '$pip install -q $name' $runas" else - echo $pip install -q "$name" + echo "$pip" install -q "$name" fi echo "installed" >> "$__messages_out" ;; @@ -64,7 +81,7 @@ case "$state_should" in then echo "su -c '$pip uninstall -q -y $name' $runas" else - echo $pip uninstall -q -y "$name" + echo "$pip" uninstall -q -y "$name" fi echo "removed" >> "$__messages_out" ;; diff --git a/cdist/conf/type/__package_pip/man.rst b/cdist/conf/type/__package_pip/man.rst index 234ceee2..5a2bc673 100644 --- a/cdist/conf/type/__package_pip/man.rst +++ b/cdist/conf/type/__package_pip/man.rst @@ -22,6 +22,16 @@ OPTIONAL PARAMETERS name If supplied, use the name and not the object id as the package name. +extra + Extra optional dependencies which should be installed along the selected + package. Can be specified multiple times. Multiple extras can be passed + in one `--extra` as a comma-separated list. + + Extra optional dependencies will be installed even when the base package + is already installed. Notice that the type will not remove installed extras + that are not explicitly named for the type because pip does not offer a + management for orphaned packages and they may be used by other packages. + pip Instead of using pip from PATH, use the specific pip path. @@ -46,6 +56,14 @@ EXAMPLES # Use pip in a virtualenv located at /foo/shinken_virtualenv as user foo __package_pip pyro --state present --pip /foo/shinken_virtualenv/bin/pip --runas foo + # Install package with optional dependencies + __package_pip mautrix-telegram --extra speedups --extra webp_convert --extra hq_thumbnails + # the extras can also be specified comma-separated + __package_pip mautrix-telegram --extra speedups,webp_convert,hq_thumbnails --extra postgres + + # or take all extras + __package_pip mautrix-telegram --extra all + SEE ALSO -------- @@ -54,12 +72,13 @@ SEE ALSO AUTHORS ------- -Nico Schottelius +| Nico Schottelius +| Matthias Stecher COPYING ------- -Copyright \(C) 2012 Nico Schottelius. 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. +Copyright \(C) 2012 Nico Schottelius, 2021 Matthias Stecher. 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. diff --git a/cdist/conf/type/__package_pip/parameter/optional_multiple b/cdist/conf/type/__package_pip/parameter/optional_multiple new file mode 100644 index 00000000..0f228715 --- /dev/null +++ b/cdist/conf/type/__package_pip/parameter/optional_multiple @@ -0,0 +1 @@ +extra diff --git a/cdist/conf/type/__package_pkg_freebsd/gencode-remote b/cdist/conf/type/__package_pkg_freebsd/gencode-remote index 3f88f6bc..ca9aa45a 100755 --- a/cdist/conf/type/__package_pkg_freebsd/gencode-remote +++ b/cdist/conf/type/__package_pkg_freebsd/gencode-remote @@ -37,6 +37,7 @@ assert () # If condition false, then echo "Assertion failed: \"$1\"" # shellcheck disable=SC2039 + # shellcheck disable=SC3044 echo "File \"$0\", line $lineno, called by $(caller 0)" exit $E_ASSERT_FAILED fi diff --git a/cdist/conf/type/__package_pkgng_freebsd/explorer/pkg_bootstrapped b/cdist/conf/type/__package_pkgng_freebsd/explorer/pkg_bootstrapped new file mode 100755 index 00000000..429f15d3 --- /dev/null +++ b/cdist/conf/type/__package_pkgng_freebsd/explorer/pkg_bootstrapped @@ -0,0 +1,4 @@ +#!/bin/sh -e +if pkg -N >/dev/null 2>&1; then + echo "YES" +fi diff --git a/cdist/conf/type/__package_pkgng_freebsd/explorer/pkg_version b/cdist/conf/type/__package_pkgng_freebsd/explorer/pkg_version index 92ce0623..1c6ba5e5 100755 --- a/cdist/conf/type/__package_pkgng_freebsd/explorer/pkg_version +++ b/cdist/conf/type/__package_pkgng_freebsd/explorer/pkg_version @@ -18,9 +18,14 @@ # along with cdist. If not, see . # # -# 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 name="$(cat "$__object/parameter/name")" else diff --git a/cdist/conf/type/__package_pkgng_freebsd/gencode-remote b/cdist/conf/type/__package_pkgng_freebsd/gencode-remote index dd36efda..05ba4cb2 100755 --- a/cdist/conf/type/__package_pkgng_freebsd/gencode-remote +++ b/cdist/conf/type/__package_pkgng_freebsd/gencode-remote @@ -43,6 +43,7 @@ fi repo="$(cat "$__object/parameter/repo")" state="$(cat "$__object/parameter/state")" curr_version="$(cat "$__object/explorer/pkg_version")" +pkg_bootstrapped="$(cat "$__object/explorer/pkg_bootstrapped")" add_cmd="pkg install -y" rm_cmd="pkg delete -y" upg_cmd="pkg upgrade -y" @@ -73,6 +74,10 @@ execcmd(){ ;; esac + if [ -z "${pkg_bootstrapped}" ]; then + echo "ASSUME_ALWAYS_YES=yes pkg bootstrap >/dev/null 2>&1" + fi + echo "$_cmd >/dev/null 2>&1" # Silence the output of the command echo "status=\$?" echo "if [ \"\$status\" -ne \"0\" ]; then" diff --git a/cdist/conf/type/__package_update_index/gencode-remote b/cdist/conf/type/__package_update_index/gencode-remote index 803468b5..a10c16d3 100755 --- a/cdist/conf/type/__package_update_index/gencode-remote +++ b/cdist/conf/type/__package_update_index/gencode-remote @@ -41,7 +41,19 @@ fi case "$type" in yum) ;; apt) - echo "apt-get --quiet update" + # There are special arguments to apt(8) to prevent aborts if apt woudn't been + # updated after the 19th April 2021 till the bullseye release. The additional + # arguments acknoledge the happend suite change (the apt(8) update does the + # same by itself). + # + # Using '-o $config' instead of the --allow-releaseinfo-change-* parameter + # allows backward compatablility to pre-buster Debian versions. + # + # See more: ticket #861 + # https://code.ungleich.ch/ungleich-public/cdist/-/issues/861 + apt_opts="-o Acquire::AllowReleaseInfoChange::Suite=true -o Acquire::AllowReleaseInfoChange::Version=true" + + echo "apt-get --quiet $apt_opts update" echo "apt-cache updated (age was: $currage)" >> "$__messages_out" ;; pacman) diff --git a/cdist/conf/type/__package_upgrade_all/gencode-remote b/cdist/conf/type/__package_upgrade_all/gencode-remote index 38aa001e..d332e851 100755 --- a/cdist/conf/type/__package_upgrade_all/gencode-remote +++ b/cdist/conf/type/__package_upgrade_all/gencode-remote @@ -28,6 +28,10 @@ apt_clean="$__object/parameter/apt-clean" apt_dist_upgrade="$__object/parameter/apt-dist-upgrade" +if [ -f "$__object/parameter/apt-with-new-pkgs" ]; then + apt_with_new_pkgs="--with-new-pkgs" +fi + if [ -f "$type" ]; then type="$(cat "$type")" else @@ -54,7 +58,7 @@ case "$type" in apt) if [ -f "$apt_dist_upgrade" ] then echo "$aptget dist-upgrade" - else echo "$aptget upgrade" + else echo "$aptget $apt_with_new_pkgs upgrade" fi if [ -f "$apt_clean" ] diff --git a/cdist/conf/type/__package_upgrade_all/man.rst b/cdist/conf/type/__package_upgrade_all/man.rst index e9e2b8ce..0c116bac 100644 --- a/cdist/conf/type/__package_upgrade_all/man.rst +++ b/cdist/conf/type/__package_upgrade_all/man.rst @@ -33,6 +33,14 @@ BOOLEAN PARAMETERS apt-dist-upgrade Do dist-upgrade instead of upgrade. +apt-with-new-pkg + Allow installing new packages when used in conjunction with + upgrade. This is useful if the update of an installed package + requires new dependencies to be installed. Instead of holding the + package back upgrade will upgrade the package and install the new + dependencies. Note that upgrade with this option will never remove + packages, only allow adding new ones. + apt-clean Clean out the local repository of retrieved package files. diff --git a/cdist/conf/type/__package_upgrade_all/parameter/boolean b/cdist/conf/type/__package_upgrade_all/parameter/boolean index 7a56a34b..cd22eb90 100644 --- a/cdist/conf/type/__package_upgrade_all/parameter/boolean +++ b/cdist/conf/type/__package_upgrade_all/parameter/boolean @@ -1,2 +1,3 @@ apt-clean apt-dist-upgrade +apt-with-new-pkgs diff --git a/cdist/conf/type/__pf_apply/deprecated b/cdist/conf/type/__pf_apply/deprecated deleted file mode 100644 index 36cfed90..00000000 --- a/cdist/conf/type/__pf_apply/deprecated +++ /dev/null @@ -1 +0,0 @@ -Consider moving to __pf_apply_anchor. Get in touch if you need __pf_apply. diff --git a/cdist/conf/type/__pf_apply/gencode-remote b/cdist/conf/type/__pf_apply/gencode-remote deleted file mode 100755 index c8f7a25a..00000000 --- a/cdist/conf/type/__pf_apply/gencode-remote +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/sh -e -# -# 2012 Jake Guffey (jake.guffey at eprotex.com) -# -# 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 . -# -# -# Apply pf(4) ruleset on *BSD -# - -# Debug -#exec >&2 -#set -x - -rcvar=$(cat "$__object/explorer/rcvar") - -cat <&2 - fi -fi -EOF - -# Debug -#set +x - diff --git a/cdist/conf/type/__pf_apply/man.rst b/cdist/conf/type/__pf_apply/man.rst deleted file mode 100644 index eee345e7..00000000 --- a/cdist/conf/type/__pf_apply/man.rst +++ /dev/null @@ -1,55 +0,0 @@ -cdist-type__pf_apply(7) -======================= - -NAME ----- -cdist-type__pf_apply - Apply pf(4) ruleset on \*BSD - - -DESCRIPTION ------------ -This type is used on \*BSD systems to manage the pf firewall's active ruleset. - - -REQUIRED PARAMETERS -------------------- -NONE - - -OPTIONAL PARAMETERS -------------------- -NONE - - -EXAMPLES --------- - -.. code-block:: sh - - # Modify the ruleset on $__target_host: - __pf_ruleset --state present --source /my/pf/ruleset.conf - require="__pf_ruleset" \ - __pf_apply - - # Remove the ruleset on $__target_host (implies disabling pf(4): - __pf_ruleset --state absent - require="__pf_ruleset" \ - __pf_apply - - -SEE ALSO --------- -:strong:`pf`\ (4), :strong:`cdist-type__pf_ruleset`\ (7) - - -AUTHORS -------- -Jake Guffey - - -COPYING -------- -Copyright \(C) 2012 Jake Guffey. 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. diff --git a/cdist/conf/type/__postfix_master/gencode-remote b/cdist/conf/type/__postfix_master/gencode-remote index 7c109a69..73de1088 100755 --- a/cdist/conf/type/__postfix_master/gencode-remote +++ b/cdist/conf/type/__postfix_master/gencode-remote @@ -67,7 +67,7 @@ case "$state_should" in remove_entry fi cat << DONE -cat >> "$config" << ${__type##*/}_DONE +cat >> "$config" << "${__type##*/}_DONE" $(cat "$entry") ${__type##*/}_DONE DONE diff --git a/cdist/conf/type/__postfix_master/parameter/optional b/cdist/conf/type/__postfix_master/parameter/optional index 792b42c5..410482b8 100644 --- a/cdist/conf/type/__postfix_master/parameter/optional +++ b/cdist/conf/type/__postfix_master/parameter/optional @@ -4,6 +4,5 @@ unpriv chroot wakeup maxproc -option comment state diff --git a/cdist/conf/type/__postfix_master/parameter/optional_multiple b/cdist/conf/type/__postfix_master/parameter/optional_multiple new file mode 100644 index 00000000..01925a15 --- /dev/null +++ b/cdist/conf/type/__postfix_master/parameter/optional_multiple @@ -0,0 +1 @@ +option diff --git a/cdist/conf/type/__postgres_conf/explorer/postgres_user b/cdist/conf/type/__postgres_conf/explorer/postgres_user new file mode 100644 index 00000000..c6582dc4 --- /dev/null +++ b/cdist/conf/type/__postgres_conf/explorer/postgres_user @@ -0,0 +1,64 @@ +#!/bin/sh -e +# -*- mode: sh; indent-tabs-mode: t -*- +# +# 2021 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 . +# + +os=$("${__explorer:?}/os") + +case ${os} +in + (alpine) + echo 'postgres' + ;; + (centos|rhel|scientific) + echo 'postgres' + ;; + (debian|devuan|ubuntu) + echo 'postgres' + ;; + (freebsd) + test -x /usr/local/etc/rc.d/postgresql || { + printf 'could not find postgresql rc script./n' >&2 + exit 1 + } + pg_status=$(/usr/local/etc/rc.d/postgresql onestatus) || { + printf 'postgresql daemon is not running.\n' >&2 + exit 1 + } + pg_pid=$(printf '%s\n' "${pg_status}" \ + | sed -n 's/^pg_ctl:.*(PID: *\([0-9]*\))$/\1/p') + + # PostgreSQL < 9.6: pgsql + # PostgreSQL >= 9.6: postgres + ps -o user -p "${pg_pid}" | sed -n '2p' + ;; + (netbsd) + echo 'pgsql' + ;; + (openbsd) + echo '_postgresql' + ;; + (suse) + echo 'postgres' + ;; + (*) + echo "Unsupported OS: ${os}" >&2 + exit 1 + ;; +esac diff --git a/cdist/conf/type/__postgres_conf/explorer/state b/cdist/conf/type/__postgres_conf/explorer/state new file mode 100644 index 00000000..4b7b0a43 --- /dev/null +++ b/cdist/conf/type/__postgres_conf/explorer/state @@ -0,0 +1,223 @@ +#!/bin/sh -e +# -*- mode: sh; indent-tabs-mode: t -*- +# +# 2021 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 . +# + +postgres_user=$("${__type_explorer:?}/postgres_user") +conf_name=${__object_id:?} + +tolower() { printf '%s' "$*" | tr '[:upper:]' '[:lower:]'; } + +tobytes() { + # NOTE: This function treats everything as base 2. + # It is not compatible with SI units. + awk 'BEGIN { FS = "\n" } + /TB$/ { $0 = ($0 * 1024) "GB" } + /GB$/ { $0 = ($0 * 1024) "MB" } + /MB$/ { $0 = ($0 * 1024) "kB" } + /kB$/ { $0 = ($0 * 1024) "B" } + /B?$/ { sub(/ *B?$/, "") } + ($0*1) == $0 # is number + ' <<-EOF + $1 + EOF +} + +tomillisecs() { + awk 'BEGIN { FS = "\n" } + /d$/ { $0 = ($0 * 24) "h" } + /h$/ { $0 = ($0 * 60) "min" } + /min$/ { $0 = ($0 * 60) "s" } + /[^m]s$/ { $0 = ($0 * 1000) "ms" } + /ms$/ { $0 *= 1 } + ($0*1) == $0 # is number + ' <<-EOF + $1 + EOF +} + +tobool() { + # prints either 'on' or 'off' + case $(tolower "$1") + in + (t|true|y|yes|on|1) + echo 'on' ;; + (f|false|n|no|off|0) + echo 'off' ;; + (*) + printf 'Inavlid bool value: %s\n' "$2" >&2 + return 1 + ;; + esac + return 0 +} + +quote() { printf '%s\n' "$*" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/'/"; } +psql_exec() { + su - "${postgres_user}" -c "psql postgres -twAc $(quote "$*")" +} + +psql_conf_source() { + # NOTE: SHOW/SET are case-insentitive, so this command should also be. + psql_exec "SELECT CASE WHEN source = 'default' OR setting = boot_val THEN 'default' ELSE source END FROM pg_settings WHERE lower(name) = lower('$1')" +} +psql_conf_cmp() ( + IFS='|' read -r lower_name vartype setting unit <<-EOF + $(psql_exec "SELECT lower(name), vartype, setting, unit FROM pg_settings WHERE lower(name) = lower('$1')") + EOF + + should_value=$2 + is_value=${setting} + + # The following case contains special cases for special settings. + case ${lower_name} + in + (archive_command) + if test "${setting}" = '(disabled)' + then + # DAFUQ PostgreSQL?! + # PostgreSQL returns (disabled) if the feature is inactive. + # We cannot compare the values unless it is enabled, first. + return 0 + fi + ;; + (archive_mode|backslash_quote|constraint_exclusion|force_parallel_mode|huge_pages|synchronous_commit) + # Although only 'on', 'off' are documented, PostgreSQL accepts all + # the "likely" variants of "on" and "off". + case $(tolower "${should_value}") + in + (on|off|true|false|yes|no|1|0) + should_value=$(tobool "${should_value}") + ;; + esac + ;; + esac + + case ${vartype} + in + (bool) + test -z "${unit}" || { + # please fix the explorer if this error occurs. + printf 'units are not supported for vartype: %s\n' "${vartype}" >&2 + exit 1 + } + + should_value=$(tobool "${should_value}") + + test "${is_value}" = "${should_value}" + ;; + (enum) + test -z "${unit}" || { + # please fix the explorer if this error occurs. + printf 'units are not supported with vartype: %s\n' "${vartype}" >&2 + exit 1 + } + + # NOTE: All enums that are currently defined are lower case, but + # PostgreSQL also accepts upper case spelling. + should_value=$(tolower "$2") + + test "${is_value}" = "${should_value}" + ;; + (integer) + # split multiples from unit, first (e.g. 8kB -> 8, kB) + case ${unit} + in + ([0-9]*) + multiple=${unit%%[!0-9]*} + unit=${unit##*[0-9 ]} + ;; + (*) multiple=1 ;; + esac + + is_value=$((setting * multiple))${unit} + + if expr "${should_value}" : '-\{0,1\}[0-9]*$' >/dev/null + then + # default unit + should_value=$((should_value * multiple))${unit} + fi + + # then, do conversion + # NOTE: these conversions work for integers only! + case ${unit} + in + (B|[kMGT]B) + # bytes + is_bytes=$(tobytes "${is_value}") + should_bytes=$(tobytes "${should_value}") + + test $((is_bytes)) -eq $((should_bytes)) + ;; + (ms|s|min|h|d) + # seconds + is_ms=$(tomillisecs "${is_value}") + should_ms=$(tomillisecs "${should_value}") + + test $((is_ms)) -eq $((should_ms)) + ;; + ('') + # no unit + is_int=${is_value} + should_int=${should_value} + + test $((is_int)) -eq $((should_int)) + ;; + esac + ;; + (real|string) + # NOTE: reals could possibly have units, but currently there none. + + test -z "${unit}" || { + # please fix the explorer if this error occurs. + printf 'units are not supported with vartype: %s\n' "${vartype}" >&2 + exit 1 + } + + test "${is_value}" = "${should_value}" + ;; + esac +) + +psql_exec 'SELECT 1' >/dev/null || { + echo 'Connection to PostgreSQL server failed' >&2 + exit 1 +} + +case $(psql_conf_source "${conf_name}") +in + ('') + printf 'Invalid configuration parameter: %s\n' "${conf_name}" >&2 + exit 1 + ;; + (default) + echo absent + ;; + (*) + if ! test -f "${__object:?}/parameter/value" + then + echo present + elif psql_conf_cmp "${conf_name}" "$(cat "${__object:?}/parameter/value")" + then + echo present + else + echo different + fi + ;; +esac diff --git a/cdist/conf/type/__postgres_conf/gencode-remote b/cdist/conf/type/__postgres_conf/gencode-remote new file mode 100755 index 00000000..27651600 --- /dev/null +++ b/cdist/conf/type/__postgres_conf/gencode-remote @@ -0,0 +1,123 @@ +#!/bin/sh -e +# -*- mode: sh; indent-tabs-mode: t -*- +# +# 2019-2021 Dennis Camera (dennis.camera at ssrq-sds-fds.ch) +# 2020 Beni Ruef (bernhard.ruef 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 . +# + +state_is=$(cat "${__object:?}/explorer/state") +state_should=$(cat "${__object:?}/parameter/state") +postgres_user=$(cat "${__object:?}/explorer/postgres_user") + +conf_name=${__object_id:?} + +if test "${state_is}" = "${state_should}" +then + exit 0 +fi + +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' "${_arg}" | sed -e "s/'/'\\\\''/g")'" + else + set -- "$@" "${_arg}" + fi + done + unset _arg + + # NOTE: Use printf because POSIX echo interprets escape sequences + printf '%s' "$*" +} + + +psql_cmd() { + printf 'su - %s -c %s\n' "$(quote "${postgres_user}")" "$(quote "$(quote psql "$@")")" +} + +case ${state_should} +in + (present) + test -n "${__object:?}/parameter/value" || { + echo 'Missing required parameter --value' >&2 + exit 1 + } + + cat <<-EOF + exec 3< "\${__object:?}/parameter/value" + $(psql_cmd postgres -tAwq -o /dev/null -v ON_ERROR_STOP=on) <<'SQL' + \\set conf_value \`cat <&3\` + ALTER SYSTEM SET ${conf_name} = :'conf_value'; + SELECT pg_reload_conf(); + SQL + exec 3<&- + EOF + ;; + (absent) + psql_cmd postgres -qwc "ALTER SYSTEM SET ${conf_name} TO DEFAULT" + ;; + (*) + printf 'Invalid --state: %s\n' "${state_should}" >&2 + printf 'Only "present" and "absent" are acceptable.\n' >&2 + exit 1 + ;; +esac + +# Restart PostgreSQL server if required to apply new configuration value +cat <&2 + exit 1 + esac + ;; + (*) + printf "Don't know how to restart services with your init (%s)\n" "${init}" >&2 + exit 1 + esac + ) +fi +EOF diff --git a/cdist/conf/type/__postgres_conf/man.rst b/cdist/conf/type/__postgres_conf/man.rst new file mode 100644 index 00000000..e035f080 --- /dev/null +++ b/cdist/conf/type/__postgres_conf/man.rst @@ -0,0 +1,60 @@ +cdist-type__postgres_conf(7) +============================ + +NAME +---- +cdist-type__postgres_conf - Alter PostgreSQL configuration + + +DESCRIPTION +----------- +Configure a running PostgreSQL server using ``ALTER SYSTEM``. + + +REQUIRED PARAMETERS +------------------- +value + The value to set (can be omitted if ``--state`` is set to ``absent``). + + +OPTIONAL PARAMETERS +------------------- +state + ``present`` or ``absent``. + Defaults to ``present``. + + +BOOLEAN PARAMETERS +------------------ +None. + + +EXAMPLES +-------- + +.. code-block:: sh + + # set timezone + __postgres_conf timezone --value Europe/Zurich + + # reset maximum number of concurrent connections to default (normally 100) + __postgres_conf max_connections --state absent + + +SEE ALSO +-------- +None. + + +AUTHORS +------- +Beni Ruef (bernhard.ruef--@--ssrq-sds-fds.ch) +Dennis Camera (dennis.camera--@--ssrq-sds-fds.ch) + + +COPYING +------- +Copyright \(C) 2019-2021 SSRQ (www.ssrq-sds-fds.ch). +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. diff --git a/cdist/conf/type/__postgres_conf/parameter/default/state b/cdist/conf/type/__postgres_conf/parameter/default/state new file mode 100644 index 00000000..e7f6134f --- /dev/null +++ b/cdist/conf/type/__postgres_conf/parameter/default/state @@ -0,0 +1 @@ +present diff --git a/cdist/conf/type/__postgres_conf/parameter/optional b/cdist/conf/type/__postgres_conf/parameter/optional new file mode 100644 index 00000000..d0460d86 --- /dev/null +++ b/cdist/conf/type/__postgres_conf/parameter/optional @@ -0,0 +1,2 @@ +state +value diff --git a/cdist/conf/type/__postgres_database/explorer/postgres_user b/cdist/conf/type/__postgres_database/explorer/postgres_user new file mode 120000 index 00000000..714e7237 --- /dev/null +++ b/cdist/conf/type/__postgres_database/explorer/postgres_user @@ -0,0 +1 @@ +../../__postgres_conf/explorer/postgres_user \ No newline at end of file diff --git a/cdist/conf/type/__postgres_database/explorer/state b/cdist/conf/type/__postgres_database/explorer/state index d68d4120..6a25df86 100755 --- a/cdist/conf/type/__postgres_database/explorer/state +++ b/cdist/conf/type/__postgres_database/explorer/state @@ -1,6 +1,7 @@ #!/bin/sh # # 2011 Steven Armstrong (steven-cdist at armstrong.cc) +# 2021 Dennis Camera (dennis.camera at ssrq-sds-fds.ch) # # This file is part of cdist. # @@ -18,25 +19,18 @@ # along with cdist. If not, see . # -case "$("${__explorer}/os")" -in - netbsd) - postgres_user='pgsql' - ;; - openbsd) - postgres_user='_postgresql' - ;; - *) - postgres_user='postgres' - ;; -esac +postgres_user=$("${__type_explorer:?}/postgres_user") +dbname=${__object_id:?} -name="$__object_id" +quote() { printf '%s\n' "$*" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/'/"; } +psql_exec() { + su - "${postgres_user}" -c "psql $(quote "$1") -twAc $(quote "$2")" +} -if test -n "$(su - "$postgres_user" -c "psql postgres -twAc \"SELECT 1 FROM pg_database WHERE datname='$name'\"")" +if psql_exec postgres "SELECT datname FROM pg_database" | grep -qFx "${dbname}" then - echo 'present' + echo 'present' else - echo 'absent' + echo 'absent' fi diff --git a/cdist/conf/type/__postgres_database/gencode-remote b/cdist/conf/type/__postgres_database/gencode-remote index 0f11cff4..7d7d6fa2 100755 --- a/cdist/conf/type/__postgres_database/gencode-remote +++ b/cdist/conf/type/__postgres_database/gencode-remote @@ -1,6 +1,7 @@ #!/bin/sh -e # # 2011 Steven Armstrong (steven-cdist at armstrong.cc) +# 2021 Dennis Camera (dennis.camera at ssrq-sds-fds.ch) # # This file is part of cdist. # @@ -18,60 +19,63 @@ # along with cdist. If not, see . # -case "$(cat "${__global}/explorer/os")" -in - netbsd) - postgres_user='pgsql' - ;; - openbsd) - postgres_user='_postgresql' - ;; - *) - postgres_user='postgres' - ;; -esac +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' "${_arg}" | sed -e "s/'/'\\\\''/g")'" + else + set -- "$@" "${_arg}" + fi + done + unset _arg + # NOTE: Use printf because POSIX echo interprets escape sequences + printf '%s' "$*" +} -name="$__object_id" -state_should="$(cat "$__object/parameter/state")" -state_is="$(cat "$__object/explorer/state")" +postgres_user=$(cat "${__object:?}/explorer/postgres_user") -if [ "$state_should" != "$state_is" ]; then - case "$state_should" in - present) - owner="" - if [ -f "$__object/parameter/owner" ]; then - owner="-O \"$(cat "$__object/parameter/owner")\"" - fi +dbname=${__object_id:?} +state_should=$(cat "${__object:?}/parameter/state") +state_is=$(cat "${__object:?}/explorer/state") - template="" - if [ -f "$__object/parameter/template" ]; then - template="--template \"$(cat "$__object/parameter/template")\"" - fi - - encoding="" - if [ -f "$__object/parameter/encoding" ]; then - encoding="--encoding \"$(cat "$__object/parameter/encoding")\"" - fi - - lc_collate="" - if [ -f "$__object/parameter/lc-collate" ]; then - lc_collate="--lc-collate \"$(cat "$__object/parameter/lc-collate")\"" - fi - - lc_ctype="" - if [ -f "$__object/parameter/lc-ctype" ]; then - lc_ctype="--lc-ctype \"$(cat "$__object/parameter/lc-ctype")\"" - fi - - cat << EOF -su - '$postgres_user' -c "createdb $owner \"$name\" $template $encoding $lc_collate $lc_ctype" -EOF - ;; - absent) - cat << EOF -su - '$postgres_user' -c "dropdb \"$name\"" -EOF - ;; - esac +if test "${state_should}" = "$state_is" +then + exit 0 fi + +case ${state_should} +in + (present) + set -- + + while read -r param_name opt + do + if test -f "${__object:?}/parameter/${param_name}" + then + set -- "$@" "${opt}" "$(cat "${__object:?}/parameter/${param_name}")" + fi + done <<-'EOF' + owner -O + template --template + encoding --encoding + lc_collate --lc-collate + lc_ctype --lc-ctype + EOF + + set -- "$@" "${dbname}" + + cat <<-EOF + su - $(quote "${postgres_user}") -c $(quote "$(quote createdb "$@")") + EOF + ;; + (absent) + cat <<-EOF + su - $(quote "${postgres_user}") -c $(quote "$(quote dropdb "${dbname}")") + EOF + ;; +esac diff --git a/cdist/conf/type/__postgres_extension/explorer/postgres_user b/cdist/conf/type/__postgres_extension/explorer/postgres_user new file mode 120000 index 00000000..714e7237 --- /dev/null +++ b/cdist/conf/type/__postgres_extension/explorer/postgres_user @@ -0,0 +1 @@ +../../__postgres_conf/explorer/postgres_user \ No newline at end of file diff --git a/cdist/conf/type/__acl/explorer/checks b/cdist/conf/type/__postgres_extension/explorer/state old mode 100755 new mode 100644 similarity index 54% rename from cdist/conf/type/__acl/explorer/checks rename to cdist/conf/type/__postgres_extension/explorer/state index 70bb0412..9d156be7 --- a/cdist/conf/type/__acl/explorer/checks +++ b/cdist/conf/type/__postgres_extension/explorer/state @@ -1,6 +1,7 @@ #!/bin/sh -e +# -*- mode: sh; indent-tabs-mode: t -*- # -# 2019 Ander Punnar (ander-at-kvlt-dot-ee) +# 2021 Dennis Camera (dennis.camera at ssrq-sds-fds.ch) # # This file is part of cdist. # @@ -17,23 +18,24 @@ # You should have received a copy of the GNU General Public License # along with cdist. If not, see . # +# Prints "present" if the extension is currently installed. +# "absent" otherwise. -# TODO check if filesystem has ACL turned on etc +quote() { printf '%s\n' "$*" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/'/"; } -if [ -f "$__object/parameter/acl" ] +postgres_user=$("${__type_explorer:?}/postgres_user") + +IFS=: read -r dbname extname < /dev/null - then - echo "missing $param '$check'" >&2 - exit 1 - fi - done + echo present +else + echo absent fi diff --git a/cdist/conf/type/__postgres_extension/gencode-remote b/cdist/conf/type/__postgres_extension/gencode-remote index af9c97f1..4ce72622 100755 --- a/cdist/conf/type/__postgres_extension/gencode-remote +++ b/cdist/conf/type/__postgres_extension/gencode-remote @@ -2,9 +2,10 @@ # # 2011 Steven Armstrong (steven-cdist at armstrong.cc) # 2013 Tomas Pospisek (tpo_deb at sourcepole.ch) +# 2021 Dennis Camera (dennis.camera at ssrq-sds-fds.ch) # # This type was created by Tomas Pospisek based on the -#__postgres_role type by Steven Armstrong +# __postgres_role type by Steven Armstrong. # # This file is part of cdist. # @@ -22,32 +23,38 @@ # along with cdist. If not, see . # -case "$(cat "${__global}/explorer/os")" +postgres_user=$(cat "${__object:?}/explorer/postgres_user") + +quote() { printf '%s\n' "$*" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/'/"; } +psql_cmd() { + printf 'su - %s -c %s\n' \ + "$(quote "${postgres_user}")" \ + "$(quote psql "$(quote "$1")" -c "$(quote "$2")")" +} + + +IFS=: read -r dbname extname <&2 + exit 1 + ;; esac diff --git a/cdist/conf/type/__postgres_extension/man.rst b/cdist/conf/type/__postgres_extension/man.rst index 79645b2b..442239f6 100644 --- a/cdist/conf/type/__postgres_extension/man.rst +++ b/cdist/conf/type/__postgres_extension/man.rst @@ -3,32 +3,36 @@ cdist-type__postgres_extension(7) NAME ---- -cdist-type__postgres_extension - manage postgres extensions +cdist-type__postgres_extension - Manage PostgreSQL extensions DESCRIPTION ----------- -This cdist type allows you to create or drop postgres extensions. +This cdist type allows you to manage PostgreSQL extensions. -The object you need to pass to __postgres_extension consists of -the database name and the extension name joined by a colon in the -following form: - -.. code-block:: sh - - dbname:extension - -f.ex. +The ``__object_id`` to pass to ``__postgres_extension`` is of the form +``dbname:extension``, e.g.: .. code-block:: sh rails_test:unaccent +**CAUTION!** Be careful when installing extensions from (untrusted) third-party +sources: + + | Installing an extension as superuser requires trusting that the extension's + author wrote the extension installation script in a secure fashion. It is + not terribly difficult for a malicious user to create trojan-horse objects + that will compromise later execution of a carelessly-written extension + script, allowing that user to acquire superuser privileges. + | – ``_ + + OPTIONAL PARAMETERS ------------------- state - either "present" or "absent", defaults to "present" + either ``present`` or ``absent``, defaults to ``present``. EXAMPLES @@ -36,24 +40,29 @@ EXAMPLES .. code-block:: sh - __postgres_extension rails_test:unaccent - __postgres_extension --present rails_test:unaccent - __postgres_extension --absent rails_test:unaccent + # Install extension unaccent into database rails_test + __postgres_extension rails_test:unaccent + + # Drop extension unaccent from database fails_test + __postgres_extension rails_test:unaccent --state absent SEE ALSO -------- -:strong:`cdist-type__postgre_database`\ (7) +- :strong:`cdist-type__postgres_database`\ (7) +- PostgreSQL "CREATE EXTENSION" documentation at: + ``_. -Postgres "Create Extension" documentation at: . -AUTHOR +AUTHORS ------- -Tomas Pospisek +| Tomas Pospisek +| Dennis Camera + COPYING ------- -Copyright \(C) 2014 Tomas Pospisek. 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. +Copyright \(C) 2014 Tomas Pospisek, 2021 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. diff --git a/cdist/conf/type/__postgres_role/explorer/postgres_user b/cdist/conf/type/__postgres_role/explorer/postgres_user new file mode 120000 index 00000000..714e7237 --- /dev/null +++ b/cdist/conf/type/__postgres_role/explorer/postgres_user @@ -0,0 +1 @@ +../../__postgres_conf/explorer/postgres_user \ No newline at end of file diff --git a/cdist/conf/type/__postgres_role/explorer/state b/cdist/conf/type/__postgres_role/explorer/state index c8e1fa9d..822816c1 100755 --- a/cdist/conf/type/__postgres_role/explorer/state +++ b/cdist/conf/type/__postgres_role/explorer/state @@ -1,6 +1,7 @@ -#!/bin/sh +#!/bin/sh -e # # 2011 Steven Armstrong (steven-cdist at armstrong.cc) +# 2020 Dennis Camera (dennis.camera at ssrq-sds-fds.ch) # # This file is part of cdist. # @@ -11,32 +12,125 @@ # # 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 +# 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 . # -case "$("${__explorer}/os")" -in - netbsd) - postgres_user='pgsql' - ;; - openbsd) - postgres_user='_postgresql' - ;; - *) - postgres_user='postgres' - ;; -esac +postgres_user=$("${__type_explorer:?}/postgres_user") +rolename=${__object_id:?} -name="$__object_id" +psql_query() { + su -l "${postgres_user}" -c "$( + printf "psql -q -F '\034' -R '\036' -wAc '%s'" \ + "$(printf %s "$*" | sed "s/'/'\\\\''/g")" + )" +} -if test -n "$(su - "$postgres_user" -c "psql postgres -twAc \"SELECT 1 FROM pg_roles WHERE rolname='$name'\"")" +password_check_login() ( + PGPASSWORD=$(cat "${__object:?}/parameter/password"; printf .) + PGPASSWORD=${PGPASSWORD%?.} + export PGPASSWORD + psql -q -w -h localhost -U "${rolename}" template1 -c '\q' >/dev/null 2>&1 +) + +role_properties=$( + psql_query "SELECT * FROM pg_roles WHERE rolname = '${rolename}'" \ + | awk ' + BEGIN { RS = "\036"; FS = "\034" } + /^\([0-9]+ rows?\)/ { exit } + NR == 1 { for (i = 1; i <= NF; i++) cols[i] = $i; next } + NR == 2 { for (i = 1; i <= NF; i++) printf "%s=%s\n", cols[i], $i }' +) + +if test -n "${role_properties}" then - echo 'present' + # Check if the user's properties match the parameters + for prop in login createdb createrole superuser + do + bool_should=$(test -f "${__object:?}/parameter/${prop}" && echo 't' || echo 'f') + bool_is=$( + printf '%s\n' "${role_properties}" | + awk -F '=' -v key="${prop}" ' + BEGIN { + if (key == "login") + key = "canlogin" + else if (key == "superuser") + key = "super" + key = "rol" key + } + $1 == key { + sub(/^[^=]*=/, "") + print + } + ' + ) + + test "${bool_is}" = "${bool_should}" || { + state='different properties' + } + done + + # Check password + passwd_stored=$( + psql_query "SELECT rolpassword FROM pg_authid WHERE rolname = '${rolename}'" \ + | awk 'BEGIN { RS = "\036" } NR == 2 { printf "%s.", $0 }') + passwd_stored=${passwd_stored%.} + + if test -s "${__object:?}/parameter/password" + then + passwd_should=$(cat "${__object:?}/parameter/password"; printf .) + fi + passwd_should=${passwd_should%?.} + + if test -z "${passwd_stored}" + then + test -z "${passwd_should}" || state="${state:-different} password" + elif expr "${passwd_stored}" : 'SCRAM-SHA-256\$.*$' >/dev/null + then + # SCRAM-SHA-256 "encrypted" password + # NOTE: There is currently no easy way to check SCRAM passwords without + # logging in + password_check_login || state="${state:-different} password" + elif expr "${passwd_stored}" : 'md5[0-9a-f]\{32\}$' >/dev/null + then + # MD5 "encrypted" password + if command -v md5sum >/dev/null 2>&1 + then + should_md5=$( + printf '%s%s' "${passwd_should}" "${rolename}" \ + | md5sum - | sed -e 's/[^0-9a-f]*$//') + elif command -v gmd5sum >/dev/null 2>&1 + then + should_md5=$( + printf '%s%s' "${passwd_should}" "${rolename}" \ + | gmd5sum - | sed -e 's/[^0-9a-f]*$//') + elif command -v openssl >/dev/null 2>&1 + then + should_md5=$( + printf '%s%s' "${passwd_should}" "${rolename}" \ + | openssl dgst -md5 | sed 's/^.* //') + fi + + if test -n "${should_md5}" + then + test "${passwd_stored}" = "md5${should_md5}" \ + || state="${state:-different} password" + else + password_check_login || state="${state:-different} password" + fi + else + # unencrypted password (unsupported since PostgreSQL 10) + test "${passwd_stored}" = "${passwd_should}" \ + || state="${state:-different} password" + fi + + test -n "${state}" || state='present' else - echo 'absent' + state='absent' fi + +echo "${state}" diff --git a/cdist/conf/type/__postgres_role/gencode-remote b/cdist/conf/type/__postgres_role/gencode-remote index 282294c9..4cb78330 100755 --- a/cdist/conf/type/__postgres_role/gencode-remote +++ b/cdist/conf/type/__postgres_role/gencode-remote @@ -1,6 +1,7 @@ #!/bin/sh -e # # 2011 Steven Armstrong (steven-cdist at armstrong.cc) +# 2020 Dennis Camera (dennis.camera at ssrq-sds-fds.ch) # # This file is part of cdist. # @@ -11,55 +12,104 @@ # # 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 +# 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 . # -case "$(cat "${__global}/explorer/os")" +quote() { + if test $# -gt 0 + then + printf '%s' "$*" + else + cat - + fi | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/'/" +} + +postgres_user=$(cat "${__object:?}/explorer/postgres_user") +rolename=${__object_id:?} +state_is=$(cat "${__object:?}/explorer/state") +state_should=$(cat "${__object:?}/parameter/state") + +if test "${state_is}" = "${state_should}" +then + exit 0 +fi + +psql_query() { + printf 'su -l %s -c %s\n' \ + "$(quote "${postgres_user}")" \ + "$(quote "psql postgres -q -w -c $(quote "$1")")" +} + +psql_set_password() { + # NOTE: Always make sure that the password does not end up in psql_history! + # NOTE: Never set an empty string as the password, because it can be + # interpreted differently by different tooling. + if test -s "${__object:?}/parameter/password" + then + cat <<-EOF + exec 3< "\${__object:?}/parameter/password" + su -l '${postgres_user}' -c 'psql -q -w postgres' <<'SQL' + \set HISTFILE /dev/null + \set pw \`cat <&3\` + ALTER ROLE "${rolename}" WITH PASSWORD :'pw'; + SQL + exec 3<&- + EOF + else + psql_query "ALTER ROLE \"${rolename}\" WITH PASSWORD NULL;" + fi +} + +role_properties_should() { + _props= + for _prop in login createdb createrole superuser + do + _props="${_props}${_props:+ }$( + if test -f "${__object:?}/parameter/${_prop}" + then + echo "${_prop}" + else + echo "no${_prop}" + fi \ + | tr '[:lower:]' '[:upper:]')" + done + printf '%s\n' "${_props}" + unset _prop _props +} + +case ${state_should} in - netbsd) - postgres_user='pgsql' - ;; - openbsd) - postgres_user='_postgresql' - ;; - *) - postgres_user='postgres' - ;; -esac - - -name="$__object_id" -state_is="$(cat "$__object/explorer/state")" -state_should="$(cat "$__object/parameter/state")" - -[ "$state_is" = "$state_should" ] && exit 0 - -case "$state_should" in - present) - if [ -f "$__object/parameter/password" ]; then - password="$(cat "$__object/parameter/password")" - fi - booleans="" - for boolean in login createdb createrole superuser; do - if [ ! -f "$__object/parameter/$boolean" ]; then - boolean="no${boolean}" - fi - upper=$(echo $boolean | tr '[:lower:]' '[:upper:]') - booleans="$booleans $upper" - done - - [ -n "$password" ] && password="PASSWORD '$password'" - cat << EOF -su - '$postgres_user' -c "psql postgres -wc \"CREATE ROLE \\\\\"$name\\\\\" WITH $password $booleans;\"" -EOF - ;; - absent) - cat << EOF -su - '$postgres_user' -c "dropuser \"$name\"" -EOF - ;; + (present) + case ${state_is} + in + (absent) + psql_query "CREATE ROLE \"${rolename}\" WITH $(role_properties_should);" + psql_set_password + ;; + (different*) + if expr "${state_is}" : 'different.*properties' >/dev/null + then + psql_query "ALTER ROLE \"${rolename}\" WITH $(role_properties_should);" + fi + + if expr "${state_is}" : 'different.*password' >/dev/null + then + psql_set_password + fi + ;; + (*) + printf 'Invalid state reported by state explorer: %s\n' "${state_is}" >&2 + exit 1 + ;; + esac + ;; + (absent) + printf 'su -l %s -c %s\n' \ + "$(quote "${postgres_user}")" \ + "$(quote "dropuser $(quote "${rolename}")")" + ;; esac diff --git a/cdist/conf/type/__pyvenv/explorer/group b/cdist/conf/type/__pyvenv/explorer/group index a655bda7..922ce3df 100755 --- a/cdist/conf/type/__pyvenv/explorer/group +++ b/cdist/conf/type/__pyvenv/explorer/group @@ -1,5 +1,24 @@ -#!/bin/sh +#!/bin/sh -e -destination="/$__object_id" +destination="/${__object_id:?}" -stat --print "%G" "${destination}" 2>/dev/null || exit 0 +# shellcheck disable=SC2012 +group_gid=$(ls -ldn "${destination}" | awk '{ print $4 }') + +# NOTE: +1 because $((notanum)) prints 0. +if test $((group_gid + 1)) -ge 0 +then + group_should=$(cat "${__object:?}/parameter/group") + + if expr "${group_should}" : '[0-9]*$' >/dev/null + then + printf '%u\n' "${group_gid}" + else + if command -v getent >/dev/null 2>&1 + then + getent group "${group_gid}" | cut -d : -f 1 + else + awk -F: -v gid="${group_gid}" '$3 == gid { print $1 }' /etc/group + fi + fi +fi diff --git a/cdist/conf/type/__pyvenv/explorer/owner b/cdist/conf/type/__pyvenv/explorer/owner index 8b3c7f8e..ebec751f 100755 --- a/cdist/conf/type/__pyvenv/explorer/owner +++ b/cdist/conf/type/__pyvenv/explorer/owner @@ -1,5 +1,19 @@ -#!/bin/sh +#!/bin/sh -e -destination="/$__object_id" +destination="/${__object_id:?}" -stat --print "%U" "${destination}" 2>/dev/null || exit 0 +# shellcheck disable=SC2012 +owner_uid=$(ls -ldn "${destination}" | awk '{ print $3 }') + +# NOTE: +1 because $((notanum)) prints 0. +if test $((owner_uid + 1)) -ge 0 +then + owner_should=$(cat "${__object:?}/parameter/owner") + + if expr "${owner_should}" : '[0-9]*$' >/dev/null + then + printf '%u\n' "${owner_uid}" + else + printf '%s\n' "$(id -u -n "${owner_uid}")" + fi +fi diff --git a/cdist/conf/type/__pyvenv/man.rst b/cdist/conf/type/__pyvenv/man.rst index 8085ff12..e2e4a1e6 100644 --- a/cdist/conf/type/__pyvenv/man.rst +++ b/cdist/conf/type/__pyvenv/man.rst @@ -61,7 +61,7 @@ EXAMPLES __pyvenv /home/foo/fooenv --pyvenv /usr/local/bin/pyvenv-3.4 # Create python virtualenv for user foo. - __pyvenv /home/foo/fooenv --group foo --user foo + __pyvenv /home/foo/fooenv --group foo --owner foo # Create python virtualenv with specific parameters. __pyvenv /home/services/djangoenv --venvparams "--copies --system-site-packages" diff --git a/cdist/conf/type/__rsync/gencode-local b/cdist/conf/type/__rsync/gencode-local index e36ded2f..e9f3c131 100755 --- a/cdist/conf/type/__rsync/gencode-local +++ b/cdist/conf/type/__rsync/gencode-local @@ -1,39 +1,104 @@ #!/bin/sh -e -# -# 2015 Dominique Roux (dominique.roux4 at gmail.com) -# -# 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 . -# -source=$(cat "$__object/parameter/source") -remote_user=$(cat "$__object/parameter/remote-user") +if ! command -v rsync > /dev/null +then + echo 'rsync is missing in local machine' >&2 + exit 1 +fi -if [ -f "$__object/parameter/destination" ]; then - destination=$(cat "$__object/parameter/destination") +src="$( cat "$__object/parameter/source" )" + +if [ ! -e "$src" ] +then + echo "$src not found" >&2 + exit 1 +fi + +if [ -f "$__object/parameter/destination" ] +then + dst="$( cat "$__object/parameter/destination" )" else - destination="/$__object_id" + dst="/$__object_id" fi -set -- -if [ -f "$__object/parameter/rsync-opts" ]; then - while read -r opts; do - set -- "$@" "--$opts" - done < "$__object/parameter/rsync-opts" +# if source is directory, then make sure that +# source and destination are ending with slash, +# because this is what you almost always want when +# rsyncing two directories. + +if [ -d "$src" ] +then + if ! echo "$src" | grep -Eq '/$' + then + src="$src/" + fi + + if ! echo "$dst" | grep -Eq '/$' + then + dst="$dst/" + fi fi -echo rsync -a \ - --no-owner --no-group \ - -q "$@" "${source}/" "${remote_user}@${__target_host}:${destination}" +remote_user="$( cat "$__object/parameter/remote-user" )" + +options="$( cat "$__object/parameter/options" )" + +if [ -f "$__object/parameter/option" ] +then + while read -r l + do + # there's a limitation in argparse: value can't begin with '-'. + # to workaround this, let's prefix opts with '\' in manifest and remove here. + # read more about argparse issue: https://bugs.python.org/issue9334 + + options="$options $( echo "$l" | sed 's/\\//g' )" + done \ + < "$__object/parameter/option" +fi + +if [ -f "$__object/parameter/owner" ] || [ -f "$__object/parameter/group" ] +then + options="$options --chown=" + + if [ -f "$__object/parameter/owner" ] + then + owner="$( cat "$__object/parameter/owner" )" + options="$options$owner" + fi + + if [ -f "$__object/parameter/group" ] + then + group="$( cat "$__object/parameter/group" )" + options="$options:$group" + fi +fi + +if [ -f "$__object/parameter/mode" ] +then + mode="$( cat "$__object/parameter/mode" )" + options="$options --chmod=$mode" +fi + +# IMPORTANT +# +# 1. we first dry-run rsync with change summary to find out +# if there are any changes and code generation is needed. +# 2. normally, to get current state or target host, we run +# such operations in type explorers, but that's not +# possible due to how rsync works. +# 3. redirecting output of dry-run to stderr to ease debugging. +# 4. to understand how that cryptic regex works, please +# open rsync manpage and read about --itemize-changes. + +export RSYNC_RSH="$__remote_exec" + +# shellcheck disable=SC2086 +if ! rsync --dry-run --itemize-changes $options "$src" "$remote_user@$__target_host:$dst" \ + | grep -E '^(<|>|c|h|\.|\*)[fdL][cstTpogunbax\.\+\?]+\s' >&2 +then + exit 0 +fi + +echo "export RSYNC_RSH='$__remote_exec'" + +echo "rsync $options $src $remote_user@$__target_host:$dst" diff --git a/cdist/conf/type/__rsync/man.rst b/cdist/conf/type/__rsync/man.rst index 94b06d63..88019c92 100644 --- a/cdist/conf/type/__rsync/man.rst +++ b/cdist/conf/type/__rsync/man.rst @@ -3,112 +3,73 @@ cdist-type__rsync(7) NAME ---- -cdist-type__rsync - Mirror directories using rsync +cdist-type__rsync - Mirror directories using ``rsync`` DESCRIPTION ----------- -WARNING: This type is of BETA quality: - -- it has not been tested widely -- interfaces *may* change -- if there is a better approach to solve the problem -> the type may even vanish - -If you are fine with these constraints, please read on. - - -This cdist type allows you to mirror local directories to the -target host using rsync. Rsync will be installed in the manifest of the type. -If group or owner are giveng, a recursive chown will be executed on the -target host. - -A slash will be appended to the source directory so that only the contents -of the directory are taken and not the directory name itself. +The purpose of this type is to bring power of ``rsync`` into ``cdist``. REQUIRED PARAMETERS ------------------- source - Where to take files from + Source directory in local machine. + If source is directory, slash (``/``) will be added to source and destination paths. OPTIONAL PARAMETERS ------------------- -group - Group to chgrp to. +destination + Destination directory. Defaults to ``$__object_id``. owner - User to chown to. + Will be passed to ``rsync`` as ``--chown=OWNER``. + Read ``rsync(1)`` for more details. -destination - Use this as the base destination instead of the object id +group + Will be passed to ``rsync`` as ``--chown=:GROUP``. + Read ``rsync(1)`` for more details. + +mode + Will be passed to ``rsync`` as ``--chmod=MODE``. + Read ``rsync(1)`` for more details. + +options + Defaults to ``--recursive --links --perms --times``. + Due to `bug in Python's argparse`_, value must be prefixed with ``\``. remote-user - Use this user instead of the default "root" for rsync operations. + Defaults to ``root``. OPTIONAL MULTIPLE PARAMETERS ---------------------------- -rsync-opts - Use this option to give rsync options with. - See rsync(1) for available options. - Only "--" options are supported. - Write the options without the beginning "--" - Can be specified multiple times. - - -MESSAGES --------- -NONE +option + Pass additional options to ``rsync``. + See ``rsync(1)`` for all possible options. + Due to `bug in Python's argparse`_, value must be prefixed with ``\``. EXAMPLES -------- - .. code-block:: sh - # You can use any source directory - __rsync /tmp/testdir \ - --source /etc - - # Use source from type - __rsync /etc \ - --source "$__type/files/package" - - # Allow multiple __rsync objects to write to the same dir - __rsync mystuff \ - --destination /usr/local/bin \ - --source "$__type/files/package" - - __rsync otherstuff \ - --destination /usr/local/bin \ - --source "$__type/files/package2" - - # Use rsync option --exclude - __rsync /tmp/testdir \ - --source /etc \ - --rsync-opts exclude=sshd_conf - - # Use rsync with multiple options --exclude --dry-run - __rsync /tmp/testing \ - --source /home/tester \ - --rsync-opts exclude=id_rsa \ - --rsync-opts dry-run - - -SEE ALSO --------- -:strong:`rsync`\ (1) + __rsync /var/www/example.com \ + --owner root \ + --group www-data \ + --mode 'D750,F640' \ + --source "$__files/example.com/www" AUTHORS ------- -Nico Schottelius +Ander Punnar COPYING ------- -Copyright \(C) 2015 Nico Schottelius. 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. +Copyright \(C) 2021 Ander Punnar. 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. diff --git a/cdist/conf/type/__rsync/manifest b/cdist/conf/type/__rsync/manifest index 9bd44c6d..64fa804e 100755 --- a/cdist/conf/type/__rsync/manifest +++ b/cdist/conf/type/__rsync/manifest @@ -1,21 +1,3 @@ #!/bin/sh -e -# -# 2015 Dominique Roux (dominique.roux4 at gmail.com) -# -# 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 . -# __package rsync diff --git a/cdist/conf/type/__rsync/parameter/default/options b/cdist/conf/type/__rsync/parameter/default/options new file mode 100644 index 00000000..d967b110 --- /dev/null +++ b/cdist/conf/type/__rsync/parameter/default/options @@ -0,0 +1 @@ +--recursive --links --perms --times diff --git a/cdist/conf/type/__rsync/parameter/optional b/cdist/conf/type/__rsync/parameter/optional index ac2b2390..833e9bbe 100644 --- a/cdist/conf/type/__rsync/parameter/optional +++ b/cdist/conf/type/__rsync/parameter/optional @@ -1,4 +1,6 @@ destination -owner group +mode +options +owner remote-user diff --git a/cdist/conf/type/__rsync/parameter/optional_multiple b/cdist/conf/type/__rsync/parameter/optional_multiple index fdb7cd88..01925a15 100644 --- a/cdist/conf/type/__rsync/parameter/optional_multiple +++ b/cdist/conf/type/__rsync/parameter/optional_multiple @@ -1 +1 @@ -rsync-opts +option diff --git a/cdist/conf/type/__sed/explorer/file b/cdist/conf/type/__sed/explorer/file new file mode 100755 index 00000000..ec3d0fe8 --- /dev/null +++ b/cdist/conf/type/__sed/explorer/file @@ -0,0 +1,16 @@ +#!/bin/sh -e + +if [ -f "$__object/parameter/file" ] +then + file="$( cat "$__object/parameter/file" )" +else + file="/$__object_id" +fi + +if [ ! -e "$file" ] +then + echo "$file does not exist" >&2 + exit 1 +fi + +cat "$file" diff --git a/cdist/conf/type/__sed/gencode-remote b/cdist/conf/type/__sed/gencode-remote new file mode 100755 index 00000000..f99c5a88 --- /dev/null +++ b/cdist/conf/type/__sed/gencode-remote @@ -0,0 +1,58 @@ +#!/bin/sh -e + +if [ -f "$__object/parameter/file" ] +then + file="$( cat "$__object/parameter/file" )" +else + file="/$__object_id" +fi + +script="$( cat "$__object/parameter/script" )" + +if [ "$script" = '-' ] +then + script="$( cat "$__object/stdin" )" +fi + +# since stdin is not available in explorer, we pull file from target with explorer + +file_from_target="$__object/explorer/file" + +sed_cmd='sed' + +if [ -f "$__object/parameter/regexp-extended" ] +then + sed_cmd="$sed_cmd -E" +fi + +# do sed dry run, diff result and if no change, then there's nothing to do +# also redirect diff's output to stderr for debugging purposes + +if echo "$script" | "$sed_cmd" -f - "$file_from_target" | diff -u "$file_from_target" - >&2 +then + exit 0 +fi + +# we can't use -i, because it's not posix, so we fly with tempfile and cp +# and we use cp because we want to preserve destination file's attributes + +# shellcheck disable=SC2016 +echo 'tmp="$__object/tempfile"' + +echo "$sed_cmd -f - '$file' > \"\$tmp\" << EOF" + +echo "$script" + +echo 'EOF' + +echo "cp \"\$tmp\" '$file'" + +# shellcheck disable=SC2016 +echo 'rm -f "$tmp"' + +echo 'change' >> "$__messages_out" + +if [ -f "$__object/parameter/onchange" ] +then + cat "$__object/parameter/onchange" +fi diff --git a/cdist/conf/type/__sed/man.rst b/cdist/conf/type/__sed/man.rst new file mode 100644 index 00000000..86789363 --- /dev/null +++ b/cdist/conf/type/__sed/man.rst @@ -0,0 +1,57 @@ +cdist-type__sed(7) +================== + +NAME +---- +cdist-type__sed - Transform text files with ``sed`` + + +DESCRIPTION +----------- +Transform text files with ``sed``. + + +REQUIRED MULTIPLE PARAMETERS +---------------------------- +script + ``sed`` script. + If ``-`` then the script is read from ``stdin``. + + +OPTIONAL PARAMETERS +------------------- +file + Path to the file. Defaults to ``$__object_id``. + +onchange + Execute this command if ``sed`` changes file. + + +BOOLEAN PARAMETERS +------------------ +regexp-extended + Use extended regular expressions in the script. + Might not be supported with every ``sed`` version. + + +EXAMPLES +-------- + +.. code-block:: sh + + __sed /tmp/foobar --script 's/foo/bar/' + + echo 's/foo/bar/' | __sed foobar --file /tmp/foobar --script - + + +AUTHORS +------- +Ander Punnar + + +COPYING +------- +Copyright \(C) 2021 Ander Punnar. 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. diff --git a/cdist/conf/type/__sed/parameter/boolean b/cdist/conf/type/__sed/parameter/boolean new file mode 100644 index 00000000..1ad75c5d --- /dev/null +++ b/cdist/conf/type/__sed/parameter/boolean @@ -0,0 +1 @@ +regexp-extended diff --git a/cdist/conf/type/__sed/parameter/optional b/cdist/conf/type/__sed/parameter/optional new file mode 100644 index 00000000..fa86f917 --- /dev/null +++ b/cdist/conf/type/__sed/parameter/optional @@ -0,0 +1,2 @@ +file +onchange diff --git a/cdist/conf/type/__sed/parameter/required_multiple b/cdist/conf/type/__sed/parameter/required_multiple new file mode 100644 index 00000000..84f7e31d --- /dev/null +++ b/cdist/conf/type/__sed/parameter/required_multiple @@ -0,0 +1 @@ +script diff --git a/cdist/conf/type/__service/manifest b/cdist/conf/type/__service/manifest index cb5af234..beb0713c 100644 --- a/cdist/conf/type/__service/manifest +++ b/cdist/conf/type/__service/manifest @@ -7,7 +7,9 @@ action="$(cat "$__object/parameter/action")" case "$manager" in 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. diff --git a/cdist/conf/type/__snakeoil_cert/explorer/ssl-cert-group b/cdist/conf/type/__snakeoil_cert/explorer/ssl-cert-group new file mode 100755 index 00000000..a6cb3dfd --- /dev/null +++ b/cdist/conf/type/__snakeoil_cert/explorer/ssl-cert-group @@ -0,0 +1,8 @@ +#!/bin/sh -e + +if grep -Eq '^ssl-cert:' /etc/group +then + echo 'present' +else + echo 'absent' +fi diff --git a/cdist/conf/type/__snakeoil_cert/explorer/state b/cdist/conf/type/__snakeoil_cert/explorer/state new file mode 100755 index 00000000..cc5aae0b --- /dev/null +++ b/cdist/conf/type/__snakeoil_cert/explorer/state @@ -0,0 +1,24 @@ +#!/bin/sh -e + +key_path="$( cat "$__object/parameter/key-path" )" + +if echo "$key_path" | grep -Fq '%s' +then + # shellcheck disable=SC2059 + key_path="$( printf "$key_path" "$__object_id" )" +fi + +cert_path="$( cat "$__object/parameter/cert-path" )" + +if echo "$cert_path" | grep -Fq '%s' +then + # shellcheck disable=SC2059 + cert_path="$( printf "$cert_path" "$__object_id" )" +fi + +if [ ! -f "$key_path" ] || [ ! -f "$cert_path" ] +then + echo 'absent' +else + echo 'present' +fi diff --git a/cdist/conf/type/__snakeoil_cert/gencode-remote b/cdist/conf/type/__snakeoil_cert/gencode-remote new file mode 100755 index 00000000..8ffbfad1 --- /dev/null +++ b/cdist/conf/type/__snakeoil_cert/gencode-remote @@ -0,0 +1,73 @@ +#!/bin/sh -e + +state="$( cat "$__object/explorer/state" )" + +if [ "$state" = 'present' ] +then + exit 0 +fi + +if [ -f "$__object/parameter/common-name" ] +then + common_name="$( cat "$__object/parameter/common-name" )" +else + common_name="$__object_id" +fi + +key_path="$( cat "$__object/parameter/key-path" )" + +if echo "$key_path" | grep -Fq '%s' +then + # shellcheck disable=SC2059 + key_path="$( printf "$key_path" "$__object_id" )" +fi + +cert_path="$( cat "$__object/parameter/cert-path" )" + +if echo "$cert_path" | grep -Fq '%s' +then + # shellcheck disable=SC2059 + cert_path="$( printf "$cert_path" "$__object_id" )" +fi + +key_type="$( cat "$__object/parameter/key-type" )" + +key_type_arg="$( echo "$key_type" | cut -d : -f 2 )" + +case "$key_type" in + rsa:*) + echo "openssl genrsa -out '$key_path' $key_type_arg" + ;; + ec:*) + echo "openssl ecparam -name $key_type_arg -genkey -noout -out '$key_path'" + ;; +esac + +# shellcheck disable=SC2016 +echo 'csr_path="$( mktemp )"' + +echo "openssl req -new -subj '/CN=$common_name' -key '$key_path' -out \"\$csr_path\"" + +echo "openssl x509 -req -sha256 -days 3650 -in \"\$csr_path\" -signkey '$key_path' -out '$cert_path'" + +# shellcheck disable=SC2016 +echo 'rm -f "$csr_path"' + +if [ "$( cat "$__object/explorer/ssl-cert-group" )" = 'present' ] +then + key_group='ssl-cert' +else + key_group='root' +fi + +echo "chmod 640 '$key_path'" + +echo "chown root '$key_path'" + +echo "chgrp $key_group '$key_path'" + +echo "chmod 644 '$cert_path'" + +echo "chown root '$cert_path'" + +echo "chgrp root '$cert_path'" diff --git a/cdist/conf/type/__snakeoil_cert/man.rst b/cdist/conf/type/__snakeoil_cert/man.rst new file mode 100644 index 00000000..b0b0a2e9 --- /dev/null +++ b/cdist/conf/type/__snakeoil_cert/man.rst @@ -0,0 +1,61 @@ +cdist-type__snakeoil_cert(7) +============================ + +NAME +---- +cdist-type__snakeoil_cert - Generate self-signed certificate + + +DESCRIPTION +----------- +The purpose of this type is to generate **self-signed** certificate and private key +for **testing purposes**. Certificate will expire in 3650 days. + +Certificate's and key's access bits will be ``644`` and ``640`` respectively. +If target system has ``ssl-cert`` group, then it will be used as key's group. +Use ``require='__snakeoil_cert/...' __file ...`` to override. + + +OPTIONAL PARAMETERS +------------------- +common-name + Defaults to ``$__object_id``. + +key-path + ``%s`` in path will be replaced with ``$__object_id``. + Defaults to ``/etc/ssl/private/%s.pem``. + +key-type + Possible values are ``rsa:$bits`` and ``ec:$name``. + For possible EC names see ``openssl ecparam -list_curves``. + Defaults to ``rsa:2048``. + +cert-path + ``%s`` in path will be replaced with ``$__object_id``. + Defaults to ``/etc/ssl/certs/%s.pem``. + + +EXAMPLES +-------- +.. code-block:: sh + + __snakeoil_cert localhost-rsa \ + --common-name localhost \ + --key-type rsa:4096 + + __snakeoil_cert localhost-ec \ + --common-name localhost \ + --key-type ec:prime256v1 + + +AUTHORS +------- +Ander Punnar + + +COPYING +------- +Copyright \(C) 2021 Ander Punnar. 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. diff --git a/cdist/conf/type/__snakeoil_cert/parameter/default/cert-path b/cdist/conf/type/__snakeoil_cert/parameter/default/cert-path new file mode 100644 index 00000000..4bbae089 --- /dev/null +++ b/cdist/conf/type/__snakeoil_cert/parameter/default/cert-path @@ -0,0 +1 @@ +/etc/ssl/certs/%s.pem diff --git a/cdist/conf/type/__snakeoil_cert/parameter/default/key-path b/cdist/conf/type/__snakeoil_cert/parameter/default/key-path new file mode 100644 index 00000000..86eb9359 --- /dev/null +++ b/cdist/conf/type/__snakeoil_cert/parameter/default/key-path @@ -0,0 +1 @@ +/etc/ssl/private/%s.pem diff --git a/cdist/conf/type/__snakeoil_cert/parameter/default/key-type b/cdist/conf/type/__snakeoil_cert/parameter/default/key-type new file mode 100644 index 00000000..f13f8ada --- /dev/null +++ b/cdist/conf/type/__snakeoil_cert/parameter/default/key-type @@ -0,0 +1 @@ +rsa:2048 diff --git a/cdist/conf/type/__snakeoil_cert/parameter/optional b/cdist/conf/type/__snakeoil_cert/parameter/optional new file mode 100644 index 00000000..76d08c0a --- /dev/null +++ b/cdist/conf/type/__snakeoil_cert/parameter/optional @@ -0,0 +1,4 @@ +common-name +key-path +key-type +cert-path diff --git a/cdist/conf/type/__ssh_authorized_key/explorer/entry b/cdist/conf/type/__ssh_authorized_key/explorer/entry index ccab0afc..aca0f2b9 100755 --- a/cdist/conf/type/__ssh_authorized_key/explorer/entry +++ b/cdist/conf/type/__ssh_authorized_key/explorer/entry @@ -25,6 +25,7 @@ type_and_key="$(tr ' ' '\n' < "$__object/parameter/key"| awk '/^(ssh|ecdsa)-[^ ] if [ -n "${type_and_key}" ] then file="$(cat "$__object/parameter/file")" + test -e "$file" || exit 0 # get any entries that match the type and key diff --git a/cdist/conf/type/__ssh_authorized_key/gencode-remote b/cdist/conf/type/__ssh_authorized_key/gencode-remote index f37aa565..cbffde94 100755 --- a/cdist/conf/type/__ssh_authorized_key/gencode-remote +++ b/cdist/conf/type/__ssh_authorized_key/gencode-remote @@ -37,9 +37,10 @@ tmpfile=\$(mktemp ${file}.cdist.XXXXXXXXXX) # preserve ownership and permissions of existing file if [ -f "$file" ]; then cp -p "$file" "\$tmpfile" + grep -v -F -x '$line' '$file' >\$tmpfile fi -grep -v -F -x '$line' '$file' > \$tmpfile || true -mv -f "\$tmpfile" "$file" +cat "\$tmpfile" >"$file" +rm -f "\$tmpfile" DONE } diff --git a/cdist/conf/type/__ssh_authorized_keys/explorer/keys b/cdist/conf/type/__ssh_authorized_keys/explorer/keys index cec25746..9694a64b 100755 --- a/cdist/conf/type/__ssh_authorized_keys/explorer/keys +++ b/cdist/conf/type/__ssh_authorized_keys/explorer/keys @@ -1,6 +1,7 @@ #!/bin/sh -e # shellcheck disable=SC1090 +# shellcheck disable=SC1091 file="$( . "$__type_explorer/file" )" if [ -f "$file" ] diff --git a/cdist/conf/type/__sshd_config/explorer/state b/cdist/conf/type/__sshd_config/explorer/state new file mode 100644 index 00000000..75c68b8a --- /dev/null +++ b/cdist/conf/type/__sshd_config/explorer/state @@ -0,0 +1,121 @@ +#!/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 . +# +# Determines the current state of the config option. +# Possible output: +# - present: "should" option present in config file +# - default: the "should" option is the default -> don’t know if present +# - absent: no such option present in config file +# + +joinlines() { sed -n -e H -e "\${x;s/^\\n//;s/\\n/${1:?}/g;p;}"; } +trlower() { tr '[:upper:]' '[:lower:]'; } +tolower() { printf '%s' "$*" | trlower; } + +default_value() { + sshd -T -f /dev/null -C "$(make_conn_spec)" \ + | sed -n -e 's/^'"$(tolower "${1:?}")"'[[:blank:]]\{1,\}//p' +} + +make_conn_spec() { + if test -s "${__object:?}/parameter/match" + then + _match_file="${__object:?}/parameter/match" + else + _match_file='/dev/null' + fi + + for _kw in \ + addr=Address \ + user=User \ + host=Host \ + laddr=LocalAddress \ + lport=LocalPort \ + rdomain=RDomain + do + _specname=${_kw%%=*} + _confname=$(tolower "${_kw#*=}") + while read -r _k _v + do + if test "$(tolower "${_k}")" = "${_confname}" + then + printf '%s=%s\n' "${_specname}" "${_v}" + continue 2 + fi + done <"${_match_file}" + + # NOTE: Print test spec even for empty keys to suppress errors like: + # 'Match User' in configuration but 'user' not in connection test specification. + # except lport: + # Invalid port '' in test mode specification lport= + test "${_specname}" = 'lport' || printf '%s=\n' "${_specname}" + done \ + | joinlines ',' + unset _match_file +} + +sshd_config_file=$(cat "${__object:?}/parameter/file") +state_should=$(cat "${__object:?}/parameter/state") + +if test -s "${__object:?}/parameter/option" +then + option_name=$(cat "${__object:?}/parameter/option") +else + option_name=${__object_id:?} +fi + +value_should=$(cat "${__object:?}/parameter/value" 2>/dev/null) \ +|| test "${state_should}" = absent || exit 0 # param optional if --state absent + +command -v sshd >/dev/null 2>&1 || { + echo 'Cannot find sshd.' >&2 + exit 1 +} + +test -e "${sshd_config_file}" || { + echo 'absent' + exit 0 +} + +value_is=$( + sshd -T -f "${sshd_config_file}" -C "$(make_conn_spec)" \ + | sed -n -e 's/^'"$(tolower "${option_name}")"'[[:blank:]]\{1,\}//p') + +if printf '%s\n' "${value_is}" | { + if test -n "${value_should}" + then + grep -q -x -F "${value_should}" + else + # if no value provided, assume "any" value + grep -q -e . + fi + } +then + if default_value "${option_name}" | grep -q -x -F "${value_is}" + then + # Might produce false positives for default values. + # TODO: Manual checking should be done, but for simplicity, this case is + # currently ignored here. + echo default + else + echo present + fi +else + echo absent +fi diff --git a/cdist/conf/type/__sshd_config/files/update_sshd_config.awk b/cdist/conf/type/__sshd_config/files/update_sshd_config.awk new file mode 100644 index 00000000..f7f30e87 --- /dev/null +++ b/cdist/conf/type/__sshd_config/files/update_sshd_config.awk @@ -0,0 +1,293 @@ +# -*- mode: awk; indent-tabs-mode: t -*- + +function usage() { + print_err("Usage: awk -f update_sshd_config.awk -- -o set|unset [-m 'User git'] -l 'X11Forwarding no' /etc/ssh/sshd_config") +} + +function print_err(s) { print s | "cat >&2" } + +function alength(a, i) { + for (i = 0; (i + 1) in a; ++i); + return i +} + +function join(sep, a, i, s) { + for (i = i ? i : 1; i in a; i++) + s = s sep a[i] + return substr(s, 2) +} + +function getopt(opts, argv, target, files, i, c, lv, idx, nf) { + # trivial getopt(3) implementation; only basic functionality + if (argv[1] == "--") i++ + for (i += 1; i in argv; i++) { + if (lv) { target[c] = argv[i]; lv = 0; continue } + if (argv[i] ~ /^-/) { + c = substr(argv[i], 2, 1) + idx = index(opts, c) + if (!idx) { + print_err(sprintf("invalid option -%c\n", c)) + continue + } + if (substr(opts, idx + 1, 1) == ":") { + # option takes argument + if (length(argv[i]) > 2) + target[c] = substr(argv[i], 3) + else + lv = 1 + } else { + target[c] = 1 + } + } else + files[++nf] = argv[i] + } +} + +# tokenise configuration line +# this function mimics the counterpart in OpenSSH (misc.c) +# but it returns two (next token SUBSEP rest) because I didn’t want to have to +# simulate any pointer magic. +function strdelim_internal(s, split_equals, old) { + if (!s) + return "" + + old = s + + if (!match(s, WHITESPACE "|" QUOTE "" (split_equals ? "|" EQUALS : ""))) + return s + + s = substr(s, RSTART) + old = substr(old, 1, RSTART - 1) + + if (s ~ "^" QUOTE) { + old = substr(old, 2) + + # Find matching quote + if (match(s, QUOTE)) { + old = substr(old, 1, RSTART) + # s = substr() + if (match(s, "^" WHITESPACE "*")) + s = substr(s, RLENGTH) + return old + } else { + # no matching quote + return "" + } + } + + if (match(s, "^" WHITESPACE "+")) { + sub("^" WHITESPACE "+", "", s) + if (split_equals) + sub(EQUALS WHITESPACE "*", "", s) + } else if (s ~ "^" EQUALS) { + s = substr(s, 2) + } + + return old SUBSEP s +} +function strdelim(s) { return strdelim_internal(s, 1) } +function strdelimw(s) { return strdelim_internal(s, 0) } + +function singleton_option(opt) { + return tolower(opt) !~ /^(acceptenv|allowgroups|allowusers|denygroups|denyusers|hostcertificate|hostkey|listenaddress|logverbose|permitlisten|permitopen|port|setenv|subsystem)$/ +} + +function print_update() { + if (mode) { + if (match_only) printf "\t" + printf "%s\n", line_should + updated = 1 + } +} + +BEGIN { + FS = "\n" # disable field splitting + + WHITESPACE = "[ \t]" # servconf.c, misc.c:strdelim_internal (without line breaks, cf. bugs) + QUOTE = "[\"]" # misc.c:strdelim_internal + EQUALS = "[=]" + + split("", opts) + split("", files) + getopt("ho:l:m:", ARGV, opts, files) + + if (opts["h"]) { usage(); exit (e="0") } + + line_should = opts["l"] + match_only = opts["m"] + num_files = alength(files) + + if (num_files != 1 || !opts["o"] || !line_should) { + usage() + exit (e=126) + } + + if (opts["o"] == "set") { + mode = 1 + } else if (opts["o"] == "unset") { + mode = 0 + } else { + print_err(sprintf("invalid mode %s\n", mode)) + exit (e=1) + } + + if (mode) { + # loop over sshd_config twice! + ARGV[2] = ARGV[1] = files[1] + ARGC = 3 + } else { + # only loop once + ARGV[1] = files[1] + ARGC = 2 + } + + split(strdelim(line_should), should, SUBSEP) + option_should = tolower(should[1]) + value_should = should[2] +} + +{ + line = $0 + + # Strip trailing whitespace. Allow \f (form feed) at EOL only + sub("(" WHITESPACE "|\f)*$", "", line) + + # Strip leading whitespace + sub("^" WHITESPACE "*", "", line) + + if (match(line, "^#" WHITESPACE "*")) { + prefix = substr(line, RSTART, RLENGTH) + line = substr(line, RSTART + RLENGTH) + } else { + prefix = "" + } + + line_type = "invalid" + option_is = value_is = "" + + if (line) { + split(strdelim(line), toks, SUBSEP) + + if (tolower(toks[1]) == "match") { + MATCH = (prefix ~ /^#/ ? "#" : "") join(" ", toks, 2) + line_type = "match" + } else if (toks[1] ~ /^[A-Za-z][A-Za-z0-9]+$/) { + # This could be an option line + line_type = "option" + option_is = tolower(toks[1]) + value_is = toks[2] + } + } else { + line_type = "empty" + } +} + +# mode: unset + +!mode { + # delete matching config + if (prefix !~ /^#/) + if (MATCH == match_only && option_is == option_should) + if (!value_should || value_should == value_is) + next + + print + next +} + + +# mode: set + +mode && NR == FNR { + if (line_type == "option") { + if (MATCH !~ /^#/) { + if (prefix ~ /^#/) { + # comment line + last_occ[MATCH, "#" option_is] = FNR + } else { + # option line + last_occ[MATCH, option_is] = FNR + } + last_occ[MATCH] = FNR + } + } else if (line_type == "invalid" && !prefix) { + # INVALID LINE + print_err(sprintf("%s: syntax error on line %u\n", ARGV[0], FNR)) + } + + next +} + +# before second pass prepare hashes containing location information to be used +# in the second pass. +mode && NR > FNR && FNR == 1 { + # First we drop the locations of commented-out options if a non-commented + # option is available. If a non-commented option is available, we will + # append new config options there to have them all at one place. + for (k in last_occ) { + if (k ~ /^#/) { + # delete entries of commented out match blocks + delete last_occ[k] + continue + } + + split(k, parts, SUBSEP) + + if (parts[2] ~ /^#/ && ((parts[1], substr(parts[2], 2)) in last_occ)) + delete last_occ[k] + } + + # Reverse the option => line mapping. The line_map allows for easier lookups + # in the second pass. + # We only keep options, not top-level keywords, because we can only have + # one entry per line and there are conflicts with last lines of "sections". + for (k in last_occ) { + if (!index(k, SUBSEP)) continue + line_map[last_occ[k]] = k + } +} + +# Second pass +mode && line_map[FNR] == match_only SUBSEP option_should && !updated { + split(line_map[FNR], parts, SUBSEP) + + # If option allows multiple values, print current value + if (!singleton_option(parts[2])) { + if (value_should != value_is) + print + } + + print_update() + + next +} + +mode { print } + +# Is a comment option +mode && line_map[FNR] == match_only SUBSEP "#" option_should && !updated { + print_update() +} + +# Last line of the should match section +mode && last_occ[match_only] == FNR && !updated { + # NOTE: Inserting empty lines is only cosmetic. It is only done if + # different options are next to each other and not in a match block + # (match blocks are usually not in the default config and thus don’t + # contain commented blocks.) + if (line && option_is != option_should && !MATCH) + print "" + print_update() +} + +END { + if (e) exit e + + if (mode && !updated) { + if (match_only && MATCH != match_only) { + printf "\nMatch %s\n", match_only + } + + print_update() + } +} diff --git a/cdist/conf/type/__sshd_config/gencode-remote b/cdist/conf/type/__sshd_config/gencode-remote new file mode 100755 index 00000000..275db4aa --- /dev/null +++ b/cdist/conf/type/__sshd_config/gencode-remote @@ -0,0 +1,98 @@ +#!/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 . +# + +joinlines() { sed -n -e H -e "\${x;s/^\\n//;s/\\n/${1:?}/g;p;}"; } + +state_is=$(cat "${__object:?}/explorer/state") +state_should=$(cat "${__object:?}/parameter/state") + +if test "${state_is}" = "${state_should}" -o "${state_is}" = 'default' +then + # nothing to do (if the value is the default, ignore its state) + exit 0 +fi + +case ${state_should} +in + (present) + mode='set' + ;; + (absent) + mode='unset' + ;; + (*) + printf 'Invalid --state: %s\n' "${state_should}" >&2 + exit 1 + ;; +esac + +sshd_config_file=$(cat "${__object:?}/parameter/file") + +quote() { printf "'%s'" "$(printf '%s' "$*" | sed -e "s/'/'\\\\''/g")"; } +drop_awk_comments() { quote "$(sed '/^[[:blank:]]*#.*$/d;/^$/d' "$@")"; } + +# Ensure the sshd_config file is there +cat <$(quote "${sshd_config_file}") + chown 0:0 $(quote "${sshd_config_file}") + chmod 0644 $(quote "${sshd_config_file}") +} + +EOF + +match_only= +if test -s "${__object:?}/parameter/match" +then + match_only=$(joinlines ' ' <"${__object:?}/parameter/match") +fi + +if test -s "${__object:?}/parameter/option" +then + option_line=$(cat "${__object:?}/parameter/option") +else + option_line=${__object_id:?} +fi + +if test -s "${__object:?}/parameter/value" +then + option_line="${option_line} $(cat "${__object:?}/parameter/value")" +fi + +# Send message on config update +printf '%s%s %s\n' "${mode}" "${match_only:+ [${match_only}]}" \ + "${option_line}" >>"${__messages_out:?}" + +# Update sshd_config (remote code) +cat <$(quote "${sshd_config_file}.tmp") \\ +|| exit + +cmp -s $(quote "${sshd_config_file}") $(quote "${sshd_config_file}.tmp") || { + sshd -t -f $(quote "${sshd_config_file}.tmp") \\ + && cat $(quote "${sshd_config_file}.tmp") >$(quote "${sshd_config_file}") \\ + || exit # stop if sshd_config file check fails +} +rm -f $(quote "${sshd_config_file}.tmp") +EOF diff --git a/cdist/conf/type/__sshd_config/man.rst b/cdist/conf/type/__sshd_config/man.rst new file mode 100644 index 00000000..c8e6b8ad --- /dev/null +++ b/cdist/conf/type/__sshd_config/man.rst @@ -0,0 +1,98 @@ +cdist-type__sshd_config(7) +========================== + +NAME +---- +cdist-type__sshd_config - Manage options in sshd_config + + +DESCRIPTION +----------- +This space intentionally left blank. + + +REQUIRED PARAMETERS +------------------- +None. + + +OPTIONAL PARAMETERS +------------------- +file + The path to the sshd_config file to edit. + Defaults to ``/etc/ssh/sshd_config``. +match + Restrict this option to apply only for certain connections. + Allowed values are what would be allowed to be written after a ``Match`` + keyword in ``sshd_config``, e.g. ``--match 'User anoncvs'``. + + Can be used multiple times. All of the values are ANDed together. +option + The name of the option to manipulate. Defaults to ``__object_id``. +state + Can be: + + - ``present``: ensure a matching config line is present (or the default + value). + - ``absent``: ensure no matching config line is present. +value + The option's value to be assigned to the option (if ``--state present``) or + removed (if ``--state absent``). + + This option is required if ``--state present``. If not specified and + ``--state absent``, all values for the given option are removed. + + +BOOLEAN PARAMETERS +------------------ +None. + + +EXAMPLES +-------- + +.. code-block:: sh + + # Disallow root logins with password + __sshd_config PermitRootLogin --value without-password + + # Disallow password-based authentication + __sshd_config PasswordAuthentication --value no + + # Accept the EDITOR environment variable + __sshd_config AcceptEnv:EDITOR --option AcceptEnv --value EDITOR + + # Force command for connections as git user + __sshd_config git@ForceCommand --match 'User git' --option ForceCommand \ + --value 'cd ~git && exec git-shell ${SSH_ORIGINAL_COMMAND:+-c "${SSH_ORIGINAL_COMMAND}"}' + + +SEE ALSO +-------- +:strong:`sshd_config`\ (5) + + +BUGS +---- +- This type assumes a nicely formatted config file, + i.e. no config options spanning multiple lines. +- ``Include`` directives are ignored. +- Config options are not added/removed to/from the config file if their value is + the default value. +- | The explorer will incorrectly report ``absent`` if OpenSSH internally + transforms one value to another (e.g. ``permitrootlogin prohibit-password`` + is transformed to ``permitrootlogin without-password``). + | Workaround: Use the value that OpenSSH uses internally. + + +AUTHORS +------- +Dennis Camera + + +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. diff --git a/cdist/conf/type/__sshd_config/manifest b/cdist/conf/type/__sshd_config/manifest new file mode 100755 index 00000000..e37afebb --- /dev/null +++ b/cdist/conf/type/__sshd_config/manifest @@ -0,0 +1,55 @@ +#!/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 . +# + +os=$(cat "${__global:?}/explorer/os") + +state_should=$(cat "${__object:?}/parameter/state") + +case ${os} +in + (alpine|centos|fedora|redhat|scientific|debian|devuan|ubuntu) + if test "${state_should}" != 'absent' + then + __package openssh-server --state present + fi + ;; + (archlinux|gentoo|slackware|suse) + if test "${state_should}" != 'absent' + then + __package openssh --state present + fi + ;; + (freebsd|netbsd|openbsd) + # whitelist + ;; + (openbmc-phosphor) + # whitelist + # OpenBMC can be configured with dropbear and OpenSSH. + # If dropbear is used, the state explorer will already fail because it + # cannot find the sshd binary. + ;; + (*) + : "${__type:?}" # make shellcheck happy + 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 diff --git a/cdist/conf/type/__sshd_config/parameter/default/file b/cdist/conf/type/__sshd_config/parameter/default/file new file mode 100644 index 00000000..d8ea5dfc --- /dev/null +++ b/cdist/conf/type/__sshd_config/parameter/default/file @@ -0,0 +1 @@ +/etc/ssh/sshd_config diff --git a/cdist/conf/type/__sshd_config/parameter/default/state b/cdist/conf/type/__sshd_config/parameter/default/state new file mode 100644 index 00000000..e7f6134f --- /dev/null +++ b/cdist/conf/type/__sshd_config/parameter/default/state @@ -0,0 +1 @@ +present diff --git a/cdist/conf/type/__sshd_config/parameter/optional b/cdist/conf/type/__sshd_config/parameter/optional new file mode 100644 index 00000000..922ab093 --- /dev/null +++ b/cdist/conf/type/__sshd_config/parameter/optional @@ -0,0 +1,4 @@ +file +option +state +value diff --git a/cdist/conf/type/__sshd_config/parameter/optional_multiple b/cdist/conf/type/__sshd_config/parameter/optional_multiple new file mode 100644 index 00000000..02b1d1a9 --- /dev/null +++ b/cdist/conf/type/__sshd_config/parameter/optional_multiple @@ -0,0 +1 @@ +match diff --git a/cdist/conf/type/__sysctl/explorer/value b/cdist/conf/type/__sysctl/explorer/value index fc85b3d8..3e93c151 100755 --- a/cdist/conf/type/__sysctl/explorer/value +++ b/cdist/conf/type/__sysctl/explorer/value @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/sh -e # # 2014 Steven Armstrong (steven-cdist at armstrong.cc) # @@ -18,5 +18,10 @@ # along with cdist. If not, see . # +if test "$(uname -s)" = NetBSD +then + PATH=$(getconf PATH) +fi + # get the current runtime value -sysctl -n "$__object_id" || true +sysctl -n "${__object_id}" || true diff --git a/cdist/conf/type/__sysctl/gencode-remote b/cdist/conf/type/__sysctl/gencode-remote index 711d54e5..f0f6deef 100755 --- a/cdist/conf/type/__sysctl/gencode-remote +++ b/cdist/conf/type/__sysctl/gencode-remote @@ -44,6 +44,8 @@ case "$os" in flag='-w' ;; netbsd) + # shellcheck disable=SC2016 + echo 'PATH=$(getconf PATH)' flag='-w' ;; freebsd|openbsd) diff --git a/cdist/conf/type/__sysctl/man.rst b/cdist/conf/type/__sysctl/man.rst index 6873003e..dbb9a1ac 100644 --- a/cdist/conf/type/__sysctl/man.rst +++ b/cdist/conf/type/__sysctl/man.rst @@ -26,6 +26,13 @@ EXAMPLES __sysctl net.ipv4.ip_forward --value 1 + # On some operating systems, e.g. NetBSD, to prevent an error if the + # MIB style name does not exist (e.g. optional kernel components), + # name and value can be separated by `?=`. The same effect can be achieved + # in cdist by appending a `?` to the key: + + __sysctl ddb.onpanic? --value -1 + AUTHORS ------- diff --git a/cdist/conf/type/__systemd_service/man.rst b/cdist/conf/type/__systemd_service/man.rst index 7eca398b..cd14c985 100644 --- a/cdist/conf/type/__systemd_service/man.rst +++ b/cdist/conf/type/__systemd_service/man.rst @@ -1,9 +1,10 @@ -cdist-type__systemd-service(7) +cdist-type__systemd_service(7) ============================== NAME ---- -cdist-type__systemd-service - Controls a systemd service state +cdist-type__systemd_service - Controls a systemd service state + DESCRIPTION ----------- @@ -14,11 +15,12 @@ service after configuration applied or shutdown one service. The activation or deactivation is out of scope. Look for the :strong:`cdist-type__systemd_util`\ (7) type instead. + REQUIRED PARAMETERS ------------------- - None. + OPTIONAL PARAMETERS ------------------- @@ -31,12 +33,12 @@ state running Service should run (default) - stoppend - Service should stopped + stopped + Service should be stopped action Executes an action on on the service. It will only execute it if the - service keeps the state **running**. There are following actions, where: + service keeps the state ``running``. There are following actions, where: reload Reloads the service @@ -48,11 +50,12 @@ BOOLEAN PARAMETERS ------------------ if-required - Only execute the action if minimum one required type outputs a message to - **$__messages_out**. Through this, the action should only executed if a + Only execute the action if at minimum one required type outputs a message + to ``$__messages_out``. Through this, the action should only executed if a dependency did something. The action will not executed if no dependencies given. + MESSAGES -------- @@ -68,12 +71,14 @@ restart reload Reloaded the service + ABORTS ------ Aborts in following cases: systemd or the service does not exist + EXAMPLES -------- .. code-block:: sh @@ -95,13 +100,15 @@ EXAMPLES # reload the service for a modified configuration file # only reloads the service if the file really changed - require="__config_file/etc/foo.conf" __systemd_service foo \ + require="__file/etc/foo.conf" __systemd_service foo \ --action reload --if-required + AUTHORS ------- Matthias Stecher + COPYRIGHT --------- Copyright \(C) 2020 Matthias Stecher. You can redistribute it diff --git a/cdist/conf/type/__timezone/gencode-remote b/cdist/conf/type/__timezone/gencode-remote index 5299f548..d8612986 100755 --- a/cdist/conf/type/__timezone/gencode-remote +++ b/cdist/conf/type/__timezone/gencode-remote @@ -22,7 +22,7 @@ # This type allows to configure the desired localtime timezone. timezone_is=$(cat "$__object/explorer/timezone_is") -timezone_should="$__object_id" +timezone_should=$(cat "$__object/parameter/tz") os=$(cat "$__global/explorer/os") if [ "$timezone_is" = "$timezone_should" ]; then @@ -34,3 +34,12 @@ case "$os" in echo "echo \"$timezone_should\" > /etc/timezone" ;; esac + +case "$os" in + openwrt) + cat < +| Steven Armstrong +| Nico Schottelius +| Ramon Salvadó +| Dennis Camera COPYING ------- -Free use of this software is -granted under the terms of the GNU General Public License version 3 (GPLv3). +Copyright \(C) 2012-2020 the `AUTHORS`_. 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. diff --git a/cdist/conf/type/__timezone/manifest b/cdist/conf/type/__timezone/manifest index 3d28ccba..307bec04 100755 --- a/cdist/conf/type/__timezone/manifest +++ b/cdist/conf/type/__timezone/manifest @@ -22,7 +22,7 @@ # # This type allows to configure the desired localtime timezone. -timezone="$__object_id" +timezone=$(cat "$__object/parameter/tz") os=$(cat "$__global/explorer/os") case "$os" in @@ -53,7 +53,10 @@ case "$os" in --file /etc/sysconfig/clock \ --delimiter '=' \ --value "\"$timezone\"" - ;; + ;; + openwrt) + : # Uses gencode-remote + ;; *) echo "Your operating system ($os) is currently not supported by this type (${__type##*/})." >&2 echo "Please contribute an implementation for it if you can." >&2 diff --git a/cdist/conf/type/__timezone/parameter/required b/cdist/conf/type/__timezone/parameter/required new file mode 100644 index 00000000..975445e4 --- /dev/null +++ b/cdist/conf/type/__timezone/parameter/required @@ -0,0 +1 @@ +tz diff --git a/cdist/conf/type/__timezone/singleton b/cdist/conf/type/__timezone/singleton new file mode 100644 index 00000000..e69de29b diff --git a/cdist/conf/type/__uci/explorer/state b/cdist/conf/type/__uci/explorer/state new file mode 100644 index 00000000..d7363dbf --- /dev/null +++ b/cdist/conf/type/__uci/explorer/state @@ -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 . +# +# 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}" - diff --git a/cdist/conf/type/__uci/files/functions.sh b/cdist/conf/type/__uci/files/functions.sh new file mode 100644 index 00000000..277f648c --- /dev/null +++ b/cdist/conf/type/__uci/files/functions.sh @@ -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 + } +) diff --git a/cdist/conf/type/__uci/files/uci_apply.sh b/cdist/conf/type/__uci/files/uci_apply.sh new file mode 100644 index 00000000..63f94290 --- /dev/null +++ b/cdist/conf/type/__uci/files/uci_apply.sh @@ -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 +} diff --git a/cdist/conf/type/__uci/gencode-remote b/cdist/conf/type/__uci/gencode-remote new file mode 100755 index 00000000..70a3d3e0 --- /dev/null +++ b/cdist/conf/type/__uci/gencode-remote @@ -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 . +# + +# 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 diff --git a/cdist/conf/type/__uci/man.rst b/cdist/conf/type/__uci/man.rst new file mode 100644 index 00000000..81a53473 --- /dev/null +++ b/cdist/conf/type/__uci/man.rst @@ -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 + + +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. diff --git a/cdist/conf/type/__uci/manifest b/cdist/conf/type/__uci/manifest new file mode 100755 index 00000000..26920011 --- /dev/null +++ b/cdist/conf/type/__uci/manifest @@ -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 . +# + +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 diff --git a/cdist/conf/type/__uci/nonparallel b/cdist/conf/type/__uci/nonparallel new file mode 100644 index 00000000..e69de29b diff --git a/cdist/conf/type/__uci/parameter/default/state b/cdist/conf/type/__uci/parameter/default/state new file mode 100644 index 00000000..e7f6134f --- /dev/null +++ b/cdist/conf/type/__uci/parameter/default/state @@ -0,0 +1 @@ +present diff --git a/cdist/conf/type/__uci/parameter/optional b/cdist/conf/type/__uci/parameter/optional new file mode 100644 index 00000000..d9080e3a --- /dev/null +++ b/cdist/conf/type/__uci/parameter/optional @@ -0,0 +1,2 @@ +state +type diff --git a/cdist/conf/type/__uci/parameter/optional_multiple b/cdist/conf/type/__uci/parameter/optional_multiple new file mode 100644 index 00000000..6d4e1507 --- /dev/null +++ b/cdist/conf/type/__uci/parameter/optional_multiple @@ -0,0 +1 @@ +value diff --git a/cdist/conf/type/__uci_section/explorer/match b/cdist/conf/type/__uci_section/explorer/match new file mode 100644 index 00000000..0768e404 --- /dev/null +++ b/cdist/conf/type/__uci_section/explorer/match @@ -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 . +# +# 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}" diff --git a/cdist/conf/type/__uci_section/explorer/options b/cdist/conf/type/__uci_section/explorer/options new file mode 100644 index 00000000..e1e60668 --- /dev/null +++ b/cdist/conf/type/__uci_section/explorer/options @@ -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 . +# +# 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 + } + }' diff --git a/cdist/conf/type/__uci_section/explorer/type b/cdist/conf/type/__uci_section/explorer/type new file mode 100644 index 00000000..1675c2e0 --- /dev/null +++ b/cdist/conf/type/__uci_section/explorer/type @@ -0,0 +1,25 @@ +#!/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 . +# +# This explorer retrieves the current section type. + +section=$("${__type_explorer:?}/match") +test -n "${section}" || exit 0 + +uci -s -N get "${section}" 2>/dev/null || true diff --git a/cdist/conf/type/__uci_section/files/functions.sh b/cdist/conf/type/__uci_section/files/functions.sh new file mode 100644 index 00000000..60cb9148 --- /dev/null +++ b/cdist/conf/type/__uci_section/files/functions.sh @@ -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,\}=' "$@" +} diff --git a/cdist/conf/type/__uci_section/files/option_state.awk b/cdist/conf/type/__uci_section/files/option_state.awk new file mode 100644 index 00000000..97cd94fb --- /dev/null +++ b/cdist/conf/type/__uci_section/files/option_state.awk @@ -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 +} diff --git a/cdist/conf/type/__uci_section/files/uci_apply.sh b/cdist/conf/type/__uci_section/files/uci_apply.sh new file mode 120000 index 00000000..4209151f --- /dev/null +++ b/cdist/conf/type/__uci_section/files/uci_apply.sh @@ -0,0 +1 @@ +../../__uci/files/uci_apply.sh \ No newline at end of file diff --git a/cdist/conf/type/__uci_section/gencode-remote b/cdist/conf/type/__uci_section/gencode-remote new file mode 100755 index 00000000..50fdfa4e --- /dev/null +++ b/cdist/conf/type/__uci_section/gencode-remote @@ -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 . +# + +# 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}" + } &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 diff --git a/cdist/conf/type/__uci_section/man.rst b/cdist/conf/type/__uci_section/man.rst new file mode 100644 index 00000000..a0ab78e8 --- /dev/null +++ b/cdist/conf/type/__uci_section/man.rst @@ -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 ``