9 Commits

  1. 4
      collabora/hooks/web_proxy-relation-joined
  2. 4
      collabora/metadata.yml
  3. 4
      cron/lib/common
  4. 100
      docker-host/hooks/install.d/90-ntfy.sh
  5. 166
      docker-host/src/bin/send
  6. BIN
      docker-host/src/etc/ssh/ntfy-key
  7. 4
      gogocarto/metadata.yml
  8. 2
      nextcloud/actions/upgrade
  9. 6
      nextcloud/hooks/init
  10. 5
      nextcloud/lib/common
  11. 31
      odoo-tecnativa/actions/load
  12. 2
      rallly/metadata.yml
  13. 3
      rsync-backup-target/build/Dockerfile
  14. 9
      rsync-backup-target/build/src/usr/local/sbin/ssh-admin-cmd-validate
  15. 151
      rsync-backup-target/build/src/usr/local/sbin/ssh-key

4
collabora/hooks/web_proxy-relation-joined

@ -3,6 +3,8 @@
set -e
URL=$(relation-get url) || exit 1
DOMAIN=$(relation-get domain) || exit 1
PROTO="${URL%%://*}"
ssl_enable=true
@ -24,6 +26,6 @@ config-add "\
services:
$MASTER_BASE_SERVICE_NAME:
environment:
server_name: \"$URL\"
server_name: \"$DOMAIN\"
extra_params: \"--o:ssl.enable=false --o:ssl.termination=${ssl_enable} \"
"

4
collabora/metadata.yml

@ -1,5 +1,5 @@
# from: collabora/code:23.05.3.1.1
docker-image: docker.0k.io/collabora:2305.3.1
# from: collabora/code:23.05.9.4.1
docker-image: docker.0k.io/collabora:2305.9.4
docker-compose:
cap_add:

4
cron/lib/common

@ -71,7 +71,9 @@ cron:get_config() {
fi
cron:get_config "$value" || return 1
;;
esac > "$cache_file"
esac > "$cache_file.tmp"
mv -v "$cache_file.tmp" "$cache_file" >&2
## if cache file is empty, this is an error
if [ ! -s "$cache_file" ]; then

100
docker-host/hooks/install.d/90-ntfy.sh

@ -0,0 +1,100 @@
#!/bin/bash
set -eux
NTFY_BROKER="${NTFY_BROKER:-core-01.0k.io}"
## Uncipher ntfy key to destination
umask 077
ntfy_key_ciphered="src/etc/ssh/ntfy-key"
if [ ! -f "$ntfy_key_ciphered" ]; then
echo "Error: ciphered ntfy key not found" >&2
exit 1
fi
ntfy_key_dest=/etc/ssh/ntfy-key
if [ ! -f "$ntfy_key_dest" ]; then
cat "$ntfy_key_ciphered" |
gpg -d --batch --yes --passphrase 'uniquepass' > "$ntfy_key_dest" || {
echo "Error while unpacking ntfy key to '${ntfy_key_dest}'" >&2
exit 1
}
fi
## Request token to ntfy server and add to config file
known_host="/root/.ssh/known_hosts"
if ! ssh-keygen -F "$NTFY_BROKER" -f "$known_host" >/dev/null; then
ssh-keyscan -H "$NTFY_BROKER" >> "$known_host" || {
echo "Error while adding '$NTFY_BROKER' to known_hosts" >&2
exit 1
}
fi
config_file="/etc/ntfy/ntfy.conf"
mkdir -p "${config_file%/*}"
if ! [ -f "$config_file" ]; then
touch "$config_file" || {
echo "Error: couldn’t create config file '$config_file'" >&2;
exit 1
}
fi
LOGIN=""
PASSWORD=""
source "$config_file" || {
echo "Error: couldn't source config file '$config_file'" >&2
exit 1
}
## Note that we require the forcing of stdin to /dev/null to avoid
## the rest of the script to be vacuumed by the ssh command.
## This effect will only happen when launching this script in special
## conditions involving stdin.
cred=$(ssh -i "$ntfy_key_dest" ntfy@"${NTFY_BROKER}" \
request-token "$LOGIN" "$PASSWORD" </dev/null) || {
echo "Error while requesting token to ntfy server" >&2
exit 1
}
## XXXvlab: ideally it should be received from the last call
server="https://ntfy.0k.io/"
login=$(printf "%q" "${cred%$'\n'*}")
password=$(printf "%q" "${cred#*$'\n'}")
## check if password doesn't contain '%'
for var in server login password; do
if [ "${!var}" == "''" ] || [[ "${!var}" == *$'\n'* ]]; then
echo "Error: empty or invalid multi-line values retrieved for '$var'" \
"from ntfy server. Received:" >&2
printf "%s" "$cred" | sed -r 's/^/ | /g' >&2
exit 1
fi
if [[ "${!var}" == *%* ]]; then
## We need a separator char for sed replacement in the config file
echo "Error: forbidden character '%' found in $var" >&2
exit 1
fi
if grep -qE "^${var^^}=" "$config_file"; then
sed -ri "s%^${var^^}=.*$%${var^^}=\"${!var}\"%g" "$config_file"
else
echo "${var^^}=\"${!var}\"" >> "$config_file"
fi
done
if ! [ -f "/etc/ntfy/topics.yml" ]; then
cat <<'EOF' > /etc/ntfy/topics.yml
.*\.(emerg|alert|crit|err|warning|notice):
- ${LOGIN}_main
EOF
fi
## provide 'send' command
cp -f "$PWD/src/bin/send" /usr/local/bin/send

166
docker-host/src/bin/send

@ -0,0 +1,166 @@
#!/bin/bash
## Send a notification with NTFY and check if the config file is complete
if [[ "$UID" == "0" ]]; then
NTFY_CONFIG_DIR="${NTFY_CONFIG_DIR:-/etc/ntfy}"
else
NTFY_CONFIG_DIR="${NTFY_CONFIG_DIR:-~/.config/ntfy}"
fi
NTFY_CONFIG_FILE="$NTFY_CONFIG_DIR/ntfy.conf"
SERVER="https://ntfy.0k.io"
[ -f "$NTFY_CONFIG_DIR/topics.yml" ] || {
echo "Error: no 'topics.yml' file found in $NTFY_CONFIG_DIR" >&2
echo " Please setup the topics for the notification channels in this file." >&2
exit 1
}
if ! [ -e "$NTFY_CONFIG_FILE" ]; then
mkdir -p "${NTFY_CONFIG_FILE%/*}"
## default option to change if needed
echo "SERVER=$SERVER" > "$NTFY_CONFIG_FILE"
elif ! grep -q "^SERVER=" "$NTFY_CONFIG_FILE"; then
echo "SERVER=$SERVER" >> "$NTFY_CONFIG_FILE"
fi
source "$NTFY_CONFIG_FILE" || {
echo "Error: could not source '$NTFY_CONFIG_FILE'" >&2
exit 1
}
SERVER="${SERVER%/}"
for var in SERVER LOGIN PASSWORD; do
if ! [ -v "$var" ]; then
echo "Error: missing $var in $NTFY_CONFIG_FILE"
exit 1
fi
done
exname=${0##*/}
channels=()
usage="$exname [options] MESSAGE"
help="\
Send MESSAGE with TITLE to the differents topics defined by a CHANNEL
$exname will read the $NTFY_CONFIG_DIR/topics.yml for channel to
topics conversion.
Usage:
$usage
Options:
-c CHANNEL Specify one or multiple channels. Default 'main'.
(can be provided mulitiple time)
-t TITLE Specify the title of the message. (it'll still be
prefixed with the hostname)
Default is empty
MESSAGE message to send.
-h Display this help and exit.
"
while [ "$#" -gt 0 ]; do
arg="$1"
shift
case "$arg" in
-h|--help)
echo "$help"
exit 0
;;
-c|--channel)
[ -n "$1" ] || {
echo "Error: no argument for channel option." >&2
echo "$usage" >&2
exit 1
}
IFS=", " channels+=($1)
shift
;;
-t|--title)
[ -n "$1" ] || {
echo "Error: no argument for title option." >&2
echo "$usage" >&2
exit 1
}
title="$1"
shift
;;
*)
[ -z "$message" ] && { message="$arg"; continue; }
echo "Error : Unexpected positional argument '$arg'." >&2
echo "$usage" >&2
exit 1
;;
esac
done
[ -n "$message" ] || {
echo "Error: missing message." >&2
echo "$usage" >&2
exit 1
}
read-0() {
local eof='' IFS=''
while [ "$1" ]; do
read -r -d '' -- "$1" || eof=1
shift
done
[ -z "$eof" ]
}
curl_opts=(
-s
-u "$LOGIN:$PASSWORD"
-d "$message"
)
title="[$(hostname)] $title"
title="${title%%+([[:space:]])}"
curl_opts+=(-H "Title: $title")
declare -A sent_topic=()
if [ "${#channels[@]}" -eq 0 ]; then
channels=("main")
fi
for channel in "${channels[@]}"; do
channel_quoted=$(printf "%q" "$channel")
content=$(cat "$NTFY_CONFIG_DIR/topics.yml")
while read-0 channel_regex topics; do
[[ "$channel" =~ ^$channel_regex$ ]] || continue
rematch=("${BASH_REMATCH[@]}")
while read-0 topic; do
ttopic=$(printf "%s" "$topic" | yq "type")
if [ "$ttopic" != '!!str' ]; then
echo "Error: Unexpected '$ttopic' type for value of channel $channel." >&2
exit 1
fi
topic=$(printf "%s" "$topic" | yq -r " \"\" + .")
if ! [[ "$topic" =~ ^[a-zA-Z0-9\$\{\}*\ \,_.-]+$ ]]; then
echo "Error: Invalid topic value '$topic' expression in $channel channel." >&2
exit 1
fi
new_topics=($(set -- "${rematch[@]}"; eval echo "${topic//\*/\\*}"))
for new_topic in "${new_topics[@]}"; do
[ -n "${sent_topic["$new_topic"]}" ] && continue
sent_topic["$new_topic"]=1
if ! out=$(curl "${curl_opts[@]}" "$SERVER/${new_topic}"); then
echo "Error: could not send message to $new_topic." >&2
echo "curl command:" >&2
echo " curl ${curl_opts[@]} $SERVER/${new_topic}" >&2
echo "$out" | sed 's/^/ | /' >&2
exit 1
fi
done
done < <(printf "%s" "$topics" | yq e -0 '.[]')
done < <(printf "%s" "$content" | yq e -0 'to_entries | .[] | [.key, .value] |.[]')
done

BIN
docker-host/src/etc/ssh/ntfy-key

4
gogocarto/metadata.yml

@ -35,8 +35,8 @@ uses:
solves:
maintenance: "Production scheduled tasks"
default-options: !bash-stdout |
bin_console="dc exec -T -u www-data $MASTER_BASE_SERVICE_NAME \"/opt/apps/$BASE_SERVICE_NAME/bin/console\""
www_data_uid=33
bin_console="dc exec -T -u \"$www_data_uid\" $MASTER_BASE_SERVICE_NAME \"/opt/apps/$BASE_SERVICE_NAME/bin/console\""
scheds=(
checkvote @daily "$bin_console app:elements:checkvote"

2
nextcloud/actions/upgrade

@ -129,7 +129,7 @@ if [ -z "$target" ]; then
info "Target latest version: ${WHITE}${last_available_versions[0]}${NORMAL}"
target=${last_upgradable_versions[0]}
else
if [[ "* $target *" != " ${last_available_versions[*]} " ]]; then
if [[ " ${last_available_versions[*]} " != *" $target "* ]]; then
err "Invalid version $target selected, please specify one of:"
for v in "${last_upgradable_versions[@]}"; do
echo " - $v"

6
nextcloud/hooks/init

@ -76,3 +76,9 @@ uid=$(docker_get_uid "$MASTER_BASE_SERVICE_NAME" "www-data")
dirs=("$SERVICE_DATASTORE/var/lib/nextcloud/data" "$SERVICE_DATASTORE/var/www/html")
mkdir -p "${dirs[@]}"
chown -R "$uid" "${dirs[@]}"
nextcloud:config:simple:add skeletondirectory "" || {
err "Failed to set ${WHITE}skeletondirectory${NORMAL} as empty."
exit 1
}

5
nextcloud/lib/common

@ -150,10 +150,11 @@ nextcloud:config:simple:add() {
return 1
fi
if [ -z "$value" ]; then
err "Value for '$key' is empty. Skipping."
if [ "$#" -lt 2 ]; then
err "No value specified for '$key'."
return 1
fi
## check for \ and ' in value and key
if [[ "$value" =~ [\\\'] ]]; then
err "Unsupported value for '$key' contains a backslash or a single quote."

31
odoo-tecnativa/actions/load

@ -160,26 +160,41 @@ check_input() {
}
}
## Beware that we are not on the host, so we need to create the
## fifo in a place we can share with the curl container.
export TMPDIR=/var/cache/compose
settmpdir RESTORE_TMPDIR
mkfifo "$RESTORE_TMPDIR/fifo"
## Using fifo because if using ``@-`` curl will load all the database
## in memory before sending it, which is NOT desirable as the size of
## the database can be greater than available memory. Using fifo, we
## force the data to be streamed.
cmd=(
docker run -i --rm --network "$container_network"
-v "$RESTORE_TMPDIR/fifo:/tmp/restore/fifo"
"$DEFAULT_CURL_IMAGE"
-sS
-X POST
-F "master_pwd=${ADMIN_PASSWORD}"
-F "backup_file=@-"
-sS
-X POST
-F "master_pwd=${ADMIN_PASSWORD}"
-F "name=${dbname}"
"${curl_opts[@]}"
-F "backup_file=@/tmp/restore/fifo"
http://${container_ip}:8069/web/database/restore
)
)
## XXXvlab: contains password, left only for advanced debug
#debug "${cmd[@]}"
#echo "COMMAND: ${cmd[@]}" >&2
out=$(set -o pipefail; check_input | "${cmd[@]}") || {
"${cmd[@]}" > "$RESTORE_TMPDIR/out" &
pid=$!
check_input > "$RESTORE_TMPDIR/fifo"
wait "$pid" || {
die "Posting to odoo restore API through curl was unsuccessfull."
}
out=$(cat "$RESTORE_TMPDIR/out")
if [[ "$out" == *"<html>"* ]]; then
errmsg=$(echo "$out" | grep "alert-danger")
errmsg=${errmsg#*>}

2
rallly/metadata.yml

@ -1,5 +1,5 @@
docker-image: docker.0k.io/rallly:3.3.0
docker-image: docker.0k.io/rallly:3.7.0
charm-resources:
- /app/docker-start.sh

3
rsync-backup-target/build/Dockerfile

@ -4,7 +4,8 @@ MAINTAINER Valentin Lab <valentin.lab@kalysto.org>
## coreutils is for ``date`` support of ``--rfc-3339=seconds`` argument.
## findutils is for ``find`` support of ``--newermt`` argument.
RUN apk add rsync sudo bash openssh-server coreutils findutils
## gawk is for ``awk`` support of unicode strings.
RUN apk add rsync sudo bash openssh-server coreutils findutils gawk
RUN ssh-keygen -A
## New user/group rsync/rsync with home dir in /var/lib/rsync

9
rsync-backup-target/build/src/usr/local/sbin/ssh-admin-cmd-validate

@ -80,6 +80,15 @@ elif [[ "$SSH_ORIGINAL_COMMAND" =~ ^"ssh-key get-type "[a-zA-Z0-9._-]+$ ]]; then
# echo "Would accept: $SSH_ORIGINAL_COMMAND" >&2
exec sudo /usr/local/sbin/ssh-key get-type "$label" "${ssh_args[@]:2}"
elif [[ "$SSH_ORIGINAL_COMMAND" =~ ^"ssh-key "(enable|disable)" "[a-zA-Z0-9._-]+$ ]]; then
log "ACCEPTED: $SSH_ORIGINAL_COMMAND"
## Interpret \ to allow passing spaces (want to avoid possible issue with \n)
#read -a ssh_args <<< "${SSH_ORIGINAL_COMMAND}"
ssh_args=(${SSH_ORIGINAL_COMMAND})
# echo "Would accept: $SSH_ORIGINAL_COMMAND" >&2
exec sudo /usr/local/sbin/ssh-key "${ssh_args[1]}" "$label" "${ssh_args[@]:2}"
elif [[ "$SSH_ORIGINAL_COMMAND" =~ ^"request-recovery-key "[a-zA-Z0-9._-]+$ ]]; then
log "ACCEPTED: $SSH_ORIGINAL_COMMAND"

151
rsync-backup-target/build/src/usr/local/sbin/ssh-key

@ -25,18 +25,115 @@ DARKPINK="${ANSI_ESC}0;35m"
DARKCYAN="${ANSI_ESC}0;36m"
DARKWHITE="${ANSI_ESC}0;37m"
read-0() {
local eof= IFS=''
while [ "$1" ]; do
read -r -d '' -- "$1" || eof=1
shift
done
[ -z "$eof" ]
}
col:normalize:size() {
local alignment="$1" colors="$2"
# Encode the associative array into a string for awk
local col_colors_string=""
for key in "${!col_colors[@]}"; do
col_colors_string+="${key}=${col_colors[$key]} "
done
# Pass the string to awk
awk -v alignment="$alignment" -v colors="$colors" -v colorStr="$col_colors_string" -v normal="$NORMAL" '
BEGIN {
# Split the color string into key-value pairs
n = split(colorStr, pairs);
for (i = 1; i <= n; i++) {
split(pairs[i], kv, "=");
col_colors[kv[1]] = kv[2];
}
}
{
# Store the entire line in the lines array
lines[NR] = $0;
# Split the line into fields and update max lengths
split($0, fields);
for (i = 1; i <= length(fields); i++) {
if (length(fields[i]) > max[i]) {
max[i] = length(fields[i]);
}
}
}
END {
# Print lines with fields padded to max, apply color
for (i = 1; i <= NR; i++) {
split(lines[i], fields);
line = "";
for (j = 1; j <= length(fields); j++) {
# Determine alignment
align = substr(alignment, j, 1) == "+" ? "+" : "-";
color_code = substr(colors, j, 1);
color_prefix = (color_code != "-" && color_code in col_colors) ? col_colors[color_code] : "";
color_suffix = (color_prefix != "") ? normal : "";
# Construct line with alignment and color
line = line color_prefix sprintf("%" align max[j] "s ", fields[j]) color_suffix;
}
print line;
}
}'
}
declare -A col_colors=(
[s]=${DARKGRAY} ## s for 'slate' (actually gray)
[r]=${DARKRED}
[g]=${DARKGREEN}
[y]=${DARKYELLOW}
[b]=${DARKBLUE}
[p]=${DARKPINK}
[c]=${DARKCYAN}
[w]=${DARKWHITE}
[S]=${GRAY}
[R]=${RED}
[G]=${GREEN}
[Y]=${YELLOW}
[B]=${BLUE}
[P]=${PINK}
[C]=${CYAN}
[W]=${WHITE}
)
ssh-key-ls() {
local label="$1" f content
for f in "${RSYNC_KEY_PATH}"/backup/"$label"/*.pub; do
shift
while read-0 f; do
[ -e "$f" ] || continue
ident=${f##*/}
if [ "$f" != "${f%.disabled}" ]; then
enabled="${RED}x"
ident=${ident%.disabled}
else
enabled="${GREEN}✓"
fi
ident=${ident%.pub}
content=$(cat "$f")
content=$(cat "$f") || return 1
key=${content#* }
key=${key% *}
printf "${DARKGRAY}..${NORMAL}%24s ${DARKCYAN}%s${NORMAL}\n" "${key: -24}" "$ident"
done
commentary=${content##* }
type=${commentary%%@*}
# printf "${DARKGRAY}..${NORMAL}%12s ${DARKPINK}%-7s${NORMAL} ${DARKCYAN}%s${NORMAL}\n" \
# "${key: -12}" "${type}" "$ident"
printf "%s %s %s %s\n" "…${key: -12}" "$type" "$enabled" "$ident"
done < <(find "${RSYNC_KEY_PATH}"/backup/"$label" \
-maxdepth 1 -mindepth 1 \
\( -name \*.pub -or -name \*.pub.disabled \) -print0 | sort -z
) | col:normalize:size "----" "ysGc"
}
@ -45,10 +142,47 @@ ssh-key-rm() {
delete="${RSYNC_KEY_PATH}/backup/$label/$ident.pub"
if ! [ -e "$delete" ]; then
echo "Error: key '$ident' not found." >&2
return 1
if ! [ -e "${delete}.disabled" ]; then
echo "Error: key '$ident' not found." >&2
return 1
fi
rm "${delete}.disabled"
return 0
fi
rm "$delete"
/usr/local/sbin/ssh-update-keys
}
ssh-key-disable() {
local label="$1" ident="$2" disable
disable="${RSYNC_KEY_PATH}/backup/$label/$ident.pub"
if ! [ -e "$disable" ]; then
if ! [ -e "${disable}.disabled" ]; then
echo "Error: key '$ident' not found." >&2
return 1
fi
echo "Already disabled." >&2
return 0
fi
mv "${disable}" "${disable}.disabled"
/usr/local/sbin/ssh-update-keys
}
ssh-key-enable() {
local label="$1" ident="$2" enable
enable="${RSYNC_KEY_PATH}/backup/$label/$ident.pub.disabled"
if ! [ -e "$enable" ]; then
if ! [ -e "${enable%.disabled}" ]; then
echo "Error: key '$ident' not found." >&2
return 1
fi
echo "Already enabled." >&2
return 0
fi
mv "${enable}" "${enable%.disabled}"
/usr/local/sbin/ssh-update-keys
}
@ -141,6 +275,11 @@ case "$1" in
shift
ssh-key-ls "$@"
;;
"disable"|"enable")
arg="$1"
shift
ssh-key-"$arg" "$@"
;;
"get-type")
shift
ssh-key-get-type "$@"

Loading…
Cancel
Save