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..348d15a1
--- /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=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..285659b0
--- /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=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 ``