From 9208145f7dfdccf75ac58452d71786cdd1b0d14e Mon Sep 17 00:00:00 2001 From: Endial Fang Date: Tue, 8 Apr 2025 10:49:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E9=BB=98=E8=AE=A4she?= =?UTF-8?q?ll=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 28 +- build.sh | 83 ++++++ prebuilds/opt/colovu/lib/libcommon.sh | 91 +++---- prebuilds/opt/colovu/lib/libfile.sh | 62 ++++- prebuilds/opt/colovu/lib/libfs.sh | 83 ++++-- prebuilds/opt/colovu/lib/liblog.sh | 104 ++++---- prebuilds/opt/colovu/lib/libnet.sh | 36 ++- prebuilds/opt/colovu/lib/libos.sh | 295 ++++++++++++++++++--- prebuilds/opt/colovu/lib/libservice.sh | 257 +++++++++++++++++- prebuilds/opt/colovu/lib/libvalidations.sh | 103 +++++-- prebuilds/opt/colovu/lib/libversion.sh | 47 ++++ 11 files changed, 968 insertions(+), 221 deletions(-) create mode 100644 build.sh create mode 100644 prebuilds/opt/colovu/lib/libversion.sh diff --git a/Makefile b/Makefile index 9940ad4..97a1668 100644 --- a/Makefile +++ b/Makefile @@ -29,39 +29,39 @@ build-arg+=--build-arg LOCAL_URL=http://pkgs.colovu.com/dist build: @echo "Build $(image_name):$(image_tag)" - @docker buildx build --progress plain --force-rm $(build-arg) -t $(image_name):$(image_tag) . + @podman build --progress plain --force-rm $(build-arg) -t $(image_name):$(image_tag) . @echo "Add tag: $(image_name):latest" - @docker tag $(image_name):$(image_tag) $(image_name):latest + @podman tag $(image_name):$(image_tag) $(image_name):latest @echo "Build complete" # 清理悬空的镜像(无TAG)及停止的容器 clearclean: clean @echo "Clean untaged images and stoped containers..." - @docker ps -a | grep "Exited" | awk '{print $$1}' | sort -u | xargs -L 1 docker rm - @docker images | grep '' | awk '{print $$3}' | sort -u | xargs -L 1 docker rmi -f + @podman ps -a | grep "Exited" | awk '{print $$1}' | sort -u | xargs -L 1 podman rm + @podman images | grep '' | awk '{print $$3}' | sort -u | xargs -L 1 podman rmi -f # 为了防止删除前缀名相同的镜像,在过滤条件中加入一个空格进行过滤 clean: @echo "Clean all images for current application..." - @docker images | grep "$(image_name) " | awk '{print $$3}' | sort -u | xargs -L 1 docker rmi -f + @podman images | grep "$(image_name) " | awk '{print $$3}' | sort -u | xargs -L 1 podman rmi -f # 更新所有 colovu 仓库的镜像 upgrade: @echo "Upgrade all images..." - @docker images | grep 'colovu' | grep -v '' | grep -v "latest-" | awk '{print $$1":"$$2}' | sort -u | xargs -L 1 docker pull + @podman images | grep 'colovu' | grep -v '' | grep -v "latest-" | awk '{print $$1":"$$2}' | sort -u | xargs -L 1 podman pull # 推送镜像至华为云 SWR 的 colovu 空间及 registry.colovu.com 的默认空间 push: @echo "Push $(image_name):$(image_tag) to Huawei Cloud SWR colovu space" - @docker tag $(image_name):$(image_tag) swr.cn-north-4.myhuaweicloud.com/colovu/$(image_name):$(image_tag) - @docker push swr.cn-north-4.myhuaweicloud.com/colovu/$(image_name):$(image_tag) + @podman tag $(image_name):$(image_tag) swr.cn-north-4.myhuaweicloud.com/colovu/$(image_name):$(image_tag) + @podman push swr.cn-north-4.myhuaweicloud.com/colovu/$(image_name):$(image_tag) @echo "Push $(image_name):$(image_tag) to registry.colovu.com" - @docker tag $(image_name):$(image_tag) registry.colovu.com/$(image_name):$(image_tag) - @docker push registry.colovu.com/$(image_name):$(image_tag) + @podman tag $(image_name):$(image_tag) registry.colovu.com/$(image_name):$(image_tag) + @podman push registry.colovu.com/$(image_name):$(image_tag) @echo "Push $(image_name):latest to Huawei Cloud SWR colovu space" - @docker tag $(image_name):latest swr.cn-north-4.myhuaweicloud.com/colovu/$(image_name):latest - @docker push swr.cn-north-4.myhuaweicloud.com/colovu/$(image_name):latest + @podman tag $(image_name):latest swr.cn-north-4.myhuaweicloud.com/colovu/$(image_name):latest + @podman push swr.cn-north-4.myhuaweicloud.com/colovu/$(image_name):latest @echo "Push $(image_name):latest to registry.colovu.com" - @docker tag $(image_name):latest registry.colovu.com/$(image_name):latest - @docker push registry.colovu.com/$(image_name):latest + @podman tag $(image_name):latest registry.colovu.com/$(image_name):latest + @podman push registry.colovu.com/$(image_name):latest @echo "Push complete" diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..fb5eae9 --- /dev/null +++ b/build.sh @@ -0,0 +1,83 @@ +#!/bin/bash + +# Ver: 1.0 by Endial Fang (endial@126.com) +# +# Docker 镜像构建脚本 (仅linux/amd64) + +IMAGE_NAME="debian" +REGISTRY_URL="registry.colovu.com/docker-proxy/" +APT_SOURCE="aliyun" + +# 获取镜像TAG +get_image_tag() { + if git rev-parse --git-dir > /dev/null 2>&1; then + if [ -z "$(git status --porcelain)" ]; then + CURRENT_SUBVERSION=$(git rev-parse --short HEAD) + else + CURRENT_SUBVERSION=$(date +%y%m%d-%H%M%S) + fi + IMAGE_TAG=$(git rev-parse --abbrev-ref HEAD | sed -e 's/master/latest/' -e 's/main/latest/')-${CURRENT_SUBVERSION} + else + CURRENT_SUBVERSION=$(date +%y%m%d-%H%M%S) + IMAGE_TAG="latest-${CURRENT_SUBVERSION}" + fi + echo "${IMAGE_TAG}" +} + +# 构建amd64架构镜像 +build() { + local TAG=$(get_image_tag) + echo "Building image ${IMAGE_NAME}:${TAG} (linux/amd64)" + + podman build --platform linux/amd64 \ + --progress plain --force-rm \ + --build-arg REGISTRY_URL=${REGISTRY_URL} \ + --build-arg APT_SOURCE=${APT_SOURCE} \ + --build-arg LOCAL_URL=http://pkgs.colovu.com/dist \ + -t ${IMAGE_NAME}:${TAG} \ + -t ${IMAGE_NAME}:latest \ + . + + echo "Build complete" +} + +# 清理工作空间 +clean() { + echo "Cleaning workspace..." + podman images | grep "${IMAGE_NAME} " | awk '{print $3}' | xargs -L 1 podman rmi -f + podman ps -a | grep "Exited" | awk '{print $1}' | xargs -L 1 podman rm + podman images | grep '' | awk '{print $3}' | xargs -L 1 podman rmi -f +} + +# 推送镜像到colovu仓库 +push_colovu() { + local TAG=$(get_image_tag) + echo "Pushing to registry.colovu.com" + podman tag ${IMAGE_NAME}:${TAG} registry.colovu.com/${IMAGE_NAME}:${TAG} + podman push registry.colovu.com/${IMAGE_NAME}:${TAG} + podman tag ${IMAGE_NAME}:latest registry.colovu.com/${IMAGE_NAME}:latest + podman push registry.colovu.com/${IMAGE_NAME}:latest +} + +# 推送镜像到华为云仓库 +push_huawei() { + local TAG=$(get_image_tag) + echo "Pushing to swr.cn-north-4.myhuaweicloud.com" + podman tag ${IMAGE_NAME}:${TAG} swr.cn-north-4.myhuaweicloud.com/colovu/${IMAGE_NAME}:${TAG} + podman push swr.cn-north-4.myhuaweicloud.com/colovu/${IMAGE_NAME}:${TAG} + podman tag ${IMAGE_NAME}:latest swr.cn-north-4.myhuaweicloud.com/colovu/${IMAGE_NAME}:latest + podman push swr.cn-north-4.myhuaweicloud.com/colovu/${IMAGE_NAME}:latest +} + +main() { + case "$1" in + build) build ;; + clean) clean ;; + push-cv) push_colovu ;; + push-hw) push_huawei ;; + push) push_colovu; push_huawei ;; + *) echo "Usage: $0 {build|clean|push-cv|push-hw|push-all}"; exit 1 ;; + esac +} + +main "$@" \ No newline at end of file diff --git a/prebuilds/opt/colovu/lib/libcommon.sh b/prebuilds/opt/colovu/lib/libcommon.sh index e262d9c..31a30d6 100644 --- a/prebuilds/opt/colovu/lib/libcommon.sh +++ b/prebuilds/opt/colovu/lib/libcommon.sh @@ -1,30 +1,32 @@ #!/bin/bash -# Ver: 1.5 by Endial Fang (endial@126.com) +# Ver: 1.6 by Endial Fang (endial@126.com) # # 通用函数库 # 加载依赖项 -. /colovu/lib/liblog.sh # 日志输出函数库 +source "/opt/colovu/lib/liblog.sh" # 函数列表 # 打印包含包含Logo的欢迎信息 print_welcome_info() { - [[ -n "${APP_NAME:-}" ]] && github_repo="https://github.com/colovu/docker-${APP_NAME}" + [[ -n "${APP_NAME:-}" ]] && github_repo="https://gitee.com/colovu/docker-${APP_NAME}" - LOG_I " ____ _ " - LOG_I " / ___|___ | | _____ ___ _ " - LOG_I "| | / _ \| |/ _ \ \ / / | | | Docker : ${BOLD}${APP_NAME:-undefined}${RESET}" - LOG_I "| |__| (_) | | (_) \ V /| |_| | Version: ${BOLD}${APP_VER:-0.0}${RESET}" - LOG_I " \____\___/|_|\___/ \_/ \__,_| PowerBy: ${BOLD}Endial@126.com${RESET}" - LOG_D " Project Repo: ${github_repo:-}" - LOG_I "" + info " ____ _ " + info " / ___|___ | | _____ ___ _ " + info "| | / _ \| |/ _ \ \ / / | | | Application: ${APP_NAME:-undefined}" + info "| |__| (_) | | (_) \ V /| |_| | Version: ${APP_VER:-0.0.0}" + info " \____\___/|_|\___/ \_/ \__,_| PowerBy: Endial@126.com" + debug " Project Repo: ${github_repo:-}" + info "" } # 根据需要打印欢迎信息 print_image_welcome() { - if [[ ! "$(id -u)" = "0" ]]; then - print_welcome_info + if [[ -z "${DISABLE_WELCOME_MESSAGE:-}" ]]; then + if [[ ! "$(id -u)" = "0" ]]; then + print_welcome_info + fi fi } @@ -34,12 +36,10 @@ print_image_welcome() { print_command_help() { local arg for arg; do - case "$arg" in - -'?'|-H|-h|--help|-help|-V|-v|--version|-version) - exec "$@" - exit - ;; - esac + if [[ "$arg" =~ ^(-[?HhVv]|--help|--version)$ ]]; then + exec "$@" + exit + fi done } @@ -53,23 +53,25 @@ print_command_help() { # ensure_config_file_exist /etc/${APP_NAME} conf.d server.conf ensure_config_file_exist() { local -r base_path="${1:?paths is missing}" - local f="" - local dist="" - - shift 1 - LOG_D "List to check: $@" - while [ "$#" -gt 0 ]; do - f="${1}" - LOG_D " Process \"${f}\"" - if [ -d "${base_path}/${f}" ]; then - dist="$(echo ${base_path}/${f} | sed -e 's/\/etc/\/srv\/conf/g')" - [[ ! -d "${dist}" ]] && LOG_D " Create directory: ${dist}" && mkdir -p "${dist}" - [[ ! -z $(ls -A "${base_path}/${f}") ]] && ensure_config_file_exist "${base_path}/${f}" $(ls -A "${base_path}/${f}") + shift + local files=("$@") + + debug "List to check: ${files[*]}" + for f in "${files[@]}"; do + debug "Processing: ${f}" + local dist="${base_path/$'\/etc'/$'\/srv\/conf'}/${f}" + + if [[ -d "${base_path}/${f}" ]]; then + [[ ! -d "$dist" ]] && { debug "Creating directory: ${dist}"; mkdir -p "$dist"; } + [[ $(ls -A "${base_path}/${f}") ]] && + ensure_config_file_exist "${base_path}/${f}" $(ls -A "${base_path}/${f}") else - dist="$(echo ${base_path}/${f} | sed -e 's/\/etc/\/srv\/conf/g')" - [[ ! -e "${dist}" ]] && LOG_D " Copy: ${base_path}/${f} ===> ${dist}" && cp "${base_path}/${f}" "${dist}" && rm -rf "/srv/conf/${APP_NAME}/.app_init_flag" + [[ ! -e "$dist" ]] && { + debug "Copying: ${base_path}/${f} => ${dist}" + cp "${base_path}/${f}" "$dist" + rm -f "/srv/conf/${APP_NAME}/.app_init_flag" + } fi - shift done } @@ -81,28 +83,19 @@ ensure_config_file_exist() { # 例子: # process_init_files /src/conf/${APP_NAME}/initdb.d/* process_init_files() { - echo local f - for f; do + while IFS= read -r -d '' f; do case "$f" in *.sh) - if [ -x "$f" ]; then - LOG_I "$0: running $f" + if [[ -x "$f" ]]; then + info "$0: executing $f" "$f" else - LOG_I "$0: sourcing $f" - . "$f" + info "$0: sourcing $f" + source "$f" fi ;; - *) LOG_W "$0: ignoring $f" ;; + *) warn "$0: ignoring $f" ;; esac - echo - done -} - -# 检测当前脚本是被直接执行的,还是从其他脚本中使用 "source" 调用的 -is_sourced() { - [ "${#FUNCNAME[@]}" -ge 2 ] \ - && [ "${FUNCNAME[0]}" = 'is_sourced' ] \ - && [ "${FUNCNAME[1]}" = 'source' ] + done < <(find "$@" -type f -print0 2>/dev/null || true) } diff --git a/prebuilds/opt/colovu/lib/libfile.sh b/prebuilds/opt/colovu/lib/libfile.sh index a8c731e..d8a2f88 100644 --- a/prebuilds/opt/colovu/lib/libfile.sh +++ b/prebuilds/opt/colovu/lib/libfile.sh @@ -1,10 +1,10 @@ #!/bin/bash -# Ver: 1.1 by Endial Fang (endial@126.com) +# Ver: 1.2 by Endial Fang (endial@126.com) # # 文件操作函数库 # 加载依赖项 -. /colovu/lib/liblog.sh # 日志输出函数库 +source "/opt/colovu/lib/libos.sh" # 函数列表 @@ -19,7 +19,7 @@ file_env() { local fileVar="${var}_FILE" local def="${2:-}" if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then - LOG_E "Both $var and $fileVar are set (but are exclusive)" + error "Both $var and $fileVar are set (but are exclusive)" exit 1 fi @@ -48,7 +48,10 @@ replace_in_file() { local result - del=$'\001' # Use a non-printable character as a 'sed' delimiter to avoid issues + # 规避使用'sed in-place' 方式操作 + # 1) 部分系统兼容性问题,如从ConfigMap中加载的文件 + # 2) 部分系统不支持"in-place" 方式操作 + local -r del=$'\001' # Use a non-printable character as a 'sed' delimiter to avoid issues if [[ $posix_regex = true ]]; then result="$(sed -E "s${del}${match_regex}${del}${substitute_regex}${del}g" "$filename")" else @@ -57,6 +60,23 @@ replace_in_file() { echo "$result" > "$filename" } +# Replace a regex-matching multiline string in a file +# 使用规则表达式在文件中替换多行数据 +# 参数: +# $1 - 文件名 +# $2 - 正则表达式 +# $3 - 替代数据表达式 +replace_in_file_multiline() { + local filename="${1:?filename is required}" + local match_regex="${2:?match regex is required}" + local substitute_regex="${3:?substitute regex is required}" + + local result + local -r del=$'\001' # Use a non-printable character as a 'sed' delimiter to avoid issues + result="$(perl -pe "BEGIN{undef $/;} s${del}${match_regex}${del}${substitute_regex}${del}sg" "$filename")" + echo "$result" > "$filename" +} + # 使用规则表达式在文件中删除数据 # 参数: # $1 - 文件名 @@ -68,7 +88,9 @@ remove_in_file() { local posix_regex=${3:-true} local result - # 因部分系统兼容性问题,需要防止使用 'sed in-place' 方式操作 + # 规避使用'sed in-place' 方式操作 + # 1) 部分系统兼容性问题,如从ConfigMap中加载的文件 + # 2) 部分系统不支持"in-place" 方式操作 if [[ $posix_regex = true ]]; then result="$(sed -E "/$match_regex/d" "$filename")" else @@ -91,3 +113,33 @@ append_in_file() { result="$(tac "$file" | sed -E "0,/($match_regex)/s||${value}\n\1|" | tac)" echo "$result" > "$file" } + +# 等待日志文件中包含指定的条目 +# 参数: +# $1 - 待查找的条目 +# $2 - 日志文件 +# $3 - 最大重试次数. Default: 12 +# $4 - 重试间隔时间(秒). Default: 5 +wait_for_log_entry() { + local -r entry="${1:-missing entry}" + local -r log_file="${2:-missing log file}" + local -r retries="${3:-12}" + local -r interval_time="${4:-5}" + local attempt=0 + + check_log_file_for_entry() { + if ! grep -qE "$entry" "$log_file"; then + debug "Entry \"${entry}\" still not present in ${log_file} (attempt $((++attempt))/${retries})" + return 1 + fi + } + debug "Checking that ${log_file} log file contains entry \"${entry}\"" + if retry_while check_log_file_for_entry "$retries" "$interval_time"; then + debug "Found entry \"${entry}\" in ${log_file}" + true + else + error "Could not find entry \"${entry}\" in ${log_file} after ${retries} retries" + debug_execute cat "$log_file" + return 1 + fi +} diff --git a/prebuilds/opt/colovu/lib/libfs.sh b/prebuilds/opt/colovu/lib/libfs.sh index 7a9f34b..43110e3 100644 --- a/prebuilds/opt/colovu/lib/libfs.sh +++ b/prebuilds/opt/colovu/lib/libfs.sh @@ -1,10 +1,10 @@ #!/bin/bash -# Ver: 1.2 by Endial Fang (endial@126.com) +# Ver: 1.3 by Endial Fang (endial@126.com) # # 文件管理函数库 # 加载依赖项 -. /colovu/lib/liblog.sh # 日志输出函数库 +source "/opt/colovu/lib/liblog.sh" # 函数列表 @@ -12,24 +12,32 @@ # 参数: # $1 - 文件路径 # $2 - 用户 +# $3 - 用户组 ensure_owned_by() { local path="${1:?path is missing}" local owner="${2:?owner is missing}" + local group="${3:-}" - chown "$owner":"$owner" "$path" + if [[ -n $group ]]; then + chown "$owner":"$group" "$path" + else + chown "$owner":"$owner" "$path" + fi } # 检测目录是否存在,如果不存在则创建,同时修改为指定的用户 # 参数: # $1 - 目录路径 # $2 - 用户 +# $3 - 用户组 ensure_dir_exists() { local dir="${1:?directory is missing}" - local owner="${2:-}" + local owner_user="${2:-}" + local owner_group="${3:-}" - mkdir -p "${dir}" - if [[ -n $owner ]]; then - ensure_owned_by "$dir" "$owner" + [ -d "${dir}" ] || mkdir -p "${dir}" + if [[ -n $owner_user ]]; then + ensure_owned_by "$dir" "$owner_user" "$owner_group" fi } @@ -39,6 +47,8 @@ ensure_dir_exists() { is_dir_empty() { local dir="${1:?missing directory}" + # 计算真实路径,避免符号链接问题 + local -r dir="$(realpath "$path")" if [[ ! -e "$dir" ]] || [[ -z "$(ls -A "$dir")" ]]; then true else @@ -46,23 +56,47 @@ is_dir_empty() { fi } +# 检测挂载的路径是否存在或为空 +# 参数: +# $1 - 文件或路径 +is_mounted_dir_empty() { + local dir="${1:?missing directory}" + + if is_dir_empty "$dir" || find "$dir" -mindepth 1 -maxdepth 1 -not -name ".snapshot" -not -name "lost+found" -exec false {} +; then + true + else + false + fi +} + # 检测指定的路径当前用户是否可写入 # 参数: # $1 - 文件或路径 -# 返回值: -# true / false is_writable() { local file="${1:?missing file}" local dir dir="$(dirname "$file")" - if [[ ( -f "$file" && -w "$file" ) || ( ! -f "$file" && -d "$dir" && -w "$dir" ) ]]; then + if [[ (-f "$file" && -w "$file") || (! -f "$file" && -d "$dir" && -w "$dir") ]]; then true else false fi } +# 生成相对路径 +# 参数: +# $1 - 路径 +# $2 - 基准路径 +relativize() { + local -r path="${1:?missing path}" + local -r base="${2:?missing base}" + + pushd "$base" >/dev/null || exit + realpath -q --no-symlinks --relative-base="$base" "$path" | sed -e 's|^/$|.|' -e 's|^/||' + popd >/dev/null || exit +} + # 循环设置目录中子目录及文件权限 # 参数: # $1 - paths (as a string). @@ -78,7 +112,7 @@ configure_permissions_ownership() { local user="" local group="" - # Validate arguments + # 参数有效性校验 shift 1 while [ "$#" -gt 0 ]; do case "$1" in @@ -99,7 +133,7 @@ configure_permissions_ownership() { group="${1:?missing group}" ;; *) - LOG_E "Invalid command line flag $1" >&2 + error "Invalid command line flag $1" >&2 return 1 ;; esac @@ -109,27 +143,28 @@ configure_permissions_ownership() { read -r -a filepaths <<< "$paths" for p in "${filepaths[@]}"; do if [[ -e "$p" ]]; then - LOG_D "Check $p" + debug "Check $p" + find -L "$p" -printf "" if [[ -n ${dir_mode} ]]; then - LOG_D "Change permissions to ${dir_mode} of directories in $p" - find -L "$p" -type d -exec chmod "$dir_mode" {} \; + debug "Change permissions to ${dir_mode} of directories in $p" + find -L "$p" -type d ! -perm "$dir_mode" -print0 | xargs -r -0 chmod "$dir_mode" fi if [[ -n ${file_mode} ]]; then - LOG_D "Change permissions to ${file_mode} of files in $p" - find -L "$p" -type f -exec chmod "$file_mode" {} \; + debug "Change permissions to ${file_mode} of files in $p" + find -L "$p" -type f ! -perm "$file_mode" -print0 | xargs -r -0 chmod "$file_mode" fi if [[ -n $user ]] && [[ -n ${group} ]]; then - LOG_D "Change ownership to ${user}:${group} of files and directories in $p" - chown -LR "$user":"$group" "$p" + debug "Change ownership to ${user}:${group} of files and directories in $p" + find -L "$p" -print0 | xargs -r -0 chown "${user}:${group}" elif [[ -n $user ]] && [[ -z $group ]]; then - LOG_D "Change user to ${user} of files and directories in $p" - chown -LR "$user" "$p" + debug "Change user to ${user} of files and directories in $p" + find -L "$p" -print0 | xargs -r -0 chown "${user}" elif [[ -z $user ]] && [[ -n $group ]]; then - LOG_D "Change group to ${group} of files and directories in $p" - chgrp -LR "$group" "$p" + debug "Change group to ${group} of files and directories in $p" + find -L "$p" -print0 | xargs -r -0 chgrp "${group}" fi else - LOG_E "$p does not exist" + error "$p does not exist" fi done } diff --git a/prebuilds/opt/colovu/lib/liblog.sh b/prebuilds/opt/colovu/lib/liblog.sh index 983143e..b55ec2b 100644 --- a/prebuilds/opt/colovu/lib/liblog.sh +++ b/prebuilds/opt/colovu/lib/liblog.sh @@ -1,83 +1,97 @@ #!/bin/bash -# Ver: 1.1 by Endial Fang (endial@126.com) +# Ver: 1.2 by Endial Fang (endial@126.com) # # 日志输出函数库 #[[ ${ENV_DEBUG:-false} = true ]] && set -x MODULE="$(basename "$0")" -RESET='\033[0m' -BOLD='\033[1m' - -# 前景色 -BLACK='\033[38;5;0m' -RED='\033[38;5;1m' -GREEN='\033[38;5;2m' -YELLOW='\033[38;5;3m' -BLUE='\033[38;5;4m' -MAGENTA='\033[38;5;5m' -CYAN='\033[38;5;6m' -WHITE='\033[38;5;7m' - -# 背景色 -ON_BLACK='\033[48;5;0m' -ON_RED='\033[48;5;1m' -ON_GREEN='\033[48;5;2m' -ON_YELLOW='\033[48;5;3m' -ON_BLUE='\033[48;5;4m' -ON_MAGENTA='\033[48;5;5m' -ON_CYAN='\033[48;5;6m' -ON_WHITE='\033[48;5;7m' - -# 函数列表 +# 定义颜色代码函数 +get_color_code() { + local color="$1" + case "$color" in + "reset") echo '\033[0m' ;; + "red") echo '\033[38;5;1m' ;; + "green") echo '\033[38;5;2m' ;; + "yellow") echo '\033[38;5;3m' ;; + "blue") echo '\033[38;5;4m' ;; + "magenta") echo '\033[38;5;5m' ;; + "cyan") echo '\033[38;5;6m' ;; + *) echo "" ;; + esac +} # 打印输出到 STDERR 设备 stderr_print() { printf "%b\\n" "${*}" >&2 } +# 判断是否处于调试模式 +is_debug_mode() { + local debug_var="${ENV_DEBUG:-false}" + shopt -s nocasematch + [[ "$debug_var" = 1 || "$debug_var" =~ ^(yes|true)$ ]] +} + # 输出实际日志信息 # 参数: # $1 - 日志信息 -LOG() { - local -r bool="${ENV_DEBUG:-false}" - shopt -s nocasematch - if [[ "$bool" = 1 || "$bool" =~ ^(yes|true)$ ]]; then - debugInfo="${CYAN}${APP_NAME:-}:${MODULE:-}" +log() { + local debug_info + if is_debug_mode; then + debug_info="$(get_color_code "cyan")${APP_NAME:-}:${MODULE:-}" else - debugInfo="${CYAN}${APP_NAME:-}" - fi - stderr_print "${debugInfo} ${MAGENTA}$(date "+%F %T.%3N")${RESET} ${*}" + debug_info="$(get_color_code "cyan")${APP_NAME:-}" + fi + local timestamp="$(get_color_code "magenta")$(date "+%F %T.%3N")$(get_color_code "reset")" + stderr_print "$debug_info $timestamp $*" } # 输出调试类日志信息,尽量少使用 # 参数: # $1 - 日志信息 -LOG_D() { - local -r bool="${ENV_DEBUG:-false}" - shopt -s nocasematch - if [[ "$bool" = 1 || "$bool" =~ ^(yes|true)$ ]]; then - LOG "${BLUE}DBG${RESET}: ${*}" - fi +debug() { + if is_debug_mode; then + log "$(get_color_code "blue")DBG$(get_color_code "reset"): $*" + fi } # 输出提示信息类日志信息 # 参数: # $1 - 日志信息 -LOG_I() { - LOG "${GREEN}INF${RESET}: ${*}" +info() { + log "$(get_color_code "green")INF$(get_color_code "reset"): $*" } # 输出警告类日志信息至sterr # 参数: # $1 - 日志信息 -LOG_W() { - LOG "${YELLOW}WRN${RESET}: ${*}" +warn() { + log "$(get_color_code "yellow")WRN$(get_color_code "reset"): $*" } # 输出错误类日志信息至sterr,并退出脚本 # 参数: # $1 - 日志信息 -LOG_E() { - LOG "${RED}ERR${RESET}: ${*}" +error() { + log "$(get_color_code "red")ERR$(get_color_code "reset"): $*" +} + +# 缩进一个字符串 +# 参数: +# $1 - 待缩进的字符串 (默认为空字符串) +# $2 - 缩进的字符数 (默认: 4) +# $3 - 缩进使用的字符 (默认: " ") +indent() { + local string="${1:-}" + local num="${2:?missing num}" + local char="${3:-" "}" + + # 生成缩进填充的字符串 + local indent_unit="" + for ((i = 0; i < num; i++)); do + indent_unit="${indent_unit}${char}" + done + + echo "$string" | sed "s/^/${indent_unit}/" } diff --git a/prebuilds/opt/colovu/lib/libnet.sh b/prebuilds/opt/colovu/lib/libnet.sh index b550282..c967655 100644 --- a/prebuilds/opt/colovu/lib/libnet.sh +++ b/prebuilds/opt/colovu/lib/libnet.sh @@ -1,10 +1,12 @@ #!/bin/bash -# Ver: 1.2 by Endial Fang (endial@126.com) +# Ver: 1.3 by Endial Fang (endial@126.com) # # 文件管理函数库 # 加载依赖项 -. /colovu/lib/liblog.sh # 日志输出函数库 +source "/opt/colovu/lib/liblog.sh" +source "/opt/colovu/lib/libos.sh" +source "/opt/colovu/lib/libvalidations.sh" # 函数列表 @@ -32,6 +34,7 @@ wait_for_dns_lookup() { local hostname="${1:?hostname is missing}" local retries="${2:-5}" local seconds="${3:-1}" + check_host() { if [[ $(dns_lookup "$hostname") == "" ]]; then false @@ -53,12 +56,17 @@ get_machine_ip() { hostname="$(hostname)" read -r -a ip_addresses <<< "$(dns_lookup "$hostname" | xargs echo)" if [[ "${#ip_addresses[@]}" -gt 1 ]]; then - LOG_W "Found more than one IP address associated to hostname ${hostname}: ${ip_addresses[*]}, will use ${ip_addresses[0]}" + warn "Found more than one IP address associated to hostname ${hostname}: ${ip_addresses[*]}, will use ${ip_addresses[0]}" elif [[ "${#ip_addresses[@]}" -lt 1 ]]; then - LOG_E "Could not find any IP address associated to hostname ${hostname}" + error "Could not find any IP address associated to hostname ${hostname}" exit 1 fi - echo "${ip_addresses[0]}" + # Check if the first IP address is IPv6 to add brackets + if validate_ipv6 "${ip_addresses[0]}" ; then + echo "[${ip_addresses[0]}]" + else + echo "${ip_addresses[0]}" + fi } # 检测指定的主机名是否可解析 @@ -68,6 +76,7 @@ get_machine_ip() { # true / false is_hostname_resolved() { local -r host="${1:?missing value}" + if [[ -n "$(dns_lookup "$host")" ]]; then true else @@ -127,3 +136,20 @@ parse_uri() { esac [[ "$uri" =~ $URI_REGEX ]] && echo "${BASH_REMATCH[${index}]}" } + +# Wait for a HTTP connection to succeed +# 等待 HTTP 连接成功 +# 参数: +# $1 - URL 地址 +# $2 - 最大重试次数 (可选) +# $3 - 重试间隔时间 (可选) +wait_for_http_connection() { + local url="${1:?missing url}" + local retries="${2:-}" + local sleep_time="${3:-}" + + if ! retry_while "debug_execute curl --silent ${url}" "$retries" "$sleep_time"; then + error "Could not connect to ${url}" + return 1 + fi +} diff --git a/prebuilds/opt/colovu/lib/libos.sh b/prebuilds/opt/colovu/lib/libos.sh index 20486b3..36a94ad 100644 --- a/prebuilds/opt/colovu/lib/libos.sh +++ b/prebuilds/opt/colovu/lib/libos.sh @@ -1,10 +1,12 @@ #!/bin/bash -# Ver: 1.3 by Endial Fang (endial@126.com) +# Ver: 1.4 by Endial Fang (endial@126.com) # # 操作系统控制函数库 # 加载依赖项 -. /colovu/lib/libfs.sh # 文件系统函数库 +source "/opt/colovu/lib/liblog.sh" +source "/opt/colovu/lib/libfs.sh" +source "/opt/colovu/lib/libvalidations.sh" # 函数列表 @@ -33,10 +35,10 @@ is_group_exists() { # true / false is_root() { if [[ "$(id -u)" = "0" ]]; then - LOG_D "Run as root." + debug "Run as root." true else - LOG_D "Run as non-root: $(id -u)" + debug "Run as non-root: $(id -u)" false fi } @@ -45,16 +47,22 @@ is_root() { # 参数: # $1 - 用户组 # 标志位: +# -i|--gid - 用户组 ID # -s|--system - 创建系统用户 (uid <= 999) ensure_group_exists() { local group="${1:?group is missing}" + local gid="" local is_system_user=false # 检测标志位 shift 1 while [ "$#" -gt 0 ]; do case "$1" in - -s|--system) + -i | --gid) + shift + gid="${1:?missing gid}" + ;; + -s | --system) is_system_user=true ;; *) @@ -65,8 +73,15 @@ ensure_group_exists() { shift done - if ! is_group_exists "$group"; then + if ! group_exists "$group"; then local -a args=("$group") + if [[ -n "$gid" ]]; then + if group_exists "$gid"; then + error "The GID $gid is already in use." >&2 + return 1 + fi + args+=("--gid" "$gid") + fi $is_system_user && args+=("--system") groupadd "${args[@]}" >/dev/null 2>&1 fi @@ -76,12 +91,16 @@ ensure_group_exists() { # 参数: # $1 - 用户 # 标志位: +# -i|--uid - 用户 ID +# -a|--append-groups - 附加用户组 (逗号分隔) # -g|--group - 用户组 # -h|--home - 用户家目录 # -s|--system - 创建系统用户 (uid <= 999) ensure_user_exists() { local user="${1:?user is missing}" + local uid="" local group="" + local append_groups="" local home="" local is_system_user=false @@ -89,15 +108,23 @@ ensure_user_exists() { shift 1 while [ "$#" -gt 0 ]; do case "$1" in - -g|--group) + -i | --uid) + shift + uid="${1:?missing uid}" + ;; + -g | --group) shift group="${1:?missing group}" ;; - -h|--home) + -a | --append-groups) + shift + append_groups="${1:?missing append_groups}" + ;; + -h | --home) shift home="${1:?missing home directory}" ;; - -s|--system) + -s | --system) is_system_user=true ;; *) @@ -108,9 +135,17 @@ ensure_user_exists() { shift done - if ! is_user_exists "$user"; then + if ! user_exists "$user"; then local -a user_args=("-N" "$user") + if [[ -n "$uid" ]]; then + if user_exists "$uid"; then + error "The UID $uid is already in use." + return 1 + fi + user_args+=("--uid" "$uid") + else $is_system_user && user_args+=("--system") + fi useradd "${user_args[@]}" >/dev/null 2>&1 fi @@ -121,6 +156,15 @@ ensure_user_exists() { usermod -g "$group" "$user" >/dev/null 2>&1 fi + if [[ -n "$append_groups" ]]; then + local -a groups + read -ra groups <<<"$(tr ',;' ' ' <<<"$append_groups")" + for group in "${groups[@]}"; do + ensure_group_exists "$group" + usermod -aG "$group" "$user" >/dev/null 2>&1 + done + fi + if [[ -n "$home" ]]; then mkdir -p "$home" usermod -d "$home" "$user" >/dev/null 2>&1 @@ -192,10 +236,10 @@ get_supported_machine_sizes() { # 转换后的数值 convert_to_mb() { local amount="${1:-}" - if [[ $amount =~ ^([0-9]+)(M|G) ]]; then + if [[ $amount =~ ^([0-9]+)(m|M|g|G) ]]; then size="${BASH_REMATCH[1]}" unit="${BASH_REMATCH[2]}" - if [[ "$unit" = "G" ]]; then + if [[ "$unit" = "g" || "$unit" = "G" ]]; then amount="$((size * 1024))" else amount="$size" @@ -208,13 +252,11 @@ convert_to_mb() { # 参数: # $@ - 待执行的命令 debug_execute() { - local bool="${ENV_DEBUG:-false}" - shopt -s nocasematch - if [[ "$bool" = 1 || "$bool" =~ ^(yes|true)$ ]]; then + if is_boolean_yes "${ENV_DEBUG:-false}"; then "$@" else "$@" >/dev/null 2>&1 - fi + fi } # 重试执行命令 @@ -225,13 +267,13 @@ debug_execute() { # 返回值: # 0 / 1 retry_while() { - local -r cmd="${1:?cmd is missing}" - local -r retries="${2:-12}" - local -r sleep_time="${3:-5}" + local cmd="${1:?cmd is missing}" + local retries="${2:-12}" + local sleep_time="${3:-5}" local return_value=1 - read -r -a command <<< "$cmd" - for ((i = 1 ; i <= retries ; i+=1 )); do + read -r -a command <<<"$cmd" + for ((i = 1; i <= retries; i += 1)); do "${command[@]}" && return_value=0 && break sleep "$sleep_time" done @@ -253,36 +295,42 @@ generate_random_string() { # 检测标志位 while [[ "$#" -gt 0 ]]; do case "$1" in - -t|--type) - shift - type="$1" - ;; - -c|--count) - shift - count="$1" - ;; - *) - echo "Invalid command line flag $1" >&2 - return 1 - ;; + -t | --type) + shift + type="$1" + ;; + -c | --count) + shift + count="$1" + ;; + *) + echo "Invalid command line flag $1" >&2 + return 1 + ;; esac shift done # 检测类型 case "$type" in - ascii) - filter="[:print:]" - ;; - alphanumeric) - filter="a-zA-Z0-9" - ;; - numeric) - filter="0-9" - ;; - *) + ascii) + filter="[:print:]" + ;; + numeric) + filter="0-9" + ;; + alphanumeric) + filter="a-zA-Z0-9" + ;; + alphanumeric+special|special+alphanumeric) + # Limit variety of special characters, so there is a higher chance of containing more alphanumeric characters + # Special characters are harder to write, and it could impact the overall UX if most passwords are too complex + filter='a-zA-Z0-9:@.,/+!=' + ;; + *) echo "Invalid type ${type}" >&2 return 1 + ;; esac # Obtain count + 10 lines from /dev/urandom to ensure that the resulting string has the expected size # Note there is a very small chance of strings starting with EOL character @@ -300,3 +348,164 @@ generate_md5_hash() { local -r str="${1:?missing input string}" echo -n "$str" | md5sum | awk '{print $1}' } + +# 为指定字符串生成 SHA1 值 +# 参数: +# $1 - 字符串 +# $2 - 算法 - 1 (default), 224, 256, 384, 512 +# 返回值: +# 字符串对应的 SHA1 +generate_sha_hash() { + local -r str="${1:?missing input string}" + local -r algorithm="${2:-1}" + echo -n "$str" | "sha${algorithm}sum" | awk '{print $1}' +} + +# 将一个字符串转换为其十六进制表示形式 +# 参数: +# $1 - 字符串 +# 返回值: +# 十六进制表示形式 +convert_to_hex() { + local -r str=${1:?missing input string} + local -i iterator + local char + for ((iterator = 0; iterator < ${#str}; iterator++)); do + char=${str:iterator:1} + printf '%x' "'${char}" + done +} + +# 获取启动时间 +# 返回值: +# 启动时间 +get_boot_time() { + stat /proc --format=%Y +} + +# 获取机器 ID +# 返回值: +# 机器 ID +get_machine_id() { + local machine_id + if [[ -f /etc/machine-id ]]; then + machine_id="$(cat /etc/machine-id)" + fi + if [[ -z "$machine_id" ]]; then + # Fallback to the boot-time, which will at least ensure a unique ID in the current session + machine_id="$(get_boot_time)" + fi + echo "$machine_id" +} + +# 获取根分区的磁盘设备 ID(如: /dev/sda1) +# 返回值: +# 根分区磁盘设备 ID +get_disk_device_id() { + local device_id="" + if grep -q ^/dev /proc/mounts; then + device_id="$(grep ^/dev /proc/mounts | awk '$2 == "/" { print $1 }' | tail -1)" + fi + # If it could not be autodetected, fallback to /dev/sda1 as a default + if [[ -z "$device_id" || ! -b "$device_id" ]]; then + device_id="/dev/sda1" + fi + echo "$device_id" +} + +# 获取根磁盘设备 ID(如: /dev/sda) +# 返回值: +# 根磁盘设备 ID +get_root_disk_device_id() { + get_disk_device_id | sed -E 's/p?[0-9]+$//' +} + +# 获取根磁盘大小 +# 返回值: +# 根磁盘大小(字节) +get_root_disk_size() { + fdisk -l "$(get_root_disk_device_id)" | grep 'Disk.*bytes' | sed -E 's/.*, ([0-9]+) bytes,.*/\1/' || true +} + +# 以指定用户身份运行命令 +# 注意:此函数将使用 chroot 命令来模拟指定用户身份运行命令, +# 参数: +# $1 - 用户(可选) +# $2..$n - 命令及命令参数 +run_as_user() { + run_chroot "$@" +} + +# 以指定用户身份执行命令 +# 注意:此命令将替换当前进程映像, +# 参数: +# $1 - 用户(可选) +# $2..$n - 命令及命令参数 +exec_as_user() { + run_chroot --replace-process "$@" +} + +# 使用 chroot 命令以指定用户身份运行命令 +# 参数: +# $1 - 用户(可选) +# $2..$n - 命令及命令参数 +# 标志: +# -r | --replace-process - 替换当前进程映像 +run_chroot() { + local userspec + local user + local homedir + local replace=false + local -r cwd="$(pwd)" + + # 解析并验证标志位 + while [[ "$#" -gt 0 ]]; do + case "$1" in + -r | --replace-process) + replace=true + ;; + --) + shift + break + ;; + -*) + stderr_print "unrecognized flag $1" + return 1 + ;; + *) + break + ;; + esac + shift + done + + # 解析并验证参数 + if [[ "$#" -lt 2 ]]; then + echo "expected at least 2 arguments" + return 1 + else + userspec=$1 + shift + + # 用户名可以包含组,因此我们解析用户 + user=$(echo "$userspec" | cut -d':' -f1) + fi + + if ! is_root; then + error "Could not switch to '${userspec}': Operation not permitted" + return 1 + fi + + # 获取用户的家目录,因为 chroot 不会正确更新此环境变量,并且一些脚本依赖于它 + homedir=$(eval echo "~${user}") + if [[ ! -d $homedir ]]; then + homedir="${HOME:-/}" + fi + + # 取得 "$@" 的值,以便正确支持 shell 参数扩展 + if [[ "$replace" = true ]]; then + exec chroot --userspec="$userspec" / bash -c "cd ${cwd}; export HOME=${homedir}; exec \"\$@\"" -- "$@" + else + chroot --userspec="$userspec" / bash -c "cd ${cwd}; export HOME=${homedir}; exec \"\$@\"" -- "$@" + fi +} diff --git a/prebuilds/opt/colovu/lib/libservice.sh b/prebuilds/opt/colovu/lib/libservice.sh index 23cc35c..8532b72 100644 --- a/prebuilds/opt/colovu/lib/libservice.sh +++ b/prebuilds/opt/colovu/lib/libservice.sh @@ -1,12 +1,13 @@ #!/bin/bash -# Ver: 1.1 by Endial Fang (endial@126.com) +# Ver: 1.2 by Endial Fang (endial@126.com) # # 服务管理函数库 # shellcheck disable=SC1091 # 加载依赖项 -. /colovu/lib/libvalidations.sh # 数据有效性检测函数库 +source "/opt/colovu/lib/liblog.sh" +source "/opt/colovu/lib/libvalidations.sh" # 函数列表 @@ -88,8 +89,6 @@ generate_cron_conf() { local schedule="* * * * *" local clean="true" - local clean="true" - # 检测标志位 shift 2 while [[ "$#" -gt 0 ]]; do @@ -115,12 +114,24 @@ generate_cron_conf() { mkdir -p /etc/cron.d if "$clean"; then - echo "${schedule} ${run_as} ${cmd}" > /etc/cron.d/"$service_name" + cat > "/etc/cron.d/${service_name}" <> /etc/cron.d/"$service_name" fi } +# 删除指定服务的 cron 配置文件 +# 参数: +# $1 - 服务名称 +remove_cron_conf() { + local service_name="${1:?service name is missing}" + local cron_conf_dir="/etc/monit/conf.d" + + rm -f "${cron_conf_dir}/${service_name}" +} + # 为指定的服务生成 monit 配置文件 # 参数: # $1 - 服务名 @@ -141,9 +152,8 @@ generate_monit_conf() { shift 4 while [[ "$#" -gt 0 ]]; do case "$1" in - --disabled) - shift - disabled="$1" + --disable) + disabled="yes" ;; *) echo "Invalid command line flag ${1}" >&2 @@ -155,7 +165,7 @@ generate_monit_conf() { is_boolean_yes "$disabled" && conf_suffix=".disabled" mkdir -p "$monit_conf_dir" - cat >"${monit_conf_dir}/${service_name}.conf${conf_suffix:-}" < "${monit_conf_dir}/${service_name}.conf${conf_suffix:-}" <"${logrotate_conf_dir}/${service_name}" + cat < "${logrotate_conf_dir}/${service_name}" ${log_path} { ${period} rotate ${rotations} @@ -210,3 +230,220 @@ $(indent "$extra" 2) } EOF } + +# 删除指定服务的 Logrotate 配置文件 +# 参数: +# $1 - 服务名称 +remove_logrotate_conf() { + local service_name="${1:?service name is missing}" + local logrotate_conf_dir="/etc/logrotate.d" + rm -f "${logrotate_conf_dir}/${service_name}" +} + +# 为指定的服务生成 Systemd 配置文件 +# 参数: +# $1 - 服务名称 +# 标志位: +# --custom-service-content - 自定义内容 +# --environment - 环境变量(可选) +# --environment-file - 环境变量文件(可选) +# --exec-start - 启动命令(必须) +# --exec-start-pre - 启动前命令(可选) +# --exec-start-post - 启动后命令(可选) +# --exec-stop - 停止命令(可选) +# --exec-reload - 重载命令(可选) +# --group - 系统组 +# --name - 服务名称(默认为 $1) +# --restart - 重启策略 +# --pid-file - PID 文件 +# --standard-output - 标准输出文件 +# --standard-error - 标准错误文件 +# --success-exit-status - 成功退出状态码 +# --type - 服务类型(默认为 Fork) +# --user - 系统用户 +# --working-directory - 工作目录 +generate_systemd_conf() { + local -r service_name="${1:?service name is missing}" + local -r systemd_units_dir="/etc/systemd/system" + local -r service_file="${systemd_units_dir}/bitnami.${service_name}.service" + # Default values + local name="$service_name" + local type="forking" + local user="" + local group="" + local environment="" + local environment_file="" + local exec_start="" + local exec_start_pre="" + local exec_start_post="" + local exec_stop="" + local exec_reload="" + local restart="always" + local pid_file="" + local standard_output="journal" + local standard_error="" + local limits_content="" + local success_exit_status="" + local custom_service_content="" + local working_directory="" + # Parse CLI flags + shift + while [[ "$#" -gt 0 ]]; do + case "$1" in + --name \ + | --type \ + | --user \ + | --group \ + | --exec-start \ + | --exec-stop \ + | --exec-reload \ + | --restart \ + | --pid-file \ + | --standard-output \ + | --standard-error \ + | --success-exit-status \ + | --custom-service-content \ + | --working-directory \ + ) + var_name="$(echo "$1" | sed -e "s/^--//" -e "s/-/_/g")" + shift + declare "$var_name"="${1:?"${var_name} value is missing"}" + ;; + --limit-*) + [[ -n "$limits_content" ]] && limits_content+=$'\n' + var_name="${1//--limit-}" + shift + limits_content+="Limit${var_name^^}=${1:?"--limit-${var_name} value is missing"}" + ;; + --exec-start-pre) + shift + [[ -n "$exec_start_pre" ]] && exec_start_pre+=$'\n' + exec_start_pre+="ExecStartPre=${1:?"--exec-start-pre value is missing"}" + ;; + --exec-start-post) + shift + [[ -n "$exec_start_post" ]] && exec_start_post+=$'\n' + exec_start_post+="ExecStartPost=${1:?"--exec-start-post value is missing"}" + ;; + --environment) + shift + # It is possible to add multiple environment lines + [[ -n "$environment" ]] && environment+=$'\n' + environment+="Environment=${1:?"--environment value is missing"}" + ;; + --environment-file) + shift + # It is possible to add multiple environment-file lines + [[ -n "$environment_file" ]] && environment_file+=$'\n' + environment_file+="EnvironmentFile=${1:?"--environment-file value is missing"}" + ;; + *) + echo "Invalid command line flag ${1}" >&2 + return 1 + ;; + esac + shift + done + # Validate inputs + local error="no" + if [[ -z "$exec_start" ]]; then + error "The --exec-start option is required" + error="yes" + fi + if [[ "$error" != "no" ]]; then + return 1 + fi + # Generate the Systemd unit + cat > "$service_file" <> "$service_file" <<< "WorkingDirectory=${working_directory}" + fi + if [[ -n "$exec_start_pre" ]]; then + # This variable may contain multiple ExecStartPre= directives + cat >> "$service_file" <<< "$exec_start_pre" + fi + if [[ -n "$exec_start" ]]; then + cat >> "$service_file" <<< "ExecStart=${exec_start}" + fi + if [[ -n "$exec_start_post" ]]; then + # This variable may contain multiple ExecStartPost= directives + cat >> "$service_file" <<< "$exec_start_post" + fi + # Optional stop and reload commands + if [[ -n "$exec_stop" ]]; then + cat >> "$service_file" <<< "ExecStop=${exec_stop}" + fi + if [[ -n "$exec_reload" ]]; then + cat >> "$service_file" <<< "ExecReload=${exec_reload}" + fi + # User and group + if [[ -n "$user" ]]; then + cat >> "$service_file" <<< "User=${user}" + fi + if [[ -n "$group" ]]; then + cat >> "$service_file" <<< "Group=${group}" + fi + # PID file allows to determine if the main process is running properly (for Restart=always) + if [[ -n "$pid_file" ]]; then + cat >> "$service_file" <<< "PIDFile=${pid_file}" + fi + if [[ -n "$restart" ]]; then + cat >> "$service_file" <<< "Restart=${restart}" + fi + # Environment flags + if [[ -n "$environment" ]]; then + # This variable may contain multiple Environment= directives + cat >> "$service_file" <<< "$environment" + fi + if [[ -n "$environment_file" ]]; then + # This variable may contain multiple EnvironmentFile= directives + cat >> "$service_file" <<< "$environment_file" + fi + # Logging + if [[ -n "$standard_output" ]]; then + cat >> "$service_file" <<< "StandardOutput=${standard_output}" + fi + if [[ -n "$standard_error" ]]; then + cat >> "$service_file" <<< "StandardError=${standard_error}" + fi + if [[ -n "$custom_service_content" ]]; then + # This variable may contain multiple miscellaneous directives + cat >> "$service_file" <<< "$custom_service_content" + fi + if [[ -n "$success_exit_status" ]]; then + cat >> "$service_file" <> "$service_file" <> "$service_file" <> "$service_file" <= 0 )); then true else @@ -43,7 +45,8 @@ is_positive_int() { # true / false is_boolean_yes() { local -r bool="${1:-}" - # comparison is performed without regard to the case of alphabetic characters + + # 忽略字母大小写 shopt -s nocasematch if [[ "$bool" = 1 || "$bool" =~ ^(yes|true)$ ]]; then true @@ -59,6 +62,7 @@ is_boolean_yes() { # true / false is_yes_no_value() { local -r bool="${1:-}" + if [[ "$bool" =~ ^(yes|no)$ ]]; then true else @@ -80,6 +84,32 @@ is_true_false_value() { fi } +# 检测数据是否为布尔值 'true/false' +# 参数: +# $1 - 待检测的数据 +is_true_false_value() { + local -r bool="${1:-}" + + if [[ "$bool" =~ ^(true|false)$ ]]; then + true + else + false + fi +} + +# 检测提供的参数是否为布尔值 1/0 +# 参数: +# $1 - 待检测的数据 +is_1_0_value() { + local -r bool="${1:-}" + + if [[ "$bool" =~ ^[10]$ ]]; then + true + else + false + fi +} + # 检测提供的参数是否为空字符串或未定义 # 参数: # $1 - 待检测的数据 @@ -87,6 +117,7 @@ is_true_false_value() { # true / false is_empty_value() { local -r val="${1:-}" + if [[ -z "$val" ]]; then true else @@ -116,7 +147,7 @@ validate_port() { break ;; -*) - LOG_E "unrecognized flag $1" + error "unrecognized flag $1" return 1 ;; *) @@ -127,35 +158,50 @@ validate_port() { done if [[ "$#" -gt 1 ]]; then - LOG_E "too many arguments provided" + error "too many arguments provided" return 2 elif [[ "$#" -eq 0 ]]; then - LOG_E "missing port argument" + error "missing port argument" return 1 else value=$1 fi if [[ -z "$value" ]]; then - LOG_E "the value is empty" + error "the value is empty" return 1 else if ! is_int "$value"; then - LOG_W "value is not an integer" + warn "value is not an integer" return 2 elif [[ "$value" -lt 0 ]]; then - LOG_W "negative value provided" + warn "negative value provided" return 2 elif [[ "$value" -gt 65535 ]]; then - LOG_W "requested port is greater than 65535" + warn "requested port is greater than 65535" return 2 elif [[ "$unprivileged" = 1 && "$value" -lt 1024 ]]; then - LOG_W "privileged port requested" + warn "privileged port requested" return 3 fi fi } +# 检测数据是否为有效的IPv6地址 +# 参数: +# $1 - 待检测的数据 +validate_ipv6() { + local ip="${1:?ip is missing}" + local stat=1 + local full_address_regex='^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$' + local short_address_regex='^((([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4}){0,6}::(([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4}){0,6})$' + + if [[ $ip =~ $full_address_regex || $ip =~ $short_address_regex || $ip == "::" ]]; then + stat=0 + fi + return $stat +} + # 检测数据是否为有效的IPv4地址 # 参数: # $1 - 待检测的数据 @@ -166,7 +212,7 @@ validate_ipv4() { local stat=1 if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then - read -r -a ip_array <<< "$(tr '.' ' ' <<< "$ip")" + read -r -a ip_array <<< "$(tr '.' ' ' <<< "$ip")" [[ ${ip_array[0]} -le 255 && ${ip_array[1]} -le 255 \ && ${ip_array[2]} -le 255 && ${ip_array[3]} -le 255 ]] stat=$? @@ -174,6 +220,21 @@ validate_ipv4() { return $stat } +# 检测数据是否为有效的IP地址 +# 参数: +# $1 - 待检测的数据 +validate_ip() { + local ip="${1:?ip is missing}" + local stat=1 + + if validate_ipv4 "$ip"; then + stat=0 + else + stat=$(validate_ipv6 "$ip") + fi + return $stat +} + # 校验字符串格式 # 标志位: # -min-length - 最小长度 @@ -203,32 +264,22 @@ validate_string() { break ;; -*) - LOG_E "unrecognized flag $1" + error "unrecognized flag $1" return 1 ;; *) - break + string="$1" ;; esac shift done - if [ "$#" -gt 1 ]; then - LOG_E "too many arguments provided" - return 2 - elif [ "$#" -eq 0 ]; then - LOG_W "missing string" - return 1 - else - string=$1 - fi - if [[ "$min_length" -ge 0 ]] && [[ "${#string}" -lt "$min_length" ]]; then - LOG_I "string length is less than $min_length" + info "string length is less than $min_length" return 1 fi if [[ "$max_length" -ge 0 ]] && [[ "${#string}" -gt "$max_length" ]]; then - LOG_I "string length is great than $max_length" + info "string length is great than $max_length" return 1 fi } diff --git a/prebuilds/opt/colovu/lib/libversion.sh b/prebuilds/opt/colovu/lib/libversion.sh new file mode 100644 index 0000000..8638e7e --- /dev/null +++ b/prebuilds/opt/colovu/lib/libversion.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# Ver: 1.0 by Endial Fang (endial@126.com) +# +# 版本管理函数库 + +# 加载依赖项 +source "/opt/colovu/lib/liblog.sh" + +# 函数列表 + +# 获取语义化版本 +# 参数: +# $1 - 版本字符串 +# $2 - 版本部分,1 为 major,2 为 minor,3 为 patch +# 返回值: +# 版本数组,包含 major,minor 和 patch +get_sematic_version () { + local version="${1:?version is required}" + local section="${2:?section is required}" + local -a version_sections + + # 用于解析版本的正则表达式:x.y.z + local -r regex='([0-9]+)(\.([0-9]+)(\.([0-9]+))?)?' + + if [[ "$version" =~ $regex ]]; then + local i=1 + local j=1 + local n=${#BASH_REMATCH[*]} + + while [[ $i -lt $n ]]; do + if [[ -n "${BASH_REMATCH[$i]}" ]] && [[ "${BASH_REMATCH[$i]:0:1}" != '.' ]]; then + version_sections[j]="${BASH_REMATCH[$i]}" + ((j++)) + fi + ((i++)) + done + + local number_regex='^[0-9]+$' + if [[ "$section" =~ $number_regex ]] && (( section > 0 )) && (( section <= 3 )); then + echo "${version_sections[$section]}" + return + else + error "Section allowed values are: 1, 2, and 3" + return 1 + fi + fi +}