summaryrefslogtreecommitdiffstats
path: root/libexec
diff options
context:
space:
mode:
Diffstat (limited to 'libexec')
-rwxr-xr-xlibexec/container/auto130
-rwxr-xr-xlibexec/container/console115
-rwxr-xr-xlibexec/container/create277
-rwxr-xr-xlibexec/container/enter125
-rwxr-xr-xlibexec/container/key152
-rwxr-xr-xlibexec/container/limit208
-rwxr-xr-xlibexec/container/list439
-rwxr-xr-xlibexec/container/log143
-rwxr-xr-xlibexec/container/move211
-rwxr-xr-xlibexec/container/remove257
-rwxr-xr-xlibexec/container/restart137
-rwxr-xr-xlibexec/container/run127
-rwxr-xr-xlibexec/container/start531
-rwxr-xr-xlibexec/container/status104
-rwxr-xr-xlibexec/container/stop289
-rwxr-xr-xlibexec/container/top118
-rwxr-xr-xlibexec/container/version48
17 files changed, 3411 insertions, 0 deletions
diff --git a/libexec/container/auto b/libexec/container/auto
new file mode 100755
index 0000000..6c1d47b
--- /dev/null
+++ b/libexec/container/auto
@@ -0,0 +1,130 @@
+#!/bin/sh
+
+# Copyright (C) 2014-2021 Daniel Baumann <daniel.baumann@open-infrastructure.net>
+#
+# SPDX-License-Identifier: GPL-3.0+
+#
+# This program 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.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+set -e
+
+PROJECT="open-infrastructure"
+PROGRAM="container"
+COMMAND="$(basename ${0})"
+
+CONFIG="/etc/${PROJECT}/${PROGRAM}/config"
+HOOKS="/etc/${PROJECT}/${PROGRAM}/hooks"
+
+Parameters ()
+{
+ GETOPT_LONGOPTIONS="force,start,stop,"
+ GETOPT_OPTIONS="f,s,t,"
+
+ PARAMETERS="$(getopt --longoptions ${GETOPT_LONGOPTIONS} --name=${COMMAND} --options ${GETOPT_OPTIONS} --shell sh -- ${@})"
+
+ if [ "${?}" != "0" ]
+ then
+ echo "'${COMMAND}': getopt exit" >&2
+ exit 1
+ fi
+
+ eval set -- "${PARAMETERS}"
+
+ while true
+ do
+ case "${1}" in
+ -f|--force)
+ FORCE="true"
+ shift 1
+ ;;
+
+ -s|--start)
+ ACTION="start"
+ shift 1
+ ;;
+
+ -t|--stop)
+ ACTION="stop"
+ shift 1
+ ;;
+
+ --)
+ shift 1
+ break
+ ;;
+
+ *)
+ echo "'${COMMAND}': getopt error" >&2
+ exit 1
+ ;;
+ esac
+ done
+}
+
+Usage ()
+{
+ echo "Usage: ${PROGRAM} ${COMMAND} -f|--force -s|--start -t|--stop" >&2
+ exit 1
+}
+
+Parameters "${@}"
+
+if [ -z "${ACTION}" ]
+then
+ Usage
+fi
+
+HOST="$(hostname -f 2> /dev/null || hostname)"
+
+# Pre hooks
+for FILE in "${HOOKS}/pre-${COMMAND}".* "${HOOKS}/${NAME}.pre-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
+
+# Run
+OPTIONS=""
+
+case "${FORCE}" in
+ true)
+ OPTIONS="${OPTIONS} -f"
+ ;;
+esac
+
+for FILE in "${CONFIG}"/*.conf
+do
+ if grep -Eqs "^ *cnt.auto=force-true" "${FILE}"
+ then
+ OPTIONS="${OPTIONS} -f"
+ fi
+
+ if grep -Eqs "^ *cnt.auto=(force-true|true)" "${FILE}" && grep -Eqs "^ *cnt.container-server=${HOST}" "${FILE}"
+ then
+ CONTAINER="$(basename ${FILE} .conf)"
+
+ cnt ${ACTION} -n ${CONTAINER} ${OPTIONS} || true
+ fi
+done
+
+# Post hooks
+for FILE in "${HOOKS}/post-${COMMAND}".* "${HOOKS}/${NAME}.post-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
diff --git a/libexec/container/console b/libexec/container/console
new file mode 100755
index 0000000..2dc6546
--- /dev/null
+++ b/libexec/container/console
@@ -0,0 +1,115 @@
+#!/bin/sh
+
+# Copyright (C) 2014-2021 Daniel Baumann <daniel.baumann@open-infrastructure.net>
+#
+# SPDX-License-Identifier: GPL-3.0+
+#
+# This program 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.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+set -e
+
+PROJECT="open-infrastructure"
+PROGRAM="container"
+COMMAND="$(basename ${0})"
+
+HOOKS="/etc/${PROJECT}/${PROGRAM}/hooks"
+MACHINES="/var/lib/machines"
+
+Parameters ()
+{
+ GETOPT_LONGOPTIONS="name:,"
+ GETOPT_OPTIONS="n:,"
+
+ PARAMETERS="$(getopt --longoptions ${GETOPT_LONGOPTIONS} --name=${COMMAND} --options ${GETOPT_OPTIONS} --shell sh -- ${@})"
+
+ if [ "${?}" != "0" ]
+ then
+ echo "'${COMMAND}': getopt exit" >&2
+ exit 1
+ fi
+
+ eval set -- "${PARAMETERS}"
+
+ while true
+ do
+ case "${1}" in
+ -n|--name)
+ NAME="${2}"
+ shift 2
+ ;;
+
+ --)
+ shift 1
+ break
+ ;;
+
+ *)
+ echo "'${COMMAND}': getopt error" >&2
+ exit 1
+ ;;
+ esac
+ done
+}
+
+Usage ()
+{
+ echo "Usage: ${PROGRAM} ${COMMAND} -n|--name NAME" >&2
+ exit 1
+}
+
+Parameters "${@}"
+
+if [ -z "${NAME}" ]
+then
+ Usage
+fi
+
+if [ ! -e "${MACHINES}/${NAME}" ]
+then
+ echo "'${NAME}': no such container" >&2
+ exit 1
+fi
+
+STATE="$(machinectl show ${NAME} 2>&1 | awk -FState= '/^State=/ { print $2 }')"
+
+case "${STATE}" in
+ running)
+ ;;
+
+ *)
+ echo "'${NAME}': container is not started" >&2
+ exit 1
+ ;;
+esac
+
+# Pre hooks
+for FILE in "${HOOKS}/pre-${COMMAND}".* "${HOOKS}/${NAME}.pre-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
+
+# Run
+machinectl login ${NAME}
+
+# Post hooks
+for FILE in "${HOOKS}/post-${COMMAND}".* "${HOOKS}/${NAME}.post-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
diff --git a/libexec/container/create b/libexec/container/create
new file mode 100755
index 0000000..bdf9fb4
--- /dev/null
+++ b/libexec/container/create
@@ -0,0 +1,277 @@
+#!/bin/sh
+
+# Copyright (C) 2014-2021 Daniel Baumann <daniel.baumann@open-infrastructure.net>
+#
+# SPDX-License-Identifier: GPL-3.0+
+#
+# This program 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.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+set -e
+
+PROJECT="open-infrastructure"
+PROGRAM="container"
+COMMAND="$(basename ${0})"
+
+CONFIG="/etc/${PROJECT}/${PROGRAM}/config"
+HOOKS="/etc/${PROJECT}/${PROGRAM}/hooks"
+MACHINES="/var/lib/machines"
+SCRIPTS="/usr/share/${PROJECT}/${PROGRAM}/scripts"
+CONFIG_TEMPLATE="/usr/share/${PROJECT}/${PROGRAM}/config/container.conf.in"
+
+Parameters ()
+{
+ GETOPT_LONGOPTIONS="name:,cnt.container-server:,cnt.overlay:,cnt.overlay-options:,bind:,bind-ro:,capability:,drop-capability:,script:,verbose,"
+ GETOPT_OPTIONS="n:,b:,c:,d:,s:,v,"
+
+ PARAMETERS="$(getopt --longoptions ${GETOPT_LONGOPTIONS} --name=${COMMAND} --options ${GETOPT_OPTIONS} --shell sh -- ${@})"
+
+ if [ "${?}" != "0" ]
+ then
+ echo "'${COMMAND}': getopt exit" >&2
+ exit 1
+ fi
+
+ eval set -- "${PARAMETERS}"
+
+ while true
+ do
+ case "${1}" in
+ -n|--name)
+ NAME="${2}"
+ shift 2
+ ;;
+
+ --cnt.auto)
+ CNT_AUTO="${2}"
+ shift 2
+ ;;
+
+ --cnt.container-server)
+ CNT_CONTAINER_SERVER="${2}"
+ shift 2
+ ;;
+
+ --cnt.overlay)
+ CNT_OVERLAY="${2}"
+ shift 2
+ ;;
+
+ --cnt.overlay-options)
+ CNT_OVERLAY_OPTIONS="${2}"
+ shift 2
+ ;;
+
+ -b|--bind)
+ BIND="${2}"
+ shift 2
+ ;;
+
+ --bind-ro)
+ BIND_RO="${2}"
+ shift 2
+ ;;
+
+ -c|--capability)
+ CAPABILITY="${2}"
+ shift 2
+ ;;
+
+ -d|--drop-capability)
+ DROP_CAPABILITY="${2}"
+ shift 2
+ ;;
+
+ -s|--script)
+ SCRIPT="${2}"
+ shift 2
+ ;;
+
+ -v|--verbose)
+ VERBOSE="true"
+ shift 1
+ ;;
+
+ --)
+ shift 1
+ break
+ ;;
+
+ *)
+ echo "'${COMMAND}': getopt error" >&2
+ exit 1
+ ;;
+ esac
+ done
+}
+
+Usage ()
+{
+ echo "Usage: ${PROGRAM} ${COMMAND} -n|--name NAME [--cnt.container-server=true|false|FQDN] [--cnt.overlay=DIRECTORY_LOWER:DIRECTORY_UPPER:DIRECTORY_WORK:DIRECTORY_MERGED] [--cnt.overlay-options=OPTION[,OPTION]] [-b|--bind DIRECTORY:DIRECTORY[:OPTIONS]] [--bind-ro DIRECTORY:DIRECTORY[:OPTIONS]] [-c|--capability CAPABILITY[,CAPABILITY]] [-d|--drop-capability DROP_CAPABILITY[,DROP_CAPABILITY]] [-s|--script SCRIPT] [-v|--verbose] [-- SCRIPT_OPTIONS]" >&2
+ exit 1
+}
+
+Parameters "${@}"
+
+if [ -z "${NAME}" ]
+then
+ Usage
+fi
+
+case "${NAME}" in
+ ALL)
+ echo "'${NAME}': name 'ALL' is reserved to expand to all available container" >&2
+ exit 1
+ ;;
+esac
+
+if [ -e "${CONFIG}/${NAME}.conf" ]
+then
+ echo "'${NAME}': container already exists or ${CONFIG}/${NAME}.conf has not been removed" >&2
+ exit 1
+fi
+
+if [ -z "${SCRIPT}" ]
+then
+ if [ -e "${SCRIPTS}/default" ]
+ then
+ TARGET="$(basename $(readlink ${SCRIPTS}/default))"
+
+ case "${TARGET}" in
+ container_script)
+ TARGET="$(basename $(readlink /etc/alternatives/container_script))"
+ ;;
+ esac
+
+ if [ -e "${SCRIPTS}/${TARGET}" ]
+ then
+ SCRIPT="${TARGET}"
+ else
+ echo "default -> '${TARGET}': no such script" >&2
+ exit 1
+ fi
+ else
+ SCRIPT="debian"
+ fi
+else
+ if [ ! -e "${SCRIPTS}/${SCRIPT}" ]
+ then
+ echo "'${SCRIPT}': no such script" >&2
+ exit 1
+ fi
+fi
+
+case "${VERBOSE}" in
+ true)
+
+cat << EOF
+################################################################################
+Creating container: ${NAME}
+################################################################################
+EOF
+
+ ;;
+esac
+
+CNT_CONTAINER_SERVER="${CNT_CONTAINER_SERVER:-$(hostname -f 2> /dev/null || hostname)}"
+
+# Pre hooks
+for FILE in "${HOOKS}/pre-${COMMAND}".* "${HOOKS}/${NAME}.pre-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
+
+# Creating rw bind mounts
+if [ -n "${BIND}" ]
+then
+ BINDS="$(echo ${BIND} | sed -e 's|;| |g')"
+
+ for ENTRY in ${BINDS}
+ do
+ DIRECTORY="$(echo ${ENTRY} | awk -F: '{ print $1 }')"
+
+ mkdir -p "${DIRECTORY}"
+ done
+fi
+
+# Creating ro bind mounts
+if [ -n "${BIND_RO}" ]
+then
+ BINDS_RO="$(echo ${BIND_RO} | sed -e 's|;| |g')"
+
+ for ENTRY in ${BINDS_RO}
+ do
+ DIRECTORY="$(echo ${ENTRY} | awk -F: '{ print $1 }')"
+
+ mkdir -p "${DIRECTORY}"
+ done
+fi
+
+# Creating overlay mounts
+if [ -n "${CNT_OVERLAY}" ]
+then
+ CNT_OVERLAYS="$(echo ${CNT_OVERLAY} | sed -e 's|;| |g')"
+
+ for ENTRY in ${CNT_OVERLAYS}
+ do
+ DIRECTORY_LOWER="$(echo ${ENTRY} | awk -F: '{ print $1 }')"
+ DIRECTORY_UPPER="$(echo ${ENTRY} | awk -F: '{ print $2 }')"
+ DIRECTORY_WORK="$(echo ${ENTRY} | awk -F: '{ print $3 }')"
+ DIRECTORY_MERGED="$(echo ${ENTRY} | awk -F: '{ print $4 }')"
+
+ for DIRECTORY in "${DIRECTORY_LOWER}" "${DIRECTORY_UPPER}" "${DIRECTORY_WORK}" "${DIRECTORY_MERGED}"
+ do
+ mkdir -p "${DIRECTORY}"
+ done
+ done
+fi
+
+# config
+mkdir -p "${CONFIG}"
+
+sed -e "s|@CNT_AUTO@|${CNT_AUTO}|g" \
+ -e "s|@CNT_CONTAINER_SERVER@|${CNT_CONTAINER_SERVER}|g" \
+ -e "s|@CNT_NETWORK_BRIDGE@|${CNT_NETWORK_BRIDGE}|g" \
+ -e "s|@CNT_OVERLAY@|${CNT_OVERLAY}|g" \
+ -e "s|@CNT_OVERLAY_OPTIONS@|${CNT_OVERLAY_OPTIONS}|g" \
+ -e "s|@NAME@|${NAME}|g" \
+ -e "s|@BIND@|${BIND}|g" \
+ -e "s|@BIND_RO@|${BIND_RO}|g" \
+ -e "s|@BOOT@|yes|g" \
+ -e "s|@CAPABILITY@|${CAPABILITY}|g" \
+ -e "s|@DIRECTORY@|${MACHINES}/${NAME}|g" \
+ -e "s|@DROP_CAPABILITY@|${DROP_CAPABILITY}|g" \
+ -e "s|@LINK_JOURNAL@|no|g" \
+ -e "s|@MACHINE@|${NAME}|g" \
+ -e "s|@NETWORK_VETH_EXTRA@|${NETWORK_VETH_EXTRA}|g" \
+ -e "s|@PRIVATE_USERS@|no|g" \
+ -e "s|@REGISTER@|yes|g" \
+"${CONFIG_TEMPLATE}" > "${CONFIG}/${NAME}.conf"
+
+# Run
+"${SCRIPTS}/${SCRIPT}" $(echo "${@}" | sed -e 's| -- | |')
+
+# Post hooks
+for FILE in "${HOOKS}/post-${COMMAND}".* "${HOOKS}/${NAME}.post-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
+
+# done
+echo "'${NAME}': container created."
diff --git a/libexec/container/enter b/libexec/container/enter
new file mode 100755
index 0000000..fed1193
--- /dev/null
+++ b/libexec/container/enter
@@ -0,0 +1,125 @@
+#!/bin/sh
+
+# Copyright (C) 2014-2021 Daniel Baumann <daniel.baumann@open-infrastructure.net>
+#
+# SPDX-License-Identifier: GPL-3.0+
+#
+# This program 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.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+set -e
+
+PROJECT="open-infrastructure"
+PROGRAM="container"
+COMMAND="$(basename ${0})"
+
+HOOKS="/etc/${PROJECT}/${PROGRAM}/hooks"
+MACHINES="/var/lib/machines"
+
+Parameters ()
+{
+ GETOPT_LONGOPTIONS="name:,"
+ GETOPT_OPTIONS="n:,"
+
+ PARAMETERS="$(getopt --longoptions ${GETOPT_LONGOPTIONS} --name=${COMMAND} --options ${GETOPT_OPTIONS} --shell sh -- ${@})"
+
+ if [ "${?}" != "0" ]
+ then
+ echo "'${COMMAND}': getopt exit" >&2
+ exit 1
+ fi
+
+ eval set -- "${PARAMETERS}"
+
+ while true
+ do
+ case "${1}" in
+ -n|--name)
+ NAME="${2}"
+ shift 2
+ ;;
+
+ --)
+ shift 1
+ break
+ ;;
+
+ *)
+ echo "'${COMMAND}': getopt error" >&2
+ exit 1
+ ;;
+ esac
+ done
+}
+
+Usage ()
+{
+ echo "Usage: ${PROGRAM} ${COMMAND} -n|--name NAME" >&2
+ exit 1
+}
+
+Parameters "${@}"
+
+if [ -z "${NAME}" ]
+then
+ Usage
+fi
+
+if [ ! -e "${MACHINES}/${NAME}" ]
+then
+ echo "'${NAME}': no such container" >&2
+ exit 1
+fi
+
+STATE="$(machinectl show ${NAME} 2>&1 | awk -FState= '/^State=/ { print $2 }')"
+
+case "${STATE}" in
+ running)
+ ;;
+
+ *)
+ echo "'${NAME}': container is not running" >&2
+ exit 1
+ ;;
+esac
+
+LEADER="$(machinectl status ${NAME} | awk '/Leader: / { print $2 }')"
+
+# Pre hooks
+for FILE in "${HOOKS}/pre-${COMMAND}".* "${HOOKS}/${NAME}.pre-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
+
+SSH_CLIENT="${SSH_CLIENT:-127.0.0.1 0 0}"
+
+# Run
+nsenter --all --target "${LEADER}" --wd="${MACHINES}/${NAME}/root" /usr/bin/script -c "LC_ALL=C.UTF-8 /bin/bash -l" -q /dev/null
+
+case "${SSH_CLIENT}" in
+ 127.0.0.1*)
+ unset SSH_CLIENT
+ ;;
+esac
+
+# Post hooks
+for FILE in "${HOOKS}/post-${COMMAND}".* "${HOOKS}/${NAME}.post-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
diff --git a/libexec/container/key b/libexec/container/key
new file mode 100755
index 0000000..4cbdaa0
--- /dev/null
+++ b/libexec/container/key
@@ -0,0 +1,152 @@
+#!/bin/sh
+
+# Copyright (C) 2014-2021 Daniel Baumann <daniel.baumann@open-infrastructure.net>
+#
+# SPDX-License-Identifier: GPL-3.0+
+#
+# This program 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.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+set -e
+
+PROJECT="open-infrastructure"
+PROGRAM="container"
+COMMAND="$(basename ${0})"
+
+KEYS="/etc/${PROJECT}/${PROGRAM}/keys"
+
+Parameters ()
+{
+ GETOPT_LONGOPTIONS="add:,list,remove:,"
+ GETOPT_OPTIONS="a:,l,r:,"
+
+ PARAMETERS="$(getopt --longoptions ${GETOPT_LONGOPTIONS} --name=${COMMAND} --options ${GETOPT_OPTIONS} --shell sh -- ${@})"
+
+ if [ "${?}" != "0" ]
+ then
+ echo "'${COMMAND}': getopt exit" >&2
+ exit 1
+ fi
+
+ eval set -- "${PARAMETERS}"
+
+ while true
+ do
+ case "${1}" in
+ -a|--add)
+ ADD="${2}"
+ ACTION="add"
+ shift 2
+ ;;
+
+ -l|--list)
+ ACTION="list"
+ shift 1
+ ;;
+
+ -r|--remove)
+ REMOVE="${2}"
+ ACTION="remove"
+ shift 2
+ ;;
+
+ --)
+ shift 1
+ break
+ ;;
+
+ *)
+ echo "'${COMMAND}': getopt error" >&2
+ exit 1
+ ;;
+ esac
+ done
+}
+
+Usage ()
+{
+ echo "Usage: ${PROGRAM} ${COMMAND} [-a|--add KEY] [-l|--list] [-r|--remove KEY]" >&2
+ exit 1
+}
+
+Parameters "${@}"
+
+if [ -z "${ACTION}" ]
+then
+ Usage
+fi
+
+# Pre hooks
+for FILE in "${HOOKS}/pre-${COMMAND}".* "${HOOKS}/${NAME}.pre-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
+
+# Run
+if [ ! -e "${KEYS}" ]
+then
+ mkdir -p "${KEYS}"
+
+ chown root:root "${KEYS}"
+ chmod 0700 "${KEYS}"
+
+cat > "${KEYS}/gnupg.conf" << EOF
+keyserver hkps://hkps.pool.sks-keyservers.net
+keyserver-options include-revoked
+keyserver-options no-honor-keyserver-url
+
+cert-digest-algo SHA512
+default-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES ZLIB ZIP Uncompressed
+personal-cipher-preferences AES256 AES192 AES
+personal-compress-preferences ZLIB ZIP Uncompressed
+personal-digest-preferences SHA512 SHA384 SHA256 SHA224
+
+no-comments
+no-emit-version
+no-greeting
+keyid-format 0xlong
+list-options show-keyring
+list-options show-uid-validity
+verify-options show-uid-validity
+with-fingerprint
+
+charset utf-8
+EOF
+
+fi
+
+case "${ACTION}" in
+ add)
+ gpg --homedir "${KEYS}" --import "${ADD}"
+ ;;
+
+ list)
+ gpg --homedir "${KEYS}" --list-keys
+ ;;
+
+ remove)
+ gpg --homedir "${KEYS}" --delete-keys "${REMOVE}"
+ ;;
+esac
+
+# Post hooks
+for FILE in "${HOOKS}/post-${COMMAND}".* "${HOOKS}/${NAME}.post-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
diff --git a/libexec/container/limit b/libexec/container/limit
new file mode 100755
index 0000000..99ef03a
--- /dev/null
+++ b/libexec/container/limit
@@ -0,0 +1,208 @@
+#!/bin/sh
+
+# Copyright (C) 2014-2021 Daniel Baumann <daniel.baumann@open-infrastructure.net>
+#
+# SPDX-License-Identifier: GPL-3.0+
+#
+# This program 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.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+set -e
+
+PROJECT="open-infrastructure"
+PROGRAM="container"
+COMMAND="$(basename ${0})"
+
+HOOKS="/etc/${PROJECT}/${PROGRAM}/hooks"
+MACHINES="/var/lib/machines"
+
+Parameters ()
+{
+ GETOPT_LONGOPTIONS="name:,blockio-device-weight:,blockio-read-bandwidth:,blockio-weight:,blockio-write-bandwidth:,cpu-quota:,cpu-shares:,memory-limit:,tasks-max:,"
+ GETOPT_OPTIONS="n:b:c:m:t:,"
+
+ PARAMETERS="$(getopt --longoptions ${GETOPT_LONGOPTIONS} --name=${COMMAND} --options ${GETOPT_OPTIONS} --shell sh -- ${@})"
+
+ if [ "${?}" != "0" ]
+ then
+ echo "'${COMMAND}': getopt exit" >&2
+ exit 1
+ fi
+
+ eval set -- "${PARAMETERS}"
+
+ while true
+ do
+ case "${1}" in
+ -n|--name)
+ NAME="${2}"
+ shift 2
+ ;;
+
+ -c|--cpu-quota)
+ CPU_QUOTA="${2}"
+ shift 2
+ ;;
+
+ --cpu-shares)
+ CPU_SHARES="${2}"
+ shift 2
+ ;;
+
+ -m|--memory-limit)
+ MEMORY_LIMIT="${2}"
+ shift 2
+ ;;
+
+ -t|--tasks-max)
+ TASKS_MAX="${2}"
+ shift 2
+ ;;
+
+ --blockio-device-weight)
+ BLOCK_IO_DEVICE_WEIGHT="${2}"
+ shift 2
+ ;;
+
+ --blockio-read-bandwidth)
+ BLOCK_IO_READ_BANDWIDTH="${2}"
+ shift 2
+ ;;
+
+ -b|--blockio-weight)
+ BLOCK_IO_WEIGHT="${2}"
+ shift 2
+ ;;
+
+ --blockio-write-bandwidth)
+ BLOCK_IO_WRITE_BANDWIDTH="${2}"
+ shift 2
+ ;;
+
+ --)
+ shift 1
+ break
+ ;;
+
+ *)
+ echo "'${COMMAND}': getopt error" >&2
+ exit 1
+ ;;
+ esac
+ done
+}
+
+Usage ()
+{
+ echo "Usage: ${PROGRAM} ${COMMAND} -n|--name NAME [--blockio-device-weight \"DEVICE WEIGHT\"] [--blockio-read-bandwidth \"DEVICE BYTES\"] [-b|--blockio-weight WEIGHT] [--blockio-write-bandwidth \"DEVICE BYTES\"] [-c|--cpu-quota QUOTA] [--cpu-shares SHARES] [-m|--memory-limit BYTES] [-t|--tasks-max NUMBER]" >&2
+ exit 1
+}
+
+Parameters "${@}"
+
+if [ -z "${NAME}" ]
+then
+ Usage
+fi
+
+if [ ! -e "${MACHINES}/${NAME}" ]
+then
+ echo "'${NAME}': no such container" >&2
+ exit 1
+fi
+
+STATE="$(machinectl show ${NAME} 2>&1 | awk -FState= '/^State=/ { print $2 }')"
+
+case "${STATE}" in
+ running)
+ ;;
+
+ *)
+ echo "'${NAME}': container is not running" >&2
+ exit 1
+ ;;
+esac
+
+if [ -n "${BLOCK_IO_DEVICE_WEIGHT}" ]
+then
+ BLOCK_IO_DEVICE_WEIGHT="BlockIODeviceWeight=${BLOCK_IO_DEVICE_WEIGHT}"
+ SET_PROPERTY="true"
+fi
+
+if [ -n "${BLOCK_IO_READ_BANDWIDTH}" ]
+then
+ BLOCK_IO_READ_BANDWIDTH="BlockIOReadBandwidth=${BLOCK_IO_READ_BANDWIDTH}"
+ SET_PROPERTY="true"
+fi
+
+if [ -n "${BLOCK_IO_WEIGHT}" ]
+then
+ BLOCK_IO_WEIGHT="BlockIOWeight=${BLOCK_IO_WEIGHT}"
+ SET_PROPERTY="true"
+fi
+
+if [ -n "${BLOCK_IO_WRITE_BANDWIDTH}" ]
+then
+ BLOCK_IO_WRITE_BANDWIDTH="BlockIOReadBandwidth=${BLOCK_IO_WRITE_BANDWIDTH}"
+ SET_PROPERTY="true"
+fi
+
+if [ -n "${CPU_QUOTA}" ]
+then
+ CPU_QUOTA="CPUQuota=${CPU_QUOTA}"
+ SET_PROPERTY="true"
+fi
+
+if [ -n "${CPU_SHARES}" ]
+then
+ CPU_SHARES="CPUShares=${CPU_SHARES}"
+ SET_PROPERTY="true"
+fi
+
+if [ -n "${MEMORY_LIMIT}" ]
+then
+ MEMORY_LIMIT="MemoryLimit=${MEMORY_LIMIT}"
+ SET_PROPERTY="true"
+fi
+
+if [ -n "${TASKS_MAX}" ]
+then
+ TASKS_MAX="TasksMax=${TASKS_MAX}"
+ SET_PROPERTY="true"
+fi
+
+if [ "${SET_PROPERTY}" != "true" ]
+then
+ Usage
+fi
+
+# Pre hooks
+for FILE in "${HOOKS}/pre-${COMMAND}".* "${HOOKS}/${NAME}.pre-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
+
+# Run
+systemctl --runtime set-property ${NAME} ${BLOCK_IO_DEVICE_WEIGHT} ${BLOCK_IO_READ_BANDWIDTH} ${BLOCK_IO_WEIGHT} ${BLOCK_IO_WRITE_BANDWIDTH} ${CPU_QUOTA} ${CPU_SHARES} ${MEMORY_LIMIT} ${TASKS_MAX}
+
+# Post hooks
+for FILE in "${HOOKS}/post-${COMMAND}".* "${HOOKS}/${NAME}.post-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
diff --git a/libexec/container/list b/libexec/container/list
new file mode 100755
index 0000000..ba794ab
--- /dev/null
+++ b/libexec/container/list
@@ -0,0 +1,439 @@
+#!/bin/sh
+
+# Copyright (C) 2014-2021 Daniel Baumann <daniel.baumann@open-infrastructure.net>
+#
+# SPDX-License-Identifier: GPL-3.0+
+#
+# This program 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.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+set -e
+
+PROJECT="open-infrastructure"
+PROGRAM="container"
+COMMAND="$(basename ${0})"
+
+CONFIG="/etc/${PROJECT}/${PROGRAM}/config"
+HOOKS="/etc/${PROJECT}/${PROGRAM}/hooks"
+MACHINES="/var/lib/machines"
+
+VERSION="$(${PROGRAM} version)"
+
+Parameters ()
+{
+ GETOPT_LONGOPTIONS="all,csv-separator:,format:,host:,nwdiag-color:,nwdiag-label:,other,started,stopped,"
+ GETOPT_OPTIONS="a,f:,h:,o,s,t,"
+
+ PARAMETERS="$(getopt --longoptions ${GETOPT_LONGOPTIONS} --name=${COMMAND} --options ${GETOPT_OPTIONS} --shell sh -- ${@})"
+
+ if [ "${?}" != "0" ]
+ then
+ echo "'${COMMAND}': getopt exit" >&2
+ exit 1
+ fi
+
+ eval set -- "${PARAMETERS}"
+
+ while true
+ do
+ case "${1}" in
+ -a|--all)
+ LIST="${LIST} all"
+ shift 1
+ ;;
+
+ --csv-separator)
+ CSV_SEPARATOR="${2}"
+ shift 2
+ ;;
+
+ -f|--format)
+ FORMAT="${2}"
+ shift 2
+ ;;
+
+ -h|--host)
+ HOST="${2}"
+ shift 2
+ ;;
+
+ --nwdiag-color)
+ NWDIAG_COLOR="${2}"
+ shift 2
+ ;;
+
+ --nwdiag-label)
+ NWDIAG_LABEL="${2}"
+ shift 2
+ ;;
+
+ -o|--other)
+ LIST="${LIST} other"
+ shift 1
+ ;;
+
+ -s|--started)
+ LIST="${LIST} started"
+ shift 1
+ ;;
+
+ -t|--stopped)
+ LIST="${LIST} stopped"
+ shift 1
+ ;;
+
+ --)
+ shift 1
+ break
+ ;;
+
+ *)
+ echo "'${COMMAND}': getopt error" >&2
+ exit 1
+ ;;
+ esac
+ done
+}
+
+Usage ()
+{
+ echo "Usage: ${PROGRAM} ${COMMAND} [-a|--all] [--csv-separator SEPARATOR] [--format FORMAT] [-h|--host HOSTNAME] [--nwdiag-color COLOR] [--nwdiag-label LABEL] [-o|--other] [-s|--started] [-t|--stopped]" >&2
+ exit 1
+}
+
+Parameters "${@}"
+
+LIST="${LIST:-started stopped}"
+FORMAT="${FORMAT:-cli}"
+HOST="${HOST:-$(hostname -f 2> /dev/null || hostname)}"
+
+CSV_SEPARATOR="${CSV_SEPARATOR:-,}"
+
+# Pre hooks
+for FILE in "${HOOKS}/pre-${COMMAND}".* "${HOOKS}/${NAME}.pre-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
+
+# Run
+List ()
+{
+ case "${FORMAT}" in
+ cli)
+ printf "%1s %-74s %-19s\n" "${STATUS}" "${BLUE}${CONTAINER}${NORMAL}" "${YELLOW}${ADDRESS}${NORMAL}"
+ ;;
+
+ csv)
+ echo "${HOST}${CSV_SEPARATOR}${CONTAINER}${CSV_SEPARATOR}${STATE}${CSV_SEPARATOR}${ADDRESS}"
+ ;;
+
+ json)
+
+cat << EOF
+ {
+ "name": "${CONTAINER}",
+ "status": "${STATE}",
+ "address": "${ADDRESS}",
+ },
+EOF
+
+ ;;
+
+ nwdiag)
+ Nwdiag_width "${CONTAINER}"
+
+ case "${STATE}" in
+ started)
+ COLOR="#73d216"
+ ;;
+
+ stopped)
+ COLOR="#cc0000"
+ ;;
+
+ other)
+ COLOR="#d3d7cf"
+ ;;
+ esac
+
+ echo " ${CONTAINER} [address = \"${ADDRESS}\", color = \"${COLOR}\", shape = \"flowchart.terminator\", width = \"${WIDTH}\"];"
+ ;;
+
+ shell|sh)
+ echo "${CONTAINER}"
+ ;;
+
+ yaml)
+
+cat << EOF
+ - name: ${CONTAINER}
+ address: ${ADDRESS}
+EOF
+
+ ;;
+
+ xml)
+
+cat << EOF
+ <container>
+ <name>${CONTAINER}</name>
+ <address>${ADDRESS}</address>
+ </container>
+EOF
+
+ ;;
+ esac
+}
+
+Nwdiag_width ()
+{
+ NAME="${1}"
+
+ CHARACTERS="$(echo "${NAME}" | wc -c)"
+
+ if [ "${CHARACTERS}" -gt 13 ]
+ then
+ # default width is 128 fitting 13 characters
+ WIDTH="$(( ${CHARACTERS} - 13 ))"
+ WIDTH="$(( ${WIDTH} * 5 ))"
+ WIDTH="$(( ${WIDTH} + 128 ))"
+ else
+ WIDTH="128"
+ fi
+}
+
+case "${FORMAT}" in
+ cli)
+ RED="$(tput setaf 1)$(tput bold)"
+ GREEN="$(tput setaf 2)$(tput bold)"
+ YELLOW="$(tput setaf 3)$(tput bold)"
+ BLUE="$(tput setaf 4)$(tput bold)"
+ WHITE="$(tput setaf 7)$(tput bold)"
+ NORMAL="$(tput sgr0)"
+
+cat << EOF
+${WHITE} ${NORMAL} Container IP Address
+--------------------------------------------------------------------------------
+EOF
+
+ ;;
+
+ csv)
+
+cat << EOF
+# compute-tools ${VERSION}
+Host${CSV_SEPARATOR}Container${CSV_SEPARATOR}Status${CSV_SEPARATOR}IPv4-Address
+EOF
+
+ ;;
+
+ json)
+
+cat << EOF
+{
+ "compute-tools": {
+ "version": "${VERSION}",
+ },
+ "host": {
+ "name": "${HOST}",
+ },
+ "container": [
+EOF
+
+ ;;
+
+ nwdiag)
+ NETWORK="$(echo ${HOST} | sed -e 's|\.|_|g')"
+
+cat << EOF
+# compute-tools ${VERSION}
+nwdiag {
+ external_connector = none;
+ network ${NETWORK} {
+EOF
+
+ if [ -n "${NWDIAG_LABEL}" ]
+ then
+ echo " label = \"${NWDIAG_LABEL}\""
+ else
+ echo " label = \"\""
+ fi
+
+ NUMBER="$(${0} --format=short | wc -l)"
+
+ if [ -n "${NWDIAG_COLOR}" ]
+ then
+ COLOR="${NWDIAG_COLOR}"
+ else
+ COLOR="#3465a4"
+ fi
+
+ Nwdiag_width "${HOST}"
+
+cat << EOF
+ # host
+ ${HOST} [color = "${COLOR}", shape = "box", numbered = "${NUMBER}", width = "${WIDTH}"];
+ # container
+EOF
+
+ ;;
+
+ yaml)
+
+cat << EOF
+---
+compute_tools:
+ version: ${VERSION}
+
+host:
+ name: ${HOST}
+
+container:
+EOF
+
+ ;;
+
+ xml)
+
+cat << EOF
+<compute-tools>
+ <version>${VERSION}</version>
+</compute-tools>
+<host>
+ <name>${HOST}</name>
+</host>
+<containers>
+EOF
+
+ ;;
+esac
+
+if ls "${MACHINES}"/* > /dev/null 2>&1
+then
+ CONTAINERS="$(cd "${MACHINES}" 2>/dev/null && find -maxdepth 1 \( -type d -or -type l \) -and -not -name 'lost+found' -printf '%P\n' | sort)"
+fi
+
+for CONTAINER in ${CONTAINERS}
+do
+ STATE="$(machinectl show ${CONTAINER} 2>&1 | awk -FState= '/^State=/ { print $2 }')"
+
+ if [ -e "${CONFIG}/${CONTAINER}.conf" ]
+ then
+ CONTAINER_SERVER="$(awk -Fcnt.container-server= '/^cnt.container-server=/ { print $2 }' ${CONFIG}/${CONTAINER}.conf)"
+ CONTAINER_SERVER="${CONTAINER_SERVER:-false}"
+
+ case "${CONTAINER_SERVER}" in
+ ${HOST}|true)
+ ;;
+
+ *)
+ STATE="other"
+ ;;
+ esac
+ else
+ STATE="other"
+ fi
+
+ case "${STATE}" in
+ running)
+ STATE="started"
+ STATUS="${GREEN}â—Ź${NORMAL}"
+ ;;
+
+ other)
+ STATUS="${WHITE}â—‹${NORMAL}"
+ ;;
+
+ *)
+ STATE="stopped"
+ STATUS="${RED}â—Ź${NORMAL}"
+ ;;
+ esac
+
+ ADDRESS=""
+
+ if ls "${MACHINES}/${CONTAINER}/etc/systemd/network"/*.network > /dev/null 2>&1
+ then
+ ADDRESS="$(awk -FAddress= '/^Address/ { print $2 }' ${MACHINES}/${CONTAINER}/etc/systemd/network/*.network | head -n1)"
+ elif [ -e "${MACHINES}/${CONTAINER}/etc/network/interfaces" ]
+ then
+ ADDRESS="$(awk '/address/ { print $2 }' ${MACHINES}/${CONTAINER}/etc/network/interfaces | head -n1)"
+ fi
+
+ ADDRESS="${ADDRESS:-n/a}"
+
+ if echo ${LIST} | grep -qs all
+ then
+ List
+ fi
+
+ for ITEM in other started stopped
+ do
+ if echo ${LIST} | grep -qs ${ITEM}
+ then
+ case "${STATE}" in
+ ${ITEM})
+ List
+ ;;
+ esac
+ fi
+ done
+done
+
+case "${FORMAT}" in
+ cli)
+
+cat << EOF
+--------------------------------------------------------------------------------
+EOF
+
+ ;;
+
+ json)
+
+cat << EOF
+ ],
+}
+EOF
+
+ ;;
+
+ nwdiag)
+
+cat << EOF
+ }
+}
+EOF
+
+ ;;
+
+ xml)
+
+cat << EOF
+</containers>
+EOF
+
+ ;;
+esac
+
+# Post hooks
+for FILE in "${HOOKS}/post-${COMMAND}".* "${HOOKS}/${NAME}.post-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
diff --git a/libexec/container/log b/libexec/container/log
new file mode 100755
index 0000000..a18c421
--- /dev/null
+++ b/libexec/container/log
@@ -0,0 +1,143 @@
+#!/bin/sh
+
+# Copyright (C) 2014-2021 Daniel Baumann <daniel.baumann@open-infrastructure.net>
+#
+# SPDX-License-Identifier: GPL-3.0+
+#
+# This program 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.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+set -e
+
+PROJECT="open-infrastructure"
+PROGRAM="container"
+COMMAND="$(basename ${0})"
+
+LOG="/var/log/${PROJECT}/${PROGRAM}.log"
+
+Parameters ()
+{
+ GETOPT_LONGOPTIONS="name:,date:,"
+ GETOPT_OPTIONS="n:,d:,"
+
+ PARAMETERS="$(getopt --longoptions ${GETOPT_LONGOPTIONS} --name=${COMMAND} --options ${GETOPT_OPTIONS} --shell sh -- ${@})"
+
+ if [ "${?}" != "0" ]
+ then
+ echo "'${COMMAND}': getopt exit" >&2
+ exit 1
+ fi
+
+ eval set -- "${PARAMETERS}"
+
+ while true
+ do
+ case "${1}" in
+ -d|--date)
+ DATE="${2}"
+ shift 2
+ ;;
+
+ -n|--name)
+ NAME="${2}"
+ shift 2
+ ;;
+
+ -u|--user)
+ USER="${2}"
+ shift 2
+ ;;
+
+ --)
+ shift 1
+ break
+ ;;
+
+ *)
+ echo "'${COMMAND}': getopt error" >&2
+ exit 1
+ ;;
+ esac
+ done
+}
+
+Usage ()
+{
+ echo "Usage: ${PROGRAM} ${COMMAND} [-n|--name NAME] [-d|--date DATE|today|today-N|yesterday] [-u|--user USER]" >&2
+ exit 1
+}
+
+Parameters "${@}"
+
+case "${DATE}" in
+ today-*)
+ DAYS="$(echo ${DATE} | awk -F- '{ print $2 }')"
+
+ START="$(date -d "${DAYS} day ago" '+%Y-%m-%d')"
+ END="$(date +%Y-%m-%d)"
+
+ DATE="(${START}"
+
+ while true
+ do
+ DATE="${DATE}|${START}"
+
+ if [ "${START}" = "${END}" ]
+ then
+ break
+ fi
+
+ START="$(date +%Y-%m-%d --date "${START} +1 day")"
+ done
+
+ DATE="${DATE})"
+ ;;
+
+ today)
+ DATE="$(date +%Y-%m-%d)"
+ ;;
+
+ yesterday)
+ DATE="$(date -d '1 day ago' '+%Y-%m-%d')"
+ ;;
+esac
+
+if ls "${LOG}"-*.gz > /dev/null 2>&1
+then
+ LOGS="$(ls ${LOG}-*.gz) ${LOG}"
+else
+ LOGS="${LOG}"
+fi
+
+for LOG in ${LOGS}
+do
+ case "${LOG}" in
+ *.gz)
+ GREP="zgrep"
+ ;;
+
+ *)
+ GREP="grep"
+ ;;
+ esac
+
+ case "${NAME}" in
+ "")
+ ${GREP} -E "^${DATE}" "${LOG}" || true
+ ;;
+
+ *)
+ ${GREP} -E "^${DATE}" "${LOG}" | grep " ${NAME}" || true
+ ;;
+ esac
+done
diff --git a/libexec/container/move b/libexec/container/move
new file mode 100755
index 0000000..e20d8a8
--- /dev/null
+++ b/libexec/container/move
@@ -0,0 +1,211 @@
+#!/bin/sh
+
+# Copyright (C) 2014-2021 Daniel Baumann <daniel.baumann@open-infrastructure.net>
+#
+# SPDX-License-Identifier: GPL-3.0+
+#
+# This program 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.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+set -e
+
+PROJECT="open-infrastructure"
+PROGRAM="container"
+COMMAND="$(basename ${0})"
+
+CONFIG="/etc/${PROJECT}/${PROGRAM}/config"
+HOOKS="/etc/${PROJECT}/${PROGRAM}/hooks"
+MACHINES="/var/lib/machines"
+
+Parameters ()
+{
+ GETOPT_LONGOPTIONS="force,new:,old:,"
+ GETOPT_OPTIONS="f,n:,o:,"
+
+ PARAMETERS="$(getopt --longoptions ${GETOPT_LONGOPTIONS} --name=${COMMAND} --options ${GETOPT_OPTIONS} --shell sh -- ${@})"
+
+ if [ "${?}" != "0" ]
+ then
+ echo "'${COMMAND}': getopt exit" >&2
+ exit 1
+ fi
+
+ eval set -- "${PARAMETERS}"
+
+ while true
+ do
+ case "${1}" in
+ -f|--force)
+ FORCE="true"
+ shift 1
+ ;;
+
+ -n|--new)
+ NEW="${2}"
+ shift 2
+ ;;
+
+ -o|--old)
+ OLD="${2}"
+ shift 2
+ ;;
+
+ --)
+ shift 1
+ break
+ ;;
+
+ *)
+ echo "'${COMMAND}': getopt error" >&2
+ exit 1
+ ;;
+ esac
+ done
+}
+
+Usage ()
+{
+ echo "Usage: ${PROGRAM} ${COMMAND} [-f|--force] -n|--new NAME -o|--old NAME" >&2
+ exit 1
+}
+
+Parameters "${@}"
+
+if [ -z "${OLD}" ] || [ -z "${NEW}" ]
+then
+ Usage
+fi
+
+if [ ! -e "${MACHINES}/${OLD}" ]
+then
+ echo "'${OLD}': no such container" >&2
+ exit 1
+fi
+
+if [ -e "${MACHINES}/${NEW}" ]
+then
+ echo "'${NEW}': container already exists" >&2
+ exit 1
+fi
+
+STATE="$(machinectl show ${OLD} 2>&1 | awk -FState= '/^State=/ { print $2 }')"
+
+case "${STATE}" in
+ running)
+ echo "'${OLD}': container is started" >&2
+ exit 1
+ ;;
+esac
+
+case "${FORCE}" in
+ true)
+ ;;
+
+ *)
+ if ${PROGRAM} list --other | grep -qs "^${OLD}$"
+ then
+ echo -n "'${OLD}': rename container to '${NEW}' [y|N]? "
+ read FORCE
+
+ FORCE="$(echo ${FORCE} | tr '[A-Z]' '[a-z]')"
+
+ case "${FORCE}" in
+ y|yes)
+ ;;
+
+ *)
+ exit 1
+ ;;
+ esac
+ fi
+ ;;
+esac
+
+# Pre hooks
+for FILE in "${HOOKS}/pre-${COMMAND}".* "${HOOKS}/${OLD}.pre-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
+
+# Run
+mv "${CONFIG}/${OLD}.conf" "${CONFIG}/${NEW}.conf"
+mv "${MACHINES}/${OLD}" "${MACHINES}/${NEW}"
+
+# rw bind mounts
+BIND="$(awk -Fbind= '/^bind=/ { print $2 }' ${CONFIG}/${NEW}.conf)"
+
+if [ -n "${BIND}" ]
+then
+ BINDS="$(echo ${BIND} | sed -e 's|;| |g')"
+
+ for BIND in ${BINDS}
+ do
+ SOURCE_OLD="$(echo ${BIND} | awk -F: '{ print $1 }')"
+ SOURCE_NEW="$(echo ${SOURCE_OLD} | sed -e "s|${OLD}|${NEW}|g")"
+
+ if [ "${SOURCE_OLD}" != "${SOURCE_NEW}" ]
+ then
+ mv "${SOURCE_OLD}" "${SOURCE_NEW}"
+ fi
+
+ TARGET_OLD="$(echo ${BIND} | awk -F: '{ print $2 }')"
+ TARGET_NEW="$(echo ${TARGET_OLD} | sed -e "s|${OLD}|${NEW}|g")"
+
+ if [ "${TARGET_OLD}" != "${TARGET_NEW}" ]
+ then
+ mv "${MACHINES}/${NEW}/${TARGET_OLD}" "${MACHINES}/${NEW}/${TARGET_NEW}"
+ fi
+ done
+fi
+
+# ro bind mounts
+BIND_RO="$(awk -Fbind-ro= '/^bind-ro=/ { print $2 }' ${CONFIG}/${NEW}.conf)"
+
+if [ -n "${BIND_RO}" ]
+then
+ BINDS_RO="$(echo ${BIND_RO} | sed -e 's|;| |g')"
+
+ for BIND_RO in ${BINDS_RO}
+ do
+ SOURCE_OLD="$(echo ${BIND_RO} | awk -F: '{ print $1 }')"
+ SOURCE_NEW="$(echo ${SOURCE_OLD} | sed -e "s|${OLD}|${NEW}|g")"
+
+ if [ "${SOURCE_OLD}" != "${SOURCE_NEW}" ]
+ then
+ mv "${SOURCE_OLD}" "${SOURCE_NEW}"
+ fi
+
+ TARGET_OLD="$(echo ${BIND_RO} | awk -F: '{ print $2 }')"
+ TARGET_NEW="$(echo ${TARGET_OLD} | sed -e "s|${OLD}|${NEW}|g")"
+
+ if [ "${TARGET_OLD}" != "${TARGET_NEW}" ]
+ then
+ mv "${MACHINES}/${NEW}/${TARGET_OLD}" "${MACHINES}/${NEW}/${TARGET_NEW}"
+ fi
+ done
+fi
+
+# config
+sed -i -e "s|${OLD}|${NEW}|g" "${CONFIG}/${NEW}.conf"
+
+# Post hooks
+for FILE in "${HOOKS}/post-${COMMAND}".* "${HOOKS}/${NEW}.post-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
diff --git a/libexec/container/remove b/libexec/container/remove
new file mode 100755
index 0000000..75ebd60
--- /dev/null
+++ b/libexec/container/remove
@@ -0,0 +1,257 @@
+#!/bin/sh
+
+# Copyright (C) 2014-2021 Daniel Baumann <daniel.baumann@open-infrastructure.net>
+#
+# SPDX-License-Identifier: GPL-3.0+
+#
+# This program 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.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+set -e
+
+PROJECT="open-infrastructure"
+PROGRAM="container"
+COMMAND="$(basename ${0})"
+
+CONFIG="/etc/${PROJECT}/${PROGRAM}/config"
+HOOKS="/etc/${PROJECT}/${PROGRAM}/hooks"
+MACHINES="/var/lib/machines"
+
+Parameters ()
+{
+ OPTIONS_ALL=""
+
+ GETOPT_LONGOPTIONS="name:,allow-stop,force,verbose,"
+ GETOPT_OPTIONS="n:,f,v,"
+
+ PARAMETERS="$(getopt --longoptions ${GETOPT_LONGOPTIONS} --name=${COMMAND} --options ${GETOPT_OPTIONS} --shell sh -- ${@})"
+
+ if [ "${?}" != "0" ]
+ then
+ echo "'${COMMAND}': getopt exit" >&2
+ exit 1
+ fi
+
+ eval set -- "${PARAMETERS}"
+
+ while true
+ do
+ case "${1}" in
+ -n|--name)
+ NAME="${2}"
+ shift 2
+ ;;
+
+ --allow-stop)
+ ALLOW_STOP="true"
+ shift 1
+
+ OPTIONS_ALL="${OPTIONS_ALL} --allow-stop"
+ ;;
+
+ -f|--force)
+ FORCE="true"
+ shift 1
+
+ OPTIONS_ALL="${OPTIONS_ALL} --force"
+ ;;
+
+ -v|--verbose)
+ VERBOSE="true"
+ shift 1
+
+ OPTIONS_ALL="${OPTIONS_ALL} --verbose"
+ ;;
+
+ --)
+ shift 1
+ break
+ ;;
+
+ *)
+ echo "'${COMMAND}': getopt error" >&2
+ exit 1
+ ;;
+ esac
+ done
+}
+
+Usage ()
+{
+ echo "Usage: ${PROGRAM} ${COMMAND} -n|--name NAME [--allow-stop] [-f|--force] [-v|--verbose]" >&2
+ exit 1
+}
+
+Rmdir ()
+{
+ DIRECTORIES="${@}"
+
+ for DIRECTORY in ${DIRECTORIES}
+ do
+ PARENT_DIRECTORY="/$(echo ${DIRECTORY} | cut -d / -f 2)"
+
+ if [ "${PARENT_DIRECTORY}" != "${DIRECTORY}" ]
+ then
+ # the directory is at least two levels down from /
+ cd "${PARENT_DIRECTORY}"
+
+ CRUFT="$(echo ${DIRECTORY} | cut -d / -f 3-)"
+ rmdir --ignore-fail-on-non-empty --parents "${CRUFT}" > /dev/null 2>&1 || true
+
+ cd "${OLDPWD}"
+ fi
+ done
+}
+
+Parameters "${@}"
+
+if [ -z "${NAME}" ]
+then
+ Usage
+fi
+
+case "${NAME}" in
+ ALL)
+ NAMES="$(${PROGRAM} list --format shell --stopped)"
+
+ for NAME in ${NAMES}
+ do
+ ${PROGRAM} remove --name ${NAME} ${OPTIONS_ALL} || true
+ done
+
+ exit 0
+ ;;
+esac
+
+if [ ! -e "${MACHINES}/${NAME}" ] && [ ! -e "${CONFIG}/${NAME}.conf" ]
+then
+ echo "'${NAME}': no such container" >&2
+ exit 1
+fi
+
+STATE="$(machinectl show ${NAME} 2>&1 | awk -FState= '/^State=/ { print $2 }')"
+
+case "${STATE}" in
+ running)
+ case "${ALLOW_STOP}" in
+ true)
+ echo "'${NAME}': container is started, stopping it now" >&2
+ ${PROGRAM} stop -n ${NAME}
+ ;;
+
+ *)
+ echo "'${NAME}': container is started" >&2
+ exit 1
+ ;;
+ esac
+ ;;
+esac
+
+case "${FORCE}" in
+ true)
+ ;;
+
+ *)
+ echo -n "'${NAME}': remove container '${NAME}' [y|N]? "
+ read FORCE
+
+ FORCE="$(echo ${FORCE} | tr '[A-Z]' '[a-z]')"
+
+ case "${FORCE}" in
+ y|yes)
+ ;;
+
+ *)
+ exit 1
+ ;;
+ esac
+ ;;
+esac
+
+case "${VERBOSE}" in
+ true)
+ RM_OPTIONS="--verbose"
+ ;;
+
+ *)
+ RM_OPTIONS=""
+ ;;
+esac
+
+# Pre hooks
+for FILE in "${HOOKS}/pre-${COMMAND}".* "${HOOKS}/${NAME}.pre-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
+
+# data
+if [ -e "${CONFIG}/${NAME}.conf" ]
+then
+ # Removing rw bind mounts
+ BIND="$(awk -Fbind= '/^bind=/ { print $2 }' ${CONFIG}/${NAME}.conf)"
+
+ if [ -n "${BIND}" ]
+ then
+ BINDS="$(echo ${BIND} | sed -e 's|;| |g')"
+
+ for BIND in ${BINDS}
+ do
+ DIRECTORY="$(echo ${BIND} | awk -F: '{ print $1 }')"
+
+ Rmdir "${DIRECTORY}"
+ done
+ fi
+
+ # Removing ro bind mounts
+ BIND_RO="$(awk -Fbind-ro= '/^bind-ro=/ { print $2 }' ${CONFIG}/${NAME}.conf)"
+
+ if [ -n "${BIND_RO}" ]
+ then
+ BINDS_RO="$(echo ${BIND_RO} | sed -e 's|;| |g')"
+
+ for BIND_RO in ${BINDS_RO}
+ do
+ DIRECTORY="$(echo ${BIND_RO} | awk -F: '{ print $1 }')"
+
+ Rmdir "${DIRECTORY}"
+ done
+ fi
+fi
+
+# Run
+case "${VERBOSE}" in
+ true)
+ echo -n "Removing container ${NAME}..."
+ ;;
+esac
+
+rm --preserve-root --one-file-system -rf ${RM_OPTIONS} "${MACHINES}/${NAME}"
+rm -f ${RM_OPTIONS} "${CONFIG}/${NAME}.conf"
+
+case "${VERBOSE}" in
+ true)
+ echo " done."
+ ;;
+esac
+
+# Post hooks
+for FILE in "${HOOKS}/post-${COMMAND}".* "${HOOKS}/${NAME}.post-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
diff --git a/libexec/container/restart b/libexec/container/restart
new file mode 100755
index 0000000..3c84f4d
--- /dev/null
+++ b/libexec/container/restart
@@ -0,0 +1,137 @@
+#!/bin/sh
+
+# Copyright (C) 2014-2021 Daniel Baumann <daniel.baumann@open-infrastructure.net>
+#
+# SPDX-License-Identifier: GPL-3.0+
+#
+# This program 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.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+set -e
+
+PROJECT="open-infrastructure"
+PROGRAM="container"
+COMMAND="$(basename ${0})"
+
+HOOKS="/etc/${PROJECT}/${PROGRAM}/hooks"
+MACHINES="/var/lib/machines"
+
+Parameters ()
+{
+ OPTIONS_ALL=""
+
+ GETOPT_LONGOPTIONS="name:,verbose,"
+ GETOPT_OPTIONS="n:,v,"
+
+ PARAMETERS="$(getopt --longoptions ${GETOPT_LONGOPTIONS} --name=${COMMAND} --options ${GETOPT_OPTIONS} --shell sh -- ${@})"
+
+ if [ "${?}" != "0" ]
+ then
+ echo "'${COMMAND}': getopt exit" >&2
+ exit 1
+ fi
+
+ eval set -- "${PARAMETERS}"
+
+ while true
+ do
+ case "${1}" in
+ -n|--name)
+ NAME="${2}"
+ shift 2
+ ;;
+
+ -v|--verbose)
+ VERBOSE="true"
+ shift 1
+
+ OPTIONS_ALL="${OPTIONS_ALL} --verbose"
+ ;;
+
+ --)
+ shift 1
+ break
+ ;;
+
+ *)
+ echo "'${COMMAND}': getopt error" >&2
+ exit 1
+ ;;
+ esac
+ done
+}
+
+Usage ()
+{
+ echo "Usage: ${PROGRAM} ${COMMAND} -n|--name NAME [-v|--verbose]" >&2
+ exit 1
+}
+
+Parameters "${@}"
+
+if [ -z "${NAME}" ]
+then
+ Usage
+fi
+
+case "${NAME}" in
+ ALL)
+ NAMES="$(${PROGRAM} list --format shell --started)"
+
+ for NAME in ${NAMES}
+ do
+ ${PROGRAM} restart --name ${NAME} ${OPTIONS_ALL} || true
+ done
+
+ exit 0
+ ;;
+esac
+
+if [ ! -e "${MACHINES}/${NAME}" ]
+then
+ echo "'${NAME}': no such container" >&2
+ exit 1
+fi
+
+# Pre hooks
+for FILE in "${HOOKS}/pre-${COMMAND}".* "${HOOKS}/${NAME}.pre-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
+
+# Run
+case "${VERBOSE}" in
+ true)
+ echo -n "Restarting container ${NAME}..."
+ ;;
+esac
+
+machinectl reboot ${NAME}
+
+case "${VERBOSE}" in
+ true)
+ echo " done."
+ ;;
+esac
+
+# Post hooks
+for FILE in "${HOOKS}/post-${COMMAND}".* "${HOOKS}/${NAME}.post-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
diff --git a/libexec/container/run b/libexec/container/run
new file mode 100755
index 0000000..f6323c0
--- /dev/null
+++ b/libexec/container/run
@@ -0,0 +1,127 @@
+#!/bin/sh
+
+# Copyright (C) 2014-2019 Daniel Baumann <daniel.baumann@open-infrastructure.net>
+#
+# SPDX-License-Identifier: GPL-3.0+
+#
+# This program 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.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+set -e
+
+PROJECT="open-infrastructure"
+PROGRAM="container"
+COMMAND="$(basename ${0})"
+
+HOOKS="/etc/${PROJECT}/${PROGRAM}/hooks"
+MACHINES="/var/lib/machines"
+
+Parameters ()
+{
+ GETOPT_LONGOPTIONS="name:,"
+ GETOPT_OPTIONS="n:,"
+
+ PARAMETERS="$(getopt --longoptions ${GETOPT_LONGOPTIONS} --name=${COMMAND} --options ${GETOPT_OPTIONS} --shell sh -- ${@})"
+
+ if [ "${?}" != "0" ]
+ then
+ echo "'${COMMAND}': getopt exit" >&2
+ exit 1
+ fi
+
+ eval set -- "${PARAMETERS}"
+
+ while true
+ do
+ case "${1}" in
+ -n|--name)
+ NAME="${2}"
+ shift 2
+ ;;
+
+ --)
+ shift 1
+ break
+ ;;
+
+ *)
+ echo "'${COMMAND}': getopt error" >&2
+ exit 1
+ ;;
+ esac
+ done
+}
+
+Usage ()
+{
+ echo "Usage: ${PROGRAM} ${COMMAND} -n|--name NAME -- COMMAND" >&2
+ exit 1
+}
+
+Parameters "${@}"
+
+if [ -z "${NAME}" ]
+then
+ Usage
+fi
+
+if [ ! -e "${MACHINES}/${NAME}" ]
+then
+ echo "'${NAME}': no such container" >&2
+ exit 1
+fi
+
+STATE="$(machinectl show ${NAME} 2>&1 | awk -F= '/^State=/ { print $2 }')"
+
+case "${STATE}" in
+ running)
+ ;;
+
+ *)
+ echo "'${NAME}': container is not running" >&2
+ exit 1
+ ;;
+esac
+
+LEADER="$(machinectl status ${NAME} | awk '/Leader: / { print $2 }')"
+
+# Pre hooks
+for FILE in "${HOOKS}/pre-${COMMAND}".* "${HOOKS}/${NAME}.pre-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
+
+SSH_CLIENT="${SSH_CLIENT:-127.0.0.1 0 0}"
+
+RUN="$(echo ${@} | awk -F-- '{ print $2}')"
+
+# Run
+nsenter --all --target "${LEADER}" --wd="${MACHINES}/${NAME}/root" /usr/bin/script -c "LC_ALL=C.UTF-8 /bin/bash -l -c \"${RUN}\"" -q /dev/null
+
+case "${SSH_CLIENT}" in
+ 127.0.0.1*)
+ unset SSH_CLIENT
+ ;;
+esac
+
+# Post hooks
+for FILE in "${HOOKS}/post-${COMMAND}".* "${HOOKS}/${NAME}.post-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
diff --git a/libexec/container/start b/libexec/container/start
new file mode 100755
index 0000000..30aee53
--- /dev/null
+++ b/libexec/container/start
@@ -0,0 +1,531 @@
+#!/bin/sh
+
+# Copyright (C) 2014-2021 Daniel Baumann <daniel.baumann@open-infrastructure.net>
+#
+# SPDX-License-Identifier: GPL-3.0+
+#
+# This program 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.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+set -e
+
+PROJECT="open-infrastructure"
+PROGRAM="container"
+COMMAND="$(basename ${0})"
+
+CONFIG="/etc/${PROJECT}/${PROGRAM}/config"
+HOOKS="/etc/${PROJECT}/${PROGRAM}/hooks"
+MACHINES="/var/lib/machines"
+
+START="false"
+SYSTEMCTL="true"
+
+Parameters ()
+{
+ OPTIONS_ALL=""
+
+ GETOPT_LONGOPTIONS="name:,force,nspawn,start,verbose,"
+ GETOPT_OPTIONS="n:,f,v,"
+
+ PARAMETERS="$(getopt --longoptions ${GETOPT_LONGOPTIONS} --name=${COMMAND} --options ${GETOPT_OPTIONS} --shell sh -- ${@})"
+
+ if [ "${?}" != "0" ]
+ then
+ echo "'${COMMAND}': getopt exit" >&2
+ exit 1
+ fi
+
+ eval set -- "${PARAMETERS}"
+
+ while true
+ do
+ case "${1}" in
+ -n|--name)
+ NAME="${2}"
+ shift 2
+ ;;
+
+ -f|--force)
+ FORCE="true"
+ shift 1
+
+ OPTIONS_ALL="${OPTIONS_ALL} --force"
+ ;;
+
+ --nspawn)
+ # internal option
+ SYSTEMCTL="false"
+ shift 1
+ ;;
+
+ --start)
+ # internal option
+ START="true"
+ SYSTEMCTL="false"
+ shift 1
+ ;;
+
+ -v|--verbose)
+ VERBOSE="true"
+ shift 1
+
+ OPTIONS_ALL="${OPTIONS_ALL} --verbose"
+ ;;
+
+ --)
+ shift 1
+ break
+ ;;
+
+ *)
+ echo "'${COMMAND}': getopt error" >&2
+ exit 1
+ ;;
+ esac
+ done
+}
+
+Usage ()
+{
+ echo "Usage: ${PROGRAM} ${COMMAND} -n|--name NAME [-f|--force]" >&2
+ exit 1
+}
+
+Parameters "${@}"
+
+if [ -z "${NAME}" ]
+then
+ Usage
+fi
+
+# hooks
+export NAME
+
+case "${NAME}" in
+ ALL)
+ NAMES="$(${PROGRAM} list --format shell --stopped)"
+
+ for NAME in ${NAMES}
+ do
+ ${PROGRAM} start --name ${NAME} ${OPTIONS_ALL} || true
+ done
+
+ exit 0
+ ;;
+esac
+
+if [ ! -e "${MACHINES}/${NAME}" ]
+then
+ echo "'${NAME}': no such container" >&2
+ exit 1
+fi
+
+case "${START}" in
+ false)
+ STATE="$(machinectl show ${NAME} 2>&1 | awk -FState= '/^State=/ { print $2 }')"
+
+ case "${STATE}" in
+ running)
+ echo "'${NAME}': container is already started" >&2
+ exit 1
+ ;;
+ esac
+ ;;
+esac
+
+if [ -e "${MACHINES}/.#${NAME}.lck" ]
+then
+ case "${FORCE}" in
+ true)
+ rm -f "${MACHINES}/.#${NAME}.lck"
+
+ VETHS="$(awk -Fnetwork-veth-extra= '/^network-veth-extra=/ { print $2 }' ${CONFIG}/${NAME}.conf | awk -F: '{ print $1 }')"
+
+ for VETH in ${VETHS}
+ do
+ ip link delete ${VETH} > /dev/null 2>&1 || true
+ done
+ ;;
+
+ *)
+ echo "'${NAME}': container is locked" >&2
+ exit 1
+ ;;
+ esac
+fi
+
+HOST_ARCHITECTURE="$(dpkg --print-architecture)"
+MACHINE_ARCHITECTURE="$(chroot ${MACHINES}/${NAME} dpkg --print-architecture)"
+
+case "${HOST_ARCHITECTURE}" in
+ amd64)
+ case "${MACHINE_ARCHITECTURE}" in
+ i386)
+ SETARCH="setarch i686"
+ ;;
+
+ *)
+ SETARCH=""
+ ;;
+ esac
+ ;;
+esac
+
+case "${START}" in
+ start)
+ ;;
+
+ *)
+ # Pre hooks
+ for FILE in "${HOOKS}/pre-${COMMAND}".* "${HOOKS}/${NAME}.pre-${COMMAND}"
+ do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+ done
+ ;;
+esac
+
+# config
+if [ -e "${CONFIG}/${NAME}.conf" ]
+then
+ CNT_OVERLAY="$(awk -Fcnt.overlay= '/^cnt.overlay=/ { print $2 }' ${CONFIG}/${NAME}.conf)"
+ CNT_OVERLAY_OPTIONS="$(awk -Fcnt.overlay-options= '/^cnt.overlay-options=/ { print $2 }' ${CONFIG}/${NAME}.conf)"
+
+ if [ -n "${CNT_OVERLAY}" ]
+ then
+ CNT_OVERLAYS="$(echo ${CNT_OVERLAY} | sed -e 's|;| |g')"
+
+ COUNT="0"
+ for CNT_OVERLAY in ${CNT_OVERLAYS}
+ do
+ DIRECTORY_LOWER="$(echo ${CNT_OVERLAY} | awk -F: '{ print $1 }')"
+ DIRECTORY_UPPER="$(echo ${CNT_OVERLAY} | awk -F: '{ print $2 }')"
+ DIRECTORY_WORK="$(echo ${CNT_OVERLAY} | awk -F: '{ print $3 }')"
+ DIRECTORY_MERGED="$(echo ${CNT_OVERLAY} | awk -F: '{ print $4 }')"
+
+ COUNT="$((${COUNT} + 1))"
+ CNT_OVERLAY_OPTION="$(echo ${CNT_OVERLAY_OPTIONS} | awk -F ';' "{ print \$${COUNT} }")"
+
+ for DIRECTORY in "${DIRECTORY_LOWER}" "${DIRECTORY_UPPER}" "${DIRECTORY_WORK}" "${DIRECTORY_MERGED}"
+ do
+ mkdir -p "${DIRECTORY}"
+ done
+
+ if ! findmnt -n -o SOURCE "${DIRECTORY_MERGED}" | grep -qs '^cnt.overlay-'
+ then
+ if [ -n "${CNT_OVERLAY_OPTION}" ]
+ then
+ CNT_OVERLAY_OPTION="-o ${CNT_OVERLAY_OPTION}"
+ fi
+
+ mount cnt.overlay-${NAME} -t overlay ${CNT_OVERLAY_OPTION} -olowerdir="${DIRECTORY_LOWER}",upperdir="${DIRECTORY_UPPER}",workdir="${DIRECTORY_WORK}",default_permissions "${DIRECTORY_MERGED}"
+ fi
+ done
+ fi
+
+ BIND="$(awk -Fbind= '/^bind=/ { print $2 }' ${CONFIG}/${NAME}.conf)"
+
+ if [ -n "${BIND}" ]
+ then
+ BINDS="$(echo ${BIND} | sed -e 's|;| |g')"
+
+ for BIND in ${BINDS}
+ do
+ DIRECTORY="$(echo ${BIND} | awk -F: '{ print $1 }')"
+
+ if [ ! -e "${DIRECTORY}" ]
+ then
+ echo "'${DIRECTORY}': creating non-existing directory for bind mounting"
+ mkdir -p "${DIRECTORY}"
+ fi
+ done
+
+ BIND=""
+
+ for DIRECTORIES in ${BINDS}
+ do
+ BIND="${BIND} --bind ${DIRECTORIES}"
+ done
+ fi
+
+ BIND_RO="$(awk -Fbind-ro= '/^bind-ro=/ { print $2 }' ${CONFIG}/${NAME}.conf)"
+
+ if [ -n "${BIND_RO}" ]
+ then
+ BINDS_RO="$(echo ${BIND_RO} | sed -e 's|;| |g')"
+
+ for BIND_RO in ${BINDS_RO}
+ do
+ DIRECTORY="$(echo ${BIND_RO} | awk -F: '{ print $1 }')"
+
+ if [ ! -e "${DIRECTORY}" ]
+ then
+ echo "'${DIRECTORY}': creating non-existing directory for bind-ro mounting"
+ mkdir -p "${DIRECTORY}"
+ fi
+ done
+
+ BIND_RO=""
+
+ for DIRECTORIES in ${BINDS_RO}
+ do
+ BIND_RO="${BIND_RO} --bind-ro ${DIRECTORIES}"
+ done
+ fi
+
+ BOOT="$(awk -Fboot= '/^boot=/ { print $2 }' ${CONFIG}/${NAME}.conf || echo yes)"
+
+ case "${BOOT}" in
+ yes)
+ BOOT="--boot"
+ ;;
+
+ *)
+ BOOT=""
+ ;;
+ esac
+
+ CAPABILITY="$(awk -Fcapability= '/^capability=/ { print $2 }' ${CONFIG}/${NAME}.conf)"
+
+ case "${CAPABILITY}" in
+ "")
+ CAPABILITY=""
+ ;;
+
+ *)
+ CAPABILITY="--capability=${CAPABILITY}"
+ ;;
+ esac
+
+ DIRECTORY="$(awk -Fdirectory= '/^directory=/ { print $2 }' ${CONFIG}/${NAME}.conf || echo ${MACHINES}/${NAMES})"
+ DIRECTORY="--directory ${DIRECTORY}"
+
+ DROP_CAPABILITY="$(awk -Fdrop-capability= '/^drop-capability=/ { print $2 }' ${CONFIG}/${NAME}.conf)"
+
+ case "${DROP_CAPABILITY}" in
+ "")
+ DROP_CAPABILITY=""
+ ;;
+
+ *)
+ DROP_CAPABILITY="--drop-capability=${DROP_CAPABILITY}"
+ ;;
+ esac
+
+ LINK_JOURNAL="$(awk -Flink-journal= '/^link-journal=/ { print $2 }' ${CONFIG}/${NAME}.conf || echo no)"
+
+ case "${LINK_JOURNAL}" in
+ yes)
+ LINK_JOURNAL="--link-journal=yes"
+ ;;
+
+ *)
+ LINK_JOURNAL="--link-journal=no"
+ ;;
+ esac
+
+ MACHINE="--machine=${NAME}"
+
+ NETWORK_VETH_EXTRA=""
+
+ VETHS="$(awk -Fnetwork-veth-extra= '/^network-veth-extra=/ { print $2 }' ${CONFIG}/${NAME}.conf)"
+
+ case "${VETHS}" in
+ "")
+ ;;
+
+ *)
+ for VETH in ${VETHS}
+ do
+ NETWORK_VETH_EXTRA="${NETWORK_VETH_EXTRA} --network-veth-extra=${VETH}"
+ INTERFACE="$(echo ${VETH} | awk -F: '{ print $1 }')"
+
+ if [ "$(echo ${INTERFACE} | wc -c)" -gt 15 ]
+ then
+ echo "'${INTERFACE}': name exceeds maximum of 15 characters, network might be not working."
+ fi
+ done
+ ;;
+ esac
+
+ NETWORK_BRIDGES="$(awk -Fcnt.network-bridge= '/^cnt.network-bridge=/ { print $2 }' ${CONFIG}/${NAME}.conf)"
+
+ case "${NETWORK_BRIDGES}" in
+ "")
+ ;;
+
+ *)
+ for BRIDGE_DEFINITION in ${NETWORK_BRIDGES}
+ do
+ INTERFACE="$(echo ${BRIDGE_DEFINITION} | awk -F: '{ print $1 }')"
+ BRIDGE="$(echo ${BRIDGE_DEFINITION} | awk -F: '{ print $2 }')"
+
+ if [ "$(echo ${INTERFACE} | wc -c)" -gt 15 ]
+ then
+ echo "'${INTERFACE}': name exceeds maximum of 15 characters, network might be not working."
+ fi
+
+ if [ -n "${BRIDGE}" ] && [ -n "${INTERFACE}" ]
+ then
+
+cat > "/etc/network/interfaces.d/${INTERFACE}" << EOF
+allow-hotplug ${INTERFACE}
+iface ${INTERFACE} inet manual
+ pre-up ip link set ${INTERFACE} up
+ post-up ip link set ${INTERFACE} master ${BRIDGE}
+ pre-down ip link set ${INTERFACE} nomaster
+ post-down ip link set ${INTERFACE} down
+EOF
+
+ else
+ echo "Warning bridge definition '${BRIDGE_DEFINITION}' not recognized (expected <bridge>:<interface>): Ignoring"
+ fi
+ done
+ ;;
+ esac
+
+ PRIVATE_USERS="$(awk -Fprivate-users= '/^private-users=/ { print $2 }' ${CONFIG}/${NAME}.conf || echo no)"
+
+ case "${PRIVATE_USERS}" in
+ yes)
+ PRIVATE_USERS="--private-users=yes"
+ ;;
+
+ *)
+ PRIVATE_USERS="--private-users=no"
+ ;;
+ esac
+
+ REGISTER="$(awk -Fregister= '/^register=/ { print $2 }' ${CONFIG}/${NAME}.conf || echo yes)"
+
+ case "${REGISTER}" in
+ yes)
+ REGISTER="--register=yes"
+ ;;
+
+ *)
+ REGISTER="--register=no"
+ ;;
+ esac
+
+ BLOCK_IO_DEVICE_WEIGHT="$(awk -FBlockIODeviceWeight= '/^BlockIODeviceWeight=/ { print $2 }' ${CONFIG}/${NAME}.conf)"
+
+ if [ -n "${BLOCK_IO_DEVICE_WEIGHT}" ]
+ then
+ BLOCK_IO_DEVICE_WEIGHT="BlockIODeviceWeight=${BLOCK_IO_DEVICE_WEIGHT}"
+ SET_PROPERTY="true"
+ fi
+
+ BLOCK_IO_READ_BANDWIDTH="$(awk -FBlockIOReadBandwidth= '/^BlockIOReadBandwidth=/ { print $2 }' ${CONFIG}/${NAME}.conf)"
+
+ if [ -n "${BLOCK_IO_READ_BANDWIDTH}" ]
+ then
+ BLOCK_IO_READ_BANDWIDTH="BlockIOReadBandwidth=${BLOCK_IO_READ_BANDWIDTH}"
+ SET_PROPERTY="true"
+ fi
+
+ BLOCK_IO_WEIGHT="$(awk -FBlockIOWeight= '/^BlockIOWeight=/ { print $2 }' ${CONFIG}/${NAME}.conf)"
+
+ if [ -n "${BLOCK_IO_WEIGHT}" ]
+ then
+ BLOCK_IO_WEIGHT="BlockIOWeight=${BLOCK_IO_WEIGHT}"
+ SET_PROPERTY="true"
+ fi
+
+ BLOCK_IO_WRITE_BANDWIDTH="$(awk -FBlockIOWriteBandwidth=/= '/^BlockIOWriteBandwidth=/ { print $2 }' ${CONFIG}/${NAME}.conf)"
+
+ if [ -n "${BLOCK_IO_WRITE_BANDWIDTH}" ]
+ then
+ BLOCK_IO_WRITE_BANDWIDTH="BlockIOWriteBandwidth=${BLOCK_IO_WRITE_BANDWIDTH}"
+ SET_PROPERTY="true"
+ fi
+
+ CPU_QUOTA="$(awk -FCPUQuota= '/^CPUQuota=/ { print $2 }' ${CONFIG}/${NAME}.conf)"
+
+ if [ -n "${CPU_QUOTA}" ]
+ then
+ CPU_QUOTA="CPUQuota=${CPU_QUOTA}"
+ SET_PROPERTY="true"
+ fi
+
+ CPU_SHARES="$(awk -FCPUShares= '/^CPUShares=/ { print $2 }' ${CONFIG}/${NAME}.conf)"
+
+ if [ -n "${CPU_SHARES}" ]
+ then
+ CPU_SHARES="CPUShares=${CPU_SHARES}"
+ SET_PROPERTY="true"
+ fi
+
+ MEMORY_LIMIT="$(awk -FMemoryLimit= '/^MemoryLimit=/ { print $2 }' ${CONFIG}/${NAME}.conf)"
+
+ if [ -n "${MEMORY_LIMIT}" ]
+ then
+ MEMORY_LIMIT="MemoryLimit=${MEMORY_LIMIT}"
+ SET_PROPERTY="true"
+ fi
+
+ TASKS_MAX="$(awk -FTasksMax= '/^TasksMax=/ { print $2 }' ${CONFIG}/${NAME}.conf)"
+
+ if [ -n "${TASKS_MAX}" ]
+ then
+ TASKS_MAX="TasksMax=${TASKS_MAX}"
+ SET_PROPERTY="true"
+ fi
+fi
+
+case "${SYSTEMCTL}" in
+ true)
+ systemctl start ${PROGRAM}@${NAME}.service
+
+ # Post hooks
+ for FILE in "${HOOKS}/post-${COMMAND}".* "${HOOKS}/${NAME}.post-${COMMAND}"
+ do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+ done
+
+ exit 0
+ ;;
+esac
+
+case "${START}" in
+ true)
+ case "${SET_PROPERTY}" in
+ true)
+ systemctl --runtime set-property ${NAME} ${BLOCK_IO_DEVICE_WEIGHT} ${BLOCK_IO_READ_BANDWIDTH} ${BLOCK_IO_WEIGHT} ${BLOCK_IO_WRITE_BANDWIDTH} ${CPU_QUOTA} ${CPU_SHARES} ${MEMORY_LIMIT} ${TASKS_MAX}
+ ;;
+ esac
+ ;;
+
+ *)
+ # Run
+
+ case "${VERBOSE}" in
+ true)
+ echo -n "Starting container ${NAME}..."
+ ;;
+ esac
+
+ ${SETARCH} systemd-nspawn --keep-unit ${BIND} ${BIND_RO} ${BOOT} ${CAPABILITY} ${DIRECTORY} ${DROP_CAPABILITY} ${MACHINE} ${NETWORK_VETH_EXTRA} ${LINK_JOURNAL} ${REGISTER}
+
+ case "${VERBOSE}" in
+ true)
+ echo " done."
+ ;;
+ esac
+ ;;
+esac
diff --git a/libexec/container/status b/libexec/container/status
new file mode 100755
index 0000000..a4b49e1
--- /dev/null
+++ b/libexec/container/status
@@ -0,0 +1,104 @@
+#!/bin/sh
+
+# Copyright (C) 2016 Simon Spöehel <simon.spoehel@open-infrastructure.net>
+#
+# SPDX-License-Identifier: GPL-3.0+
+#
+# This program 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.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+set -e
+
+PROJECT="open-infrastructure"
+PROGRAM="container"
+COMMAND="$(basename ${0})"
+
+CONFIG="/etc/${PROJECT}/${PROGRAM}/config"
+HOOKS="/etc/${PROJECT}/${PROGRAM}/hooks"
+MACHINES="/var/lib/machines"
+
+Parameters ()
+{
+ GETOPT_LONGOPTIONS="name:,"
+ GETOPT_OPTIONS="n:,"
+
+ PARAMETERS="$(getopt --longoptions ${GETOPT_LONGOPTIONS} --name=${COMMAND} --options ${GETOPT_OPTIONS} --shell sh -- ${@})"
+
+ if [ "${?}" != "0" ]
+ then
+ echo "'${COMMAND}': getopt exit" >&2
+ exit 1
+ fi
+
+ eval set -- "${PARAMETERS}"
+
+ while true
+ do
+ case "${1}" in
+ -n|--name)
+ NAME="${2}"
+ shift 2
+ ;;
+
+ --)
+ shift 1
+ break
+ ;;
+
+ *)
+ echo "'${COMMAND}': getopt error" >&2
+ exit 1
+ ;;
+ esac
+ done
+}
+
+Usage ()
+{
+ echo "Usage: ${PROGRAM} ${COMMAND} -n|--name NAME" >&2
+ exit 1
+}
+
+Parameters "${@}"
+
+if [ -z "${NAME}" ]
+then
+ Usage
+fi
+
+if [ ! -e "${MACHINES}/${NAME}" ]
+then
+ echo "'${NAME}': no such container" >&2
+ exit 1
+fi
+
+# Pre hooks
+for FILE in "${HOOKS}/pre-${COMMAND}".* "${HOOKS}/${NAME}.pre-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
+
+# Run
+systemctl status ${PROGRAM}@${NAME}.service --full
+
+# Post hooks
+for FILE in "${HOOKS}/post-${COMMAND}".* "${HOOKS}/${NAME}.post-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
diff --git a/libexec/container/stop b/libexec/container/stop
new file mode 100755
index 0000000..6b8caee
--- /dev/null
+++ b/libexec/container/stop
@@ -0,0 +1,289 @@
+#!/bin/sh
+
+# Copyright (C) 2014-2021 Daniel Baumann <daniel.baumann@open-infrastructure.net>
+#
+# SPDX-License-Identifier: GPL-3.0+
+#
+# This program 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.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+set -e
+
+PROJECT="open-infrastructure"
+PROGRAM="container"
+COMMAND="$(basename ${0})"
+
+CONFIG="/etc/${PROJECT}/${PROGRAM}/config"
+HOOKS="/etc/${PROJECT}/${PROGRAM}/hooks"
+MACHINES="/var/lib/machines"
+
+CLEAN="false"
+
+Parameters ()
+{
+ OPTIONS_ALL=""
+
+ GETOPT_LONGOPTIONS="name:,force,clean,verbose,"
+ GETOPT_OPTIONS="n:,f,v,"
+
+ PARAMETERS="$(getopt --longoptions ${GETOPT_LONGOPTIONS} --name=${COMMAND} --options ${GETOPT_OPTIONS} --shell sh -- ${@})"
+
+ if [ "${?}" != "0" ]
+ then
+ echo "'${COMMAND}': getopt exit" >&2
+ exit 1
+ fi
+
+ eval set -- "${PARAMETERS}"
+
+ while true
+ do
+ case "${1}" in
+ -n|--name)
+ NAME="${2}"
+ shift 2
+ ;;
+
+ -f|--force)
+ FORCE="true"
+ shift 1
+
+ OPTIONS_ALL="${OPTIONS_ALL} --force"
+ ;;
+
+ --clean)
+ # internal option
+ CLEAN="true"
+ shift 1
+
+ OPTONS_ALL="${OPTIONS_ALL} --clean"
+ ;;
+
+ -v|--verbose)
+ VERBOSE="true"
+ shift 1
+
+ OPTIONS_ALL="${OPTIONS_ALL} --verbose"
+ ;;
+
+ --)
+ shift 1
+ break
+ ;;
+
+ *)
+ echo "'${COMMAND}': getopt error" >&2
+ exit 1
+ ;;
+ esac
+ done
+}
+
+Usage ()
+{
+ echo "Usage: ${PROGRAM} ${COMMAND} -n|--name NAME [-f|--force] [-v|--verbose]" >&2
+ exit 1
+}
+
+Rmdir ()
+{
+ DIRECTORIES="${@}"
+
+ for DIRECTORY in ${DIRECTORIES}
+ do
+ PARENT_DIRECTORY="/$(echo ${DIRECTORY} | cut -d / -f 2)"
+
+ if [ "${PARENT_DIRECTORY}" != "${DIRECTORY}" ]
+ then
+ # the directory is at least two levels down from /
+ cd "${PARENT_DIRECTORY}"
+
+ CRUFT="$(echo ${DIRECTORY} | cut -d / -f 3-)"
+ rmdir --ignore-fail-on-non-empty --parents "${CRUFT}" > /dev/null 2>&1 || true
+
+ cd "${OLDPWD}"
+ fi
+ done
+}
+
+Parameters "${@}"
+
+if [ -z "${NAME}" ]
+then
+ Usage
+fi
+
+case "${NAME}" in
+ ALL)
+ NAMES="$(${PROGRAM} list --format shell --started)"
+
+ for NAME in ${NAMES}
+ do
+ ${PROGRAM} stop --name ${NAME} ${OPTIONS_ALL} || true
+ done
+
+ exit 0
+ ;;
+esac
+
+if [ ! -e "${MACHINES}/${NAME}" ]
+then
+ echo "'${NAME}': no such container" >&2
+ exit 1
+fi
+
+# Pre hooks
+for FILE in "${HOOKS}/pre-${COMMAND}".* "${HOOKS}/${NAME}.pre-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
+
+STATE="$(machinectl show ${NAME} 2>&1 | awk -FState= '/^State=/ { print $2 }')"
+
+case "${CLEAN}" in
+ true)
+ # Removing overlay mounts
+ CNT_OVERLAY="$(awk -Fcnt.overlay= '/^cnt.overlay=/ { print $2 }' ${CONFIG}/${NAME}.conf)"
+
+ if [ -n "${CNT_OVERLAY}" ]
+ then
+ CNT_OVERLAYS="$(echo ${CNT_OVERLAY} | sed -e 's|;| |g')"
+
+ for CNT_OVERLAY in ${CNT_OVERLAYS}
+ do
+ DIRECTORY_LOWER="$(echo ${CNT_OVERLAY} | awk -F: '{ print $1 }')"
+ DIRECTORY_UPPER="$(echo ${CNT_OVERLAY} | awk -F: '{ print $2 }')"
+ DIRECTORY_WORK="$(echo ${CNT_OVERLAY} | awk -F: '{ print $3 }')"
+ DIRECTORY_MERGED="$(echo ${CNT_OVERLAY} | awk -F: '{ print $4 }')"
+
+ umount -f "${DIRECTORY_MERGED}"
+
+ Rmdir "${DIRECTORY_LOWER}" "${DIRECTORY_UPPER}" "${DIRECTORY_WORK}" "${DIRECTORY_MERGED}"
+ done
+ fi
+
+ # Removing rw bind mounts
+ BIND="$(awk -Fbind= '/^bind=/ { print $2 }' ${CONFIG}/${NAME}.conf)"
+
+ if [ -n "${BIND}" ]
+ then
+ BINDS="$(echo ${BIND} | sed -e 's|;| |g')"
+
+ for BIND in ${BINDS}
+ do
+ DIRECTORY="$(echo ${BIND} | awk -F: '{ print $1 }')"
+
+ Rmdir "${DIRECTORY}"
+ done
+ fi
+
+ # Removing ro bind mounts
+ BIND_RO="$(awk -Fbind-ro= '/^bind-ro=/ { print $2 }' ${CONFIG}/${NAME}.conf)"
+
+ if [ -n "${BIND_RO}" ]
+ then
+ BINDS_RO="$(echo ${BIND_RO} | sed -e 's|;| |g')"
+
+ for BIND_RO in ${BINDS_RO}
+ do
+ DIRECTORY="$(echo ${BIND_RO} | awk -F: '{ print $1 }')"
+
+ Rmdir "${DIRECTORY}"
+ done
+ fi
+
+ # Removing network configuration
+ VETHS="$(awk -Fnetwork-veth-extra= '/^network-veth-extra=/ { print $2 }' ${CONFIG}/${NAME}.conf)"
+
+ case "${VETHS}" in
+ "")
+ ;;
+
+ *)
+ for VETH in ${VETHS}
+ do
+ INTERFACE="$(echo ${VETH} | awk -F: '{ print $1 }')"
+ FILE="/etc/network/interfaces.d/${INTERFACE}"
+
+ if [ -f "${FILE}" ]
+ then
+ rm -f "${FILE}"
+ fi
+ done
+ ;;
+ esac
+
+ exit 0
+ ;;
+
+ *)
+ ;;
+esac
+
+case "${STATE}" in
+ running)
+ ;;
+
+ *)
+ echo "'${NAME}': container is already stopped" >&2
+ exit 1
+ ;;
+esac
+
+case "${FORCE}" in
+ true)
+ MODE="terminate"
+ ;;
+
+ *)
+ MODE="poweroff"
+ ;;
+esac
+
+# Run
+case "${VERBOSE}" in
+ true)
+ echo -n "Stopping container ${NAME}..."
+ ;;
+esac
+
+machinectl ${MODE} ${NAME}
+
+case "${FORCE}" in
+ true)
+ VETHS="$(awk -Fnetwork-veth-extra= '/^network-veth-extra=/ { print $2 }' ${CONFIG}/${NAME}.conf | awk -F: '{ print $1 }')"
+
+ for VETH in ${VETHS}
+ do
+ ip link delete ${VETH} > /dev/null 2>&1 || true
+ done
+ ;;
+esac
+
+case "${VERBOSE}" in
+ true)
+ echo " done."
+ ;;
+esac
+
+# Post hooks
+for FILE in "${HOOKS}/post-${COMMAND}".* "${HOOKS}/${NAME}.post-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
diff --git a/libexec/container/top b/libexec/container/top
new file mode 100755
index 0000000..d9baa1c
--- /dev/null
+++ b/libexec/container/top
@@ -0,0 +1,118 @@
+#!/bin/sh
+
+# Copyright (C) 2014-2021 Daniel Baumann <daniel.baumann@open-infrastructure.net>
+#
+# SPDX-License-Identifier: GPL-3.0+
+#
+# This program 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.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+set -e
+
+PROJECT="open-infrastructure"
+PROGRAM="container"
+COMMAND="$(basename ${0})"
+
+Parameters ()
+{
+ GETOPT_LONGOPTIONS="delay:,"
+ GETOPT_OPTIONS="d:,"
+
+ PARAMETERS="$(getopt --longoptions ${GETOPT_LONGOPTIONS} --name=${COMMAND} --options ${GETOPT_OPTIONS} --shell sh -- ${@})"
+
+ if [ "${?}" != "0" ]
+ then
+ echo "'${COMMAND}': getopt exit" >&2
+ exit 1
+ fi
+
+ eval set -- "${PARAMETERS}"
+
+ while true
+ do
+ case "${1}" in
+ -d|--delay)
+ DELAY="${2}"
+ shift 2
+ ;;
+
+ --)
+ shift 1
+ break
+ ;;
+
+ *)
+ echo "'${COMMAND}': getopt error" >&2
+ exit 1
+ ;;
+ esac
+ done
+}
+
+Usage ()
+{
+ echo "Usage: ${PROGRAM} ${COMMAND} [-d|--delay DELAY]" >&2
+ exit 1
+}
+
+Parameters "${@}"
+
+DELAY="${DELAY:-1}"
+HOST="$(hostname -f 2> /dev/null || hostname)"
+
+# Pre hooks
+for FILE in "${HOOKS}/pre-${COMMAND}".* "${HOOKS}/${NAME}.pre-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
+
+# Run
+Top ()
+{
+ HOME="$(tput cup 0 0)"
+ ED="$(tput ed)"
+ EL="$(tput el)"
+
+ printf '%s%s' "${HOME}" "${ED}"
+
+ while true
+ do
+ ROWS="$(tput lines)"
+ COLS="$(tput cols)"
+ CMD="${@}"
+
+ ${SHELL:=sh} -c "${CMD}" | head -n ${ROWS} | while IFS= read LINE
+ do
+ printf '%-*.*s%s\n' ${COLS} ${COLS} "${LINE}" "${EL}"
+ done
+
+ printf '%s%s' "${ED}" "${HOME}"
+ sleep ${DELAY}
+ done
+}
+
+trap 'clear' EXIT HUP INT QUIT TERM
+
+Top "${PROGRAM} list && printf '%-59s %-19s\n' \" Host: ${HOST}\" \"\$(date +%Y-%m-%d\ %H:%M:%S)\""
+
+# Post hooks
+for FILE in "${HOOKS}/post-${COMMAND}".* "${HOOKS}/${NAME}.post-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
diff --git a/libexec/container/version b/libexec/container/version
new file mode 100755
index 0000000..c2b7716
--- /dev/null
+++ b/libexec/container/version
@@ -0,0 +1,48 @@
+#!/bin/sh
+
+# Copyright (C) 2014-2021 Daniel Baumann <daniel.baumann@open-infrastructure.net>
+#
+# SPDX-License-Identifier: GPL-3.0+
+#
+# This program 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.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+set -e
+
+PROJECT="open-infrastructure"
+PROGRAM="container"
+COMMAND="$(basename ${0})"
+
+HOOKS="/etc/${PROJECT}/${PROGRAM}/hooks"
+SHARE="/usr/share/${PROJECT}/${PROGRAM}"
+
+# Pre hooks
+for FILE in "${HOOKS}/pre-${COMMAND}".* "${HOOKS}/${NAME}.pre-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done
+
+# Run
+cat "${SHARE}/VERSION.txt"
+
+# Post hooks
+for FILE in "${HOOKS}/post-${COMMAND}".* "${HOOKS}/${NAME}.post-${COMMAND}"
+do
+ if [ -x "${FILE}" ]
+ then
+ "${FILE}"
+ fi
+done