1#!/usr/bin/env bash
2# -*- coding: utf-8; mode: sh indent-tabs-mode: nil -*-
3# SPDX-License-Identifier: AGPL-3.0-or-later
4# shellcheck disable=SC2059,SC1117
5
6# ubuntu, debian, arch, fedora, centos ...
7DIST_ID=$(source /etc/os-release; echo "$ID");
8# shellcheck disable=SC2034
9DIST_VERS=$(source /etc/os-release; echo "$VERSION_ID");
10
11ADMIN_NAME="${ADMIN_NAME:-$(git config user.name)}"
12ADMIN_NAME="${ADMIN_NAME:-$USER}"
13
14ADMIN_EMAIL="${ADMIN_EMAIL:-$(git config user.email)}"
15ADMIN_EMAIL="${ADMIN_EMAIL:-$USER@$(hostname)}"
16
17if [[ -z "${REPO_ROOT}" ]]; then
18    REPO_ROOT=$(dirname "${BASH_SOURCE[0]}")
19    while [ -h "${REPO_ROOT}" ] ; do
20        REPO_ROOT=$(readlink "${REPO_ROOT}")
21    done
22    REPO_ROOT=$(cd "${REPO_ROOT}/.." && pwd -P )
23fi
24
25if [[ -z ${TEMPLATES} ]]; then
26    TEMPLATES="${REPO_ROOT}/utils/templates"
27fi
28
29if [[ -z "$CACHE" ]]; then
30    CACHE="${REPO_ROOT}/cache"
31fi
32
33if [[ -z ${DIFF_CMD} ]]; then
34    DIFF_CMD="diff -u"
35    if command -v colordiff >/dev/null;  then
36        DIFF_CMD="colordiff -u"
37    fi
38fi
39
40DOT_CONFIG="${DOT_CONFIG:-${REPO_ROOT}/.config.sh}"
41
42source_dot_config() {
43    if [[ ! -e "${DOT_CONFIG}" ]]; then
44        err_msg "configuration does not exists at: ${DOT_CONFIG}"
45        return 42
46    fi
47    # shellcheck disable=SC1090
48    source "${DOT_CONFIG}"
49}
50
51sudo_or_exit() {
52    # usage: sudo_or_exit
53
54    if [ ! "$(id -u)" -eq 0 ];  then
55        err_msg "this command requires root (sudo) privilege!" >&2
56        exit 42
57    fi
58}
59
60required_commands() {
61
62    # usage:  required_commands [cmd1 ...]
63
64    local exit_val=0
65    while [ -n "$1" ]; do
66
67        if ! command -v "$1" &>/dev/null; then
68            err_msg "missing command $1"
69            exit_val=42
70        fi
71        shift
72    done
73    return $exit_val
74}
75
76# colors
77# ------
78
79# shellcheck disable=SC2034
80set_terminal_colors() {
81    _colors=8
82    _creset='\e[0m'  # reset all attributes
83
84    _Black='\e[0;30m'
85    _White='\e[1;37m'
86    _Red='\e[0;31m'
87    _Green='\e[0;32m'
88    _Yellow='\e[0;33m'
89    _Blue='\e[0;34m'
90    _Violet='\e[0;35m'
91    _Cyan='\e[0;36m'
92
93    _BBlack='\e[1;30m'
94    _BWhite='\e[1;37m'
95    _BRed='\e[1;31m'
96    _BGreen='\e[1;32m'
97    _BYellow='\e[1;33m'
98    _BBlue='\e[1;34m'
99    _BPurple='\e[1;35m'
100    _BCyan='\e[1;36m'
101}
102
103if [ ! -p /dev/stdout ]; then
104    set_terminal_colors
105fi
106
107# reST
108# ----
109
110if command -v fmt >/dev/null; then
111    export FMT="fmt -u"
112else
113    export FMT="cat"
114fi
115
116rst_title() {
117    # usage: rst_title <header-text> [part|chapter|section]
118
119    case ${2-chapter} in
120        part)     printf "\n${_BGreen}${1//?/=}${_creset}\n${_BCyan}${1}${_creset}\n${_BGreen}${1//?/=}${_creset}\n";;
121        chapter)  printf "\n${_BCyan}${1}${_creset}\n${_BGreen}${1//?/=}${_creset}\n";;
122        section)  printf "\n${_BCyan}${1}${_creset}\n${_BGreen}${1//?/-}${_creset}\n";;
123        *)
124            err_msg "invalid argument '${2}' in line $(caller)"
125            return 42
126            ;;
127    esac
128}
129
130rst_para() {
131    # usage:  RST_INDENT=1 rst_para "lorem ipsum ..."
132    local prefix=''
133    if [[ -n $RST_INDENT ]] && [[ $RST_INDENT -gt 0 ]]; then
134        prefix="$(for i in $(seq 1 "$RST_INDENT"); do printf "  "; done)"
135        echo -en "\n$*\n" | $FMT | prefix_stdout "$prefix"
136    else
137        echo -en "\n$*\n" | $FMT
138    fi
139}
140
141die() {
142    echo -e "${_BRed}ERROR:${_creset} ${BASH_SOURCE[1]}: line ${BASH_LINENO[0]}: ${2-died ${1-1}}" >&2;
143    exit "${1-1}"
144}
145
146die_caller() {
147    echo -e "${_BRed}ERROR:${_creset} ${BASH_SOURCE[2]}: line ${BASH_LINENO[1]}: ${FUNCNAME[1]}(): ${2-died ${1-1}}" >&2;
148    exit "${1-1}"
149}
150
151err_msg()  { echo -e "${_BRed}ERROR:${_creset} $*" >&2; }
152warn_msg() { echo -e "${_BBlue}WARN:${_creset}  $*" >&2; }
153info_msg() { echo -e "${_BYellow}INFO:${_creset}  $*" >&2; }
154
155clean_stdin() {
156    if [[ $(uname -s) != 'Darwin' ]]; then
157        while read -r -n1 -t 0.1; do : ; done
158    fi
159}
160
161wait_key(){
162    # usage: wait_key [<timeout in sec>]
163
164    clean_stdin
165    local _t=$1
166    local msg="${MSG}"
167    [[ -z "$msg" ]] && msg="${_Green}** press any [${_BCyan}KEY${_Green}] to continue **${_creset}"
168
169    [[ -n $FORCE_TIMEOUT ]] && _t=$FORCE_TIMEOUT
170    [[ -n $_t ]] && _t="-t $_t"
171    printf "$msg"
172    # shellcheck disable=SC2086
173    read -r -s -n1 $_t
174    echo
175    clean_stdin
176}
177
178ask_yn() {
179    # usage: ask_yn <prompt-text> [Ny|Yn] [<timeout in sec>]
180
181    local EXIT_YES=0 # exit status 0 --> successful
182    local EXIT_NO=1  # exit status 1 --> error code
183
184    local _t=$3
185    [[ -n $FORCE_TIMEOUT ]] && _t=$FORCE_TIMEOUT
186    [[ -n $_t ]] && _t="-t $_t"
187    case "${FORCE_SELECTION:-${2}}" in
188        Y) return ${EXIT_YES} ;;
189        N) return ${EXIT_NO} ;;
190        Yn)
191            local exit_val=${EXIT_YES}
192            local choice="[${_BGreen}YES${_creset}/no]"
193            local default="Yes"
194            ;;
195        *)
196            local exit_val=${EXIT_NO}
197            local choice="[${_BGreen}NO${_creset}/yes]"
198            local default="No"
199            ;;
200    esac
201    echo
202    while true; do
203        clean_stdin
204        printf "$1 ${choice} "
205        # shellcheck disable=SC2086
206        read -r -n1 $_t
207        if [[ -z $REPLY ]]; then
208            printf "$default\n"; break
209        elif [[ $REPLY =~ ^[Yy]$ ]]; then
210            exit_val=${EXIT_YES}
211            printf "\n"
212            break
213        elif [[ $REPLY =~ ^[Nn]$ ]]; then
214            exit_val=${EXIT_NO}
215            printf "\n"
216            break
217        fi
218        _t=""
219        err_msg "invalid choice"
220    done
221    clean_stdin
222    return $exit_val
223}
224
225tee_stderr () {
226
227    # usage::
228    #   tee_stderr 1 <<EOF | python -i
229    #   print("hello")
230    #   EOF
231    #   ...
232    #   >>> print("hello")
233    #    hello
234
235    local _t="0";
236    if [[ -n $1 ]] ; then _t="$1"; fi
237
238    (while read -r line; do
239         # shellcheck disable=SC2086
240         sleep $_t
241         echo -e "$line" >&2
242         echo "$line"
243    done)
244}
245
246prefix_stdout () {
247    # usage: <cmd> | prefix_stdout [prefix]
248
249    local prefix="${_BYellow}-->|${_creset}"
250
251    if [[ -n $1 ]] ; then prefix="$1"; fi
252
253    # shellcheck disable=SC2162
254    (while IFS= read line; do
255        echo -e "${prefix}$line"
256    done)
257}
258
259append_line() {
260
261    # usage: append_line <line> <file>
262    #
263    # Append line if not exists, create file if not exists. E.g::
264    #
265    #     append_line 'source ~/.foo' ~/bashrc
266
267    local LINE=$1
268    local FILE=$2
269    grep -qFs -- "$LINE" "$FILE" || echo "$LINE" >> "$FILE"
270}
271
272cache_download() {
273
274    # usage: cache_download <url> <local-filename>
275
276    local exit_value=0
277
278    if [[ -n ${SUDO_USER} ]]; then
279        sudo -u "${SUDO_USER}" mkdir -p "${CACHE}"
280    else
281        mkdir -p "${CACHE}"
282    fi
283
284    if [[ -f "${CACHE}/$2" ]] ; then
285        info_msg "already cached: $1"
286        info_msg "  --> ${CACHE}/$2"
287    fi
288
289    if [[ ! -f "${CACHE}/$2" ]]; then
290        info_msg "caching: $1"
291        info_msg "  --> ${CACHE}/$2"
292        if [[ -n ${SUDO_USER} ]]; then
293            sudo -u "${SUDO_USER}" wget --progress=bar -O "${CACHE}/$2" "$1" ; exit_value=$?
294        else
295            wget --progress=bar -O "${CACHE}/$2" "$1" ; exit_value=$?
296        fi
297        if [[ ! $exit_value = 0 ]]; then
298            err_msg "failed to download: $1"
299        fi
300    fi
301}
302
303backup_file() {
304
305    # usage: backup_file /path/to/file.foo
306
307    local stamp
308    stamp=$(date +"_%Y%m%d_%H%M%S")
309    info_msg "create backup: ${1}${stamp}"
310    cp -a "${1}" "${1}${stamp}"
311}
312
313choose_one() {
314
315    # usage:
316    #
317    #   DEFAULT_SELECT= 2 \
318    #     choose_one <name> "your selection?" "Coffee" "Coffee with milk"
319
320    local default=${DEFAULT_SELECT-1}
321    local REPLY
322    local env_name=$1 && shift
323    local choice=$1;
324    local max="${#@}"
325    local _t
326    [[ -n $FORCE_TIMEOUT ]] && _t=$FORCE_TIMEOUT
327    [[ -n $_t ]] && _t="-t $_t"
328
329    list=("$@")
330    echo -e "${_BGreen}Menu::${_creset}"
331    for ((i=1; i<= $((max -1)); i++)); do
332        if [[ "$i" == "$default" ]]; then
333            echo -e "  ${_BGreen}$i.${_creset}) ${list[$i]} [default]"
334        else
335            echo -e "  $i.) ${list[$i]}"
336        fi
337    done
338    while true; do
339        clean_stdin
340        printf "$1 [${_BGreen}$default${_creset}] "
341
342        if (( 10 > max )); then
343            # shellcheck disable=SC2086
344            read -r -n1 $_t
345        else
346            # shellcheck disable=SC2086,SC2229
347            read -r $_t
348        fi
349        # selection fits
350        [[ $REPLY =~ ^-?[0-9]+$ ]] && (( REPLY > 0 )) && (( REPLY < max )) && break
351
352        # take default
353        [[ -z $REPLY ]] && REPLY=$default && break
354
355        _t=""
356        err_msg "invalid choice"
357    done
358    eval "$env_name"='${list[${REPLY}]}'
359    echo
360    clean_stdin
361}
362
363install_template() {
364
365    # usage:
366    #
367    #     install_template [--no-eval] [--variant=<name>] \
368    #                      {file} [{owner} [{group} [{chmod}]]]
369    #
370    # E.g. the origin of variant 'raw' of /etc/updatedb.conf is::
371    #
372    #    ${TEMPLATES}/etc/updatedb.conf:raw
373    #
374    # To install variant 'raw' of /etc/updatedb.conf without evaluated
375    # replacements you can use::
376    #
377    #    install_template --variant=raw --no-eval \
378    #                     /etc/updatedb.conf root root 644
379
380    local _reply=""
381    local do_eval=1
382    local variant=""
383    local pos_args=("$0")
384
385    for i in "$@"; do
386        case $i in
387            --no-eval) do_eval=0; shift ;;
388            --variant=*) variant=":${i#*=}"; shift ;;
389            *) pos_args+=("$i") ;;
390        esac
391    done
392
393    local dst="${pos_args[1]}"
394    local template_origin="${TEMPLATES}${dst}${variant}"
395    local template_file="${TEMPLATES}${dst}"
396
397    local owner="${pos_args[2]-$(id -un)}"
398    local group="${pos_args[3]-$(id -gn)}"
399    local chmod="${pos_args[4]-644}"
400
401    info_msg "install (eval=$do_eval): ${dst}"
402    [[ -n $variant ]] && info_msg "variant --> ${variant}"
403
404    if [[ ! -f "${template_origin}" ]] ; then
405        err_msg "${template_origin} does not exists"
406        err_msg "... can't install $dst"
407        wait_key 30
408        return 42
409    fi
410
411    if [[ "$do_eval" == "1" ]]; then
412        template_file="${CACHE}${dst}${variant}"
413        info_msg "BUILD template ${template_file}"
414        if [[ -n ${SUDO_USER} ]]; then
415            sudo -u "${SUDO_USER}" mkdir -p "$(dirname "${template_file}")"
416        else
417            mkdir -p "$(dirname "${template_file}")"
418        fi
419        # shellcheck disable=SC2086
420        eval "echo \"$(cat ${template_origin})\"" > "${template_file}"
421        if [[ -n ${SUDO_USER} ]]; then
422            chown "${SUDO_USER}:${SUDO_USER}" "${template_file}"
423        fi
424    else
425        template_file=$template_origin
426    fi
427
428    mkdir -p "$(dirname "${dst}")"
429
430    if [[ ! -f "${dst}" ]]; then
431        info_msg "install: ${template_file}"
432        sudo -H install -v -o "${owner}" -g "${group}" -m "${chmod}" \
433             "${template_file}" "${dst}" | prefix_stdout
434        return $?
435    fi
436
437    if [[ -f "${dst}" ]] && cmp --silent "${template_file}" "${dst}" ; then
438        info_msg "file ${dst} allready installed"
439        return 0
440    fi
441
442    info_msg "diffrent file ${dst} allready exists on this host"
443
444    while true; do
445        choose_one _reply "choose next step with file $dst" \
446                   "replace file" \
447                   "leave file unchanged" \
448                   "interactiv shell" \
449                   "diff files"
450
451        case $_reply in
452            "replace file")
453                info_msg "install: ${template_file}"
454                sudo -H install -v -o "${owner}" -g "${group}" -m "${chmod}" \
455                     "${template_file}" "${dst}" | prefix_stdout
456                break
457                ;;
458            "leave file unchanged")
459                break
460                ;;
461            "interactiv shell")
462                echo -e "// edit ${_Red}${dst}${_creset} to your needs"
463                echo -e "// exit with [${_BCyan}CTRL-D${_creset}]"
464                sudo -H -u "${owner}" -i
465                $DIFF_CMD "${dst}" "${template_file}"
466                echo
467                echo -e "// ${_BBlack}did you edit file ...${_creset}"
468                echo -en "//  ${_Red}${dst}${_creset}"
469                if ask_yn "//${_BBlack}... to your needs?${_creset}"; then
470                    break
471                fi
472                ;;
473            "diff files")
474                $DIFF_CMD "${dst}" "${template_file}" | prefix_stdout
475        esac
476    done
477}
478
479
480service_is_available() {
481
482    # usage:  service_is_available <URL>
483
484    [[ -z $1 ]] && die_caller 42 "missing argument <URL>"
485    local URL="$1"
486    http_code=$(curl -H 'Cache-Control: no-cache' \
487         --silent -o /dev/null --head --write-out '%{http_code}' --insecure \
488         "${URL}")
489    exit_val=$?
490    if [[ $exit_val = 0 ]]; then
491        info_msg "got $http_code from ${URL}"
492    fi
493    case "$http_code" in
494        404|410|423) exit_val=$http_code;;
495    esac
496    return "$exit_val"
497}
498
499# golang
500# ------
501
502go_is_available() {
503
504    # usage:  go_is_available $SERVICE_USER && echo "go is installed!"
505
506    sudo -i -u "${1}" which go &>/dev/null
507}
508
509install_go() {
510
511    # usage:  install_go "${GO_PKG_URL}" "${GO_TAR}" "${SERVICE_USER}"
512
513    local _svcpr="  ${_Yellow}|${3}|${_creset} "
514
515    rst_title "Install Go in user's HOME" section
516
517    rst_para "download and install go binary .."
518    cache_download "${1}" "${2}"
519
520    tee_stderr 0.1 <<EOF | sudo -i -u "${3}" | prefix_stdout "$_svcpr"
521echo \$PATH
522echo \$GOPATH
523mkdir -p \$HOME/local
524rm -rf \$HOME/local/go
525tar -C \$HOME/local -xzf ${CACHE}/${2}
526EOF
527    sudo -i -u "${3}" <<EOF | prefix_stdout
528! which go >/dev/null &&  echo "ERROR - Go Installation not found in PATH!?!"
529which go >/dev/null &&  go version && echo "congratulations -- Go installation OK :)"
530EOF
531}
532
533# system accounts
534# ---------------
535
536service_account_is_available() {
537
538    # usage:  service_account_is_available "$SERVICE_USER" && echo "OK"
539
540    sudo -i -u "$1" echo \$HOME &>/dev/null
541}
542
543drop_service_account() {
544
545    # usage:  drop_service_account "${SERVICE_USER}"
546
547    rst_title "Drop ${1} HOME" section
548    if ask_yn "Do you really want to drop ${1} home folder?"; then
549        userdel -r -f "${1}" 2>&1 | prefix_stdout
550    else
551        rst_para "Leave HOME folder $(du -sh "${1}") unchanged."
552    fi
553}
554
555interactive_shell(){
556
557    # usage:  interactive_shell "${SERVICE_USER}"
558
559    echo -e "// exit with [${_BCyan}CTRL-D${_creset}]"
560    sudo -H -u "${1}" -i
561}
562
563
564# systemd
565# -------
566
567SYSTEMD_UNITS="${SYSTEMD_UNITS:-/lib/systemd/system}"
568
569systemd_install_service() {
570
571    # usage:  systemd_install_service "${SERVICE_NAME}" "${SERVICE_SYSTEMD_UNIT}"
572
573    rst_title "Install System-D Unit ${1}" section
574    echo
575    install_template "${2}" root root 644
576    wait_key
577    systemd_activate_service "${1}"
578}
579
580systemd_remove_service() {
581
582    # usage:  systemd_remove_service "${SERVICE_NAME}" "${SERVICE_SYSTEMD_UNIT}"
583
584    if ! ask_yn "Do you really want to deinstall systemd unit ${1}?"; then
585        return 42
586    fi
587    systemd_deactivate_service "${1}"
588    rm "${2}"  2>&1 | prefix_stdout
589}
590
591systemd_activate_service() {
592
593    # usage:  systemd_activate_service "${SERVICE_NAME}"
594
595    rst_title "Activate ${1} (service)" section
596    echo
597    tee_stderr <<EOF | bash 2>&1
598systemctl enable  ${1}.service
599systemctl restart ${1}.service
600EOF
601    tee_stderr <<EOF | bash 2>&1
602systemctl status --no-pager ${1}.service
603EOF
604}
605
606systemd_deactivate_service() {
607
608    # usage:  systemd_deactivate_service "${SERVICE_NAME}"
609
610    rst_title "De-Activate ${1} (service)" section
611    echo
612    tee_stderr <<EOF | bash 2>&1 | prefix_stdout
613systemctl stop    ${1}.service
614systemctl disable ${1}.service
615EOF
616}
617
618systemd_restart_service() {
619
620    # usage:  systemd_restart_service "${SERVICE_NAME}"
621
622    rst_title "Restart ${1} (service)" section
623    echo
624    tee_stderr <<EOF | bash 2>&1
625systemctl restart ${1}.service
626EOF
627    tee_stderr <<EOF | bash 2>&1
628systemctl status --no-pager ${1}.service
629EOF
630}
631
632
633# nginx
634# -----
635
636nginx_distro_setup() {
637    # shellcheck disable=SC2034
638
639    NGINX_DEFAULT_SERVER=/etc/nginx/nginx.conf
640
641    # Including *location* directives from a dedicated config-folder into the
642    # server directive is, what fedora and centos (already) does.
643    NGINX_APPS_ENABLED="/etc/nginx/default.d"
644
645    # We add a apps-available folder and linking configurations into the
646    # NGINX_APPS_ENABLED folder.  See also nginx_include_apps_enabled().
647    NGINX_APPS_AVAILABLE="/etc/nginx/default.apps-available"
648
649    case $DIST_ID-$DIST_VERS in
650        ubuntu-*|debian-*)
651            NGINX_PACKAGES="nginx"
652            NGINX_DEFAULT_SERVER=/etc/nginx/sites-available/default
653            ;;
654        arch-*)
655            NGINX_PACKAGES="nginx-mainline"
656            ;;
657        fedora-*|centos-7)
658            NGINX_PACKAGES="nginx"
659            ;;
660        *)
661            err_msg "$DIST_ID-$DIST_VERS: nginx not yet implemented"
662            ;;
663    esac
664}
665nginx_distro_setup
666
667install_nginx(){
668    info_msg "installing nginx ..."
669    pkg_install "${NGINX_PACKAGES}"
670    case $DIST_ID-$DIST_VERS in
671        arch-*|fedora-*|centos-7)
672            systemctl enable nginx
673            systemctl start nginx
674            ;;
675    esac
676}
677
678nginx_is_installed() {
679    command -v nginx &>/dev/null
680}
681
682nginx_reload() {
683
684    info_msg "reload nginx .."
685    echo
686    if ! nginx -t; then
687       err_msg "testing nginx configuration failed"
688       return 42
689    fi
690    systemctl restart nginx
691}
692
693nginx_install_app() {
694
695    # usage:  nginx_install_app [<template option> ...] <myapp.conf>
696    #
697    # <template option>:   see install_template
698
699    local template_opts=()
700    local pos_args=("$0")
701
702    for i in "$@"; do
703        case $i in
704            -*) template_opts+=("$i");;
705            *)  pos_args+=("$i");;
706        esac
707    done
708
709    nginx_include_apps_enabled "${NGINX_DEFAULT_SERVER}"
710
711    install_template "${template_opts[@]}" \
712                     "${NGINX_APPS_AVAILABLE}/${pos_args[1]}" \
713                     root root 644
714    nginx_enable_app "${pos_args[1]}"
715    info_msg "installed nginx app: ${pos_args[1]}"
716}
717
718nginx_include_apps_enabled() {
719
720    # Add the *NGINX_APPS_ENABLED* infrastruture to a nginx server block.  Such
721    # infrastruture is already known from fedora and centos, including apps (location
722    # directives) from the /etc/nginx/default.d folder into the *default* nginx
723    # server.
724
725    # usage: nginx_include_apps_enabled <config-file>
726    #
727    #   config-file: Config file with server directive in.
728
729    [[ -z $1 ]] && die_caller 42 "missing argument <config-file>"
730    local server_conf="$1"
731
732    # include /etc/nginx/default.d/*.conf;
733    local include_directive="include ${NGINX_APPS_ENABLED}/*.conf;"
734    local include_directive_re="^\s*include ${NGINX_APPS_ENABLED}/\*\.conf;"
735
736    info_msg "checking existence: '${include_directive}' in file  ${server_conf}"
737    if grep "${include_directive_re}" "${server_conf}"; then
738        info_msg "OK, already exists."
739        return
740    fi
741
742    info_msg "add missing directive: '${include_directive}'"
743    cp "${server_conf}" "${server_conf}.bak"
744
745    (
746        local line
747        local stage=0
748        while IFS=  read -r line
749        do
750            echo "$line"
751            if [[ $stage = 0 ]]; then
752                if [[ $line =~ ^[[:space:]]*server*[[:space:]]*\{ ]]; then
753                    stage=1
754                fi
755            fi
756
757            if [[ $stage = 1 ]]; then
758                echo "        # Load configuration files for the default server block."
759                echo "        $include_directive"
760                echo ""
761                stage=2
762            fi
763        done < "${server_conf}.bak"
764    ) > "${server_conf}"
765
766}
767
768nginx_remove_app() {
769
770    # usage:  nginx_remove_app <myapp.conf>
771
772    info_msg "remove nginx app: $1"
773    nginx_dissable_app "$1"
774    rm -f "${NGINX_APPS_AVAILABLE}/$1"
775}
776
777nginx_enable_app() {
778
779    # usage:  nginx_enable_app <myapp.conf>
780
781    local CONF="$1"
782
783    info_msg "enable nginx app: ${CONF}"
784    mkdir -p "${NGINX_APPS_ENABLED}"
785    rm -f "${NGINX_APPS_ENABLED}/${CONF}"
786    ln -s "${NGINX_APPS_AVAILABLE}/${CONF}" "${NGINX_APPS_ENABLED}/${CONF}"
787    nginx_reload
788}
789
790nginx_dissable_app() {
791
792    # usage:  nginx_disable_app <myapp.conf>
793
794    local CONF="$1"
795
796    info_msg "disable nginx app: ${CONF}"
797    rm -f "${NGINX_APPS_ENABLED}/${CONF}"
798    nginx_reload
799}
800
801
802# Apache
803# ------
804
805apache_distro_setup() {
806    # shellcheck disable=SC2034
807    case $DIST_ID-$DIST_VERS in
808        ubuntu-*|debian-*)
809            # debian uses the /etc/apache2 path, while other distros use
810            # the apache default at /etc/httpd
811            APACHE_SITES_AVAILABLE="/etc/apache2/sites-available"
812            APACHE_SITES_ENABLED="/etc/apache2/sites-enabled"
813            APACHE_MODULES="/usr/lib/apache2/modules"
814            APACHE_PACKAGES="apache2"
815            ;;
816        arch-*)
817            APACHE_SITES_AVAILABLE="/etc/httpd/sites-available"
818            APACHE_SITES_ENABLED="/etc/httpd/sites-enabled"
819            APACHE_MODULES="modules"
820            APACHE_PACKAGES="apache"
821            ;;
822        fedora-*|centos-7)
823            APACHE_SITES_AVAILABLE="/etc/httpd/sites-available"
824            APACHE_SITES_ENABLED="/etc/httpd/sites-enabled"
825            APACHE_MODULES="modules"
826            APACHE_PACKAGES="httpd"
827            ;;
828        *)
829            err_msg "$DIST_ID-$DIST_VERS: apache not yet implemented"
830            ;;
831    esac
832}
833
834apache_distro_setup
835
836install_apache(){
837    info_msg "installing apache ..."
838    pkg_install "$APACHE_PACKAGES"
839    case $DIST_ID-$DIST_VERS in
840        arch-*|fedora-*|centos-7)
841            if ! grep "IncludeOptional sites-enabled" "/etc/httpd/conf/httpd.conf"; then
842                echo "IncludeOptional sites-enabled/*.conf" >> "/etc/httpd/conf/httpd.conf"
843            fi
844            systemctl enable httpd
845            systemctl start httpd
846            ;;
847    esac
848}
849
850apache_is_installed() {
851    case $DIST_ID-$DIST_VERS in
852        ubuntu-*|debian-*) (command -v apachectl) &>/dev/null;;
853        arch-*) (command -v httpd) &>/dev/null;;
854        fedora-*|centos-7) (command -v httpd) &>/dev/null;;
855    esac
856}
857
858apache_reload() {
859
860    info_msg "reload apache .."
861    echo
862    case $DIST_ID-$DIST_VERS in
863        ubuntu-*|debian-*)
864            sudo -H apachectl configtest
865            sudo -H systemctl force-reload apache2
866            ;;
867        arch-*|fedora-*|centos-7)
868            sudo -H httpd -t
869            sudo -H systemctl force-reload httpd
870            ;;
871    esac
872}
873
874apache_install_site() {
875
876    # usage:  apache_install_site [<template option> ...] <mysite.conf>
877    #
878    # <template option>:   see install_template
879
880    local template_opts=()
881    local pos_args=("$0")
882
883    for i in "$@"; do
884        case $i in
885            -*) template_opts+=("$i");;
886            *)  pos_args+=("$i");;
887        esac
888    done
889
890    install_template "${template_opts[@]}" \
891                     "${APACHE_SITES_AVAILABLE}/${pos_args[1]}" \
892                     root root 644
893    apache_enable_site "${pos_args[1]}"
894    info_msg "installed apache site: ${pos_args[1]}"
895}
896
897apache_remove_site() {
898
899    # usage:  apache_remove_site <mysite.conf>
900
901    info_msg "remove apache site: $1"
902    apache_dissable_site "$1"
903    rm -f "${APACHE_SITES_AVAILABLE}/$1"
904}
905
906apache_enable_site() {
907
908    # usage:  apache_enable_site <mysite.conf>
909
910    local CONF="$1"
911
912    info_msg "enable apache site: ${CONF}"
913
914    case $DIST_ID-$DIST_VERS in
915        ubuntu-*|debian-*)
916            sudo -H a2ensite -q "${CONF}"
917            ;;
918        arch-*)
919            mkdir -p "${APACHE_SITES_ENABLED}"
920            rm -f "${APACHE_SITES_ENABLED}/${CONF}"
921            ln -s "${APACHE_SITES_AVAILABLE}/${CONF}" "${APACHE_SITES_ENABLED}/${CONF}"
922            ;;
923        fedora-*|centos-7)
924            mkdir -p "${APACHE_SITES_ENABLED}"
925            rm -f "${APACHE_SITES_ENABLED}/${CONF}"
926            ln -s "${APACHE_SITES_AVAILABLE}/${CONF}" "${APACHE_SITES_ENABLED}/${CONF}"
927            ;;
928    esac
929    apache_reload
930}
931
932apache_dissable_site() {
933
934    # usage:  apache_disable_site <mysite.conf>
935
936    local CONF="$1"
937
938    info_msg "disable apache site: ${CONF}"
939
940    case $DIST_ID-$DIST_VERS in
941        ubuntu-*|debian-*)
942            sudo -H a2dissite -q "${CONF}"
943            ;;
944        arch-*)
945            rm -f "${APACHE_SITES_ENABLED}/${CONF}"
946            ;;
947        fedora-*|centos-7)
948            rm -f "${APACHE_SITES_ENABLED}/${CONF}"
949            ;;
950    esac
951    apache_reload
952}
953
954# uWSGI
955# -----
956
957uWSGI_SETUP="${uWSGI_SETUP:=/etc/uwsgi}"
958uWSGI_USER=
959uWSGI_GROUP=
960
961# How distros manage uWSGI apps is very different.  From uWSGI POV read:
962# - https://uwsgi-docs.readthedocs.io/en/latest/Management.html
963
964uWSGI_distro_setup() {
965    case $DIST_ID-$DIST_VERS in
966        ubuntu-*|debian-*)
967            # init.d --> /usr/share/doc/uwsgi/README.Debian.gz
968            # For uWSGI debian uses the LSB init process, this might be changed
969            # one day, see https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=833067
970            uWSGI_APPS_AVAILABLE="${uWSGI_SETUP}/apps-available"
971            uWSGI_APPS_ENABLED="${uWSGI_SETUP}/apps-enabled"
972            uWSGI_PACKAGES="uwsgi"
973            ;;
974        arch-*)
975            # systemd --> /usr/lib/systemd/system/uwsgi@.service
976            # For uWSGI archlinux uses systemd template units, see
977            # - http://0pointer.de/blog/projects/instances.html
978            # - https://uwsgi-docs.readthedocs.io/en/latest/Systemd.html#one-service-per-app-in-systemd
979            uWSGI_APPS_AVAILABLE="${uWSGI_SETUP}/apps-archlinux"
980            uWSGI_APPS_ENABLED="${uWSGI_SETUP}"
981            uWSGI_PACKAGES="uwsgi"
982            ;;
983        fedora-*|centos-7)
984            # systemd --> /usr/lib/systemd/system/uwsgi.service
985            # The unit file starts uWSGI in emperor mode (/etc/uwsgi.ini), see
986            # - https://uwsgi-docs.readthedocs.io/en/latest/Emperor.html
987            uWSGI_APPS_AVAILABLE="${uWSGI_SETUP}/apps-available"
988            uWSGI_APPS_ENABLED="${uWSGI_SETUP}.d"
989            uWSGI_PACKAGES="uwsgi"
990            uWSGI_USER="uwsgi"
991            uWSGI_GROUP="uwsgi"
992            ;;
993        *)
994            err_msg "$DIST_ID-$DIST_VERS: uWSGI not yet implemented"
995            ;;
996esac
997}
998
999uWSGI_distro_setup
1000
1001install_uwsgi(){
1002    info_msg "installing uwsgi ..."
1003    pkg_install "$uWSGI_PACKAGES"
1004    case $DIST_ID-$DIST_VERS in
1005        fedora-*|centos-7)
1006            # enable & start should be called once at uWSGI installation time
1007            systemctl enable uwsgi
1008            systemctl restart uwsgi
1009            ;;
1010    esac
1011}
1012
1013uWSGI_restart() {
1014
1015    # usage:  uWSGI_restart() <myapp.ini>
1016
1017    local CONF="$1"
1018
1019    [[ -z $CONF ]] && die_caller 42 "missing argument <myapp.ini>"
1020    info_msg "restart uWSGI service"
1021    case $DIST_ID-$DIST_VERS in
1022        ubuntu-*|debian-*)
1023            # the 'service' method seems broken in that way, that it (re-)starts
1024            # the whole uwsgi process.
1025            service uwsgi restart "${CONF%.*}"
1026            ;;
1027        arch-*)
1028            # restart systemd template instance
1029            if uWSGI_app_available "${CONF}"; then
1030                systemctl restart "uwsgi@${CONF%.*}"
1031            else
1032                info_msg "[uWSGI:systemd-template] ${CONF} not installed (no need to restart)"
1033            fi
1034            ;;
1035        fedora-*|centos-7)
1036            # in emperor mode, just touch the file to restart
1037            if uWSGI_app_enabled "${CONF}"; then
1038                touch "${uWSGI_APPS_ENABLED}/${CONF}"
1039                # it seems, there is a polling time in between touch and restart
1040                # of the service.
1041                sleep 3
1042            else
1043                info_msg "[uWSGI:emperor] ${CONF} not installed (no need to restart)"
1044            fi
1045            ;;
1046        *)
1047            err_msg "$DIST_ID-$DIST_VERS: uWSGI not yet implemented"
1048            return 42
1049            ;;
1050    esac
1051}
1052
1053uWSGI_prepare_app() {
1054
1055    # usage:  uWSGI_prepare_app <myapp.ini>
1056
1057    [[ -z $1 ]] && die_caller 42 "missing argument <myapp.ini>"
1058
1059    local APP="${1%.*}"
1060
1061    case $DIST_ID-$DIST_VERS in
1062        fedora-*|centos-7)
1063            # in emperor mode, the uwsgi user is the owner of the sockets
1064            info_msg "prepare (uwsgi:uwsgi)  /run/uwsgi/app/${APP}"
1065            mkdir -p "/run/uwsgi/app/${APP}"
1066            chown -R "uwsgi:uwsgi"  "/run/uwsgi/app/${APP}"
1067            ;;
1068        *)
1069            info_msg "prepare (${SERVICE_USER}:${SERVICE_GROUP})  /run/uwsgi/app/${APP}"
1070            mkdir -p "/run/uwsgi/app/${APP}"
1071            chown -R "${SERVICE_USER}:${SERVICE_GROUP}"  "/run/uwsgi/app/${APP}"
1072            ;;
1073    esac
1074}
1075
1076
1077uWSGI_app_available() {
1078    # usage:  uWSGI_app_available <myapp.ini>
1079    local CONF="$1"
1080
1081    [[ -z $CONF ]] && die_caller 42 "missing argument <myapp.ini>"
1082    [[ -f "${uWSGI_APPS_AVAILABLE}/${CONF}" ]]
1083}
1084
1085uWSGI_install_app() {
1086
1087    # usage:  uWSGI_install_app [<template option> ...] <myapp.ini>
1088    #
1089    # <template option>:  see install_template
1090
1091    local pos_args=("$0")
1092
1093    for i in "$@"; do
1094        case $i in
1095            -*) template_opts+=("$i");;
1096            *)  pos_args+=("$i");;
1097        esac
1098    done
1099    uWSGI_prepare_app "${pos_args[1]}"
1100    mkdir -p "${uWSGI_APPS_AVAILABLE}"
1101    install_template "${template_opts[@]}" \
1102                     "${uWSGI_APPS_AVAILABLE}/${pos_args[1]}" \
1103                     root root 644
1104    uWSGI_enable_app "${pos_args[1]}"
1105    uWSGI_restart "${pos_args[1]}"
1106    info_msg "uWSGI app: ${pos_args[1]} is installed"
1107}
1108
1109uWSGI_remove_app() {
1110
1111    # usage:  uWSGI_remove_app <myapp.ini>
1112
1113    local CONF="$1"
1114
1115    [[ -z $CONF ]] && die_caller 42 "missing argument <myapp.ini>"
1116    info_msg "remove uWSGI app: ${CONF}"
1117    uWSGI_disable_app "${CONF}"
1118    uWSGI_restart "${CONF}"
1119    rm -f "${uWSGI_APPS_AVAILABLE}/${CONF}"
1120}
1121
1122uWSGI_app_enabled() {
1123    # usage:  uWSGI_app_enabled <myapp.ini>
1124
1125    local exit_val=0
1126    local CONF="$1"
1127
1128    [[ -z $CONF ]] && die_caller 42 "missing argument <myapp.ini>"
1129    case $DIST_ID-$DIST_VERS in
1130        ubuntu-*|debian-*)
1131            [[ -f "${uWSGI_APPS_ENABLED}/${CONF}" ]]
1132            exit_val=$?
1133            ;;
1134        arch-*)
1135            systemctl -q is-enabled "uwsgi@${CONF%.*}"
1136            exit_val=$?
1137            ;;
1138        fedora-*|centos-7)
1139            [[ -f "${uWSGI_APPS_ENABLED}/${CONF}" ]]
1140            exit_val=$?
1141            ;;
1142        *)
1143            # FIXME
1144            err_msg "$DIST_ID-$DIST_VERS: uWSGI not yet implemented"
1145            exit_val=1
1146            ;;
1147    esac
1148    return $exit_val
1149}
1150
1151# shellcheck disable=SC2164
1152uWSGI_enable_app() {
1153
1154    # usage:   uWSGI_enable_app <myapp.ini>
1155
1156    local CONF="$1"
1157
1158    [[ -z $CONF ]] && die_caller 42 "missing argument <myapp.ini>"
1159    case $DIST_ID-$DIST_VERS in
1160        ubuntu-*|debian-*)
1161            mkdir -p "${uWSGI_APPS_ENABLED}"
1162            rm -f "${uWSGI_APPS_ENABLED}/${CONF}"
1163            ln -s "${uWSGI_APPS_AVAILABLE}/${CONF}" "${uWSGI_APPS_ENABLED}/${CONF}"
1164            info_msg "enabled uWSGI app: ${CONF} (restart required)"
1165            ;;
1166        arch-*)
1167            mkdir -p "${uWSGI_APPS_ENABLED}"
1168            rm -f "${uWSGI_APPS_ENABLED}/${CONF}"
1169            ln -s "${uWSGI_APPS_AVAILABLE}/${CONF}" "${uWSGI_APPS_ENABLED}/${CONF}"
1170            systemctl enable "uwsgi@${CONF%.*}"
1171            info_msg "enabled uWSGI app: ${CONF} (restart required)"
1172            ;;
1173        fedora-*|centos-7)
1174            mkdir -p "${uWSGI_APPS_ENABLED}"
1175            rm -f "${uWSGI_APPS_ENABLED}/${CONF}"
1176            ln -s "${uWSGI_APPS_AVAILABLE}/${CONF}" "${uWSGI_APPS_ENABLED}/${CONF}"
1177            chown "${uWSGI_USER}:${uWSGI_GROUP}" "${uWSGI_APPS_ENABLED}/${CONF}"
1178            info_msg "enabled uWSGI app: ${CONF}"
1179            ;;
1180        *)
1181            # FIXME
1182            err_msg "$DIST_ID-$DIST_VERS: uWSGI not yet implemented"
1183            ;;
1184    esac
1185}
1186
1187uWSGI_disable_app() {
1188
1189    # usage:   uWSGI_disable_app <myapp.ini>
1190
1191    local CONF="$1"
1192
1193    [[ -z $CONF ]] && die_caller 42 "missing argument <myapp.ini>"
1194    case $DIST_ID-$DIST_VERS in
1195        ubuntu-*|debian-*)
1196            service uwsgi stop "${CONF%.*}"
1197            rm -f "${uWSGI_APPS_ENABLED}/${CONF}"
1198            info_msg "disabled uWSGI app: ${CONF} (restart uWSGI required)"
1199            ;;
1200        arch-*)
1201            systemctl stop "uwsgi@${CONF%.*}"
1202            systemctl disable "uwsgi@${CONF%.*}"
1203            rm -f "${uWSGI_APPS_ENABLED}/${CONF}"
1204            ;;
1205        fedora-*|centos-7)
1206            # in emperor mode, just remove the app.ini file
1207            rm -f "${uWSGI_APPS_ENABLED}/${CONF}"
1208            ;;
1209        *)
1210            # FIXME
1211            err_msg "$DIST_ID-$DIST_VERS: uWSGI not yet implemented"
1212            ;;
1213    esac
1214}
1215
1216# distro's package manager
1217# ------------------------
1218
1219_apt_pkg_info_is_updated=0
1220
1221pkg_install() {
1222
1223    # usage: TITEL='install foobar' pkg_install foopkg barpkg
1224
1225    rst_title "${TITLE:-installation of packages}" section
1226    echo -e "\npackage(s)::\n"
1227    # shellcheck disable=SC2068
1228    echo "  " $@ | $FMT
1229
1230    if ! ask_yn "Should packages be installed?" Yn 30; then
1231        return 42
1232    fi
1233    case $DIST_ID in
1234        ubuntu|debian)
1235            if [[ $_apt_pkg_info_is_updated == 0 ]]; then
1236                export _apt_pkg_info_is_updated=1
1237                apt update
1238            fi
1239            # shellcheck disable=SC2068
1240            apt-get install -m -y $@
1241            ;;
1242        arch)
1243            # shellcheck disable=SC2068
1244            pacman -Sy --noconfirm $@
1245            ;;
1246        fedora)
1247            # shellcheck disable=SC2068
1248            dnf install -y $@
1249            ;;
1250	centos)
1251            # shellcheck disable=SC2068
1252            yum install -y $@
1253            ;;
1254    esac
1255}
1256
1257pkg_remove() {
1258
1259    # usage: TITEL='remove foobar' pkg_remove foopkg barpkg
1260
1261    rst_title "${TITLE:-remove packages}" section
1262    echo -e "\npackage(s)::\n"
1263    # shellcheck disable=SC2068
1264    echo "  " $@ | $FMT
1265
1266    if ! ask_yn "Should packages be removed (purge)?" Yn 30; then
1267        return 42
1268    fi
1269    case $DIST_ID in
1270        ubuntu|debian)
1271            # shellcheck disable=SC2068
1272            apt-get purge --autoremove --ignore-missing -y $@
1273            ;;
1274        arch)
1275            # shellcheck disable=SC2068
1276            pacman -R --noconfirm $@
1277            ;;
1278        fedora)
1279            # shellcheck disable=SC2068
1280            dnf remove -y $@
1281            ;;
1282	centos)
1283            # shellcheck disable=SC2068
1284            yum remove -y $@
1285            ;;
1286    esac
1287}
1288
1289pkg_is_installed() {
1290
1291    # usage: pkg_is_install foopkg || pkg_install foopkg
1292
1293    case $DIST_ID in
1294        ubuntu|debian)
1295            dpkg -l "$1" &> /dev/null
1296            return $?
1297            ;;
1298        arch)
1299            pacman -Qsq "$1" &> /dev/null
1300            return $?
1301            ;;
1302        fedora)
1303            dnf list -q --installed "$1" &> /dev/null
1304            return $?
1305            ;;
1306	centos)
1307            yum list -q --installed "$1" &> /dev/null
1308            return $?
1309            ;;
1310    esac
1311}
1312
1313# git tooling
1314# -----------
1315
1316# shellcheck disable=SC2164
1317git_clone() {
1318
1319    # usage:
1320    #
1321    #    git_clone <url> <name> [<branch> [<user>]]
1322    #    git_clone <url> <path> [<branch> [<user>]]
1323    #
1324    #  First form uses $CACHE/<name> as destination folder, second form clones
1325    #  into <path>.  If repository is allready cloned, pull from <branch> and
1326    #  update working tree (if needed, the caller has to stash local changes).
1327    #
1328    #    git clone https://github.com/searx/searx searx-src origin/master searxlogin
1329    #
1330
1331    local url="$1"
1332    local dest="$2"
1333    local branch="$3"
1334    local user="$4"
1335    local bash_cmd="bash"
1336    local remote="origin"
1337
1338    if [[ ! "${dest:0:1}" = "/" ]]; then
1339        dest="$CACHE/$dest"
1340    fi
1341
1342    [[ -z $branch ]] && branch=master
1343    [[ -z $user ]] && [[ -n "${SUDO_USER}" ]] && user="${SUDO_USER}"
1344    [[ -n $user ]] && bash_cmd="sudo -H -u $user -i"
1345
1346    if [[ -d "${dest}" ]] ; then
1347        info_msg "already cloned: $dest"
1348        tee_stderr 0.1 <<EOF | $bash_cmd 2>&1 |  prefix_stdout "  ${_Yellow}|$user|${_creset} "
1349cd "${dest}"
1350git checkout -m -B "$branch" --track "$remote/$branch"
1351git pull --all
1352EOF
1353    else
1354        info_msg "clone into: $dest"
1355        tee_stderr 0.1 <<EOF | $bash_cmd 2>&1 |  prefix_stdout "  ${_Yellow}|$user|${_creset} "
1356mkdir -p "$(dirname "$dest")"
1357cd "$(dirname "$dest")"
1358git clone --branch "$branch" --origin "$remote" "$url" "$(basename "$dest")"
1359EOF
1360    fi
1361}
1362
1363# containers
1364# ----------
1365
1366in_container() {
1367    # Test if shell runs in a container.
1368    #
1369    # usage:  in_container && echo "process running inside a LXC container"
1370    #         in_container || echo "process is not running inside a LXC container"
1371    #
1372    # sudo_or_exit
1373    # hint:   Reads init process environment, therefore root access is required!
1374    # to be safe, take a look at the environment of process 1 (/sbin/init)
1375    # grep -qa 'container=lxc' /proc/1/environ
1376
1377    # see lxc_init_container_env
1378    [[ -f /.lxcenv ]]
1379}
1380
1381LXC_ENV_FOLDER=
1382if in_container; then
1383    # shellcheck disable=SC2034
1384    LXC_ENV_FOLDER="lxc-env/$(hostname)/"
1385fi
1386
1387lxc_init_container_env() {
1388
1389    # usage: lxc_init_container_env <name>
1390
1391    # Create a /.lxcenv file in the root folder.  Call this once after the
1392    # container is inital started and before installing any boilerplate stuff.
1393
1394    info_msg "create /.lxcenv in container $1"
1395    cat <<EOF | lxc exec "${1}" -- bash | prefix_stdout "[${_BBlue}${1}${_creset}] "
1396touch "/.lxcenv"
1397ls -l "/.lxcenv"
1398EOF
1399}
1400
1401# apt packages
1402LXC_BASE_PACKAGES_debian="bash git build-essential python3 python3-venv"
1403
1404# pacman packages
1405LXC_BASE_PACKAGES_arch="bash git base-devel python"
1406
1407# dnf packages
1408LXC_BASE_PACKAGES_fedora="bash git @development-tools python"
1409
1410# yum packages
1411LXC_BASE_PACKAGES_centos="bash git python3"
1412
1413case $DIST_ID in
1414    ubuntu|debian) LXC_BASE_PACKAGES="${LXC_BASE_PACKAGES_debian}" ;;
1415    arch)          LXC_BASE_PACKAGES="${LXC_BASE_PACKAGES_arch}" ;;
1416    fedora)        LXC_BASE_PACKAGES="${LXC_BASE_PACKAGES_fedora}" ;;
1417    centos)        LXC_BASE_PACKAGES="${LXC_BASE_PACKAGES_centos}" ;;
1418    *) err_msg "$DIST_ID-$DIST_VERS: pkg_install LXC_BASE_PACKAGES not yet implemented" ;;
1419esac
1420
1421lxc_install_base_packages() {
1422    info_msg "install LXC_BASE_PACKAGES in container $1"
1423    case $DIST_ID in
1424        centos) yum groupinstall "Development Tools" -y  ;;
1425    esac
1426    pkg_install "${LXC_BASE_PACKAGES}"
1427}
1428
1429
1430lxc_image_copy() {
1431
1432    # usage: lxc_image_copy <remote image> <local image>
1433    #
1434    #        lxc_image_copy "images:ubuntu/20.04"  "ubu2004"
1435
1436    if lxc_image_exists "local:${LXC_SUITE[i+1]}"; then
1437        info_msg "image ${LXC_SUITE[i]} already copied --> ${LXC_SUITE[i+1]}"
1438    else
1439        info_msg "copy image locally ${LXC_SUITE[i]} --> ${LXC_SUITE[i+1]}"
1440        lxc image copy "${LXC_SUITE[i]}" local: \
1441            --alias  "${LXC_SUITE[i+1]}" | prefix_stdout
1442    fi
1443}
1444
1445lxc_init_container() {
1446
1447    # usage: lxc_init_container <image name> <container name>
1448
1449    local image_name="$1"
1450    local container_name="$2"
1451
1452    if lxc info "${container_name}" &>/dev/null; then
1453        info_msg "container '${container_name}' already exists"
1454    else
1455        info_msg "create container instance: ${container_name}"
1456        lxc init "local:${image_name}" "${container_name}"
1457    fi
1458}
1459
1460lxc_exists(){
1461
1462    # usage: lxc_exists <name> || echo "container <name> does not exists"
1463
1464    lxc info "$1" &>/dev/null
1465}
1466
1467lxc_image_exists(){
1468    # usage: lxc_image_exists <alias> || echo "image <alias> does locally not exists"
1469
1470    lxc image info "local:$1" &>/dev/null
1471
1472}
1473
1474lxc_delete_container() {
1475
1476    #  usage: lxc_delete_container <container-name>
1477
1478    if lxc info "$1" &>/dev/null; then
1479        info_msg "stop & delete instance ${_BBlue}${1}${_creset}"
1480        lxc stop "$1" &>/dev/null
1481        lxc delete "$1" | prefix_stdout
1482    else
1483        warn_msg "instance '$1' does not exist / can't delete :o"
1484    fi
1485}
1486
1487lxc_delete_local_image() {
1488
1489    #  usage: lxc_delete_local_image <container-name>
1490
1491    info_msg "delete image 'local:$i'"
1492    lxc image delete "local:$i"
1493}
1494
1495
1496# IP
1497# --
1498
1499global_IPs(){
1500    # usage: global_IPS
1501    #
1502    # print list of host's SCOPE global addresses and adapters e.g::
1503    #
1504    #   $ global_IPs
1505    #   enp4s0|192.168.1.127
1506    #   lxdbr0|10.246.86.1
1507    #   lxdbr0|fd42:8c58:2cd:b73f::1
1508
1509    ip -o addr show | sed -nr 's/[0-9]*:\s*([a-z0-9]*).*inet[6]?\s*([a-z0-9.:]*).*scope global.*/\1|\2/p'
1510}
1511
1512primary_ip() {
1513
1514    case $DIST_ID in
1515        arch)
1516            ip -o addr show \
1517                | sed -nr 's/[0-9]*:\s*([a-z0-9]*).*inet[6]?\s*([a-z0-9.:]*).*scope global.*/\2/p' \
1518                | head -n 1
1519            ;;
1520        *)  hostname -I | cut -d' ' -f1 ;;
1521    esac
1522}
1523
1524# URL
1525# ---
1526
1527url_replace_hostname(){
1528
1529    # usage:  url_replace_hostname <url> <new hostname>
1530
1531    # to replace hostname by primary IP::
1532    #
1533    #   url_replace_hostname http://searx-ubu1604/morty $(primary_ip)
1534    #   http://10.246.86.250/morty
1535
1536    # shellcheck disable=SC2001
1537    echo "$1" | sed "s|\(http[s]*://\)[^/]*\(.*\)|\1$2\2|"
1538}
1539