545 lines
13 KiB
Bash
Executable File
545 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Default config for:
|
|
# curl -fsSL https://punchsky.com/install | bash
|
|
#
|
|
# Fill these values before publishing this script at https://punchsky.com/install.
|
|
WORK_DIR="${WORK_DIR:-/opt/app}"
|
|
CURRENT_USER="${SUDO_USER:-$(id -un)}"
|
|
SERVICE_USER="${SERVICE_USER:-$CURRENT_USER}"
|
|
SERVICE_GROUP="${SERVICE_GROUP:-$(id -gn "$SERVICE_USER")}"
|
|
|
|
# Configure programs here before publishing this script.
|
|
# Format:
|
|
# INSTALL_<APP>=(
|
|
# "service_name"
|
|
# "download_url"
|
|
# "start_command"
|
|
# "stop_command"
|
|
# "service_user"
|
|
# "service_group"
|
|
# )
|
|
#
|
|
# IOT_ENV and EFKA_ENV are rendered into the corresponding systemd service
|
|
# file as Environment= lines, using the [prod] sections from iot/env.file and
|
|
# efka/env.file. Directory creation is driven by those service env values.
|
|
# IOT_DIR_ENV_KEYS and EFKA_DIR_ENV_KEYS list env keys whose values are
|
|
# directories that must be created if they do not exist.
|
|
#
|
|
# Use {install_dir}, {work_dir} and {service_name} in commands and directories.
|
|
# service_user and service_group are optional.
|
|
|
|
INSTALL_IOT=(
|
|
"iot"
|
|
"https://punchsky.cn/down/iot-0.1.0.tar.gz"
|
|
"{install_dir}/bin/iot foreground"
|
|
"{install_dir}/bin/iot stop"
|
|
""
|
|
""
|
|
)
|
|
|
|
IOT_ENV=(
|
|
"IOT_API_URL=http://127.0.0.1/api/v1"
|
|
"ENDPOINT_ROOT_DIR=/var/lib/endpoint/database/"
|
|
"ENDPOINT_LOG_PATH=/var/lib/endpoint/endpoint_log"
|
|
"IOT_MNESIA_DIR=/var/lib/iot/mnesia/"
|
|
)
|
|
|
|
IOT_DIR_ENV_KEYS=(
|
|
"ENDPOINT_ROOT_DIR"
|
|
"ENDPOINT_LOG_PATH"
|
|
"IOT_MNESIA_DIR"
|
|
)
|
|
|
|
INSTALL_EFKA=(
|
|
"efka"
|
|
"https://punchsky.cn/down/efka-0.1.0.tar.gz"
|
|
"{install_dir}/bin/efka foreground"
|
|
"{install_dir}/bin/efka stop"
|
|
""
|
|
""
|
|
)
|
|
|
|
EFKA_ENV=(
|
|
"EFKA_DETS_DIR=/var/lib/efka/dets/"
|
|
"EFKA_IOT_HOST=localhost"
|
|
"EFKA_AUTH_UUID=qbxmjyzrkpntfgswaevodhluicqzxplkm"
|
|
"EFKA_AUTH_TOKEN=zpxlkvmqwnbghytrujsdieofazxcvbnm"
|
|
"EFKA_DOCKER_ROOT_DIR=/var/lib/efka/docker/"
|
|
"EFKA_MNESIA_DIR=/var/lib/efka/mnesia"
|
|
)
|
|
|
|
EFKA_DIR_ENV_KEYS=(
|
|
"EFKA_DETS_DIR"
|
|
"EFKA_DOCKER_ROOT_DIR"
|
|
"EFKA_MNESIA_DIR"
|
|
)
|
|
|
|
TEMP_FILES=()
|
|
INSTALLED_SERVICES=()
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage:
|
|
curl -fsSL https://punchsky.com/install | bash
|
|
|
|
curl -fsSL https://punchsky.com/install | bash -s -- \
|
|
-w <work_dir>
|
|
|
|
Config:
|
|
Configure programs inside this script before publishing it.
|
|
|
|
Options:
|
|
-w, --work-dir Working directory used for installation.
|
|
--user Default service user. Default: current user.
|
|
--group Default service group. Default: current user's primary group.
|
|
-h, --help Show this help.
|
|
|
|
Array config inside script:
|
|
INSTALL_IOT
|
|
INSTALL_EFKA
|
|
IOT_ENV
|
|
EFKA_ENV
|
|
IOT_DIR_ENV_KEYS
|
|
EFKA_DIR_ENV_KEYS
|
|
|
|
Package format:
|
|
INSTALL_<APP>=(
|
|
"service_name"
|
|
"download_url"
|
|
"start_command"
|
|
"stop_command"
|
|
"service_user"
|
|
"service_group"
|
|
)
|
|
|
|
Placeholders:
|
|
{install_dir} will be replaced with the extracted package directory.
|
|
{work_dir} will be replaced with the root work directory.
|
|
{service_name} will be replaced with the service name.
|
|
|
|
Example:
|
|
curl -fsSL https://punchsky.com/install | bash
|
|
EOF
|
|
}
|
|
|
|
log() {
|
|
printf '[install] %s\n' "$*"
|
|
}
|
|
|
|
cleanup() {
|
|
local file
|
|
|
|
set +u
|
|
for file in "${TEMP_FILES[@]}"; do
|
|
[[ -n "$file" ]] && rm -f "$file"
|
|
done
|
|
set -u
|
|
}
|
|
|
|
fail() {
|
|
printf '[install][error] %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
require_command() {
|
|
command -v "$1" >/dev/null 2>&1 || fail "Missing required command: $1"
|
|
}
|
|
|
|
require_option_value() {
|
|
local option="$1"
|
|
local value="${2:-}"
|
|
|
|
[[ -n "$value" ]] || fail "$option requires a value"
|
|
}
|
|
|
|
require_root_runner() {
|
|
if [[ "${EUID}" -ne 0 ]]; then
|
|
require_command sudo
|
|
fi
|
|
}
|
|
|
|
run_as_root() {
|
|
if [[ "${EUID}" -eq 0 ]]; then
|
|
"$@"
|
|
else
|
|
sudo "$@"
|
|
fi
|
|
}
|
|
|
|
strip_archive_suffix() {
|
|
local filename="$1"
|
|
|
|
case "$filename" in
|
|
*.tar.gz) printf '%s\n' "${filename%.tar.gz}" ;;
|
|
*.tgz) printf '%s\n' "${filename%.tgz}" ;;
|
|
*) fail "Unsupported archive suffix: $filename. Expected .tar.gz or .tgz" ;;
|
|
esac
|
|
}
|
|
|
|
parse_args() {
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-w|--work-dir)
|
|
require_option_value "$1" "${2:-}"
|
|
WORK_DIR="${2:-}"
|
|
shift 2
|
|
;;
|
|
--user)
|
|
require_option_value "$1" "${2:-}"
|
|
SERVICE_USER="${2:-}"
|
|
shift 2
|
|
;;
|
|
--group)
|
|
require_option_value "$1" "${2:-}"
|
|
SERVICE_GROUP="${2:-}"
|
|
shift 2
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
fail "Unknown argument: $1"
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
validate_args() {
|
|
[[ -n "$WORK_DIR" ]] || fail "--work-dir is required"
|
|
[[ "$WORK_DIR" = /* ]] || fail "--work-dir must be an absolute path"
|
|
|
|
require_install_config INSTALL_IOT
|
|
require_install_config INSTALL_EFKA
|
|
}
|
|
|
|
prepare_work_dir() {
|
|
log "Creating root work directory: $WORK_DIR"
|
|
run_as_root install -d -m 0755 "$WORK_DIR"
|
|
}
|
|
|
|
render_service_file() {
|
|
local service_name="$1"
|
|
local service_user="$2"
|
|
local service_group="$3"
|
|
local install_dir="$4"
|
|
local start_command="$5"
|
|
local stop_command="$6"
|
|
local env_line
|
|
|
|
cat <<EOF
|
|
[Unit]
|
|
Description=${service_name} service
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=${service_user}
|
|
Group=${service_group}
|
|
WorkingDirectory=${install_dir}
|
|
EOF
|
|
|
|
while IFS= read -r env_line; do
|
|
[[ -n "$env_line" ]] || continue
|
|
validate_environment_line "$env_line"
|
|
printf 'Environment="%s"\n' "$env_line"
|
|
done < <(service_env_lines "$service_name")
|
|
|
|
printf 'ExecStart=%s\n' "$start_command"
|
|
|
|
if [[ -n "$stop_command" ]]; then
|
|
printf 'ExecStop=%s\n' "$stop_command"
|
|
fi
|
|
|
|
cat <<'EOF'
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
}
|
|
|
|
write_service_file() {
|
|
local service_file="$1"
|
|
local service_name="$2"
|
|
local service_user="$3"
|
|
local service_group="$4"
|
|
local install_dir="$5"
|
|
local start_command="$6"
|
|
local stop_command="$7"
|
|
|
|
if [[ -f "$service_file" ]]; then
|
|
local backup_file="${service_file}.bak.$(date +%Y%m%d%H%M%S)"
|
|
log "Backing up existing service file: $backup_file"
|
|
run_as_root cp "$service_file" "$backup_file"
|
|
fi
|
|
|
|
log "Writing systemd service file: $service_file"
|
|
render_service_file "$service_name" "$service_user" "$service_group" "$install_dir" "$start_command" "$stop_command" | run_as_root tee "$service_file" >/dev/null
|
|
}
|
|
|
|
validate_install_item() {
|
|
local service_name="$1"
|
|
local download_url="$2"
|
|
local start_command="$3"
|
|
|
|
[[ -n "$service_name" ]] || fail "Invalid package config: service name is empty"
|
|
[[ -n "$download_url" ]] || fail "Invalid package config for $service_name: download URL is empty"
|
|
[[ -n "$start_command" ]] || fail "Invalid package config for $service_name: start command is empty"
|
|
[[ "$service_name" =~ ^[a-zA-Z0-9_.@-]+$ ]] || fail "Invalid service name: $service_name"
|
|
}
|
|
|
|
require_install_config() {
|
|
local config_name="$1"
|
|
local config_count
|
|
local service_name
|
|
local download_url
|
|
local start_command
|
|
|
|
eval "config_count=\${#${config_name}[@]}"
|
|
eval "service_name=\"\${${config_name}[0]:-}\""
|
|
eval "download_url=\"\${${config_name}[1]:-}\""
|
|
eval "start_command=\"\${${config_name}[2]:-}\""
|
|
|
|
[[ "$config_count" -ge 4 ]] || fail "Invalid $config_name config. Expected service name, URL and commands"
|
|
[[ -n "$service_name" ]] || fail "Invalid $config_name config: service name is empty"
|
|
[[ -n "$download_url" ]] || fail "Invalid $config_name config: download URL is empty"
|
|
[[ -n "$start_command" ]] || fail "Invalid $config_name config: start command is empty"
|
|
}
|
|
|
|
replace_placeholders() {
|
|
local value="$1"
|
|
local install_dir="$2"
|
|
local service_name="$3"
|
|
|
|
value="${value//\{install_dir\}/$install_dir}"
|
|
value="${value//\{work_dir\}/$WORK_DIR}"
|
|
value="${value//\{service_name\}/$service_name}"
|
|
|
|
printf '%s\n' "$value"
|
|
}
|
|
|
|
validate_environment_line() {
|
|
local env_line="$1"
|
|
local env_key="${env_line%%=*}"
|
|
|
|
[[ "$env_line" == *=* ]] || fail "Invalid environment line: $env_line"
|
|
[[ "$env_key" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]] || fail "Invalid environment key: $env_key"
|
|
}
|
|
|
|
service_env_lines() {
|
|
local service_name="$1"
|
|
local env_array_name=""
|
|
local env_count
|
|
local index
|
|
local env_line
|
|
|
|
case "$service_name" in
|
|
iot)
|
|
env_array_name="IOT_ENV"
|
|
;;
|
|
efka)
|
|
env_array_name="EFKA_ENV"
|
|
;;
|
|
*)
|
|
return 0
|
|
;;
|
|
esac
|
|
|
|
eval "env_count=\${#${env_array_name}[@]}"
|
|
for ((index=0; index<env_count; index++)); do
|
|
eval "env_line=\"\${${env_array_name}[$index]}\""
|
|
printf '%s\n' "$env_line"
|
|
done
|
|
}
|
|
|
|
service_dir_env_keys() {
|
|
local service_name="$1"
|
|
local dir_keys_array_name=""
|
|
local dir_keys_count
|
|
local index
|
|
local env_key
|
|
|
|
case "$service_name" in
|
|
iot)
|
|
dir_keys_array_name="IOT_DIR_ENV_KEYS"
|
|
;;
|
|
efka)
|
|
dir_keys_array_name="EFKA_DIR_ENV_KEYS"
|
|
;;
|
|
*)
|
|
return 0
|
|
;;
|
|
esac
|
|
|
|
eval "dir_keys_count=\${#${dir_keys_array_name}[@]}"
|
|
for ((index=0; index<dir_keys_count; index++)); do
|
|
eval "env_key=\"\${${dir_keys_array_name}[$index]}\""
|
|
printf '%s\n' "$env_key"
|
|
done
|
|
}
|
|
|
|
env_value_for_key() {
|
|
local service_name="$1"
|
|
local env_key="$2"
|
|
local env_line
|
|
local value
|
|
|
|
while IFS= read -r env_line; do
|
|
[[ "$env_line" == "${env_key}="* ]] || continue
|
|
value="${env_line#*=}"
|
|
printf '%s\n' "$value"
|
|
return 0
|
|
done < <(service_env_lines "$service_name")
|
|
|
|
fail "Missing env key $env_key for $service_name"
|
|
}
|
|
|
|
create_dependency_dirs_from_env() {
|
|
local service_name="$1"
|
|
local install_dir="$2"
|
|
local service_user="$3"
|
|
local service_group="$4"
|
|
local env_key
|
|
local dependency_dir
|
|
|
|
while IFS= read -r env_key; do
|
|
[[ -n "$env_key" ]] || continue
|
|
dependency_dir="$(env_value_for_key "$service_name" "$env_key")"
|
|
dependency_dir="$(replace_placeholders "$dependency_dir" "$install_dir" "$service_name")"
|
|
|
|
[[ "$dependency_dir" = /* ]] || fail "Dependency directory must be an absolute path: $dependency_dir"
|
|
|
|
log "Creating dependency directory from $env_key: $dependency_dir"
|
|
run_as_root install -d -m 0755 "$dependency_dir"
|
|
run_as_root chown -R "${service_user}:${service_group}" "$dependency_dir"
|
|
done < <(service_dir_env_keys "$service_name")
|
|
}
|
|
|
|
install_program() {
|
|
local service_name="$1"
|
|
local download_url="$2"
|
|
local start_command="$3"
|
|
local stop_command="$4"
|
|
local service_user="$5"
|
|
local service_group="$6"
|
|
local clean_url="$download_url"
|
|
local archive_name
|
|
|
|
validate_install_item "$service_name" "$download_url" "$start_command"
|
|
|
|
clean_url="${clean_url%%\?*}"
|
|
clean_url="${clean_url%%#*}"
|
|
archive_name="$(basename "$clean_url")"
|
|
[[ -n "$archive_name" ]] || fail "Cannot parse archive filename from URL: $download_url"
|
|
|
|
local app_dir_name
|
|
app_dir_name="$(strip_archive_suffix "$archive_name")"
|
|
|
|
local archive_path="${WORK_DIR}/${archive_name}"
|
|
local install_dir="${WORK_DIR}/${app_dir_name}"
|
|
local service_file="/etc/systemd/system/${service_name}.service"
|
|
local temp_archive
|
|
temp_archive="$(mktemp)"
|
|
TEMP_FILES+=("$temp_archive")
|
|
|
|
service_user="${service_user:-$SERVICE_USER}"
|
|
service_group="${service_group:-$SERVICE_GROUP}"
|
|
start_command="$(replace_placeholders "$start_command" "$install_dir" "$service_name")"
|
|
stop_command="$(replace_placeholders "$stop_command" "$install_dir" "$service_name")"
|
|
|
|
log "Downloading $download_url"
|
|
wget --tries=3 --timeout=20 -O "$temp_archive" "$download_url"
|
|
run_as_root install -m 0644 "$temp_archive" "$archive_path"
|
|
|
|
log "Creating install directory: $install_dir"
|
|
run_as_root install -d -m 0755 "$install_dir"
|
|
|
|
log "Extracting archive to $install_dir"
|
|
run_as_root tar -zxvf "$archive_path" -C "$install_dir"
|
|
|
|
log "Setting owner for install directory: ${service_user}:${service_group}"
|
|
run_as_root chown -R "${service_user}:${service_group}" "$install_dir"
|
|
|
|
create_dependency_dirs_from_env "$service_name" "$install_dir" "$service_user" "$service_group"
|
|
|
|
write_service_file "$service_file" "$service_name" "$service_user" "$service_group" "$install_dir" "$start_command" "$stop_command"
|
|
INSTALLED_SERVICES+=("$service_name")
|
|
}
|
|
|
|
install_program_from_config() {
|
|
local config_name="$1"
|
|
local service_name
|
|
local download_url
|
|
local start_command
|
|
local stop_command
|
|
local service_user
|
|
local service_group
|
|
|
|
eval "service_name=\"\${${config_name}[0]:-}\""
|
|
eval "download_url=\"\${${config_name}[1]:-}\""
|
|
eval "start_command=\"\${${config_name}[2]:-}\""
|
|
eval "stop_command=\"\${${config_name}[3]:-}\""
|
|
eval "service_user=\"\${${config_name}[4]:-}\""
|
|
eval "service_group=\"\${${config_name}[5]:-}\""
|
|
|
|
install_program \
|
|
"$service_name" \
|
|
"$download_url" \
|
|
"$start_command" \
|
|
"$stop_command" \
|
|
"$service_user" \
|
|
"$service_group"
|
|
}
|
|
|
|
install_programs() {
|
|
install_program_from_config INSTALL_IOT
|
|
install_program_from_config INSTALL_EFKA
|
|
}
|
|
|
|
reload_systemd() {
|
|
log "Reloading systemd"
|
|
run_as_root systemctl daemon-reload
|
|
}
|
|
|
|
enable_and_start_installed_services() {
|
|
local service_name
|
|
|
|
set +u
|
|
for service_name in "${INSTALLED_SERVICES[@]}"; do
|
|
set -u
|
|
log "Enabling service on boot: ${service_name}.service"
|
|
run_as_root systemctl enable "${service_name}.service"
|
|
|
|
log "Starting service: ${service_name}.service"
|
|
run_as_root systemctl start "${service_name}.service"
|
|
set +u
|
|
done
|
|
set -u
|
|
}
|
|
|
|
main() {
|
|
trap cleanup EXIT
|
|
|
|
parse_args "$@"
|
|
validate_args
|
|
|
|
require_command wget
|
|
require_root_runner
|
|
require_command tar
|
|
require_command systemctl
|
|
|
|
prepare_work_dir
|
|
install_programs
|
|
reload_systemd
|
|
enable_and_start_installed_services
|
|
|
|
log "Installation completed"
|
|
}
|
|
|
|
main "$@"
|