metascript and printclonedep

will move to metascript

Signed-off-by: philippe lhardy <philippe.lhardy@astrolabe.coop>
This commit is contained in:
2025-11-01 15:12:45 +01:00
parent 65cc0f4c62
commit ab6746ed5c
10 changed files with 1201 additions and 36 deletions

990
lib/metascript.sh Normal file
View File

@@ -0,0 +1,990 @@
#!/bin/bash
# SPDX-FileCopyrightText: 2025 artlog@l0g.eu
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# metascript to include
#
# to generate file header for caller script do :
# bash <relative_path_to>/metascript.sh header
# ex:
# bash lib/metascript.sh header
metascript_version=v1.0.0
# if called directly
if [[ "$0" =~ ^(.*)/metascript.sh$ ]]
then
prefix=${BASH_REMATCH[1]}
case $1 in
header)
cat <<EOF
#!/bin/bash
# SPDX-FileCopyrightText: 2025 artlog@l0g.eu
# SPDX-License-Identifier: AGPL-3.0-or-later
# header generated by $0 $1
# metascript_version=$metascript_version
toolsdir=$prefix
pushd \$toolsdir >/dev/null
toolsdir=\$(pwd)
source metascript.sh
popd >/dev/null
metarun=metarun
# change default to sudo if needed
# metasudo=sudo
while [[ \$# > 0 ]]
do
case "\$1" in
*)
parsemetaarg "\$1"
;;
esac
shift
done
usage
EOF
;;
version)
echo $metascript_version
;;
*)
echo "[WARNING] only header and version arguments are supported. (prefix=$prefix), this script is intended to be included not to be directly called." >&2
exit 1
esac
exit 0
fi
if [[ "$metascript_included" == "yes" ]]
then
log_warn "metascript already included ($0)"
else
metascript_included=yes
# at this step $(pwd) should be this of metascript.sh
# while $0 is this of caller script using metascript
if [[ -z $toolsdir ]]
then
# assume all tools are in lib/
toolsdir=$(dirname $(readlink -f $0))/lib
# all tools resources are relative to this directory
fi
if [[ -z $log_functions ]]
then
log_functions=$toolsdir/log_functions.sh
[[ -f $log_functions ]] || { echo "[FATAL] Missing $log_functions , toolsdir=$toolsdir script=$0" >&2 ; exit 1 ;}
source $log_functions
fi
metascript_usage()
{
cat <<EOF >&2
metascript commands :
help|usage help or usage of this {$0} tool. stop after usage display.
dryrun|show|showdoc display what should/will be done
defersource= script file defining defer() non standard function
defer= defer function to use, default is showdoc
toolsresourcesdir= where to pick resource
default to parent of script $toolsresourcesdir
mostly used with dryrun
apply default : will actual do work without defer
EOF
}
usage()
{
echo "[WARNING} no specific usage function for {$0}, to improve by developer" >&2
echo >&2
metascript_usage
}
showinfo()
{
echo "$@"
}
metarun()
{
$defer $metasudo "$@"
}
autoquoteargs()
{
echo -n "$1"
shift
while [[ $# > 0 ]]
do
if [[ "$1" =~ [\ \$] ]]
then
echo -n " '$1'"
else
echo -n " $1"
fi
shift
done
}
echoarray()
{
declare -a arr=("${@}")
declare -i len=${#arr[@]}
# Show passed array
for ((n = 0; n < len; n++))
do
echo -en " \"${arr[$n]}\""
done
echo
}
deferpipe()
{
cat
echo "# <previous line> | $@"
}
include_source()
{
if [[ -f $1 ]]
then
source $1
else
log_fatal "Missing $1 script"
exit 1
fi
}
showdoc()
{
if [[ $1 =~ ^tools/ ]]
then
# assumes it handles ENV_METASCRIPT_DEFER
"$@"
else
echo '```'
autoquoteargs "$@"
echo
echo '```'
fi
}
redirectto()
{
tofile=$1
if [[ -n $defer ]]
then
echo "Copy to $tofile"
echo '```'
cat
echo '```'
else
cat > $tofile
fi
}
execredirectfrom()
{
fromfile=$1
shift
enforcefile "$fromfile" exists
if [[ -n $defer ]]
then
echo '```'
autoquoteargs "$@"
echo ' < '"$fromfile"
echo '```'
else
"$@" < $fromfile
fi
}
execredirectto()
{
tofile=$1
shift
if [[ -n $defer ]]
then
echo '```'
autoquoteargs "$@"
echo ' > '"$tofile"
echo '```'
else
"$@" > $tofile
fi
}
execredirecttoroot()
{
tofile=$1
shift
if [[ -n $defer ]]
then
echo '```'
autoquoteargs "$@"
echo ' | sudo tee '"$tofile"' >/dev/null'
echo '```'
else
"$@" | sudo tee $tofile >/dev/null
fi
}
execredirectas_to()
{
while [[ $# > 0 ]]
do
case $1 in
user=*)
if [[ "$1" =~ ^user=([a-z]+[a-z0-9]*)$ ]]
then
asuser=${BASH_REMATCH[1]}
else
log_fatal "execredirecttouser missing user= in '$@'"
[[ -n $defer ]] || exit 1
fi
;;
chroot=*)
aschroot=${1/chroot=}
;;
*)
tofile=$1
shift
break
esac
shift
done
local command=()
if [[ -n $aschroot ]]
then
command=(sudo chroot)
if [[ -n $asuser ]]
then
# no group yet
command+=(--userspec=$asuser:$asuser)
fi
command+=($aschroot)
elif [[ -n $asuser ]]
then
command=(sudo -u $asuser)
fi
command+=(tee $tofile)
if [[ -n $defer ]]
then
echo '```'
autoquoteargs "$@"
echo ' | '"${command[@]}"' >/dev/null'
echo '```'
else
"$@" | "${command[@]}" >/dev/null
fi
}
pipeto()
{
if [[ -n $defer ]]
then
echo '```'
echo -n 'cat << EOF| '
autoquoteargs "$@"
echo
cat
echo "EOF"
echo '```'
else
cat | "$@"
fi
}
query_ext()
{
local prompt="$1"
local var=$2
if [[ -n $defer ]]
then
# uppercase it
eval $var=${var^^}
$defer query $prompt "$(eval echo \$$var)"
else
echo -ne $prompt
read $var
fi
}
# two arguments first prompt, second name of var
query_password()
{
local prompt="$1"
local var=$2
if [[ -n $defer ]]
then
# uppercase it
eval $var=${var^^}
$defer query_password "\"$prompt\"" "$(eval echo \$$var)"
else
read -sp "$prompt" $var
echo
fi
}
# echo yes if reply match ^[Yy]([eE][sS]|)$ or no if does not match.
query_yesno()
{
local prompt="$1"
local yesno=no
read -p "$prompt (Yes/No) " yesno
if [[ $yesno =~ ^[Yy]([eE][sS]|)$ ]]
then
echo yes
else
echo no
fi
}
# through hardcoded 'secret' variable
create_secret()
{
# global secret
# declare -g secret
# export -n secret
local -i length=$1
[[ -z $length ]] && length = 32
if (( length < 8 ))
then
log_warn "secret length $length < 8. very small"
fi
# secret=$(echo $RANDOM | md5sum | head -c $length)
secret=$(tr -dc A-Za-z0-9 </dev/urandom | head -c $length)
}
check_missing_dest_dir()
{
local dir=$1
if [[ -n $defer ]]
then
$defer "create $dir if it does not exist"
else
if [[ ! -d $dir ]]
then
echo "[ERROR] '$dir' does not exist please create it."
exit 1
fi
fi
}
sed_substitute_expr()
{
local from="$1"
shift
local to="$1"
shift
local s='/'
if [[ $from =~ [\\] ]]
then
# escape char \ should be doubled
from=${from//\\/\\\\}
fi
if [[ $from =~ $s ]]
then
# echo "[ERROR] character $s is prohibited due to sed usage" >&2
from=${from//$s/\\$s}
fi
if [[ $from =~ \[ ]]
then
from=${from//\[/\\\[}
fi
if [[ $from =~ \* ]]
then
from=${from//\*/\\\*}
fi
if [[ $from =~ ^(.*)\$$ ]]
then
from=${BASH_REMATCH[1]}'\$'
fi
if [[ $from =~ ^\^(.*)$ ]]
then
from='\^'${BASH_REMATCH[1]}
fi
if [[ $to =~ [\\] ]]
then
# escape char \ should be doubled
to=${to//\\/\\\\}
fi
if [[ $to =~ $s ]]
then
# echo "[ERROR] character $s is prohibited due to sed usage" >&2
# echo "This is a limitation of metascript.sh script, replaced by \$s" >&2
to=${to//$s/\\$s}
fi
if [[ $to =~ [\&] ]]
then
# echo "[ERROR] character & is prohibited due to sed usage" >&2
to=${to//\&/\\\&}
fi
# replace it globaly
echo "s$s$from$s$to${s}g"
}
sedreplacefromto()
{
local from="$1"
local to="$2"
shift 2
local sedexpr="$1"
execredirectto $to sed "$sedexpr" $from
shift
while [[ $# > 0 ]]
do
sedexpr="$1"
$defer sed -i "$sedexpr" $to
shift
done
}
replacefromto()
{
local from="$1"
local to="$2"
shift 2
if [[ -n $defer ]]
then
$defer "replace $@ from '$from' into '$to'"
else
local sedexpr=$(sed_substitute_expr "$1" "$2")
execredirectto $to sed "$sedexpr" $from
shift 2
while [[ $# > 0 ]]
do
sedexpr=$(sed_substitute_expr "$1" "$2")
$defer sed -i "$sedexpr" $to
shift 2
done
fi
}
sedreplacein()
{
local file=$1
shift
while [[ $# > 0 ]]
do
$defer sed -i "$1" $file
shift
done
}
replacein()
{
local infile=$1
shift
if [[ -n $defer ]]
then
$defer "replace $@ into '$infile'"
else
while [[ $# > 0 ]]
do
sedexpr=$(sed_substitute_expr "$1" "$2")
$defer sed -i "$sedexpr" $infile
shift 2
done
fi
}
parsemetaarg()
{
case $1 in
apply)
defer=
;;
defersource=*)
defersource=${1/defersource=/}
;;
defer=*)
defer=${1/defer=/}
;;
dryrun|show|showdoc)
defer=showdoc
;;
debug)
set -ex
;;
metasudo=*)
metasudo=${1/metasudo=}
;;
help|usage)
usage
exit 0
;;
toolsresourcesdir=*)
toolsresourcesdir=${1/toolsresourcesdir=/}
;;
scl_enable=*)
scl_args=(scl enable ${1/scl_enable=/} --)
;;
*)
log_error "unrecognized argument '$1'"
usage
exit 1
;;
esac
}
enforcearg()
{
local var="$1"
local default="$2"
eval value='$'"$var"
if [[ -z $value ]]
then
log_error "{$0} expect '$var' to be set ex $var=$default"
if [[ -n $defer ]]
then
[[ -z $default ]] && default="DEFAULT"
log_warn "in defer/dryrun force $var=$default"
eval "$var=$default"
else
usage
exit 1
fi
fi
}
enforce()
{
objecttype=$1
shift
case $objecttype in
file|dir)
object=$1
shift
constraint=$1
case $constraint in
exists|does_not_exist|create_if_needed)
enforce${objecttype} "$object" "$@"
;;
*)
log_error "enforce $objecttype '$object' $@"
log_fatal "constraint '$constraint' unsupported. Please fix the code."
exit 1
;;
esac
;;
var|arg)
enforcearg "$@"
;;
*)
log_error "enforce $objecttype $@"
log_fatal "objectttype $objecttype unsupported"
exit 1
;;
esac
}
enforcefile()
{
local file="$1"
local constraint="$2"
if [[ ! -f "$file" ]]
then
case $constraint in
exists)
log_error "Missing expected $file"
[[ -n $defer ]] || exit 1
;;
create_if_needed)
$metarun touch $file
;;
esac
else
if [[ $constraint = does_not_exist ]]
then
log_error "'$file' already exists. Move it away"
[[ -n $defer ]] || exit 1
fi
fi
}
enforcedir()
{
local dir="$1"
local constraint="$2"
if [[ -e $dir ]]
then
case $constraint in
does_not_exist)
if [[ -d "$dir" ]]
then
log_error "'$dir' already exists"
[[ -n $defer ]] || exit 1
else
log_error "'$dir' already exists and is not a directory as expected"
[[ -n $defer ]] || exit 1
fi
;;
create_if_needed|exists)
if [[ ! -d "$dir" ]]
then
log_error "'$dir' already exists and is not a directory as expected"
[[ -n $defer ]] || exit 1
fi
;;
esac
else
case $constraint in
exists)
log_error "Missing expected directory $dir"
[[ -n $defer ]] || exit 1
;;
create_if_needed)
if [[ ! -d "$dir" ]]
then
$metarun mkdir -p "$dir"
fi
;;
esac
fi
}
applymetaargs()
{
if [[ -n $defer ]]
then
if [[ -n $defersource ]]
then
if [[ -f $defersource ]]
then
log_any "source $defersource"
source $defersource
else
exit_fatal "defersource $defersource provided but not a file"
fi
fi
# $showinfo "generated with $0 $allparms"
export ENV_METASCRIPT_DEFER="$defer"
export ENV_METASCRIPT_RESOURCESDIR="$toolsresourcesdir"
fi
}
defaultmetainit()
{
while [[ $# > 0 ]]
do
parsemetaarg "$1"
shift
done
applymetaargs
}
read_organisation()
{
organisation_file=$(find organisation -name '*.conf')
if [[ -f $organisation_file ]]
then
while read line
do
case $line in
name=*)
organisation_name=${line/name=/}
;;
domain=*)
organisation_domain=${line/domain=/}
;;
ldap_base=*)
organisation_ldap_base=${line/ldap_base=/}
;;
image_keyword=*)
organisation_image_keyword=${line/image_keyword=/}
;;
*)
log_warn "'$line' not recognized as an organisation parameter"
;;
esac
done < $organisation_file
fi
}
check_variable_match()
{
local var="$1"
local match="$2"
local default="$3"
local value=""
eval value='$'"$var"
if [[ -z "$value" ]]
then
echo "set $var to default value $default"
eval "$var=$default"
eval value='$'"$var"
fi
if [[ ! $value =~ $match ]]
then
log_error "$var $value should match $match"
exit 1
fi
}
check_variable_in()
{
local var="$1"
local default="$2"
shift 2
local value=""
local values="$@"
eval value='$'"$var"
if [[ -z "$value" ]]
then
echo "set $var to default value $default"
eval "$var=$default"
eval value='$'"$var"
fi
while [[ $# > 0 ]]
do
if [[ "$value" = "$1" ]]
then
return
fi
shift
done
log_error "'$var' should be within $values it is '$value'"
exit 1
}
check_root()
{
if [[ -n $defer ]]
then
$defer "{$0} script to run run as root or with sudo"
else
[[ $EUID -eq 0 ]] || {
log_error "{$0} You have to be root or use sudo to run this script"
exit 1;
}
fi
}
get_timestamp_second()
{
echo "$(date +"%Y%m%d%H%M%S")"
}
todo()
{
log_any TODO "$@"
}
get_resource_var() {
local varname="$1"
local default_value="$2"
eval value='$'"$varname"
if [[ -z $value ]]
then
echo "# ($0:metascript.sh:$LINENO) $(date)" >>$collect_context
if [[ -z $default_value ]]
then
log_error "resource $varname does not exists and no non empty default provided"
echo "# $varname=<MISSING>" >>$collect_context
exit 1
fi
log_warn "($0) Using default value '$varname'='$default_value' HARDCODED in script : should be fixed with proper default file .resources.var, see traces in $collect_context"
read "$varname" <<<"$default_value"
echo "$varname=$default_value" >>$collect_context
fi
}
setup_resources_var()
{
local resource_var="$1"
enforcefile "$resource_var" exists
if [[ -f $resource_var ]]
then
while read line
do
if [[ $line =~ ^([a-zA-Z0-9_]+)=(.+)$ ]]
then
varname=${BASH_REMATCH[1]}
value=${BASH_REMATCH[2]}
read "$varname" <<<"$value"
elif [[ $line =~ ^# ]]
then
if [[ -n $defer ]]
then
$defer : "$line"
fi
else
log_warn "Invalid syntax in $resource_var '$line' does not match any expected expression"
fi
done <"$resource_var"
fi
}
# allow to check mounted points
mountpoint_get_device()
{
local mount_point="$1"
mount_point=$(readlink -f "$mount_point")
$defer awk "{ if (\$2 == \""$mount_point"\") print \$1 ;}" /proc/mounts
}
device_get_mountpoints()
{
local device="$1"
$defer awk "{ if (\$1 == \""$device"\") print \$2 ;}" /proc/mounts
}
mount_if_needed()
{
local mountdevice="$1"
local mountpoint="$2"
local param="$3"
if [[ -n $defer ]]
then
$defer 'device=$(mountpoint_get_device '"$mountpoint"')'
else
device=$(mountpoint_get_device "$mountpoint")
fi
enforcedir "$mountpoint" exists
# device is a device ...
# enforcefile "$mountdevice" exists
if [[ -z $device ]]
then
$metarun mount $param "$mountdevice" "$mountpoint"
elif [[ "$device" == "$mountdevice" ]]
then
log_info "$device already mounted on $mountdevice"
else
log_warn "Another device $device is mounted on $mountpoint, not $mountdevice"
fi
}
umount_if_needed()
{
local mountdevice="$1"
local mountpoint="$2"
local param="$3"
device=$(mountpoint_get_device "$mountpoint")
if [[ -n "$device" ]]
then
if [[ "$device" == "$mountdevice" ]]
then
$metarun umount "$mountpoint"
else
log_warn "Another device $device is mounted on $mountpoint, not $mountdevice"
if [[ $param == '--bind' ]]
then
log_warn "Unmouning ANYWAY ( mount --bind show root device, not mounted directory )"
$metarun umount "$mountpoint"
fi
fi
else
log_warn "no device found mounted for umount_if_needed $@"
fi
}
exec_bg()
{
if [[ -z $defer ]]
then
"$@" &
else
$defer "$@" '&'
fi
}
# collect all hardcoded values.
mkdir -p ~/.artlog
collect_context=~/.artlog/collect_context.var
if [[ -z $toolsresourcesdir ]]
then
# project directory
# from current $(pwd) will follow parent dir hierarchy to find .resources.var
dir="$(pwd)"
while [[ -n $dir ]] && [[ -d $dir ]] && [[ ! -f $dir/.resources.var ]]
do
new_dir=$(dirname "$dir")
if [[ $new_dir == $dir ]]
then
# protect against infinite loop
break
fi
dir="$new_dir"
done
if [[ -f $dir/.resources.var ]]
then
toolsresourcesdir=$dir
else
if [[ -z $ENV_METASCRIPT_RESOURCESDIR ]]
then
toolsresourcesdir=$toolsparentdir
else
toolsresourcesdir=$ENV_METASCRIPT_RESOURCESDIR
fi
fi
fi
resources_var=$toolsresourcesdir/.resources.var
if [[ -f $resources_var ]]
then
setup_resources_var "$resources_var"
else
log_warn "No $resources_var found"
fi
# quick way to give scl patches to fill scl_arg array
if [[ -f $toolsresourcesdir/.scl_env ]]
then
source $toolsresourcesdir/.scl_env
fi
# empty defer means doit
defer=$ENV_METASCRIPT_DEFER
showinfo=showinfo
allparms="$@"
applymetaargs=applymetaargs
metarun=$defer
# metascript included
fi