From c38a32bc78249174d8ca593d7a33b7db6bf0caf3 Mon Sep 17 00:00:00 2001 From: Endial Fang Date: Thu, 8 Jan 2026 15:13:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=20Debian=20=E7=B3=BB=E7=BB=9F=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- debian/etc/apt/sources/aliyun.sources | 13 + debian/etc/apt/sources/default.sources | 13 + debian/etc/apt/sources/ustc.sources | 13 + debian/usr/local/LICENSE | 21 + debian/usr/local/lib/libcommon.sh | 103 +++++ debian/usr/local/lib/libfile.sh | 145 +++++++ debian/usr/local/lib/libfs.sh | 170 ++++++++ debian/usr/local/lib/liblog.sh | 97 +++++ debian/usr/local/lib/libnet.sh | 155 ++++++++ debian/usr/local/lib/libos.sh | 511 +++++++++++++++++++++++++ debian/usr/local/lib/libservice.sh | 449 ++++++++++++++++++++++ debian/usr/local/lib/libvalidations.sh | 285 ++++++++++++++ debian/usr/local/lib/libversion.sh | 47 +++ debian/usr/local/sbin/download_pkg | 183 +++++++++ debian/usr/local/sbin/install_pkg | 70 ++++ debian/usr/local/sbin/select_source | 31 ++ 16 files changed, 2306 insertions(+) create mode 100644 debian/etc/apt/sources/aliyun.sources create mode 100644 debian/etc/apt/sources/default.sources create mode 100644 debian/etc/apt/sources/ustc.sources create mode 100644 debian/usr/local/LICENSE create mode 100644 debian/usr/local/lib/libcommon.sh create mode 100644 debian/usr/local/lib/libfile.sh create mode 100644 debian/usr/local/lib/libfs.sh create mode 100644 debian/usr/local/lib/liblog.sh create mode 100644 debian/usr/local/lib/libnet.sh create mode 100644 debian/usr/local/lib/libos.sh create mode 100644 debian/usr/local/lib/libservice.sh create mode 100644 debian/usr/local/lib/libvalidations.sh create mode 100644 debian/usr/local/lib/libversion.sh create mode 100755 debian/usr/local/sbin/download_pkg create mode 100755 debian/usr/local/sbin/install_pkg create mode 100755 debian/usr/local/sbin/select_source diff --git a/debian/etc/apt/sources/aliyun.sources b/debian/etc/apt/sources/aliyun.sources new file mode 100644 index 0000000..2f187ea --- /dev/null +++ b/debian/etc/apt/sources/aliyun.sources @@ -0,0 +1,13 @@ +Types: deb +# http://snapshot.debian.org/archive/debian/20230703T000000Z +URIs: http://mirrors.aliyun.com/debian +Suites: bookworm bookworm-updates +Components: main +Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg + +Types: deb +# http://snapshot.debian.org/archive/debian-security/20230703T000000Z +URIs: http://mirrors.aliyun.com/debian-security +Suites: bookworm-security +Components: main +Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg \ No newline at end of file diff --git a/debian/etc/apt/sources/default.sources b/debian/etc/apt/sources/default.sources new file mode 100644 index 0000000..961921b --- /dev/null +++ b/debian/etc/apt/sources/default.sources @@ -0,0 +1,13 @@ +Types: deb +# http://snapshot.debian.org/archive/debian/20230703T000000Z +URIs: http://deb.debian.org/debian +Suites: bookworm bookworm-updates +Components: main +Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg + +Types: deb +# http://snapshot.debian.org/archive/debian-security/20230703T000000Z +URIs: http://deb.debian.org/debian-security +Suites: bookworm-security +Components: main +Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg diff --git a/debian/etc/apt/sources/ustc.sources b/debian/etc/apt/sources/ustc.sources new file mode 100644 index 0000000..baf9786 --- /dev/null +++ b/debian/etc/apt/sources/ustc.sources @@ -0,0 +1,13 @@ +Types: deb +# http://snapshot.debian.org/archive/debian/20230703T000000Z +URIs: http://mirrors.ustc.edu.cn/debian +Suites: bookworm bookworm-updates +Components: main +Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg + +Types: deb +# http://snapshot.debian.org/archive/debian-security/20230703T000000Z +URIs: http://mirrors.ustc.edu.cn/debian-security +Suites: bookworm-security +Components: main +Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg \ No newline at end of file diff --git a/debian/usr/local/LICENSE b/debian/usr/local/LICENSE new file mode 100644 index 0000000..80e3bb7 --- /dev/null +++ b/debian/usr/local/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Endial Fang (endial@126.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/debian/usr/local/lib/libcommon.sh b/debian/usr/local/lib/libcommon.sh new file mode 100644 index 0000000..543a898 --- /dev/null +++ b/debian/usr/local/lib/libcommon.sh @@ -0,0 +1,103 @@ +#!/bin/bash +# Ver: 1.6 by Endial Fang (endial@126.com) +# +# 通用函数库 + +# 加载依赖项 +source "/usr/local/lib/liblog.sh" + +BOLD='\033[1m' + +# 函数列表 + +# 打印包含包含Logo的欢迎信息 +print_welcome_info() { + [[ -n "${APP_NAME:-}" ]] && github_repo="https://gitee.com/colovu/docker-${APP_NAME}" + + 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 [[ -z "${DISABLE_WELCOME_MESSAGE:-}" ]]; then + if [[ ! "$(id -u)" = "0" ]]; then + print_welcome_info + fi + fi +} + +# 检测可能导致容器执行后直接退出的命令,如"--help";如果存在,直接返回 0 +# 参数: +# $1 - 待检测的参数表 +print_command_help() { + local arg + for arg; do + if [[ "$arg" =~ ^(-[?HhVv]|--help|--version)$ ]]; then + exec "$@" + exit + fi + done +} + +# 检测应用相应的配置文件是否存在,如果不存在,则从默认配置文件目录拷贝一份 +# 默认配置文件路径:/etc/${APP_NAME} +# 目标配置文件路径:/srv/conf/${APP_NAME} +# 参数: +# $1 - 基础路径 +# $* - 基础路径下的文件及目录列表,以" "分割 +# 例子: +# ensure_config_file_exist /etc/${APP_NAME} conf.d server.conf +ensure_config_file_exist() { + local -r base_path="${1:?paths is missing}" + 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 + [[ ! -e "$dist" ]] && { + debug "Copying: ${base_path}/${f} => ${dist}" + cp "${base_path}/${f}" "$dist" + rm -f "/srv/conf/${APP_NAME}/.app_init_flag" + } + fi + done +} + +# 根据脚本扩展名及权限,执行相应的初始化脚本 +# 参数: +# $1 - 文件列表,支持路径通配符 +# 使用: +# process_init_files [file [file [...]]] +# 例子: +# process_init_files /src/conf/${APP_NAME}/initdb.d/* +process_init_files() { + local f + while IFS= read -r -d '' f; do + case "$f" in + *.sh) + if [[ -x "$f" ]]; then + info "$0: executing $f" + "$f" + else + info "$0: sourcing $f" + source "$f" + fi + ;; + *) warn "$0: ignoring $f" ;; + esac + done < <(find "$@" -type f -print0 2>/dev/null || true) +} diff --git a/debian/usr/local/lib/libfile.sh b/debian/usr/local/lib/libfile.sh new file mode 100644 index 0000000..908e209 --- /dev/null +++ b/debian/usr/local/lib/libfile.sh @@ -0,0 +1,145 @@ +#!/bin/bash +# Ver: 1.2 by Endial Fang (endial@126.com) +# +# 文件操作函数库 + +# 加载依赖项 +source "/usr/local/lib/libos.sh" + +# 函数列表 + +# 检测"*_FILE"文件,并从文件中读取信息作为参数值;环境变量不允许 VAR 与 VAR_FILE 方式并存 +# 变量: +# $1 - 需要设置的环境变量名称 +# $2 - 该变量对应的默认值(Option) +# +# 使用: file_env ENV_VAR [DEFAULT] +file_env() { + local var="$1" + local fileVar="${var}_FILE" + local def="${2:-}" + if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then + error "Both $var and $fileVar are set (but are exclusive)" + exit 1 + fi + + local val="$def" + if [ "${!var:-}" ]; then + val="${!var}" + elif [ "${!fileVar:-}" ]; then + val="$(< "${!fileVar}")" + fi + + export "$var"="$val" + unset "$fileVar" +} + +# 使用规则表达式在文件中替换数据 +# 参数: +# $1 - 文件名 +# $2 - 正则表达式 +# $3 - 替代数据表达式 +# $4 - 是否使用POSIX表达式. Default: true +replace_in_file() { + local filename="${1:?filename is required}" + local match_regex="${2:?match regex is required}" + local substitute_regex="${3:?substitute regex is required}" + local posix_regex=${4:-true} + + local result + + # 规避使用'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 + result="$(sed "s${del}${match_regex}${del}${substitute_regex}${del}g" "$filename")" + fi + 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 - 文件名 +# $2 - 正则表达式 +# $3 - 是否使用POSIX表达式. Default: true +remove_in_file() { + local filename="${1:?filename is required}" + local match_regex="${2:?match regex is required}" + local posix_regex=${3:-true} + local result + + # 规避使用'sed in-place' 方式操作 + # 1) 部分系统兼容性问题,如从ConfigMap中加载的文件 + # 2) 部分系统不支持"in-place" 方式操作 + if [[ $posix_regex = true ]]; then + result="$(sed -E "/$match_regex/d" "$filename")" + else + result="$(sed "/$match_regex/d" "$filename")" + fi + echo "$result" > "$filename" +} + +# 在符合条件的行后增加文本 +# 参数: +# $1 - 文件名 +# $2 - 正则表达式 +# $3 - 待增加的文本 +append_in_file() { + local file="${1:?missing file}" + local match_regex="${2:?missing pattern}" + local value="${3:?missing value}" + + # We read the file in reverse, replace the first match (0,/pattern/s) and then reverse the results again + 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/debian/usr/local/lib/libfs.sh b/debian/usr/local/lib/libfs.sh new file mode 100644 index 0000000..83c2e44 --- /dev/null +++ b/debian/usr/local/lib/libfs.sh @@ -0,0 +1,170 @@ +#!/bin/bash +# Ver: 1.3 by Endial Fang (endial@126.com) +# +# 文件管理函数库 + +# 加载依赖项 +source "/usr/local/lib/liblog.sh" + +# 函数列表 + +# 确保指定的 文件/路径 所属权为指定的 用户/组 +# 参数: +# $1 - 文件路径 +# $2 - 用户 +# $3 - 用户组 +ensure_owned_by() { + local path="${1:?path is missing}" + local owner="${2:?owner is missing}" + local group="${3:-}" + + 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_user="${2:-}" + local owner_group="${3:-}" + + [ -d "${dir}" ] || mkdir -p "${dir}" + if [[ -n $owner_user ]]; then + ensure_owned_by "$dir" "$owner_user" "$owner_group" + fi +} + +# 检测目录是否存在或为空 +# 参数: +# $1 - 目录路径 +is_dir_empty() { + local dir="${1:?missing directory}" + + # 计算真实路径,避免符号链接问题 + local -r dir="$(realpath "$path")" + if [[ ! -e "$dir" ]] || [[ -z "$(ls -A "$dir")" ]]; then + true + else + false + 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 - 文件或路径 +is_writable() { + local file="${1:?missing file}" + local dir + dir="$(dirname "$file")" + + 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). +# Flags: +# -f|--file-mode - 文件权限模式 +# -d|--dir-mode - 目录权限模式 +# -u|--user - 用户 +# -g|--group - 用户组 +configure_permissions_ownership() { + local -r paths="${1:?paths is missing}" + local dir_mode="" + local file_mode="" + local user="" + local group="" + + # 参数有效性校验 + shift 1 + while [ "$#" -gt 0 ]; do + case "$1" in + -f|--file-mode) + shift + file_mode="${1:?missing mode for files}" + ;; + -d|--dir-mode) + shift + dir_mode="${1:?missing mode for directories}" + ;; + -u|--user) + shift + user="${1:?missing user}" + ;; + -g|--group) + shift + group="${1:?missing group}" + ;; + *) + error "Invalid command line flag $1" >&2 + return 1 + ;; + esac + shift + done + + read -r -a filepaths <<< "$paths" + for p in "${filepaths[@]}"; do + if [[ -e "$p" ]]; then + debug "Check $p" + find -L "$p" -printf "" + if [[ -n ${dir_mode} ]]; then + 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 + 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 + 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 + 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 + debug "Change group to ${group} of files and directories in $p" + find -L "$p" -print0 | xargs -r -0 chgrp "${group}" + fi + else + error "$p does not exist" + fi + done +} diff --git a/debian/usr/local/lib/liblog.sh b/debian/usr/local/lib/liblog.sh new file mode 100644 index 0000000..b55ec2b --- /dev/null +++ b/debian/usr/local/lib/liblog.sh @@ -0,0 +1,97 @@ +#!/bin/bash +# Ver: 1.2 by Endial Fang (endial@126.com) +# +# 日志输出函数库 + +#[[ ${ENV_DEBUG:-false} = true ]] && set -x +MODULE="$(basename "$0")" + +# 定义颜色代码函数 +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 debug_info + if is_debug_mode; then + debug_info="$(get_color_code "cyan")${APP_NAME:-}:${MODULE:-}" + else + 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 - 日志信息 +debug() { + if is_debug_mode; then + log "$(get_color_code "blue")DBG$(get_color_code "reset"): $*" + fi +} + +# 输出提示信息类日志信息 +# 参数: +# $1 - 日志信息 +info() { + log "$(get_color_code "green")INF$(get_color_code "reset"): $*" +} + +# 输出警告类日志信息至sterr +# 参数: +# $1 - 日志信息 +warn() { + log "$(get_color_code "yellow")WRN$(get_color_code "reset"): $*" +} + +# 输出错误类日志信息至sterr,并退出脚本 +# 参数: +# $1 - 日志信息 +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/debian/usr/local/lib/libnet.sh b/debian/usr/local/lib/libnet.sh new file mode 100644 index 0000000..69d674b --- /dev/null +++ b/debian/usr/local/lib/libnet.sh @@ -0,0 +1,155 @@ +#!/bin/bash +# Ver: 1.3 by Endial Fang (endial@126.com) +# +# 文件管理函数库 + +# 加载依赖项 +source "/usr/local/lib/liblog.sh" +source "/usr/local/lib/libos.sh" +source "/usr/local/lib/libvalidations.sh" + +# 函数列表 + +# 域名解析 +# 参数: +# $1 - 需要解析的主机名 +# $2 - IP 地址版本 v4/v6, 为空时解析所有版本 +# 返回值: +# IP地址 +dns_lookup() { + local host="${1:?host is missing}" + local ip_version="${2:-}" + + getent "ahosts${ip_version}" "$host" | awk '/STREAM/ {print $1 }' | head -n 1 +} + +# 尝试解析域名并返回对应的 IP +# 参数: +# $1 - 主机名 +# $2 - 尝试次数 +# $3 - 重试间隔时间(秒) +# 返回值: +# IP地址 +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 + else + true + fi + } + # Wait for the host to be ready + retry_while "check_host ${hostname}" "$retries" "$seconds" + dns_lookup "$hostname" +} + +# 获取当前主机 IP +# 返回值: +# IP地址 +get_machine_ip() { + local -a ip_addresses + local hostname + hostname="$(hostname)" + read -r -a ip_addresses <<< "$(dns_lookup "$hostname" | xargs echo)" + if [[ "${#ip_addresses[@]}" -gt 1 ]]; then + warn "Found more than one IP address associated to hostname ${hostname}: ${ip_addresses[*]}, will use ${ip_addresses[0]}" + elif [[ "${#ip_addresses[@]}" -lt 1 ]]; then + error "Could not find any IP address associated to hostname ${hostname}" + exit 1 + fi + # 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 +} + +# 检测指定的主机名是否可解析 +# 参数: +# $1 - 待检测的主机名 +# 返回值: +# true / false +is_hostname_resolved() { + local -r host="${1:?missing value}" + + if [[ -n "$(dns_lookup "$host")" ]]; then + true + else + false + fi +} + +# 解析 URL +# 参数: +# $1 - URI 字符串 +# $2 - 类型字符串. 有效值 (scheme, authority, userinfo, host, port, path, query or fragment) +# 返回值: +# 字符串 +parse_uri() { + local uri="${1:?uri is missing}" + local component="${2:?component is missing}" + + # Solution based on https://tools.ietf.org/html/rfc3986#appendix-B with + # additional sub-expressions to split authority into userinfo, host and port + # Credits to Patryk Obara (see https://stackoverflow.com/a/45977232/6694969) + local -r URI_REGEX='^(([^:/?#]+):)?(//((([^@/?#]+)@)?([^:/?#]+)(:([0-9]+))?))?(/([^?#]*))?(\?([^#]*))?(#(.*))?' + # || | ||| | | | | | | | | | + # |2 scheme | ||6 userinfo 7 host | 9 port | 11 rpath | 13 query | 15 fragment + # 1 scheme: | |5 userinfo@ 8 :... 10 path 12 ?... 14 #... + # | 4 authority + # 3 //... + local index=0 + case "$component" in + scheme) + index=2 + ;; + authority) + index=4 + ;; + userinfo) + index=6 + ;; + host) + index=7 + ;; + port) + index=9 + ;; + path) + index=10 + ;; + query) + index=13 + ;; + fragment) + index=14 + ;; + *) + stderr_print "unrecognized component $component" + return 1 + ;; + 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/debian/usr/local/lib/libos.sh b/debian/usr/local/lib/libos.sh new file mode 100644 index 0000000..e84c812 --- /dev/null +++ b/debian/usr/local/lib/libos.sh @@ -0,0 +1,511 @@ +#!/bin/bash +# Ver: 1.4 by Endial Fang (endial@126.com) +# +# 操作系统控制函数库 + +# 加载依赖项 +source "/usr/local/lib/liblog.sh" +source "/usr/local/lib/libfs.sh" +source "/usr/local/lib/libvalidations.sh" + +# 函数列表 + +# 检测指定用户账户是否存在 +# 参数: +# $1 - 用户账户 +# 返回值: +# 0 / 1 +is_user_exists() { + local user="${1:?user is missing}" + id "$user" >/dev/null 2>&1 +} + +# 检测指定用户分组是否存在 +# 参数: +# $1 - 用户组 +# 返回值: +# 0 / 1 +is_group_exists() { + local group="${1:?group is missing}" + getent group "$group" >/dev/null 2>&1 +} + +# 检测当前是否为 root 用户 +# 返回值: +# true / false +is_root() { + if [[ "$(id -u)" = "0" ]]; then + debug "Run as root." + true + else + debug "Run as non-root: $(id -u)" + false + fi +} + +# 确保指定用户组在系统中存在 +# 参数: +# $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 + -i | --gid) + shift + gid="${1:?missing gid}" + ;; + -s | --system) + is_system_user=true + ;; + *) + echo "Invalid command line flag $1" >&2 + return 1 + ;; + esac + shift + done + + 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 +} + +# 确保指定用户在系统中存在 +# 参数: +# $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 + + # Validate arguments + shift 1 + while [ "$#" -gt 0 ]; do + case "$1" in + -i | --uid) + shift + uid="${1:?missing uid}" + ;; + -g | --group) + shift + group="${1:?missing group}" + ;; + -a | --append-groups) + shift + append_groups="${1:?missing append_groups}" + ;; + -h | --home) + shift + home="${1:?missing home directory}" + ;; + -s | --system) + is_system_user=true + ;; + *) + echo "Invalid command line flag $1" >&2 + return 1 + ;; + esac + shift + done + + 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 + + if [[ -n "$group" ]]; then + local -a group_args=("$group") + $is_system_user && group_args+=("--system") + ensure_group_exists "${group_args[@]}" + 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 + configure_permissions_ownership "$home" -d "775" -f "664" -u "$user" -g "$group" + fi +} + +# 获取系统可用内存大小(MB)信息 +# 返回值: +# 内存大小(兆字节) +get_total_memory() { + echo $(($(grep MemTotal /proc/meminfo | awk '{print $2}') / 1024)) +} + +# 获取以内存定量方式描述的机器类型 +# 标志位: +# --memory - 内存大小 (MB,可选) +# 返回值: +# 类型名称 +get_machine_size() { + local memory="" + + # 检测标志位 + while [[ "$#" -gt 0 ]]; do + case "$1" in + --memory) + shift + memory="${1:?missing memory}" + ;; + *) + echo "Invalid command line flag $1" >&2 + return 1 + ;; + esac + shift + done + + if [[ -z "$memory" ]]; then + debug "Memory was not specified, detecting available memory automatically" + memory="$(get_total_memory)" + fi + sanitized_memory=$(convert_to_mb "$memory") + if [[ "$sanitized_memory" -gt 26000 ]]; then + echo 2xlarge + elif [[ "$sanitized_memory" -gt 13000 ]]; then + echo xlarge + elif [[ "$sanitized_memory" -gt 6000 ]]; then + echo large + elif [[ "$sanitized_memory" -gt 3000 ]]; then + echo medium + elif [[ "$sanitized_memory" -gt 1500 ]]; then + echo small + else + echo micro + fi +} + +# 获取已定义的所有内存大小描述 +# 返回值: +# 描述值列表 +get_supported_machine_sizes() { + echo micro small medium large xlarge 2xlarge +} + +# 将以字符串表示的内存大小转换为以MB为单位的内存大小值 (i.e. 2G -> 2048) +# 参数: +# $1 - 内存大小 +# 返回值: +# 转换后的数值 +convert_to_mb() { + local amount="${1:-}" + if [[ $amount =~ ^([0-9]+)(m|M|g|G) ]]; then + size="${BASH_REMATCH[1]}" + unit="${BASH_REMATCH[2]}" + if [[ "$unit" = "g" || "$unit" = "G" ]]; then + amount="$((size * 1024))" + else + amount="$size" + fi + fi + echo "$amount" +} + +# 如果禁用调试模式,将输出信息重定向至 /dev/null +# 参数: +# $@ - 待执行的命令 +debug_execute() { + if is_boolean_yes "${ENV_DEBUG:-false}"; then + "$@" + else + "$@" >/dev/null 2>&1 + fi +} + +# 重试执行命令 +# 参数: +# $1 - 命令 (字符串) +# $2 - 最大尝试次数. 默认值: 12 +# $3 - 重试前等待时间(秒). 默认值: 5 +# 返回值: +# 0 / 1 +retry_while() { + 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 + "${command[@]}" && return_value=0 && break + sleep "$sleep_time" + done + return $return_value +} + +# 生成随机字符串 +# 标志位: +# -t|--type - 字符串类型 (ascii, alphanumeric, numeric). 默认值: ascii +# -c|--count - 字符串长度. 默认值: 32 +# 返回值: +# 字符串 +generate_random_string() { + local type="ascii" + local count="32" + local filter + local result + + # 检测标志位 + 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 + ;; + esac + shift + done + + # 检测类型 + case "$type" in + 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 + # Therefore, the higher amount of lines read, this will happen less frequently + result="$(head -n "$((count + 10))" /dev/urandom | tr -dc "$filter" | head -c "$count")" + echo "$result" +} + +# 为指定字符串生成 MD5 值 +# 参数: +# $1 - 字符串 +# 返回值: +# 字符串对应的 MD5 +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/debian/usr/local/lib/libservice.sh b/debian/usr/local/lib/libservice.sh new file mode 100644 index 0000000..a28a8ba --- /dev/null +++ b/debian/usr/local/lib/libservice.sh @@ -0,0 +1,449 @@ +#!/bin/bash +# Ver: 1.2 by Endial Fang (endial@126.com) +# +# 服务管理函数库 + +# shellcheck disable=SC1091 + +# 加载依赖项 +source "/usr/local/lib/liblog.sh" +source "/usr/local/lib/libvalidations.sh" + +# 函数列表 + +# 获取并返回服务 PID +# 参数: +# $1 - PID 文件 +# 返回值: +# PID +get_pid_from_file() { + local pid_file="${1:?pid file is missing}" + + if [[ -f "$pid_file" ]]; then + if [[ -n "$(< "$pid_file")" ]] && [[ "$(< "$pid_file")" -gt 0 ]]; then + echo "$(< "$pid_file")" + fi + fi +} + +# 检测 PID 对应的服务是否在运行中 +# 参数: +# $1 - PID +# 返回值: +# 0 / 1 +is_service_running() { + local pid="${1:?pid is missing}" + + kill -0 "$pid" 2>/dev/null +} + +# 通过发送信号停止一个指定 PID 的服务 +# 参数: +# $1 - PID 文件 +# $2 - 信号 (可选) +stop_service_using_pid() { + local pid_file="${1:?pid file is missing}" + local signal="${2:-}" + local pid + + pid="$(get_pid_from_file "$pid_file")" + [[ -z "$pid" ]] || ! is_service_running "$pid" && return + + if [[ -n "$signal" ]]; then + kill "-${signal}" "$pid" + else + kill "$pid" + fi + + local counter=10 + while [[ "$counter" -ne 0 ]] && is_service_running "$pid"; do + sleep 1 + counter=$((counter - 1)) + done +} + +# 启动一个 cron 守护进程 +# 返回值: +# true / false +cron_start() { + if [[ -x "/usr/sbin/cron" ]]; then + /usr/sbin/cron + elif [[ -x "/usr/sbin/crond" ]]; then + /usr/sbin/crond + else + false + fi +} + +# 为指定的服务生成 cron 配置文件 +# 参数: +# $1 - 服务名称 +# $2 - 命令 +# 标志位: +# --run-as - 运行的用户. 默认值: root +# --schedule - Cron 周期配置. 默认值: * * * * * +generate_cron_conf() { + local service_name="${1:?service name is missing}" + local cmd="${2:?command is missing}" + local run_as="root" + local schedule="* * * * *" + local clean="true" + + # 检测标志位 + shift 2 + while [[ "$#" -gt 0 ]]; do + case "$1" in + --run-as) + shift + run_as="$1" + ;; + --schedule) + shift + schedule="$1" + ;; + --no-clean) + clean="false" + ;; + *) + echo "Invalid command line flag ${1}" >&2 + return 1 + ;; + esac + shift + done + + mkdir -p /etc/cron.d + if "$clean"; then + 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 - 服务名 +# $2 - PID 文件 +# $3 - 启动命令 +# $4 - 停止命令 +# 标志位: +# --disabled - 是否禁用. 默认值: no +generate_monit_conf() { + local service_name="${1:?service name is missing}" + local pid_file="${2:?pid file is missing}" + local start_command="${3:?start command is missing}" + local stop_command="${4:?stop command is missing}" + local monit_conf_dir="/etc/monit/conf.d" + local disabled="no" + + # 检测标志位 + shift 4 + while [[ "$#" -gt 0 ]]; do + case "$1" in + --disable) + disabled="yes" + ;; + *) + echo "Invalid command line flag ${1}" >&2 + return 1 + ;; + esac + shift + done + + is_boolean_yes "$disabled" && conf_suffix=".disabled" + mkdir -p "$monit_conf_dir" + cat > "${monit_conf_dir}/${service_name}.conf${conf_suffix:-}" <&2 + return 1 + ;; + esac + shift + done + + mkdir -p "$logrotate_conf_dir" + cat < "${logrotate_conf_dir}/${service_name}" +${log_path} { + ${period} + rotate ${rotations} + dateext + compress + copytruncate + missingok +$(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 + false + fi +} + +# 检测数据是否为布尔值 '1' 或字符串 'yes/true' +# 参数: +# $1 - 待检测的数据 +# 返回值: +# true / false +is_boolean_yes() { + local -r bool="${1:-}" + + # 忽略字母大小写 + shopt -s nocasematch + if [[ "$bool" = 1 || "$bool" =~ ^(yes|true)$ ]]; then + true + else + false + fi +} + +# 检测数据是否为字符串 'yes/no' +# 参数: +# $1 - 待检测的数据 +# 返回值: +# true / false +is_yes_no_value() { + local -r bool="${1:-}" + + if [[ "$bool" =~ ^(yes|no)$ ]]; then + true + else + false + fi +} + +# 检测数据是否为字符串 'true/false' +# 参数: +# $1 - 待检测的数据 +# 返回值: +# true / false +is_true_false_value() { + local -r bool="${1:-}" + if [[ "$bool" =~ ^(true|false)$ ]]; then + true + else + false + 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 - 待检测的数据 +# 返回值: +# true / false +is_empty_value() { + local -r val="${1:-}" + + if [[ -z "$val" ]]; then + true + else + false + fi +} + +# 检测数据是否为有效的端口号 +# 标志位: +# -unprivileged - 没有特权的端口 +# 参数: +# $1 - 待检测的数据 +# 返回值: +# true / false 或 错误消息 +validate_port() { + local value + local unprivileged=0 + + # Parse flags + while [[ "$#" -gt 0 ]]; do + case "$1" in + -unprivileged) + unprivileged=1 + ;; + --) + shift + break + ;; + -*) + error "unrecognized flag $1" + return 1 + ;; + *) + break + ;; + esac + shift + done + + if [[ "$#" -gt 1 ]]; then + error "too many arguments provided" + return 2 + elif [[ "$#" -eq 0 ]]; then + error "missing port argument" + return 1 + else + value=$1 + fi + + if [[ -z "$value" ]]; then + error "the value is empty" + return 1 + else + if ! is_int "$value"; then + warn "value is not an integer" + return 2 + elif [[ "$value" -lt 0 ]]; then + warn "negative value provided" + return 2 + elif [[ "$value" -gt 65535 ]]; then + warn "requested port is greater than 65535" + return 2 + elif [[ "$unprivileged" = 1 && "$value" -lt 1024 ]]; then + 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 - 待检测的数据 +# 返回值: +# 0 / 1 +validate_ipv4() { + local ip="${1:?ip is missing}" + 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")" + [[ ${ip_array[0]} -le 255 && ${ip_array[1]} -le 255 \ + && ${ip_array[2]} -le 255 && ${ip_array[3]} -le 255 ]] + stat=$? + fi + 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 - 最小长度 +# -max-length - 最大长度 +# 参数: +# $1 - 待检测的数据 +# 返回值: +# 0 / 1 +validate_string() { + local string + local min_length=-1 + local max_length=-1 + + # Parse flags + while [ "$#" -gt 0 ]; do + case "$1" in + -min-length) + shift + min_length=${1:-} + ;; + -max-length) + shift + max_length=${1:-} + ;; + --) + shift + break + ;; + -*) + error "unrecognized flag $1" + return 1 + ;; + *) + string="$1" + ;; + esac + shift + done + + if [[ "$min_length" -ge 0 ]] && [[ "${#string}" -lt "$min_length" ]]; then + info "string length is less than $min_length" + return 1 + fi + if [[ "$max_length" -ge 0 ]] && [[ "${#string}" -gt "$max_length" ]]; then + info "string length is great than $max_length" + return 1 + fi +} diff --git a/debian/usr/local/lib/libversion.sh b/debian/usr/local/lib/libversion.sh new file mode 100644 index 0000000..190d991 --- /dev/null +++ b/debian/usr/local/lib/libversion.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# Ver: 1.0 by Endial Fang (endial@126.com) +# +# 版本管理函数库 + +# 加载依赖项 +source "/usr/local/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 +} diff --git a/debian/usr/local/sbin/download_pkg b/debian/usr/local/sbin/download_pkg new file mode 100755 index 0000000..ac31dce --- /dev/null +++ b/debian/usr/local/sbin/download_pkg @@ -0,0 +1,183 @@ +#!/bin/bash +# Ver: 1.2 by Endial Fang (endial@126.com) +# +# shell 执行参数,分别为 -e(命令执行错误则退出脚本) -u(变量未定义则报错) -x(打印实际待执行的命令行) +set -eu + +# 定义错误处理函数,添加错误位置信息 +error() { + local error_location="$1" + local error_message="$2" + echo "Error at $error_location: $error_message" >&2 + exit 1 +} + +print_usage() { + echo "Usage: download_pkg \"\" [OPTIONS]" + echo "" + echo "Download and install Third-Part packages" + echo "" + echo "Commands:" + echo " download Download a package." + echo " install Download and install a package." + echo " unpack Download and unpack a package." + echo "" + echo "Options:" + echo " -g, --checkpgp Package release bucket." + echo " -s, --checksum SHA256 verification checksum." + echo " -h, --help Show this help message and exit." + echo "" + echo "PACKAGE-NAME: Name with extern name" + echo "URLS: String with URL list" + echo "" + echo "Examples:" + echo " - Unpack package" + echo " \$ download_pkg unpack redis-5.0.8.tar.gz \"http://download.redis.io/releases\"" + echo "" + echo " - Verify and Install package" + echo " \$ download_pkg install redis-5.0.8.tar.gz \"http://download.redis.io/releases\" -s 42cf86a114d2a451b898fcda96acd4d01062a7dbaaad2801d9164a36f898f596" + echo "" +} + +check_pgp() { + local name_asc=${1:?missing asc file name} + local name=${2:?missing file name} + local keys="${3:?missing key id}" + + GNUPGHOME="$(mktemp -d)" + if which gpg >/dev/null 2>&1; then + local key_servers=("pgp.mit.edu" "keys.gnupg.net" "keyserver.pgp.com" "ha.pool.sks-keyservers.net") + for key in $keys; do + for server in "${key_servers[@]}"; do + if gpg --batch --keyserver "$server" --recv-keys "${key}" --timeout 10; then + break + fi + done + done + gpg --batch --verify "$name_asc" "$name" || error "PGP verification" "PGP verification failed" + command -v gpgconf > /dev/null && gpgconf --kill all + fi +} + +# 获取并解析参数 +parsed_args=$(getopt -o g:s:h -l "checkpgp:,checksum:,help" -n "download-pkg" -- "$@") +if [ $? -ne 0 ]; then + error "Parameter parsing" "Failed to parse command line arguments." + print_usage + exit 1 +fi + +eval set -- "$parsed_args"; +while true; do + case "$1" in + -g|--checkpgp) + shift + if [ -n "$1" ]; then + package_keys=$1 + shift + fi + ;; + -s|--checksum) + shift + if [ -n "$1" ]; then + package_sha256=$1 + shift + fi + ;; + -h|--help) + print_usage + exit 0 + ;; + --) + shift + break + ;; + esac +done + +# 检测输入的命令是否合法 +case "$1" in + download|install|unpack) ;; + *) + error "Command validation" "Unrecognized command: $1" + print_usage + exit 1 + ;; +esac + +# 检测输入参数是否足够,需要至少提供软件包名称 及 下载路径 +if [ $# -lt 3 ]; then + error "Parameter validation" "Insufficient parameters. Please provide package name and URLs." + print_usage + exit 1 +fi + +install_root=/usr/local +cache_root=/tmp + +package="$2" +package_urls=$3 + +# 检查缓存目录中是否已存在该软件包 +if [ -e "$cache_root/$package" ]; then + echo "Package already exists in cache: $cache_root/$package" +else + cd $install_root + echo "Downloading $package package..." + for url in $package_urls; do + echo "Try $url/$package" + if wget -T 30 -O "$cache_root/$package" "$url/$package" && [ -s "$cache_root/$package" ]; then + if [ -n "${package_keys:-}" ]; then + wget -T 10 -O "$cache_root/$package.asc" "$url/$package.asc" || wget -T 10 -O "$cache_root/$package.asc" "$url/$package.sign" || : + if [ ! -e "$cache_root/$package.asc" ]; then + error "PGP signature download" "Failed to download PGP signature file." + exit 1 + fi + fi + break + else + echo "Failed to download from $url/$package" + fi + done +fi + +if [ -n "${package_sha256:-}" ]; then + echo "Verifying package integrity" + if ! echo "$package_sha256 *$cache_root/$package" | sha256sum -c -; then + error "SHA256 verification" "SHA256 verification failed" + fi +fi + +if [ -e "$cache_root/$package.asc" ]; then + echo "Verifying package with PGP" + check_pgp "$cache_root/$package.asc" "$cache_root/$package" "$package_keys" +fi + +# If the tarball has too many files, it can trigger a bug +# in overlayfs when using tar. Install bsdtar in the container image +# to workaround it. As the overhead is too big (~40 MB), it is not added by +# default. Source: https://github.com/coreos/bugs/issues/1095 + +# 安装或解压软件 +case "$1" in + download) + echo "Download success: $cache_root/$package" + ;; + install) + echo "Installing $package" + cp $cache_root/$package /usr/local/sbin/ + ;; + unpack) + if ! tar -taf $cache_root/$package >/dev/null 2>&1; then + error "Package integrity check" "Invalid or corrupt '$package' package." + exit 1 + fi + echo "Unpacking $package to $cache_root" + cd $cache_root + if which bsdtar >/dev/null 2>&1; then + bsdtar -xf $cache_root/$package + else + tar --no-same-owner -xaf $cache_root/$package + fi + ;; +esac diff --git a/debian/usr/local/sbin/install_pkg b/debian/usr/local/sbin/install_pkg new file mode 100755 index 0000000..ca637fc --- /dev/null +++ b/debian/usr/local/sbin/install_pkg @@ -0,0 +1,70 @@ +#!/bin/bash +# Ver: 1.2 by Endial Fang (endial@126.com) +# +# shell 执行参数,分别为 -e(命令执行错误则退出脚本) -u(变量未定义则报错) -x(打印实际待执行的命令行) +set -eu + +# 检查用户权限 +if [ "$EUID" -ne 0 ]; then + echo "Error: This script must be run as root." + exit 1 +fi + +print_usage() { + echo "Usage: install_pkg " + echo "" + echo "Download and install packages" + echo "" + echo "Options:" + echo " -h, --help Show this help message and exit." + echo "" + echo "Examples:" + echo " - Install bash & curl" + echo " \$ install_pkg bash curl" + echo "" +} + +if [ $# -lt 1 ]; then + print_usage + exit 1 +fi + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) + print_usage + exit 0 + ;; + *) + break + ;; + esac +done + +retry=0 +max=2 +export DEBIAN_FRONTEND=noninteractive && + +until [ $retry -gt $max ]; do + set +e + ( + echo "Update and install packages..." && + apt-get update && + apt-get install -y --no-install-recommends $* + ) + CODE=$? + set -e + if [ $CODE -eq 0 ]; then + break + fi + if [ $retry -eq $max ]; then + echo "Failed to install packages after $max retries." + exit $CODE + fi + echo "apt failed, retrying" + retry=$(($retry + 1)) +done + +apt-get clean || : +rm -rf /var/lib/apt/lists/* /var/cache/apt/archives || : +echo "Installation completed successfully." diff --git a/debian/usr/local/sbin/select_source b/debian/usr/local/sbin/select_source new file mode 100755 index 0000000..7389789 --- /dev/null +++ b/debian/usr/local/sbin/select_source @@ -0,0 +1,31 @@ +#!/bin/bash +# Ver: 1.2 by Endial Fang (endial@126.com) +# +# 此脚本用于根据传入的参数选择对应的 apt 源配置文件并复制到指定目录 +# shell 执行参数,分别为 -e(命令执行错误则退出脚本) -u(变量未定义则报错) -x(打印实际待执行的命令行) +set -eu + +# 检查是否有足够的权限 +if [ "$EUID" -ne 0 ]; then + echo "Error: This script must be run as root." + exit 1 +fi + +# 获取用户传入的参数,若未传入则使用默认值 "default" +source_name=${1:-default} + +# 定义源文件路径 +source_file="/etc/apt/sources/${source_name}.sources" + +# 检查源文件是否存在 +if [ ! -f "$source_file" ]; then + echo "Error: Source file $source_file does not exist." + exit 1 +fi + +# 定义目标文件路径 +target_file="/etc/apt/sources.list.d/debian.sources" + +# 复制源文件到目标文件 +cp "$source_file" "$target_file" +echo "Successfully selected apt source: $source_name"